URP 渲染实战 / 第 5 章 经典光照模型 / 案例 5.4
案例 5.4Phong 高光入门
目标:实现 Bui Tuong Phong 在 1975 年提出的高光模型,理解反射向量、高光指数和它们的几何意义。
原理
镜子会把光沿着“镜面反射方向”反射出去:入射角等于反射角,入射光、法线和反射光在同一平面内。完美镜面只在恰好对准反射方向时才看得到光源,而真实表面不是完美光滑的,反射光会在反射方向周围散开一点。Phong 用一个经验公式描述这个“散开”:视线 V 越接近反射方向 R,高光越亮,亮度按 (R·V)n 衰减:
ks 是高光颜色(强度),n 是高光指数(Shininess)。n 越大,(R·V)n 从 1 掉到 0 的速度越快,高光就越小、越“硬”,表面看起来越光滑。反射向量的公式可以从下图直接读出来:把 L 投影到 N 上得到 (N·L)N,L 关于法线的镜像就是两倍投影减去 L 本身。
代码
Assets/Cookbook/Ch05/Phong.shader(节选:frag)
half4 frag(Varyings input) : SV_Target { float3 N = normalize(input.normalWS); float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS); // 指向相机 Light mainLight = GetMainLight(); float3 L = mainLight.direction; // 指向光源 half3 radiance = mainLight.color * mainLight.distanceAttenuation; float NdotL = dot(N, L); half3 diffuse = _BaseColor.rgb * saturate(NdotL); // reflect(i, n) 要求 i 是“射入”方向,所以传 -L float3 R = reflect(-L, N); float RdotV = saturate(dot(R, V)); // 高光指数常常上百,用 float 计算,避免 half 精度下出现色带 float spec = pow(RdotV, _Shininess); // 背光面不应该出现高光(R 可能恰好朝向相机) spec *= step(0.0, NdotL); half3 color = (diffuse + _SpecColor.rgb * spec) * radiance; color += _BaseColor.rgb * SampleSH(N) * _AmbientStrength; return half4(color, 1); }
void main() { vec3 N = normalize(normalWS); vec3 V = GetWorldSpaceNormalizeViewDir(positionWS); // 指向相机 Light mainLight = GetMainLight(); vec3 L = mainLight.direction; // 指向光源 vec3 radiance = mainLight.color * mainLight.distanceAttenuation; float NdotL = dot(N, L); vec3 diffuse = _BaseColor.rgb * saturate(NdotL); // reflect(i, n) 要求 i 是“射入”方向,所以传 -L vec3 R = reflect(-L, N); float RdotV = saturate(dot(R, V)); // 高光指数常常上百,用 float 计算,避免 half 精度下出现色带 float spec = pow(RdotV, _Shininess); // 背光面不应该出现高光(R 可能恰好朝向相机) spec *= step(0.0, NdotL); vec3 color = (diffuse + _SpecColor.rgb * spec) * radiance; color += _BaseColor.rgb * SampleSH(N) * _AmbientStrength; fragColor = vec4(color, 1.0); }
- reflect 的第一个参数是“入射方向”。HLSL 的
reflect(i, n)计算的是i − 2·dot(i, n)·n,i 指向表面。我们的 L 指向光源,所以要传-L。这是新手最常见的错误之一,写反了高光会出现在完全错误的位置。 - 背光面屏蔽。当 N·L < 0 时,光其实照不到这个点,但 R 依然可以算出来,而且可能恰好朝向相机,于是背光面会冒出一个“幽灵高光”。乘以
step(0, NdotL)把它关掉。 - 精度。高光指数动辄上百,
pow在 half 精度下很容易在高光边缘产生色带甚至溢出。本书在高光计算上统一使用 float,移动端如果要省寄存器,至少保证pow的底数是 float。 - 颜色组合。漫反射乘反照率,高光乘高光颜色,两者相加后再整体乘以入射光。高光颜色对电介质(塑料、漆、木头)应该是灰白色,因为它们的反射不“染色”。

注意 Phong 模型在 V 与 R 夹角超过 90° 时 R·V 变为负数,被截断成 0,所以高光的“外沿”在 α = 90° 处被一刀切掉。在高光指数很小(n < 4)时,这条切边会在低角度的大平面上显现出来;n 稍大时 cosnα 早已接近 0,看不出来。