由浅到浅入门批量渲染(完)

发表于2020-12-21
评论0 1.03w浏览

好久不见。

 

这是第36篇与游戏开发有关的文章。

 

上回简述了与优化骨骼蒙皮动画有关的内容接下来我们将来到《由浅到浅》系列的最后一篇:介绍两种批量渲染骨骼蒙皮动画单位的优化方案:烘焙顶点动画烘焙骨骼矩阵动画

 

简单来说,它们基本的思路,都是将骨骼蒙皮动画的“结果”预先保存在一张纹理中;然后在运行时通过GPU从这张纹理中采样,并使用采样结果来更新顶点属性;再结合实例化技术(GPU instancing),达到高效、大批量渲染的目的。

 

如果你之前对这类优化方案并不了解,看了上面的描述,也仍然一头雾水;那太好了,这篇文章(没准)可以帮助你快速入门这类优化方案。

 

下面我们就简单的介绍一下它的工作流程及原理吧。

 

| 烘焙顶点动画

 

可以简单的将它的工作流程分为两个阶段:

  • 非运行状态下的烘焙阶段
  • 运行状态下的播放阶段。

 

烘焙阶段

这个阶段其实是为后面的播放阶段准备动画资源,你也可以把这个资源简单的理解为一种特殊类型的动画文件。

 

首先,在编辑状态下,让携带骨骼蒙皮动画的角色,按照一定帧率播放动画。

 

WwF0Cg96ww5yLTZa0AjI.gif

非运行状态下播放动画


 

我们知道:动画播放时,角色网格会被蒙皮网格渲染器(SkinnedMeshRenderer)更新而产生变形(顶点变化);如果在动画播放的同时,记录下每一个顶点在这一时刻相对于角色坐标系(通常是角色脚下)的位置。那么,当动画播放完毕时,我们会得到一张“每一个顶点在每一个关键帧时的位置表”,我们就叫它“帧顶点位置表”吧。
 

可是“帧顶点位置表”名字太长,你可能不太好记,我打字也比较麻烦,所以我们后面就叫它“表表”吧。

r7oJpMY4aKzrexo6l2ts.png

播放某动画时,记录下的表表

 

除了表表,我们还能得到与它对应的动画信息,比如这个动画的名称、时长、帧率、总帧数、是否需要循环播放等信息。

 

到这里,第一个阶段我们需要的内容就准备就绪了,可以进入下一个阶段:播放动画阶段

 

播放阶段

在动画播放时,通过一个变量(播放时长)来更新播放进度;结合上一阶段我们记录下来的动画总长度、动画总帧数信息,就可以计算出当前动画播放到了第几帧(当前帧数 = 已播放时长 / 动画总时长 x 动画总帧数)。

 

一旦得到当前动画帧,就表示可以通过表表锁定一行顶点数据

 

NqqWCzAqsi3mXD14948H.png

通过关键帧找到的顶点数据
 

接下来,只要遍历这行顶点数据;然后根据索引找到网格中对应的顶点并更新它的位置,就等于完成了蒙皮工作。

 

4bdBFMosgIKoxdye6Pcp.gif

每一帧都通过表表来更新顶点属性,动画就播放起来了

 

如你所见,使用这种方式来更新角色动画,其实是直接使用了预先处理好的骨骼动画、蒙皮网格渲染器的作用结果,是一种用空间换时间的策略。

 

既然通过一个播放进度表表和一些简单的动画信息,就能直接完成蒙皮(对顶点属性的更新),那么我们就不再需要以下内容了:

  • 骨骼信息
  • 动画控制器
  • 蒙皮网格渲染器

 

使用CPU来读取表表,并遍历更新所有顶点,显然不如GPU来的高效;所以接下来,我们将这一步放到渲染管线中的顶点变换阶段,借着GPU处理顶点的契机,完成使用表表中的数据对顶点属性进行更新。

 

| 使用纹理保存动画数据

 

首先,为了便于GPU读取表表,我们将其保存为一张纹理,可以称之为动画纹理。

 

比如,对于一个拥有505个顶点的模型来说,我们可以将表表中的信息保存到一张 512 x Height 大小的纹理中。

 

这其中,纹理的宽度用来表示顶点的数量,而纹理的高度用来表示关键帧,所以Height的值取决于动画长度以及动画帧率。

 

L0ptZ4fWEG2ZRGD9qby4.png

将表表中的顶点位置“烘焙”到纹理中
 

我们通过UV坐标来获取这张纹理上的像素,就可以被理解为:取第U个顶点在第V帧时的坐标。

 

当然,除此之外,我们还会将动画信息保存成为动画资源,将重要信息进行序列化(动画名称、动画长度、总帧数、是否循环等)。

 

HE2QSlyp8PcfV51AAQTt.png

使用ScriptableObject保存动画信息
 

后面的事情就简单了:在播放动画时,CPU将当前播放的关键帧传给顶点着色器;顶点着色器计算出对应的V坐标;结合顶点索引及动画纹理的宽度计算出U,既可采样出这个顶点基于角色坐标系下的坐标;接下来用这个坐标再进行后面的空间变换就可以了。

 

法向量

由于动画播放时,顶点的实时位置是从纹理中采样,而非从网格中读取的(不再使用蒙皮网格渲染器,顶点缓冲区内的数据不会被修改),所以顶点属性中的法线信息也无法使用了(永远是静止状态下的);如果需要获取正确的法向量,那就需要在烘焙顶点坐标时也同样将法线烘焙下来,并在顶点变换阶段将这个法向量也采样出来。

 

9RcGCnE361RPEib4gVKw.png

烘焙的法线纹理

 

存在多个动画

通常情况下,角色不会只包含一个动画;比如小兵通常拥有空闲、移动、攻击三个动画。如果对每一个动画都烘焙一或两张(法向量)纹理,那贴图的数量将很快不受控制。鉴于所有动画对应的顶点数量一致,也就是纹理的宽度都相同,我们可以将多个动画纹理进行合并。

 

s37d3nKP1cPWA4jIVJ6H.png

三个动作被合并到了一张纹理中
 

将多张动画纹理进行上下排列、合并后,我们只要将每个动画的起始、终止的V坐标范围追加到动画资源中即可。然后在播放、切换动画时,根据当前动画所在的起始位置以及播放进度,就能算出正确的V坐标了。

 

eqZ2JNpylkUIr4sTt62O.png

保存动作信息时需要额外记录一些属性

 

当然,如果网格的顶点数量少,而动画数量多,我们也可以多列放置动画(前提是放的下)。

te8yvxMxmjQ7BEbYxLCz.png

多列放置动作
 

动画过渡

简单的动画过渡很容易实现,只要在切换动画时,分别计算出当前动画和下一个动画的播放位置,然后传给GPU进行两次顶点位置采样,再对两次采样的结果进行插值即可。

xImwpkFSDIGdj01ORbl9.gif

带动画过渡

e7rfsdCxW592xz2RoUR4.gif

不带动画过渡


使用实例化渲染

实例化渲染的特点是使用相同网格相同材质通过不同的实例属性完成大批量的带有一定差异性的渲染;而烘焙顶点恰好符合了实例化渲染的使用需求。

 

所以,我们只需将控制动画播放的关键属性:比如过渡动画播放的V坐标、当前和下一个动画的插值比例等,放入实例化数据数组中进行传递;再在顶点着色器中,对关键属性获取并使用即可。

 

J1uT4f5wjdTP0dngttHS.png

实例化渲染时获取到的关键属性
 

当然,如果你想实现更多不同的效果,比如你附带了一张遮罩纹理,用来调整diffuse或做某些特殊的计算,那你也需要将控制参数加到实例化数据中就可以了。

 

顶点着色器采样

在顶点着色器中是无法使用tex2D进行采样的,需要使用tex2Dlod进行代替但是这个特性需要shader model 3.0(#pragma target 3.0)才可以支持。

 

顶点索引

可以通过语义SV_VertexID来获取顶点索引,但是在移动平台上这个特性需要OpenGL ES3.0(#pragma target 3.5)才可以使用(当然也可以在烘焙阶段将顶点索引保存到网格属性中)。

 

与蒙皮网格渲染器的比较

  • 不再需要CPU计算动画和蒙皮,提升了性能
  • 可以通过实例化技术批量化渲染角色,减少DC

 

烘焙顶点的主要问题

  • 模型顶点数量受限:如果纹理的最大尺寸限制在2048 x 2048,那么只能烘焙下顶点数在2048个以下的模型
  • 记录顶点动画的纹理过大:(顶点位置)纹理格式需设置为TextureFormat.RGBAHalf
  • 存储的动作长度有限
LYIuuvaW14R3pFPLGHtx.png

烘焙顶点的动画长度参考表
 

| 烘焙骨骼矩阵

 

除了烘焙顶点,另一种常用的优化方案是烘焙骨骼矩阵动画

 

听名字就知道,烘焙骨骼矩阵与烘焙顶点位置,原理十分相似;最大的差异在于它们在烘焙时所记录的内容不一样:烘焙顶点记录下来的是每个顶点的位置,而烘焙骨骼矩阵记录下来的是每一根骨骼的矩阵,仅此而已。

NyQ2gdPGwuSvGeof1VAk.png

记录下每一根骨骼在每一帧动画播放后的矩阵


烘焙骨骼矩阵最大的意义在于它补上了烘焙顶点的短板:受顶点数量限制烘焙的动画纹理过大 及 纹理数量较多

 

动画纹理使用量差异

烘焙顶点对于纹理(面积)的使用是受顶点数量决定的,可以简单理解为:

 

纹理面积使用量 = 顶点数量 x 动画长度 x 内容数量

 

所以,当模型的顶点数量过多时(数以千计),这种烘焙方式或者无法烘焙下整个模型(顶点数量>2048),或者需要一张或多张(法线、切线)大尺寸纹理(<2048 && > 1024)。

 

但是,烘焙骨骼矩阵主要取决于骨骼的数量,可以简单理解为:

 

纹理面积使用量 = 骨骼数量 x 动画长度 x 矩阵烘焙方式(x1、x2 或 x3)

 

在移动平台上,通常20根左右的骨骼就可以取得不错的表现效果,所以相对于烘焙顶点,烘焙骨骼可以记录下更长的动画,同时它也不再受顶点数量的限制,也无需对法线或切线进行特殊处理(因为可以在采样后通过矩阵计算得出)。

DrxjluPWi6isq1gQKkKG.png

两种方式烘焙的动画纹理尺寸差异较大

 

烘焙阶段

与烘焙顶点相似,烘焙骨骼也需要先在非运行状态下,预先播放一次动画;并在动画播放时,记录下每个关键帧下每根骨骼的转换矩阵。

 

这里有三点需要注意。

 

第一,记录下的矩阵是每根骨骼从网格坐标系转换到角色坐标系下的矩阵:

 

Matrix_meshToRole = Matrix_boneLocalToRole x Matrix_meshToBoneLocal

 

Matrix_meshToBoneLocal可以通过mesh的bindposes获取;而Matrix_boneLocalToRole可以通过bone的transform计算获得。

 

第二,需要将每个顶点与骨骼的关系记录到网格信息中,这个关系是指顶点会被哪根骨骼影响(骨骼索引)以及影响的大小(权重值)。

 

每个顶点最多可以受4根骨骼影响,但是被越多骨骼影响意味着播放时会有越多的采样矩阵计算,通常限制在2根骨骼就能得到不错的效果;骨骼索引和权重可以通过mesh的boneWeights得知,在烘焙纹理时可以将它保存在mesh中不用的uv中,以便在顶点着色器中获取。

t3metjFbM8xsNuGu2iNC.png

每个uv可以保存下一根骨骼的索引和权重,通常使用两个uv就可以了

 

第三,对于不同的骨骼动画,烘焙矩阵的方式也不一定相同。

 

例如,如果骨骼动画中每根骨骼只会相对于上层骨骼进行旋转变换,那我们烘焙一个四元数就够了,也就是一根骨骼的一个矩阵只占用一个像素;但是如果骨骼除了旋转,还有平移甚至缩放的操作,那我们就需要2-3个像素来储存一个骨骼的矩阵了。

c70bJaMveyaNosoiDByz.gif

拥有特长的角色,需要特殊处理(来自《匹诺曹》)

E36w3dLpigI4nJnJnO6y.png

也可以简粗的将一个矩阵完整的保存在三个像素中

 

播放阶段

与烘焙顶点相同的是,烘焙骨骼矩阵也是在顶点变换阶段,通过对动画纹理的采样,完成顶点坐标计算的;但是它的计算方式相对于烘焙顶点要复杂一些。

 

这里主要是需要根据烘焙矩阵的方式,通过采样来还原从网格坐标系到角色坐标系的转换矩阵;例如,在烘焙阶段将完整的矩阵保存在三个像素中,那转换时就需要采样三次才能拼凑出一个完整的矩阵。

 

当然,一旦得到转换矩阵,角色坐标系下的顶点位置、法向量等就可以通过计算获取,后面的变换就可以继续了。

 

相比于烘焙顶点

正如前文所说,烘焙骨骼不再受顶点数量的限制,可以记录下更长的动画时间,烘焙纹理的尺寸和数量也有明显的优势。

wHRkOXrIwqyvLTiVxeJt.png

烘焙骨骼的动画长度参考表

adrycP0wbiX3yAumbuta.png

相同模型下的烘焙动画长度对比

 

但是,烘焙骨骼这种方式会在顶点着色器中需要进行多次顶点采样,在模型顶点数较多、或渲染单位数量较多时,其效率会略低于烘焙顶点。

8qJ23YrOJY1AAFogEMGu.png

烘焙骨骼的在顶点着色器中的采样更多

2Uxyo69b6xU91h3g8VSM.gif

两种烘焙方式分组比较

3WFR7ulZsp78qqIdHZ56.png

华为P20上的两种烘焙方式的对比

 

| 小结

 

以上,就是针对批量渲染骨骼蒙皮动画单位的两种优化方案。

 

针对这两种方案,我个人认为并没有绝对的孰优孰劣;正如你所看到的,烘焙骨骼在处理复杂模型或多动作时更具优势;但如果渲染数量较多、模型顶点数较少、表现需求较弱(无需法向量参与计算)时,烘焙顶点也是值得尝试的,因为它在性能上会更好一些。

 

其他优化方案

其实,除了上述两种优化方案外,坊间还有一些特殊的小技巧:比如在手游《三国志大战M》的开场战斗表演中,某些带动画的模型角色,也通过实例化达到了批量渲染的目的。

RcshfN9aYFJaV5wOX6w0.png

部分小兵不是简单的“片”,而是带模型的单位

 

但是通过GPA抓帧工具,会发现同一个模型的网格会在同一帧存在多个不同的“姿势”;我推测这里应该是将利用了若干个、使用相同模型的、骨骼蒙皮动画的播放“结果”,通过实例化渲染的方式,复制到多个位置上,由于每个骨骼蒙皮动画的播放进度都略有不同,它们混在一起后的效果就会比较自然,不失为一种巧妙的方法。

zhIvObuP3MFGNdA2Ofr3.gif

渲染时每个模型的“姿势”都略有差异

 

| 写在最后

 

至此,这回的《由浅到浅入门批量渲染》系列就全部更新完了。

 

快到年底了,是时候总结一下碌碌无为的2020年,并为迎接2021年做些准备了,所以今年就不计划再更新文章了。

 

明年见。

 

我的公众号 偶尔学学Unity 会特别不定期更新与游戏开发可能有关的文章,欢迎关注,谢谢。

E6mG7DKsIGe7KRY5dF1a.png
  • 允许他人重新传播作品,但他人重新传播时必须在所使用作品的正文开头的显著位置,注明用户的姓名、来源及其采用的知识共享协议,并与该作品在磨坊上的原发地址建立链接
  • 可对作品重新编排、修改、节选或者以作品为基础进行创作和发布
  • 可将作品进行商业性使用

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引