Dinosaur Tutorial 2
   
 
   
 
   
 
 
3.7 mb zipped max file - requires the full version of ACT
 
divx avi - 2.4 mb
   
divx avi - 964KB
 
divx avi - 888KB
   
divx avi - 1.4 mb
 
jpg - 87KB
   
jpg - 125KB
 
jpg - 139KB
   
   
You can download the latest divx video codec from www.divx.com
 

Tutorial 2

Character Rigging - the neck of cgCharacter-a-saurus
By Mark Snoswell. October 3rd 2001

 

 
     
 
zipped Max file - (1.3 mb)
 
 


The goal.
The first thing is to identify just what is required. In this case we have a long neck - there are 13 vertebra from the shoulders to the base of the head. When we look closely at the vertebra and the neck shape it becomes apparent that there is more up and down motion possible than sideways (lateral) motion - just due to the interlocking of the vertebra. It is also clear that twisting along the axis is limited. Overall we want control of the base and top of the neck for up and down motion and single smooth interpolation of lateral and twisting motions. Furthermore we don't want the twisting to alter the other axis of motion - so it should be applied as a local rotation after the other rotations.

For our model the steps are:
1. Smooth lateral motion (Z axis) along the whole Neck.
2. Smooth up-down motion for the upper and lower neck (Y axis)
3. Smooth twist along local X axis spread over the whole neck.

User Interface - manipulators
We definitely need a good visual control setup - preferably a custom manipulator. This should also always be close to the neck and offer an instant visual guide to the neck orientation. We have found that it is not useful to have controls like this as spinners or other screen space controls… this gets messy and there is no visual alignment of controls with the 3D model. It is much more intuitive, efficient and easy to use if there are 3D controls near the controlled objects. In our first version here we have used a PlaneAngle Manipulator for the X axis twist and two 3 sided tubes for the base and top of the neck. They are linked together with locks on all the transforms that we don't want - for instance the top of the neck has only one axis allowed - the Y axis for up and down. This whole control rig is positioned near the base of the spine and linked to the parent of the vertebra to be controlled…. This way the control rig will always be handy and right where it is meant to be.

Later on we will replace our 3 object control rig with a single custom manipulator that does the same - but has better visual indication of the angles

How to implement control.
This is the hard bit and where Max really lets us down. What we really need is a System Controller for the Neck. A System Controller is a single controller that controls a whole set of objects. The IK controller is a System Controller as is Biped. For the sort of control we have identified, let's look at the possible alternatives: The IK controller just won't work as we can't control the twisting. We could use a path deform - but this will deform the vertebra and it's not really going to do what we want - apart from anything else the pivot points of the vertebra will all remain the same - it's just the vertices that get deformed. Blended orientation controllers - taking their blends from the control objects - This would work but it's very time consuming to set up, prone to Max cashing and difficult to modify.
Scripted controllers on the vertebra. This would work, but again, it gets complicated - your scripts are spread all over the place and it is difficult to manage and modify. It is also prone to crash Max as the complexity goes up.

What we really need is a single scripted System Controller. This is exactly the approach we have taken. The only problem is that Max just does not support this solution! The big question is where do you put your System Control script?… remembering that it must run interactively in the viewport, at render time before each frame is set up, and it must load with the Max file for efficient management of data… this is how we did it…

Track View top level nodes.
If you look in the Track View you will see that we have added a new top level node called "Character_Rigging". This is where we will be putting all the scripted System Controllers. There is just one there now - a float script controller called "Neck_control". We don't care what type of script controller it is really - because the end result is irrelevant (float, point3, transform…) as the script will be directly setting the transforms for the controlled objects. You create top level nodes in the MaxSCript Listener with the newTrackViewNode command. Here are the three lines that created the Character_rigging node and added the Neck_Control controller:

 
 
n = newTrackViewNode "Character_Rigging"
c = float_script()
addTrackViewController n c "Neck_control"

For more information look in the "Track View Node" help topic in the MaxScript help

 
 

Save the Initial transforms.
The first thing we need to do now is to get and save the initial data - that is the names of the objects we are going to control and their initial transforms. (the transform matrix holds the complete position, rotation and scale information of an object). In addition to the thirteen vetebra we will be manipulating we will also store the data for their immediate parent - the 14th vertebra. We will use this information although we will not manipulate the 14th vertebra. The easiest way to get the initial transform data is to print it out in the MaxScript Listener window in a format ready to cut and paste it into an array declaration. Here is a simple one line script to do this… first select the first 14 vertebra and then type…

for obj in selection do format "%,\n" obj.transform

we can do the same sort of thing to generate a list of names to paste into an array.

for obj in selection do format "$%," obj.name

With a little editing we now have the two arrays with the initial information - (MaxScript simply reads past the end of one line on the next to complete statements. This makes it easy to lay out long array declarations into readable formats).

local neck_obj = #( $mSaurus_C_vert_01,$mSaurus_C_vert_02,$mSaurus_C_vert_03,$mSaurus_C_vert_04, $mSaurus_C_vert_05,$mSaurus_C_vert_06,$mSaurus_C_vert_07,$mSaurus_C_vert_08,$mSaurus_C_vert_09, $mSaurus_C_vert_10,$mSaurus_C_vert_11,$mSaurus_C_vert_12,$mSaurus_C_vert_13,$mSaurus_C_vert_14)

local neck_tr = #( (matrix3 [-0.00691494,0.994506,0.104453] [-0.999651,-0.00421581,-0.0260388] [-0.0254554,-0.104596,0.994187] [-6.77272,-376.65,230.071]), (matrix3 [-0.00573862,0.998616,0.0522729] [-0.999745,-0.00458906,-0.0220837] [-0.0218133,-0.0523863,0.998387] [-6.82334,-370.047,230.713]), (matrix3 [-0.00356831,0.998126,-0.0610912] [-0.999804,-0.00474449,-0.0191189] [-0.0193729,0.061011,0.997947] [-6.86346,-362.631,230.612]), (matrix3 [-0.000789427,0.974365,-0.224971] [-0.999856,-0.00457183,-0.0162926] [-0.0169035,0.224926,0.974227] [-6.8907,-354.341,229.498]), (matrix3 [0.00172078,0.920503,-0.390733] [-0.999896,-0.00400564,-0.0138403] [-0.0143052,0.390716,0.9204] [-6.89503,-345.858,226.866]), (matrix3 [0.00342017,0.852638,-0.52249] [-0.999929,-0.00295741,-0.0113717] [-0.0112412,0.522492,0.852568] [-6.88262,-338.149,223.035]), (matrix3 [0.00442991,0.793353,-0.608747] [-0.999944,-0.00222531,-0.010177] [-0.00942855,0.608758,0.793298] [-6.85444,-331.142,218.261]), (matrix3 [0.00442967,0.793352,-0.608747] [-0.999956,-0.00140325,-0.00910524] [-0.00807788,0.608761,0.793311] [-6.81506,-323.963,212.289]), (matrix3 [0.0038686,0.83867,-0.544627] [-0.999964,-0.00073271,-0.00823131] [-0.00730239,0.54464,0.838637] [-6.77807,-316.867,206.649]), (matrix3 [0.00247784,0.93358,-0.358361] [-0.999971,-0.000177045,-0.00737547] [-0.00694906,0.358369,0.933553] [-6.7496,-309.497,202.376]), (matrix3 [0.000621876,0.999048,-0.0436188] [-0.999981,0.000361754,-0.00597167] [-0.00595021,0.0436217,0.999029] [-6.73711,-300.798,199.955]), (matrix3 [1.67151e-005,0.991445,0.130526] [-0.999992,0.000471645,-0.00345449] [-0.00348649,-0.130525,0.991437] [-6.74338,-290.983,200.157]), (matrix3 [-3.08133e-007,0.987689,0.156434] [-0.999999,0.000101989,-0.000645905] [-0.000653907,-0.156433,0.987687] [-6.75497,-281.116,201.5]), (matrix3 [-4.07968e-007,0.979925,0.199368] [-1,-4.29179e-007,0] [0,-0.199368,0.979925] [-6.75772,-270.405,203.375])

Take Total Control - and still getting messages!
For efficiency we will take total control of the neck vertebra. This means that we will have them all unlinked - we will do the parenting. When you are doing transform controllers this is much more efficient than having Max double up on the work of parenting all the objects. It also breaks the long reference chain the neck generates in the Max hierarchy.
The problem with taking total control and having everything unlinked is that you have to make sure your scripted System Controller gets all the right messages to update.

There are three main sets of message events to get right:

1. Interactive viewport updates when the control objects are used.
2. Interactive viewport updates when the parent object is manipulated
3. Pre - geometery set up message, per frame at render time

The first two aren't hard as MaxScript controllers can now use the dependson command to list other controllers (or objects) that they depend on.

roll_angle = $mSaurus__neck_roll.angle.controller
neck_base = $mSaurus__neck_base
neck_top = $mSaurus__neck_top
dependson roll_angle neck_base.transform.controller neck_top.transform.controller
$mSaurus_C_vert_14.transform.controller

The third one is real hard as Max does not provide this event message!!! However I worked out a little trick a few years ago to get around this… It turns out that the Ambient Light controller is evaluated during rendering each frame before geometry set-up. So what I do is make the Ambient Light controller a list controller (so you can still set the Ambient Light normally) with the second controller a float_script() controller called "System_Script_trigger". In this script you will add a call to any scripts you want rendered once per frame before geometry set-up… it is effectively generating the system event you need that the Max API does not provide. Here is the single line you have to add to trigger our Neck_control script:

a = trackviewnodes.character_rigging.neck_control.value

To cover the circumstance where the other dependant objects are not visible and you still want the neck to work you add the additional line

dependson trackviewnodes.character_rigging.neck_control

Now we have a scripted System Controller for the neck that is called correctly at all times - neat hey?

Controlling rotation
We want to apply cumulative rotations from the base of the neck to the top - it's cumulative because the vertebra are all linked together. Now the first thing to note is that object's transformation matricies are in saved world space regardless of their linkage - this is for efficiency. Lets show you the code that does a smooth linear rotation from one end of the spine to the other first…

coordsys parent base_rot = inverse ((quat (neck_base.rotation.angle / 13) neck_base.rotation.axis) as matrix3)

for j = 13 to 1 by -1 do neck_obj[j].transform = neck_tr[j] * inverse neck_tr[j+1] * base_rot * neck_obj[j+1].transform

The first line simply gets the rotation from the control object and divides by 13 - ready to apply to each vertebra. The rotation is expressed as matrix3 ready to use.

In the for loop:
change to the default (before any updates) parent co-ordinate space
neck_tr[j] * inverse neck_tr[j+1]

Apply the rotation
* base_rot

Apply the transformation to the current (updated last tiem round the loop) parent position.
* neck_obj[j+1].transform

Save the result into the current vetebra (which is the parent for the next vertebra)
neck_obj[j].transform =

... all very simple when you know how.
As the roll is just along one axis I have written a small script that is more efficient than the matrix multiply.

fn roll_x ang tr = (tr.row2 *= (quat -ang tr.row1); tr.row3 = cross tr.row1 tr.row2; return tr)

ang = roll.value + (ang_inc = roll.value / neck_obj.count)

for j = 1 to 13 do neck_obj[j].transform = roll_x (ang -= ang_inc) neck_obj[j].transform

In the full implementation the first rotation loop is more complicated to take into account three separate rotations - the smooth Z rotation along the whole neck, and the base and top of neck Y rotations.

coordsys parent Zrot = rotateZMatrix (neck_base.rotation.z_rotation / 8) --neck Z axis

t = 0.0; tInc = 1.0/12.0

for j = 13 to 1 by -1 do (

a = (t ^ 2) / 4 --weighting for neck_top Y axis
b = (exp -(((3.5*t)-1) ^ 2)) / 4 --weighting for neck_base Y axis
coordsys parent top_Yrot = rotateYMatrix (neck_top.rotation.y_rotation * a)
coordsys parent base_Yrot = rotateYMatrix (neck_base.rotation.y_rotation * b)

neck_obj[j].transform = neck_tr[j] * inverse neck_tr[j+1] * base_Yrot * top_Yrot * Zrot * neck_obj[j+1].transform
t += tinc
)

As you can see it is all relatively simple. If you are writing your own scripts to do matrix operations remember that the order in which you do the matrix operation is important!


Things that don't work - Max Bugs!
If you put your System Controller scripts on helper objects or geometry then you have to remember that they won't be evaluated unless they have to be for the frame! You will also run into the added problem that they will probably evaluate after geometery-setup and will therefore lag behind by one frame - this is if the object with your script it not linked to something… if you have it further up in the hierarchy to force an early evaluation then you will get viewport update ghosting and lags in interactive mode!

If you try to set up the array data as persistent global's then you will get errors when loading your file as Max loads persistent global variables after geometry - and Controller Scripts that try to use the variables. Nor does it help to protect your controller script code with Try ( expression) catch () statements. Once a script has failed once Max will often not attempt to evaluate it again!
It also does not work to declare your data in another script as global and then call that only if your variables are undefined - it's a Max bug and this won't work without you manually opening up your control script and evaluating it after loading the file! Warning: If you open up the Track View in graph mode (or change to graph mode) with one of the System Controller scripts selected then you will be forced to wait a long time while Max evaluates the script for its entire validity interval - I want to try to detect this and skip most of the body of the script - I just haven't got around to trying that yet.

Concluding comments
We have tried every different way possible to control sets of objects in character rigging and our System Controller Script method shown here appears to be the best. It carefully avoids a whole stack of Max bugs, problems and deficiencies. However it is not optimal… the Max architecture really needs changing. There is no control system separate from objects. There is no control over the modifier pipeline execution. There are insufficient events - particular at render time… However we are looking into ways we can implement generalised System Controllers with scriptable elements for character rigging - this is the only viable approach until the Max architecture receives a major overhaul (or better yet a complete rewrite).

For those of you who are horrified by the thought of all the scripted controllers, rotations, transform matricies and custom manipulators… sorry. Character rigging is a complex task - even just the bare logic of what a user wants to do can get complicated. This is not a generalist task but rather one that will probably always best be done by a TD (Technical Director) specialising in this area… we're working hard to make it easier for everyone and spearhead the push for better support for this sort of stuff, but it's always going to be a bit hard.

If anyone has alternate or better ways for doing this stuff please let us know and email me, mark@cgCharacter.com