【译】glTF教程:蒙皮(二十)

【译】glTF教程:蒙皮(二十)

2019-02-26

蒙皮

顶点剥皮的过程有点复杂。它将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显示了这个几何图形,通过轮廓渲染可以更好地显示结构。


图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所示。


图20b:关节1逆结合矩阵的几何变换。

乍一看,这种转变可能有违直觉。但是这个变换的目标是“撤销”由各自节点的初始全局变换所做的变换,这样就可以根据节点的实际全局变换来计算节点对网格顶点的影响。

顶点蒙皮实现

现有渲染库的用户几乎不需要手动处理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的几何变换。

图像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_jointa_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所示。


图20d:皮肤矩阵的计算。

对于给定的例子,将这个皮肤矩阵应用于顶点的结果如图20e所示。


图20e:在动画过程中,带有轮廓渲染的皮肤示例的几何图形。