I tried to implement mesh-skinning and got stuck in bone-space (or mesh-space, I'm not quite sure actually).
The model I use is built in blender. I export it as gltf and import that gltf into a custom tool that uses AssImp to
load the gltf data into my own format. AssImp is row-major but I use column-major matrices. I account for that
when converting the AssImp matrices into my own mat4-type.
My tool basically puts all the vertices/indices into an array, followed by
another array containing the bones' mOffsetMatrix. mOffsetMatrix is the matrix contained with each bone
that converts from mesh- to bonespace according to the doc:
http://assimp.sourceforge.net/lib_html/structai_bone.html
Also, I export the skelton's node-hierarchy. AssImp stores this as a tree, so each Node contains a pointer-array
to its child-nodes, if it has any. For my own format I flatten this node-tree into an array (nothing against flattening curves,
by the way, but for this I think the array was more suitable). See the image below, how this looks in practice:
Each entry in the flattened Node-Array contains two indices. The first one is the index into the Node-Array itself
saying what the parent node is. The second index tells us where to find the mOffsetMatrix for this particular
node within the Bone-Array. When importing the model using my own model-format I also create an
"Animation-Array". I initialize each element with a 4x4 identity matrix.
Since I do not care about keyframes for now I use this array to do "programmed animation". So rather
than using the animation keyframes exported by Blender into the gltf file and using those to
pose my model I simply update this array by hand every frame. So in the main loop I update, for example,
the upper right arm:
| mat4 arm_rot_y = rotate_y (arm_r_rot_y); /* arm_r_rot_y gets incremented of some amount every frame */
md_mesh.animation_matrices[14] = arm_rot_y;
|
I happen to know that index 14 is the bone of the upper right arm. This is not pretty but as I am not sure
where my mistake is and I do not know where the code is going yet I hesitate to do something "generic".
Okay, so the Animation-Array of "md_mesh" is updated.
Next I call a function named
| update_skeleton(&md_mesh)
|
.
This function now goes through the Node-Array to update the skeleton:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | void update_skeleton(MdMesh * mesh)
{
Node * skeleton_nodes = mesh->skeleton_nodes;
uint32_t node_count = mesh->node_count;
for (uint32_t i = 0; i < node_count; ++i) {
Node * node = &(skeleton_nodes[i]);
int parent_index = node->parent_index;
uint32_t bone_index = node->bone_index;
mat4 local_transform = mesh->animation_matrices[bone_index];
mat4 offset_mat = mesh->bones[bone_index].offset_matrix;
if (parent_index >= 0) {
uint32_t parent_bone_index = skeleton_nodes[parent_index].bone_index;
mat4 parent_mat = mesh->tmp_matrices[parent_bone_index];
mesh->tmp_matrices[bone_index] = mat4_x_mat4(parent_mat,
local_transform);
}
else {
mesh->tmp_matrices[bone_index] = local_transform;
}
}
}
|
I loop through the Node-Array ("skeleton_nodes") and if the node has a parent I apply the parent
matrix to the current, local transform. The local transforms are all the identity matrix except
for our upper right arm, which we just updated with a rotation matrix. If there is no
parent (parent index = -1) just the local transform is written. I use tmp_matrices to not override
the Animation-Array (animation_matrices) itself. tmp_matrices is, finally, the mat4 array that
gets sent to the Vertex-Shader.
Okay, I know more stuff has to be done, but let's see what this gives us:
Alright, so the child-nodes of the upper right arm inherit the rotation. That seems to work.
I applied a rotation around the y-axis. I use a right-handed system in View-Space. Y is up,
Z goes into the viewers face (ouch) and X points to the right. In Blender, Z is up. But during export
to gltf +Y is up is enabled. So the model displays just fine in my renderer.
The arm should rotate around its shoulder-socker but it rather rotates around its parent, spine2.
At least it look like it. So rather just appying the hard-coded rotation to the Animation-Array I am
converting to bone-space first:
| mat4 arm_offset = md_mesh.bones[14].offset_matrix;
md_mesh.animation_matrices[14] = mat4_x_mat4(arm_rot_y, arm_offset);
|
This gives me the following result:
Oh well, that looks... interesting.
Anyways, the arm, or to be more precise here, the upper right arms "shoulder" appears to rotate
around the mesh's origin. I put the mesh origin at its feet in Blender, by the way. Also,
the arm is rotated. So, I am not sure if this mOffsetMatrix got us from Bone- to Meshspace now
or vice versa? Well, if I assume we are in bone-space now, let's go back to mesh-space by
applying the inverse of the mOffsetMatrix:
| md_mesh.animation_matrices[14] = mat4_x_mat4(mat4_inverse(arm_offset), mat4_x_mat4(arm_rot_y, arm_offset));
|
That gives the following output:
So at least the arm found its correct position again. Furthermore, it rotates around its "shoulder".
But why is the rotation around the x-axis now? The only explanation I have for this, is that in Blender
the local rotation of the upper right arm's bone is, in fact, the Y-Axis:
.
So this is actually a "good" thing (I think). I want to be able to animate the character using
bone-transforms and not relative to the View-Space.
However, it still does not to seem quite right. The arm now looks a bit distorted. In fact,
if I apply a translation matrix after doing the rotation, it looks like as if the further the vertices
are away from the model, the more scaling is applied:
| mat4 trans = mat4_identity();
trans = translate(trans, (vec3){ 0, .5, 0 });
trans = mat4_x_mat4(trans, arm_rot_y);
md_mesh.animation_matrices[14] = mat4_x_mat4(mat4_inverse(arm_offset), mat4_x_mat4(trans, arm_offset));
|
Also, again, I applied the translate into the *positive* Y direction and not the negative X. The positive Y
in Blender of the upper right arm in local bone-space points the the left.
This is also a hint that the transformations I am doing are in local bonespace after
applying the arm_offset matrix.
Phew, that was a lot. As you can see I am not quite sure about what space I am in sometimes ;) This writeup
is already the best result I could get after swapping matrices and making many other things. Most of the time
it would simply trash the whole model which sometimes also looked cool but was definitely very incorrect.
Maybe someone who reads this immediately notices a very obvious mistake I am doing but I got to a point
where I don't see the forest for the trees anymore. That is why I am asking for some advice now. Any
thoughts about this are highly appreciated.
Take care of yourselves and stay healthy.
Cheers! Pythno.