找回密码
 立即注册
查看: 317|回复: 0

[笔记] 《Unity Shader入门精要》笔记(三十三)

[复制链接]
发表于 2022-11-26 10:38 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第十六章《Unity中的渲染优化技术》的下半部分内容《渲染性能优化的方法》。

本文相关代码,详见:

原书代码,详见原作者github:
<hr/>1. 减少Draw Call数目

最常见的优化技术:批处理。实现原理:减少每帧需要的Draw Call的数目。

为了把一个物体渲染到屏幕上,GPU需要检查哪些光源影响了该物体,绑定Shader并设置它的参数,再把渲染命令发给GPU。若场景中包含大量的物体,则需要很多遍这样的操作,为了节省不必要的耗时,将多个物体的网格信息一并发给GPU,这就是批处理的本质。

什么样的物体可以进行一起批处理?
使用相同材质的物体。因为它们之间的不同仅仅在于顶点数据的差别,将它们合并在一起发送给GPU,可以完成一次批处理。

批处理方式:

  • 动态批处理
优点:一切都是Unity自动完成,无需自己做任何操作,物体可移动;
缺点:限制太多,一不小心就破坏这种机制,导致无法动态批处理一些使用了相同材质的物体。

  • 静态批处理
优点:自由度高,限制少;
缺点:占用更多内存,且对应物体不能再移动(即时脚本中尝试改变物体的位置也是无效的)。

1.1 动态批处理

实现原理:
每一帧把可以进行批处理的模型网格进行合并,再把合并后的模型数据传递给GPU,然后使用同一材质对其渲染。

需要的条件:

  • 网格的顶点属性规模要小于900
如果Shader中需要使用顶点位置、法线、纹理坐标这3个顶点属性,想要让模型被动态批处理,他们的顶点数目不能超过300。

  • 物体的缩放存在分量负值个数的奇偶性要一致
比如:一个物体缩放为(1, 2, -3),另一个物体缩放为(1, -2, -3),那么这两个物体不能进行合批处理。原因是缩放分量负值个数会改变物体的CullMode,当物体缩放存在1个或3个负值时,物体的CullMode使用的是Front,当物体缩放存在0个或2个负值时,使用的是Back的CullMode,不同的渲染状态无法合成一个批次传递给GPU。

  • 使用光照纹理(lightmap)的物体需小心处理
这些物体需要额外的参数,如:光照纹理上的索引、偏移量、缩放信息等,如果让这些物体可以被批处理,需保证它们指向光照纹理中的同一位置。

  • 多Pass的Shader会中断批处理
前向渲染中,有时需要额外的Pass来为模型添加更多的光照效果,这样会导致模型无法被动态批处理。

1.2 静态批处理

实现原理:
只在运行开始阶段,把需要静态批处理的模型合并到一个新的网格结构中,这些模型不可以在运行时被移动。

相比动态批处理,因为只进行一次合并操作,所以更加高效;但缺点是占用了额外的内存存储合并后的几何结构(物体原来的网格信息也依然会在内存中,合并后的网格是从原网格中复制的)。

实现方法:
物体的Inspector面板上的Static复选框勾选上,即表示该物体会被静态批处理。

合并后的网格会被命名为Combined Mesh (root:scene),笔者曾经开发的游戏中就遇到了因静态批处理导致的问题:场景中某个物体,发现运行后代码无法改变其坐标,且编辑器Scene窗口在运行状态下移动该物体也无效,后来发现是那个物体勾选了Static框。

内部实现:
Unity首先把这些静态物体变换到世界空间下,构建成一个更大的顶点和索引缓存。对于使用同一材质的物体,Unity只需调用一个Draw Call就可以绘制全部物体;对于使用各不同材质的物体,静态批处理同样可以提升渲染性能,尽管仍然需要调用多个Draw Call,但静态批处理可以减少这些Draw Call之间的状态切换,而这些切换往往是费时的操作。

1.3 共享材质

无论动态批处理还是静态批处理,都要求模型之间共享同一个材质。但模型之间总需要有不同的渲染属性,如:不同的纹理、颜色等,这时需要一些策略见谅地公用材质。


  • 纹理不同
如果不同物体间只是纹理不同,可以考虑将这些物体的纹理打成一张图集,这样它们使用的就是同一张纹理,因此可以公用同一个材质,然后不同的物体只需使用不同的采样坐标对纹理采样即可。

  • 颜色不同
如果不同的物体之间只是颜色不同,可以考虑在运行时通过APIRenderer.material访问物体的材质并修改材质的颜色即可,这个材质是共享材质的复制体,修改它不会影响到其他物体的颜色。如果想要统一修改所有物体的颜色,可以通过APIRenderer.sharedMaterial,并执行相同的操作即可。

1.4 批处理的注意事项

一些小建议:

  • 不改变位置的物体尽可能使用静态批处理;
  • 在同一帧渲染的小纹理尽可能打成一张大的图集;
  • 留意动态批处理的各种条件限制,同时可通过调整场景中物体的位置、渲染队列等属性,让尽可能多的物体一起被动态批处理;
  • 批处理的物体会丢失模型空间下的顶点属性,无法做基于模型空间的运算。

2. 减少需要处理的顶点数目

顶点数目可能会造成GPU的性能瓶颈,当GPU耗时过长时,需要减少定点的数目,有3个常用的顶点优化策略。

2.1 优化几何体

尽可能减少模型中三角面片的数目,一些对模型没有影响、或是肉眼来看可有可无的顶点尽可能去掉。Unity的渲染统计窗口中可以查看渲染当前帧需要的三角面片数目和顶点数目,需要注意的是:Unity中线束的顶点数目往往要多于建模软件里显示的顶点数。主要有2个原因:

  • 分离纹理坐标(uv splits)
建模时一个顶点的纹理左右有多个,例如:一个立方体,它的6个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。而对GPU来说,顶点的每个属性和顶点之间必须是一对一的关系,所以必须将这个顶点拆分成多个具有不同纹理坐标的顶点。

  • 产生平滑的边界(smoothing splits)
对于平滑边界,顶点可能会对应多个法线信息或切线信息,道理和分离纹理坐标类似。

2.2 模型的LOD技术

减少顶点数目的另一个方法是使用LOD(Level of Details)数据,原理是:当一个物体离相机很远时,很多细节是无法察觉到的,因此LOD允许当物体逐渐原理相机时,减少模型上的面片数量,从而提高性能。当然,我们需要对这个物体多准备几套精度不同的模型。Unity自带LOD Group组件,感兴趣的小伙伴可自行研究。

2.3 遮挡剔除技术

遮挡剔除(Occlusion Culling)也是顶点优化的一种策略。它可以用来消除那些被其他物体遮挡住的物体,这意味着看不见的顶点不会被GPU渲染,从而提升性能。

这里需要把遮挡剔除和相机的视椎体剔除加以区分。

  • 视椎体剔除
只会剔除掉那些不在相机的视野范围内的对象,但不会判断视野中是否有物体被其他物体遮挡。

  • 遮挡剔除
会使用一个虚拟的相机来遍历场景,从而构建一个潜在可见的对象几何的层级结构。运行时,每个相机将会使用这个数据来识别哪些物体可见,哪些物体被挡住。

需要注意的是:遮挡剔除对CPU有一定的开销,面对性能优化,遮挡剔除带来的性能提升更大,还是增加CPU负载带来的影响更大,这需要我们去权衡。

3. 减少需要处理的片元数目

过多的片元是造成GPU性能瓶颈的另一原因,其优化的重点在于减少Overdraw,Overdraw指的是同一个像素被绘制多次。
Unity提供了查看Overdraw的视图,在Scene窗口左上方的下拉框中选中Overdraw即可:



然而它只是提供了查看物体互相遮挡的层数,并不是真正的最终屏幕绘制的Overdraw。它显示的是:如果没有使用任何深度测试和其他优化策略时的Overdraw。重叠的部分越多,颜色越深。

3.1 控制绘制顺序

控制绘制顺序是减少Overdraw的重要优化策略。由于深度测试的存在,如果可以保证物体都是从前往后绘制的,那么久可以很大程度减少Overdraw,因为后面绘制的物体无法通过深度测试,不会进行后面的渲染处理。


  • 不透明(Opaque)物体
渲染队列数目小于2500(如:Background、Geometry、AlphaTest队列)的对象,从前往后绘制。

  • 半透明物体(Transparent)物体
渲染队列数目大于等于2500(如:Transparent、Overlay队列)的对象,从后往前绘制。

只要尽可能把物体的队列设置为不透明物体的渲染队列,就可以尽量避免使用半透明队列。合理控制不透明物体的渲染顺序,可以很大程度减少Overdraw,从而提高性能。

举例:

  • 第一人称射击游戏,游戏的主角往往会挡住屏幕的很大一部分区域,因此可以将主角的渲染队列调小,先渲染,这样被主角手臂、枪挡住的敌人或掩体就不会再被渲染;
  • 而对于一些敌方角色,由于它们通常在掩体的后面,所以可以将它们的渲染队列调大,这样先渲染掩体,掩体后面的敌人就不用再被渲染;
  • 对于天空盒来说,它几乎覆盖所有的像素,将它的队列设置为“Geometry+1”,就可以保证其最后渲染,减少Overdraw。

3.2 时刻警惕透明物体

绝大多数情况,半透明物体的Overdraw是不可避免的。

半透明物体主要有以下几种:

  • 场景中的半透明对象;
  • GUI(大多会被设置为半透明);
  • 透明的粒子特效;

对应的策略:

  • 尽可能使用不透明物体;
  • 减少GUI所占的面积,或场景物体和GUI分2个相机渲染;
  • 根据运行设备的性能,权衡是否开启或关闭一些特效功能。

在移动平台上,透明度测试也会影响游戏性能。透明度测试会使用discard或clip操作,导致一些硬件的优化策略失效。

比如:PowerVR使用的基于瓦片的延迟渲染技术,为了减少Overdraw它会调用片元着色器前就判定哪些瓦片被真正渲染。但由于透明度测试在片元着色器中使用了discard函数改变了片元是否会被渲染的结果,因此GPU无法使用上述的优化策略,只有执行了所有的片元着色器后,GPU才知道哪些片元会被真正渲染到屏幕上,这样原先减少的Overdraw的优化就都失效了。

3.3 减少实时光照和阴影

如果场景中包含了过多的点光源,并且使用了多个Pass的Shader,那么很可能会造成性能下降。对于逐像素的点光源,被这些光源着凉的物体需要被再一次渲染,且无论是静态批处理还是动态批处理,这些额外处理逐像素光照的Pass无法进行批处理。

光照优化策略:

  • 使用烘焙技术
提前将光照信息烘焙到一张光照纹理(lightmap)中,在运行时对纹理进行采样。

  • 使用God Ray
它一般不是真的光源,而是通过透明纹理模拟的。

  • 控制逐像素光源数量
移动平台,除平行光外一个物体使用的逐像素光源数目应该小于1,如果一定要使用更多实时光,可以选择逐顶点光照。

  • 使用LUT
把复杂的光照信息存储到一张查找纹理(lookup texture,也被称为查找表,lookup table, LUT),运行时只需要使用光源方向、视角方向、法线方向等参数对LUT采样得到光照结果;此外还可以利用查找表模拟更复杂的BRDF模型。

实时阴影同样是非常消耗性能的效果,不仅是CPU需要提交更多的Draw Call,GPU也要做贡多的处理。

阴影优化策略:

  • 使用烘焙
把静态物体的阴影信息存储到光照纹理中。

  • 模型阴影
关闭阴影投射和阴影接收,有美术建模时直接将阴影面片做到模型中。

4. 节省带宽

大量未经压缩的纹理以及过大的分辨率会造成由于带宽引发的性能瓶颈。

4.1 减少纹理大小

纹理的尺寸规范:

  • 手机端纹理(或纹理图集)大小不能超过2048*2048;
  • 纹理(图集)长宽应该设置为4的倍数;

减少纹理大小的策略:

  • 使用Mipmap(多级渐远纹理)技术
纹理的导入属性面板中,把纹理类型设置为Advanced,然后勾选Generate Mip Maps,Unity就会为同一张纹理创建出很多大小不同的小纹理。当然不用担心内存占用,多生成的小纹理占用内存总和只有原尺寸纹理的1/3。

  • 纹理压缩
Unity会根据不同的设备选择不同的压缩格式,我们只需要将纹理压缩格式设置为自动压缩即可。但对于一些有画质要求的纹理,一般不希望对它进行纹理压缩。

4.2 利用分辨率缩放

过高的分辨率也是造成性能下降的原因之一,尤其对于很多低端手机,除了分辨率高其他硬件条件不尽如人意,也就是游戏性能的两大瓶颈:过大的屏幕分辨率、糟糕的GPU。

对于特定设备,将其屏幕分辨率设低,再放大到屏幕的尺寸,虽然降低游戏效果,但是可以带来性能上的提升。Unity中设置分辨率的代码:
Screen.SetResolution
雨松MOMO的文章:https://www.xuanyusong.com/archives/3205

5. 减少计算复杂度

计算复杂度同样会影响游戏的渲染性能,可通过两方面的技术来减少计算复杂度:Shader的LOD技术、代码方面的优化。

5.1 Shader的LOD技术

与模型的LOD技术类似,Shader的LOD技术科控制使用的Shader等级。
原理:只有Shader的LOD值小于某个设定的值,这个Shader才会被使用。

通常会在SubShader中使用类似下面的语句来指明该Shader的LOD值:
SubShader
{
    Tags { "RenderTYpe" = "Opaque"}
    LOD 200
}

在Unity Shader的导入面板上看到该Shader使用的LOD值。默认情况下,LOD等级可以无限大,有时需要去掉一些使用了复杂计算的Shader渲染,这是氪使用Shader.maximumLOD或Shader.globalMaximumLOD来设置允许的最大LOD值。

Unity内置的Shader使用了不同的LOD值,例如:Diffuse的LOD值为200,Bumped Specular的LOD值为400。

5.2 代码方面的优化

优化策略:

  • 尽可能使用低精度的浮点值进行运算;

    • float/highp适用于存储顶点坐标等变量,但它计算速度最慢,所以应尽量避免在片元着色器中使用这种精度的计算;
    • half/mediump适用于一些标量、纹理坐标等变量,其计算速度约为float的两倍;
    • fixed/lowp适用于大多数颜色变量和归一化后的方向矢量,对于一些精度要求不高的计算,尽量使用这种精度,其计算速度约为float的4倍;但要注意:避免频繁的swizzle操作(如:color.xwxw),避免不同精度间的转换;

  • 从顶点着色器向下一阶段传递变量时,应尽可能减少变量的数量

    • 使用float4传递两个纹理的uv,而不是分两个float2传递两个纹理的uv;
    • 上一条对于PowerVR是例外,应该直接把不同的纹理坐标存储在不同的插值变量中,性能会更好。使用类似tex2D(_MainTex, uv.zw)语句进行采样,GPU就无法进行一些纹理的预读取,因为它会认为这些纹理坐标是需要依赖其他数据的。

  • 尽可能不要使用全屏的屏幕后处理

    • 如果必须,尽量使用fixed/lowp进行低精度运算(纹理坐标除外,可使用half/mediump);
    • 高精度运算可使用查找表(LUT)或转移到顶点着色器进行处理;
    • 尽量把多个特效合并到一个Shader中,例如:颜色校正和添加噪声等屏幕特效在Bloom特效的最后一个Pass中进行合成;

  • 代码优化规则

    • 尽量不要使用分支语句和循环语句;
    • 避免使用sin、tan、pow、log等较为复杂的数学运算,用查找表代替;
    • 尽量不要使用discard操作,这会影响硬件的某些优化;

  • 使用缩放思想来选择性地开启特效

5.3 根据硬件条件进行缩放

两个原则:

  • 保证游戏基本的配置可以在所有的平台上运行良好;
  • 一些觉有更高表现能力的设备,可以开启一些更“养眼”的效果,如:使用更高分辨率、开启屏幕后处理特效、开启例子效果;

6. 本章扩展阅读


  • Unity官方手册的移动平台优化实践指南:https://docs.unity3d.com/2020.1/Documentation/Manual/MobileOptimizationPracticalGuide.html
  • 优化图像性能:https://docs.unity3d.com/Manual/OptimizingGraphicsPerformance.html
  • SIGGRAPH 2011上,Unity进行的关于移动平台上Shader优化的演讲:https://blog.unity.com/community/fast-mobile-shaders-talk-at-siggraph
  • Unite 2013会议上《针对移动平台优化Unity游戏》的演讲
  • GDC 2014 上,Unity展示如何使用内置的分析器分析移动平台的游戏性能,Youtube上可找到对应视频
  • SIGGRAPH 2015会议上,《Moving Mobile Graphics》课程中,来自Unity的Renaldas Zioma讲解了移动平台上PBR的优化技术
  • Unite 2011上,《ShadowGun》的开发者给出了该游戏使用的渲染和优化技术,Youtube上可找到对应视频
  • Unity自带的项目《Angry Bots》,可直接在Unity资源商店下载到完整的项目源代码

以上是本次笔记的所有内容,下一篇笔记我们将进入新的章节,一起学习《Unity的表面着色器探秘》的相关内容。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 16:10 , Processed in 0.167266 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表