It moves!

I took inspiration from Epic’s Unreal Engine when I implemented the animation system in ItsyScape.

There’s three distinct types: Animation, Skeleton, and Model. An Animation animates the bones of a Skeleton, and a Model is bound to a Skeleton.

Thus, the Skeleton must be loaded first:

local skeleton = Skeleton("Resources/Test/Human.lskel")

(That was easy!)

I use Assimp to convert 3D models to their parts. The format for a skeleton looks something like this:

{ ["Armature"] = { inverseBindPose = { 1.000000, -0.000000, 0.000000, -0.000000, -0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 1.000000, 0.000000, -0.000000, -0.000000, 0.000000, -0.000000, 1.000000, }, }, ["root"] = { parent = "Armature", inverseBindPose = { 1.000000, -0.000000, 0.000000, -0.000000, -0.000000, 1.000000, -0.000000, 0.000000, 0.000000, -0.000000, 1.000000, -0.000000, -0.000000, 0.000000, -0.000000, 1.000000, }, }, -- ... }

It’s executed as a Lua chunk with an empty environment (so no built-in functions).

In order to get the inverse bind pose of each matrix, I had to walk the scene graph from the parent node to the skeleton, and then from the skeleton to each bone. The inverse bind pose is simple the inverse of each bone’s global transform (i.e., parentN * parent1 * bone). Assimp doesn’t include the complete list of bones in each mesh, only the necessary bones; however, like in Unreal, I want to attach objects to bones at runtime, so I need the full skeleton.

Next up is the mesh format. The file format follows the behavior of love.graphics.newMesh and can, in fact, be directly used as parameters with only one minor runtime modification:

{ format = { { 'VertexPosition', 'float', 3 }, { 'VertexNormal', 'float', 3 }, { 'VertexTexture', 'float', 2 }, { 'VertexBoneIndex', 'float', 4 }, { 'VertexBoneWeight', 'float', 4 }, }, vertices = { { 0.426207, 0.885919, 0.155932, 0.000000, 0.000000, 1.000000, 0.778139, 0.221861, "head", false, false, false, 1.000000, 0.000000, 0.000000, 0.000000, }, { -0.157818, 1.469944, 0.155932, 0.000000, 0.000000, 1.000000, 0.221861, 0.778139, "head", false, false, false, 1.000000, 0.000000, 0.000000, 0.000000, }, -- ... } }

The format is something like this:

position.x, position.y, position.z normal.x, normal.y, normal.z texture.s, texture.t bone[0...3] boneWeight[0...3]

So I iterate over the skeleton, replacing bone[n] with the bone index from the Skeleton, or 1 if false (since the weight should be 0, this doesn’t matter).

After the replacement, the mesh can be created like so:

local t = {} -- the above table local mesh = love.graphics.newMesh(t.format, t.vertices, 'static', 'triangles') for _, element in ipairs(t.format) do mesh:setAttributeEnabled(element[1], true) end

Nice!

Lastly, animations are pretty simple. The first animation is exported from the model by the tool. It has a list of bones with key frames for positions, rotations, and translations. Currently, there must be the same number of positions/rotations/translations in each key frame, but there’s no technical reason that has to be; the only reason is I’m lazy :).

Again, they’re stored in a simple format and loaded as a Lua chunk.

Some problems I encountered were:

  1. When calculating the inverse bind the post, the entire scene graph needs to be walked. I was just walking from the root bone down; this gave me some weird results:

  1. Apply the inverse bind pose after all transforms have been generated.
  1. Use apitrace when nothing shows up. :)

In other news, I got movement working:

It follows a list of nodes (in this case, TilePathNodes) and uses steering behaviors to stay on track.