【译】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所示。