C++ Vertex Skinning Example

Hello fellow Blender users, I have started learning C++ and OpenGL.

Now I try to learn more about vertex skinning. I want to make a simple application that does vertex skinning.
(If you wonder why it worth the trouble then the answer is for educational purposes mostly, then everything else.)

Check down there on Vertex Skinning section.


#include <iostream>
#include <array>


#include <GL/glew.h>
#include <GLFW/glfw3.h>


#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>


using namespace std;


int main()
{
    // Bone positions
    array<glm::vec3, 3> positions
    {
        glm::vec3(0, -2, 0),
        glm::vec3(0, 2, 0), // Move 2 units up from root (local space)
        glm::vec3(0, 2, 0) // Move 2 units up from 1 (local space)
    };


    // Bone matrices
    array<glm::mat4, 3> bones;


    // Vertex positions
    array<glm::vec3, 6> vertices
    {
        glm::vec3(-0.5f, -2, 0),
        glm::vec3( 0.5f, -2, 0),
        glm::vec3(-0.5f,  0, 0),
        glm::vec3( 0.5f,  0, 0),
        glm::vec3(-0.5f,  2, 0),
        glm::vec3( 0.5f,  2, 0)
    };


    // Vertex bone ids
    array<int, 6> vertBoneIDs = { 0, 0, 1, 1, 2, 2 };


    // Vertex bone weights
    array<float, 6> vertBoneWeights = { 1, 1, 1, 1, 1, 1 };


    // Edges
    array<int, 16> indices =
    {
        0, 1, 1, 3, 3, 2, 2, 0,
        2, 3, 3, 5, 5, 4, 4, 2
    };


    //{ Setup window
    int width = 800;
    int height = 600;
    glfwInit();
    GLFWwindow* window;
    window = glfwCreateWindow(width, height, "GLFW", NULL, NULL);
    glfwMakeContextCurrent(window);
    // Camera
    glm::mat4 matrixProj = glm::perspective(45.0f, (float)width/height, 0.1f, 100.0f);
    glm::mat4 matrixView = glm::lookAt(glm::vec3(0, 0, 10), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
    //}


    while (!glfwWindowShouldClose(window))
    {
        glClear(GL_COLOR_BUFFER_BIT);
        glLoadMatrixf(glm::value_ptr(matrixProj * matrixView));


        //{ Bone transformations
        // Prepare some values
        float sinValue = glm::sin(glfwGetTime());
        float cosValue = glm::cos(glfwGetTime());
        float rotValue = sinValue * 100;
        glm::vec3 rotVec = glm::vec3(0, 0, 1);


        // Base bone
        bones[0] = glm::translate(positions[0]);
        bones[0] *= glm::rotate(rotValue*0.25f, rotVec);


        // Secondary bone
        bones[1] = bones[0];
        bones[1] *= glm::translate(positions[1]);
        bones[1] *= glm::rotate(rotValue*0.5f, rotVec);


        // Third bone
        bones[2] = bones[1];
        bones[2] *= glm::translate(positions[2]);
        //}


        //{ Vertex skinning
        for (int i = 0; i < vertices.size(); i++)
        {
            // Something wrong goes here...
//            glm::mat4 v =
//                glm::translate(vertices[i])
//                * bones[vertBoneIDs[i]];


//            vertices[i] = glm::vec3(v[3].x, v[3].y, v[3].z);
        }
        //}


        //{ Drawing


        // Bones
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINE_STRIP);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Bone joints
        glPointSize(10.0f);
        glColor3f(1.0f, 0.0f, 0.0f);
        glBegin(GL_POINTS);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Edges
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINES);
        for (int i = 0; i < indices.size(); i+=2)
        {
            glm::vec3 a = vertices[indices[i]];
            glm::vec3 b = vertices[indices[i+1]];
            glVertex3f(a.x, a.y, a.z);
            glVertex3f(b.x, b.y, b.z);
        }
        glEnd();
        //}


        glfwSwapBuffers(window);
        glfwPollEvents();
    }


    glfwDestroyWindow(window);
    glfwTerminate();


    return 0;
}



Currently keeping things as simple as possible everything is done in immediate mode and is very hard coded, but later it will be improved and many more features will be added. I have looked at many sources on Github but I don’t understand how they work, I want only a simple solution.

At the very least, you need a place to store the skinned vertex positions that is separate from the static posture.

You are directly updating your ‘vertices’ array inside your skinning loop. Each frame, the vertex positions will have the bone transformations reapplied. For example:

Suppose you have 1 vertex at (0, 0, 0) and you have a bone transform that moved it 1 unit down the X axis (1, 0, 0).

Frame 1:
The point would be moved from (0,0,0) to (1,0,0)

Frame 2:
Because you updated your ‘vertices’ array with the transformed value the point will move from (1,0,0) to (2,0,0)

Frame 3:
Moves from (2,0,0) to (3,0,0).

This progressive application of the transforms will blow-up your vertex positions.

You need another array similar to your ‘vertices’ array where you store the locations after the bone transformation.


 // Vertex positions
    array<glm::vec3, 6> vertices
    {
        glm::vec3(-0.5f, -2, 0),
        glm::vec3( 0.5f, -2, 0),
        glm::vec3(-0.5f,  0, 0),
        glm::vec3( 0.5f,  0, 0),
        glm::vec3(-0.5f,  2, 0),
        glm::vec3( 0.5f,  2, 0)
    };

// Skinned vertex positions
array<glm::vec3, 6> skinnedVertices
{
glm::vec3(-0.5f, -2, 0),
glm::vec3( 0.5f, -2, 0),
glm::vec3(-0.5f, 0, 0),
glm::vec3( 0.5f, 0, 0),
glm::vec3(-0.5f, 2, 0),
glm::vec3( 0.5f, 2, 0)
};


Then in your skinning loop you do:


        //{ Vertex skinning
        for (int i = 0; i < vertices.size(); i++)
        {
            skinnedVertices[i] = bones[vertBoneIDs[i]] * vertices[i];
        }
        //}

I have not used the ‘glm’ vertices before but your code looks incorrect to me. I highly suspect that it is designed to do “matrix * vector”.

When you render, you use the ‘skinnedVertices’ array instead of the raw ‘vertices’ array. There might be other problems in your code. I have to run or tested any of this.

Hello kastoria, thanks for dropping by, you suggestions helped a lot because most tricky parts of the code worked.

  1. you need a place to store the skinned vertex positions
    That is correct, I saw that in most examples they either did the drawing right on the fly so they never stored anything. Also other examples that used vbos means that they store the vertices in GPU memory and they leave them intact while all the effort goes to the shader.

  2. I highly suspect that it is designed to do “matrix * vector”.
    I have not used glm enough to know the ins and outs of it, but as it seems they only way to multiply matrix by vector is only through vector4.

  3. When you render, you use the ‘skinnedVertices’ array instead of the raw ‘vertices’ array.
    The rendering code seems to be OK but if there’s any mistake feedback is welcomed.

Now a problem remains to solve, is to double check how bone transformations are handled because something fishy going on there.

As you can see in this image if the root bone (bone[0]) gets rotated then it makes the whole vertices slide away (look at the part on the right portion of the image).
.

So I comment out the rotation part to prevent things from going wrong (left part of the image), however I will keep an eye in case there is a solution to this problem, currently browsing scenegraph source codes in order to find how node transformations are handled.

#include <iostream>#include <array>


#include <GL/glew.h>
#include <GLFW/glfw3.h>


#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>


using namespace std;


int main()
{
    // Bone positions
    array<glm::vec3, 3> positions
    {
        glm::vec3(0, -2, 0),
        glm::vec3(0, 2, 0), // Move 2 units up from root (local space)
        glm::vec3(0, 2, 0) // Move 2 units up from 1 (local space)
    };


    // Bone matrices
    array<glm::mat4, 3> bones;


    // Vertex positions
    array<glm::vec3, 6> vertices
    {
        glm::vec3(-0.5f, -2, 0),
        glm::vec3( 0.5f, -2, 0),
        glm::vec3(-0.5f,  0, 0),
        glm::vec3( 0.5f,  0, 0),
        glm::vec3(-0.5f,  2, 0),
        glm::vec3( 0.5f,  2, 0)
    };


    array<glm::vec3, 6> verticesSkinned;


    // Vertex bone ids
    array<int, 6> vertBoneIDs = { 0, 0, 1, 1, 2, 2 };


    // Vertex bone weights
    array<float, 6> vertBoneWeights = { 1, 1, 1, 1, 1, 1 };


    // Edges
    array<int, 16> indices =
    {
        0, 1, 1, 3, 3, 2, 2, 0,
        2, 3, 3, 5, 5, 4, 4, 2
    };


    //{ Setup window
    int width = 800;
    int height = 600;
    glfwInit();
    GLFWwindow* window;
    window = glfwCreateWindow(width, height, "GLFW", NULL, NULL);
    glfwMakeContextCurrent(window);
    // Camera
    glm::mat4 matrixProj = glm::perspective(45.0f, (float)width/height, 0.1f, 100.0f);
    glm::mat4 matrixView = glm::lookAt(glm::vec3(0, 0, 10), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
    //}


    while (!glfwWindowShouldClose(window))
    {
        glClear(GL_COLOR_BUFFER_BIT);
        glLoadMatrixf(glm::value_ptr(matrixProj * matrixView));


        //{ Bone transformations
        // Prepare some values
        float sinValue = glm::sin(glfwGetTime());
        float cosValue = glm::cos(glfwGetTime());
        float rotValue = sinValue * 100;


        glm::vec3 rotVec = glm::vec3(0, 0, 1);


        // Base bone
        bones[0] = glm::translate(positions[0]);
        //bones[0] *= glm::rotate(rotValue*0.25f, rotVec);
        // Secondary bone
        bones[1] = bones[0];
        bones[1] *= glm::translate(positions[1]);
        bones[1] *= glm::rotate(rotValue*0.5f, rotVec);
        // Third bone
        bones[2] = bones[1];
        bones[2] *= glm::translate(positions[2]);
        //}


        //{ Vertex skinning
        for (int i = 0; i < verticesSkinned.size(); i++)
        {
            glm::mat4 bone = bones[vertBoneIDs[i]];
            glm::vec4 vert = glm::vec4(vertices[i], 0);
            glm::vec4 v = bone * vert;
            verticesSkinned[i] = glm::vec3(v.x, v.y, v.z);
        }
        //}


        //{ Drawing


        // Bones
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINE_STRIP);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Bone joints
        glPointSize(10.0f);
        glColor3f(1.0f, 0.0f, 0.0f);
        glBegin(GL_POINTS);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Edges
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINES);
        for (int i = 0; i < indices.size(); i+=2)
        {
            glm::vec3 a = verticesSkinned[indices[i]];
            glm::vec3 b = verticesSkinned[indices[i+1]];
            glVertex3f(a.x, a.y, a.z);
            glVertex3f(b.x, b.y, b.z);
        }
        glEnd();
        //}


        glfwSwapBuffers(window);
        glfwPollEvents();
    }


    glfwDestroyWindow(window);
    glfwTerminate();


    return 0;
}



When you rotate the root bone, does your skeleton leave your mesh or does your mesh slide off the skeleton?

I have found that rotating the root bone is bad idea, as you say there is mesh sliding, but for translations there’s no problem.

Here is the status up until now. Everything goes well so far, one thing as you see is that skinned vertices are transformed too far from their places and I look into figuring out why this happens.



#include <iostream>
#include <array>


#include <GL/glew.h>
#include <GLFW/glfw3.h>


#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>


using namespace std;


int main()
{
    // Bone positions
    array<glm::vec3, 3> positions
    {
        glm::vec3(0, -2, 0),
        glm::vec3(0, 2, 0),
        glm::vec3(0, 2, 0)
    };


    // Bone matrices
    array<glm::mat4, 3> bones;


    // Vertex positions
    array<glm::vec3, 6> vertices
    {
        glm::vec3(-0.5f, -2, 0),
        glm::vec3( 0.5f, -2, 0),
        glm::vec3(-0.5f,  0, 0),
        glm::vec3( 0.5f,  0, 0),
        glm::vec3(-0.5f,  2, 0),
        glm::vec3( 0.5f,  2, 0)
    };


    array<glm::vec3, 6> verticesSkinned;


    // Vertex bone ids
    array<int, 6> vertBoneIDs = { 0, 0, 1, 1, 2, 2 };


    // Vertex bone weights
    array<float, 6> vertBoneWeights = { 1, 1, 1, 1, 1, 1 };


    // Edges
    array<int, 16> indices =
    {
        0, 1, 1, 3, 3, 2, 2, 0,
        2, 3, 3, 5, 5, 4, 4, 2
    };


    //{ Setup window
    int width = 800;
    int height = 600;
    glfwInit();
    GLFWwindow* window;
    window = glfwCreateWindow(width, height, "GLFW", NULL, NULL);
    glfwMakeContextCurrent(window);
    // Camera
    glm::mat4 matrixProj = glm::perspective(45.0f, (float)width/height, 0.1f, 100.0f);
    glm::mat4 matrixView = glm::lookAt(glm::vec3(0, 0, 10), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
    //}


    while (!glfwWindowShouldClose(window))
    {
        glClear(GL_COLOR_BUFFER_BIT);
        glLoadMatrixf(glm::value_ptr(matrixProj * matrixView));


        //{ Bone transformations
        // Prepare some values
        float sinValue = glm::sin(glfwGetTime());
        float cosValue = glm::cos(glfwGetTime());
        float rotValue = sinValue * 100;
        glm::vec3 rotVec = glm::vec3(0, 0, 1);


        // Base bone
        bones[0] = glm::translate(positions[0]);
        bones[0] *= glm::translate(glm::vec3(sinValue, 0, 0));
        // Secondary bone
        bones[1] = bones[0];
        bones[1] *= glm::translate(positions[1]);
        bones[1] *= glm::rotate(rotValue*0.25f, rotVec);


        // Third bone
        bones[2] = bones[1];
        bones[2] *= glm::translate(positions[2]);


        //}


        //{ Vertex skinning
        for (int i = 0; i < verticesSkinned.size(); i++)
        {
            glm::mat4 bone = bones[vertBoneIDs[i]];
            glm::vec4 vert = glm::vec4(vertices[i], 1);
            glm::vec4 v = bone * vert;
            verticesSkinned[i] = glm::vec3(v.x, v.y, v.z);
        }
        //}


        //{ Drawing


        // Bones
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINE_STRIP);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Bone joints
        glPointSize(10.0f);
        glColor3f(1.0f, 0.0f, 0.0f);
        glBegin(GL_POINTS);
        for (int i = 0; i < bones.size(); i++)
            glVertex2f((bones[i])[3].x, (bones[i])[3].y);
        glEnd();


        // Edges
        glPointSize(1.0f);
        glColor3f(1.0f, 1.0f, 1.0f);
        glBegin(GL_LINES);
        for (int i = 0; i < indices.size(); i+=2)
        {
            glm::vec3 a = verticesSkinned[indices[i]];
            glm::vec3 b = verticesSkinned[indices[i+1]];
            glVertex3f(a.x, a.y, a.z);
            glVertex3f(b.x, b.y, b.z);
        }
        glEnd();
        //}


        glfwSwapBuffers(window);
        glfwPollEvents();
    }


    glfwDestroyWindow(window);
    glfwTerminate();


    return 0;
}


Here’s a Windows Codeblocks MinGW project setup for testing purposes.

Libraries used: GLEW, GLFW, GLM (these are not included)

The nice think is that you are doing all this on the CPU so it should be very simple to debug the code. You have a limited number of bones and vertices so and should be able to calculate by hand where your vertices should be located. You should be able to find the problem if you do the following steps:

  1. Compute a vertex position by hand and keep all your work.
  2. Step though your code and see if you have implemented your work correctly.