【Unity】渲染性能优化

您所在的位置:网站首页 spriterenderer合批 【Unity】渲染性能优化

【Unity】渲染性能优化

2023-05-17 08:59| 来源: 网络整理| 查看: 265

上一篇简单介绍了渲染阶段上有那些性能点,这一篇来介绍一下这些性能点是怎么产生的,并且该怎么解决。

造成渲染性能点的原因以及如何解决1、CPU计算和收集渲染所需数据组装描述符和材质 阶段的性能点

物体的合批。如果场景中存在大量的琐碎的物体,那么 CPU 在做视锥体剔除 和 收集这些物体的渲染信息时的耗时就会增加。对于合批通常使用这三种方法:建模时就手动合并网格、静态合批、动态合批。

对于建模合批:这里不建议在建模时就把各种物体的网格合并为一个,因为这样做会产生一个巨大的且有很多顶点数的物体,由于其体积过大,CPU无法在视锥体剔除阶段将其剔除,那么就会有过多的网格信息需要从CPU传到GPU,此时容易引发带宽问题。这里推荐的做法是建模时要思考个体与整体的可见性问题,例如:对于书架里的书,当里面一本书可见时,通常一组书都可见,那么就可以把一组书的网格合并为一个,甚至当一本书可见时通常整个书架都可见,所以书和书架的网格都可以在建模时就合并在一起。

对于静态合批:对于静态不会动的物体,在Unity中我们可以将其标记为 Static,即静态物体,那么在游戏运行时,Unity就会对使用相同材质的静态物体的网格进行合并。注意:Unity并不是简单的把所有静态物体都合并为一个网格。 Unity在静态合批时也会考虑到视锥体剔除问题,参与静态合批的物体自身会有一个索引,反过来根据索引我们也可以找到具体的物体对象,那么就可以动态的决定该对象是否隐藏(参与渲染)。

对于动态合批:一些顶点数较小的网格,在运行时Unity会动态的对它们的网格进行合并,因此即使这些物体是动态的也没有关系。 注意:要使用静态合批和动态合批,首先要在设置中开启:

在 PlayerSetting 中开启静合批和动态合批

动态合批的顶点数限制的具体定义:

一个网格, 如果 Shader 中使用了 Vertex Position, Normal 和 单个 UV,那么只有在顶点数不超过 300 个时,该网格才能参与动态合批。

一个网格,如果 Shader 中 使用了 Vertex Position, Normal, UV0, UV1和Tangent,那么只有在顶点数不超过 180 个时,该网格才能参与动态合批。

还有一些情况会导致无法进行动态合批,如 Shader 有多个Pass、材质不同的物体无法合批在一起等等,github.com/Unity-Technologies/BatchBreakingCause 这篇官方文档介绍了所有导致合批失败的原因。通过 Unity的 FrameDebuger 我们也可以知道一些合批失败的原因:

对于原本能合批却合批失败的物体,FrameDebuger会给出原因

动态合批通常的应用场景有两个:粒子系统 和 UI。 粒子系统没什么好说的,要注意的是 UI 的动态合批:

1、不同 Canvas 下的 UI 无法动态合批

2、不同层级下的UI无法动态合批(这里的层级可以理解为 一个 UI 其下面垫了几层的UI,这一块可以根据 FrameDebuger 的合批结果来进行调整)

3、alpha = 0,depth = -1 的 UI 无法进行合批

4、depth = x 的 UI,只能与 depth ≤ x 的 UI 进行合批

5、动态合批本身也是有代价的。 当调用 Canvas.BuildBatch 或者,UI 元素产生变化时 就会重新进行 动态合批。 因此我们应该尽量把经常产生变化的 UI 和 不怎么产生变化的静态UI 分别放在两个 Canvas 下,这样可以降低 UI 动态合批的复杂度,加快合批运行的时间。

过多的材质。过多的材质与上面的合批基本原因和处理方法是一样的。场景中有大量的材质就会导致收集材质信息时变得复杂。那么对于能够合批的物体,合批后它们使用的就是同一个材质。 而对于材质使用的Shader和属性都一样,但贴图不一样的情况,我们可以试着将贴图进行合并.

复杂的动画。Unity是可以对动画文件进行压缩的,对于一些动画我们不需要太高的精度那么就可以进行压缩,来加快动画运算的速度和减少动画文件的体积。并且对于动画文件中存储的一些数值的精度我们可以通过代码工具来修改,从而进行动画压缩,例如对于动画文件里 1.123456789 的数值,我们可以修改为 1.1234。并且有的动画文件中,某一帧和它的前一帧和后一帧是没有变化的,那么该帧就可以删除,这个也可以用代码工具来检测和解决。

2、 CPU向GPU传递渲染所需数据 阶段的性能点

这一部分就是带宽问题了。除了压缩资源就是压缩资源。当然还有一些不在本篇范畴内的手段(流式加载、VirtualTexture 等)

贴图资源。

压缩格式:PC端压缩格式一般保持默认就好了。移动端贴图建议使用ASTC6x6进行压缩,如果觉得压缩后效果不行 可以换为 ASTC4x4。对于 HDR 贴图 可以采样 ASTC HDR 6x6 或 4x4 的压缩格式。 一些用来保存高精度数据的贴图,如果没有 Alpha 通道,可以使用 RGB9e5 32bit Shared Exponent Float 的压缩格式,有 Alpha 通道的话那就只能用 RGBAHalf 的格式了。

贴图大小:贴图大小就是尽可能的小,比如先做一个 2048x2048 的贴图,然后不断降低尺寸,直到效果上发生明显变化并且无法接受时,就得到了最适合的贴图大小。并且对于一些具有四方连续特性的细节贴图,我们可以只取其中的一小部分,放在一个尺寸很小的贴图中,然后通过 Tilling And Offset 来采样得到完整的内容。

Mipmap:对于始终只出现在很远处的物体,其贴图尺寸我们可以给很小,并且不需要生成 Mipmap。 同样对于始终只出现在很近处的物体,我们可以给个较大的贴图尺寸,并且不需要生成 Mipmap

网格资源

建模时控制好网格的顶点数

网格 LOD 策略

曲面细分着色器配合高度图和区块划分来做到地形LOD

在网格资源设置中,关闭不需要导入的东西

可以尝试开启 Mesh Compression 对网格进行压缩,看得到的效果是否可以接受

非静态物体(不需要参与烘焙光照贴图的物体)取消勾选 GenerateLightmapUV

对于一些顶点数很多,同时体积很大很容易一直出现在视野范围内的物体。可以将其拆分为多个物体。这样在经过视锥体剔除后,就不需要向GPU传入太多的顶点数据。

3、CPU发起DrawCall 阶段的性能点

DrawCall 的数量,要减小 DrawCall 的数量,其实就是减少物体和材质的数量,这些在上面的 CPU计算和收集渲染所需数据组装描述符和材质阶段 部分已经介绍了,这里就不赘述了。另外对于 DrawCall 数量的影响就是 Shader 的 Pass 数,和在前向渲染的管线中点光源的数量,但这两方面属于是那种如果在不降低渲染效果的情况下那么就该是多少就是多少没办法减小的问题,如果非要减小那么就要对渲染管线进行改造,插入一些现代的优化方案,由于这些方案通常都比较复杂,不在本篇范畴类,有机会会专门做一篇进行介绍。

DrawCall 的复杂度。DrawCall 不单单只是一个指令,一个 DrawCall 中通常包含了多个指令。那么其包含的指令数的多少也就决定了其本身的复杂度。而 DrawCall 中最常见的指令就是告诉GPU材质的属性:

一个DrawCall中包含了多条指令

对于设置一个浮点数的值,需要调用 一次 glUniformf 指令(这里以 OpenGL 为例),而设置一个浮点向量的值,同样也只需要调用 一次 glUniform4fv 指令。因此我们就可以把 4个浮点数变量 合并为 1个浮点向量变量。这样 4条指令就可以合并为 1条指令。而要做到这样,我们只需要在编写Shader时,将4个浮点变量的声明改为1个浮点向量的声明。但是浮点向量在材质面板上的显示方式对于美术极不友好,调节起来很变扭也很不直观,这里可以使用 ShaderGUI 和 MaterialPropertyDrawer 类来对材质面板进行定制,以达到美术友好,这一部分我会放在后面的 骚操作篇进行细说。

4、GPU进行渲染和计算 阶段的性能点

Overdraw

对于特效引起的Overdraw,一定要在特效制作时关注 "像素填充比" 这一概念:

像素填充比,即在一个特效面片中 alpha不为0 的像素 占整个面片像素的比例。在特效制作时,要让像素填充比尽可能的大。下面给出例子:

对于上面这个特效面片,像素填充比就过小,会产生大量没有必要的 Overdraw。此时应该改变网格,减低片面网格的高度,或者将网格做成月牙形。

而对于由于渲染顺序不合理导致的 Overdraw,就只有自己根据实际情况去调整渲染队列了。例如对于一个 有河流、草、树木 和 地面的场景,我们应该 先渲染草,然后渲染树木的树叶、之后是树木、接着是地面,最后是河流。这样在渲染地面时,地面就会有大量像素因为被草和树木遮挡而没能通过深度测试,也就没有被光栅化渲染,从而减少了Overdraw。而因为河流是一个半透明物体(我们可以透过河流看到河水下面的地面),因此河流需要在地面渲染之后才渲染,这样才能和地面做半透明混合。

Shader本身的计算复杂度

LUT策略。我们可以把Shader中这类计算:其输入变量的值范围在0~1或者可以转换为0~1,其输出变量的值范围也在0~1或者可以转换为0~1,那么我们就可以把0~1范围的所有输入都看作一个贴图的UV值,而0~1的所有输出都看作一个贴图的颜色值。那么我们就可以提前离线的将所有输入值都进行计算得到输出值,并存储在一个贴图中。那么 Shader 在 实时计算时,我们只需要拿到输入变量去采样贴图即可,从而避免了复杂的运算。

合并运算。对于 GPU 来说,一个浮点数的运算 和 一个浮点向量的运算使用的指令数是相同的。因此如果几个浮点数需要做相同的运算,那么就可以先把这几个浮点数合并为几个浮点向量,然后再运算。并且,Unity在编译着色器时,会在一些地方帮我们做出这种优化。

在移动端还要注意数值变量的精度问题。在移动端GPU上,数值变量有 fixed、half、float 的区分,其数值精度分别为 8位、16位和32位,位数越高,计算过程就越慢。但在一些GPU上(如苹果的GPU 和 部分华为手机使用的GPU),如果运算结果超过了其定义的精度范围,就会出现渲染异常的情况(渲染结果出现黑色块,或渲染结果不正确等)。这里我的建议是,在声明变量时,颜色使用fixed,世界空间下的位置信息使用float,如果采样的贴图会用到很大的Tilling那么uv也是用float,否则使用 half,其它的变量都使用half。

在移动端还要注意GMEM_Load机制问题。GMEM Load 即 Graphic Memory Load 在 移动端 GPU 的 TileBase 架构下, 其触发表明上一帧的 Frame Buffer 在这一帧渲染时被从 GPU 主存加载到了正在渲染的 Tile 内存中。其会引起严重的渲染性能问题!例如在一款手机上屏幕被分为了30个Tile, 如果触发了 GMEM Load 那么在每次渲染一个 Tile 之前都会从 主存 加载 FrameBuffer,而 Frame Buffer占用内存比较大,加载时间会比较慢,并且在加载完毕后 GPU 内部还需要一系列调度才能让渲染开始进行,因此 GMEM Load 会很大程度的降低GPU运行的效率。 在 Unity 中触发 GMEM Load 的操作有:

开启 HDR。在移动端即使开启了 HDR,颜色缓冲仍然是 RGBA8 格式的,Unity 会创建出一个 RT(RGBA Half 或者 R11G11B10 格式,取决于你的 Graphic Setting),渲染结果先输出到这个 RT 上,并做了编码,之后该 RT 通过解码和Tonemapping 输出到颜色缓冲上(相当于一个后处理),但是 Unity 自己的这个后处理会触发 GMEM Load

GrabPass。因为 GrabPass 就是直接复制 Frame Buffer 的颜色缓冲。

相机的 ClearFlag 是 DepthOnly 或者 DontClear (这个在我的记忆中不是那么确定,总之好像确实有可能会导致GMEM Load)

混乱和大量的RT (这里说的混乱和大量是因为通常简单的使用 RT 是不会导致 GMEM Load 的,但某些情况下会触发,具体是哪些情况我也说不上来,因为触发时 RT 的管理真的太乱了...)。 另外对于不需要 深度/模板缓冲的RT,创建时要把它的 depth 设为 0,这样即使因为 RT 触发了 GMEM Load,由于 GMEM Load Color 和 GMEM Load Depth and Stencil 是两个分开的操作,所以这样可以避免 GMEM Load Depth and Stencil,从而做到尽量避免 GMEM Load。

 5、在一些情况下GPU会向CPU回传数据 阶段的性能点

没什么好说的,这里能引起的只有带宽问题。通常游戏开发中不会遇到这一阶段。对于这一阶段我们只需要留意使用 GPU 做计算功能时,其结果应该尽可能的是留在GPU的内存中,作为GPU后面计算或渲染的输入,如果一定要把结果传给CPU,那就要注意结果的大小,避免产生带宽问题。

好啦,这一篇就先到这里了。后面的篇章会介绍怎么使用工具确定这些具体的性能点,以及一些更复杂、高级的性能优化方案吧~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3