Mesh Skinning
April 6, 2022About 4 min
Mesh Skinning
当一个网格和一个骨骼被创建。每个顶点会分配给一个或多个骨骼,这个过程叫做绑定(Rigging)。骨骼被创建时的姿势叫做绑定姿势(bind pose),这个姿势把骨骼正好融合在网格内部。

这里我认为绑定姿势也就是我们在软件中编辑动画的基础姿势。骨骼和蒙皮之间的关系是配置好了的,如权重等。换句话说就是骨骼和蒙皮之间的距离是固定好了的。我们在计算其他姿势时就可以获得这个距离来计算其他姿势下蒙皮的 位置,因为骨骼的位置就是姿势的样子,在模型文件中已经存了数据。
Understand how a skinned mesh is different from a non-skinnedmesh
蒙皮是处理决定那个顶点应该被分配到那个骨骼上。一个顶点可以影响到多个骨骼。
- Rig skinning:每个顶点对应一个骨骼,把一个顶点乘以多个矩阵得到最后的坐标。在关节处不能自然的弯曲。通过将三角形的呃呃不同顶点分配给不同的骨头,可以避免在关节处(如肘部)的网格断裂。这样造成网格不能保持好体积,这样看起来很奇怪
- Smooth skinning:顶点对应着多个骨骼。超过一个骨骼可以影响到一个顶点。每个影响都有一个权重,这个权重用于混合顶点。下图就是同一顶点影响两个骨骼的情况,我们取0.5的权重值。
Understand the entire skinning pipeline
普通顶点管线和绑定蒙皮顶点关系对比:
我们需要明白一个目的,我们最终需要呈现在屏幕中是蒙皮,也就是说我们要渲染的是蒙皮。而在模型文件我们一般是存储的骨骼节点的位置,所以我们需要实时根据骨骼节点位置去计算出对应蒙皮三角形中顶点的位置。
这就是为什们我们需要增加两个步骤的原因。这里为什么我们乘以绑定矩阵的逆就可以蒙皮空的坐标呢?
我们可以这样思考,把姿势看作矩阵:
- 把绑定姿势作为基础姿势
B
,其他姿势C
都可以通过绑定姿势乘以一个矩阵M
得到,那么我们就可以得到公式:C
=B
*M
。 - 现在有一个姿势
C
,并且已知绑定姿势B
,就可以求到M
=C
* inverse(B
) - 再使用
M
乘以蒙皮的顶点,就得到蒙皮点在这个姿势下模型空间位置。 - 后面就是可以一样的计算了。
Implement a skeleton class
所有的绑定姿势和绑定姿势的逆都共享给所有游戏场景中的角色。
Load the bind pose of a skeleton from a glTF file
- 加载rest姿势
- 读取有多少个
cgltf_skin
- 从每个皮肤中读取逆矩阵
inverse_bind_matrices
- 对逆矩阵取逆就可以获得节点的世界坐标
- 再根据世界坐标求到每个节点的本地坐标
Implement a skinned mesh class
Load skinned meshes from a gLTF file
加载文件后需要把Mesh中的Attribute设置,其中有:
- positions
- normals
- texCoords
- influences
- weights
Implement CPU skinning
// skin a vertex is to linearly blend matrices into a single skin matrix
// then transform the vertex by this skin matrix
void Mesh::CPUSkin(Skeleton& skeleton, Pose& pose)
{
unsigned int numVerts = (unsigned int)mPosition.size();
if (numVerts == 0)
{
return;
}
mSkinnedPosition.resize(numVerts);
mSkinnedNormal.resize(numVerts);
pose.GetMatrixPalette(mPosePalette);
std::vector<mat4> invBindPosePalette = skeleton.GetInvBindPose();
for (unsigned int i = 0; i < numVerts; i++)
{
ivec4& j = mInfluences[i];
vec4& w = mWeights[i];
mat4 m0 = (mPosePalette[j.x] * invBindPosePalette[j.x]) * w.x;
mat4 m1 = (mPosePalette[j.y] * invBindPosePalette[j.y]) * w.y;
mat4 m2 = (mPosePalette[j.z] * invBindPosePalette[j.z]) * w.z;
mat4 m3 = (mPosePalette[j.w] * invBindPosePalette[j.w]) * w.w;
mat4 skin = m0 + m1 + m2 + m3;
mSkinnedPosition[i] = transformPoint(skin, mPosition[i]);
mSkinnedNormal[i] = transformPoint(skin, mNormal[i]);
}
mPosAttrib->Set(mSkinnedPosition);
mNormAttrib->Set(mNormal);
}
Implement GPU skinning
使用顶点着色器实现蒙皮:
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
in vec3 position;
in vec3 normal;
in vec2 texCoord;
in vec4 weights;
in ivec4 joints;
uniform mat4 pose[120];
uniform mat4 invBindPose[120];
out vec3 norm;
out vec3 fragPos;
out vec2 uv;
void main()
{
mat4 skin = (pose[joints.x] * invBindPose[joints.x]) * weights.x;
skin += (pose[joints.y] * invBindPose[joints.y]) * weights.y;
skin += (pose[joints.z] * invBindPose[joints.z]) * weights.z;
skin += (pose[joints.w] * invBindPose[joints.w]) * weights.w;
gl_Position = projection * view * model * skin * vec4(position, 1.0);
fragPos = vec3(model * skin * vec4(position, 1.0));
norm = vec3(model * skin * vec4(normal, 0.0f));
uv = texCoord;
}