home News software projects tutorials
Max Script
• Rolling Ball
Contact
• Forum
• e-mail
Site Search

Tutorials - Max Script

Rolling Ball

Problem Definition

One of my friends asked me to help him to roll a tilt ball that he has already animated with a path/key frames. Since the ball is bouncing around and thus changing directions it's not easy to animate the rotation of the ball so that the roll amount matches with the distance the ball is traveled. First I tried some expression controllers but then I realized that since the current orientation of ball depends on the path it has traveled, it's a job for max script. With max script it's possible to query the past situations.

So here is the test max scene I created. For compatibility this max file if created using max r4 and we'll use r4 throughout this tutorial too.

In this scene there's a sphere object animated using a path constraint controller. It moves along the path but does not roll. Our task is to roll it. We just begin with assigning max script controller to the ball's rotation channel.

Assigning the Script Controller

• Open track view (curve editor in max6), find Sphere01 in the tree and select the Rotation controller under Transform (Sphere01/Transform/Rotation)
• Click the assign controller button and pick Rotation Script and then click OK

• If the Script controller dialog is not already displayed, right click on the rotation controller and  just pick "properties". That should open the Script controller dialog.

by default the script area should have the text

`quat 0 0 0 1`

Lets first explain what it means.

Quaternion Explained

In 3ds max rotations are usually kept as kind of 4D complex numbers called quaternions. Although  in 3d it's possible to define a direction by just two numbers (such as spherical coordinates; longitude/latitude values), and the orientation of an object in 3D space with only 3 numbers (such as Euler X,Y,Z rotations) , when it comes to animation more numbers are needed. Even 3 rotation values are not enough since that may present problems such as gimbal lock. Gimbal lock is the situation in which you loose control in one axis because axises  overlap due to earlier rotations. In short we need 4 values to be able to define animation (interpolation) of orientations so that during the animation the object rotates in a natural way and rotates from one orientation to another using the shortest possible way.

You can imagine a quaternion as consisting of one rotation axis (which is defined as a 3D vector) and the amount of rotation around this axis. Actually the the last value is not the angle itself but cosine of the half of the angle: cos(Theta/2). Also the the rotation axis (as defined by the first 3 values (x, y, z)) is scaled by the sine of half of the rotation angle). So the values for Quaternion Q is

Quat q = {x,y,z,w} = {vx*sin(T/2), vy*sin(T/2), vz*sin(T/2), cos(T/2)}

{0 0 0 1} quaternion we see here is unit quaternion. since w is 1 which means the angle is zero (no rotation). since sin(0)=0 then all the first tree values are 0.

Although Quaternions seem to be quite complex at first glance, they'll make our task easier since what we need to do is rotate the ball around the axis which is perpendicular to the movement (like a wheel). But as the ball changes direction that rotation axis should be changed accordingly too.

Finding the Movement Direction and Distance

First we need to find the movement direction in the current frame. we can easily do that by subtracting the previous position from the current one. Since max script has no mechanism to give the node that this controller is applied to, we have to specify the object in max script ourselves. The reason of max script's not being able to give the object that this script is assigned to is that same script can be shared by many objects (nodes) and therefore there can be ambiguity. So we'll specify the object explicitly in the script. This makes the code less portable but more robust. (Though in Max6 they introduced a way to get this; but again what we'll use is both more compatible and more robust)

obj = \$Sphere01 -- you may need to change this

To be able to get the position of the object in different time values we'll use "at time x" feature of max script. Such that to get positions of the object in t0 and t1 times we write

p0 = at time t0 obj.position
p1 = at time t1 obj.position

t0 is the previous frame time, and t1 is the current time. (We'll talk about those a bit more later. After getting the positions of current and previous frames, we can determine the direction of movement by subtracting the positions. Also we can find  the unit vector by dividing it to the length of the vector. Also note that length of that vector is the distance that the ball is travelled.

dif = p1-p0                -- difference in positions
len = Length(dif)      -- distance that's travelled
vec = dif / len           -- normalized movement vector.

Finding the Rotation Axis

now we have the traveled distance and the travel direction. As I said the rotation axis is perpendicular to the movement vector. but there are infinite number of vectors perpendicular to the movement vector. We need another criteria to pick. Assuming that the ground is horizontally oriented (no hills or such), we can say that the force that the ground applied to the ball is in up direction (+Z vector). The rotation axis should be perpendicular to that vector too. So we need to find a vector which is perpendicular to both vec (that we found earlier) and +Z (0,0,1) vectors. So if take cross product of those two vectors we'll find the vector we are seeking.

rotax = cross vec [0, 0, 1]

Finding the Rotation Amount

If the ball rotates a full turn, it'd travel a distance equal to it's circumference. So if the divide the traveled distance that we found earlier to it's circumference, we'll find how many rotations it needs to travel that distance. As you know the circumference of circle is 2*pi*r. If we want to support the radius changes we have to use the average of the radii in t0 and t1 times. So the rotation angle (in degrees)  that is required to travel len distance is.

angle = 360*len/(2*(r0+r1)/2*pi)

which reduces to

angle = 360*len/((r0+r1)*pi)

Building the Quaternion

So we have the rotation axis (rotax) and and the rotation amount (angle). We can build the quaternion. Max script has a Quaternion constructor which requires just those two values we have (so we don't need to deal with cosine and sine of the half angles)

rotdif = quat angle rotax

This quaternion defines the rotation from previous frame to current frame. But what scripted rotation controller wants from us to return the total rotation (that is including the all past rotations). So we need to apply this rotation to the previous frame's result. but the previous frame's rotation depends on the earlier rotations just similarly. If the animation was played starting from frame 0 and advances one frame at a time, we could just store and use the values for the next frame. But we can not be sure about that since user may scrub the time slider. So we need to calculate the previous frames' values too. fortunately it's easy to do so by defining a function which gives the rotation at a given time value and calling the same function recursively with previous frame's time when we need it. So func(t) calls func(t-1) which calls func(t-2) etc. This has to end at some point. So we assume that t=0 there's no rotation (object local Z is aligned with world).

for the previous frame we just define a time step value (timeres). Currently it's 1 frame but you can change it if more precision or speed is required.

Putting all Togather

Here is the final script we come up with. As I said you need to change the obj definition to math with your object's name. Also you may need to change the timeres value which defines the time steps in which the movement assumed to be linear. We could avoid the rotation axis calculations by turning the Follow option in path constraint ON, but I wanted to make it compatible with key framed (and even procedural) animations too.

```-- You need to change the below assingment.
-- If the name of the object you are assigning this controller
-- is "Ball", then convert the line to
-- obj = \$Ball
-----------------------------------------------------
obj = \$Sphere01 		-- change this
timeres = 1f 			-- time resolution
-----------------------------------------------------
fn getrot t =
(
if t<=0f then return quat 0 0 0 1 -- t=0 => no rotation
t0 = t-timeres		-- previous frame time
t1 = t			-- current time

rot0 = getrot(t0)           -- previous rotation:

p0 = at time t0 obj.position-- previous position
p1 = at time t1 obj.position-- current position
if(p0==p1) then return rot0 -- no distance is traveled

dif = p1-p0                 -- difference in positions
len = Length(dif)           -- distance that's traveled
vec = dif / len             -- normalized movement vector.

rotax = cross vec [0, 0, 1] -- rotation axis
angle = 360*len/((r0+r1)*pi)-- rotation amount (in degs)
rotdif = quat angle rotax   -- rotation from t0 to t1
rot1 = rot0 + rotdif        -- total rotation
)

getrot(currentTime)
```

Click Evaluate and if no error is shot, click Close in the script rotation dialog box.

Have Fun

The result should be like the one in roll02.max file. Here is a DivX 5.0.5 animation showing the result (66KB)

I noticed that in the recent versions of 3ds max, the above script may cause 3ds max to crash. Looks like the cause of this crash is stack overflow due to recursion. Fortunately it's very easy to convert this script to a non-recursive form by using loops instead of recursion (maybe it's what I should have done in the first place). Anyhow below is the modified non-recursive form of the script. I also added a starttime variable which lets you set the time where object starts to roll.

Script will get slower when the current time is far away from the starttime; this is normal. The reason is that it needs to calculate all the rotation history for the object until it reaches to the current time. You can try increasing the timeres value if it becomes too slow.

```-- You need to change the below assingment.
-- If the name of the object you are assigning this controller
-- is "Ball", then convert the line to
-- obj = \$Ball
-----------------------------------------------------
obj         = \$Sphere01 -- change this
starttime   = 0f        -- reference time
timeres     = 1f        -- time resolution
-----------------------------------------------------
fn getrot t =
(
rot = quat 0 0 0 1                      -- at the reference frame the rotation is zero

-- loop in time to find the cumulative rotation at the given time t
for curtime = starttime+timeres to t by timeres do
(
t0 = curtime - timeres              -- previous frame time
t1 = curtime                        -- current time

p0 = at time t0 obj.position        -- previous position
p1 = at time t1 obj.position        -- current position
if(p0!=p1) do                       -- if no distance is traveled, rotation is the same as previous frame
(
dif = p1-p0                     -- difference in positions
len = Length(dif)               -- distance that's traveled
vec = dif / len                 -- normalized movement vector.

rotax = cross vec [0, 0, 1]     -- rotation axis
angle = 360*len/((r0+r1)*pi)    -- rotation amount (in degrees)
rotdif = quat angle rotax       -- Quaternion which defines rotation from t0 to t1
rot = rot + rotdif              -- add this to the result rotation
)
)

return rot
)

getrot(currentTime)
```

visited 123324 times since 4/29/2004 7:53:39 PM (24.6138817414447 visits per day)
config | web | home jordan 11 berd jordan 6 lakers gamma jordan 11