入门顶点动画纹理的实例化绘制

发表于2019-09-30
评论2 5k浏览

好久不见。

 

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

 

| 写在最前

网上有多种通过GPU实现骨骼动画的实例化绘制方法,本文介绍的是其中的一种:将顶点信息逐帧写入纹理后,在顶点着色器中通过读取动画纹理,提取顶点位置并变换,最终实现角色动画的方法。

 

本文将简述其实现原理,并分享一个(完成了一半的)网格合并及实例化绘制工具。

 

| 如何提高绘制效率

当产生了“要将大量游戏对象呈现给玩家”的需求时,我们就会碰到这样一个问题:如何才能提高GPU的绘制效率。

 

SVfy2oLyIwmJji6rXsnl.png

批量绘制较多的骑兵

 

通常情况下CPU对GPU发起的绘制命令,才是性能的瓶颈所在。CPU为绘制准备数据、显存加载数据、为GPU设置渲染状态等行为所花费的时间,通常比GPU绘制所花费的时间要多。这也就是为什么我们经常会把DrawCall次数当成快速评判渲染效率的“KPI”。

 

反观Unity提供的Static batching(静态合批)Dynamic batching(动态合批),也都是从减少CPU到GPU的调用次数为出发点,尽量一次发送一个大的网格(一大堆顶点数据),以减少CPU和GPU的通信次数,提高彼此的工作效率。

 

但是无论静态还是动态合批,在大量游戏对象绘制的需求面前,都不太合适。

 

静态合批从名字上就知道不能用来绘制移动物体,而且其本身还会产生非常大的内存开销(它需要额外的内存空间来存储合并的网格);动态合批也有自己的问题,如顶点数量的限制、材质球限制、无法作用于蒙皮网格(SkinnedMeshRenderer)等,还会对CPU产生不小的压力(因为它要不停地去动态计算并合并网格)。

 

| 实例化绘制

实例化绘制技术的出现,就是为了在不提高CPU负担的基础之上,解决CPU到GPU调用开销大的问题。对于相同的物体(同一个网格),只需一次调用,GPU就会根据我们想要绘制的次数,啪啪啪一通画,非常的高效。

 

但是简单重复绘制一个物体多次(比如重复绘制1000次小兵),并没有任何意义。为了能够绘制出1000个不同的小兵,我们还需要提前为GPU准备一些额外的数据,比如1000个转换矩阵(画在不同的位置)、1000个混合色(呈现不同的颜色)等,最终在屏幕上呈现出千军万马的画面。

 

| 骨骼动画与实例化绘制

如果我们想要在游戏世界上呈现非常多相同的、静止不动的石头,那到此为止就可以了。我们使用Unity提供的手动实例化绘制接口Graphics.DrawMeshInstanced,通过传入同一个石头的网格和每一个石头的转换矩阵,就可以实现需求(其实Unity也会自动为添加了MeshRenderer组件的单位尝试使用实例化绘制以提高效率)。

 

但是对战场中的小兵做这种简单地操作就不太合适了,这个道理早在1994年上映的电影《精武英雄》中,就已经明确的告诉过我们了。

 

YsudsNNIK3RzMPuYQImt.png

船越文夫在教导陈真 图源网络

 

这是因为小兵通常是采用骨骼动画来实现动作的,而骨骼动画对于蒙皮网格的驱动,是CPU即时计算出来的。每个小兵相同时刻的状态可能都不同,也就是说相同网格同一时刻的顶点位置会有很大差别,因此无法直接进行实例化绘制。

 

既然CPU上即时计算的骨骼动画无法进行实例化绘制,我们就不让CPU计算,而让这些计算发生在GPU上,便可将问题解决。

 

| 它的原理很简单

1、将骨骼动画每一帧对网格各个顶点的变化结果存在一张纹理中,其中纹理的横坐标是顶点索引,纵坐标是时间,而横纵相交对应的值,是这一时刻该顶点在本地空间下的坐标

 

2、有了这张“顶点动画纹理”,在顶点着色器中,我们就可以忽视传入顶点着色器的顶点位置信息;而以当前所处理的顶点索引U,以动画播放至此的时间刻度为V,从上一步的纹理坐标中采样。而采样到的结果,就是当前这个顶点此时的位置。

 

3、接下来的步骤便与传统绘制一样,与MVP矩阵相乘做空间变换,传入片段着色器中着色等...可以很容易的想象到,连续为网格上所有顶点设置不同时间下的空间位置,最终绘制到屏幕上时,就能呈现出动画效果了。

 

| 一些相对重要的细节

1、用实例化ID来获取差异实例单位的属性

由于我们的最终目标是绘制多个不同动画状态的单位,因此从动画纹理中,用于采样信息的时间刻度值,是根据实例化ID,从保存实例化属性的数据块中获取到的,这样就可以实现每个实例化单位的动画播放进度的差异。


 

2、合并多个不同的网格

手动调用实例化绘制接口时,只能传入一个网格。而我们平时使用的游戏对象,通常是由若干个蒙皮网格和若干个普通网格组成。比如一个骑兵模型:士兵和马匹分别是两个蒙皮网格;而士兵手持的武器通常是一个普通网格,以方便后期做武器替换。

 

WjP3U7YHNwYiKdMT6R6k.png

一个游戏对象可能会由两种、多个网格组合而成

 

因此我们会在编辑器模式下,将整个对象包含的网格合并成一个网格,并将这个网格保存成资源,以便后面调用绘制命令时作为实参传入。

 

YWH2GsGll7mvGbTxnAIV.png

合并成为一个网格

 

3、多贴图时处理UV

此外,有些模型上不同的网格还对应了不同的贴图,比如网格Mesh_0,使用了贴图Texture_0,网格Mesh_1使用了贴图Texture_1,由于网格进行了合并,如果针对合并后的网格使用同一张贴图,便会出现错误。

 

VvXM5rIFbWS9iZlfJEue.png

胯下战马错误的颜色采样

 

针对这种情况我们要在合并时做特殊处理,一种处理方式是合并多张贴图,如将Texture_0与Texture_1合并,然后偏移原本Mesh_1的uv坐标,但是这要求两张贴图都不能太大,否则无法合并到一张贴图中;另一种方法是仍然保留两张贴图Texture_0和Texture_1,但是对Mesh_0和Mesh_1的uv2做特殊处理,如使用uv2x保存两张贴图的Lerp值。​这样片段着色器中对两张贴图的采样结果做二次计算后,就可以得到正确的颜色了。

 

LDktHa3ctKdyvAJqldD2.gif

为战士和战马分别替换贴图​

 

4、动画的混合

通过纹理实现的动画也可以实现简单的混合效果,它是通过在顶点着色器中对多个动画纹理进行采样,然后根据一个混合比例,对多个位置信息进行计算以实现的。

 

qcPLHM9duEgvr8TRJZv5.gif

根据速度一维向量进行的Locomotion状态混合

 

5、脱离了Renderer的渲染

由于是直接调用了Graphics.DrawMeshInstanced进行的绘制,因此并没有GameObject被创建出来,减少了对象的创建数量,一定程度上也减少了内存及CPU的开销;但是需要自己在loop中组织数据的更新及渲染的更新。

YLU02XJGfbJmJkTu3lmX.png

脱离了GameObject+Renderer的绘制

 

| 使用动画纹理的优点

1、易于理解、易于实现;

2、CPU的计算(合并网格、记录动画信息)发生在编辑器阶段,游戏运行时CPU没有额外的开销;

3、可以实现实例化绘制,充分发挥GPU的绘制效率。

 

| 使用动画纹理的缺点

1、记录顶点动画的纹理大小,一方面取决于模型的顶点数量,另一方面取决于动画的长度,如果顶点数量过多,或动画过长,生成的纹理就会很大,对显存的占用量也会上升;

2、实现动画混合,需要从多个动画纹理中采样并进行计算,采样次数多;

3、无法使用动画状态机控制动作;

4、动作信息在存储时会受保存格式的精度影响,因此读取出来的动画可能不够精确;

5、无法实现骨骼动画中的IK(反向动力学)等。

 

虽然有不少缺点,但是如果你的目的是大批量绘制环境装饰(树、草、石头)或细节要求不高的杂鱼小兵、路人,它都是你实现目的优秀手段,值得你去使用它。

 

| 写在最后

最后,分享一个没有写完的网格合并及实例化绘制工具,可以实现上述简单的功能。

pbG3cYQgQprWgf2iCVyR.png

通过工具生成动画资源文件

 

2j2e8PZetHmQHB5XJqFI.gif

简单的动画播放
 

cKb7f7qQXEDhMZZecnaj.png

大批携带动画角色的实例化绘制


 

工具及Demo下载地址:https://github.com/elsong823/AnimationBaker

 

非常感谢您能读到这里,祝您国庆节快乐,下回见。

 

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

r307WvCiBpg4C105LmwU.jpg


 

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

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

标签: