三种光照模型各写一遍:Lambert / Phong / Blinn-Phong
Lambert、Phong、Blinn-Phong 是实时光照的三块基石:一个管漫反射,两个管高光。它们各只有两三行代码,但把「为什么这么算」写对,比抄对更重要。这篇三个模型各完整写一遍,并给出选择依据。
约定
以下代码为 Built-in 风格单方向光片段(URP 的写法差异见 /notes/unity/engineering-1/)。输入:世界空间法线 N、视线 V(指向摄像机)、光线 L(指向光源)——全部归一化,来源见 /notes/unity/structure-2/。
Lambert:漫反射的全部
float ndl = max(0.0, dot(N, L));
float3 diffuse = albedo.rgb * lightColor * ndl;
物理直觉:同样多的光能量,摊在多大面积上取决于表面与光线的夹角——正对时摊在 1 份面积,斜射时摊在 1/cosθ 份面积,所以单位面积能量乘 cosθ,而 cosθ 就是 dot(N, L)。
严格公式里漫反射是 albedo/π,引擎把 π 折进了光源强度标定里,自己写 shader 时不用管——但要知道引擎光源的强度定义是带这个系数的,自己加灯参数对不上时先想这里。
max(0, ...) 截断背面:负值在物理上是「光从背面来」,实时管线里直接归零。想要柔和过渡(角色不出现死黑分界)用 Half-Lambert(wrap):
float ndl = dot(N, L) * 0.5 + 0.5; // [-1,1] → [0,1]
卡通/烘焙风常用;代价是光看起来「绕到背面」了,不物理但好看。
Phong:经典高光
float3 R = reflect(-L, N); // 反射方向
float rdv = max(0.0, dot(R, V));
float3 specular = lightColor * pow(rdv, shininess) * specStrength;
直觉:把表面想象成 imperfect 镜子,只有「反射方向恰好对准眼睛」的那一小圈高光可见。shininess 越大高光越锐利——但它和材质粗糙度没有直接映射,纯经验参数。
Blinn-Phong:换半程向量,更快也更稳
float3 H = normalize(L + V); // 半程向量
float ndh = max(0.0, dot(N, H));
float3 specular = lightColor * pow(ndh, shininess) * specStrength;
H 是 L 与 V 的角平分方向。N·H = 1 意味着表面恰好是 L 与 V 的「镜面平分面」——和 Phong 的 R·V = 1 是同一件事的两种写法。Blinn-Phong 的优势:
- 省一次
reflect,且dot(N,H)天然不会像dot(R,V)那样在掠射角出现负值毛刺。 - 高光形状更符合微表面直觉:
shininess与粗糙度的关系在下一篇从微表面模型推出。
实时管线里 Blinn-Phong 基本取代了 Phong,写老式高光默认用 Blinn。
完整可跑的三合一片段
fixed4 frag(V2F i) : SV_Target
{
float3 N = normalize(i.normalWS);
float3 L = normalize(_WorldSpaceLightPos0.xyz);
float3 V = normalize(_WorldSpaceCameraPos - i.positionWS);
fixed4 albedo = tex2D(_MainTex, i.uv);
// 漫反射(Lambert)
float3 diffuse = albedo.rgb * _LightColor0.rgb * max(0.0, dot(N, L));
// 高光(Blinn-Phong)
float3 H = normalize(L + V);
float ndh = max(0.0, dot(N, H));
float3 specular = _LightColor0.rgb * pow(ndh, _Shininess) * _SpecStrength.rgb;
// 环境项(占位:真正的环境光来自 SH/烘焙,见 /notes/unity/lighting-4/)
float3 ambient = albedo.rgb * UNITY_LIGHTMODEL_AMBIENT.rgb;
return fixed4(ambient + diffuse + specular, 1.0);
}
什么时候用哪个
| 场景 | 选择 |
|---|---|
| 哑光表面(石头、布) | 只要 Lambert |
| 塑料、上漆、湿表面 | Lambert + Blinn-Phong |
| 风格化角色、避免死黑 | Half-Lambert + Blinn-Phong |
| 想要「参数有物理含义」的材质 | 直接上 PBR,见 /notes/unity/lighting-3/ |
金属不能用 Blinn-Phong 硬凑:金属的漫反射几乎为零、高光带颜色,这三模型都表达不了——那是微表面模型的事,见 /notes/unity/lighting-2/。
自检三问
dot(N, L)为什么恰好是漫反射系数?- Blinn-Phong 比 Phong 省了什么、稳在哪?
- 为什么金属材质用 Blinn-Phong 调不出正确观感?