Our engine made one giant leap in the rendering world. We can now successfully render a character model, fully rigged, textured, and animated, using the Havok tools. The accompanying video shows the Havok Girl running in place.
Let’s get started with the steps I took to reach this point, beginning with where I left off last week: missing geometry. When I rendered that model, I only pulled the vertex buffer information from the HKX file. I made sure to uncheck ‘Ignore Triangle Indices’ in the Havok Content Tools, but that didn’t seem to have any effect. I then decided to pull the index buffer information as well and build the mesh using a triangle list – this was the source of my problem. Using both the vertex buffer and index buffer filled in the missing geometry and thus a static mesh was born. This also optimizes the data coming in, because there is no need to store duplicated vertex data, as the index buffer handles triangle ordering.
Once I had a static mesh successfully imported, it was time to tackle animation. I began with understanding the structure of information that Havok stores in an HKX file, and knowing what each component of an animation actually is. The graph below shows the different objects Havok creates, what those objects store, and how they relate to one another.
- hkaAnimationContainer: everything you need is in this object. This object is essentially the root, storing all necessary data associated with the model, including the skeleton, animation bindings, mesh bindings, and attachments. Any piece of data that you need from your HKX file will likely come from this object. I’ll note that some data can come from the hkxScene object, but it is unorganized; the structure of the hkaAnimationContainer is much friendlier.
- hkaAnimationBinding: this object stores the data associated with a specific animation. For every animation exported, an hkaAnimationBinding object is created with access to various animation settings and an hkaInterleavedUncompressedAnimation.
- hkaInterleavedUncompressedAnimation: this object stores the individual transforms associated with an animation, including annotation tracks and duration. When an animation is created, the hkaAnimationBinding is interpreted, and the hkaInterleavedUncompressedAnimation is extracted from that.
- hkaSkeleton: the skeleton is a fundamental object associated with the animation container. It stores all of the bone data, including parent indices and bone names, as well as the transforms of each bone that will put the skeleton in a reference pose.
- hkaBone: the skeleton is comprised of a number of hkaBones. These objects have a name and DOF properties.
- hkaMeshBinding: the animation container also contains a number of hkaMeshBindings, which have information regarding a specific mesh, the bone transforms linked with the mesh, and any mappings that were included.
- hkxMesh: each mesh binding has an hkxMesh object. This object has the bulk of the vertex and index buffer information. A mesh is composed of one or more hkxMeshSections.
- hkxMeshSection: an individual section has the data that we most care about: vertices, indices, and materials; the stuff you see! These objects are composed of an hkxVertexBuffer and hkxIndexBuffer.
- hkxVertexBuffer: vertex data (should be familiar at this point).
- hkxIndexBuffer: index data (should be familiar at this point).
Once this was understood, it was a matter of grabbing the appropriate data, storing it, and sending it to the various systems for rendering. This took some time figuring out, as it was a lot of data to keep track of and figuring out exactly how it all hooked up together. The final step was smooth skinning. This step involved stepping through the animation, grabbing the bone transforms for the skeletal pose, and sending the combined mesh and bone transforms to the shader for rendering. The bone weighting for smooth skinning is detailed below:
// Iterate through the first three weight values and average them
for (int iBone = 0; iBone < 3; iBone++)
// Update the Weight Value
lastWeight = lastWeight + input_VS.blendWeights[iBone];
// Update the output Position and Normal
output_Position += mul(input_Position, (bones[input_VS.blendIndices[iBone]])) * input_VS.blendWeights[iBone];
output_Normal += mul(input_Normal, (bones[input_VS.blendIndices[iBone]])) * input_VS.blendWeights[iBone];
// Calculate the final Weight Value
lastWeight = 1.0f - lastWeight;
// Perform the final update on the Position and Normal
output_Position += (mul(input_Position, (bones[input_VS.blendIndices])) * lastWeight);
output_Normal += (mul(input_Normal, (bones[input_VS.blendIndices])) * lastWeight);
The code involves grabbing four bone weights associated with a given vertex, and averaging them together to get the vertex position.
It took many hours to get to this point, and I am proud of the results. This was a huge step for us and we are excited about our next steps.