URP 渲染实战 / 第 5 章 经典光照模型 / 案例 5.9
案例 5.9经典光照函数库与实验台进阶
目标:把本章的模型整理成一个可复用的 HLSL 函数库,统一接口和约定;再做一个用关键字自由组合漫反射与高光的“实验台” Shader,方便对比和选型。
函数库
把散落在各个 Shader 里的公式收拢到一个 include 文件里,需要先统一约定,否则组合时很容易出错。这里的约定和 URP 保持一致:所有函数返回值已经乘过 N·L(或它的替代项),并且不除以 π;高光函数统一使用归一化版本。
Assets/Cookbook/Ch05/ClassicLighting.hlsl
// 案例 5.9 经典光照模型函数库。 // 约定(与 URP 一致):所有函数的返回值都已经乘过 N·L(或它的替代品), // 而且没有除以 π——URP 的漫反射写作 albedo * lightColor * N·L,高光的归一化系数因此也整体乘了 π。 #ifndef COOKBOOK_CH05_CLASSIC_LIGHTING_INCLUDED #define COOKBOOK_CH05_CLASSIC_LIGHTING_INCLUDED float DiffuseLambert(float3 N, float3 L) { return saturate(dot(N, L)); } float DiffuseHalfLambert(float3 N, float3 L, float power) { return pow(dot(N, L) * 0.5 + 0.5, power); } // 能量守恒的 Wrap:w = 0 时等于 Lambert float DiffuseWrap(float3 N, float3 L, float w) { return saturate((dot(N, L) + w) / ((1.0 + w) * (1.0 + w))); } // Oren-Nayar 定性模型(无三角函数版本),sigma 为微表面坡度的标准差(弧度) float DiffuseOrenNayar(float3 N, float3 L, float3 V, float sigma) { float NdotL = saturate(dot(N, L)); float NdotV = saturate(dot(N, V)); float s2 = sigma * sigma; float A = 1.0 - 0.5 * s2 / (s2 + 0.33); float B = 0.45 * s2 / (s2 + 0.09); float s = dot(L, V) - NdotL * NdotV; return NdotL * (A + B * max(0.0, s) / max(max(NdotL, NdotV), 1e-4)); } // 归一化 Phong:(n+2)/2 · (R·V)^n · N·L float SpecularPhong(float3 N, float3 L, float3 V, float n) { float RdotV = saturate(dot(reflect(-L, N), V)); return (n + 2.0) * 0.5 * pow(RdotV, n) * saturate(dot(N, L)); } // 归一化 Blinn-Phong:(n+8)/8 · (N·H)^n · N·L float SpecularBlinnPhong(float3 N, float3 L, float3 V, float n) { float3 H = SafeNormalize(L + V); return (n + 8.0) * 0.125 * pow(saturate(dot(N, H)), n) * saturate(dot(N, L)); } // 用 0~1 的“光滑度”驱动高光指数,映射与 URP SimpleLit 相同:smoothness=0 → 2,=1 → 2048 float ShininessFromSmoothness(float smoothness) { return exp2(10.0 * smoothness + 1.0); } #endif
// 案例 5.9 经典光照模型函数库(GLSL 版,对应 Ch05/ClassicLighting.hlsl)。 // 约定(与 URP 一致):所有函数的返回值都已经乘过 N·L(或它的替代品), // 而且没有除以 π——URP 的漫反射写作 albedo * lightColor * N·L,高光的归一化系数因此也整体乘了 π。 #ifndef COOKBOOK_CH05_CLASSIC_LIGHTING_INCLUDED #define COOKBOOK_CH05_CLASSIC_LIGHTING_INCLUDED float DiffuseLambert(vec3 N, vec3 L) { return saturate(dot(N, L)); } float DiffuseHalfLambert(vec3 N, vec3 L, float power) { return pow(dot(N, L) * 0.5 + 0.5, power); } // 能量守恒的 Wrap:w = 0 时等于 Lambert float DiffuseWrap(vec3 N, vec3 L, float w) { return saturate((dot(N, L) + w) / ((1.0 + w) * (1.0 + w))); } // Oren-Nayar 定性模型(无三角函数版本),sigma 为微表面坡度的标准差(弧度) float DiffuseOrenNayar(vec3 N, vec3 L, vec3 V, float sigma) { float NdotL = saturate(dot(N, L)); float NdotV = saturate(dot(N, V)); float s2 = sigma * sigma; float A = 1.0 - 0.5 * s2 / (s2 + 0.33); float B = 0.45 * s2 / (s2 + 0.09); float s = dot(L, V) - NdotL * NdotV; return NdotL * (A + B * max(0.0, s) / max(max(NdotL, NdotV), 1e-4)); } // 归一化 Phong:(n+2)/2 · (R·V)^n · N·L float SpecularPhong(vec3 N, vec3 L, vec3 V, float n) { float RdotV = saturate(dot(reflect(-L, N), V)); return (n + 2.0) * 0.5 * pow(RdotV, n) * saturate(dot(N, L)); } // 归一化 Blinn-Phong:(n+8)/8 · (N·H)^n · N·L float SpecularBlinnPhong(vec3 N, vec3 L, vec3 V, float n) { vec3 H = SafeNormalize(L + V); return (n + 8.0) * 0.125 * pow(saturate(dot(N, H)), n) * saturate(dot(N, L)); } // 用 0~1 的“光滑度”驱动高光指数,映射与 URP SimpleLit 相同:smoothness=0 → 2,=1 → 2048 float ShininessFromSmoothness(float smoothness) { return exp2(10.0 * smoothness + 1.0); } #endif
注意 Half-Lambert 函数本身没有乘 N·L:它的整个意义就是用 0.5·N·L + 0.5 去代替 N·L。这也是为什么约定里要写“或它的替代项”。ShininessFromSmoothness 与 URP Simple Lit 采用同样的映射,这样同一套贴图在两个 Shader 之间切换时手感一致。
实验台 Shader
Assets/Cookbook/Ch05/ClassicLab.shader(节选:frag)
half4 frag(Varyings input) : SV_Target { half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb; float3 N = normalize(input.normalWS); float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS); Light mainLight = GetMainLight(); float3 L = mainLight.direction; half3 radiance = mainLight.color * mainLight.distanceAttenuation; #if defined(_DIFFUSE_HALFLAMBERT) float d = DiffuseHalfLambert(N, L, 2.0); #elif defined(_DIFFUSE_WRAP) float d = DiffuseWrap(N, L, _Wrap); #elif defined(_DIFFUSE_ORENNAYAR) float d = DiffuseOrenNayar(N, L, V, _Roughness); #else float d = DiffuseLambert(N, L); #endif float n = ShininessFromSmoothness(_Smoothness); #if defined(_SPECULAR_PHONG) float s = SpecularPhong(N, L, V, n); #elif defined(_SPECULAR_BLINNPHONG) float s = SpecularBlinnPhong(N, L, V, n); #else float s = 0.0; #endif half3 color = (albedo * d + _SpecColor.rgb * s) * radiance; color += albedo * SampleSH(N) * _AmbientStrength; return half4(color, 1); }
void main() { vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb; vec3 N = normalize(normalWS); vec3 V = GetWorldSpaceNormalizeViewDir(positionWS); Light mainLight = GetMainLight(); vec3 L = mainLight.direction; vec3 radiance = mainLight.color * mainLight.distanceAttenuation; #if defined(_DIFFUSE_HALFLAMBERT) float d = DiffuseHalfLambert(N, L, 2.0); #elif defined(_DIFFUSE_WRAP) float d = DiffuseWrap(N, L, _Wrap); #elif defined(_DIFFUSE_ORENNAYAR) float d = DiffuseOrenNayar(N, L, V, _Roughness); #else float d = DiffuseLambert(N, L); #endif float n = ShininessFromSmoothness(_Smoothness); #if defined(_SPECULAR_PHONG) float s = SpecularPhong(N, L, V, n); #elif defined(_SPECULAR_BLINNPHONG) float s = SpecularBlinnPhong(N, L, V, n); #else float s = 0.0; #endif vec3 color = (albedo * d + _SpecColor.rgb * s) * radiance; color += albedo * SampleSH(N) * _AmbientStrength; fragColor = vec4(color, 1.0); }
两个 KeywordEnum 分别生成 4 个和 3 个关键字,用 shader_feature_local_fragment 声明,共 12 种组合,只有实际被材质用到的组合才会进入构建。
坑:用脚本切换 KeywordEnum在 Unity 2022 中,
new Material(shader) 时会按属性的默认值自动打开对应的关键字(例如默认值 0 会打开 _DIFFUSE_LAMBERT)。之后如果只调用 EnableKeyword("_DIFFUSE_WRAP"),两个关键字会同时打开,最终用哪个变体取决于代码里 #if 的先后顺序,表现为“改了没反应”。正确做法是同时设置属性值、打开目标关键字、关闭同组其它关键字。本书配图脚本里的 SetKeywordEnum() 辅助函数就是做这件事的。
怎么选
| 模型 | 相对开销 | 观感 | 典型用途 |
|---|---|---|---|
| Lambert | 1 次点积 | 标准、交界线清晰 | 一切漫反射的默认选择,PBR 的漫反射项 |
| Half-Lambert | Lambert + 1 次乘加(+ 平方) | 暗部通透、对比低 | 卡通、角色、无 GI 的移动端 |
| Wrap(守恒) | Lambert + 1 次除法 | 交界线柔和,可染色 | 皮肤、蜡、植物叶片的廉价近似 |
| Oren-Nayar | 3 次点积 + 1 次除法 | 粗糙、正面光下平坦 | 岩石、陶土、粗布、月面 |
| Minnaert | 2 次 pow | 边缘可亮可暗 | 天鹅绒、绒毛、风格化 |
| Phong | reflect + pow | 圆形高光 | 历史意义,少用 |
| Blinn-Phong | normalize + pow | 掠射时高光拉长,更真实 | 经典高光的默认选择,Simple Lit |