12. 光照模型实战:从 Lambert 到 Phong
光照是 3D 图形学的灵魂。没有光照,所有的物体看起来都是平面的色块。 本章我们将从零开始,一步步实现最经典、最常用的 Phong 光照模型。
1. 拆解光照:三个核心成分
在经典图形学中,我们把光照简化为三个部分的叠加:
- 环境光 (Ambient):基础底色,模拟光线的散射,保证暗部不全是黑的。
- 漫反射 (Diffuse):模拟粗糙表面的反光,光线越垂直于表面越亮。
- 镜面光 (Specular):模拟光滑表面的高光,取决于观察角度。
$$ \text{最终颜色} = \text{环境光} + \text{漫反射} + \text{镜面光} $$
2. 准备工作:所需变量
要计算光照,我们需要以下几个关键向量(都需要在 Fragment Shader 中):
vNormal(N):法线,决定面朝哪里。lightDir(L):光线方向,光从哪里来。viewDir(V):视线方向,相机在哪里(用于计算高光)。
⚠️ 重要提示:所有参与点积 (
dot) 计算的向量,必须先归一化 (normalize)!
3. 第一步:环境光 (Ambient)
这是最简单的。它就是一个常数颜色。
void main() {
vec3 lightColor = vec3(1.0, 1.0, 1.0); // 白光
float ambientStrength = 0.1; // 环境光强度 (很弱)
vec3 ambient = ambientStrength * lightColor;
// ...
gl_FragColor = vec4(ambient * objectColor, 1.0);
}
4. 第二步:漫反射 (Diffuse / Lambert)
核心原理:
- 如果光线垂直射向表面(夹角 0 度),最亮。
- 如果光线擦着表面过去(夹角 90 度),没光。
- 如果光线从背面射来(夹角 > 90 度),全黑。
数学工具是 点积 (Dot Product):dot(N, L) 等于 cos(夹角)。
// 在 Fragment Shader 中
void main() {
// 1. 归一化法线和光线方向
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(uLightPos - vFragPos); // 光源位置 - 片段位置
// 2. 计算点积
// max(..., 0.0) 是为了剔除背面的光(点积为负数的情况)
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// ...
}
5. 第三步:镜面光 (Specular / Phong)
核心原理:
- 像镜子一样,光线射进来,会沿着反射方向射出去。
- 如果你的眼睛(相机)刚好在反射光的方向上,你就会看到刺眼的高光。
你需要计算两个向量的夹角:反射光方向 (R) 和 视线方向 (V)。
void main() {
float specularStrength = 0.5; // 高光强度
// 1. 计算视线方向 (V)
vec3 viewDir = normalize(uCameraPos - vFragPos);
// 2. 计算反射方向 (R)
// reflect 函数要求入射光是指向表面的,所以用 -lightDir
vec3 reflectDir = reflect(-lightDir, norm);
// 3. 计算高光系数
// dot(viewDir, reflectDir) 计算视线和反射光的夹角
// pow(..., 32.0) 是"反光度" (Shininess)。值越大,光斑越小越锐利。
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = specularStrength * spec * lightColor;
// ...
}
6. 最终组合 (The Complete Code)
把它们加在一起,就是著名的 Phong 光照模型。
// --- Vertex Shader ---
varying vec3 vNormal;
varying vec3 vFragPos; // 世界空间中的位置
void main() {
// 将法线变换到世界空间 (注意:如果有非均匀缩放,需用 normalMatrix)
vNormal = mat3(modelMatrix) * normal;
// 计算世界空间坐标,传给 Fragment Shader 用于计算光线方向
vFragPos = vec3(modelMatrix * vec4(position, 1.0));
gl_Position = projectionMatrix * viewMatrix * vec4(vFragPos, 1.0);
}
// --- Fragment Shader ---
uniform vec3 uLightPos; // 光源位置
uniform vec3 uCameraPos; // 相机位置 (camera.position)
uniform vec3 uObjectColor;// 物体颜色
varying vec3 vNormal;
varying vec3 vFragPos;
void main() {
// 0. 基础准备
vec3 lightColor = vec3(1.0);
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(uLightPos - vFragPos);
// 1. 环境光 (Ambient)
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// 2. 漫反射 (Diffuse)
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// 3. 镜面光 (Specular)
float specularStrength = 0.5;
vec3 viewDir = normalize(uCameraPos - vFragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); // 32.0 是反光度
vec3 specular = specularStrength * spec * lightColor;
// 4. 最终合并
vec3 result = (ambient + diffuse + specular) * uObjectColor;
gl_FragColor = vec4(result, 1.0);
}
7. Blinn-Phong:一个更快的改进
在早期的硬件上,计算 reflect 函数比较慢。Jim Blinn 提出了一种改进方法:Blinn-Phong。
它不计算反射光,而是计算 "半程向量" (Halfway Vector)。
- 半程向量 H = 光线方向 L + 视线方向 V 的角平分线。
- 原理:如果 H 和法线 N 很接近,说明视线和反射光也很接近。
// Blinn-Phong 的镜面光计算
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0);
优势:
- 计算量略小(只有加法和归一化)。
- 高光看起来更自然、更柔和。
- Three.js 的
MeshPhongMaterial默认使用的其实是 Blinn-Phong!
📝 编程作业
难度:⭐⭐⭐ 困难 | 预计时间:90 分钟
任务要求
实现完整的 Phong 光照模型,这是图形学的核心技能。
具体要求
- 环境光:实现基础的环境光
- Lambert 漫反射:实现基础的漫反射光照
- Phong 镜面高光:添加镜面反射高光
- Blinn-Phong 改进:可选,改进为 Blinn-Phong 模型
代码框架
// 顶点着色器
precision mediump float;
precision mediump int;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform mat3 normalMatrix;
attribute vec3 position;
attribute vec3 normal;
varying vec3 vNormal;
varying vec3 vFragPos;
void main() {
// 计算世界空间位置和法线
vFragPos = vec3(modelMatrix * vec4(position, 1.0));
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * viewMatrix * vec4(vFragPos, 1.0);
}
// 片段着色器
precision mediump float;
uniform vec3 uLightPos;
uniform vec3 uCameraPos;
uniform vec3 uObjectColor;
varying vec3 vNormal;
varying vec3 vFragPos;
void main() {
vec3 lightColor = vec3(1.0);
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(uLightPos - vFragPos);
// TODO 1: 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// TODO 2: 漫反射
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// TODO 3: 镜面光
float specularStrength = 0.5;
vec3 viewDir = normalize(uCameraPos - vFragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = specularStrength * spec * lightColor;
// 合并所有光照
vec3 result = (ambient + diffuse + specular) * uObjectColor;
gl_FragColor = vec4(result, 1.0);
}
JavaScript 部分
const geometry = new THREE.SphereGeometry(1, 32, 32);
const uniforms = {
uLightPos: { value: new THREE.Vector3(2, 2, 2) },
uCameraPos: { value: camera.position },
uObjectColor: { value: new THREE.Color(1.0, 0.5, 0.31) }
};
// 在动画循环中更新相机位置
uniforms.uCameraPos.value.copy(camera.position);
检查清单
- 环境光正确显示(暗部不是全黑)
- 漫反射有明显的明暗变化
- 镜面高光清晰可见
- 所有向量都正确归一化
- 光照效果随视角变化
扩展挑战
- 🌟 实现多光源支持(2-3 个光源)
- 🌟 添加光源颜色控制
- 🌟 实现可交互的光源位置(鼠标控制)
- 🌟 改进为 Blinn-Phong 模型
参考资源
- 完整作业清单:编程作业清单.md