GameTech/URP 渲染实战

案例 13.2写一个“PBR 校验”Shader进阶

目标:写一个读取 URP Lit 材质属性的校验 Shader,按 PBR 原则把超出范围的像素用醒目的斜条纹标出来,并能一键作用于整个场景。

规则按材质类型分开判断,比 Rendering Debugger 更贴近 13.4 节的原则:

条件标记含义
0.1 < 金属度 < 0.9黄色条纹金属度灰区,只应出现在过渡边缘
金属度 ≥ 0.9 且 Albedo 最大分量 < sRGB 180品红条纹金属的 F0 过暗
电介质且亮度 < sRGB 30红色条纹电介质过暗
电介质且亮度 > sRGB 240蓝色条纹电介质过亮
Assets/Cookbook/Ch13/PBRValidate.shader(节选:thresholds)
// 阈值用 sRGB 0~1 表示(美术在取色器里看到的数值 / 255)。
// 全局变量,由 C# 用 Shader.SetGlobalVector 设置;w <= 0 时使用下面的默认值。
float4 _PBRValidateRange;   // x 电介质最暗  y 电介质最亮  z 金属最暗
#define DEFAULT_RANGE float3(30.0 / 255.0, 240.0 / 255.0, 180.0 / 255.0)
#define METAL_GRAY_LOW  0.1
#define METAL_GRAY_HIGH 0.9
// 阈值用 sRGB 0~1 表示(美术在取色器里看到的数值 / 255)。
// 全局变量,由 C# 用 Shader.SetGlobalVector 设置;w <= 0 时使用下面的默认值。
uniform vec4 _PBRValidateRange;   // x 电介质最暗  y 电介质最亮  z 金属最暗
#define DEFAULT_RANGE vec3(30.0 / 255.0, 240.0 / 255.0, 180.0 / 255.0)
#define METAL_GRAY_LOW  0.1
#define METAL_GRAY_HIGH 0.9
Assets/Cookbook/Ch13/PBRValidate.shader(节选:validate)
half4 frag(Varyings i) : SV_Target
{
    half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb * _BaseColor.rgb;   // 线性
#if defined(_METALLICSPECGLOSSMAP)
    half metallic = SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, i.uv).r;
#else
    half metallic = _Metallic;
#endif

    float3 range = _PBRValidateRange.w > 0 ? _PBRValidateRange.xyz : DEFAULT_RANGE;
    // 回到 sRGB 再比较,和美术在 Photoshop/Substance 里看到的数值一致
    half lumS = LinearToSRGB(Luminance(albedo).xxx).x;
    half maxS = LinearToSRGB(max(albedo.r, max(albedo.g, albedo.b)).xxx).x;

    half3 flag = 0; half bad = 0;
    if (metallic > METAL_GRAY_LOW && metallic < METAL_GRAY_HIGH) { flag = half3(1, 0.85, 0); bad = 1; }   // 黄
    else if (metallic >= METAL_GRAY_HIGH)
    {
        if (maxS < range.z) { flag = half3(1, 0, 1); bad = 1; }                                          // 品红
    }
    else
    {
        if (lumS < range.x)      { flag = half3(1, 0.1, 0.1); bad = 1; }                                // 红
        else if (lumS > range.y) { flag = half3(0.15, 0.35, 1); bad = 1; }                              // 蓝
    }

    // 底图:Albedo 亮度 × 简单的半兰伯特,保留形体,又不让光照干扰“材质值”的判断
    half ndl = dot(normalize(i.normalWS), GetMainLight().direction) * 0.5 + 0.5;
    half3 baseCol = Luminance(albedo).xxx * (0.35 + 0.65 * ndl);

    // 屏幕空间斜条纹:一眼能和“本来就是这个颜色的材质”区分开
    half stripe = frac((i.positionCS.x + i.positionCS.y) / 14.0) < 0.5;
    half3 c = lerp(baseCol, flag, bad * lerp(0.35, 0.9, stripe));
    return half4(c, 1);
}
void main()
{
    vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb;   // 线性
#if defined(_METALLICSPECGLOSSMAP)
    float metallic = texture(_MetallicGlossMap, uv).r;
#else
    float metallic = _Metallic;
#endif

    vec3 range = _PBRValidateRange.w > 0.0 ? _PBRValidateRange.xyz : DEFAULT_RANGE;
    // 回到 sRGB 再比较,和美术在 Photoshop/Substance 里看到的数值一致
    float lumS = LinearToSRGB(vec3(Luminance(albedo))).x;
    float maxS = LinearToSRGB(vec3(max(albedo.r, max(albedo.g, albedo.b)))).x;

    vec3 flag = vec3(0.0); float bad = 0.0;
    if (metallic > METAL_GRAY_LOW && metallic < METAL_GRAY_HIGH) { flag = vec3(1, 0.85, 0); bad = 1.0; }   // 黄
    else if (metallic >= METAL_GRAY_HIGH)
    {
        if (maxS < range.z) { flag = vec3(1, 0, 1); bad = 1.0; }                                          // 品红
    }
    else
    {
        if (lumS < range.x)      { flag = vec3(1, 0.1, 0.1); bad = 1.0; }                                // 红
        else if (lumS > range.y) { flag = vec3(0.15, 0.35, 1); bad = 1.0; }                              // 蓝
    }

    // 底图:Albedo 亮度 × 简单的半兰伯特,保留形体,又不让光照干扰“材质值”的判断
    float ndl = dot(normalize(normalWS), GetMainLight().direction) * 0.5 + 0.5;
    vec3 baseCol = vec3(Luminance(albedo)) * (0.35 + 0.65 * ndl);

    // 屏幕空间斜条纹:一眼能和“本来就是这个颜色的材质”区分开
    float stripe = fract((gl_FragCoord.x + gl_FragCoord.y) / 14.0) < 0.5 ? 1.0 : 0.0;
    vec3 c = mix(baseCol, flag, bad * mix(0.35, 0.9, stripe));
    fragColor = vec4(c, 1.0);
}

几个设计考虑:

把它作用到整个场景,用的是 URP 14 自带的 Render Objects Renderer Feature 的 Override Mode = Shader。与 Override Material 不同,Override Shader 会保留每个物体原材质的属性值,只替换 Shader,这正是校验视图需要的。在 Renderer 资产上手动添加时,按下列设置填写:

  1. 选中项目使用的 Universal Renderer Data 资产,Add Renderer Feature › Render Objects。
  2. Event 选 AfterRenderingOpaques;Filters › Queue 选 Opaque,Layer Mask 选 Everything。
  3. Overrides › Override Mode 选 Shader,Shader 选 Cookbook/Ch13/PBRValidate,Pass Index 为 0。
  4. 需要校验时勾选该 Feature,平时取消勾选即可。

配图脚本里用代码做了同样的配置:

Assets/Cookbook/Ch13/Editor/Ch13Shots.cs(节选:validate_feature)
// 用 RenderObjects(Override Mode = Shader)把整个场景的不透明物体用 PBRValidate 重画一遍。
// 与在 Renderer 资产的 Inspector 里手动添加 Render Objects Feature 并这样填写等价。
static void AddValidateFeature(Shot s)
{
    Shader.SetGlobalVector("_PBRValidateRange", new Vector4(30f / 255f, 240f / 255f, 180f / 255f, 1f));
    var f = s.Feature<RenderObjects>();
    f.settings.Event = RenderPassEvent.AfterRenderingOpaques;
    f.settings.filterSettings.RenderQueueType = RenderQueueType.Opaque;
    f.settings.filterSettings.LayerMask = ~0;
    f.settings.overrideMode = RenderObjects.RenderObjectsSettings.OverrideMaterialMode.Shader;
    f.settings.overrideShader = Shader.Find("Cookbook/Ch13/PBRValidate");
    f.settings.overrideShaderPassIndex = 0;
    f.Create();   // 改完 settings 要重新 Create,Feature 才会按新设置建 Pass
}
图 13-14左:正常着色;右:用 Render Objects(Override Shader)把 PBRValidate 应用到整个场景。过暗的黑球标红、纯白球标蓝、金属度 0.5 的球标黄、暗色金属标品红;正常的电介质、正常金属和砖墙地面保持灰度显示;后方刷漆金属环面上,只有漆皮剥落的过渡边缘出现黄色——这是允许的灰区。

后方环面的结果值得注意:它使用的 Mask 贴图(案例 13.1 生成)在过渡带上有 0~1 之间的金属度,校验视图把这些边缘标成黄色。这不是错误,而是提醒你:过渡带的宽度是否合理?如果整片都是黄色,说明过渡带太宽了。校验工具的作用是把问题“显影”,最终判断仍然需要人来做。

注意Override Shader 模式下,物体原材质的局部关键字能否带到新 Shader,取决于新 Shader 是否声明了同名关键字。本例的环面正确显示了金属区域,说明 _METALLICSPECGLOSSMAP 的状态被带过来了;如果你的校验 Shader 没有声明这个关键字,所有使用金属度贴图的 Lit 材质都会退回到 _Metallic 滑条的值,校验结果就不可信。