【译】glTF教程:蒙皮(二十)
蒙皮
顶点剥皮的过程有点复杂。它将glTF资产中包含的几乎所有元素集合在一起。本节将基于示例:简单蒙皮小节中的示例解释顶点剥皮的基础知识。
几何数据
顶点剥皮示例的几何结构是一个索引三角形网格,由8个三角形和10个顶点组成。它们在xy平面上形成一个矩形,左下点在原点(0,0,0)右上点在(1,2,0)所以顶点的位置是:
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 0.5, 0.0,
1.0, 0.5, 0.0,
0.0, 1.0, 0.0,
1.0, 1.0, 0.0,
0.0, 1.5, 0.0,
1.0, 1.5, 0.0,
0.0, 2.0, 0.0,
1.0, 2.0, 0.0
三角形的指标是
0, 1, 3,
0, 3, 2,
2, 3, 5,
2, 5, 4,
4, 5, 7,
4, 7, 6,
6, 7, 9,
6, 9, 8,
原始数据存储在第一个buffer中。索引和顶点位置由索引0和1上的 bufferView对象定义,对应的索引0和1上的accessor对象提供对这些缓冲区视图的类型访问。图20a显示了这个几何图形,通过轮廓渲染可以更好地显示结构。
这个几何数据包含在唯一网格的网格原语中,这个网格原语连接到场景的主节点。mesh原语包含额外的属性,即"JOINTS_0" an和d "WEIGHTS_0"属性。下面将解释这些属性的用途。
骨架结构
在给定的示例中,有两个节点定义框架。它们被称为“骨骼节点”或“关节节点”,因为它们可以被想象成骨骼之间的关节。skin通过在其 joints属性中列出这些节点的索引来引用它们。
"nodes" : [
...
{
"children" : [ 2 ],
"translation" : [ 0.0, 1.0, 0.0 ]
},
{
"rotation" : [ 0.0, 0.0, 0.0, 1.0 ]
}
],
第一个关节节点有一个translation 属性,定义沿y轴大约1.0的平移。第二个关节节点有一个rotation属性,该属性最初描述了大约0度的旋转(因此,根本没有旋转)。这个旋转稍后将被动画改变,让骨骼左右弯曲,并显示顶点蒙皮的效果。
蒙皮
skin是顶点蒙皮的核心元素。在这个例子中,只有一个皮肤:
"skins" : [
{
"inverseBindMatrices" : 4,
"joints" : [ 1, 2 ]
}
],
皮肤包含一个名为joints的数组,它列出了定义骨架层次结构的节点的索引。此外,皮肤包含对属性inverseBindMatrices中的访问器的引用。这个访问器为每个关节提供一个矩阵。每个矩阵都将几何变换成各自关节的空间。这意味着每个矩阵都是各自关节在初始构型下的全局变换的逆矩阵。在给定的例子中,对于两个关节节点,初始全局变换的逆是相同的:
1.0 0.0 0.0 0.0
0.0 1.0 0.0 -1.0
0.0 0.0 1.0 0.0
0.0 0.0 0.0 1.0
这个矩阵将网格沿y轴平移-1左右,如图20b所示。
乍一看,这种转变可能有违直觉。但是这个变换的目标是“撤销”由各自节点的初始全局变换所做的变换,这样就可以根据节点的实际全局变换来计算节点对网格顶点的影响。
顶点蒙皮实现
现有渲染库的用户几乎不需要手动处理glTF资产中包含的顶点剥皮数据:实际的剥皮计算通常在顶点着色器中进行,这是各个库的底层实现细节。然而,知道顶点剥皮数据应该如何处理可能有助于创建正确、有效的顶点剥皮模型。因此,本节将使用一些伪代码和GLSL中的示例简要总结顶点剥皮的应用。
联合矩阵
蒙皮网格的顶点位置最终由顶点着色器计算。在这些计算中,顶点着色器必须考虑骨架的当前位置,以便计算适当的顶点位置。这个信息以矩阵数组的形式传递给顶点着色器,也就是关节矩阵。这是一个数组,也就是一个uniform变量,它包含一个4乘4矩阵用于骨架的每个关节。在着色器中,这些矩阵结合起来计算每个顶点的实际剥皮矩阵:
...
uniform mat4 u_jointMat[2];
...
void main(void)
{
mat4 skinMat =
a_weight.x * u_jointMat[int(a_joint.x)] +
a_weight.y * u_jointMat[int(a_joint.y)] +
a_weight.z * u_jointMat[int(a_joint.z)] +
a_weight.w * u_jointMat[int(a_joint.w)];
....
}
每个关节的关节矩阵必须对顶点进行以下变换:
- 顶点必须准备好与关节节点的“当前”全局变换进行转换。因此,它们用关节节点的
inverseBindMatrix进行变换。这是关节节点在初始状态时的全局变换的倒数,此时还没有应用动画。 - 顶点必须用关节节点的“当前”全局变换进行变换。加上从
inverseBindMatrix的变换,这将导致顶点的变换仅仅基于节点的当前变换,在当前关节节点的坐标空间中。 - 顶点必须用网格所附节点的全局变换的inverse进行变换,因为这个变换已经用模型-视图-矩阵完成了,因此必须从剥皮计算中取消。
:因此,计算关节“j”的关节矩阵的伪代码如下:
jointMatrix(j) =
globalTransformOfNodeThatTheMeshIsAttachedTo^-1 *
globalTransformOfJointNode(j) *
inverseBindMatrixForJoint(j);
注:顶点蒙皮在其他情况下通常涉及一个矩阵,称为“绑定形状矩阵”。这个矩阵的作用是将蒙皮网格的几何形状转化为关节的坐标空间。在glTF中,这个矩阵被省略,并且假设这个转换或者是与网格数据的预相乘,或者是与逆绑定矩阵的后相乘。
图20c显示了使用关节1的关节矩阵对示例:简单蒙皮示例中的几何图形所做的转换。图像显示的是动画中间状态的转换,即当动画已经修改了关节节点的旋转时,描述的是绕z轴旋转45度左右。
图像20c的最后一个面板显示了如果仅用关节1的关节矩阵进行变换,几何图形会是什么样子。几何图形的这种状态从来都不是真正可见的:在顶点着色器中计算的“实际”几何图形将“组合”这些几何图形,因为它们是根据不同的关节矩阵创建的,基于关节和下面解释的权重。
蒙皮的关节和重量
如上所述,网格原语包含顶点剥皮所需的新属性。特别是 "JOINTS_0"和"WEIGHTS_0"属性。每个属性引用一个访问器,该访问器为网格的每个顶点提供一个数据元素。
"JOINTS_0"属性指的是一个访问器,它包含在剥皮过程中应该对顶点有影响的关节的索引。为了简单和高效,这些指标通常存储为4D向量,限制可能影响顶点的关节数为4。在给定的例子中,关节信息非常简单:
Vertex 0: 0, 1, 0, 0,
Vertex 1: 0, 1, 0, 0,
Vertex 2: 0, 1, 0, 0,
Vertex 3: 0, 1, 0, 0,
Vertex 4: 0, 1, 0, 0,
Vertex 5: 0, 1, 0, 0,
Vertex 6: 0, 1, 0, 0,
Vertex 7: 0, 1, 0, 0,
Vertex 8: 0, 1, 0, 0,
Vertex 9: 0, 1, 0, 0,
这意味着每个顶点都应该受到关节0和关节1的影响。(这里忽略了每个向量的后两个分量。如果有多个关节,那么这个访问器的一个条目可以包含
3, 1, 8, 4,
也就是说对应的顶点应该受到关节3,1,8和4的影响)
属性 "WEIGHTS_0"指的是一个访问器,它提供关于每个关节对每个顶点的影响程度的信息。在给定的例子中,权重如下:
Vertex 0: 1.00, 0.00, 0.0, 0.0,
Vertex 1: 1.00, 0.00, 0.0, 0.0,
Vertex 2: 0.75, 0.25, 0.0, 0.0,
Vertex 3: 0.75, 0.25, 0.0, 0.0,
Vertex 4: 0.50, 0.50, 0.0, 0.0,
Vertex 5: 0.50, 0.50, 0.0, 0.0,
Vertex 6: 0.25, 0.75, 0.0, 0.0,
Vertex 7: 0.25, 0.75, 0.0, 0.0,
Vertex 8: 0.00, 1.00, 0.0, 0.0,
Vertex 9: 0.00, 1.00, 0.0, 0.0,
同样,每个条目的最后两个组件是不相关的,因为只有两个关节。
将"JOINTS_0" 和 "WEIGHTS_0"属性组合起来,可以得到关于每个关节对每个顶点的影响的确切信息。例如,给定的数据意味着顶点6应该受到关节0的25%和关节1的75%的影响。
在顶点着色器中,这些信息用于创建关节矩阵的线性组合。这个矩阵叫做各自顶点的皮肤矩阵。因此,"JOINTS_0" 和 "WEIGHTS_0"属性的数据被传递给着色器。在本例中,它们分别作为 a_joint 和 a_weight属性变量给出:
...
attribute vec4 a_joint;
attribute vec4 a_weight;
uniform mat4 u_jointMat[2];
...
void main(void)
{
mat4 skinMat =
a_weight.x * u_jointMat[int(a_joint.x)] +
a_weight.y * u_jointMat[int(a_joint.y)] +
a_weight.z * u_jointMat[int(a_joint.z)] +
a_weight.w * u_jointMat[int(a_joint.w)];
vec4 pos = u_modelViewMatrix * skinMat * vec4(a_position,1.0);
gl_Position = u_projectionMatrix * pos;
}
然后,在使用模型-视图-矩阵进行转换之前,使用皮肤矩阵转换顶点的原始位置。这个变换的结果可以想象为顶点与各自关节矩阵的加权变换,如图20d所示。
对于给定的例子,将这个皮肤矩阵应用于顶点的结果如图20e所示。




