如何快速将大量游戏对象呈现给玩家?本文将介绍一种通过GPU实现骨骼动画的实例化绘制方法,并简述其实现原理。
有多种通过GPU实现骨骼动画的实例化绘制方法,本文介绍的是其中的一种:将顶点信息逐帧写入纹理后,在顶点着色器中通过读取动画纹理,提取顶点位置并变换,最终实现角色动画的方法。
本文将简述其实现原理,并分享一个(完成了一半的)网格合并及实例化绘制工具。
如何提高绘制效率
当产生了“要将大量游戏对象呈现给玩家”的需求时,我们就会碰到这样一个问题:如何才能提高GPU的绘制效率?
批量绘制较多的骑兵
通常情况下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组件的单位尝试使用实例化绘制以提高效率)。
但是对战场中的小兵做这种简单地操作就不太合适了。
这是因为小兵通常是采用骨骼动画来实现动作的,而骨骼动画对于蒙皮网格的驱动,是CPU即时计算出来的。每个小兵相同时刻的状态可能都不同,也就是说相同网格同一时刻的顶点位置会有很大差别,因此无法直接进行实例化绘制。
既然CPU上即时计算的骨骼动画无法进行实例化绘制,我们就不让CPU计算,而让这些计算发生在GPU上,便可将问题解决。
它的原理很简单
1、将骨骼动画每一帧对网格各个顶点的变化结果存在一张纹理中,其中纹理的横坐标是顶点索引,纵坐标是时间,而横纵相交对应的值,是这一时刻该顶点在本地空间下的坐标。
2、有了这张“顶点动画纹理”,在顶点着色器中,我们就可以忽视传入顶点着色器的顶点位置信息;而以当前所处理的顶点索引为U,以动画播放至此的时间刻度为V,从上一步的纹理坐标中采样。而采样到的结果,就是当前这个顶点此时的位置。
3、接下来的步骤便与传统绘制一样,与MVP矩阵相乘做空间变换,传入片段着色器中着色等...可以很容易的想象到,连续为网格上所有顶点设置不同时间下的空间位置,最终绘制到屏幕上时,就能呈现出动画效果了。
一些相对重要的细节
1、用实例化ID来获取差异实例单位的属性
由于我们的最终目标是绘制多个不同动画状态的单位,因此从动画纹理中,用于采样信息的时间刻度值,是根据实例化ID,从保存实例化属性的数据块中获取到的,这样就可以实现每个实例化单位的动画播放进度的差异。
2、合并多个不同的网格
手动调用实例化绘制接口时,只能传入一个网格。而我们平时使用的游戏对象,通常是由若干个蒙皮网格和若干个普通网格组成。比如一个骑兵模型:士兵和马匹分别是两个蒙皮网格;而士兵手持的武器通常是一个普通网格,以方便后期做武器替换。
一个游戏对象可能会由两种、多个网格组合而成
因此我们会在编辑器模式下,将整个对象包含的网格合并成一个网格,并将这个网格保存成资源,以便后面调用绘制命令时作为实参传入。
合并成为一个网格
3、多贴图时处理UV
此外,有些模型上不同的网格还对应了不同的贴图,比如网格Mesh_0,使用了贴图Texture_0,网格Mesh_1使用了贴图Texture_1,由于网格进行了合并,如果针对合并后的网格使用同一张贴图,便会出现错误。
胯下战马错误的颜色采样
针对这种情况我们要在合并时做特殊处理,一种处理方式是合并多张贴图,如将Texture_0与Texture_1合并,然后偏移原本Mesh_1的uv坐标,但是这要求两张贴图都不能太大,否则无法合并到一张贴图中;另一种方法是仍然保留两张贴图Texture_0和Texture_1,但是对Mesh_0和Mesh_1的uv2做特殊处理,如使用uv2的x保存两张贴图的Lerp值。这样片段着色器中对两张贴图的采样结果做二次计算后,就可以得到正确的颜色了。
为战士和战马分别替换贴图
4、动画的混合
通过纹理实现的动画也可以实现简单的混合效果,它是通过在顶点着色器中对多个动画纹理进行采样,然后根据一个混合比例,对多个位置信息进行计算以实现的。
根据速度一维向量进行的Locomotion状态混合
5、脱离了Renderer的渲染
由于是直接调用了Graphics.DrawMeshInstanced进行的绘制,因此并没有GameObject被创建出来,减少了对象的创建数量,一定程度上也减少了内存及CPU的开销;但是需要自己在loop中组织数据的更新及渲染的更新。
脱离了GameObject+Renderer的绘制
使用动画纹理的优缺点
优点
1、易于理解、易于实现;
2、CPU的计算(合并网格、记录动画信息)发生在编辑器阶段,游戏运行时CPU没有额外的开销;
3、可以实现实例化绘制,充分发挥GPU的绘制效率。
缺点
1、记录顶点动画的纹理大小,一方面取决于模型的顶点数量,另一方面取决于动画的长度,如果顶点数量过多,或动画过长,生成的纹理就会很大,对显存的占用量也会上升;
2、实现动画混合,需要从多个动画纹理中采样并进行计算,采样次数多;
3、无法使用动画状态机控制动作;
4、动作信息在存储时会受保存格式的精度影响,因此读取出来的动画可能不够精确;
5、无法实现骨骼动画中的IK(反向动力学)等。
虽然有不少缺点,但是如果你的目的是大批量绘制环境装饰(树、草、石头)或细节要求不高的杂鱼小兵、路人,它都是你实现目的优秀手段,值得你去使用它。
最后
最后,分享一个没有写完的网格合并及实例化绘制工具,可以实现上述简单的功能。
通过工具生成动画资源文件
简单的动画播放
大批携带动画角色的实例化绘制
工具及Demo下载地址:
https://github.com/elsong823/AnimationBaker
来源:公众号“偶尔学学Unity”
上一篇 如何在平台游戏中实现走路与跳跃?
热门课程
专业讲师指导 快速摆脱技能困惑相关文章
多种教程 总有一个适合自己专业问题咨询
你担心的问题,火星帮你解答想学习UI设计,却不知道哪里的培训机构比较好?本文推荐一家专业的UI设计培训机构,助你快速掌握技能。
想学习平面设计却不知道该去哪里?本文为您介绍学习平面设计的好去处。
想学习VR软件开发但不知道选择哪家?本文为您介绍如何选择学VR软件开发的最佳选择。
想学习后期动画制作技巧?本文详细介绍火星影视后期动画培训,助您成为后期动画专家!
专业的影视美术提升培训机构为学员提供全方位的影视美术技能培训,助力学员在影视美术领域取得成功。
想要学习交互设计却不知道从哪里入手?本文为您提供选择最佳学习路径的指南,助您顺利成为交互设计师。
1. 打开微信扫一扫,扫描左侧二维码
2. 添加老师微信,马上领取免费课程资源
同学您好!