第7章:光照与材质
7.1 章节概述
光照是让 3D 场景看起来真实的关键。没有光照,物体只是纯色的几何形状;有了光照,物体才有明暗变化、高光反射,显得立体真实。
本章将深入讲解:
- 光照基础:光源类型、光照模型
- Phong 光照模型:环境光、漫反射、镜面反射
- Blinn-Phong 改进:半程向量
- 材质系统:材质属性定义
- 多光源渲染:处理多个光源
7.2 光照基础概念
7.2.1 现实世界中的光照
光照的物理过程
光源发出光线
│
▼
光线照射到物体表面
│
├──→ 一部分被吸收(变成热能)
│
├──→ 一部分被反射
│ │
│ ├──→ 漫反射(向各个方向均匀散射)
│ │
│ └──→ 镜面反射(按反射定律反射)
│
└──→ 一部分穿透(透明物体)
我们看到的颜色 = 反射进入眼睛的光的颜色
白光照射红色物体:
- 红色成分被反射 → 我们看到红色
- 其他成分被吸收7.2.2 光源类型
常见光源类型
1. 方向光(Directional Light)
──────────────────────────────────
模拟无限远的光源(如太阳)
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
┌────────────────────────┐
│ 场景 │
└────────────────────────┘
特点:
- 所有光线平行
- 只有方向,没有位置
- 不衰减
2. 点光源(Point Light)
──────────────────────────────────
从一点向所有方向发射光
💡
╱│╲│╲
╱ │ ╲│ ╲
╱ │ │ ╲
╱ │ │ ╲
┌────────────────────────┐
│ 场景 │
└────────────────────────┘
特点:
- 有位置
- 光线向四周发散
- 随距离衰减
3. 聚光灯(Spotlight)
──────────────────────────────────
锥形光束
💡
╲
╲
╲ 内锥角
────────╲────────
╲ 外锥角
┌────────────────────────┐
│ 场景 │
└────────────────────────┘
特点:
- 有位置和方向
- 光束有角度限制
- 锥角外无光
4. 区域光(Area Light)
──────────────────────────────────
从一个面发射光(如窗户、荧光灯)
┌─────────────┐
│ 光源面 │
└─────────────┘
╲│╱
●
特点:
- 产生软阴影
- 计算复杂
- 实时渲染中常用近似方法7.3 Phong 光照模型
7.3.1 模型概述
Phong 光照模型是最经典的局部光照模型,由三个分量组成:
Phong 光照模型
最终颜色 = 环境光 + 漫反射 + 镜面反射
I = Ia + Id + Is
┌─────────────────────────────────────┐
│ │
│ 光源 │
│ 💡 │
│ │ │
│ L │ │
│ ╲ │ │
│ ╲ ▼ │
│ ╲ N │
│ ╲ ↑ R │
│ ╲ │ ╱ │
│ ╲ │ ╱ │
│ ╲│ ╱ │
│ ─────────●────╱─────── │
│ 表面 │
│ │
│ L = 入射光方向(指向光源) │
│ N = 表面法线 │
│ R = 反射光方向 │
│ V = 观察方向(指向相机) │
│ │
└─────────────────────────────────────┘7.3.2 环境光(Ambient)
环境光
模拟场景中的间接光照(反弹的光)
↓ ↗ ↙ ↓ ↗
↘ ↗ ↙ ↘ ↗ ↙
↗ ↙ ↓ ↗ ↙
所有方向均匀照射
计算:
Ia = Ka × La
Ka = 材质的环境光反射系数(颜色)
La = 环境光强度
特点:
- 与位置、法线、观察方向无关
- 提供基础亮度,防止阴影区全黑
- 是一种简化的全局光照近似glsl
// GLSL 实现
vec3 ambient = u_ambientColor * u_materialAmbient;7.3.3 漫反射(Diffuse)
漫反射 - Lambert 定律
光线照射到粗糙表面,向各个方向均匀散射
光源
↓ L
↓
N ↑ ↓
│╲ ↓
│ ╲↓
───────┼──●──────── 表面
│
θ = L 和 N 的夹角
Lambert 定律:
反射强度与 cos(θ) 成正比
Id = Kd × Ld × max(0, dot(N, L))
Kd = 材质的漫反射系数
Ld = 光源的漫反射强度
N = 法线向量(归一化)
L = 入射光方向(归一化,指向光源)
为什么是 cos(θ)?
垂直照射: 斜射:
↓ ↓ ↓ ╲ ╲ ╲
───────── ─────────────
全部能量集中 能量分散
在小面积 到大面积
单位面积接收的能量 ∝ cos(θ)glsl
// GLSL 实现
vec3 N = normalize(v_normal);
vec3 L = normalize(u_lightPosition - v_worldPosition);
float diff = max(dot(N, L), 0.0);
vec3 diffuse = u_lightColor * u_materialDiffuse * diff;7.3.4 镜面反射(Specular)
镜面反射 - Phong 高光
光滑表面的镜面反射,产生高光
光源
↓
L ↓
╲ ↓
╲ ↓ R(反射方向)
╲ ↓ ╱
╲↓ ╱
N ↑ ╲ ╱ V(观察方向)
│ ●─────────→ 👁
────────┼────────────── 表面
│
当 V 接近 R 时,看到高光
反射向量计算:
R = reflect(-L, N) = 2 * dot(N, L) * N - L
Phong 镜面反射公式:
Is = Ks × Ls × pow(max(0, dot(R, V)), shininess)
Ks = 材质的镜面反射系数
Ls = 光源的镜面光强度
shininess = 光泽度(越大,高光越集中)
Shininess 效果:
shininess = 8 shininess = 32 shininess = 128
██████ ████ ██
████████ ██████ ████
██████████ ████████ ██████
████████ ██████ ████
██████ ████ ██
大而模糊 适中 小而锐利
(粗糙表面) (塑料) (金属/镜面)glsl
// GLSL 实现
vec3 R = reflect(-L, N);
vec3 V = normalize(u_cameraPosition - v_worldPosition);
float spec = pow(max(dot(R, V), 0.0), u_shininess);
vec3 specular = u_lightColor * u_materialSpecular * spec;7.3.5 完整的 Phong 着色器
glsl
// ============ 顶点着色器 ============
attribute vec3 a_position;
attribute vec3 a_normal;
uniform mat4 u_modelMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat3 u_normalMatrix;
varying vec3 v_worldPosition;
varying vec3 v_normal;
void main() {
vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0);
v_worldPosition = worldPos.xyz;
// 使用法线矩阵变换法线
v_normal = u_normalMatrix * a_normal;
gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;
}
// ============ 片段着色器 ============
precision mediump float;
varying vec3 v_worldPosition;
varying vec3 v_normal;
// 光源
uniform vec3 u_lightPosition;
uniform vec3 u_lightColor;
// 材质
uniform vec3 u_materialAmbient;
uniform vec3 u_materialDiffuse;
uniform vec3 u_materialSpecular;
uniform float u_shininess;
// 环境光
uniform vec3 u_ambientColor;
// 相机
uniform vec3 u_cameraPosition;
void main() {
// 归一化向量
vec3 N = normalize(v_normal);
vec3 L = normalize(u_lightPosition - v_worldPosition);
vec3 V = normalize(u_cameraPosition - v_worldPosition);
vec3 R = reflect(-L, N);
// 环境光
vec3 ambient = u_ambientColor * u_materialAmbient;
// 漫反射
float diff = max(dot(N, L), 0.0);
vec3 diffuse = u_lightColor * u_materialDiffuse * diff;
// 镜面反射
float spec = pow(max(dot(R, V), 0.0), u_shininess);
vec3 specular = u_lightColor * u_materialSpecular * spec;
// 组合
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}7.4 Blinn-Phong 模型
7.4.1 半程向量
Blinn-Phong 是对 Phong 模型的改进,使用半程向量(Half Vector) 代替反射向量:
Blinn-Phong 改进
问题:计算反射向量 R = reflect(-L, N) 比较昂贵
解决:使用半程向量 H
L H V
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲ │ ╱
N ↑ ╲ │ ╱
│ ╲ │ ╱
│ ╲│╱
────────┼─────●──────── 表面
半程向量 H = normalize(L + V)
H 是 L 和 V 的中间方向
新的镜面反射公式:
Is = Ks × Ls × pow(max(0, dot(N, H)), shininess)
用 dot(N, H) 代替 dot(R, V)
为什么有效?
当 V = R 时(完美镜面反射方向),H 正好等于 N
dot(N, H) 最大
优点:
- 计算更简单(不需要 reflect)
- 对于大视角效果更好
- 结果略有不同,但通常更真实glsl
// Blinn-Phong 镜面反射
vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), u_shininess);
vec3 specular = u_lightColor * u_materialSpecular * spec;
// 注意:Blinn-Phong 的 shininess 通常需要 2-4 倍于 Phong
// 才能达到相似的高光大小7.5 光源实现
7.5.1 方向光
javascript
// 方向光结构
const directionalLight = {
direction: [0, -1, 0], // 向下照射
color: [1, 1, 1],
intensity: 1.0
};glsl
// 方向光着色
// L 直接使用光的方向(取反,指向光源)
vec3 L = normalize(-u_lightDirection);
// 方向光不衰减
float diff = max(dot(N, L), 0.0);7.5.2 点光源
javascript
// 点光源结构
const pointLight = {
position: [3, 5, 3],
color: [1, 1, 1],
intensity: 1.0,
// 衰减参数
constant: 1.0,
linear: 0.09,
quadratic: 0.032
};glsl
// 点光源着色(含衰减)
vec3 L = u_lightPosition - v_worldPosition;
float distance = length(L);
L = normalize(L);
// 衰减公式
// attenuation = 1 / (constant + linear * d + quadratic * d²)
float attenuation = 1.0 / (
u_constant +
u_linear * distance +
u_quadratic * distance * distance
);
// 应用衰减
vec3 diffuse = u_lightColor * u_materialDiffuse * diff * attenuation;
vec3 specular = u_lightColor * u_materialSpecular * spec * attenuation;7.5.3 聚光灯
javascript
// 聚光灯结构
const spotlight = {
position: [0, 5, 0],
direction: [0, -1, 0], // 向下照射
color: [1, 1, 1],
intensity: 1.0,
cutOff: Math.cos(12.5 * Math.PI / 180), // 内锥角
outerCutOff: Math.cos(17.5 * Math.PI / 180), // 外锥角
// 衰减参数
constant: 1.0,
linear: 0.09,
quadratic: 0.032
};glsl
// 聚光灯着色
vec3 L = normalize(u_lightPosition - v_worldPosition);
// 检查是否在锥形内
float theta = dot(L, normalize(-u_spotDirection));
// 平滑边缘
float epsilon = u_cutOff - u_outerCutOff;
float intensity = clamp((theta - u_outerCutOff) / epsilon, 0.0, 1.0);
// 应用聚光效果
vec3 diffuse = u_lightColor * u_materialDiffuse * diff * intensity * attenuation;
vec3 specular = u_lightColor * u_materialSpecular * spec * intensity * attenuation;7.6 材质系统
7.6.1 材质结构
javascript
/**
* 材质类
*/
class Material {
constructor(options = {}) {
// 基础颜色
this.ambient = options.ambient || [0.1, 0.1, 0.1];
this.diffuse = options.diffuse || [0.8, 0.8, 0.8];
this.specular = options.specular || [1.0, 1.0, 1.0];
this.shininess = options.shininess || 32;
// 纹理贴图
this.diffuseMap = options.diffuseMap || null;
this.normalMap = options.normalMap || null;
this.specularMap = options.specularMap || null;
}
bind(gl, program) {
const loc = (name) => gl.getUniformLocation(program, name);
gl.uniform3fv(loc('u_materialAmbient'), this.ambient);
gl.uniform3fv(loc('u_materialDiffuse'), this.diffuse);
gl.uniform3fv(loc('u_materialSpecular'), this.specular);
gl.uniform1f(loc('u_shininess'), this.shininess);
// 绑定纹理...
}
}
// 预定义材质
const Materials = {
gold: new Material({
ambient: [0.24725, 0.1995, 0.0745],
diffuse: [0.75164, 0.60648, 0.22648],
specular: [0.628281, 0.555802, 0.366065],
shininess: 51.2
}),
silver: new Material({
ambient: [0.19225, 0.19225, 0.19225],
diffuse: [0.50754, 0.50754, 0.50754],
specular: [0.508273, 0.508273, 0.508273],
shininess: 51.2
}),
chrome: new Material({
ambient: [0.25, 0.25, 0.25],
diffuse: [0.4, 0.4, 0.4],
specular: [0.774597, 0.774597, 0.774597],
shininess: 76.8
}),
rubber: new Material({
ambient: [0.02, 0.02, 0.02],
diffuse: [0.01, 0.01, 0.01],
specular: [0.4, 0.4, 0.4],
shininess: 10
}),
jade: new Material({
ambient: [0.135, 0.2225, 0.1575],
diffuse: [0.54, 0.89, 0.63],
specular: [0.316228, 0.316228, 0.316228],
shininess: 12.8
})
};7.6.2 使用纹理贴图
glsl
// 使用漫反射贴图的着色器
precision mediump float;
varying vec3 v_worldPosition;
varying vec3 v_normal;
varying vec2 v_texCoord;
uniform sampler2D u_diffuseMap;
uniform sampler2D u_specularMap;
// ... 其他 uniform
void main() {
vec3 N = normalize(v_normal);
vec3 L = normalize(u_lightPosition - v_worldPosition);
vec3 V = normalize(u_cameraPosition - v_worldPosition);
vec3 H = normalize(L + V);
// 从纹理获取材质属性
vec4 diffuseTexture = texture2D(u_diffuseMap, v_texCoord);
float specularTexture = texture2D(u_specularMap, v_texCoord).r;
// 环境光
vec3 ambient = u_ambientColor * diffuseTexture.rgb * 0.1;
// 漫反射
float diff = max(dot(N, L), 0.0);
vec3 diffuse = u_lightColor * diffuseTexture.rgb * diff;
// 镜面反射
float spec = pow(max(dot(N, H), 0.0), u_shininess);
vec3 specular = u_lightColor * vec3(specularTexture) * spec;
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, diffuseTexture.a);
}7.7 多光源渲染
7.7.1 方法概述
多光源处理方法
方法1:单 Pass,在着色器中循环
──────────────────────────────────
优点:简单,一次绘制完成
缺点:光源数量受限于 uniform 数量
for (int i = 0; i < NUM_LIGHTS; i++) {
result += calculateLight(lights[i]);
}
方法2:多 Pass,混合叠加
──────────────────────────────────
优点:支持任意数量光源
缺点:需要多次绘制,性能开销
第1 Pass: 绘制第一个光源贡献
第2 Pass: 混合添加第二个光源
...
方法3:延迟渲染(Deferred Rendering)
──────────────────────────────────
优点:光源数量几乎无限
缺点:实现复杂,内存开销大
G-Buffer 存储几何信息
然后为每个光源做光照计算7.7.2 单 Pass 多光源
glsl
// 定义光源结构(GLSL 不支持 struct 数组作为 uniform,需要展开)
#define MAX_LIGHTS 4
uniform int u_numLights;
uniform vec3 u_lightPositions[MAX_LIGHTS];
uniform vec3 u_lightColors[MAX_LIGHTS];
uniform float u_lightIntensities[MAX_LIGHTS];
void main() {
vec3 N = normalize(v_normal);
vec3 V = normalize(u_cameraPosition - v_worldPosition);
// 环境光
vec3 result = u_ambientColor * u_materialAmbient;
// 累加每个光源的贡献
for (int i = 0; i < MAX_LIGHTS; i++) {
if (i >= u_numLights) break;
vec3 L = normalize(u_lightPositions[i] - v_worldPosition);
vec3 H = normalize(L + V);
// 漫反射
float diff = max(dot(N, L), 0.0);
// 镜面反射
float spec = pow(max(dot(N, H), 0.0), u_shininess);
// 衰减
float distance = length(u_lightPositions[i] - v_worldPosition);
float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);
// 累加
result += u_lightColors[i] * u_lightIntensities[i] * attenuation * (
u_materialDiffuse * diff +
u_materialSpecular * spec
);
}
gl_FragColor = vec4(result, 1.0);
}7.7.3 JavaScript 端设置多光源
javascript
class LightManager {
constructor(gl, program, maxLights = 4) {
this.gl = gl;
this.program = program;
this.maxLights = maxLights;
this.lights = [];
}
addLight(light) {
if (this.lights.length < this.maxLights) {
this.lights.push(light);
}
}
bind() {
const gl = this.gl;
const program = this.program;
// 设置光源数量
gl.uniform1i(
gl.getUniformLocation(program, 'u_numLights'),
this.lights.length
);
// 设置每个光源的属性
const positions = [];
const colors = [];
const intensities = [];
this.lights.forEach((light, i) => {
positions.push(...light.position);
colors.push(...light.color);
intensities.push(light.intensity);
});
// 填充未使用的槽位
while (positions.length < this.maxLights * 3) {
positions.push(0, 0, 0);
colors.push(0, 0, 0);
intensities.push(0);
}
gl.uniform3fv(
gl.getUniformLocation(program, 'u_lightPositions'),
new Float32Array(positions)
);
gl.uniform3fv(
gl.getUniformLocation(program, 'u_lightColors'),
new Float32Array(colors)
);
gl.uniform1fv(
gl.getUniformLocation(program, 'u_lightIntensities'),
new Float32Array(intensities)
);
}
}
// 使用
const lightManager = new LightManager(gl, program);
lightManager.addLight({
position: [3, 5, 3],
color: [1, 0.9, 0.8],
intensity: 1.0
});
lightManager.addLight({
position: [-3, 2, 0],
color: [0.2, 0.2, 1.0],
intensity: 0.5
});
// 渲染时
lightManager.bind();7.8 法线贴图
7.8.1 什么是法线贴图?
法线贴图原理
普通几何 vs 法线贴图
没有法线贴图的墙壁:
┌──────────────────────┐
│ │ 几何是平的
│ │ 光照计算使用平面法线
│ │ 看起来也是平的
└──────────────────────┘
有法线贴图的墙壁:
┌──────────────────────┐
│ ≈≈ ≈≈ ≈≈ ≈≈ │ 几何仍然是平的
│ ≈≈ ≈≈ ≈≈ ≈≈ ≈≈ │ 但法线从贴图读取
│ ≈≈ ≈≈ ≈≈ ≈≈ │ 光照计算使用扰动的法线
└──────────────────────┘ 看起来凹凸不平
法线贴图存储的是什么?
每个像素存储一个法线向量:
- R 通道 → X 分量 (0-255 映射到 -1 到 1)
- G 通道 → Y 分量
- B 通道 → Z 分量
大多数法线贴图呈蓝紫色:
因为默认法线 (0, 0, 1) 映射为 (128, 128, 255)7.8.2 切线空间(Tangent Space)
切线空间
法线贴图存储的是切线空间(Tangent Space)中的法线
N (法线方向)
↑
│
│
──────●──────→ T (切线方向)
╱
╱
↙ B (副切线方向)
TBN 矩阵:将切线空间转换到世界空间
TBN = [T | B | N] (三个列向量)
世界空间法线 = TBN × 切线空间法线glsl
// 计算 TBN 矩阵(简化版,假设 tangent 已经在顶点属性中)
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec3 a_tangent;
attribute vec2 a_texCoord;
varying mat3 v_TBN;
varying vec2 v_texCoord;
varying vec3 v_worldPosition;
void main() {
// 变换到世界空间
vec3 T = normalize(mat3(u_modelMatrix) * a_tangent);
vec3 N = normalize(mat3(u_modelMatrix) * a_normal);
// 重新正交化(格拉姆-施密特)
T = normalize(T - dot(T, N) * N);
// 计算副切线
vec3 B = cross(N, T);
// TBN 矩阵
v_TBN = mat3(T, B, N);
// ... 其他计算
}glsl
// 片段着色器中使用法线贴图
precision mediump float;
varying mat3 v_TBN;
varying vec2 v_texCoord;
varying vec3 v_worldPosition;
uniform sampler2D u_normalMap;
void main() {
// 从法线贴图采样
vec3 normalMap = texture2D(u_normalMap, v_texCoord).rgb;
// 转换到 [-1, 1] 范围
normalMap = normalMap * 2.0 - 1.0;
// 转换到世界空间
vec3 N = normalize(v_TBN * normalMap);
// 使用 N 进行光照计算...
}7.9 完整光照系统
javascript
/**
* 完整的光照系统封装
*/
class LightingSystem {
constructor(gl) {
this.gl = gl;
this.ambientColor = [0.1, 0.1, 0.1];
this.directionalLights = [];
this.pointLights = [];
this.spotlights = [];
}
setAmbient(r, g, b) {
this.ambientColor = [r, g, b];
}
addDirectionalLight(direction, color, intensity = 1.0) {
this.directionalLights.push({
direction: [...direction],
color: [...color],
intensity
});
}
addPointLight(position, color, intensity = 1.0, attenuation = {}) {
this.pointLights.push({
position: [...position],
color: [...color],
intensity,
constant: attenuation.constant || 1.0,
linear: attenuation.linear || 0.09,
quadratic: attenuation.quadratic || 0.032
});
}
addSpotlight(position, direction, color, cutOff, outerCutOff, intensity = 1.0) {
this.spotlights.push({
position: [...position],
direction: [...direction],
color: [...color],
cutOff: Math.cos(cutOff * Math.PI / 180),
outerCutOff: Math.cos(outerCutOff * Math.PI / 180),
intensity
});
}
bind(program) {
const gl = this.gl;
const loc = (name) => gl.getUniformLocation(program, name);
// 环境光
gl.uniform3fv(loc('u_ambientColor'), this.ambientColor);
// 方向光
gl.uniform1i(loc('u_numDirectionalLights'), this.directionalLights.length);
this.directionalLights.forEach((light, i) => {
gl.uniform3fv(loc(`u_dirLights[${i}].direction`), light.direction);
gl.uniform3fv(loc(`u_dirLights[${i}].color`), light.color);
gl.uniform1f(loc(`u_dirLights[${i}].intensity`), light.intensity);
});
// 点光源
gl.uniform1i(loc('u_numPointLights'), this.pointLights.length);
this.pointLights.forEach((light, i) => {
gl.uniform3fv(loc(`u_pointLights[${i}].position`), light.position);
gl.uniform3fv(loc(`u_pointLights[${i}].color`), light.color);
gl.uniform1f(loc(`u_pointLights[${i}].intensity`), light.intensity);
gl.uniform1f(loc(`u_pointLights[${i}].constant`), light.constant);
gl.uniform1f(loc(`u_pointLights[${i}].linear`), light.linear);
gl.uniform1f(loc(`u_pointLights[${i}].quadratic`), light.quadratic);
});
// 聚光灯(类似)...
}
}
// 使用示例
const lighting = new LightingSystem(gl);
lighting.setAmbient(0.1, 0.1, 0.15); // 略带蓝色的环境光
lighting.addDirectionalLight(
[0, -1, -1], // 从上方斜射
[1.0, 0.95, 0.9], // 暖白色
0.8 // 强度
);
lighting.addPointLight(
[3, 2, 0], // 位置
[1.0, 0.5, 0.2], // 橙色
1.0, // 强度
{ linear: 0.09, quadratic: 0.032 }
);
// 渲染时
lighting.bind(program);7.10 本章小结
核心概念
| 概念 | 说明 |
|---|---|
| 环境光 | 模拟间接光照,均匀照亮 |
| 漫反射 | Lambert 定律,与入射角余弦成正比 |
| 镜面反射 | 产生高光,与反射角和视角关系相关 |
| Phong 模型 | Ambient + Diffuse + Specular |
| Blinn-Phong | 使用半程向量优化镜面计算 |
| 衰减 | 光强随距离减弱 |
| 法线贴图 | 扰动表面法线,模拟凹凸细节 |
关键公式
Lambert 漫反射:
Id = Kd × Ld × max(0, dot(N, L))
Phong 镜面反射:
Is = Ks × Ls × pow(max(0, dot(R, V)), shininess)
Blinn-Phong 镜面反射:
Is = Ks × Ls × pow(max(0, dot(N, H)), shininess)
H = normalize(L + V)
衰减:
attenuation = 1 / (constant + linear × d + quadratic × d²)7.11 练习题
基础练习
实现带有漫反射和镜面反射的球体
尝试不同的 shininess 值,观察高光变化
实现点光源,让光源围绕物体旋转
进阶练习
实现多光源场景(2-3 个不同颜色的光源)
加载并使用法线贴图
挑战练习
- 实现完整的 PBR(物理渲染)材质
下一章预告:在第8章中,我们将学习帧缓冲和离屏渲染,实现后处理效果。
文档版本:v1.0
字数统计:约 14,000 字
代码示例:45+ 个
