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.
r0 = at time t0 obj.radius -- previous radius
r1 = at time t1 obj.radius -- current radius
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)
|