Unity Shader学习笔记(五) - 噪声、优化与PBS工作流
本文最后更新于 2025年2月10日 下午
噪声
消融
略
水波
水面的流动一般使用时间变量+噪声采样+改变法线方向的方式实现。除此之外,要想模拟较好的水波效果,还需要进行反射、折射的计算。其中,反射还涉及到菲涅尔反射(fresnel=pow(1-max(0, v·n),4)
),因为视线与水面越平行,反射率就越高。此处,反射、折射使用CubeMap作为采样源。
Shader如下:
1 |
|
非均匀雾效
VS与先前的全局雾效一致。
FS:
1 |
|
优化
影响性能的优化如下:
- CPU:DrawCall数量、脚本逻辑复杂程度、物理模拟次数
- GPU:顶点数量、逐顶点计算复杂度、片段数量、逐片段计算复杂度
- 带宽:纹理尺寸/压缩格式、帧缓冲分辨率
减少Drawcall
Drawcall是CPU对GPU发出的一次绘制请求。
渲染一千个三角形网格比渲染包含了一千个三角形的网格耗时更久,因为前者的drawcall数量是后者的一千倍。CPU会花费大把时间在提交drawcall上,而在提交的过程中,GPU将会等待CPU完成提交后才会渲染。
批处理(Batching)是一种常见的优化drawcall数量的技术。使用同一个材质的物体可以进行批处理。
Unity支持动态批处理和静态批处理。前者的一切操作是Unity自动完成的,但其存在诸多限制,以至于一不小心就会破坏动态批处理。静态批处理自由度较高,但会占用更多内存,并且经过静态批处理的物体将不可以再移动。
动态批处理
其原理为:在每一帧把符合条件的模型网格进行合并,然后把合并后的模型数据传递给GPU,再用同一材质对其进行渲染.。
主要的条件限制如下:
- 网格顶点属性规模要小于某个定值。顶点属性规模的意思是,有N个顶点属性,M个顶点,则N*M应当小于定值。
- 若使用了Lightmap,则材质使用的有关Lightmap的属性必须相同。
- 单Pass
静态批处理
其原理为:在开始运行时,一次性将需要静态批处理的模型合并到同一网格。它与动态批处理的本质区别在于,静态批处理只需要运行一次。
启用静态批处理的方式是,勾选GO Inspector名称右侧的Static复选框(或勾选下拉栏中的Batching Static)。
其具体实现原理为:
- 首先,将静态物体的顶点坐标变换到世界空间下。
- 为需要Batching的物体构建一个更大的顶点和索引缓存。
- 对于使用了同一材质的物体,调用一个Drawcall将其全部提交
- 对于使用不同材质的物体,减少它们之间的状态切换
共享材质
当一个材质从同一个着色器创建,赋值给了两个模型。但两个模型的材质属性有所不同,如纹理、颜色等,也会导致两个Drawcall。我们可以通过Atlas、顶点数据等策略规避这个问题。
图集(Atlas)是把若干纹理合并到一张大的纹理的优化策略。使用了同一张纹理,就能使用同一个材质,减少Drawcall。
纹理以外的材质属性,有微小变化的,可以使用网格顶点数据存储这些属性。无论如何,只要使用同一个材质实例的网格,都会共享所有属性的数值。使用同一个材质实例的网格,我们称这些网格使用了共享材质,表现为Renderer.sharedMaterial
。使用setFloat
等方法改变sharedMaterial
属性时,所有使用该材质的网格均会变化。
若使用Renderer.material
修改材质,本质上是创建了sharedMaterial
的一个复制体,会破坏批处理。
注意事项
使用批处理需要谨记下列建议:
- 在保证内存占用可以接受的情况下,尽量选择静态批处理,且注意静态批处理的物体不可移动。
- 若使用动态批处理,则需要尽可能避免破坏限制。
同时也需要注意,若Shader中存在模型空间运算(如顶点动画),则必须使用DisableBatching
标签取消批处理,否则会导致错误。此外,对于半透明材质物体,一定注意其在Hierachy下的排列顺序满足从后往前,否则会破坏批处理。
减少顶点数量
几何体
在3D建模时,尽可能减少三角形数量,同时移除不必要的硬边(Hard Edge)和纹理衔接,以避免边界平滑和纹理分离。
这里解释一下上面几个专有名词的概念。
在Unity中显示的顶点数往往多于建模软件中的顶点数,这是因为GPU有时需要把一个顶点拆分为更多的顶点,一是为了分离纹理坐标(UV Split),另一个是为了产生平滑边界。对于前者,举个例子,一个立方体的三个面可能共用一个顶点,但在这三个面上该顶点对应的纹理坐标并不相同,所以就需要拆分出三个顶点。对于后者,一个顶点可能会对应多个法线或切线信息,而GPU对于一个顶点只能处理一个属性变量,所以就需要拆分出多个。
LOD
使用LOD Group
组件为物体构建LOD(Level of Details),为一个物体准备多个不同细节层次的模型,并给组件赋值。
遮挡剔除
遮挡剔除(Occlusion Culling)用于消除在其他物体之后,无法看到的物体。
Occlusion Culling与视锥体剔除(Frustum Culling)不同。前者主要做深度判断,后者主要做NDC范围判断。
遮挡剔除作用于Renderer
组件。被更近的Render遮挡的Renderer将会被删除。
遮挡剔除本身需要CPU算力。适合开启遮挡剔除的情景如下:
- 只有当Overdraw到一定程度时,以至于GPU遇到瓶颈时,才需要开启遮挡剔除。
- 用户设备的内存足够大,因为需要存放遮挡剔除数据
- 彼此连接的狭长室内场景
- 运行时不会生成场景几何体(即所有需要渲染的物体在场景加载时就已经确定),如地形破坏、大量即时生成的GO等。
通过在Occlusion Culling窗口更改参数,并在场景中使用遮挡区域,就可以开启遮挡剔除。具体步骤如下:
- 为场景中所有运行时始终不移动且具有Renderer组件的被遮挡物(即会被其他物体遮挡的物体)设置为Occludee Static
- 为场景中所有运行时始终不移动、不透明且具有Mesh Renderer或Terrain组件的遮挡物(会遮挡其他物体的物体)设置为Occluder Static
- 启用摄像机的Occlusion Culling属性
- 在Window-Rendering-Occlusion Culling窗口的Bake选项卡中进行烘焙。
- 若要在Scene中查看Occulusion Culling效果,则在Occlusion Culling窗口活跃时,选中一个相机GO,观察Scene视图。此时,无法被相机看到的物体应当消失。
动态被遮挡物无需烘焙,只需要在Renderer组件中开启Dynamic Occulusion组件。此时,当它被静态遮挡物遮挡时,将会被剔除。但存在类似于“鹰眼视觉”、“墙后显示”的需求时,应当关闭该属性。若确定该动态物体不可能被遮挡剔除,则关闭该属性。
使用Occlusion Area
组件定义需要进行遮挡剔除的场景区域。Unity在烘焙遮挡数据时,将对组件包围盒内的区域进行精度更高的烘焙。如果场景内没有该组件,则会烘焙所有静态遮挡、被遮挡物体,导致漫长的烘焙时间和过大的遮挡数据。
使用Occlusion Portal
组件定义遮挡入口。遮挡入口关闭时将作为静态遮挡物,否则就不会遮挡游戏对象。改变该组件的open
属性以切换打开/关闭状态。
减少片段数量
减少片段数量的关键在于减少Overdraw。可通过Scene试图左上角的下拉菜单中选择Overdraw以查看Overdraw情况。
控制绘制顺序
Unity中,Render Queue小于2500的物体是从前往后绘制的,因为深度测试的存在,使得后面的物体被遮挡的片段会被直接剔除。大于2500的物体则是从后往前绘制,因为要考虑到半透明物体的渲染次序。
我们要尽可能根据实际情况排列物体顺序。例如,在FPS中,先玩家,再绘制掩体,再绘制敌人。
小心透明物体
半透明物体必定会造成Overdraw。大部分UI对象为半透明,所以将UI相机和主相机分离可以减少大量的Overdraw。
在移动设备上,我们要尽量避免使用Alpha Test,因为discard/clip操作会导致一些硬件优化策略(与TBDR有关)失效。此时,Blend的性能反而比Test更优。
减少实时光照和阴影
多用Light Map烘焙,少用RealTime光源;LUT也是一种避免光照计算的好方法。
节省带宽
前面提到,纹理大小和分辨率是影响带宽的重要因素。因此,我们可以:
- 对于纹理,其长宽比最好为正方形,长宽最好为2的整数幂,多使用MipMap(在导入设置的高级选项中)
- 对于分辨率,略。
减少计算复杂度
Shader LOD
ShaderLab中,LOD
语义可以指定Shader的LOD值。当LOD值小于特定值时才使用该Shader,而使用了超过设定值的Shader的物体将不会被渲染。
优化Shader代码
- 注意float、half类型的使用。通常,float适用于顶点坐标等变量,应当尽量在VS中使用;half适用于标量、纹理坐标等变量;fixed适用于颜色和单位向量。对于half、fixed变量,应当尽量避免Swizzle操作。
- 定义appdata、v2f结构体时,应尽量减少插值寄存器(TEXCOORD语义标记)的数量。例如,对于两个float2,把他们放在一个float4里存储。
- 避免全屏后处理。如果不可避免使用全屏后处理,使用fixed或half进行计算。如果实在要涉及到高精度计算,则使用LUT或转移到VS中处理。
- 把多个后处理特效合并到一个Shader中以减少Pass数量。
- 避免使用控制语句。
- 避免使用复杂数学计算函数
- 避免使用discard操作
PBR
本节主要介绍Unity Standard Shader使用方法。
Standard Shader支持Metallic Workflow和Specular Workflow,前者较常用且为默认工作流。
使用Standard Shader时,需要在Edit-Project Settings-Player-Other Settings-Color Space中设置颜色空间为Linear。因为PBS需要在线性空间中计算。
下面介绍部分主要材质属性。
- Albedo:物体整体颜色
- Metallic:物体金属度。设置为纹理时,A通道为Smoothness
- Smoothness:Metallic的附属属性,用于定义材质表面光滑程度。
在Specular工作流中,Specular将用于替换Metallic,用于定义镜面反射强度。
- Normal Map:法线贴图
- Height Map:高度图
- …
工作流描述
设置光照环境
打开Window-Lighting窗口,将HDRI拖入Scene选项卡下的Skybox属性中。
设置环境光照来源(Ambient Source),可以是Skybox,也可以是渐变/固定颜色。
设置环境光强度(Ambient Intensity)。
环境光来自反射源的反射。默认反射源(Reflection Resource)为天空盒。若不想让物体接受环境光,则设置反射源为Custom并留空。
实时全局光照(Global Illumination,GI)使得场景物体不仅可以收到直接光影响,也可以收到间接光影响。
对于光源组件的Bake属性,我们可以根据设备性能选择不同的选项。Realtime模式将让场景内所有受到此光源影响的物体都会进行实时光照计算;Baked模式将会在Bake时生成Lightmap用于采样获取光照结果,但物体移动后再采样Lightmap会出现差错;Mix模式会将标记为Static的物体进行Bake,其他物体采用实时模式。
光源组件的Bounce Intensity属性反映了GI中受此光源影响的物体的间接光(由此光源发出的光反射得到)强度。调整Light窗口Scene选项卡下General GI参数块中的Bounce Boost和Indirect Intensity参数能全局控制间接光照强度。
反射探针
对于金属度高、平滑度高的物体,将能精确映射反射源的内容。然而,在变化较大的场景中,如果物体四周的环境总是在发生变化,就会发生穿帮。为此,我们可以使用反射探针(Reflection Probe)。它允许我们在场景的特定位置对整个场景的环境反射进行采样。
反射探针有Baked、Realtime和Custom类型。对于第一种,运行时探针中存储的Cubemap不会发生变化,适合“环境不变物体变”的情况。对于第二种,会实时更新Cubemap,但性能开销较大(可以通过脚本精确触发探针更新);对于第三种,可以使用自定义Cubemap完成环境反射,也可以烘焙。
反射探针应当放置在明显具有反射现象的物体旁边,或容易发生遮挡的物体周围。放置完毕后,还要设置探针管辖的区域,区域内的反射物体将采用探针的Cubemap作为反射源。存在多个管辖区域重叠的探针时,Unity会像处理Light Probe那样,进行探针的平滑混合。
反射探针不仅能提高反射真实度,也能模拟互相反射的效果(即两个反射物体互相靠近)。