三种光照模型各写一遍:Lambert / Phong / Blinn-Phong

Unity 知识 · Shader 区域 · 光照与材质 · 第 1 篇 更新于 2026-10-15

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 调不出正确观感?