GameTech/URP 渲染实战

案例 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);
图 9-19上:只输出镜面项(F0 = 0.9 的“白金属”,Exp 显示);下:完整直接光照,深红色塑料(F0 = 0.04,Raw 显示)。感知粗糙度均为 0.1 → 0.9,只有一盏方向光,没有环境光。

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