渲染优化:1、提高渲染性能的方案

  • 一、什么是Drawcall? 什么是SetPassCall?

Drawcall是CPU向GPU发送绘制命令的接口调用。理论上每一个不同材质的物件需要渲染在屏幕上时,CPU都会调用图形API ( openGL or Diract3D ) 的Draw接口触发显卡进行绘制。
SetPassCall是unity后来提出的概念,现代计算机拥有更加优秀的硬件和API,认为提交渲染请求不昂贵,提交渲染所需数据和改变渲染状态更昂贵,所以又了此概念。SetPassCall :在绘制此 Pass 前,需要设置的所有状态配置、或是BUFFER设置,都算是 SetPassCall 的内容,或是叫:SetGPUDataBeforeDraw 会更适合理解(在绘制前设置GPU数据,这些数据包括渲染系统,如:DX 或是 OpenGL 的状态值,或是 Buffer 数据)。

  • 二、为什么优化?

Drawcall对硬件和驱动而言,要求大量设置状态(使用哪些顶点、哪些shader等)和状态转换。而Drawcall最大的消耗在于:如果每次drawcall只提交少量的数据将导致CPU瓶颈,CPU无法将GPU填满。Drawcall对GPU的耗费在于硬件一直等待CPU提交数据,而无法得到有效利用。GPU大量的时间耗费在不断切换状态和正确性检测上。 GPU在Draw Call之间,为了防止前后Draw的依赖关系造成绘制错误或者资源竞用,一般会在Draw Call后Flush整个流水线,小粒度的Draw Call对GPU流水线来说是个很大的浪费。(这个问题在D3D老版本存在,在新版D3D11中得到改善。)实际上unity官方指出,Drawcall数量的降低并非重点,重点是减少批次(SetPassCall)的数量,Drawcall优化实际上是对批次数量的优化。

  • 三、如何优化?

在Unity中渲染优化主要有以下几个策略:Drawcall batching,合并打包图集,减少光照和阴影以及遮挡剔除和视锥剔除等。以下分别谈一下各个策略的优缺点。
总体上有三个大方向原则:
1.减少CPU和GPU的数据交换:

  • 合批(Batch)

  • 减少顶点数、三角形数

  • 视锥裁剪

    • BVH
    • Portal
    • BSP
    • OSP
  • 避免每帧提交Buffer数据
    CPU版的粒子、动画会每帧修改、提交数据,可移至GPU端。

  • 减少渲染状态设置和查询
    例如:glGetUniformLocation会从GPU内存查询状态,耗费很多时间周期。
    避免每帧设置、查询渲染状态,可在初始化时缓存状态。

  • 启用GPU Instance

  • 开启LOD

  • 避免从显存读数据

2.减少过绘制:

  • 避免Tex Kill操作
  • 避免Alpha Test
  • 避免Alpha Blend
  • 开启深度测试
    • Early-Z
    • 层次Z缓冲(Hierarchical Z-Buffering,HZB)
  • 开启裁剪:
    • 背面裁剪
    • 遮挡裁剪
    • 视口裁剪
    • 剪切矩形(scissor rectangle)
  • 控制物体数量
    • 粒子数量多且面积小,由于像素块机制,会加剧过绘制情况
    • 植物、沙石、毛发等也如此

3.Shader优化:

  • 避免if、switch分支语句
  • 避免for循环语句,特别是循环次数可变的
  • 减少纹理采样次数
  • 禁用clip或discard操作
  • 减少复杂数学函数调用

下面就其中一些技术方案进行详细阐述。

  • 四、具体优化技术
(一)Drawcall Batching 合批技术

Unity中对Drawcall的批次有两种:静态批次(static batching)和动态批次(dynamic batching)。但不论静态批次还是动态批次都要求对象的材质是共享的,即不同材质的对象是无法进行批次的。而且要注意的一点:如果在脚本中调用材质时,使用Renderer.material会造成材质的拷贝,而使用Renderer.sharedMaterial来调用则不会拷贝材质。

1. 静态批次 Drawcall static batching
  场景中的多个物件如果是不移动的(包括位置、缩放、旋转等),并且共享同一材质,比如地形、建筑、花盆等,那么可以选择采用静态批次。静态批次只需要在Inspector勾选static选项即可。静态批次需要注意的是,unity会将进行批次的多个对象合并成一个大的对象,也会导致内存损耗,有时候要避免太多对象静态批次造成的内存过高。这也表明,优化并非绝对做好某一方面,而是平衡各个硬件的瓶颈和效率,选择相对适中的方案。
具体做法:在场景中需要静态合批的模型,勾选static选项,并且PlayerSetting里勾选上Static Batching。

2. 动态批次 Drawcall dynamic batching
  动态批次是运动的物件在unity中也可以进行批次渲染,动态批次不需要手动设置,是unity自动进行的,但是这里有诸多陷阱和约束,开发者需要遵守一定的限制条件才能享受动态批次的好处。
具体做法:PlayerSetting里勾选上Dynamic Batching即可。然后遵循一下的限制条件。版本更迭会后会有所变化。
  根据unity官方文档描述:
  1) . 动态批次是逐顶点处理的,因此仅对少于900个顶点的mesh有效。如果shader使用了顶点位置,法线和UV那么仅支持低于300顶点的mesh,而如果shader使用了顶点位置,法线、UV0、UV1和切向量,则之多仅支持180顶点。
  2) . 缩放问题
  缩放对于批次是有影响的,这里涉及到一个统一缩放和非统一缩放的概念。统一缩放即为三轴同比例缩放,比如(1,1,1),(2,2,2)(5,5,5)... 非统一缩放即为三轴不同比例缩放,如(1,2,1)(2,1,1)(1,2,3)等等。
  Unity对统一缩放的对象是不进行动态批次的,而对非同一缩放的对象是可以进行动态批次的。这里有点诡异,查阅了一些资料,解释如下:
  对于非同一缩放的物件,unity将其mesh进行了复制,因此即便是从相同物件进行的非同一缩放的两个对象是两份mesh;对统一缩放的对象来说,unity不对mesh进行复制,而是使用同一mesh进行缩放,此时复制mesh来进行批次渲染是不值得的,但是对于非统一缩放的对象,既然已经复制了mesh(不是为了批次,而是其他原因决定复制mesh),那么进行批次是顺带实现的。
(参考 Dynamic Batching and Scale ——unity3d answers
  3) . 使用了不同的材质,即便实质上是相同的(比如两个一模一样的材质),也不会进行批次。
  4) . 拥有lightmap的物件含有额外的材质属性,比如lightmap偏移和缩放系数等,所以拥有lightmap的物件不能批次。
  5) . 多通道的shader会妨碍批处理操作,接受实时阴影的物件无法批次。
  注意:**** unity渲染是有顺序的,渲染排序有可能打断动态批次。
  例如:
  场景中有物件ABC,假设AB使用同一材质1,C使用材质2.那么drawcall有可能是2个,也有可能是3个。
如果顺序为:

  • 渲染A,使用材质1
  • 渲染B,使用材质1
  • 渲染C,使用材质2
    那么drawcall是2个,AB进行了动态批次。
    如果顺序为:
  • 渲染A,使用材质1
  • 渲染C,使用材质2
  • 渲染B,使用材质1
      那么drawcall就是3个,AB的批次被C打断了。
    渲染顺序跟什么有关呢?
    首先根据物件到摄像机的距离,进行远处物件先渲染近处物件后渲染。相同材质的物件尽量在一层,不要让不同材质的物件进入这一层。如果无法保证这一点,那么还有一种方法:修改shader中渲染队列值。即打开shader 将subshader中的tag{}中queue 修改为小于2500的值。
    渲染队列小于等于2500时,unity认为其是不透明的,对于不同材质但z值相同对象,unity不对其进行排序,这样能保证相同材质的多个对象能是一个批次,不同材质的对象如果进入两个相同材质的对象之间,不会打破批次;
    渲染队列大于2500时,unity会对不同材质的对象进行排序,此时如果不同材质的对象进入到两个相同材质的对象之间的话,会使相同材质的对象批次被打破。
      批次先写到这,其实很多网上都有,不过有些没深入讲解,也有些没给出解决办法,我就使用每个方案时遇到的困难给出了自己的解决方案。其实批次还有不少研究的地方,之后想到了会继续更新。

延伸阅读:

· Unity -Draw Call Batching

· Unity Drawcall 优化手记

(二)合并图集

其实合并图集也是利用了Unity的Drawcall batching。将多个纹理进行打包成图集是为了减少材质,这样多个对象共享一个材质,并进而使用同一个纹理和shader,触发unity的动态批次。图集打包工具有很多,Asset store中也可以搜到不少,比如Texture Packer FreeDrawCall Optimizer(收费) Mesh Baker Free 等等都可以将贴图打包合并。但是合并图集也有缺点,合并贴图时应该注意选择同时出现在屏幕的对象贴图进行合并。如果不能做到这一点,那么合并图集可能起到反作用,即渲染一个对象需要加载过多无用贴图,造成内存占用率升高。合并图集再打包优化的时候也有提到。

(三)光照和阴影

实时光照和阴影可能增加Drawcall,带有光源计算的shader材质会因为光照产生多个Drawcall。使用灯光会打断Drawcall batching,尽量使用烘焙灯光贴图等技巧来实现灯光效果。

延伸阅读:

· Forward Rendering Path Details

· Light Troubleshooting and Performance

(四)遮挡剔除、视锥剔除

这两个Unity提供的剔除方案,也叫LOD技术,就是将不在摄像机视野之内的物体,不对这些对象渲染。

(五)网格合并技术

这个技术实际上是静态合批的延伸,静态的本质就是将“能够合批”物体的所有的网格按照它的位置预先重新合并生成一个大的新网格对象存到内存, 由于这些物体满足合批条件,都是同一个材质球,这样渲染这些物体只要把合并后的这个新网格对象一次提交给GPU,那么就能够实现把这些物体合批处理,降低Drawcall。
但是静态合批后的物体,不能够移动,所以利用外部工具,事先就对模型进行合并,然后再使用。
静态合批技术实际上,降低的是SetPassCall数。

(六)合并材质

这几技术实际上也是合批技术的延伸,只不过也是用外部工具的方式,事先把一个场景的贴图,全部材质搞成一个,贴图也合并在一起。

(七)巧用面片替代模型

1、在一些摄像机不会转动,固定视角的场景,可以考虑使用图片或者简单片面来代替具体的模型场景,达到以假乱真的效果。
2、很远很模糊的地方,使用面片的方式来替代。
3、把一些超远景的效果做到天空盒里面

(八)shader层面优化

1.shader中用贴图混合的方式去代替多重通道计算。
2.shader中注意float/half/fixed的使用。
3.shader中不要用复杂的计算pow,sin,cos,tan,log等。
4.shader中越少Fragment越好。
5.避免if、switch分支语句
6.避免for循环语句,特别是循环次数可变的
7.减少纹理采样次数
8.禁用clip或discard操作
9.减少复杂数学函数调用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。