In this example we show how an animated character's tight C code can be adapted in Vodka to provide for modular, composable animation scripting.
The original glutmech (courtesy of Simon Parkinson-Bates) is one of the standard examples that come with most OpenGL installations. There are several keyboard commands that allow the user to move individual limbs of the robot, as well as a fairly smooth animation mode that makes the robot appear to be walking towards the user.
What we are interested in is how this animation is scripted and how
it might be extended to make the robot do other interesting things.
Looking at the original C code reveals
that most of the work is done by
the function animation_walk, which is registered as an on-idle callback.
Each time animation_walk is called, it updates the robot limbs' positions
and calls glutPostRedisplay, which triggers another callback to update
the on-screen drawing.
void animation_walk(void) { float angle; static int step; if (step == 0 || step == 2) { /* for(frame=3.0; frame<=21.0; frame=frame+3.0){ */ if (frame >= 0.0 && frame <= 21.0) { if (frame == 0.0) frame = 3.0; angle = (180 / M_PI) * (acos(((cos((M_PI / 180) * frame) * 2.043) + 1.1625) / 3.2059)); if (frame > 0) { elevation = -(3.2055 - (cos((M_PI / 180) * angle) * 3.2055)); } else elevation = 0.0; if (step == 0) { hip11 = -(frame * 1.7); if (1.7 * frame > 15) heel1 = frame * 1.7; heel2 = 0; ankle1 = frame * 1.7; if (frame > 0) hip21 = angle; else hip21 = 0; ankle2 = -hip21; shoulder1 = frame * 1.5; shoulder2 = -frame * 1.5; elbow1 = frame; elbow2 = -frame; } else { hip21 = -(frame * 1.7); if (1.7 * frame > 15) heel2 = frame * 1.7; heel1 = 0; ankle2 = frame * 1.7; if (frame > 0) hip11 = angle; else hip11 = 0; ankle1 = -hip11; shoulder1 = -frame * 1.5; shoulder2 = frame * 1.5; elbow1 = -frame; elbow2 = frame; } if (frame == 21) step++; if (frame < 21) frame = frame + 3.0; } } if (step == 1 || step == 3) { /* for(x=21.0; x>=0.0; x=x-3.0){ */ if (frame <= 21.0 && frame >= 0.0) { angle = (180 / M_PI) * (acos(((cos((M_PI / 180) * frame) * 2.043) + 1.1625) / 3.2029)); if (frame > 0) elevation = -(3.2055 - (cos((M_PI / 180) * angle) * 3.2055)); else elevation = 0.0; if (step == 1) { elbow2 = hip11 = -frame; elbow1 = heel1 = frame; heel2 = 15; ankle1 = frame; if (frame > 0) hip21 = angle; else hip21 = 0; ankle2 = -hip21; shoulder1 = 1.5 * frame; shoulder2 = -frame * 1.5; } else { elbow1 = hip21 = -frame; elbow2 = heel2 = frame; heel1 = 15; ankle2 = frame; if (frame > 0) hip11 = angle; else hip11 = 0; ankle1 = -hip11; shoulder1 = -frame * 1.5; shoulder2 = frame * 1.5; } if (frame == 0.0) step++; if (frame > 0) frame = frame - 3.0; } } if (step == 4) step = 0; distance += 0.1678; glutPostRedisplay(); } void display(void) { // OpenGL drawing code } int main(int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("glutmech: Vulcan Gunner"); glutIdleFunc(animation_walk); glutDisplayFunc(display); glutMainLoop(); }
Taking a closer look at the code reveals that the animation consists of four keyframes that define the rotation parameters of the robot's joints at four discrete moments in time. Between these keyframes, values are interpolated linearly.
![]() |
![]() |
![]() |
![]() |
By looking even closer, we can see that the animation sequence is in fact three animations taking place in parallel: moving the legs, the arms, and the torso.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
With this knowledge, we can refactor the code quite a bit. We don't bother porting the whole code to Vodka, as our strategy is to leave the low-level stuff to native Java methods which can then be cleverly composed by Vodka code. Porting the code from C to Java is fairly straightforward.
Isolating the three individual animation sequences and dropping the trailing
redisplay trigger (the glutPostRedisplay() statement), we arrive at
four methods (one for each pair
of adjacent keyframes, in wrap-around style)
per sequence. The parameter k, with a domain of 1...7, defines the
interpolated frame.
public class TestMech { public void animation_legs0(int k) { float frame = 3.0f * k; float angle = (180 / M_PI) * ((float)Math.acos((((float)Math.cos((M_PI / 180) * frame) * 2.043f) + 1.1625f) / 3.2059f)); hip11 = -(frame * 1.7f); if (1.7f * frame > 15) heel1 = frame * 1.7f; heel2 = 0; ankle1 = frame * 1.7f; if (frame > 0) hip21 = angle; else hip21 = 0; ankle2 = -hip21; } public void animation_legs1(int k) {...} public void animation_legs2(int k) {...} public void animation_legs3(int k) {...} public void animation_arms0(int k) {...} public void animation_arms1(int k) {...} public void animation_arms2(int k) {...} public void animation_arms3(int k) {...} public void animation_body0(int k) {...} public void animation_body1(int k) {...} public void animation_body2(int k) {...} public void animation_body3(int k) {...} public void display() { // handle OpenGL drawing } }
Chaining the keyframe interpolation sequences together is now done in Vodka. We parameterize
over the mech object and, more importantly, over a function nextFrame that is
called each time computation of a new animation frame is about to start.
new animateLegs; new animateArms; new animateBody; def animateLegs(mech, nextFrame): { # animate to keyframe 1 for i in 1..7: { mech->animation_legs0(i); nextFrame(); } # animate to keyframe 2 for i in 1..7: { mech->animation_legs1(i); nextFrame(); } # animate to keyframe 3 for i in 1..7: { mech->animation_legs2(i); nextFrame(); } # animate back to keyframe 0 for i in 1..7: { mech->animation_legs3(i); nextFrame(); } return(); } def animateArms(mech, nextFrame): {...} def animateBody(mech, nextFrame): {...}
Using Vodka's postfix for-loop notation, we can express the above code a little more terse.
new animateLegs; new animateArms; new animateBody; def animateLegs(mech, nextFrame): { mech->animation_legs0(i)->nextFrame() for i in 1..7; mech->animation_legs1(i)->nextFrame() for i in 1..7; mech->animation_legs2(i)->nextFrame() for i in 1..7; mech->animation_legs3(i)->nextFrame() for i in 1..7; return(); } def animateArms(mech, nextFrame): {...} def animateBody(mech, nextFrame): {...}
Now we can run any one of the three animations, but only one at a time.
In the original callback-oriented model, nextFrame would trigger a repaint
(via glutPostRedisplay) and wait until the drawing has completed. We
can achieve the same effect more straightforwardly, by just using the
mech's display method as nextFrame.
mech = TestMech(); (mech, mech->display)->animateLegs->loop();
The interesting part is now to combine the three basic animations to
the original, composite one. With the help of the higher order
function animateParallel, we can accomplish just that. Note how the
given nextFrame function parameter is only called after each of the
three animation sequences have called their nextFrame parameter:
new animateParallel; def animateParallel(mech, a, b, c, nextFrame): { new nfa; new nfb; new nfc; def nfa() & nfb() & nfc(): { nextFrame(); returnto.nfa() & returnto.nfb() & returnto.nfc(); } mech->a(nfa) & mech->b(nfb) & mech->c(nfc); return(); }
With this utility function, which re-introduces a top-down control structure, we can run the combined animation sequence.
mech = TestMech(); (mech, animateBody, animateArms, animateLegs, mech->display)->animateParallel->loop();
And we can build more complicated animations. Below we define another animation
routine, animateFire, and integrate it with the other ones into
a procedural animation script. The robot now does four regular steps, then waves its arms,
does two little hops forward, fires its cannons, and starts over.
new animateFire; def animateFire(mech, nextFrame): { for i in 1..10: { mech->elbow1Subtract(); mech->elbow2Subtract(); mech->shoulder1Subtract() if i % 3 == 0; mech->shoulder2Subtract() if i % 3 == 0; nextFrame(); } for i in 1..50: { mech->FireCannon(); nextFrame(); } for i in 1..10: { mech->elbow1Add(); mech->elbow2Add(); mech->shoulder1Add() if i % 3 == 0; mech->shoulder2Add() if i % 3 == 0; nextFrame(); } return(); } new script; def script(nextFrame): { mech->animateParallel(animateBody, animateLegs, animateArms, nextFrame); mech->animateParallel(animateBody, animateLegs, animateArms, nextFrame); mech->animateArms(nextFrame); mech->animateBody(nextFrame); mech->animateFire(nextFrame); script(nextFrame); } script(mech->display);
Let's take another look at the nextFrame parameter each animation function
takes. The function nextFrame is invoked after one
animation frame is produced and returns control when computation of the next
frame can commence, i.e. the frame has been fully rendered to the screen.
In its role, nextFrame is thus very similar to the yield
parameter of generator functions: it yields control until a new value is to be
produced. In fact, partially applied animation
functions like mech->animateLegs can be used just like any other
generator!
Therefore, we do not even need a custom-made animateParallel function
as we can use the library function zip instead. The original animation
can now be expressed as:
mech = TestMech(); for frame in repeat zip(mech->animateLegs, mech->animateArms, mech->animateBody): mech->display();
In addition, we can use generator expressions to build animation scripts like the one above and iterate over the individual steps and animation frames by generator comprehension:
script = [ (mech->animateLegs, mech->animateArms, mech->animateBody)->zip(), (mech->animateLegs, mech->animateArms, mech->animateBody)->zip(), mech->animateArms, mech->animateBody, mech->animateFire ]; for step in script->repeat(): for frame in step: mech->display();
In this simulation-oriented example, we have seen how our language can serve as a tool to coordinate native-code building-blocks on a higher level of abstraction. In addition, we have seen how generators can serve as a means to control synchronization and interactivity.