Advanced Expressions Setup - The Desk Lamp

Download the Tutorial Scenes Here

View the Completed Animation Here

Years ago, Pixar introduced us to Luxo, Jr., (the first CGI short film to garner an Oscar nomination) a cute hopping desklamp who, along with his wise and benevolent father lamp, discovers that you can sometimes have TOO much fun. As Luxo, Jr. hops around the scene, his electrical cord whips up and down and around, following his motion. It's amazing how sophisticated this animation is for one created in 1986. While a team of animators created the original, this tutorial will explore how to automate the various parts of a mechanical assembly such as a desklamp so that just one animator can achieve a similar task.

Chapter 1: the basic lamp assembly

1.1: The Basic Lamp Setup

Figure 1 illustrates the basic assembly for the lamp.

Figure 1: Sketch of the lamp assembly.

Each "arm" of the lamp consists of dual struts, attached to the various joints, the base joint, the elbow joint, and the upper joint. There are also springs providing counter-tension running from the base and elbow joints to one of their respective connected struts. Finally, the head of the lamp is attached to the upper joint, and the whole assembly is attached to the base. The finished hierarchy looks something like this:

However, we will start constructing the scene in pieces so we can more easily digest the expression setups involved. First, load up the scene desklamp_first.lws. This scene has no Relativity expressions applied yet…just a barebones scene with the hierarchies in place. The hierarchy should look like Figure 2.

Figure 2: the lamp hierarchy.

Note that not even the springs are in place yet. First we need to focus on getting these basic parts functioning properly. Bring up the generic plug-in "Relativizer" and apply Relativity to "desklamp_piece01:layer1" and "desklamp_joint01:layer1". Now, a brief observation: if you move an actual desk lamp around, both the struts in each arm end up rotating at the same angle relative to the joint. So, if strut 2 is 30 degrees rotated, strut 1 will be also. Also notice how the lamp itself remains at the same angle relative to the world as you rotate any one arm of the lamp, so it would almost seem that the joint at the top of any two struts ends up canceling out the rotational values of the struts. This should give us enough of a description of the behavior of the lamp to apply some expressions to it. Rather than having to rotate both struts in an arm, it would make more sense to rotate one strut and have the other follow that rotation. Hence, we will automate the first strut "piece01" relative to piece02:

  1. Bring up the Relativity interface for "desklamp_piece01:layer1" and give it the following Rel Expression in the bank slot:B:
  1. Bring up the interface for "deklamp_joint01:layer1" and give it the following Rel Expression (again in the bank slot):
B: -B(desklamp_piece02.lwo:layer1,t)
  1. Keyframe the "piece02" object on its bank rotation and watch now as the first strut and the joint rotate properly as well. The example scene file "desklamp_second.lws" shows this stage of the set-up process already completed
  1. The expressions for the upper struts and the joint at the base of the "head" need similar expressions.
The scene file "desklamp_third.lws" shows the result of applying these Relativity expressions.

1.2: Creating a Pseudo-IK controller for the lamp's head

At this point, you should be able to keyframe your lamp into any pose you want by simply keyframing the "piece02" and "piece03" struts of the lamp. Everything else should follow automagically. Were we just wanting to animate the lamp moving around a bit, this would probably be sufficient (except for setting up the springs, of course); however, since we want to create a Pixar-style hopping lamp, it would be easier if we had control of the position of the lamp's head at every point. Hence, we are going to set up a pseudo-IK type system where a "stretch" null will allow us to precisely place our head's height with respect to the base of the lamp.

Load the scene "desklamp_joints_final_pseudoIK_first.lws". This scene shows the lamp posed from a fully stretched to a fully squashed position (done by animating the bank rotation of strut 2 and 3) and a null keyframed to the position of the head at both extremes. Note that while the null matches up at the beginning and end, it doesn't match well for intermediate positions of the lamp. In the scene "desklamp_joints_final_pseudoIK_second.lws", the rotations of the struts in the lamp have been corrected to match the elevation of the null from the highest to the lowest position. You'll also see that there is a null placed near the base joint of the lamp, and another null, named "dist_null" with an Relativity GAP() expression applied to measure the distance between these two nulls. With this scene, we have captured what should happen with the stretch null moved to various heights…all we need to do now is build an expression that plays back this animation based on the distance between the two nulls--the base null and the stretch null…basically a link between keyframed values and the positions of these nulls.

To achieve this linked playback of the animation, we're going to use Dr. Blend Machinist on the animations of struts 2 and 3.

  1. With "desklamp_joints_final_pseudoIK_second.lws" loaded, apply Relativity to "desklamp_piece03:layer1" and "desklamp_piece02:layer1".
  2. Select the "dist_null" object and drag the frame slider from frame 0 to 12, noting the high and low value on the X channel for this null…this will give you the maximum and minimum distances for the stretch null from the base null over which to play back the animation. I noted the following maximum and minimum values, respectively: 6.9607 m and .2356919 m (i.e. 235.6919 mm). Bring up the Relativity interface for "desklamp_piece02:layer1".
  3. Next to the bank slot, select Dr. Blend Machinist. Fill the following values in the fields:
  1. Click OK. You should now have the following expressions
  1. Click the "Copy" button to copy these expressions to the copy buffer.
  2. Change the "Current Object" selector to " desklamp_piece03:layer1"
  3. Click the "Paste" button.
  4. Click the "Continue" button on the Relativity interface. Nothing should really look like it's changed much.
  5. To see the automated expressions, select "stretch_null", change the frame to frame 12, press the Enter key, change the frame number to 60 and press Enter again. Press the Delete key and press the enter key to delete the key on frame 12…you've now effectively transferred the keyframe on frame 12 to frame 60. Play back the animation to see that the lamp's head does indeed follow the "stretch_null" up and down in a very IK-like fashion. You can even go to frame 0, turn on "Auto Key", make Relativity interactive by selecting the "Make_Rel_Interactive" generic plug-in and drag the "stretch_null" up and down, watching the lamp assembly follow (note: you might see occasional gaps in the assembly, as the high level of interdependency between all these pieces can slightly confuse LightWave in interactive mode).
You can see the final results of the pseudo-IK setup in the scene "desklamp_pseudoIK_final.lws".

1.3: Springs and things

Next, we will turn our attention to the springs. Load the scene "desklamp_with_springs_first.lws". You may also want to examine the spring object, "lamp_spring_final.lwo" in Modeler to see how it was designed. It has two endomorphs, the base and the "compressed" morph…the compressed one is significantly shorter than the base. While in modeler, the distance between the centers of the spring ends in the normal and compressed modes was measured. These turned out to be, respectively, 2.095 m and 1.530 m. Here are some things to note in the spring scene itself:

  1. Each spring has an associated "spring_base", "spring_align", and "spring_tip" null.
  2. The purpose of the spring base is to rotate the spring so that it is pointing roughly at the spring tip…this way, Relativity's targeting system (which works best when the targeting does not go above 90 degrees pitch or below -90 degrees pitch) can point the springs at their appropriate tip nulls.
  3. The spring_align null will more precisely target each spring at the appropriate "spring_tip" null. This way, the springs will always point directly at the nubbin on which their tip is supposed to be suspended.
  4. Each spring itself is rotated to that it points down the positive Z axis of its parent "spring_align" null.
To align the bases of the springs, perform the following steps:
  1. Keyframe spring_base (1) and spring_base (2) to HPB = (90,-96,0) at frame 0
  2. Keyframe spring_base (3) and spring_base (4) to HPB = (-90,-65,0) at frame 0
The next step is to target the springs precisely at their tip nulls:
  1. Apply Relativity to each of the four "spring_align" nulls.  Give each the following expression in its "Special Functions" slot:
TARGET(spring_tip (#ex),t)
The (#ex) will cause each to point to its appropriately numbered tip null, "spring_align (1)" will point to "spring_tip (1)", "spring_align (2)" will point to "spring_tip (2)", etc.
Finally, we need to assign a morph expression to each of the springs:
  1. Apply the Relativity_Morph plug-in as a deformation plug-in to each of the spring objects.
  2. Give each the following expressions:
What this will do is create a blend value from 0 to 1 as the expression "GAP(spring_base (#ex),t,spring_tip (#ex),t)" changes values from 2.095 to 1.530 (again the "#ex" in the expressions will help each expression point to the appropriately numbered base and tip nulls).  When the distance between the two nulls is 2.095, the BLEND expression will evaluate to 0, which will mean that the spring is 100% base endomorph.  When teh distance is 1.530, the BLEND expression will evaluate to 1, which will make the spring 100% morphed to the endomorph "compressed".
The scene file "desklamp_with_springs_final.lws" shows the lamp with all the spring expressions in place.

Figure 3: close-up of the springs assembly

Chapter 2: The Cord

We will now turn our attention to animating the lamp's cord, a critical part of the original Luxo, Jr. animation.  Ideally, we want to be able to drag the lamp around and have it automatically generate impulse waves down the length of the cord, as well as follow the lamp without having to resort to any great amount of manual keyframing.

Open up the scene "cord_first.lws" and take a look around.  Not much to see...just a subdivided cord object and a blank scene.  The first step toward getting a proper cord is setting up a Relativity bone snake.  Invoke the generic plug-in "*REL:Scene_Professors".  It will bring up an interface where you can choose one of several scene professors from the drop-down list.  Choose Dr. Snake-maker and use the following values in the panel's fields:

Click OK and drag your scene slider forward/backward (or use the left and right arrow keys) to reset everything.  You should see a jumble of bones around the end of the cord object...don't panic; this is normal.  Relativity, by default, will leave all these bones order to get them in their proper position, we're going to need to rest them with Relativity disabled:

  1. Invoke the generic plug-in "*REL:Disable_Relativity".  You should see a pop-up appear indicating that Rel is now disabled
  2. Move the frame slider back/forth by one frame.  All the bones should now be extended down the length of the cord
  3. Select the first bone, press the "r" key to rest it, down-arrow to the next bone, and repeat until all bones are rested
  4. Invoked the generic plug-in "*REL:Enable_Relativity" and cycle the frame back/forth once again...your object should now be jumbled with the bones.
In order to stretch out the bone snake, keyframe the object "cord_parent" to Z= -29 by frame 10.  You might want to experiment around with keyframing the cord parent to various positions in space and watch how the bones snake around behind it.  The scene "cord_second.lws" shows the expected results of all the previous steps.

The next set of steps will create a small "hop" in the cord:
  1. Duplicate the keyframe for "cord_parent" on frame 10 to frame 9 (i.e. go to frame 10 with "cord_parent" selected, press Enter once, change the frame value to 9 and press Enter again) and set all channels on frame 10 to linear in the graph editor.  
  2. Keyframe "cord_parent" to XYZ=<2.4411 m, 0, -31.3896 m> at frame 22.
  3. Kill the keyframe for "cord" at frame 0.
  4. Keyframe "cord" to XYZ=<0,0,0> at frame 10 and frame 22
  5. At frame 16, keyframe "cord" to XYZ=<0,1.0604 m,0>
  6. Note that the cord now takes a small "hop" from frame 10 to frame 22
While it may seem odd for now, the reason for splitting the motion of the head of the cord into two motions will become apparent in a few minutes...suffice it to say for now that it is purposeful that "cord_parent" move only along the XZ plane (i.e. the "floor") while "cord" moves only along Y.  The example scene "cord_third.lws" shows these steps completed.

As of yet, the most crucial part of the cord rig still needs to be done...after the "hop", the cord just sits there.  To get the Luxo, Jr. effect, the impulse wave needs to travel down the length of the cord.  The easiest way to pull this off would be to have the target nulls follow the motion of the cord in Y while still following the parent in X and Z as the cord is dragged around the scene.  Invoke the "*REL:Global_Motion_Access" plug-in to allow easy access to all Relativity instances in the scene.  In the menu in the top left corner of the Relativity panel, select the null object "snaktrg0" (note: you may have to use the "Next 20 Objects Using Relativity" menu choice to bring that object into the menu list).

Change the expressions to look like this:
  1. XMINPATH(cord_parent,0.933333*(#ex+1),t)
  2. Y(cord.lwo,t-#ex*0.1)
  3. ZMINPATH(cord_parent,0.933333*(#ex+1),t)
Click the Copy button and paste these expressions to the other 29 target nulls (HINT: you can select target object 29, press the escape key to remove focus from any of the fields, press the "v" key to paste, press the down arrow key to go to the next object, press "v" again, etc. until you have pasted the new expressions to all of the nulls).  Following is an explanation of how these expressions work:

0.933333*(0+1) = 0.933333 * 1 = 0.93333
This expression for snaktrg1 would evaluate to 0.93333*2 = 1.866666, for snaktrg2 it would evaluate to 2.799999, etc.  Interpreting this, each null follows at a multiple of 0.93333 behind the "cord_parent" null object in X and Z.
The example scene "cord_fourth.lws" shows the cord rig completed up to this stage.  The motion of the impulses looks ok, but it needs some better control.  Say we used the same "#ex" numerical extension values to add some damping the further away from the tip of the cord that we get.  You may have noticed a null called "damp" in these scenes.  This null's X value will be used to add a damping factor to the Y value of each target null based on "#ex".  Change the expressions for all the target nulls to:
Variables panel
  1. #ex*X(damp,t)
  2. COND(#a<1,#a,1)
Main panel
  1. Y(cord.lwo,t-#ex*0.1)*(1-#b)
This will subtract a damping factor from the Y expression for the target null...this factor will get increasingly larger the further down the chain we go...but the COND() statement in variable slot B will prevent the damping value from ever becoming negative.  This expression uses the X of "damp" to control the damping amount...try keyframing "damp" to various X values and see how the cord reacts.

A final change to the expressions involves using the "delay" null to control how quickly the wave propagates down the cord.  A small delay value will cause each target null to more rapidly follow the tip of the cord, causing a faster and more spread-out wave.  To incorporate the delay null into the expressions, change the Y expression to:
  1. Y(cord.lwo,t-#ex*X(delay,t))*(1-#b)
for each of the target nulls.  This will allow us to modify what used to be "0.1" in the expressions a few steps ago with the X value of the "delay" null.  Once done, all the target nulls should have the following expressions:
Variables Panel:
  1. #ex*X(damp,t)
  2. COND(#a<1,#a,1)
Main Panel:
  1. XMINPATH(cord_parent,0.933333*(#ex+1),t)
  2. Y(cord.lwo,t-#ex*X(delay,t))*(1-#b)
  3. ZMINPATH(cord_parent,0.933333*(#ex+1),t)
The example scene "cord_fifth.lws" shows all this in place.  Try keyframing "damp" and "delay" to different values to see how the cord target null expressions react.

Chapter 3: combining it all together

The scene file "desklamp_with_cord_and_springs.lws" shows everything combined.  The lamp is hopping around (using the stretch null to control the position of the "head") and the cord is automatically dragging and pulsating behind.  If you examine the expressions, you will see that things have been modified a bit further from what you've created in previous sections of this tutorial.  These changes are further explained below.

3.1: Adding some Expressions Finesse

Once I started animating the lamp, it became clear that the stretching motion provided by the stretch null was not enough to give the lamp some follow-through and recoil from each of its hops.  Further, it seemed that it needed to lean forward a bit more as it took off to give it a sense of carrying itself forward into each leap.  To facilitate this need for extra control, I created two nulls, "upper_rotate" and "lower_rotate", that I could use, with modified expressions, to add extra rotation to the upper and lower arms respectively.  The bank expressions, then, for piece02 and piece03 were modified to:




respectively.  This will add the bank value of the appropriate null to each arm, allowing finer control of their motions.  You can examine the keyframed bank values for each of these nulls to see how they were used to better animate the lamp.

Once the hopping motion had improved, it became apparent that the lamp's head was not showing any of the necessary inertia relative to the lamp's upper arm.  If the lamp were extending itself rapidly, the head should swing slightly downward, where if it's contracting, the head should swing slightly upward.  The following expressions on the lamp's head took its world-coordinate speed and direction into account to add some extra rotation:

  1. (YW(SELF,t)+YW(SELF,t+0.03)+YW(SELF,t-0.03))/3 //determine average position at present frame
  2. (YW(SELF,t-0.03)+YW(SELF,t)+YW(SELF,t-0.06))/3 //determine average position a frame ago
  3. (#a-#b)/0.03 //world coordinate velocity of our head object
  4. COND(#c<0,-1,1) //return -1 if our velocity is negative, +1 if it's positive
  5. #d*sqrt(abs(#c)) //take the square root of the speed and multiply by variable d
  6. B(SELF,t)+0.05*#e //add a little to the bank using this speed factor

and for the actual bank expression, just use F:

  1. #f

If you examine the scene, you will notice that this expression adds just a little wobble to the lamp's head as it bounces around.  One exercise for the might be better if the lamp head had a slight delay in reacting to its motion....what changes could be done to A and B to give the lamp a several-frame delay?

Finally, a modified "flashlight" expression is used to ramp up the lensflare intensity based on the angle of the light to the camera:

  1. XW(camera,t) - XW(lamp_light,t) //vector pointing from lamp_light to camera
  2. YW(camera,t) - YW(lamp_light,t)
  3. ZW(camera,t) - ZW(lamp_light,t)
  4. FORX(lamp_light,t) //forward vector of lamp_light
  5. FORY(lamp_light,t)
  6. FORZ(lamp_light,t)
  7. sqrt((#a*#a)+(#b*#b)+(#c*#c))  //magnitude of vec in ABC
  8. sqrt((#d*#d)+(#e*#e)+(#f*#f)) //magnitude of vec DEF
  9. #a/#g   //normalized vector ABC
  10. #b/#g
  11. #c/#g
  12. #d/#h //normalized vector DEF
  13. #e/#h
  14. #f/#h
  15. (#i*#l) + (#j*#m) + (#k*#n) //dot product
  16. acos(#o) //angle between the two vectors
  17. COND(#o>0,#p,0.5*_pi) //only use positive dot-prod values...
  18. TBLEND(1,0,0,0.5*_pi,#q)

Channel expression: (0.7*#r*#r*#r) //cube the value to make it "spike" more narrowly

This animation should help you understand just how useful expressions can be in your work.  Imagine having to animate all those struts, springs, joints, and lensflares by hand (and imagine, most of all, having to go back and tweak some motion just slightly and needing to re-do all those keyframes and envelopes).  Using Relativity expressions, animators have a world of tools available at their fingertips to take their animations further...all the way back to the venerable Luxo, Jr.

All files Copyright © 2002, Prem Subrahmanyam, All Rights Reserved.  Object, Scene, and Animation files may be used for personal education, but may not be redistributed in any form without the express permission of the author, Prem Subrahmanyam.  

Back to the Relativity Page.