URP 渲染实战 / 第 9 章 PBR 理论 / 案例 9.6
案例 9.6镜面项与完整直接光的粗糙度扫描进阶
目标:看完整的 Cook-Torrance 镜面项和“漫反射 + 镜面”的最终结果怎样随粗糙度变化,体会能量守恒。
镜面模式的关键代码就是 Shader 片元函数里的这几行:
Assets/Cookbook/Ch09/BRDFTerms.shader(节选:frag)
float3 N = normalize(i.normalWS); float3 V = GetWorldSpaceNormalizeViewDir(i.positionWS); Light light = GetMainLight(); float3 L = light.direction; float3 H = SafeNormalize(L + V); float NoL = saturate(dot(N, L)); float NoV = max(dot(N, V), 1e-4); // 防止边缘处除零 float NoH = saturate(dot(N, H)); float VoH = saturate(dot(V, H)); float LoH = VoH; // h 是 l、v 的角平分线,两者相等 float a = _Roughness * _Roughness; // α = perceptualRoughness² float D = EvalD(NoH, a); float G = EvalG(NoL, NoV, a); float3 F = C9_F_Schlick(_F0.rgb, VoH); // Cook-Torrance 镜面 BRDF:D·G·F / (4 (n·l)(n·v)) float3 fSpec = D * G * F / max(4.0 * NoL * NoV, 1e-4); float3 fDiff = (_DiffuseModel < 0.5) ? C9_Lambert(_Albedo.rgb) : C9_DisneyDiffuse(_Albedo.rgb, NoL, NoV, LoH, _Roughness); // Unity 的灯光“颜色×强度”约定:白色 Lambert 面正对强度 1 的灯时输出 1, // 相当于入射辐照度 E = π · lightColor,所以出射辐射率 = f · π · lightColor · (n·l)。 float3 E = PI * light.color * NoL; float3 c; if (_Mode < 0.5) c = D.xxx; else if (_Mode < 1.5) c = (NoL > 0.0 ? G : 0.0).xxx; else if (_Mode < 2.5) c = C9_F_Schlick(_F0.rgb, NoV); else if (_Mode < 3.5) c = F; else if (_Mode < 4.5) c = fSpec * E; else if (_Mode < 5.5) c = fDiff * E; else c = ((1.0 - F) * fDiff + fSpec) * E; // 漫反射只拿“没被镜面反射走”的那部分 // 显示映射:Raw 直接输出(超过 1 会被截断);Exp 用 1 - e^(-x·k) 把大范围数值压进 0..1 if (_Display > 0.5) c = 1.0 - exp(-c * _Exposure); else c *= _Exposure; return half4(c, 1);
vec3 N = normalize(normalWS); vec3 V = GetWorldSpaceNormalizeViewDir(positionWS); Light light = GetMainLight(); vec3 L = light.direction; vec3 H = SafeNormalize(L + V); float NoL = saturate(dot(N, L)); float NoV = max(dot(N, V), 1e-4); // 防止边缘处除零 float NoH = saturate(dot(N, H)); float VoH = saturate(dot(V, H)); float LoH = VoH; // h 是 l、v 的角平分线,两者相等 float a = _Roughness * _Roughness; // α = perceptualRoughness² float D = EvalD(NoH, a); float G = EvalG(NoL, NoV, a); vec3 F = C9_F_Schlick(_F0.rgb, VoH); // Cook-Torrance 镜面 BRDF:D·G·F / (4 (n·l)(n·v)) vec3 fSpec = D * G * F / max(4.0 * NoL * NoV, 1e-4); vec3 fDiff = (_DiffuseModel < 0.5) ? C9_Lambert(_Albedo.rgb) : C9_DisneyDiffuse(_Albedo.rgb, NoL, NoV, LoH, _Roughness); // Unity 的灯光“颜色×强度”约定:白色 Lambert 面正对强度 1 的灯时输出 1, // 相当于入射辐照度 E = π · lightColor,所以出射辐射率 = f · π · lightColor · (n·l)。 vec3 E = PI * light.color * NoL; vec3 c; if (_Mode < 0.5) c = vec3(D); else if (_Mode < 1.5) c = vec3(NoL > 0.0 ? G : 0.0); else if (_Mode < 2.5) c = C9_F_Schlick(_F0.rgb, NoV); else if (_Mode < 3.5) c = F; else if (_Mode < 4.5) c = fSpec * E; else if (_Mode < 5.5) c = fDiff * E; else c = ((1.0 - F) * fDiff + fSpec) * E; // 漫反射只拿“没被镜面反射走”的那部分 // 显示映射:Raw 直接输出(超过 1 会被截断);Exp 用 1 - e^(-x·k) 把大范围数值压进 0..1 if (_Display > 0.5) c = 1.0 - exp(-c * _Exposure); else c *= _Exposure; fragColor = vec4(c, 1.0);


上排展示了能量守恒的直观形态:高光从一个极亮的小点,逐渐摊开成覆盖半个球的柔光,越摊越暗。下排是“真实感”更强的结果:光滑塑料的高光极小、极亮(Raw 显示下已截断为纯白),粗糙塑料的高光几乎溶进了漫反射。注意下排暗部是纯黑的——我们只算了直接光,没有环境光。真实场景中,光滑物体的“质感”主要来自环境反射,而不是这一个小亮点,这也是为什么第 8 章的反射探针对 PBR 如此重要。