URP 渲染实战 / 第 13 章 PBR 材质制作与校准 / 案例 13.2
案例 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
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 Lit 完全一致(
_BaseMap、_BaseColor、_Metallic、_MetallicGlossMap),并声明了同名关键字_METALLICSPECGLOSSMAP。因此它可以直接“替换”Lit:在材质上把 Shader 换成它,所有贴图和数值都保留;更重要的是可以作为 RenderObjects 的 Override Shader 使用。 - 比较在 sRGB 空间进行。采样得到的是线性值,用
LinearToSRGB转回 sRGB 再和阈值比较,这样阈值可以直接使用美术熟悉的 0~255 数值。 - 阈值是全局变量。如果写成材质属性,在 Override Shader 模式下,原材质上不存在这些属性,取不到值;全局变量由 C# 统一设置,w 分量为 0 时使用默认值。
- 底图只保留亮度和简单的形体明暗。校验视图的目的是看材质值,彩色光照和高光只会干扰判断;斜条纹是屏幕空间的,可以和“本来就是红色的材质”区分开。
把它作用到整个场景,用的是 URP 14 自带的 Render Objects Renderer Feature 的 Override Mode = Shader。与 Override Material 不同,Override Shader 会保留每个物体原材质的属性值,只替换 Shader,这正是校验视图需要的。在 Renderer 资产上手动添加时,按下列设置填写:
- 选中项目使用的 Universal Renderer Data 资产,。
- Event 选 AfterRenderingOpaques;Filters › Queue 选 Opaque,Layer Mask 选 Everything。
- Overrides › Override Mode 选 Shader,Shader 选 Cookbook/Ch13/PBRValidate,Pass Index 为 0。
- 需要校验时勾选该 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 }

后方环面的结果值得注意:它使用的 Mask 贴图(案例 13.1 生成)在过渡带上有 0~1 之间的金属度,校验视图把这些边缘标成黄色。这不是错误,而是提醒你:过渡带的宽度是否合理?如果整片都是黄色,说明过渡带太宽了。校验工具的作用是把问题“显影”,最终判断仍然需要人来做。
注意Override Shader 模式下,物体原材质的局部关键字能否带到新 Shader,取决于新 Shader 是否声明了同名关键字。本例的环面正确显示了金属区域,说明
_METALLICSPECGLOSSMAP 的状态被带过来了;如果你的校验 Shader 没有声明这个关键字,所有使用金属度贴图的 Lit 材质都会退回到 _Metallic 滑条的值,校验结果就不可信。本案例出自《URP 渲染实战》第 13 章「PBR 材质制作与校准」。在章节中阅读上下文 →