URP 渲染实战 / 第 30 章 调试与排错 / 案例 30.1
案例 30.1可切换输出的调试视图 Shader入门
目标:写一个带法线贴图、能接收阴影的 Shader,用一个枚举属性切换输出最终光照、世界法线、UV、N·L、阴影衰减、切线空间法线和世界网格。
Assets/Cookbook/Ch30/DebugView.shader(节选:debug)
half4 frag(Varyings i) : SV_Target { half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv), _BumpScale); half3 bitangentWS = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz); half3 normalWS = normalize(TransformTangentToWorld(normalTS, half3x3(i.tangentWS.xyz, bitangentWS, i.normalWS))); Light light = GetMainLight(TransformWorldToShadowCoord(i.positionWS)); half ndl = saturate(dot(normalWS, light.direction)); int view = (int)_DebugView; // 约定:方向向量 [-1,1] 映射到 [0,1] 再显示;标量直接显示为灰度 if (view == 1) return half4(normalWS * 0.5 + 0.5, 1); if (view == 2) return half4(frac(i.uv), 0, 1); if (view == 3) return half4(ndl.xxx, 1); if (view == 4) return half4(light.shadowAttenuation.xxx, 1); if (view == 5) return half4(normalTS * 0.5 + 0.5, 1); if (view == 6) { float3 g = abs(frac(i.positionWS * 2 + 0.5) - 0.5) / fwidth(i.positionWS * 2); half line01 = 1 - saturate(min(g.x, min(g.y, g.z))); return half4(lerp(half3(0.15, 0.15, 0.18), half3(1, 0.6, 0.2), line01), 1); } half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb; half3 color = albedo * (light.color * ndl * light.shadowAttenuation + SampleSH(normalWS)); return half4(color, 1); }
void main() { vec3 normalTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale); vec3 bitangentWS = tangentWS.w * cross(normalWS, tangentWS.xyz); vec3 nWS = normalize(TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangentWS, normalWS))); Light light = GetMainLight(TransformWorldToShadowCoord(positionWS)); float ndl = saturate(dot(nWS, light.direction)); int view = int(_DebugView); // 约定:方向向量 [-1,1] 映射到 [0,1] 再显示;标量直接显示为灰度 if (view == 1) { fragColor = vec4(nWS * 0.5 + 0.5, 1.0); return; } if (view == 2) { fragColor = vec4(fract(uv), 0.0, 1.0); return; } if (view == 3) { fragColor = vec4(vec3(ndl), 1.0); return; } if (view == 4) { fragColor = vec4(vec3(light.shadowAttenuation), 1.0); return; } if (view == 5) { fragColor = vec4(normalTS * 0.5 + 0.5, 1.0); return; } if (view == 6) { vec3 g = abs(fract(positionWS * 2.0 + 0.5) - 0.5) / fwidth(positionWS * 2.0); float line01 = 1.0 - saturate(min(g.x, min(g.y, g.z))); fragColor = vec4(mix(vec3(0.15, 0.15, 0.18), vec3(1, 0.6, 0.2), line01), 1.0); return; } vec3 albedo = texture(_BaseMap, uv).rgb; vec3 color = albedo * (light.color * ndl * light.shadowAttenuation + SampleSH(nWS)); fragColor = vec4(color, 1.0); }
调试模式用一个普通的 float 属性加 if 分支,而不是关键字:调试 Shader 不在乎这点分支开销,却能避免变体数量翻倍,切换时也不需要重新编译。[Enum(Final,0, NormalWS,1, …)] 让 Inspector 显示成下拉菜单。投影阴影借用了 Lit 的 ShadowCaster Pass(UsePass),这会让本 Shader 不兼容 SRP Batcher(两个 Pass 的 UnityPerMaterial 不一致),对调试 Shader 无所谓。

其他几条实用技巧:
- 看编译后的代码:在 Shader 的 Inspector 中点击 Compile and show code,可以选择平台,查看编译器生成的代码,确认某段代码是否被优化掉、某个关键字分支是否存在、实际用了多少指令和寄存器。
- 用 Frame Debugger 确认参数:在怀疑 Shader 逻辑之前,先确认输入对了——纹理绑的是不是那张、参数是不是传进去了、关键字有没有开。很多“Shader bug”其实是 C# 没把参数设对。
- 二分注释:Shader 编译报错位置不明确时(尤其是宏展开后的错误),把一半代码注释掉、直接 return 一个常量,逐步缩小范围。
- 读错误信息里的文件和行号:错误经常报在 URP 的库文件里(例如第 28 章遇到的
Shadows.hlsl的LerpWhiteTo)。这通常不是库有 bug,而是你的 include 顺序或缺少某个 include,导致库代码依赖的东西没有定义。