2021SC@SDUSC
Bones determine the position and orientation of the whole model in the world coordinate system. When rendering a static model, because the vertices of the model are defined in the model coordinate system, it is only necessary to transform the model coordinate system of each vertex to the world coordinate system. For bone animation, we set the position and orientation of the model, which is actually setting the position and orientation of the root bone, and then calculate the position and orientation of each bone according to the transformation information between parent and child bones in the bone hierarchy.
Bone data structure
The skeleton of any type of character, whether human, upright or reptile, can be described as a tree structure gradually expanded with a joint as the root node. The whole skeleton is a tree structure description containing parent-child relationship.
Dust3D provides a unified bone tissue form for (lactating) animals, which is divided into Neck, Limb, Tail and Joint marks. Users can choose the bone type of "Animal" when using. For non biological or other irregular bones, you can use Joint to customize the marking of Joint parts.
//Bone marker enum class BoneMark { None = 0, Neck, Limb, Tail, Joint, Count };
Bone class, used to define a controllable bone.
class RigBone { public: QString name; int index = -1; int parent = -1; //Position of bone head QVector3D headPosition; //Position of the bone tail QVector3D tailPosition; //Bone head node radius float headRadius = 0.0; //Bone tail node radius float tailRadius = 0.0; QColor color; std::map<QString, QString> attributes; //Vector pointing to child bones std::vector<int> children; };
The joint node class stores the parent node index, child node index, current node position and transformation information.
bindMatrix: Matrix4 (usually 4x4 matrix) representing the basic transformation of bones. Bind the skeleton to a skin mesh, and its inverse matrix invertbindmatrix will also be calculated.
JointNodeTree stores the RigBone and JointNode sequences.
struct JointNode { //Parent node index int parentIndex; QString name; //Node location QVector3D position; //Transform information QVector3D bindTranslation; QVector3D translation; QQuaternion rotation; QMatrix4x4 bindMatrix; QMatrix4x4 inverseBindMatrix; //Child node index std::vector<int> children; }; class JointNodeTree { public: const std::vector<JointNode> &nodes() const; JointNodeTree(const std::vector<RigBone> *resultRigBones); void updateRotation(int index, const QQuaternion &rotation); void updateTranslation(int index, const QVector3D &translation); void updateMatrix(int index, const QMatrix4x4 &matrix); private: std::vector<JointNode> m_boneNodes; };
updateMatrix
We start from a vertex, assuming that the vertex is affected by bone2. Bone B has a parent node bone A. We know the bindmatrix matrix from bone B to its parent node bone a and the transformation matrix from bone B to the world coordinate system. This matrix will help us transform the mesh vertex in the world coordinate system into the coordinate system of bone B, And the transformation matrix from bone a to the world coordinate system, then the final position of a vertex = the transformation matrix from bone a to the world coordinate system × bindMatrix × Transformation matrix from skeleton a to world coordinate system × Original location.
void JointNodeTree::updateMatrix(int index, const QMatrix4x4 &matrix) { const QMatrix4x4 &localMatrix = matrix; updateTranslation(index, QVector3D(localMatrix(0, 3), localMatrix(1, 3), localMatrix(2, 3))); //Convert local coordinates to world coordinates float scalar = std::sqrt(std::max(0.0f, 1.0f + localMatrix(0, 0) + localMatrix(1, 1) + localMatrix(2, 2))) / 2.0f; float x = std::sqrt(std::max(0.0f, 1.0f + localMatrix(0, 0) - localMatrix(1, 1) - localMatrix(2, 2))) / 2.0f; float y = std::sqrt(std::max(0.0f, 1.0f - localMatrix(0, 0) + localMatrix(1, 1) - localMatrix(2, 2))) / 2.0f; float z = std::sqrt(std::max(0.0f, 1.0f - localMatrix(0, 0) - localMatrix(1, 1) + localMatrix(2, 2))) / 2.0f; x *= x * (localMatrix(2, 1) - localMatrix(1, 2)) > 0 ? 1 : -1; y *= y * (localMatrix(0, 2) - localMatrix(2, 0)) > 0 ? 1 : -1; z *= z * (localMatrix(1, 0) - localMatrix(0, 1)) > 0 ? 1 : -1; float length = std::sqrt(scalar * scalar + x * x + y * y + z * z); updateRotation(index, QQuaternion(scalar / length, x / length, y / length, z / length)); }
Establish bone tree structure
JointNodeTree::JointNodeTree(const std::vector<RigBone> *resultRigBones) { if (nullptr == resultRigBones || resultRigBones->empty()) return; m_boneNodes.resize(resultRigBones->size()); m_boneNodes[0].parentIndex = -1; //Link the elements in the RigBones sequence for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) { const auto &bone = (*resultRigBones)[i]; auto &node = m_boneNodes[i]; node.name = bone.name; node.position = bone.headPosition; QMatrix4x4 parentMatrix; //When the current node is the root node, the transformation information is the node position if (-1 == node.parentIndex) { node.bindTranslation = node.position; } else { //Otherwise, the transformation information from the child node to the parent node is calculated const auto &parentNode = m_boneNodes[node.parentIndex]; node.bindTranslation = node.position - parentNode.position; parentMatrix = parentNode.bindMatrix; } QMatrix4x4 translationMatrix; translationMatrix.translate(node.bindTranslation); node.bindMatrix = parentMatrix * translationMatrix; //Calculate the inverse of the transformation matrix node.inverseBindMatrix = node.bindMatrix.inverted(); node.children = bone.children; //Sets the parent node index of the child node of the current node for (const auto &childIndex: bone.children) m_boneNodes[childIndex].parentIndex = i; } }
Create a linked list of bone nodes
void RigGenerator::buildBoneNodeChain() { std::vector<std::tuple<size_t, std::unordered_set<size_t>, bool>> segments; std::unordered_set<size_t> middle; size_t middleStartNodeIndex = m_object->nodes.size(); for (size_t nodeIndex = 0; nodeIndex < m_object->nodes.size(); ++nodeIndex) { const auto &node = m_object->nodes[nodeIndex]; if (!BoneMarkIsBranchNode(node.boneMark)) //If the current node is the node of the skeleton branch, it enters the next cycle continue; m_branchNodesMapByMark[(int)node.boneMark].push_back(nodeIndex); if (BoneMark::Neck == node.boneMark) { //Node marked Neck if (middleStartNodeIndex == m_object->nodes.size()) middleStartNodeIndex = nodeIndex; } else if (BoneMark::Tail == node.boneMark) { //Node marked Tail middleStartNodeIndex = nodeIndex; } std::unordered_set<size_t> left; std::unordered_set<size_t> right; //The child nodes are divided into two trees according to the node index splitByNodeIndex(nodeIndex, &left, &right); if (left.size() > right.size()) //Control the height difference between the left and right, and always the height of the left subtree std::swap(left, right); for (const auto &it: right) middle.insert(it); segments.push_back(std::make_tuple(nodeIndex, left, false)); } for (const auto &it: segments) { const auto &nodeIndex = std::get<0>(it); const auto &left = std::get<1>(it); for (const auto &it: left) middle.erase(it); middle.erase(nodeIndex); } middle.erase(middleStartNodeIndex); if (middleStartNodeIndex != m_object->nodes.size()) //Establish tuples for the root node and the remaining middle sequence segments.push_back(std::make_tuple(middleStartNodeIndex, middle, true)); for (const auto &it: segments) { const auto &fromNodeIndex = std::get<0>(it); const auto &left = std::get<1>(it); const auto &isSpine = std::get<2>(it); const auto &fromNode = m_object->nodes[fromNodeIndex]; std::vector<std::vector<size_t>> boneNodeIndices; std::unordered_set<size_t> visited; size_t attachNodeIndex = fromNodeIndex; //Recursive collection of bone nodes collectNodesForBoneRecursively(fromNodeIndex, &left, &boneNodeIndices, 0, &visited); //Find the end of the limb node if (BoneMark::Limb == fromNode.boneMark) { for (const auto &neighbor: m_neighborMap[fromNodeIndex]) { if (left.find(neighbor) == left.end()) { attachNodeIndex = neighbor; break; } } } //Store (root node, bone node, spine node, end node) tuple sequence m_boneNodeChain.push_back({fromNodeIndex, boneNodeIndices, isSpine, attachNodeIndex}); } for (size_t i = 0; i < m_boneNodeChain.size(); ++i) { const auto &chain = m_boneNodeChain[i]; const auto &node = m_object->nodes[chain.fromNodeIndex]; const auto &isSpine = chain.isSpine; //Store the nodes with corresponding tags in the corresponding sequence if (isSpine) { m_spineChains.push_back(i); continue; } if (BoneMark::Neck == node.boneMark) { m_neckChains.push_back(i); } else if (BoneMark::Tail == node.boneMark) { m_tailChains.push_back(i); } else if (BoneMark::Limb == node.boneMark) { if (node.origin.x() > 0) { m_leftLimbChains.push_back(i); } else if (node.origin.x() < 0) { m_rightLimbChains.push_back(i); } } } }