海田1 发表于 2021-2-25 10:14

浅析 Unity 中骨骼动画的各种实现

前言

骨骼动画是游戏中常用的,用来简化、规范动画的制作, 压缩动画数据的技术,它将模型里的顶点按照一定的权重与若干 bone 绑定起来。播放动画时,通过将骨骼切换到新的姿势,即可改变关联的顶点,从而改变模型。本文将列举一下各种骨骼动画实现方式并简单分析一下优缺点。
骨骼动画需要完成的工作

骨骼动画要做的事情分为两部分
采样动画数据获取当前帧各个骨骼的姿势(或者说矩阵),也就是骨骼变换。各个顶点根据绑定的骨骼以及权重,混合矩阵,用将坐标变换到新位置,这个过程被称作蒙皮(Skinning)。
Unity 默认实现

Unity 默认的骨骼动画实现,是在 CPU 中采样 Animation Clip 里的曲线数据,这里因为曲线保存的并非直接的骨骼变换矩阵,而是某个 bone 相对父节点的 local Position、local Rotation、local Scale、所以要递归地计算每个 bone 的变换矩阵。
之后的蒙皮计算也是在 CPU 中进行的,但是这个过程是多线程的。
优点:
骨骼变换是在 Unity 动画系统管理下的完成的,可以方便地支持 blend Tree 与 ik 之类的特性。 蒙皮过程都是在 CPU 完成的,所以交付给 GPU 的 Mesh 和普通的 Mesh 没有什么区别,可以统一接口、简化开发。
缺点:大部分蒙皮网格的顶点数目与骨骼数目都不少,而骨骼变换和蒙皮计算的开销和这两者线性相关。多线程可以减轻一定负担,但是对于核心数少的平台,比如移动端,给 CPU 带来的负载还是太大了。
Unity 内置的 GPU Skinning

骨骼变换部分和默认实现没有什么区别,但是把蒙皮的过程放在 GPU 进行,但是并不是每个pass 都会进行一次,这个对于需要绘制多个 pass 的 render 而言是冗余的开销,于是 Unity 的做法是先进行一次蒙皮计算,然后通过 transform feedback(取决于平台调用的接口可能不一样)将计算结果写回顶点缓存,这样后续各个 pass 绘制时就可以直接读这个计算结果。
优点:相比默认实现,将蒙皮计算放到 GPU 进行,可以减轻 CPU 的负担,本身 GPU 在蒙皮这种高度可并行的任务上相比 CPU 优势明显。缺点:因为要使用 transorm feedback 这类接口,对图形接口的版本有要求。
Unity 的 Compute Skinning

GPU Skinning 是在 vertex shader 里完成蒙皮计算,Unity 可以使用 Compute Skinning , 它会把蒙皮放在 computer shader 里计算,计算结果同样也会写回顶点缓存供后续其他 pass 绘制,但是不需要 transform feedback 这类流输出接口,直接在 compute queue 里计算然后输出 buffer。
优点:和 GPU Skinning 一样也能减少 CPU 负担。缺点:需要支持 compute shader ,图形接口的版本要求比 GPU Skinning 还要高,却看不出什么直接的性能优势。
对 GPU Instance 的支持

上述代码里都是使用 uniform 数组传送骨骼的矩阵给 GPU,游戏里各个 render 一般不会在同一时间里都播放同个动画片段的同一帧,所以各个 render 绘制时的骨骼矩阵数据不一致,也就是渲染状态或者说上下文不一样,所以没有办法应用 GPU instance,每个 render 都要消耗单独的 drawcall,对 CPU 造成相当大的负担。
解决这个问题的是,让这些 render 共享一样的渲染状态,将动画数据,也就是动画片段的每一帧每一根骨骼的当前矩阵全都传给 GPU,这样无论 render 当前是在播放哪一个片段的哪一帧,都可以直接从当前上下文获得,不需要渲染状态的切换,从而就能够使用 GPU Instance了。
将骨骼动画烘焙至贴图

那么该如何将完整的动画数据传给 GPU 呢? uniform 肯定是不行的,简单计算一下,每个骨骼矩阵需要保存至少 3x4 个浮点数,假设有 n 根骨骼,每个动画都有 m 帧,一共有 k 个动画片段,那么 uniform 就需要保存 12 * n * m * k 个。这些空间还要留给使用 GPU instance 保存 transfrom 或者其他乱起八糟的东西。所以应该选择使用贴图。
这里还需要注意贴图的格式,因为精度的要求以及数值的范围问题,不能使用普通的 8-bit RGBA。
因为在 vertex shader 里,uv并没有确定,自然 mipmap level 也无法确定,所以只能调用tex2Dlod 接口。
优点:
相比起内置的 GPU Skinning,骨骼的变换通过预计算烘焙到贴图,运行时没有这部分工作的开销,而蒙皮则也是在 vertex shader 进行的,极大地解放了 CPU。因为GPU instance,原本若干个 drawcall 被合并,CPU 的压力又被进一步减轻了。
缺点:
引入了额外的贴图,自然要承担贴图带来的内存占用、带宽消耗的问题。多个 pass 绘制的情况下,每次都要在顶点着色器里进行一次蒙皮计算。贴图的格式以及 tex2Dlod 接口的调用也提出了兼容性的要求。需要在使用的 shader 里引入额外的代码进行蒙皮。因为动画数据完全烘焙至贴图中,unity 动画系统提供的类似 blendTree,ik 等功能无法使用。

间接绘制

上面的做法虽然支持了 GPU Instance ,但是绘制多个 pass 时,vertex shader 要反复进行蒙皮计算。有一个解决方法是先在 Compute shader 算出各个 Instance 的 Mesh 输出到一个 Buffer 中,然后调用 DrawProceduralIndirect 绘制。
优点:相比起上述 instance 实现,可以减小反复进行蒙皮计算带来的 GPU 计算压力以及采样骨骼变换贴图带来的带宽消耗。缺点:
相比起上述 instance 实现,缓存 instance 的 mesh 数据,进一步提高了内存消耗。使用 computer shader 等特性提高了图形接口版本要求。

总结

上述即为我目前接触到的骨骼动画实现方式,可以看出它们各有优缺点,有的在性能的某方面上有优势,有的支持更多特性。Unity 的提供的功能更倾向于通用性,对于工作流以及额外的特性支持更友好,但在性能上处于劣势,如果项目面临性能压力,就要进行各种魔改,但这不可避免地会带来制作与维护上的成本以及兼容性的问题。还是那句正确的废话,要结合项目自身情况选择合适的优化方案。
参考:

poney 发表于 2021-2-25 10:17

为什么会有虚构创作声明

冀苍鸾 发表于 2021-2-25 10:20

加着玩的[害羞]

杨柳657 发表于 2021-2-25 10:27

写的很好,学习了

馥琳 发表于 2021-2-25 10:32

谢谢~

素色流年783 发表于 2021-2-25 10:38

总结的不错

简单350 发表于 2021-2-25 10:40

吃大佬们玩剩的东西[看看你]

海田1 发表于 2021-2-25 10:49

思路很清晰,赞

塞翁364 发表于 2021-2-25 10:50

谢谢~
页: [1]
查看完整版本: 浅析 Unity 中骨骼动画的各种实现