Learning record of source code analysis - skin

Posted by elacdude on Thu, 11 Nov 2021 11:22:56 +0100

2021SC@SDUSC

Skin algorithm

Bone skinning algorithm, also known as bone skin binding method or bone subspace deformation method (SSD), is an important part of bone animation. It is a method to study how bones drive skin mesh movement and update skin vertex position in real time. When the model skeleton moves, we can make the vertices on the skin mesh do the same movement with the bones, which produces the deformation effect of the skin mesh.

Rigid binding algorithm

The rigid binding algorithm uses the skeleton at the bottom to carry the motion, and each bone joint controls a skin vertex to obtain the new position information after the vertex transformation of the skin mesh. Skin and vertices are controlled one-on-one.
Rigid binding algorithm formula:

  • 5: Position in the world coordinate system before vertex transformation
  • V is transformed into the displacement vector from the initial position of the skin vertex to the initial position of the associated joint through the matrix Li, and then the new position V 'of the skin vertex in the world coordinate system is obtained through the absolute transformation matrix Mi of the bone

Flexible binding algorithm

Dust3D should adopt the flexible binding algorithm. The biggest difference between Dust3D and the rigid binding algorithm is that each skin vertex may be affected by one or more bone joints. When determining the new position of the skin vertex after transformation, it needs to be jointly determined by these affected bone joints.

Mesh structure is a collection of vertices, which stores vertex sequence, triangular patch index, weight value, texture index and other information. Among them, weight value information is directly related to skin. The function of skin information is to make each node exert influence on the vertices at the same time according to different weight. According to the influence of each node, the effect on the model is that multiple nodes pull on the vertices in the skin mesh. During modeling, a reasonable weight value of mesh in different areas is specified to make the mesh produce a smooth transition at the connection, Instead of the rigid fracture in the joint animation, so as to avoid the generation of cracks.

SkinnedMeshCreator

SkinnedMeshCreator::SkinnedMeshCreator(const Object &object,
        const std::map<int, RigVertexWeights> &resultWeights) :
    m_object(object),
    m_resultWeights(resultWeights)
{
    m_verticesOldIndices.resize(m_object.triangles.size());
    m_verticesBindNormals.resize(m_object.triangles.size());
    m_verticesBindPositions.resize(m_object.triangles.size());
    const std::vector<std::vector<QVector3D>> *triangleVertexNormals = m_object.triangleVertexNormals();
    //For each triangular patch on the original mesh, its index, vertex index and vertex normal are stored
    for (size_t triangleIndex = 0; triangleIndex < m_object.triangles.size(); triangleIndex++) {
        for (int j = 0; j < 3; j++) {
            int oldIndex = m_object.triangles[triangleIndex][j];
            m_verticesOldIndices[triangleIndex].push_back(oldIndex);
            m_verticesBindPositions[triangleIndex].push_back(m_object.vertices[oldIndex]);
            if (nullptr != triangleVertexNormals)
                m_verticesBindNormals[triangleIndex].push_back((*triangleVertexNormals)[triangleIndex][j]);
            else
                m_verticesBindNormals[triangleIndex].push_back(QVector3D());
        }
    }
    
    //The fill color of the triangle patch
    std::map<std::pair<QUuid, QUuid>, QColor> sourceNodeToColorMap;
    for (const auto &node: object.nodes)
        sourceNodeToColorMap.insert({{node.partId, node.nodeId}, node.color});
    
    m_triangleColors.resize(m_object.triangles.size(), Theme::white);
    const std::vector<std::pair<QUuid, QUuid>> *triangleSourceNodes = object.triangleSourceNodes();
    if (nullptr != triangleSourceNodes) {
        for (size_t triangleIndex = 0; triangleIndex < m_object.triangles.size(); triangleIndex++) {
            const auto &source = (*triangleSourceNodes)[triangleIndex];
            m_triangleColors[triangleIndex] = sourceNodeToColorMap[source];
        }
    }
}

createMeshFromTransform

Linear hybrid skinning algorithm is the most commonly used skinning algorithm. It obtains a series of new positions of a vertex under the influence of each bone, and then calculates the weighted average of these position data to obtain the final result:

Where, V represents the position in the world coordinate system before the vertex transformation, V 'represents the position after the vertex transformation, i represents the number of bones that simultaneously affect the vertex, and wi represents the influence weight exerted by the ith bone on the vertex, taking a value between 0 and 1; M represents the position related to vertices under the initial reference pose of the model
The transformation matrix of the ith bone from local coordinates to world coordinates. Through the matrix Mi, the bone i can be changed from the initial position to the new position when the animation data comes, Mi × V represents the position of V under the separate influence of bone i.

Model *SkinnedMeshCreator::createMeshFromTransform(const std::vector<QMatrix4x4> &matricies)
{
    std::vector<std::vector<QVector3D>> transformedPositions = m_verticesBindPositions;
    std::vector<std::vector<QVector3D>> transformedPoseNormals = m_verticesBindNormals;
    
    if (!matricies.empty()) {
        for (size_t i = 0; i < transformedPositions.size(); ++i) {
            for (size_t j = 0; j < 3; ++j) {
                const auto &weight = m_resultWeights[m_verticesOldIndices[i][j]];
                QMatrix4x4 mixedMatrix;
                transformedPositions[i][j] = QVector3D();
                transformedPoseNormals[i][j] = QVector3D();
                for (int x = 0; x < MAX_WEIGHT_NUM; x++) {
                    float factor = weight.boneWeights[x];
                    if (factor > 0) {
                        //Linear hybrid skin formula
                        transformedPositions[i][j] += matricies[weight.boneIndices[x]] * m_verticesBindPositions[i][j] * factor;
                        transformedPoseNormals[i][j] += matricies[weight.boneIndices[x]] * m_verticesBindNormals[i][j] * factor;
                    }
                }
            }
        }
    }
    
    ShaderVertex *triangleVertices = new ShaderVertex[m_object.triangles.size() * 3];
    int triangleVerticesNum = 0;
    for (size_t triangleIndex = 0; triangleIndex < m_object.triangles.size(); triangleIndex++) {
        for (int i = 0; i < 3; i++) {
            //Transformed Mesh information
            ShaderVertex &currentVertex = triangleVertices[triangleVerticesNum++];
            const auto &sourcePosition = transformedPositions[triangleIndex][i];
            const auto &sourceColor = m_triangleColors[triangleIndex];
            const auto &sourceNormal = transformedPoseNormals[triangleIndex][i];
            currentVertex.posX = sourcePosition.x();
            currentVertex.posY = sourcePosition.y();
            currentVertex.posZ = sourcePosition.z();
            currentVertex.texU = 0;
            currentVertex.texV = 0;
            currentVertex.colorR = sourceColor.redF();
            currentVertex.colorG = sourceColor.greenF();
            currentVertex.colorB = sourceColor.blueF();
            currentVertex.normX = sourceNormal.x();
            currentVertex.normY = sourceNormal.y();
            currentVertex.normZ = sourceNormal.z();
            currentVertex.metalness = Model::m_defaultMetalness;
            currentVertex.roughness = Model::m_defaultRoughness;
        }
    }
    
    return new Model(triangleVertices, triangleVerticesNum);
}