GPU skinned animations in libgdx

(Without using modelbatch)

We’ve been using the old libgdx 3d api for about 7 months now, in order to develop our game. We recently (last week or so) decided to make the transition to the new 3d API to keep our libgdx versions up to date with the nightlies. The transition itself was rather simple, and we’re still keeping things at a level very near what we used in the old API. For example, we’re not using the modelBatch class nor any of the shaders provided to us.

The main thing that we’ve been missing graphically has been animated 3d objects. We did have a keyframed animation system that we developed ourself to animate between two static states of an obj-model, but the downsides were just too many for us to bother adding it to the actual game.

So, we finally sat down trying to implement animations today. After a few hours of reading through the source files of modelbatch, skeletontest and defaultshader – we finally understood what was going on and could start implementing our own version.

This blogpost will go through how we handle and render our animations, and hopefully others that would like to stick to a lower level of opengl can benefit from it.

The first thing we do is ofcourse to load the file. We do this using the libgdx class AssetManager:

assets.load("data/model/charModelAnim.g3dj",Model.class);

The file can be loaded either as g3db (binary) or as g3dj (json). You can convert the most used file formats to either of these two using the fbx-converter (https://github.com/libgdx/fbx-conv), we used fbx files that our artist created for us.

When the file has been loaded, its time to create the actual objects. We create modelInstances in the constructor of our characters:

Model characterModel = assets.get("data/model/charModelAnim.g3dj");
charInstance = new ModelInstance(characterModel);
animationController = new AnimationController(charInstance);
animationController.animate(charInstance.animations.get(0).id, -1, 1f, null, 0.2f); // Starts the animaton

Now we have our animation set up, and all that remains is for the character to call:

 animationController.update(deltaTime);

during its update method.

This is all the logic that we need to do the actual updating of the animation, and it’s time to start thinking about how to render it. In order to render our animated objects, we do the following:

charShader.begin();
// Bind whatever uniforms / textures you need
for (GameCharacter ch : g.characters){
    Array<Renderable> renderables = new Array<Renderable>();
    final Pool<Renderable> pool = new Pool<Renderable>() {
        @Override
        protected Renderable newObject () {
            return new Renderable();
        }
        @Override
        public Renderable obtain () {
            Renderable renderable = super.obtain();
            renderable.lights = null;
            renderable.material = null;
            renderable.mesh = null;
            renderable.shader = null;
            return renderable;
        }
    };
    ch.charInstance.getRenderables(renderables, pool);
    Matrix4 idtMatrix = new Matrix().idt();
    float[] bones = new float[12*16];
    for (int i = 0; i < bones.length; i++)
    bones[i] = idtMatrix.val[i%16];
    for (Renderable render : renderables) {
        mvpMatrix.set(g.cam.combined);
        mvpMatrix.mul(render.worldTransform);
        charShader.setUniformMatrix("u_mvpMatrix", mvpMatrix);
        nMatrix.set(g.cam.view);
        nMatrix.mul(render.worldTransform);
        charShader.setUniformMatrix("u_modelViewMatrix", nMatrix);
        nMatrix.inv();
        nMatrix.tra();
        charShader.setUniformMatrix("u_normalMatrix", nMatrix);
        StaticVariables.tempMatrix.idt();
        for (int i = 0; i < bones.length; i++) {
            final int idx = i/16;
            bones[i] = (render.bones == null || idx >= render.bones.length || render.bones[idx] == null) ?
            idtMatrix.val[i%16] : render.bones[idx].val[i%16];
        }
        charShader.setUniformMatrix4fv("u_bones", bones, 0, bones.length);
        render.mesh.render(charShader, render.primitiveType, render.meshPartOffset, render.meshPartSize);
    }
}
charShader.end();

So basically what happends is, we loop all characters that we want to render. We create an array of renderables, aswell as a pool of renderables. These are needed in order to collect the renderables from the modelInstance (via instance.getRenderables(array, pool)). We also need to create and initiate an array of floats to represent each bone matrix. We have 12 bone matrixes, and each matrix contains 16 floats. Now, we loop all renderables that we have collected, and set the matrixes that we need as usual. We also need to loop and fill the float array with the data from the matrices, if there is such data to be found.

(Also, please note that you should not be creating new objects each rendercall, as in the code above, but instead create them once and reuse them.)

So, there’s all the code we call at the CPU in order to initiate the rendering. Now all that is left is to show our shader code! All the skinning occurs in the vertex shader, and that is why we will only share that this time (Our fragment shader can be found in the normalmap source as linked from another blogpost).

//Firstly, we need to define loads of new attributes, one for each bone.
attribute vec2 a_boneWeight0;
attribute vec2 a_boneWeight1;
....
attribute vec2 a_boneWeight11;
//We also need to take the bonematrices
uniform mat4 u_bones[12];
void main() {
    // Calculate skinning for each vertex
    mat4 skinning = mat4(0.0);
    skinning += (a_boneWeight0.y) * u_bones[int(a_boneWeight0.x)];
    skinning += (a_boneWeight1.y) * u_bones[int(a_boneWeight1.x)];
    ...
    skinning += (a_boneWeight11.y) * u_bones[int(a_boneWeight11.x)];
    //Include skinning into the modelspace position
    vec4 pos = skinning * vec4(a_position,1.0);
    // Rest of code is justlike usual
    v = vec3((u_modelViewMatrix * pos).xyz);
    vsN = normalize(vec3(u_normalMatrix * skinning * vec4(a_normal, 0.0)).xyz); //viewspaceNormal
    gl_Position = u_mvpMatrix * pos;
    v_texCoord = a_texCoord0;
}

So thats basically it, and we got away without having to rewrite our entire render engine to be able to use the new 3d API!

 

 

Kommentera

E-postadressen publiceras inte. Obligatoriska fält är märkta *

Följande HTML-taggar och attribut är tillåtna: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>