Skip to main content

第 7 节:光照模型

🎯 学习目标

  • 理解 Lambert 漫反射光照
  • 理解 Phong 镜面高光
  • 实现完整的光照模型
  • 对比原生 WebGL 和 Three.js 的光照系统

📖 理论:光照模型

Lambert 漫反射

光线在粗糙表面均匀反射,亮度取决于光线与法线的夹角:

float NdotL = max(dot(normal, lightDir), 0.0);
vec3 diffuse = lightColor * NdotL;

Phong 镜面高光

光线在光滑表面产生高光,取决于视线与反射光线的夹角:

vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
vec3 specular = lightColor * spec;

Blinn-Phong 改进

使用半角向量代替反射向量,计算更快:

vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), shininess);

完整光照模型

vec3 ambient = ambientColor * ambientStrength;
vec3 diffuse = lightColor * max(dot(normal, lightDir), 0.0);
vec3 specular = lightColor * pow(max(dot(normal, halfDir), 0.0), shininess);
vec3 result = ambient + diffuse + specular;

💻 原生 WebGL 实现

实现 Blinn-Phong 光照模型:

<!DOCTYPE html>
<html>
<head>
<title>光照模型 - 原生 WebGL</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>

<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.3/gl-matrix-min.js"></script>
<script>
const canvas = document.getElementById("glcanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const gl = canvas.getContext("webgl");
if (!gl) {
alert("无法初始化 WebGL");
}

const mat4 = glMatrix.mat4;
const vec3 = glMatrix.vec3;

// Shader 源码
const vertexShaderSource = `
attribute vec4 aVertexPosition;
attribute vec3 aNormal;

uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;

varying vec3 vNormal;
varying vec3 vPosition;

void main() {
vec4 worldPos = uModelMatrix * aVertexPosition;
vPosition = worldPos.xyz;
vNormal = normalize(uNormalMatrix * aNormal);
gl_Position = uProjectionMatrix * uViewMatrix * worldPos;
}
`;

const fragmentShaderSource = `
precision mediump float;

varying vec3 vNormal;
varying vec3 vPosition;

uniform vec3 uLightPosition;
uniform vec3 uLightColor;
uniform vec3 uViewPosition;
uniform vec3 uAmbientColor;
uniform float uShininess;

void main() {
// 环境光
vec3 ambient = uAmbientColor * 0.2;

// 漫反射
vec3 lightDir = normalize(uLightPosition - vPosition);
float diff = max(dot(vNormal, lightDir), 0.0);
vec3 diffuse = uLightColor * diff;

// 镜面高光(Blinn-Phong)
vec3 viewDir = normalize(uViewPosition - vPosition);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(vNormal, halfDir), 0.0), uShininess);
vec3 specular = uLightColor * spec;

vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}
`;

// 编译函数(复用之前的)
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("编译错误:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}

function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("链接错误:", gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}

const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);

// 球体顶点数据(简化版,使用立方体代替)
// 实际项目中应该使用 SphereGeometry 或手动生成球体顶点
const geometry = {
positions: [
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, // 前面
-1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, // 后面
-1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, // 上面
-1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // 下面
1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, // 右面
-1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, // 左面
],
normals: [
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // 前面
0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, // 后面
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // 上面
0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // 下面
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // 右面
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // 左面
],
indices: [
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23,
],
};

// 创建缓冲区
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(geometry.positions), gl.STATIC_DRAW);

const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(geometry.normals), gl.STATIC_DRAW);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(geometry.indices), gl.STATIC_DRAW);

// 配置属性
const positionLoc = gl.getAttribLocation(program, "aVertexPosition");
const normalLoc = gl.getAttribLocation(program, "aNormal");

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.enableVertexAttribArray(normalLoc);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);

// 获取 Uniform 位置
const modelLoc = gl.getUniformLocation(program, "uModelMatrix");
const viewLoc = gl.getUniformLocation(program, "uViewMatrix");
const projLoc = gl.getUniformLocation(program, "uProjectionMatrix");
const normalMatLoc = gl.getUniformLocation(program, "uNormalMatrix");
const lightPosLoc = gl.getUniformLocation(program, "uLightPosition");
const lightColorLoc = gl.getUniformLocation(program, "uLightColor");
const viewPosLoc = gl.getUniformLocation(program, "uViewPosition");
const ambientLoc = gl.getUniformLocation(program, "uAmbientColor");
const shininessLoc = gl.getUniformLocation(program, "uShininess");

// 创建矩阵
const modelMatrix = mat4.create();
const viewMatrix = mat4.create();
const projectionMatrix = mat4.create();
const normalMatrix = mat4.create();

mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 0.1, 100.0);
mat4.lookAt(viewMatrix, [0, 0, 5], [0, 0, 0], [0, 1, 0]);

gl.enable(gl.DEPTH_TEST);

// 渲染循环
let angle = 0;
function animate() {
requestAnimationFrame(animate);

angle += 0.02;
mat4.identity(modelMatrix);
mat4.rotateY(modelMatrix, modelMatrix, angle);

// 计算法线矩阵(模型矩阵的逆矩阵的转置)
mat4.invert(normalMatrix, modelMatrix);
mat4.transpose(normalMatrix, normalMatrix);

// 设置 Uniform
gl.uniformMatrix4fv(modelLoc, false, modelMatrix);
gl.uniformMatrix4fv(viewLoc, false, viewMatrix);
gl.uniformMatrix4fv(projLoc, false, projectionMatrix);
gl.uniformMatrix3fv(normalMatLoc, false, [
normalMatrix[0],
normalMatrix[1],
normalMatrix[2],
normalMatrix[4],
normalMatrix[5],
normalMatrix[6],
normalMatrix[8],
normalMatrix[9],
normalMatrix[10],
]);
gl.uniform3f(lightPosLoc, 2, 2, 2);
gl.uniform3f(lightColorLoc, 1, 1, 1);
gl.uniform3f(viewPosLoc, 0, 0, 5);
gl.uniform3f(ambientLoc, 1, 1, 1);
gl.uniform1f(shininessLoc, 32.0);

gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, geometry.indices.length, gl.UNSIGNED_SHORT, 0);
}

animate();
</script>
</body>
</html>

关键点

  1. 法线矩阵:需要将法线从模型空间变换到世界空间
  2. 光照计算:在片段着色器中计算,获得平滑效果
  3. 向量归一化:所有方向向量必须归一化
  4. Blinn-Phong:使用半角向量提高性能

🎨 Three.js 对比实现

Three.js 提供了多种内置光照材质:

<!DOCTYPE html>
<html>
<head>
<title>光照模型 - Three.js</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>

<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>

<script type="module">
import * as THREE from "three";

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);

const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("canvas"),
});
renderer.setSize(window.innerWidth, window.innerHeight);

// 创建几何体
const geometry = new THREE.SphereGeometry(1, 32, 32);

// 使用 MeshStandardMaterial(PBR 材质)
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.5,
roughness: 0.5,
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(2, 2, 2);
scene.add(directionalLight);

// 渲染循环
function animate() {
requestAnimationFrame(animate);

mesh.rotation.y += 0.02;

renderer.render(scene, camera);
}

animate();
</script>
</body>
</html>

Three.js 的光照材质

  1. MeshBasicMaterial:不受光照影响
  2. MeshLambertMaterial:Lambert 漫反射
  3. MeshPhongMaterial:Phong 光照模型
  4. MeshStandardMaterial:PBR 材质(物理渲染)

🔍 原理对比分析

1. 光照计算

原生 WebGL

  • 需要手动实现所有光照计算
  • 需要手动传递所有参数
  • 完全控制光照效果

Three.js

  • 内置多种光照模型
  • 自动处理参数传递
  • 使用方便,但灵活性有限

2. 法线处理

原生 WebGL

// 需要手动计算法线矩阵
mat4.invert(normalMatrix, modelMatrix);
mat4.transpose(normalMatrix, normalMatrix);

Three.js

// 自动处理法线变换
// BufferGeometry 自动计算法线

3. 材质系统

原生 WebGL

  • 需要手动编写 Shader
  • 需要手动管理 Uniform

Three.js

  • 内置多种材质
  • 自动生成 Shader
  • 支持自定义 Shader

📝 编程作业

基础作业(⭐)

任务:实现 Lambert 漫反射光照

要求

  1. 创建一个球体
  2. 实现基础的 Lambert 光照
  3. 添加一个可移动的光源

检查清单

  • 球体有正确的光照效果
  • 理解法线的作用
  • 理解点积的计算

进阶作业(⭐⭐)

任务:实现完整的 Blinn-Phong 光照

要求

  1. 添加环境光
  2. 添加漫反射
  3. 添加镜面高光
  4. 实现可调节的光源位置和强度

检查清单

  • 三种光照分量都正确
  • 高光效果明显
  • 可以调节参数

挑战作业(⭐⭐⭐)

任务:实现 PBR 材质

要求

  1. 实现基于物理的渲染
  2. 支持金属度和粗糙度
  3. 支持环境贴图

扩展功能

  • 支持多光源
  • 支持阴影
  • 支持法线贴图

🎓 本节总结

关键概念

  1. Lambert 漫反射:粗糙表面的均匀反射
  2. Phong 镜面高光:光滑表面的高光
  3. Blinn-Phong:使用半角向量的改进模型
  4. 法线矩阵:将法线从模型空间变换到世界空间

原生 WebGL vs Three.js

方面原生 WebGLThree.js
光照计算手动实现内置材质
法线处理手动计算自动处理
灵活性完全控制受限于 API
代码量

最佳实践

  1. 使用 Blinn-Phong:比 Phong 性能更好
  2. 归一化向量:所有方向向量必须归一化
  3. 在片段着色器计算:获得平滑的光照效果
  4. 使用 PBR:Three.js 中优先使用 MeshStandardMaterial

下一步

完成作业后,进入下一节:第 8 节:光照模型


📚 参考资源