Coding 3D Animation #3: spline interpolation on the vertex shader

A comparison of different keyframe interpolation methods In this post, we’ll look at how the keyframe interpolation technique actually works in Codea. If you didn’t grab the 3D Animation Loader code in part 2, here’s the source code link again. Last time we dealt with exporting and importing the models; this time we’ll focus on how the mesh is actually built and animated in Rig:BuildMesh, Rig:anim and the spline interpolation shader. Let’s start with the shader (the shader tab). Part Two is here

Interpolating key frames on the vertex shader

So we have in this instance four keyframes represented by four .obj files. Instead of making separate meshes for these, we are going to load all of them onto one mesh. Then, during the runtime, we can supply the shader with integers representing the frames we wish to interpolate between, and a float describing the amount we want to interpolate by.

Loading the keyframes into attribute buffers

Each point on the model is going to be represented not by a single position vector, but by a series of vectors, one for each keyframe. So that the lighting remains consistent, we are also going to need a matching set of normals. Texture coordinates and vertex colours won’t change frame-to-frame, so these can just be represented with the standard built-in attribute buffers. Here we meet our first problem. Arrays are supported in OpenGL Shader Language. We’re using one to hold the integers pointing to the keyframes we’re interpolating between:

    uniform int frames[4];

However, you cannot use arrays for the vertex attributes (the variables that vary by vertex) in Open GL Shader Language 1.0.1 The shader would be a lot more elegant if we could just write:

    attribute vec3 position[4];

But instead we have to write:

    attribute vec3 position;
    attribute vec3 position2; //not possible for attributes to be arrays in Gl Es2.0 
    attribute vec3 position3;
    attribute vec3 position4;

And we also need to write our own “hash” look-up function for this pseudo-array, plus another almost identical one for the normal array:2

    vec3 getPos(int no) //home-made hash, ho hum.  
    {
        if (no==1) return position;
        if (no==2) return position2;
        if (no==3) return position3;
        if (no==4) return position4;
    }

So, having declared our custom attribute buffers in the shader, we then have to load the .obj files into them. This is handled in Rig:BuildMesh:

    --add frames    
    local pos={m:buffer("position2"), m:buffer("position3"), m:buffer("position4")}
    local norm = {m:buffer("normal2"), m:buffer("normal3"), m:buffer("normal4")}

We set up local arrays to access the custom attribute buffers. This mirrors the hash look up function inside the shader, in a way. Next we step through the keyframes, starting from number 2 (as keyframe 1 is the regular position buffer):

    for i=2, #self.obj do
        local frame=self.obj[i]
        for j=1,#frame.v do
           local v = frame.v[j]
            pos[i-1][j]=vec3(v.x,v.y,v.z) --nb must make an independent copy of the vector
            local n = frame.n[j]
            norm[i-1][j]=vec3(n.x,n.y,n.z)
        end
        print ("added frame "..i)
    end

Loading the data into the custom attribute buffers has to be done as a “deep copy”: we need to step through every vector in the model, and even through every value (xyz) in the vector. With all of the keyframes loaded into the mesh, it’s time to start animating!

Lerp!

To animate the model, we simply need to tell the shader which two frames we are interpolating between, and by how much, on a scale of 0.0 to 1.0, we want to interpolate between them. By gradually varying these two values, we can animate the model. To get these values I firstly cue up a sequence of frames using Rig:cueAnim. The animation must be at least 4 frames long, for reasons that will become clear later. If the animation is shorter than this, you can pad the ending with extra copies of the final frame (note to self: automate this padding). The frames in the animation (or rather the integers that represent the frames) are held in self.frames. To effect the animation, a single self.frame variable is tweened between 0 and the total number of frames. Every tick, Rig:anim is called. self.frame is modulated to produce the integer representing the frame we are interpolating from, and the fractional amount indicating how far between the two to interpolate. There are many ways to interpolate between the two values. In the 3D Animation Loader there are two vertex shaders implementing different interpolation methods. The simplest is linear interpolation, or lerp for short (see linearVert in the shaders tab). As luck has is, Open GL Shader Language has a lerp function built in, mix. To lerp between, say, position2 and position3 we could write:

   vec3 framePos = mix(position2, position3, frameBlend);

where frameBlend represents the instance we are at between positions 2 and 3, on a scale of 0.0 to 1.0. It’s identical to vec3 framePos = position2 * (1. - frameBlend) + position3 * frameBlend; By default, the code uses the spline interpolation. If you’d like to view the linear interpolation make this change in Rig:BuildMesh:

    m.shader=linearShader -- splineShader 

You can also see the two methods compared side-by-side in the video at the top of this post. If you have a limited number of keyframes, lerp can look a little robotic. Viewed in profile, the feet are moving in a somewhat diamond-shaped pattern, rather than an organically flowing line. In some cases, this may be the look you want. However, if we want to achieve a smoother look, without increasing the number of keyframes, we need a different interpolation technique. We need a spline.

Catmull-Rom

There are many spline algorithms out there. Catmull-Rom is a useful one for us, because the curve it describes passes through all of the control points, so it means that we get smoothly flowing motion, and our keyframes are accurately represented. It’s also relatively light, computationally speaking: just a lot of multiplication, but no sine or cosine needed. The algorithm can be found in the splineVert vertex shader:

    vec3 CatmullRom(float u, float u2, float u3, vec3 x0, vec3 x1, vec3 x2, vec3 x3 ) //returns value between x1 and x2
    {
    return ((2. * x1) + 
           (-x0 + x2) * u + 
           (2.*x0 - 5.*x1 + 4.*x2 - x3) * u2 + 
           (-x0 + 3.*x1 - 3.*x2 + x3) * u3) * 0.5;
    }

The first 3 values u, u2, u3, represent the interpolation amount, and that amount squared and cubed respectively. As this amount is uniform across the shader, the squaring and cubing is done in the shader pre-amble:

    uniform float frameBlend; // how much to blend by
    float frameBlend2 = frameBlend * frameBlend; //pre calculated squared and cubed for Catmull Rom
    float frameBlend3 = frameBlend * frameBlend2;

Next, we pass the spline function 4 vector values. The middle 2 vectors represent the start and end points that we are interpolating between. The first and last vectors are the control points for the curve (this is why when we cue the animation, there needs to be at least four keyframes). Catmull-Rom spline Catmull-Rom spline. Source This function smoothly connects our keyframes, making it appear as if there are more than just four. In part four of this series I’ll consider some of the limitations of this approach, and discuss where to take this method from here. Comments, criticisms, and questions are welcome below. Thank you!


  1. “Attribute variables cannot be declared as arrays or structures” The OpenGLES Shading Language Version: 1.00 (2009), p. 30. 
  2. If someone can come up with a more elegant method than this I’d love to hear it. You can’t have pointers to other variables in GLSL either, so I can’t think of any way other than this to pass into the shader a pointer to the correct position and normal variable. 
Advertisements

One thought on “Coding 3D Animation #3: spline interpolation on the vertex shader

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s