前言

UI优化核心就是一点:减低DrawCall对于DC(DrawCall)问题来说

是游戏客户端程序员头疼以及及其影响性能的重大原因之一

DrawCall介绍

简而言之,Draw Call 就是你的CPU要求你的GPU绘制一些东西
就是你的CPU在说:嘿,用这些纹理和照明信息在那角落画这张椅子。

1
2
CPU准备好需要绘制的元素,对底层图形程序接口进行调用的过程。
也可以理解为:CPU向GPU发布一条渲染指令,就是一次DrawCall的过程。简称DC。

DC作用

问题是,准备Unity Draw Call会占用CPU大量的时间和精力。Unity必须将场景内容转换为GPU可以理解的格式。这个过程开销最昂贵的部分是设置正确的渲染参数,例如纹理,着色器,网格等。

手动设置渲染参数很繁琐。这就是游戏开发人员引入材质概念的原因。

这是否意味着我们不能一次绘制太多对象?

不是的。

游戏开发人员使用批处理将相似对象的渲染分组到同一个Draw Call中。这样,CPU只需支付一次绘制调用即可渲染多个对象。

在使用批处理时,我们要求GPU一次在这里,那里和后面画三把椅子,而不是在三个不同的时间。

为什么要减少DC

  • 可能有太多Draw Call的信号

    • 弱信号:电池消耗过快

    • 弱信号:设备升温

    • 弱信号:游戏不能顺滑的运行

    • 弱信号:VR用户比以往任何时候都头晕

    • 强信号:Unity Profiler显示了渲染线程瓶颈

总之, DrawCall越高对显卡的消耗就越大。(影响CPU,导致游戏性能下降)

*影响CPU性能的原因:*

1
2
3
当CPU发布一条指令后,传到GPU处理,GPU处理渲染速度很快,对于30条还是300条时间影响不是很大。基本都早早的完成“工作”而处于闲置状态。

而对于CPU而言,每一次 DrawCall 前,CPU 都需要做一系列准备工作,才能让 GPU 正确渲染出图像。而 CPU 的每一次内存显存读写、数据处理和渲染状态切换都会带来一定的性能和时间消耗。这些相对于 GPU 渲染来说非常慢。大量的 DrawCall 会让 CPU 忙到不可开交,而 GPU 大部分时间都在摸鱼,是导致游戏性能下降的主要原因。
  • 它使移植到未来平台变得更加容易。你肯定花了数千小时来开发游戏。这是每个开发者交付游戏都必须支付的基准成本。一旦你支付了该成本,为什么不从移植到其他平台中获利呢?你将花费一小部分成本使销量成倍增长。问题是,游戏的优化程度越高,移植过程中的工作量就越少。
  • 避免亡羊补牢。一开始就对其进行优化!你可能知道优化你的同事一年前创建的十几种资源的感觉,尤其是当你的同事离开项目时。这在Asset Store购物时尤其要注意的是:商店中的大多数资源并不完善,很可能使用多种材质设置。这将导致不可能以移动端作为兼容目标。
  • 它可以使你的游戏提高游戏效率。将使用更少的CPU资源,并获得更好的性能,这在VR中至关重要。你将从玩家的电池中窃取更少的能量。玩家和社区将通过更好的评论和买更多的应用内购来奖励你。用户玩游戏的时间越长,他们看到的广告越多,他们内容付费的速度就越快。

在使用Unity时,默认情况下你会不由自主的添加Draw Call。除非你有意识注意资产的性能,否则资产多数会更倾向于使用不同的材质。随着时间的流逝,不同的材质不断被添加,各种不能合批的Draw Call也会增加。这将会导致性能爆炸。

Batches vs SetPasses

Batches SetPasses
name 绘图调用(Draw Call) 材质更改
introduce 这些是简单的绘制命令 更改材质很昂贵,因为我们必须设置一个新的渲染状态。
exp 在此处绘制此对象,然后在此处绘制另一个对象。这主要是关于使用当前全局渲染状态绘制相同着色器、相似参数的对象。 其中包括着色器参数和管线设置,例如Alpha Blending,Z-Test,Z-Writing。

如果你真的想达到最优,我们启用批处理。批处理喜欢相同的材料。启用批处理可将Draw Call计数减少到1。在这里,我们得到了最理想的输出:❤️1 SetPass,1 Batch❤️️

1
2
3
SetPassCall: 通俗的讲解就是更换重新装在渲染管线里面的Shader代码和配置,就像换画笔一样的。SetPassCall开销非常的大,所以尽可能的要少用一些不同的Shader,再一个场景里面。尽可能的让同一个Shader 绘制最多的物体后再切换下一个Shader。尽量避免绘制物体的时候平凡交叉的来回切换Shader来节约SetPassCall的次数。

可以把Shader 设置为常驻内存缓存,这样节约SetPassCall所带来的开销。

unity DC批处理

我们不希望绘制一个对象10次,而是一次绘制了10个对象。

这就是批处理(batching)的力量。

批处理Draw Call的主要要求是对象使用相同的绘制属性(材质)。在这种情况下,Unity可以将不同的网格合并为使用统一材质的单个网格。

就想前面说的,默认情况下,大多数资源将使用不同的材质。但是不用担心,我们将看到将几种材质合并为单个方法。

下面是一张流程图,总结了在Unity中进行合批的方法和限制

个人补充:静态批处理复杂场景不要用,可能会导致渲染队列混乱,产生严重的OverDraw

img

你的切入点是要查找要那些对象需要使用相同的材质

使用相同的材质是批处理工作的前提。不同的材质具有不同的绘图设置,这些设置会更改全局GPU渲染状态。

如果这些对象不使用完全相同的材料,但它们足够相似,则必须将它们合并为一个。这通常涉及创建共享的纹理图集,并更新单个对象的UV坐标以指向新的正确位置。下面会提到一些可以帮助你的工具

一旦你的对象使用相同的材质,便可以选择许多方法来批处理这些Draw Call。

我建议您使用的批处理技术取决于要批处理的对象的性质。

合并Unity 材质(Materials)

合并材质的第一个要求是:

要批处理的对象的必须使用相同的着色器材质

更换当前着色器(Shader)是你可以执行的最昂贵的操作之一。这会大大降低渲染速度。

几乎每个游戏都必须在某种程度上更换着色器,这很正常,现在你知道它的切换成本,就应该尝试减少项目中的着色器数量(包括着色器变体)。

如果你可以将两个相似的着色器合并到同一着色器中,那么你将获得巨大的性能优势。

因此,第一步是尽可能从项目中减少着色器。处理完成之后,你会得到许多原始材质,即使它们使用相同的Shader,看起来的效果和之前也没什么区别。

一旦目标对象使用相同的着色器,下一步就是合并其材质。这可能很复杂,因为他们可能具有不同的材质参数,例如:

  • 纹理:每种材质通常具有一个或多个与其他材质不共享的纹理。在不同材质上使用相同纹理的一种方法是创建包含所有单独纹理的较大纹理。这些纹理称为图集。
  • 材质参数:例如金属,镜面反射和其他参数。要合并这些值,你可以找到适合所有条件的共同平均值,也可以在特定通道中创建包含该值的纹理图集。你可以将3个或4个纹理通道用于不同的参数,例如将金属值存储在红色通道中。

现在,你有了多个具有相同材质的对象,但它们必须具有不同的参数,则可以把他们收集成为MaterialPropertyBlock。你可以为每个需要自定义参数的渲染器创建MaterialPropertyBlock,而不是创建单个材质实例。然后,你可以在每个Block中设置自己的参数。虽然这不会减少Draw Call的次数,但是会降低渲染的成本,因为你明确地告诉了Unity每个对象的不同之处。

为共享着色器的材质创建纹理图集通常遵循以下几个步骤:

  1. 创建一个大纹理,我们将其称为纹理图集
  2. 获取所有材质的纹理通道,并将其纹理复制到新创建的纹理图集中。
  3. 遍历使用这些材料的网格以重新计算其UV。新的UV将指向包含原始纹理的纹理图集的新的子区域。
  4. 禁用旧的网格,然后使用具有更新的UV的新网格
  5. 使用合并过的材质替换原本材质。
  6. 对着色器使用的每个纹理属性重复所有这些步骤。

我建议你在3d软件中执行此操作。如果有时间的话,这是最好的方法,因为它可以使你更好地控制过程。这可以提高输出质量,因为你可以调整关键变量,例如纹理像素分辨率。你还可以应用更高级的技术,例如调色板。

unity 减少DC技术

  • 动态合批
  • 静态合批
  • 降低shader的等级特性
  • 场景优化策略——遮挡技术。
  • rectMask2D替代Mask
  • Unity静态批处理(Unity Static Batching)
  • Unity GPU Instancing
  • Unity动态批处理(Dynamic Batching)
  • Unity运行时批处理API

合批

一次Draw Call中批量处理多个物体。只要物体的变换和材质引用相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。

    注意:简单来说在一个Canvas下,需要相同的材质,相同的纹理以及相同的Z值。例如Ul上的字体Texture使用的是字体的图集,往往和我们自己的UI图集不一样,因此无法合批。还有UI的动态更新会影响网格的重绘,因此需要动静分离。

静态合批 (内存换性能)

  • 需要做的事情

    1
    把要进行静态批处理的GameObject在Inspector面板右上角的Static勾选(实际上只需要勾选Batching Static即可)

img

你可以在Player Setting下找到这个设置,如图所示。选择您要启用的目标平台。

请注意,稍后在player settings中也会启用/禁用动态批处理(Dynamic Batching)。

更准确地说,Unity将查找启用了batching static标志的对象。然后,Unity将尝试合并公用材质的对象。

Unity静态批处理通过创建包含各个网格的巨大网格来工作。但是Unity也会保持原始网格的完整,因此我们仍然能够单独渲染它们。这样我们可以仅绘制可见视野内的对象,而丢弃不可见的对象,使得视锥裁切正常工作。

  • 优点

因为只需要进行一次,所以性能会比动态批处理要好。

  • 缺点
1
使用静态合批需要额外的内存开销来存储合并后的几何数据。

静态批处理的主要限制是每批可以具有的顶点和索引的数量,通常为每个64k,可以在此处检查限制更新(如果有)。

1
因为需要额外维护多一份数据,所以包体会变大,占用的内存也会变多(不能有超级大量的相同模型(如:森林里的树))

静态批处理的缺点是增加了内存使用量。如果您有100个石头,每个石头模型占用1MB,则可以预期内存使用量将超过100MB。发生这种情况的原因是,巨大的批处理网格将所有石头一起包含在一个网格中。

1
2
3
进行了静态批处理之后的GameObject不能在游戏运行时改变位置或者是跟渲染有关的属性。并且因为把所有要静态批处理的GameObject都合并成一个大网格保存起来,所以这实际上相当于即使是同一个GameObject,也需要复制一份网格数据一起保存在这个大网格的顶点数据里面去,这样就导致了占用的内存变多了。

静态合批就是多渲染一套合并后的网格 ,提前存在内存里,内存当然就大了。
1
无法移动

将static的静态物体(永远不会移动、旋转和缩放) ,如果相同材质球,面数在一定范围之内。unity会自动合并成一个batch送往GPU处理。

  • 原理
1
2
3
在开始阶段把需要静态批处理的GameObject进行一次网格合并操作,然后把这个合并之后的大网格保存起来,后续都是用这个网格而不需要再进行合并。

在预处理阶段,把一些材质相同的模型的顶点统一变换到世界空间坐标下,并且新构建一个大的VB把数据保存下来,在绘制时,就会把这个大的VB提交上去,只需要设置一次渲染状态,再进行多次drawcall绘画出每个子模型。 所以Static Batching是不会减少drawcall的,但由于只修改了一次渲染状态依然可以减少CPU的消耗。而且在渲染前,也可以进行视锥体剔除,减少顶点着色器对不可见的顶点的处理次数,提交GPU的效率。
  • 静态合批得限制

使用静态合批虽然可以提升游戏性能,但是设置为静态的物体在整个游戏中就不能再运动了,强行使它们运动会出问题。而且即使按照以上步骤进行了静态合批,也不一定保证会成功,必须满足以下全部条件,静态合批才会成功:

1、游戏对象处于激活状态。
2、游戏对象有一个Mesh Filter组件,并且该组件已启用。
3、Mesh Filter组件具有对网格的引用。
4、网格已启用Read/Write功能。
5、网格的顶点计数大于0。
6、该网格尚未与另一个网格组合。
7、游戏对象有一个Mesh Renderer组件,并且该组件已启用。
8、网格渲染器组件不将任何材质与DisableBatching标记设置为true的着色器一起使用。
9、要批处理在一起的网格使用相同的顶点属性。例如, Unity可以将使用顶点位置、顶点法线和一个UV的网格与另一个UV进行批处理,但不能将使用顶点定位、顶点法线、UVO、UV1和顶点切线的网格进行批处理。

Unity GPU Instancing

如果我们有这些Draw Call:

  • 绘制动态石头1
  • 绘制动态石头100

然后使用GPU Instancing将它们转换为一个Draw Call:

GPU实例化让你可以非常高效地绘制相同的网格几次。Unity通过向GPU传递转一个Transform列表来做到这一点。毕竟,每块石头都有自己的位置,旋转和缩放。

与静态批处理相比,这是一项强大的技术,因为它不会激增内存使用量,并且不需要对象是静态的。

但是,创建Transform列表会降低性能。如果在游戏过程中没有物体移动/旋转/缩放,则只需支付一次此开销。但是,如果对象每帧都更改一次,则需要每帧支付一次开销。

推荐一个插件:GPUInstance比Unity默认的要好用的多。

动态合批

你可以对使用不同网格物体的动态对象进行动态批处理。

1
如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。动态批处理操作是自动完成的,并不需要你进行额外的操作。
  • 优点

不用自己做任何事情,Unity会在游戏中自动进行动态批处理,只要满足下述条件。

Unity动态批处理受到更加严格的限制。你只能将其应用于具有少于300个顶点和900个顶点属性(颜色,UV等)的网格。材质也应使用single-pass着色器。此处有完整的限制列表。

出现此限制的原因是在运行时创建这些批处理的CPU性能成本。与单独发出绘图调用相比,超过300个顶点很难证明批量CPU的成本合理。

1
2
3
4
5
1. 顶点属性要小于900。例如,如果shader中需要使用顶点位置、法线和纹理坐标这三个顶点属性,那么要想让模型能够被动态批处理,它的顶点数目不能超过300。因此,优化策略就是shader的优化,少使用顶点属性,或者模型顶点数要尽可能少。(这个是《UnityShader入门精要》这本书上说到的,同时书上也说了不一定是900,可能不同版本的Unity会有所区别,这个可以自己在Unity中去手动验证得出)

2. 多Pass的shader会中断批处理。

3. 使用LightingMap的物体需要小心处理。为了让这些物体可以被动态批处理,需要保证它们指向LightingMap中的同一位置。
  • 缺点

不仅如此,动态批处理非常不可预测。你无法真正确定对象将如何被批处理。结果通常会随着帧的变化而变化。打开Unity Frame Debugger并查看结果,在每帧之间动态批处理的结果发生巨大变化是令人困惑的。

  • 原理
1
2
3
4
Unity会检测哪些GameObject使用了同一个共享材质,然后去合并这些使用了同一个共享材质的网格顶点数据,形成一个新的大网格,然后传给显存,直接渲染这个大网格就相当于渲染了所有的被合并的小网格,而这只需要一次DrawCall。

在每一帧运行时,计算相同材质的模型,把他合并批次进行渲染。动态合批只需要设置一次渲染状态,且能减少drawcall次数。

为什么用了不同的材质,不同的贴图,不同的材质属性等会导致不能动态批处理呢?(OpenGL)

 以OpenGL为例(DirectX也是同理)。OpenGL中要渲染一个东西出来的,需要顶点着色器和片元着色器,这个对应的是ShaderLab中的顶点着色器和片元着色器,然后还需要把要渲染的网格的顶点属性作为一个数组绑定到VBO(顶点缓存对象)中去,然后绑定VAO(顶点数组对象)并设置顶点数组的属性,然后一些需要外部设置的着色器属性也是在这个阶段进行设置,当做完这些之后,再调用glDrawElements(也就是一次DrawCall)去渲染这个物体。这就是OpenGL渲染一个东西的最简单的流程。所以回到问题,为什么贴图,材质属性等必须一样呢?因为如果不一样的话,他们就不能通过一次DrawCall去设置这些属性和贴图了,想想看,如果A物体使用了贴图A,B物体使用了贴图B,如果把他们合并成一个大网格,要直接在一个DrawCall里渲染出来的话,这个合并好的大网格到底该用贴图A还是贴图B呢?无论使用哪个,都是不对的。所以A物体和B物体不能进行批处理。

Unity运行时批处理API

UI图集的作用

​ 图集就是碎图合成大图 降低内存,减少dc。

1
2
3
UI图集有合批没有的优点,就是热更新的时候因为小文件变少了,所以会快一些。

UI图集就是UI的动态合批。

UI图集完成合批的条件是什么?

    深度 贴图 材质  =>  排序好的列表当前这个依次和前面对比是否贴图和材质ID相同决定是否合批。

Unity游戏开发客户端面经——性能优化(初级)

Unity基础:DrawCall从入门到精通 - 知乎 (zhihu.com)