Skip to content

第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 练习题

基础练习

  1. 实现带有漫反射和镜面反射的球体

  2. 尝试不同的 shininess 值,观察高光变化

  3. 实现点光源,让光源围绕物体旋转

进阶练习

  1. 实现多光源场景(2-3 个不同颜色的光源)

  2. 加载并使用法线贴图

挑战练习

  1. 实现完整的 PBR(物理渲染)材质

下一章预告:在第8章中,我们将学习帧缓冲和离屏渲染,实现后处理效果。


文档版本:v1.0
字数统计:约 14,000 字
代码示例:45+ 个

如有转载或 CV 的请标注本站原文地址