本节摘要:Blinn-Phong 在 Lambert 之上补齐两块拼图:高光项让物体有了"材质感",环境光体系让多光源有了组织方式。本节给出三成分的完整实现,对照 Phong 与 Blinn-Phong 的高光差异,并整理一套"看画面调参数"的实战对照表。
Lambert 只关心"光进了表面多少",不管"出来多少奔着你的眼睛"。金属球上一枚晃眼的小白点——高光——恰好是"反射光恰好朝向观察者"的产物。Phong 模型的思路直白:算出反射光方向 R,拿它与"指向观察者的方向 V"做点积,重合程度越高高光越亮,再套一个指数把亮斑收窄。
Blinn-Phong 是同一条思路的省事版:不显式算反射向量,改用半程向量 H——L 与 V 的角平分线。H 离法线越近,说明反射方向与视线越接近,高光越强。两者的高光在视觉上几乎等价,但 Blinn-Phong 少一次向量反射运算,且在法线贴图场景下表现更稳,成了事实标准:
vec3 V = normalize(uViewPos - vWorldPos); // 片元指向相机 vec3 H = normalize(L + V); // 半程向量:光与视线的角平分线 float spec = pow(max(dot(N, H), 0.0), uShininess);
指数 uShininess 直接控制亮斑大小:8 左右是大而软的光斑(粗糙塑料),256 以上是针尖般的锐利反光(抛光金属)。注意 dot(N, H) 同样要 max(..., 0)——光源在物体背后时 H 与 N 的点积为负,pow 一个负数在不同驱动上会给出不同结果,包括 NaN。
把上一节的 Lambert 代码整体保留,插入高光段,就是一份可编译的 Blinn-Phong:
// 片元着色器:环境光 + 漫反射 + Blinn-Phong 高光 #version 330 core in vec3 vNormal; in vec3 vWorldPos; out vec4 FragColor; uniform vec3 uLightDir; uniform vec3 uLightColor; uniform vec3 uBaseColor; uniform vec3 uAmbient; uniform vec3 uViewPos; // 相机世界坐标 uniform float uShininess; // 高光锐度:8 到 256 uniform float uSpecStrength; // 高光强度:0.1 到 1.0 常见 void main() { vec3 N = normalize(vNormal); vec3 V = normalize(uViewPos - vWorldPos); vec3 L = normalize(uLightDir); // 成分之一:环境光兜底 vec3 ambient = uAmbient; // 成分之二:Lambert 漫反射 float ndl = max(dot(N, L), 0.0); vec3 diffuse = uLightColor * ndl; // 成分之三:Blinn-Phong 高光 vec3 H = normalize(L + V); float spec = pow(max(dot(N, H), 0.0), uShininess); vec3 specular = uLightColor * spec * uSpecStrength; vec3 color = uBaseColor * (ambient + diffuse) + specular; FragColor = vec4(color, 1.0); }
注意三成分的混合方式有讲究:环境光与漫反射乘物体本色(光被表面染色后反射),高光不乘本色(镜面反射基本保留光源原色)——这就是金属高光永远是灯的颜色、而不是物体颜色的原因。明白这一点,"塑料感"的一大来源就找到了:把高光也乘了本体色的材质,怎么看都像蜡制水果。

Blinn-Phong 的参数不多,但每个都对应一类典型观感问题。这张表建议存下来,调材质时逐条对照:
| 画面症状 | 病根 | 调整 |
|---|---|---|
| 暗部死黑像剪影 | 环境光太弱 | 抬 uAmbient,或加半球环境光 |
| 整体像塑料水果 | 高光乘了本体色 | 高光项不乘 uBaseColor |
| 亮斑糊成一片 | shininess 太低 | 从 32 起步,金属类拉到 128 以上 |
| 亮斑小到看不见 | shininess 太高而强度太低 | 同时抬 uSpecStrength |
| 明暗交界生硬 | 缺过渡与多灯 | 加第二盏补光灯或换 PBR |
| 高光颜色不对 | 高光乘了贴图色 | 检查乘法次序 |
多光源的组织沿用第 3 章的 uniform 数组方案:每盏灯贡献一份 ambient + diffuse + specular,循环累加。光源循环是片元阶段的大头开销,肉眼可感的做法是按距离裁剪贡献趋零的灯。
⚠️ 常见坑:高光在低多边形大三角形上会出现"整块亮斑"而非光点——高光频率高于顶点密度时,插值追不上。解法是提高网格密度,或把光照挪进片元侧逐像素算(本节的代码已经是逐像素版,这个问题多见于把光照写在顶点着色器的老式管线)。
Blinn-Phong 的三成分之外还有一味廉价佐料值得添上——边缘光(rim light)。原理是菲涅尔效应的定性版:视线与表面越接近平行(掠射角),反射越强,物体轮廓边缘于是泛起一圈亮边。
// 加入现有 main 的任意位置:V 已在正文代码中算好 float rim = 1.0 - max(dot(N, V), 0.0); // 掠射角越接近 1 rim = pow(rim, uRimPower); // 收窄亮边宽度,4 到 8 常见 vec3 color = base + specular + uRimColor * rim;
角色背光时的轮廓分离感、科幻界面的辉光边、卡通渲染的描边替代方案——全都指着这行代码。它与菲涅尔的正式版(6.3 节的 F 项)是同一个物理现象的粗细两版:要氛围用这行,要物理用下一节。
叠加时留个心眼:rim 乘的是独立颜色而不是本体色,且不参与能量守恒的账本——它是彻头彻尾的风格化武器,写实场景里权重宜低,否则轮廓泛光一过,塑料感反而回来了。
经典光照到此封顶,它的经验参数终究是拟合的产物。下一节换上物理约束:PBR。