第9章:光照与着色模型
9.1 光照物理
9.1.1 光的本质
在计算机图形学中,我们需要模拟光与物体的交互来创建真实感图像。理解光的物理特性是基础。
光的基本概念
光是电磁波,可见光波长范围:380nm - 780nm
波长与颜色的对应:
┌─────────────────────────────────────────────────────┐
│ 380nm 450nm 490nm 560nm 590nm 620nm 780nm │
│ 紫 蓝 青 绿 黄 橙 红 │
│ ■ ■ ■ ■ ■ ■ ■ │
└─────────────────────────────────────────────────────┘
辐射度量学单位:
| 物理量 | 单位 | 说明 |
|--------|------|------|
| 辐射通量 Φ | W(瓦特)| 单位时间的能量 |
| 辐射强度 I | W/sr | 每立体角的通量 |
| 辐照度 E | W/m² | 入射到表面的通量密度 |
| 辐射出射度 M | W/m² | 从表面发出的通量密度 |
| 辐射率 L | W/(m²·sr) | 每单位面积、单位立体角的通量 |
立体角(Solid Angle):
╱╲
╱ ╲
╱ ╲
╱ Ω ╲ ← 立体角
╱________╲
r
Ω = A / r²(单位:sr,球面度)
整个球面的立体角 = 4π sr9.1.2 光与表面的交互
光与表面的交互方式
入射光到达表面后:
入射光
╲
╲
╲
──────────●────────── 表面
╱│╲
╱ │ ╲
╱ │ ╲
反射 │ 折射
│
吸收
三种主要现象:
1. 反射(Reflection)
- 镜面反射:入射角 = 反射角
- 漫反射:向各方向均匀散射
2. 折射(Refraction)
- 光进入不同介质时改变方向
- 斯涅尔定律:n₁sin(θ₁) = n₂sin(θ₂)
3. 吸收(Absorption)
- 部分光能被材质吸收转化为热
- 决定物体的颜色
能量守恒:
反射光 + 折射光 + 吸收能量 = 入射光9.1.3 反射类型
反射的分类
1. 理想镜面反射(Specular)
入射 法线 反射
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
────────┴────────
反射方向:R = 2(N·L)N - L
2. 理想漫反射(Diffuse / Lambertian)
出射光
╱ │ ╲
╱ │ ╲
╱ │ ╲
──────────────────────
入射光
向所有方向均匀反射
3. 光泽反射(Glossy)
╱╲
╱ ╲
╱ ╲
─────────────────
介于镜面和漫反射之间
有一定的模糊
实际材质通常是多种反射的组合:
- 塑料:漫反射 + 镜面高光
- 金属:强镜面反射 + 颜色化高光
- 皮肤:次表面散射 + 表面反射9.2 局部光照模型
9.2.1 局部与全局光照
局部光照 vs 全局光照
局部光照(Local Illumination):
- 只考虑光源直接照射
- 不考虑物体之间的光反射
- 计算简单、速度快
- 例如:Phong、Blinn-Phong
全局光照(Global Illumination):
- 考虑所有光的传播(直接 + 间接)
- 物体之间会互相照亮
- 计算复杂、更真实
- 例如:光线追踪、光子映射、路径追踪
场景对比:
局部光照: 全局光照:
┌────────────────┐ ┌────────────────┐
│ ● │ │ ● │
│ 光源 │ │ 光源 │
│ ╲ │ │ ╲ ╱ │
│ ╲ │ │ ●────────┤← 间接光
│ ──────── │ │ ──────── │
│ 阴影? │ │ 软阴影 │
│ 不处理 │ │ 颜色溢出 │
└────────────────┘ └────────────────┘9.2.2 光源类型
常见光源类型
1. 平行光(Directional Light)
- 无限远,光线平行
- 模拟太阳
- 参数:方向、颜色
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
2. 点光源(Point Light)
- 从一点向四周发射
- 有衰减
- 参数:位置、颜色、衰减
╱│╲
╱ │ ╲
╱ ● ╲
╱ │ ╲
╱ │ ╲
3. 聚光灯(Spot Light)
- 锥形光照
- 有衰减和角度
- 参数:位置、方向、角度、颜色
●
╱ ╲
╱ ╲
╱_____╲
4. 面光源(Area Light)
- 有面积的光源
- 产生软阴影
- 计算复杂
┌──────┐
│ │
└──────┘9.3 Phong 模型
9.3.1 Phong 光照模型
Phong 模型
Phong 模型将光照分为三个分量:
总光照 = 环境光 + 漫反射 + 镜面反射
I = Iₐ + Id + Is
1. 环境光(Ambient)
Iₐ = kₐ × Lₐ
- kₐ:材质的环境光系数
- Lₐ:环境光颜色
- 模拟间接光照的简化
2. 漫反射(Diffuse)
Id = kd × Ld × max(0, N·L)
N
│
│ θ
─────┼─────
╱
L
- kd:材质的漫反射系数
- Ld:光源颜色
- N:表面法向量
- L:光源方向
- cos(θ) = N·L
3. 镜面反射(Specular)
Is = ks × Ls × max(0, R·V)ⁿ
N R
│ ╱
│ ╱
─────┼──╱──────
╱ │
L │
V(视线)
- ks:材质的镜面反射系数
- Ls:光源颜色
- R:反射方向
- V:视线方向
- n:光泽度(越大高光越锐利)
反射向量计算:
R = 2(N·L)N - L9.3.2 Phong 模型实现
javascript
/**
* Phong 光照计算
*/
class PhongLighting {
/**
* 计算 Phong 光照
* @param normal 法向量(归一化)
* @param lightDir 指向光源的方向(归一化)
* @param viewDir 指向相机的方向(归一化)
* @param material 材质参数
* @param lightColor 光源颜色
*/
static calculate(normal, lightDir, viewDir, material, lightColor) {
// 环境光
const ambient = {
r: material.ambient.r * lightColor.r,
g: material.ambient.g * lightColor.g,
b: material.ambient.b * lightColor.b
};
// 漫反射
const NdotL = Math.max(0, this.dot(normal, lightDir));
const diffuse = {
r: material.diffuse.r * lightColor.r * NdotL,
g: material.diffuse.g * lightColor.g * NdotL,
b: material.diffuse.b * lightColor.b * NdotL
};
// 计算反射向量
const reflect = this.reflect(lightDir, normal);
// 镜面反射
const RdotV = Math.max(0, this.dot(reflect, viewDir));
const specFactor = Math.pow(RdotV, material.shininess);
const specular = {
r: material.specular.r * lightColor.r * specFactor,
g: material.specular.g * lightColor.g * specFactor,
b: material.specular.b * lightColor.b * specFactor
};
// 合并
return {
r: Math.min(1, ambient.r + diffuse.r + specular.r),
g: Math.min(1, ambient.g + diffuse.g + specular.g),
b: Math.min(1, ambient.b + diffuse.b + specular.b)
};
}
/**
* 计算反射向量
* R = 2(N·L)N - L
*/
static reflect(lightDir, normal) {
const NdotL = this.dot(normal, lightDir);
return {
x: 2 * NdotL * normal.x - lightDir.x,
y: 2 * NdotL * normal.y - lightDir.y,
z: 2 * NdotL * normal.z - lightDir.z
};
}
static dot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
}
// 材质示例
const plasticMaterial = {
ambient: { r: 0.1, g: 0.1, b: 0.1 },
diffuse: { r: 0.6, g: 0.0, b: 0.0 },
specular: { r: 1.0, g: 1.0, b: 1.0 },
shininess: 32
};
// GLSL 版本
const phongVertexShader = `
attribute vec3 aPosition;
attribute vec3 aNormal;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
vPosition = worldPosition.xyz;
vNormal = normalize(uNormalMatrix * aNormal);
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
}
`;
const phongFragmentShader = `
precision mediump float;
uniform vec3 uLightPosition;
uniform vec3 uViewPosition;
uniform vec3 uLightColor;
uniform vec3 uAmbient;
uniform vec3 uDiffuse;
uniform vec3 uSpecular;
uniform float uShininess;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(uLightPosition - vPosition);
vec3 viewDir = normalize(uViewPosition - vPosition);
// 环境光
vec3 ambient = uAmbient * uLightColor;
// 漫反射
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = uDiffuse * uLightColor * diff;
// 镜面反射
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), uShininess);
vec3 specular = uSpecular * uLightColor * spec;
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}
`;9.4 Blinn-Phong 模型
9.4.1 半角向量
Blinn-Phong 改进
Phong 模型的问题:
- 当视角与反射方向夹角 > 90° 时,高光突然消失
- 计算反射向量开销较大
Blinn-Phong 解决方案:使用半角向量(Half Vector)
N
│
H ────┼────
╱│╲
╱ │ ╲
L │ V
半角向量:
H = normalize(L + V)
镜面反射改为:
Is = ks × Ls × max(0, N·H)ⁿ
优点:
1. 计算更简单(不需要反射向量)
2. 高光更平滑
3. 在大角度时表现更好
对比:
Phong 高光: Blinn-Phong 高光:
╭───╮ ╭───────╮
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
│ │ │ │
Phong 高光更锐利 Blinn-Phong 更柔和9.4.2 Blinn-Phong 实现
javascript
/**
* Blinn-Phong 光照计算
*/
class BlinnPhongLighting {
static calculate(normal, lightDir, viewDir, material, lightColor) {
// 环境光
const ambient = {
r: material.ambient.r * lightColor.r,
g: material.ambient.g * lightColor.g,
b: material.ambient.b * lightColor.b
};
// 漫反射
const NdotL = Math.max(0, this.dot(normal, lightDir));
const diffuse = {
r: material.diffuse.r * lightColor.r * NdotL,
g: material.diffuse.g * lightColor.g * NdotL,
b: material.diffuse.b * lightColor.b * NdotL
};
// 计算半角向量
const halfVector = this.normalize({
x: lightDir.x + viewDir.x,
y: lightDir.y + viewDir.y,
z: lightDir.z + viewDir.z
});
// 镜面反射(使用半角向量)
const NdotH = Math.max(0, this.dot(normal, halfVector));
const specFactor = Math.pow(NdotH, material.shininess);
const specular = {
r: material.specular.r * lightColor.r * specFactor,
g: material.specular.g * lightColor.g * specFactor,
b: material.specular.b * lightColor.b * specFactor
};
return {
r: Math.min(1, ambient.r + diffuse.r + specular.r),
g: Math.min(1, ambient.g + diffuse.g + specular.g),
b: Math.min(1, ambient.b + diffuse.b + specular.b)
};
}
static dot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
static normalize(v) {
const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
return { x: v.x / len, y: v.y / len, z: v.z / len };
}
}
// GLSL 片段着色器
const blinnPhongFragmentShader = `
precision mediump float;
uniform vec3 uLightPosition;
uniform vec3 uViewPosition;
uniform vec3 uLightColor;
uniform vec3 uAmbient;
uniform vec3 uDiffuse;
uniform vec3 uSpecular;
uniform float uShininess;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(uLightPosition - vPosition);
vec3 viewDir = normalize(uViewPosition - vPosition);
// 半角向量
vec3 halfDir = normalize(lightDir + viewDir);
// 环境光
vec3 ambient = uAmbient * uLightColor;
// 漫反射
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = uDiffuse * uLightColor * diff;
// 镜面反射(Blinn-Phong)
float spec = pow(max(dot(normal, halfDir), 0.0), uShininess);
vec3 specular = uSpecular * uLightColor * spec;
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}
`;9.5 BRDF
9.5.1 BRDF 的概念
BRDF(双向反射分布函数)
BRDF 描述了光在物体表面的反射特性。
定义:
fr(ωi, ωo) = dLo(ωo) / (Li(ωi) × cos(θi) × dωi)
其中:
- ωi:入射方向
- ωo:出射方向
- Lo:出射辐射率
- Li:入射辐射率
- θi:入射角
图示:
Lo(ωo)
╱
╱
─────╱──────
╱
╱ Li(ωi)
BRDF 回答:从 ωi 方向入射的光,有多少从 ωo 方向反射出去?
BRDF 的性质:
1. 非负性
fr(ωi, ωo) ≥ 0
2. 亥姆霍兹互易性
fr(ωi, ωo) = fr(ωo, ωi)
3. 能量守恒
∫ fr(ωi, ωo) × cos(θo) dωo ≤ 19.5.2 常见 BRDF 模型
Lambertian BRDF(理想漫反射)
fr = kd / π
- 各向同性
- 最简单的 BRDF
- 能量守恒:∫(kd/π)cos(θ)dω = kd
Phong BRDF
fr = kd/π + ks × (n+2)/(2π) × cos^n(θr)
- kd:漫反射系数
- ks:镜面反射系数
- n:光泽度
- θr:反射角
Cook-Torrance BRDF
用于物理渲染(PBR):
fr = kd × fLambert + ks × DGF / (4×cosθi×cosθo)
其中:
- D:法线分布函数(NDF)
- G:几何遮蔽函数
- F:菲涅尔方程9.5.3 PBR 材质
javascript
/**
* PBR(Cook-Torrance)BRDF
*/
class PBRBRDF {
/**
* 法线分布函数(GGX/Trowbridge-Reitz)
*/
static D_GGX(NdotH, roughness) {
const a = roughness * roughness;
const a2 = a * a;
const NdotH2 = NdotH * NdotH;
const nom = a2;
const denom = NdotH2 * (a2 - 1) + 1;
return nom / (Math.PI * denom * denom);
}
/**
* 几何遮蔽函数(Schlick-GGX)
*/
static G_SchlickGGX(NdotV, roughness) {
const r = roughness + 1;
const k = (r * r) / 8;
const nom = NdotV;
const denom = NdotV * (1 - k) + k;
return nom / denom;
}
/**
* Smith 几何函数
*/
static G_Smith(NdotV, NdotL, roughness) {
const ggx1 = this.G_SchlickGGX(NdotV, roughness);
const ggx2 = this.G_SchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
/**
* 菲涅尔方程(Schlick 近似)
*/
static F_Schlick(cosTheta, F0) {
const pow5 = Math.pow(1 - cosTheta, 5);
return {
r: F0.r + (1 - F0.r) * pow5,
g: F0.g + (1 - F0.g) * pow5,
b: F0.b + (1 - F0.b) * pow5
};
}
/**
* 计算 PBR BRDF
*/
static calculate(normal, lightDir, viewDir, material) {
const halfDir = this.normalize({
x: lightDir.x + viewDir.x,
y: lightDir.y + viewDir.y,
z: lightDir.z + viewDir.z
});
const NdotL = Math.max(0.001, this.dot(normal, lightDir));
const NdotV = Math.max(0.001, this.dot(normal, viewDir));
const NdotH = Math.max(0.001, this.dot(normal, halfDir));
const HdotV = Math.max(0.001, this.dot(halfDir, viewDir));
// F0:非金属约 0.04,金属使用 albedo
const F0 = material.metallic > 0.5 ? material.albedo :
{ r: 0.04, g: 0.04, b: 0.04 };
// Cook-Torrance 镜面 BRDF
const D = this.D_GGX(NdotH, material.roughness);
const G = this.G_Smith(NdotV, NdotL, material.roughness);
const F = this.F_Schlick(HdotV, F0);
const denom = 4 * NdotV * NdotL;
const specular = {
r: (D * G * F.r) / denom,
g: (D * G * F.g) / denom,
b: (D * G * F.b) / denom
};
// 漫反射(金属无漫反射)
const kS = F;
const kD = {
r: (1 - kS.r) * (1 - material.metallic),
g: (1 - kS.g) * (1 - material.metallic),
b: (1 - kS.b) * (1 - material.metallic)
};
const diffuse = {
r: kD.r * material.albedo.r / Math.PI,
g: kD.g * material.albedo.g / Math.PI,
b: kD.b * material.albedo.b / Math.PI
};
return {
r: (diffuse.r + specular.r) * NdotL,
g: (diffuse.g + specular.g) * NdotL,
b: (diffuse.b + specular.b) * NdotL
};
}
static dot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
static normalize(v) {
const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
return { x: v.x / len, y: v.y / len, z: v.z / len };
}
}9.6 全局光照
9.6.1 渲染方程
渲染方程(Rendering Equation)
Kajiya 1986 年提出的光照传输统一公式:
Lo(p, ωo) = Le(p, ωo) + ∫ fr(p, ωi, ωo) × Li(p, ωi) × cos(θi) dωi
各项含义:
- Lo:出射辐射率
- Le:自发光
- fr:BRDF
- Li:入射辐射率
- cos(θi):朗伯余弦项
- 积分:对半球所有入射方向
图示:
Lo(ωo)
╱
╱
∫ fr × Li dωi ← 对半球积分
╱│╲
╱ │ ╲
────●──────────────
p
这是递归方程:Li 本身可能是其他表面的 Lo
全局光照算法的目标就是求解这个方程9.6.2 常见全局光照技术
全局光照技术
1. 光线追踪(Ray Tracing)
- 从相机发射光线
- 递归跟踪反射/折射
- 适合镜面材质
2. 路径追踪(Path Tracing)
- 随机采样光路
- 无偏估计渲染方程
- 收敛慢但结果准确
3. 光子映射(Photon Mapping)
- 两步法:发射光子 + 收集
- 适合焦散效果
4. 辐射度(Radiosity)
- 预计算漫反射光传输
- 适合建筑可视化
5. 实时技术:
- 屏幕空间反射(SSR)
- 屏幕空间环境光遮蔽(SSAO)
- 光照探针(Light Probes)
- 反射探针(Reflection Probes)9.6.3 简单光线追踪
javascript
/**
* 简单光线追踪器
*/
class SimpleRayTracer {
constructor(width, height) {
this.width = width;
this.height = height;
this.scene = {
spheres: [],
lights: [],
camera: null
};
this.maxDepth = 5;
}
/**
* 追踪光线
*/
trace(ray, depth = 0) {
if (depth > this.maxDepth) {
return { r: 0, g: 0, b: 0 };
}
// 找最近的交点
let closest = null;
let minDist = Infinity;
for (const sphere of this.scene.spheres) {
const t = this.intersectSphere(ray, sphere);
if (t > 0 && t < minDist) {
minDist = t;
closest = sphere;
}
}
if (!closest) {
return this.backgroundColor();
}
// 计算交点
const hitPoint = {
x: ray.origin.x + ray.direction.x * minDist,
y: ray.origin.y + ray.direction.y * minDist,
z: ray.origin.z + ray.direction.z * minDist
};
// 计算法向量
const normal = this.normalize({
x: hitPoint.x - closest.center.x,
y: hitPoint.y - closest.center.y,
z: hitPoint.z - closest.center.z
});
// 局部光照
let color = this.computeLocalLighting(hitPoint, normal, closest.material);
// 反射
if (closest.material.reflectivity > 0) {
const reflectDir = this.reflect(ray.direction, normal);
const reflectRay = {
origin: this.offsetPoint(hitPoint, normal),
direction: reflectDir
};
const reflectColor = this.trace(reflectRay, depth + 1);
color = {
r: color.r * (1 - closest.material.reflectivity) +
reflectColor.r * closest.material.reflectivity,
g: color.g * (1 - closest.material.reflectivity) +
reflectColor.g * closest.material.reflectivity,
b: color.b * (1 - closest.material.reflectivity) +
reflectColor.b * closest.material.reflectivity
};
}
return color;
}
/**
* 计算局部光照
*/
computeLocalLighting(point, normal, material) {
let color = { r: 0, g: 0, b: 0 };
for (const light of this.scene.lights) {
const lightDir = this.normalize({
x: light.position.x - point.x,
y: light.position.y - point.y,
z: light.position.z - point.z
});
// 阴影测试
const shadowRay = {
origin: this.offsetPoint(point, normal),
direction: lightDir
};
if (this.isInShadow(shadowRay, light)) {
continue;
}
// Phong 光照
const NdotL = Math.max(0, this.dot(normal, lightDir));
color.r += material.color.r * light.color.r * NdotL;
color.g += material.color.g * light.color.g * NdotL;
color.b += material.color.b * light.color.b * NdotL;
}
// 环境光
color.r += material.color.r * 0.1;
color.g += material.color.g * 0.1;
color.b += material.color.b * 0.1;
return {
r: Math.min(1, color.r),
g: Math.min(1, color.g),
b: Math.min(1, color.b)
};
}
/**
* 渲染图像
*/
render() {
const pixels = new Uint8ClampedArray(this.width * this.height * 4);
const camera = this.scene.camera;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
// 生成主光线
const ray = this.generateRay(x, y, camera);
// 追踪光线
const color = this.trace(ray);
// 写入像素
const i = (y * this.width + x) * 4;
pixels[i] = color.r * 255;
pixels[i + 1] = color.g * 255;
pixels[i + 2] = color.b * 255;
pixels[i + 3] = 255;
}
}
return new ImageData(pixels, this.width, this.height);
}
// 辅助方法...
intersectSphere(ray, sphere) {
const oc = {
x: ray.origin.x - sphere.center.x,
y: ray.origin.y - sphere.center.y,
z: ray.origin.z - sphere.center.z
};
const a = this.dot(ray.direction, ray.direction);
const b = 2 * this.dot(oc, ray.direction);
const c = this.dot(oc, oc) - sphere.radius * sphere.radius;
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) return -1;
return (-b - Math.sqrt(discriminant)) / (2 * a);
}
reflect(dir, normal) {
const d = 2 * this.dot(dir, normal);
return {
x: dir.x - d * normal.x,
y: dir.y - d * normal.y,
z: dir.z - d * normal.z
};
}
dot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
normalize(v) {
const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
return { x: v.x / len, y: v.y / len, z: v.z / len };
}
}9.7 本章小结
光照模型对比
| 模型 | 复杂度 | 真实感 | 应用 |
|---|---|---|---|
| Phong | 低 | 中 | 简单场景 |
| Blinn-Phong | 低 | 中 | 实时渲染 |
| Cook-Torrance | 中 | 高 | PBR |
| 光线追踪 | 高 | 高 | 离线渲染 |
| 路径追踪 | 很高 | 最高 | 电影级 |
关键公式
Phong 模型:
I = Ia + Id + Is
= ka×La + kd×Ld×(N·L) + ks×Ls×(R·V)^n
Blinn-Phong:
Is = ks×Ls×(N·H)^n
H = normalize(L + V)
渲染方程:
Lo = Le + ∫ fr×Li×cosθ dω关键要点
- 局部光照只考虑直接光照,计算简单
- Blinn-Phong 是 Phong 的改进,使用半角向量
- BRDF 描述材质的反射特性
- PBR 使用物理正确的 BRDF(GGX + Smith + Fresnel)
- 全局光照考虑所有光的传播,更真实但计算量大
下一章预告:在第10章中,我们将学习纹理映射,包括纹理过滤、Mipmap 和各种贴图技术。
文档版本:v1.0
字数统计:约 11,000 字
