3
\$\begingroup\$

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:

fragmented bob

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.

tearing wuson

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;
}
\$\endgroup\$
  • 1
    \$\begingroup\$ One problem I can see is that you're adding all four bone transforms in your shader when in some cases a vertex may be affected by less bones. I would suggest you create your own animation & models, starting out simply and then building the complexity: Start with a single bone w/ translation. Single bone w/ translation & rotation. Multiple bones w/ translation. Multiple bones w/ translation & rotation. \$\endgroup\$ – Soapy Feb 18 '16 at 9:20
  • 1
    \$\begingroup\$ Maybe I'm wrong about this, but shouldn't you be stretching various triangles and not literally moving them? The first picture looks awesome though. \$\endgroup\$ – Krythic Feb 18 '16 at 17:21
  • \$\begingroup\$ @Soapy That doesn't matter, the arrays are only populated by the values that actually affect a vertex, so if a vertex is affected by less than 4 bones, the remaining weights are equal to 0, thus Bones[0] * 0 == 0. \$\endgroup\$ – Yattabyte Feb 18 '16 at 17:24
  • \$\begingroup\$ @Krythic I don't quite understand what you mean. If a bone moves, the triangles should move. \$\endgroup\$ – Yattabyte Feb 18 '16 at 17:29
  • \$\begingroup\$ I will admit, I am naive on the subject; my engine is simple and just moves through animation poses to complete an animation. My comment was directed at the fact that your images show obvious triangle separation. I was always under the impression that triangles were stretched into various positions with bones facilitating in the current transformation position, rotation, etc. \$\endgroup\$ – Krythic Feb 18 '16 at 17:33
3
\$\begingroup\$

All of my issues boiled down to misusing the model's index array.

At first I purposely avoided using the model's indices because I wanted to have per vertex normals rather than per face normals. However, I completely forgot that the vertex list may not correspond to the correct rendering order, which lead to the results in my opening post.

On my way to solving this I had additional problems, which I unfortunately did not save any images of. Since I needed the model's indices but didn't want to render using an index list, I needed to create a modified vertex/uv/normal/tangent list which would contain their respective information but in the order of the index list:

vector<int> &indices = getIndices();
vector<vec3> &oldVertices = getOldVertices();

int total = indices.size();

vector<vec3> newVertices;
newVerticies.reserve(total);

for (int x = 0; x < total; ++x) {
    newVertices.push_Back(oldVertices[indicies[x]]);
}

Just doing this alone for each attribute is not enough! Since we are dealing with animation, we also have per-vertex bone information, so that list also needed to get adjusted the same way.

These changes alone solved my problem.

\$\endgroup\$
  • \$\begingroup\$ After picking up a serious amount of modeling recently, I found myself recalling this post when I started to merge vertices together in order to reduce the vertice count and make everything more uniform. I realized then that your issue may actually be partly due to the fact that your triangles don't actually connect to the vertices of other nearby faces. This looks to be true given the pictures you provided. Perhaps you could also look into merging your vertices so that triangles share the same vertices? Just an idea. \$\endgroup\$ – Krythic May 9 '16 at 2:04
  • \$\begingroup\$ I did totally solve the problem, it was a foolish mistake on my part, as I made a fundamental assumption error. \$\endgroup\$ – Yattabyte May 9 '16 at 2:08
  • \$\begingroup\$ Alright, man. Just wanted to share my most recent thoughts with you. It's crazy that Blender doesn't actually merge vertices together with local ones, even if they share the same vector...you literally can't tell it's happening either. Good luck on your project. \$\endgroup\$ – Krythic May 9 '16 at 2:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.