88.降低着色器计算量

88.性能优化-GPU-着色器优化-降低计算量


88.1 知识点

尽量使用针对移动平台的着色器

Unity 内置了很多面向移动平台的 Shader,即使项目不是移动平台,也可以视情况选用。前提是:这些 Shader 带来的效果损失在项目可接受范围内。

这样无需改任何 Shader 代码,只切换使用的内置 Shader 就能获得明显的性能提升。

使用小的数据类型并且避免精度转换

  1. 优先使用更小的数据类型:在 GPU 上用 halffixed 往往比 float 更快,尤其在移动端。注意:桌面 GPU 上 half 常被当 float 处理,收益有限。
  2. 避免频繁的精度转换:在同一 Shader 内尽量统一精度,减少 half/float 等之间的转换,因为精度转换会带来额外开销。

尽量使用 GPU 优化的辅助函数

Unity 提供大量内置 Shader 库(如 UnityCG.cgincUnityShaderVariables.cgincLighting.cgincHLSLSupport.cginc 等),其中封装了常用计算,并做了跨平台兼容(坐标系、色彩空间等)。有需求时引用这些文件并使用内置函数即可。

常见类别包括:常规运算(abssignminmaxsaturateclamp)、插值与混合(lerpsmoothstep)、三角函数、指数与对数(explogpowsqrtrsqrt)、向量操作(dotcrossnormalizereflectrefract 等)、亮度/灰度(Luminance)、Gamma/Linear 转换(GammaToLinearSpaceLinearToGammaSpace)、顶点/法线空间变换(UnityObjectToClipPosUnityObjectToWorldPosUnityObjectToWorldNormal 等)、采样(tex2DtexCUBE)等。

实际开发中优先使用这些函数,既能避免重复实现,也能保证在各平台上正确且高效。

参考文档:

删除不必要的输入数据

顶点阶段用结构体决定传入/使用的模型数据,片元阶段用结构体决定从顶点传出的数据。应避免传入或传出用不到的字段:例如顶点不需要法线、切线时就不要在结构体里声明;片元用不到的数据也不要从顶点插值出去,否则会白做插值计算。

只公开所需的变量

Shader 中应只把真正需要外部控制的变量暴露为材质参数,这样能减少常量缓冲区的传输量,并让 Inspector 更简洁。固定不变的量可以直接写在 Shader 里,不必做成材质参数。

预计算

对常用且不变的数据,应提前在 CPU 或纹理里算好,避免每帧在 Shader 里重复计算。

例如:

  1. 光照烘焙:把光照结果预计算到光照贴图,运行时直接采样。
  2. LUT 纹理(Look-Up Table):把复杂、昂贵的函数结果预烘焙到一张纹理,Shader 里用坐标“查表”取结果,用一次纹理采样替代复杂运算。例如要频繁做 pow(x, 2.2) 的 Gamma 校正时,可预生成一张 256 宽的 1D 纹理存 [0,1] 区间的 pow(x, 2.2),在 Shader 中采样:
float gammaCorrect = tex2D(_GammaLUT, float2(x, 0)).r;

减少逐像素计算

能在顶点阶段算的就不要放到片元阶段。顶点数通常远小于像素数,从顶点插值到片元的成本低于在片元里逐像素计算。例如顶点颜色渐变可在顶点算好再插值到片元。

注意:并非所有效果都适合从片元移到顶点,需根据视觉效果取舍。

合并多次计算

避免对同一表达式重复计算,应存结果再复用。

错误示例:多次计算 sin(x)

float a = sin(x) * 0.5;
float b = sin(x) * 2.0;

正确示例:只算一次再复用:

float s = sin(x);
float a = s * 0.5;
float b = s * 2.0;

利用常量缓冲区

把多个 Shader 共用的常量(灯光、矩阵、全局材质参数等)放进常量缓冲区(CBUFFER),可以一次传输、多处使用,减少 CPU 到 GPU 的频繁更新。

常量缓冲区(Constant Buffer / CBuffer)是 GPU 中专门存放 uniform 变量的区域。

Shader 中用法示例:

// 在 UnityPerMaterial 块中声明材质相关 uniform,便于合批与减少传参
CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;   // 基础颜色
    float _Roughness;    // 粗糙度
CBUFFER_END

常用内置块名:UnityPerCamera(相机相关)、UnityPerFrame(时间、屏幕等)、UnityPerDraw(物体矩阵等)、UnityPerMaterial(材质参数)。

C# 中配合 MaterialPropertyBlock 设置材质参数示例:

这段代码的作用是:在运行时给共用同一份材质的多个物体设置各自不同的 Shader 参数(如颜色、粗糙度),而不必为每个物体 new Material()。这样既能避免材质实例过多、有利于合批,又不会破坏材质共享;参数通过 MaterialPropertyBlock 挂在渲染器上,与 Shader 里的 CBUFFER 对应,减少重复上传。

// 获取 Shader 中属性的名称 ID,避免每帧字符串查找
int baseColorId = Shader.PropertyToID("_BaseColor");
int roughnessId = Shader.PropertyToID("_Roughness");

MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock();
renderer.GetPropertyBlock(propertyBlock);           // 先读取当前已设置的属性,避免覆盖
propertyBlock.SetColor(baseColorId, Color.red);     // 设置颜色
propertyBlock.SetFloat(roughnessId, 0.5f);          // 设置浮点参数
renderer.SetPropertyBlock(propertyBlock);            // 写回渲染器,不破坏材质实例共享

88.2 知识点代码

Lesson88_性能优化_GPU_着色器优化_降低计算量.cs

public class Lesson88_性能优化_GPU_着色器优化_降低计算量
{
    #region 知识点一 尽量使用针对移动平台的着色器

    //Unity中内置了很多专门针对移动平台的Shader
    //那么即使我们开发的是非移动平台项目也可以选择使用它
    //前提:
    //使用这些Shader带来的效果损失项目是可以接受的

    //这样我们不需要修改任何Shader代码
    //直接改变使用的内置Shader就能带来明显性能提升

    #endregion

    #region 知识点二 使用小的数据类型并且避免精度转换

    //1.在GPU里使用更小的数据类型来计算比使用更大的数据类型速度可能会更快(尤其是在移动端时)
    //  因此我们可以更多的选择使用half、fixed,尽量少用float类型
    //  注意:
    //  桌面GPU中,half常常会被当作float处理,性能提升有限
    //2.我们应该避免频繁的在低精度和高精度之间相互转换
    //  尽量在同一Shader内保持一致的精度选择
    //  因为精度转换会损耗性能

    #endregion

    #region 知识点三 尽量使用GPU优化的辅助函数

    //Unity中提供了很多内置Shader库文件
    //比如:
    //UnityCG.cginc
    //UnityShaderVariables.cginc
    //Lighting.cginc
    //HLSLSupport.cginc
    //等等
    //这些文件中包含了很多封装好的函数和宏
    //这些内容一方面封装了 HLSL/CG 的常用计算
    //另一方面还对跨平台兼容性(坐标系、色彩空间等)做了优化

    //当我们有相关计算需求时,我们可以引用这些文件
    //并使用这些内置函数来进行计算
    //比如:
    //1.常规运算:abs、sign、min、max、saturate、clamp
    //2.插值与混合:lerp(a,b,t)、smoothstep(edge0,edge1,x)
    //3.三角函数:sin、cos、tan、asin、acos、atan2
    //4.指数与对数:exp、exp2、log、log2、pow(x,y)、sqrt、rsqrt(快速倒数平方根)
    //5.向量操作:dot(点积)、cross(叉积)、normalize、length、distance、
    //         reflect(反射向量)、refract(折射向量)、faceforward
    //6.亮度/灰度:Luminance
    //7.Gamma/Linear 转换:GammaToLinearSpace(col)、LinearToGammaSpace(col)
    //8.顶点空间变换:UnityObjectToClipPos(v)、UnityObjectToWorldPos(v)
    //  UnityWorldToObjectDir(d)、UnityWorldToClipPos(v)
    //9.法线变换:UnityObjectToWorldNormal(n)、UnityWorldToObjectNormal(n)
    //10.采样/纹理工具:tex2D(sampler, uv)、texCUBE(sampler, dir)
    //等等

    //在实际开发中,优先使用 Unity 提供的这些函数
    //可以避免重复造轮子,并确保在不同平台上获得正确且高效的结果

    //参考文档:
    //HLSL/CG内置函数(Unity基本支持)
    //https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions

    //Unity封装的内容
    //https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html
    //Unity内置着色器源码,可以看到内置文件
    //https://unity.com/releases/editor/whats-new/6000.0.58f1#installs

    #endregion

    #region 知识点四 删除不必要的输入数据

    //在Shader开发中
    //顶点着色器阶段
    //需要自定义结构体决定需要传入和使用哪些模型数据
    //片元着色器阶段
    //需要自定义结构体决定从顶点着色器中传出哪些数据

    //我们应该尽量避免传入和传出不需要使用的数据
    //比如
    //在顶点着色器中如果不需要用到法线、切线等数据
    //就不要在结构体中进行声明
    //在片元着色器中不需要用到的数据也不要从顶点传出
    //否则会进行插值相关的计算

    #endregion

    #region 知识点五 只公开所需的变量

    //在Shader中应该只保留真正需要外部控制的变量成为材质参数
    //这样可以减少常量缓冲区传输的开销
    //还可以让Inspector窗口更简洁
    //如果某些成员是固定值,可以直接写死在Shader中
    //不用公开成材质参数

    #endregion

    #region 知识点六 预计算

    //对于一些常用不变的数据
    //我们可以提前计算好,放在CPU或者纹理贴图中
    //不要每帧都在Shader中重复计算
    //举例:
    //1.光照烘焙贴图就是把光照结果预先算好放入了光照纹理中,直接取出来使用
    //2.一些复杂函数结果,可以预烘焙为LUT纹理
    //  Shader内直接从其中采样即可
    //  LUT纹理:Look-Up Table(查找表纹理)
    //          把复杂、昂贵的计算结果 提前存到一张纹理里,在 Shader 中直接通过坐标去"查表"取结果
    //          用一次纹理采样(内存带宽消耗)替代复杂的数学计算
    //  比如:
    //  假设你要频繁算 pow(x,2.2) 来做 Gamma 校正
    //  如果不用LUT纹理,每次计算开销大 float gammaCorrect = pow(x, 2.2);
    //  我们可以先在 CPU 或工具里预生成一个 256 像素宽的 1D 纹理,存放 [0,1] 区间的 pow(x,2.2) 值
    //  在Shader中我们直接在该纹理中采样即可
    //  float gammaCorrect = tex2D(_GammaLUT, float2(x, 0)).r;

    #endregion

    #region 知识点七 减少逐像素计算

    //能在顶点阶段计算的,就不要放在片元阶段
    //因为 顶点数量 绝大多数情况下都是远远小于 像素数量 的
    //顶点 到 片元阶段 中插值计算的效率 是优于 在片元中逐像素计算的
    //比如:如果只是顶点颜色渐变,直接在顶点算好,再插值到片元

    //注意:并不是所有效果都适合从片元转移到顶点计算

    #endregion

    #region 知识点八 合并多次计算

    //在Shader编程时
    //避免重复算同样的表达式,应该把结果存起来复用
    //错误做法:
    //float a = sin(x) * 0.5;
    //float b = sin(x) * 2.0;
    //正确做法:
    //float s = sin(x);
    //float a = s * 0.5;
    //float b = s * 2.0;

    #endregion

    #region 知识点九 利用常量缓冲区

    //应该把多个 Shader 共享的常量数据(灯光参数、矩阵、全局材质参数)
    //放到常量缓冲区(CBUFFER)中
    //这样的好处是 一次传输,多处使用,减少 CPU 到 GPU 的频繁数据更新

    //常量缓冲区(Constant Buffer,简称 CBuffer)
    //是 GPU 内存中一块 专门存放统一变量(Uniforms) 的区域
    //使用方式
    //Shader代码中
    //CBUFFER_START(自定义名字 或 Unity内置常量缓冲区名)
    //    float4 _BaseColor;
    //    float _Roughness;
    //CBUFFER_END
    //内置常量缓冲区名
    //UnityPerCamera:相机相关(矩阵、位置)
    //UnityPerFrame:时间、屏幕尺寸、帧率相关
    //UnityPerDraw:物体的世界矩阵等
    //UnityPerMaterial:材质参数

    //C#代码中
    //int BaseColorID = Shader.PropertyToID("_BaseColor");
    //int RoughnessID = Shader.PropertyToID("_Roughness");
    //MaterialPropertyBlock mpb = new MaterialPropertyBlock();
    //renderer.GetPropertyBlock(mpb);
    //mpb.SetColor(BaseColorID, Color.red);
    //mpb.SetFloat(RoughnessID, 0.5f);
    //renderer.SetPropertyBlock(mpb);

    #endregion
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏