Assimp COLLADA import to custom format animation problem

Started by
1 comment, last by MickeMan 9 years, 6 months ago

Hello everyone. I'm busting my *** off trying to solve this problem I'm having.

I've read some blogs and forum posts concerning this issue and I'm not quite able to find a solution. Help me, Game Dev, youre my only hope.

(Everything is programmed in Windows 7 Visual Express 2013 32 bit with glew btw)

COLLADA Export and Import

So I've made a new project to act as a middleman to take a COLLADA file and export to my own format. A very simple format that starts off with sizes of VBO, Index Buffer, amount of bones, inverse bind pose, bone relative offsets, animations, frames and transformation of each frame. Everything you need so its easy to import directly on the GPU.

The framework I choose for this excercise is assimp. Basically it takes the COLLADA file, parses it and populates into their own structure. I then take that structure and write basically everything as a sequence of floats. A vertex has 3 floats, a transformation has 16 floats etc.

The vertex buffer gets written as such:


unsigned int v;

	for (v = 0; v < scene->mMeshes[0]->mNumVertices; ++v) {
		for (GLint i = 0; i < 4; ++i) {
			bones[i] = -1.0f;
			weights[i] = -1.0f;
		}

		out.write((char*)&scene->mMeshes[0]->mVertices[v].x,	sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mVertices[v].y,	sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mVertices[v].z,	sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mNormals[v].x,		sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mNormals[v].y,		sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mNormals[v].z,		sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mTextureCoords[0][v].x, sizeof(GLfloat));
		out.write((char*)&scene->mMeshes[0]->mTextureCoords[0][v].y, sizeof(GLfloat));

		if (num_bones != 0) {
			GLfloat wb = 0;

			for (GLint i = 0; i < scene->mMeshes[0]->mNumBones; ++i) {
				for (GLuint j = 0; j < scene->mMeshes[0]->mBones[i]->mNumWeights; ++j) {
					if (scene->mMeshes[0]->mBones[i]->mWeights[j].mVertexId == v) {
						weights[(int)wb] = (GLfloat)scene->mMeshes[0]->mBones[i]->mWeights[j].mWeight;
						bones[(int)wb] = (GLfloat)i;

						wb = wb + 1;
					}
				}
			}
			//write the number of bones
			out.write((char*)&wb, sizeof(GLfloat));

			//write the bone lookups
			for (unsigned int i = 0; i < 4; ++i) {
				out.write((char*)&bones[i], sizeof(GLfloat));
			}

			//write the weights
			for (unsigned int i = 0; i < 4; ++i) {
				out.write((char*)&weights[i], sizeof(GLfloat));
			}
		}
	}
//write indices
for (GLuint i = 0; i < scene->mMeshes[0]->mNumFaces; ++i) {
for (GLuint j = 0; j < scene->mMeshes[0]->mFaces.mNumIndices; ++j) {
out.write((char*)&scene->mMeshes[0]->mFaces.mIndices[j], sizeof(GLuint));
}
}

Nothing fancy. Just write each vertex to file basically.

Next I write the transformation for the skeleton itself, and then the bone - parent index, bind poses and local transforms for each bone


//write the transformation for the root node only!
for (GLint a = 0; a < 4; ++a) {
	for (GLint b = 0; b < 4; ++b) {
		out.write((char*)&scene->mRootNode->mTransformation[b][a], sizeof(scene->mRootNode->mTransformation[b][a]));
	}
}

//write bone parents
//bone index b has parent parents[b], if parent is the skeleton itself, the bone has parent = -1
for (GLint bone = 0; bone < scene->mMeshes[0]->mNumBones; ++bone) {
	logmsg("bone: %d, parent: %d\n", bone, parents[bone]);
	out.write((char*)&parents[bone], sizeof(parents[bone]));
}

//bind poses (this seems to be inverted already?)
for (GLint bone = 0; bone < scene->mMeshes[0]->mNumBones; ++bone) {
	for (GLint a = 0; a < 4; ++a) {
		for (GLint b = 0; b < 4; ++b) {
			out.write((char*)&scene->mMeshes[0]->mBones[bone]->mOffsetMatrix[b][a], sizeof(scene->mMeshes[0]->mBones[bone]->mOffsetMatrix[b][a]));
		}
	}
}

//transformation poses, or the local transformation of the bone
for (GLint bone = 0; bone < scene->mMeshes[0]->mNumBones; ++bone) {
	for (GLint a = 0; a < 4; ++a) {
		for (GLint b = 0; b < 4; ++b) {
			out.write((char*)&transformations[bone][b][a], sizeof(transformations[bone][b][a]));
		}
	}
}

In my 3D engine I load everything and run it through my skinning shader, i pass the uniform of the bone transformation lookups like so:


void M3DArmature::update_animation(s_bone_node* node, GLint current_frame, float dt) {
  if (!node->skip) { // skip the root bone, which is the skeleton transformation itself
	node->absolute_transform = node->parent->absolute_transform * node->local_transform;

	_shader_data->skinned_transforms[node->index] = node->absolute_transform * node->inv_bind_pose;
  }
  else {
	//not quite sure why i need to inverse the skeletons transform, but if i dont do it, the mesh is turned 90 degrees backwards
	node->absolute_transform = glm::inverse(_root_node->local_transform);
  }

  for (GLint i = 0; i < node->num_children; ++i) {
	//update each child in the hierarchy
	update_animation(node->children[i], 0, 0.0f);
  }
}

And heres my shader:


#version 410
layout (location = 0) in vec3 in_position;
layout (location = 1) in vec3 in_normal;
layout (location = 2) in vec2 in_texcoord;
layout (location = 3) in float in_num_bones;
layout (location = 4) in vec4 in_bone_indices;
layout (location = 5) in vec4 in_vertex_weights;

uniform mat4 matrix;
uniform mat4 bones[64];

out vec2 texCoord;

void main() {
	vec4 newVertex = vec4(0,0,0,0);
	int index = 0;
	
	for (index = 0; index < int(in_num_bones); ++index) {
		newVertex += (bones[int(in_bone_indices[index])] * vec4(in_position,1)) * in_vertex_weights[index];
	}

	newVertex = vec4(newVertex.xyz, 1);
	gl_Position = matrix * newVertex;
//skip normals for now
	texCoord = in_texcoord;
}

And heres what the mesh looks like (Picture 1)

Great.

Now we save the animation data. This is a bit trickier because the assimp documentation is lacking a bit, I had to look through the source code to come up with the solution.


GLint animations = 0;

if (!scene->HasAnimations()) {
	out.write((char*)&animations, sizeof(GLint));
	out.close();
	return true;
}

animations = 1;
//write num animations, if 0 it means we will skip loading animations
out.write((char*)&animations, sizeof(GLint));

logmsg("loading animations...\n");

aiMatrix4x4** animation_transforms = new aiMatrix4x4*[scene->mMeshes[0]->mNumBones];

GLint frames = scene->mAnimations[0]->mChannels[0]->mNumPositionKeys;

logmsg("num channels: %d\n", scene->mAnimations[0]->mNumChannels);

for (GLint i = 0; i < scene->mAnimations[0]->mNumChannels; ++i) {
	aiNodeAnim* animnode = scene->mAnimations[0]->mChannels[i];
	GLint bone_index = GetIndexForBone(animnode->mNodeName, scene->mMeshes[0]->mBones, scene->mMeshes[0]->mNumBones);

	animation_transforms[bone_index] = new aiMatrix4x4[animnode->mNumPositionKeys];
	GLint j;

	logmsg("num frames: %d\n", frames);

	for (j = 0; j < frames; ++j) {
		aiMatrix4x4 mat_rotation;
		aiVector3D mat_scale = animnode->mScalingKeys[j].mValue;
		aiVector3D mat_translation = animnode->mPositionKeys[j].mValue;

		mat_rotation = aiMatrix4x4(animnode->mRotationKeys[j].mValue.GetMatrix());

		aiMatrix4x4 mat;
		mat = aiMatrix4x4(animnode->mRotationKeys[j].mValue.GetMatrix());
		mat.a1 *= mat_scale.x; mat.b1 *= mat_scale.x; mat.c1 *= mat_scale.x;
		mat.a2 *= mat_scale.y; mat.b2 *= mat_scale.y; mat.c2 *= mat_scale.y;
		mat.a3 *= mat_scale.z; mat.b3 *= mat_scale.z; mat.c3 *= mat_scale.z;
		mat.a4 = mat_translation.x; mat.b4 = mat_translation.y; mat.c4 = mat_translation.z;

		animation_transforms[bone_index][j] = mat;
	}
}

//write matrix4x4 [numbones][frames]
for (GLint b = 0; b < scene->mMeshes[0]->mNumBones; ++b) {
	//write num frames
	out.write((char*)&frames, sizeof(GLint));
	for (GLint a = 0; a < frames; ++a) {
		for (GLint m = 0; m < 4; ++m) {
			for (GLint n = 0; n < 4; ++n) {
				out.write((char*)&animation_transforms[b][a][n][m], sizeof(animation_transforms[b][a][n][m]));
			}
		}
	}
}

The code above I'm still unsure about... But to the best of what I've read on the internet and the assimp source, i believe that i do in fact store the correct transform for each bone and frame in animation_transform

So with my animation data I can now update the shader uniform:


void M3DArmature::update_animation(s_bone_node* node, GLint current_frame, float dt) {
	if (!node->skip) {
		//this is kind of working
		node->absolute_transform = node->parent->absolute_transform * node->animations[0].transforms[1];
		_shader_data->skinned_transforms[node->index] = node->absolute_transform * node->inv_bind_pose;
	}
	else {
		node->absolute_transform = glm::inverse(_root_node->local_transform);
	}

	for (GLint i = 0; i < node->num_children; ++i) {
		update_animation(node->children[i], current_frame, dt);
	}
}

So what I'm doing here is just forcing animation 0 (we only have one animation written to file anyways atm), and use frame 1 (not 0) where the only transform ive done in the armature is to rotate the right front leg back. The result is puzzling to me (Picture 2)

Why is the bone that has changed only incorrect? Shouldnt ALL bones be incorrect? Everything you see in the code posted is what im doing with the transforms. Im not toying around with anything. For testing purposes i added + 1.0f to a matrix[][] value transformation and then everything looked screwed up, as expected.

Ive tried more or less every combination of inverse bind pose, transpose (i upload the shader array transposed btw), multiply everything with everything and nothing with everything. I am literally stuck!

Please help sad.png

Advertisement

I'm still having issues with this. I feel like assimp does some kind of optimization, like it only stores transforms of the animation in mScalingKeys, mRotationKeys, mPositionKeys if there is a change in the bone transforms relative to .... something?

But when I change from

node->absolute_transform = node->parent->absolute_transform *node->animations[0].transforms[1];

to

node->absolute_transform = node->parent->absolute_transform;

The model looks like something out of a bad sci fi movie.

In case anyone is interested, I managed to solve it by totally removing assimp.

In fact, I decided that COLLADA wasnt making me very happy and looked around for simpler and a bit more clear format.

Turns out that the DirectX .x format is excellent. The documentation is brilliant and parsing it is really simple. I made a loader which converts the .x file to my own binary format and suprisingly, it worked directly in my game engine. So I still dont know how assimp alters the animation data, but comparing the assimp matrices with the COLLADA matrices shows clear differences. Thats strange to me. Whats even stranger was the lack of explanation in the documentation on how to handle the animation data. Obviously there are working examples out there using assimp, but why go through all the fuzz when the .x format is so easy to use.

For anyone trying to tackle COLLADA my recomendation is to avoid it. Use something simpler, like .x. Even though its intended for DirectX, it worked beautifully with OpenGL.

So, case closed.

This topic is closed to new replies.

Advertisement