I've been trying to implement skeletal animation in my engine using this tutorial, and I've been having problems. Using the model they provided, I get results like this:
But on some other models, such as Wuson, it isn't fragmented, but the triangles don't seem to move together as precisely as intended.
Both of these models appear correct and proper when static (in their bind pose), and render properly using the Assimp Viewer, so assimp does support them well enough.
What I'm looking for is possible areas of investigation, because this is a complicated task and many parts can go wrong.
I'm fairly certain (like 95%) that I'm binding the data buffers correct for both the per-index bone ID's and weights. I tested this by filling the bone's transformation array with a simple scaled matrix and got the expected uniformly scaled model.
Everything else such as the bone extraction and animation are nearly 99% identical to that of the tutorial, so I would put higher confidence in those being correct. I really don't know why I'm getting the results that I am.
Structs Used:
// Info for weight and ID of 4 bones assigned to each vertex
struct VertexBoneData {
int IDs[NUM_BONES_PER_VEREX];
float Weights[NUM_BONES_PER_VEREX];
VertexBoneData()
{
Reset();
};
void Reset()
{
ZERO_MEM(IDs);
ZERO_MEM(Weights);
}
void AddBoneData(int BoneID, float Weight) {
for (int i = 0; i < ARRAY_SIZE_IN_ELEMENTS(IDs); i++)
if (Weights[i] == 0.0) {
IDs[i] = BoneID;
Weights[i] = Weight;
return;
}
assert(0);
}
};
// Basic bone information
struct BoneInfo {
aiMatrix4x4 BoneOffset;
aiMatrix4x4 FinalTransformation;
};
// basic mesh information
struct MeshInfo {
int startIndex = 0;
int meshSize = 0;
TextureInfo texture;
MeshInfo() {
startIndex = 0;
meshSize = 0;
}
};
// Holds all of the information a model needs
struct ModelInfo {
int numMeshes = 0;
int numBones = 0;
int skin = -1;
vector<MeshInfo> meshes;
vector<BoneInfo> boneTransforms;
std::map<string, int> boneMap;
const aiScene* scene = 0;
Assimp::Importer importer;
ModelInfo() {
numMeshes = 0;
numBones = 0;
skin = -1;
scene = 0;
}
MeshInfo& at(int x) {
return meshes.at(x);
}
};
Model Loading:
// Cycle through all the meshes in the model
for (int a = 0, meshCount = scene->mNumMeshes; a < meshCount; ++a) {
aiMesh* mesh = scene->mMeshes[a];
MeshInfo currentMeshInfo;
currentMeshInfo.startIndex = VertexList.size();
// Cycle through each of this mesh's faces
for (int b = 0, faceCount = mesh->mNumFaces; b < faceCount; ++b) {
const aiFace &face = mesh->mFaces[b];
// Grab information for each of the 3 indices of this face
for (int y = 0; y < 3; y++) {
const aiVector3D vertex = mesh->mVertices[face.mIndices[y]];
VertexList.push_back(vec3(vertex.x, vertex.y, vertex.z));
// Do same for tangents, normals, etc //
}
}
currentMeshInfo.meshSize = VertexList.size() - currentMeshInfo.startIndex;
for (int i = 0, numBones = mesh->mNumBones; i < numBones; i++) {
int BoneIndex = 0;
string BoneName(mesh->mBones[i]->mName.data);
if (m_BoneMapping.find(BoneName) == m_BoneMapping.end()) {
BoneIndex = NumBones;
NumBones++;
BoneInfo bi;
boneInfo.push_back(bi);
}
else
BoneIndex = m_BoneMapping[BoneName];
boneMap[BoneName] = BoneIndex;
boneInfo[BoneIndex].BoneOffset = mesh->mBones[i]->mOffsetMatrix;
// Entire mesh is dumped into single vertex list
// VertexID is within the mesh's "local space"
// Thus we add it to the total amount of vertices prior to this mesh
for (int j = 0; j < mesh->mBones[i]->mNumWeights; j++) {
int VertexID = currentMeshInfo.startIndex + mesh->mBones[i]->mWeights[j].mVertexId;
float Weight = mesh->mBones[i]->mWeights[j].mWeight;
bones[VertexID].AddBoneData(BoneIndex, Weight);
}
}
}
}
Animating:
void DModelComponent::Transform(float time)
{
const aiScene *scene = ModelInformation.scene;
int &NumBones = ModelInformation.numBones;
vector<ModelManager::BoneInfo> &BoneInfo = ModelInformation.boneTransforms;
if (scene != 0) {
aiMatrix4x4 Identity = aiMatrix4x4();
float TicksPerSecond = scene->mAnimations[0]->mTicksPerSecond != 0 ?
scene->mAnimations[0]->mTicksPerSecond : 25.0f;
float TimeInTicks = glfwGetTime() * TicksPerSecond;
float AnimationTime = fmod(TimeInTicks, scene->mAnimations[0]->mDuration);
ReadNodeHeirarchy(AnimationTime, scene->mRootNode, Identity);
// Collect transforms into a single vector, to be sent to shader
transforms.resize(NumBones);
for (int i = 0; i < NumBones; i++)
transforms[i] = BoneInfo[i].FinalTransformation;
}
}
void DModelComponent::ReadNodeHeirarchy(float AnimationTime, const aiNode* pNode, const aiMatrix4x4& ParentTransform)
{
const aiScene* scene = ModelInformation.scene;
string NodeName(pNode->mName.data);
const aiAnimation* pAnimation = scene->mAnimations[0];
aiMatrix4x4 NodeTransformation = pNode->mTransformation;
const aiNodeAnim* pNodeAnim = FindNodeAnim(pAnimation, NodeName);
if (pNodeAnim) {
// Interpolate scaling and generate scaling transformation matrix
aiVector3D Scaling;
CalcInterpolatedScaling(Scaling, AnimationTime, pNodeAnim);
aiMatrix4x4 ScalingM = InitScaleTransform(Scaling.x, Scaling.y, Scaling.z);
// Interpolate rotation and generate rotation transformation matrix
aiQuaternion RotationQ;
CalcInterpolatedRotation(RotationQ, AnimationTime, pNodeAnim);
aiMatrix4x4 RotationM = aiMatrix4x4(RotationQ.GetMatrix());
// Interpolate translation and generate translation transformation matrix
aiVector3D Translation;
CalcInterpolatedPosition(Translation, AnimationTime, pNodeAnim);
aiMatrix4x4 TranslationM = InitTranslationTransform(Translation.x, Translation.y, Translation.z);
// Combine the above transformations
NodeTransformation = TranslationM * RotationM * ScalingM;
}
aiMatrix4x4 GlobalTransformation = ParentTransform * NodeTransformation;
aiMatrix4x4 GlobalInverseTransform = (scene->mRootNode->mTransformation);
GlobalInverseTransform.Inverse();
vector<ModelManager::BoneInfo> &BoneInfo = ModelInformation.boneTransforms;
map<string, int> &BoneMap = ModelInformation.boneMap;
if (BoneMap.find(NodeName) != BoneMap.end()) {
int BoneIndex = BoneMap[NodeName];
BoneInfo[BoneIndex].FinalTransformation = GlobalInverseTransform * GlobalTransformation * BoneInfo[BoneIndex].BoneOffset;
}
for (unsigned int i = 0; i < pNode->mNumChildren; i++) {
ReadNodeHeirarchy(AnimationTime, pNode->mChildren[i], GlobalTransformation);
}
}
Methods such as "CalcInterpolatedRotation()" and such are completely identical to the ones posted within the tutorial, hyperlinked at the beginning of this post.
Rendering:
// omitted other shader uniform bindings
GLuint sid = shader->getPID();
GLuint id = glGetUniformLocation(sid, "Bones");
if (transforms.size()>0)
// Bind array of bone transformations
glUniformMatrix4fv(id, transforms.size(), GL_TRUE, &transforms[0][0][0]);
// Render each mesh within the model, defined by its starting index and size
ModelManager::ModelInfo &model = ModelInformation;
for (int x = 0, total = model.numMeshes; x < total; x++) {
ModelManager::MeshInfo &mesh = model.at(x);
mesh.texture.Bind();
glDrawArrays(GL_TRIANGLES, mesh.startIndex, mesh.meshSize);
}
Shader:
During my geometry pass, I do the following:
layout(location = 0) in vec3 vertex;
// ... extra ... //
layout(location = 4) in ivec4 BoneIDs;
layout(location = 5) in vec4 Weights;
const int MAX_BONES = 100;
uniform mat4 mMatrix;
uniform mat4 pMatrix;
uniform mat4 vMatrix;
uniform mat4 Bones[MAX_BONES];
uniform bool useBones;
void main(void)
{
mat4 BoneTransform = mat4(1.0);
if (useBones) {
BoneTransform = Bones[BoneIDs[0]] * Weights[0];
BoneTransform += Bones[BoneIDs[1]] * Weights[1];
BoneTransform += Bones[BoneIDs[2]] * Weights[2];
BoneTransform += Bones[BoneIDs[3]] * Weights[3];
}
mat4 worldMVP = pMatrix * vMatrix * mMatrix;
vec4 v = BoneTransform * vec4(vertex,1.0);
gl_Position = worldMVP * v;
}