概述
对于一个游戏来说,它主要需要两种计算资源:CPU和GPU,CPU主要负责帧率,GPU主要负责分辨率。
主要影响因素:
(1)CPU
-
过多的draw call
-
复杂的脚本或者物理模拟
(2)GPU
-
顶点处理
- 过多的顶点
- 过多的逐顶点计算
-
片元处理
-
过多的片元(既可能是由于分辨率造成的,也可能是overdraw造成的)
-
过多的逐片元计算
-
(3)带宽
- 使用了尺寸很大且未压缩的纹理
- 分辨率过高的帧缓存
主要优化技术
(1) CPU优化
- 使用批处理技术(batching)减少draw call数目
(2) GPU优化
- 减少需要处理的顶点数目
- 优化几何体
- 使用模型的LOD(Level of Detail)技术
- 使用遮挡剔除(Occlusion Culling)技术
- 减少需要处理的额片元数目
- 控制绘制顺序
- 警惕透明物体
- 减少实时光照
- 减少计算复杂度
- 使用shader的LOD(Level of Detail)技术
- 代码方面的优化
(3)节省内存带宽
- 减少纹理大小
- 利用分辨率缩放
CPU优化(batching)
动态批处理
动态批处理的原理是,每一帧把可以进行批处理的模型网格进行合并,再把合并后的数据传递给GPU,然后使用同一个材质对其渲染。
限制
1,900个顶点以下的模型。 2,如果我们使用了顶点坐标,法线,UV,那么就只能最多300个顶点。 3,如果我们使用了UV0,UV1,和切线,又更少了,只能最多150个顶点。 4,合并网格的材质球的实例必须相同。即材质球属性不能被区分对待,材质球对象实例必须是同一个。 5,如果他们有Lightmap数据,必须相同的才有机会合批。 6,使用多个pass的Shader是绝对不会被合批。因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会。 8,延迟渲染是无法被合批。
静态批处理
静态合批是勾选Static,Unity在Build的时候,会自动下生成合并的网格,并将它以文件形式存储合并后的数据,这样在当场景被加载时,一次性提交整个合并模型的顶点数据,根据引擎的场景管理系统判断各个子模型的可见性。然后设置一次渲染状态,调用多次Draw call分别绘制每一个子模型。
2.1.1 使用
PlayerSettings中开启static batching,对需要静态合批物体的Static打钩即可
2.1.2 前提:
共享相同的材质 运行时不能移动,旋转或缩放
2.1.3 优点
静态合批采用了以空间换时间的策略来提升渲染效率。
静态合批并不减少Draw call的数量,但是由于我们预先把所有的子模型的顶点变换到了世界空间下,并且这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API会缓存绘制命令,起到了渲染优化的目的。另外,在运行时所有的顶点位置处理不再需要进行计算,节约了计算资源。
2.1.4 缺点
- 打包之后体积增大,应用运行时所占用的内存体积也会增大。
- 需要额外的内存来存储合并的几何体。
- 注意如果多个GameObject在静态批处理之前共享相同的几何体,则会在编辑器或运行时为每个GameObject创建几何体的副本,这会增大内存VBO会增大的开销。例如,在密集的森林级别将树标记为静态可能会产生严重的内存影响。
- 静态合批在大多数平台上的限制是64k顶点和64k索引
共享材质
无论是动态批处理还是静态批处理,要求共享同一个材质。这意味着只要调整了参数,就会影响到所有使用这个材质的对象。如果想要细微的调整,就得利用网格顶点数据来存储这些参数(放到VBO中)。
(注:不仅仅是同一种材质,而是同一材质的一个特定实例)
批处理注意事项
-
优先静态批处理,小心内存消耗
-
使用动态批处理时,小心顶点属性和数量
-
批处理需要把多个模型变换到空间坐标下再合并,shader中不要有在模型坐标下的计算
(注:可以在shader中使用DisableBatching使其不会被批处理)
-
半透明物体需要从后往前绘制,如果做不到则不会被批处理
GPU优化
减少需要处理的顶点数目
优化几何体
Unity里显示的顶点数目一般大于建模软件中模型的顶点数量。这是因为Unity站在GPU的角度上去计算顶点数目的,在GPU看来,有时需要把一个顶点拆分成两个或者更多的顶点。一个顶点拆分成一分为多的原因主要有两个:分离纹理坐标(uv splits),产生平滑边界(smoothing splits)
- 分离纹理坐标:同一个顶点的纹理坐标可能并不相同**(在不同的面上)**,对于GPU来说,它必须把这个顶点分成多个顶点
- 产生平滑边界:在多个面交叉处的顶点可能有多个切线和法线,所以也要拆分成多个顶点。
所以建模师要尽可能减少模型中三角面片的数目,对模型没有影响的顶点删除
模型的LOD技术
另一个减少顶点数目的方法是使用LOD技术,这种技术的原理是,当一个物体离摄像机很很远时,减少模型上的偏远数量,
在unity中,可以使用LOD Group组件来为一个物体构建一个LOD。我们需要为同一个对象准备多个包含不同细节程度的模型,然后把它们赋给LOD Group组件中不同等级,Unity就会自动判断当前位置上需要使用哪个等级的模型。
遮挡剔除技术
“遮挡剔除”过程可防止 Unity 为那些被其他游戏对象完全挡住(遮挡)的游戏对象执行渲染计算。
摄像机在每一帧中执行剔除操作,这些操作会检查场景中的渲染器,并排除(剔除)那些不需要绘制的渲染器。默认情况下,摄像机执行视锥体剔除,这一过程将排除所有不在摄像机视锥体范围内的渲染器。但是,视锥体剔除不会检查渲染器是否被其他游戏对象遮挡,因此 Unity 仍会浪费 CPU 和 GPU 时间进行在最终帧中不可见的渲染器的渲染操作。遮挡剔除将阻止 Unity 执行这些徒劳的操作。
常规视锥体剔除将会渲染摄像机视野内的所有渲染器。
遮挡剔除将会删除被更近的渲染器完全遮挡的渲染器。
要确定遮挡剔除是否有可能改善项目的运行时性能,请考虑以下事项:
- 防止无意义的渲染操作可以节省 CPU 和 GPU 时间。Unity 的内置遮挡剔除在 CPU 上执行运行时计算,这可能会抵消其节省的 CPU 时间。因此,当项目因过度绘制而具有 GPU 密集型特征时,遮挡剔除最有可能提高性能。
- Unity 在运行时将遮挡剔除数据加载到内存中。必须确保有足够的内存来加载此数据。
- 当场景中一些界限明确的小区域被实体游戏对象彼此隔开时,遮挡剔除的效果最好。一个常见的例子是通过走廊连接的房间。
- 可以使用遮挡剔除来遮挡动态游戏对象,但动态游戏对象不能遮挡其他游戏对象。如果项目会在运行时生成场景几何体,则 Unity 的内置遮挡剔除不适用于该项目。
减少需要处理的片元数目
另一个造成GPU瓶颈的是需要处理过多的片元。这部分优化的重点在于减少overdraw。简单来说,overdraw指的就是同一个像素被绘制了多次。
控制绘制顺序
- 不透明物体 从前到后, 半透明物体从后到前
- FPS第一人称射击游戏这类的,主角人物的shader第一个绘制
- 天空盒最后绘制
警惕透明物体
-
少用透明度测试改用透明度混合,discard会使硬件优化策略失效
(注:基于瓦片的延迟渲染技术会在fragment shader之前检查该片元是否真正需要被渲染,但之后fs里面的透明度测试改变了片元是否被渲染,就会使该策略失效)
-
GUI中也少用半透明图片,并且减少它们在屏幕中的占比面积
减少实时光照和阴影
-
少用实时光照和阴影,增加了drawcall和overdraw,并且不利于批处理
-
静态物体多用烘焙的方式,利用lightmap和LUT技术等
带宽优化
减少纹理大小
-
不要使用特别大的纹理图,一般小于或等于 256×256 为适合。同时图片的尺寸要为 2 的 n次方,这样不仅能保证所有的设备都识别该纹理,而且计算速度也是最快的。
-
使用压缩纹理。压缩纹理比非压缩纹理具有更快的运算速度和更小的存储空间要求,而且很容易使用图形硬件纹理缓冲。
-
对于大场景贴图而言,进行纹理采样时要尽量使用 mipmap,虽然这样相对而言会占用一些内存,但是,这样会提高纹理采样的效率,同时会得到更好的画面效果。
利用分辨率缩放
- 调整分辨率大小,对特定机型进行分辨率的缩放,防止发热等现象出现。
计算上的优化
着色计算的位置优化
与着色计算相关的任务有 3 个可能的执行位置: CPU、顶点着色器及片元着色器。从获得更高画面质量考虑,很多开发人员会把大量的着色计算相关代码放在片元着色器中。但在不影响画面质量或可以略微牺牲一点画面质量的情况下,可以考虑将相关代码的位置做一些变化,以换取性能的提升,主要包括如下两点。
- 每当把计算任务安排到片元着色器中时,都应该考量一下:若将这个计算任务安排到顶点着色器中,画面质量会不会有影响,若有影响,在不在可以接受的范围内。如果条件允许,则应该将相应的计算任务安排到顶点着色器中进行。因为顶点着色器的执行频率远低于片元着色器,这样做一般可以获得较为明显的性能提升。
- 每当把计算任务安排到顶点着色器中时,要首先判断此计算任务是对于每个顶点单独计算、结果不同,还是所有顶点共享一个相同计算结果。如果是所有顶点共享一个相同计算结果的情况,则应该将此计算任务交由CPU 执行,然后由宿主程序将计算结果作为一致变量传入顶点着色器供使用。
- 注: 在有些情况下,对于某个复杂计算的某部分是所有顶点共享的,那么,就应该把这部分计算提取出来交由 CPU 一次完成,然后将计算结果作为一致变量传入顶点着色器供使用。对于一个 3D 模型而言,顶点少则数百上千,多则几万、几十万甚至上百万,这样做可以降低计算负载,获得显著的性能提升。
着色计算的代码优化
编写着色器代码时,开发人员应该特别小心,这是由于着色器代码的执行频率很高,因此应该尽量优化这部分代码,使其运算量以及复杂度降到最低,这样会提高着色程序的执行效率。具体需要注意以下几点。
- 编写着色代码时常常会进行一些计算,计算时要尽量通过代数方法简化计算,这样可以最大限度地提高运算速度。例如, p =sqrt(2 * (X+1))可以改写成 1.414 * sqrt(X+1)等。
- 编写着色代码时常会对向量进行操作,例如,将向量归一化,求两个向量的点积、叉积等。对于这些操作,着色器提供了强有力的支持,内置了很多内建函数,方便开发人员使用。例如,归一化向量运算函数 normalize、求点积运算函数 dot、求向量折射函数 refract 等。这些函数大部分都是硬件实现的,开发时可以直接利用,能够降低代码的复杂度,优化计算速度。
- 编写着色代码时经常会对矩阵进行操作,例如,将两个矩阵进行相乘运算,对于此类操作,着色器同样也提供了强有力的支持,内置了很多运算及函数,方便开发人员使用。例如,两个矩阵相乘直接使用“ * ”即可完成,这样无论从编码复杂度还是运算速度来看,效果都很不错。
其他tips
- 尽可能不要使用分支语句和循环语句
- 尽可能避免使用类似sin、tan、pow、log等极为复杂的数学运算。我们可以使用查找表LUT来作为代替。
- 尽可能不要使用discard操作,因为这会影响硬件的某些优化。