本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
文章目录
上一节我们描述了如何通过减少DrawCall来减轻CPU的资源调度压力。本章中我们将从其他方面来优化性能:
减少需要处理的顶点数目
优化几何体
3D游戏制作离不开3D模型。由于渲染时需要逐顶点逐面片渲染,因此优化3D模型是很重要的。
一般的原则是优化模型的网格结构,减少模型中的顶点数量(面数)
有时,Unity中显示的顶点数量会超过模型本身的顶点数量。这是由于在GPU看来,有时为了计算的需要,会将一个顶点拆分为两个顶点,主要目的有两个:一个是为了分离纹理坐标(uv splits),另一个是为了产生平滑的边界(smoothing splits)
它们的本质其实是对于GPU来说的,顶点的每一个属性和顶点之间必须是一对一的关系。例如一个立方体,它的六个面之间有一些相同的顶点,但是这些顶点相对于每个面的纹理坐标就不同了,因此就会拆分为多个不同纹理坐标的顶点,平滑边界也是类似的。因此一个顶点可能会对应多个法线信息和切线信息。
因此我们需要尽可能减少顶点数量,一个建议是——移除不必要的硬边以及纹理衔接,避免边界平滑和纹理分离。
模型的LOD技术
另一个减少顶点数目的方法是使用LOD技术,LOD技术的原理是为模型准备好几套不同细节程度的模型(纹理),然后根据摄像机和物体间的距离选择为模型加载的LOD层级,Unity就会自动使用对应等级的模型。
一般离得越远则细节程度越低,离得越近细节程度越高
遮挡剔除技术
遮挡剔除就是Occlusion Culling,就是剔除那些我们看不到的物体(面),避免计算不必要的资源。
像摄像机自带的Culling效果,对层级手动设置的Culling效果,包括Shader的背面和正面剔除都是出于这一目的的。
注意区分摄像机的遮挡剔除和视锥体剔除(Frustum Culling),视锥体剔除是剔除不在摄像机视锥内的物体,而遮挡剔除是剔除摄像机视锥内看不见的物体,其原理是用一个虚拟摄像机来遍历场景,并识别那些物体是可见的,哪些是不可见的。
LOD技术和遮挡剔除技术可以同时减少CPU和GPU的负荷,CPU可以提交更少的DrawCall,而GPU需要处理的顶点和面数也少了。
减少需要处理的片元数目
另一个造成GPU的性能瓶颈的原因是需要处理过多的片元。除了我们之前说的减少模型网格顶点数减少片元外,这部分的重点更在于减少OverDraw——即同一个像素的重复绘制。
想象一下,有一个B物体挡在了A物体的前面。若我们先渲染A物体,再渲染B物体,那么同一个像素被渲染了两次,显然A物体是没必要渲染的,就造成了OverDraw,这时我们可以用深度测试来避免这一情况
我们可以再Scene视图下查看overdraw的情况,来判断物体之间的遮挡。
控制绘制顺序
为了避免OverDraw ,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,就可以最大限度减少OverDraw。这是因为,在后面绘制的物体由于无法通过深度测试因此不会重复渲染。
在Unity中,那些渲染队列数目小于2500的(如BackGround ,Geometry,AlphaTest)的对象都被认为是不透明的(Opaque)的物体,不透明的物体通常是从前往后绘制的。
而其他队列例如(“Transparent”,“Overlay"等)则是从后往前绘制的,因为这些队列常常是透明度渲染的,这些渲染就比较麻烦,为了正确的渲染效果我们往往还需要手动控制渲染顺序。
在开发中,我们考虑到摄像机下一些人或物在屏幕上的渲染顺序,为它们设置不同的渲染队列,将节省很多渲染时间。
小心透明物体
对于半透明物体来说,由于它们没有开启深度写入,因此如果想要得到正确的渲染结果,必须从后往前渲染,因此为了正确的渲染效果,半透明物体一定会产生overdraw的。
因此,如果屏幕上存在大量半透明物体或者粒子效果,甚至多层半透明效果相互堆叠的情况,就会有大量的OverDraw,这是我们需要避免的。特别是GUI对象的渲染常常要用到半透明效果。
对于上述的情况,我们可以减少视窗中GUI的面积以减少OverDraw的像素。也可以把GUI的绘制和三维场景的绘制交给不同的摄像机,并且三维摄像机的视角范围尽量不要和GUI的相互重叠。
减少实时光照和阴影
实时光照和阴影的计算是很昂贵的,我们之前讲光照的时候说过,实时光照的渲染(特别不属于主光源的部分,例如点光源)会调用每个照射到的物体上的Pass。导致Pass一下子增多了。
例如一个场景如果包含了3个逐像素的光照,而且使用了逐像素的Shader,那么很有可能将DrawCall的数量提高3倍。
并且无论是静态批处理还是动态批处理,额外的逐像素光照pass都会导致中断批处理!
当然游戏是少不了光照的,少用实时光不代表不用光照效果,我们往往会为模型提前烘焙好一张光照纹理和阴影纹理,根据光照方向来进行计算采样来代替实时光照效果。只有某些动态物体才会使用实时光照
节省带宽
大量使用未经压缩的纹理以及使用过大的分辨率都会造成由于带宽引发的性能瓶颈。
减少纹理大小
使用纹理图集atlas可以帮助我们减少DrawCall的数量(因为图集中的所有纹理都可以通过一次DrawCall提交),除此之外纹理大小也是一个需要考虑的问题。所有纹理的长宽比最好是正方形,而且长宽值最好是2的整数幂,这样很多的优化策略才能发挥最大的作用。
除此之外,我们还可以用多级渐远技术和纹理压缩来优化纹理,当勾选了Generate Mip Maps则生成多级渐远纹理,和LOD技术相似,根据远近来选择渲染的纹理。在GUI和2D游戏中使用的纹理等,都应该为纹理生成相应的多级渐远纹理。
纹理压缩同样可以节省带宽,不同的GPU架构可能有不同的纹理压缩格式,除了有时我们需要纹理特别清晰时(例如GUI)我们不希望对纹理进行压缩。
使用分辨率缩放
过大的屏幕分辨率和糟糕的GPU也会造成带宽压力大, 在特定的设备上,我们还需要对程序指定分辨率缩放。
减少计算复杂度
Shader 的LOD技术
Shader的LOD技术和模型的LOD技术类似,其原理是当Shader的LOD值小于当前的LOD值,该shader才会被使用,否则使用了该Shader的物体不渲染。
在未设置LOD值的Shader中,LOD是无限大的,也就是无论如何都使用。在设定了LOD值之后,我们若想要剔除部分复杂计算的Shader就可以通过设置LOD值来剔除,方法是使用Shader.maximumLOD
或Shader.globalMaximumLOD
来设定允许的最大LOD
代码方面的优化
通常来说,游戏需要计算的数量排序是:游戏对象数量<顶点数量<像素数量。因此我们应当尽可能将操作放在游戏对象上或者顶点上。
例如之前我们采样坐标时放在了顶点着色器上而非片元着色器,就大大提高了计算性能。
(以下说的优化策略在某些设备上不成立,这很正常)
- 尽量使用低精度的浮点数进行运算。最高精度的float类型用于存储顶点坐标等变量,但是像颜色值,归一化值这类范围为[0,1]的我们就会使用fixed,half通常使用与一些标量,纹理坐标等变量,计算速度大概是float的两倍。尽量避免不同精度间的值转换。(特别考虑别在片元着色器中使用高精度计算)
- 对于大多数的GPU,在数据传递阶段,例如用v2f将数据从顶点着色器传递到片元着色器的时候,我们尽可能使用少的插值变量,例如可以用一个fixed4存储,就不用两个fixed2。(有些平台,例如PowerVR例外)
3.尽量不使用全屏的屏幕后处理效果,尽量把多个特效合并在一个Pass中
- 尽量别用if分支语句和for循环语句
- 尽量避免sin,tan,pow,log等复杂数学运算,我们可以用查找表来代替
- 尽量不要使用discard