Walking Robot ("glutmech") Example

robot image

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.

Original Code

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();
}
	

Keyframe Animation

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.

Identifying the Building Blocks

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

Re-assembling the Parts

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();
	

Parallel Composition

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

The Power of Generators

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.