UPL9-11——降低 Shader 填充率压力

回顾:关于填充率的优化思路

具体可见:UPL8——影响 GPU 性能的主要因素 的 填充率部分

  1. 降低要处理的像素(片元)数量

    1. 降低分辨率

      1. 动态分辨率(Dynamic Resolution Scaling)

        基本原理:
        在 GPU 压力大时,降低渲染分辨率,然后再放大到屏幕分辨率输出
        在 GPU 空闲时,提高渲染分辨率,获得更清晰的画面
        目标是保持 稳定帧率,URP 项目中有动态分辨率功能开关

      2. 注视点渲染(Foveated Rendering)

        基本原理:
        人眼的视觉分辨率,中央注视点区域最清晰,周边区域分辨率敏感度低
        在中央区域渲染高分辨率,在边缘区域渲染低分辨率,从而减少像素处理量
        一般 VR 项目中会使用
        一般 VR 设备会提供对应功能,通过设备 API 开启

      3. 降低项目分辨率

        基于目标设备,合理设定项目目标分辨率

    2. 减少或优化屏幕后处理效果的使用

      在 1/2 或 1/4 分辨率下渲染,或直接不使用

    3. 降低抗锯齿采样次数或关闭

      4×MSAA ——> 2×MSAA 或关闭

  2. 减少单个像素(片元)计算量

    1. 控制 Shader Pass 数量

      能合并的效果放在一个 Pass,避免片元被重复算

    2. 减少半透明的使用

      减少层数,必要时用 透明测试(AlphaTest)
      可见:US3S3L4——透明度测试

    3. 合理使用光照模式

      少用逐像素实时光照

    4. 减少纹理采样次数

      合并贴图、使用 Mipmap

  3. 避免无效像素(片元)计算

    1. 减少 OverDraw

    2. 优化UI系统

      减少叠层,避免全屏大半透明面板,合并图集减少 DrawCall

    3. 剔除不可见对象

      用 Occlusion Culling 和 视锥剔除

禁用不需要的特性

降低填充率压力的主要思路就是

  • 减少被着色的像素数
  • 减少每个像素的着色成本

而着色器中的透明度、深度写入、透明度测试、透明度混合等特性
都会增加每个像素的着色成本,会进行更多的计算和逻辑处理
因此,少一个特性就等于少一段像素级逻辑,可以有效降低片元着色器工作量

比如我们可以通过渲染标签、渲染状态禁用一些处理

  • 不投射阴影

    1
    Tags { "ForceNoShadowCasting" = "True" }
  • 关闭深度缓冲

    1
    ZWrite Off    // 不写入深度缓冲

等等

使用基于着色器的 LOD

我们可以强制 Unity 使用更简单的着色器来渲染远端对象,这是一种节省填充率的有效方法
特别是将游戏部署到多个平台或者需要支持多种硬件功能时

Shader LOD 就是给 Shader 提供多档复杂度版本
引擎根据全局或局部 Shader 的 maximumLOD 参数选择合适的 SubShader
从而减少片元计算开销,从而降低填充率压力

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Shader "Custom/LODExample"
{
SubShader {
LOD 300
Pass {
// 高级效果(带法线贴图、PBR 等)
}
}
SubShader {
LOD 150
Pass {
// 中级效果(只有 Diffuse + 简单光照)
}
}
SubShader {
LOD 50
Pass {
// 最低级效果(纯颜色/贴图)
}
}
}

在 C# 中:

1
2
3
4
5
//全局修改:
Shader.globalMaximumLOD = 200;
//局部修改,只对某个 Shader 生效
Shader s = Shader.Find("着色器路径");
s.maximumLOD = 150;

使用光照剔除

光照相关的计算是着色器处理中的高开销项,灯光组件上有 Culling Mask (剔除遮罩,内置渲染管线) 或 Rendering Layer Mask (渲染图层蒙版,SRP管线)
他们的作用都是决定场景中哪些层级的对象会受到该光源影响,我们可以通过该参数让某些层不被对应光源影响
通过它们来限制光源影响范围,从而减少着色器中光照相关的计算

image

谨慎使用实时阴影

实时阴影的渲染过程消耗是比较大的
因为在渲染底层,会渲染阴影贴图,片元阶段还会进行阴影贴图采样,进行过滤处理等等
因此尽量避免使用实时阴影,可以有效减少渲染和采样计算压力。

可以采用以下方式去优化:

  1. 烘焙光照阴影
  2. 降低阴影质量的方式

使用烘焙的光照纹理

实时光源计算会对渲染造成很大的压力,每个像素的计算量会很大
因此,通过使用烘焙光照纹理的方式,可以大幅减少每像素的实时光照计算与采样

  • 静态物体我们可以使用 光照烘焙贴图
  • 动态物体我们可以使用 Light Probe(光照探针)、Reflection Probe(反射探针)

等等

通过这些方式我们可以有效减少光照处理带来的压力

主要思路就是把实时的计算改为提前算好,从纹理中取出算好的颜色进行渲染

减少多重渲染目标 (MRT)

多重渲染目标 (MRT, Multiple Render Targets),表示 一次绘制调用中,同时输出到多张 Render Target (RT)
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct FragOut
{
float4 color0 : SV_Target0;
float4 color1 : SV_Target1;
float4 color2 : SV_Target2;
};

FragOut frag()
{
FragOut o;
o.color0 = ...; // G-Buffer Albedo
o.color1 = ...; // G-Buffer Normal
o.color2 = ...; // G-Buffer Specular
return o;
}

这样 GPU 一次片元着色,就能把结果写入多张纹理

MRT 一般用在

  1. 延迟渲染中使用

    G-Buffer 通常需要 3–5 张纹理存储不同的材质信息
    一次几何 Pass 写多个 RT,下一步光照 Pass 再利用这些数据

  2. 自定义特效

    需要同时输出颜色和 ID 贴图、或者存储额外的 Mask 数据

MRT 带来的开销:

  1. 填充率压力更大

    本来只写一个 RT(一次片元输出),现在要写 3–4 个 RT → 每个像素的写入次数成倍增加

  2. 显存占用更大

    每个 RT 都是一张纹理(可能是全屏大小),分辨率越高,显存压力越大

  3. 带宽受限时特别明显

    尤其在移动平台,GPU 的 ROP(光栅输出单元)数量有限,同时写多个 RT 可能导致瓶颈

因此我们应该 能不用 MRT 就不用
如果一定要使用,也应该通过纹理合并、纹理压缩、降低精度、避免无效输出 方式去进行优化
在移动平台上对 MRT 的支持有限,建议谨慎使用