reference: Efficient Morph Target Animation Using OpenGL ES 3.0 James L. Jones
介绍
移动平台上对高质量图形渲染的需求推进了GPU和图形API的发展,例如OpenGL ES 3.0;这些硬件/API上的进步使得程序员能够编写更加简洁、高效的图形学算法实现。其中的一个受益领域就是手机游戏中角色的面部动画,该技术通常通过目标变形(morph target)来实现。目标变形动画通常要求美术离线制作模型的多个姿态,然后,在程序运行时,这些姿态会以不同的权重混合在一起,以创建一个动画序列,例如眨眼、皱眉等表情。与蒙皮技术配合使用,便能构建起功能丰富的角色动画系统。
从历史来看,目标变形动画已经在PC等平台游戏中大量使用,但由于早期的图形API局限性较大,导致移动平台上的实现开销较大。本章介绍了使用OpenGL ES 3.0中引入的transform feedback API的目标变形动画的高效实现。
之前的工作
基于OpenGL ES 2.0 API的移动平台GPU,人们提出了一个有趣的几何纹理方法。该方法根据顶点结构,在纹理中存储目标姿态之间的顶点位移。变形过程将完全在绑定到帧缓冲区对象的纹理上执行,然后再按顶点将最终纹理作用于置换网格顶点。该方法是存在问题的。最明显的一个问题是,平台提供的纹理单元的最大数量可能为0。这意味着在某些平台上需要使用替代方案。OpenGL ES 3.0中标准化的Transform feedback功能可以使得实现更加简单,避免了顶点纹理编解码的操作。
变形目标
目标变形动画用于需要对模型应用许多小的逐顶点修改的情况,这与骨骼系统处理的较大幅度的运动变化不一样。变形目标的一个实用的例子是为游戏角色创建逼真的面部表情动画中的面部肌肉动画。(见图4.1)
一种实现需要存储多个版本的网格体,称为目标姿态或关键姿态。这些姿态将和基本姿态一起存储,基本姿态用于表现角色正常状态下的面部动画。如果需要创建不同的动画序列,需要将每个网格顶点的位置按权重与一个或多个目标姿态混合。该权重与相应的目标姿态相关联,并表示了该目标姿态以多大程度影响了结果。
为了能够在目标姿态之间进行融合,可以使用差分网格。这是为每个目标姿态创建的网格,该网格给出了目标姿态和基本姿态之间的顶点差值。这些差值向量用作整个向量空间中的基底(也就是说,可以通过权重向量,对这些基底进行线性组合,构造最终网格体的每个顶点)。更准确地说,对于时间为t的每个输出顶点vi,我们已知基本姿态顶点为bi,N个目标姿态的顶点为pi,权重向量为w,那么我们有:
上面的公式总结了目标变形动画所需的所有条件。但是,大多数情况下,每帧的总权重可能只有一部分会改变。因此,我们希望避免每帧重新计算所有目标姿态的权重贡献。一个更好的方案是跟踪权重向量的变化以及内存中的当前姿态。对于帧h中的变化,新位置等于当前姿态加上该位置的差值:
我们可以发现,每帧的位置变化取决于每帧的权重变化:
利用以上信息,我们设计了一种方案,仅对发生了变化的权重(即)计算并更新姿态。
实现
这一实现利用绑定到transform feedback对象的顶点缓冲区来跨帧存储并更新当前姿态。不幸的是,OpenGL ES 3.0不支持同时读写同一缓冲区,所以我们可以创建两个缓冲区交替使用(即每帧输入缓冲区和输出缓冲区交换)。通过遍历顶点并减去基本姿态,就可以预计算得到差异网格。合理的起始数据将被加载到feedback顶点缓冲区(在这个例子中使用了基本姿态)。在每一帧中,我们使用变化的权重来更新顶点缓冲区中的当前姿态。这一更新可以使用批处理,也可以不使用。最终,我们像平常一样渲染更新后的顶点缓冲区。我们以与更新顶点位置相同的方式更新顶点法线,以得到正确的动画法线。(见图4.2)
后更新
在我们更新姿态之前,我们必须首先计算权重的改变。下面的伪代码演示了这是如何实现的:
// inputs:
// w[] : current frame weight vector
// p[] : previous frame weight vector
// dw[] : delta weight vector
// per-frame weight update
animate(w)
for i = 0 to length(w):
dw[i] = w[i] - p[i]
p[i] = w[i]
使用差异权重,我们现在可以检查权重向量中哪些数据发生了变化。针对发生变化的权重,使用关键姿态执行transform feedback pass。
// inputs:
// q[] : array of difference mesh VBOs
// dw[] : delta weight vector
// vbo[] : array of vertex buffer objects for storing current pose
// tfo : transform feedback object
// shader : shader program for accumulation
// per frame pose update:
glBindTransformFeedback(tfo);
glEnable(RASTERIZER_DISCARD);
glUseProgram(shader);
for(i = 0; i < length(q); i++) :
// only for weights that have chagned:
if(abs(dw[i]) != 0) :
// set the weight uniform
glUniform1f(..., dw[i]);
// bind the output VBO to TBO
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo[1]);
// bind the inputs to vertex shader
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(ATTRIBUTE_STREAM_0, ...);
glBindBuffer(GL_ARRAY_BUFFER, q[i]);
glVertexAttribPointer(ATTRIBUTE_STREAM_1, ...);
// draw call performs per-vertex accumulation in vertex shader.
glEnableTransformFeedback();
glDrawArrays(...);
glDisableTransformFeedback();
// vertices for rendering are referenced with vbo[0]
swap(vbo[0], vbo[1]);
批处理
为了效率,我们还可以做更多的优化。在这一版本中,通过批处理将更新合并得到更少的pass。我们可以在每个pass中使用一个顶点着色器处理多个关键姿态,而不是每次仅对一个姿态传递属性和权重。然后我们就能以尽可能少的pass来执行更新。
// inputs:
// q[] : difference mesh VBOs, where corresponding dw != 0
// vbo[] : array of vertex buffer objects for storing current pose
// dw[] : delta weight vector
// shader[] : shader programs for each batch size up to b
// b : max batch size
// per-frame batched pose update:
// ... similar setup as before ...
for(i = 0;i < length(q); ) :
k = min(length(q), i + b) - i
glUseProgram(shader[k]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo[1]);
//bind attributes for pass
for(j = 0; j < b; j++) :
if(j < k) :
glEnableVertexAttribArray(ATTRIBUTE_STREAM_1 + j);
glBindBuffer(GL_ARRAY_BUFFER, q[i + j]);
glVertexAttribPointer(ATTRIBUTE_STREAM_1 + j, ...);
else :
glDisableVertexAttribArray(ATTRIBUTE_STREAM_1 + j);
// set the delta weights
glUniform1fv(...);
// bind current pose as input and draw
glEnableVertexAttribArray(ATTRIBUTE_STRREAM_0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(ATTRIBUTE_STREAM_0, ...);
glEnableTransformFeedback();
glDrawArrays(...);
glDisableTransformFeedback();
swap(vbo[0], vbo[1]);
i = i + k;
此技术需要多个版本的shader,其中每个版本都会对更多的输入属性执行加法运算,直到达到最大批处理大小。需要注意的是,可用输入属性的最大数量受API限制,该值(必须至少为16)可以通过glGetIntegerv函数查询,参数为GL_MAX_VERTEX_ATTRIBS。
结果
在统计图4.3中的数据时,使用了七个目标的最大批处理大小(批处理大小表示在顶点着色器中执行的加法数量)。该演示针对不同批处理大小多次执行。对于每次运行结果,使用了平均帧率。
这些结果表明,批处理pass减少了动画更新时重绘几何图形的开销。根据特定帧目标的数量,我们应该选择足够大的批处理大小来降低成本。
结论
本章演示了使用transform feedback的高效目标变形动画系统。该技术可在渲染之前作为单独pass计算,并可在此技术基础上轻松实现其他技术(如蒙皮)