| |
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 |
|