URP 渲染实战 / 第 5 章 经典光照模型 / 案例 5.3
案例 5.3Wrap Lighting 与交界线染色进阶
目标:用一个参数在 Lambert 和 Half-Lambert 之间连续过渡,并给出“能量守恒”版本;再利用 Wrap 多出来的那一圈,给交界线染上颜色,做出廉价的次表面散射感。
原理
Wrap Lighting 让光“绕过”交界线,照亮背光面的一部分。参数 w(0~1)表示绕过去多远:
w = 0 时就是 Lambert;w = 1 时就是不平方的 Half-Lambert;中间的值让归零点落在 θ = arccos(−w) 处,比如 w = 0.5 时,光一直照到 120°。
这个式子有一个不太显眼的问题:它让表面变亮了。被照亮的区域变大,而峰值(正对光源处)仍然是 1,球体反射出去的总能量就比 Lambert 多。可以算一下:设 μ = cos θ,一个被平行光照射的球,反射的总能量正比于 D(μ) 在 μ ∈ [−1, 1] 上的积分。Lambert 是 ∫01 μ dμ = 1/2;Wrap 是 ∫−w1 (μ + w)/(1 + w) dμ = (1 + w)/2,多出了 (1 + w) 倍。所以只要把分母再乘一个 (1 + w),总能量就和 Lambert 相同了:
能量守恒版本的效果是:光“铺开”了,但峰值被压低,整体亮度不变。图 5-6 里橙色的曲线是不守恒的 w = 0.5 版本。
代码
Assets/Cookbook/Ch05/WrapLighting.shader(节选:wrap)
half WrapDiffuse(half NdotL, half w) { #if defined(_WRAP_ENERGY_CONSERVING) // 分母 (1+w)^2:被照亮的范围变大了,峰值相应压低,总能量与 Lambert 相同 return saturate((NdotL + w) / ((1 + w) * (1 + w))); #else // 经典写法:w = 0 时退化为 Lambert,w = 1 时等价于未平方的 Half-Lambert return saturate((NdotL + w) / (1 + w)); #endif }
Assets/Cookbook/Ch05/WrapLighting.shader(节选:frag)
half4 frag(Varyings input) : SV_Target { half3 N = normalize(input.normalWS); Light mainLight = GetMainLight(); half3 radiance = mainLight.color * mainLight.distanceAttenuation; half NdotL = dot(N, mainLight.direction); half wrap = WrapDiffuse(NdotL, _Wrap); // 交界线染色:只在“Wrap 有、Lambert 没有”的那一圈加颜色,模拟光在皮下散射后带出的红色 half scatter = saturate(wrap - saturate(NdotL)); half3 diffuse = _BaseColor.rgb * radiance * wrap + _TerminatorTint.rgb * radiance * scatter; half3 ambient = _BaseColor.rgb * SampleSH(N) * _AmbientStrength; return half4(diffuse + ambient, 1); }
void main() { vec3 N = normalize(normalWS); Light mainLight = GetMainLight(); vec3 radiance = mainLight.color * mainLight.distanceAttenuation; float NdotL = dot(N, mainLight.direction); float wrap = WrapDiffuse(NdotL, _Wrap); // 交界线染色:只在“Wrap 有、Lambert 没有”的那一圈加颜色,模拟光在皮下散射后带出的红色 float scatter = saturate(wrap - saturate(NdotL)); vec3 diffuse = _BaseColor.rgb * radiance * wrap + _TerminatorTint.rgb * radiance * scatter; vec3 ambient = _BaseColor.rgb * SampleSH(N) * _AmbientStrength; fragColor = vec4(diffuse + ambient, 1.0); }
能量守恒用 shader_feature_local_fragment 做成材质开关,两种写法编译成两个变体,运行时没有分支开销。交界线染色的思路是:wrap − saturate(NdotL) 恰好只在“Wrap 照亮了、Lambert 没照亮”的那一圈大于 0,用它乘上一个偏红的颜色叠加上去,就能模拟光在皮肤、蜡、玉石内部散射后从交界线附近透出的红晕。


深入Wrap 加染色只是次表面散射(Subsurface Scattering)最粗糙的近似:它不考虑物体厚度,也不区分红绿蓝光在皮肤里的散射距离差异。第 12 章会介绍预积分皮肤 LUT 和薄片透射,它们是 Wrap 思路的“正规化”版本。