5 min read

A touchy subject—defining an IPO from scratch

Many paths of motion of objects are hard to model by hand, for example, when we want the object to follow a precise mathematical curve or if we want to coordinate the movement of multiple objects in a way that is not easily accomplished by copying IPOs or defining IPO drivers.

Imagine the following scenario: we want to interchange the position of some objects over the duration of some time in a fluid way without those objects passing through each other in the middle and without even touching each other. This would be doable by manually setting keys perhaps, but also fairly cumbersome, especially if we would want to repeat this for several sets of objects. The script that we will devise takes care of all of those details and can be applied to any two objects.

Code outline: orbit.py

The orbit.py script that we will design will take the following steps:

  1. Determine the halfway point between the selected objects.
  2. Determine the extent of the selected objects.
  3. Define IPO for object one.
  4. Define IPO for object two.

Determining the halfway point between the selected objects is easy enough: we will just take the average location of both objects. Determining the extent of the selected objects is a little bit more challenging though. An object may have an irregular shape and determining the shortest distance for any rotation of the objects along the path that the object will be taking is difficult to calculate. Fortunately, we can make a reasonable approximation, as each object has an associated bounding box.

This bounding box is a rectangular box that just encapsulates all of the points of an object. If we take half the body diagonal as the extent of an object, then it is easy to see that this distance may be an exaggeration of how close we can get to another object without touching, depending on the exact form of the object. But it will ensure that we never get too close. This bounding box is readily available from an object’s getBoundBox() method as a list of eight vectors, each representing one of the corners of the bounding box. The concept is illustrated in the following figure where the bounding boxes of two spheres are shown:

The length of the body diagonal of a bounding box can be calculated by determining both the maximum and minimum values for each x, y, and z coordinate. The components of the vector representing this body diagonal are the differences between these maximums and minimums. The length of the diagonal is subsequently obtained by taking the square root of the sum of squares of the x, y, and z components. The function diagonal() is a rather terse implementation as it uses many built-in functions of Python. It takes a list of vectors as an argument and then iterates over each component (highlighted. x, y, and z components of a Blender Vector may be accessed as 0, 1, and 2 respectively):

def diagonal(bb):
maxco=[]
minco=[]
for i in range(3):
maxco.append(max(b[i] for b in bb))
minco.append(min(b[i] for b in bb))
return sqrt(sum((a-b)**2 for a,b in zip(maxco,minco)))

It determines the extremes for each component by using the built-in max() and min() functions. Finally, it returns the length by pairing each minimum and maximum by using the zip() function.

The next step is to verify that we have exactly two objects selected and inform the user if this isn’t the case by drawing a pop up (highlighted in the next code snippet). If we do have two objects selected, we retrieve their locations and bounding boxes. Then we calculate the maximum distance w each object has to veer from its path to be half the minimum distance between them, which is equal to a quarter of the sum of the lengths of the body diagonals of those objects:

obs=Blender.Scene.GetCurrent().objects.selected

if len(obs)!=2:
Draw.PupMenu('Please select 2 objects%t|Ok')
else:
loc0 = obs[0].getLocation()
loc1 = obs[1].getLocation()

bb0 = obs[0].getBoundBox()
bb1 = obs[1].getBoundBox()

w = (diagonal(bb0)+diagonal(bb1))/4.0

Before we can calculate the trajectories of both objects, we first create two new and empty Object IPOs:

ipo0 = Ipo.New('Object','ObjectIpo0')
ipo1 = Ipo.New('Object','ObjectIpo1')

We arbitrarily choose the start and end frames of our swapping operation to be 1 and 30 respectively, but the script could easily be adapted to prompt the user for these values. We iterate over each separate IPO curve for the Location IPO and create the first point (or key frame) and thereby the actual curve by assigning a tuple (framenumber, value) to the curve (highlighted lines of the next code). Subsequent points may be added to these curves by indexing them by frame number when assigning a value, as is done for frame 30 in the following code:

for i,icu in enumerate((Ipo.OB_LOCX,Ipo.OB_LOCY,Ipo.OB_LOCZ)):
ipo0[icu]=(1,loc0[i])
ipo0[icu][30]=loc1[i]

ipo1[icu]=(1,loc1[i])
ipo1[icu][30]=loc0[i]

ipo0[icu].interpolation = IpoCurve.InterpTypes.BEZIER
ipo1[icu].interpolation = IpoCurve.InterpTypes.BEZIER

Note that the location of the first object keyframed at frame 1 is its current location and the location keyframed at frame 30 is the location of the second object. For the other object this is just the other way around. We set the interpolation modes of these curves to “Bezier” to get a smooth motion. We now have two IPO curves that do interchange the location of the two objects, but as calculated they will move right through each other.

Our next step therefore is to add a key at frame 15 with an adjusted z-component. Earlier, we calculated w to hold half the distance needed to keep out of each other’s way. Here we add this distance to the z-component of the halfway point of the first object and subtract it for the other:

mid_z = (loc0[2]+loc1[2])/2.0
ipo0[Ipo.OB_LOCZ][15] = mid_z + w
ipo1[Ipo.OB_LOCZ][15] = mid_z - w

Finally, we add the new IPOs to our objects:

obs[0].setIpo(ipo0)
obs[1].setIpo(ipo1)

The full code is available as swap2.py in the file orbit.blend (download full code from here). The resulting paths of the two objects are sketched in the next screenshot:

LEAVE A REPLY

Please enter your comment!
Please enter your name here