矩阵变换
变换基础
在 3D 图形中,所有的几何变换都可以用矩阵乘法表示。理解矩阵变换是 WebGL 开发的核心技能。
坐标系统
WebGL 中的坐标系统:
code
Y+
│
│
│
└─────── X+
╱
╱
Z+- X 轴:水平向右
- Y 轴:垂直向上
- Z 轴:指向屏幕外
坐标空间
| 空间 | 说明 | 变换 |
|---|---|---|
| 模型空间 | 物体自身坐标系 | - |
| 世界空间 | 场景全局坐标系 | 模型矩阵 |
| 观察空间 | 相机坐标系 | 视图矩阵 |
| 裁剪空间 | 归一化坐标系 | 投影矩阵 |
| 屏幕空间 | 像素坐标 | 视口变换 |
基本变换矩阵
平移矩阵
将物体沿 X、Y、Z 轴移动:
code
| 1 0 0 Tx | | x | | x + Tx |
| 0 1 0 Ty | × | y | = | y + Ty |
| 0 0 1 Tz | | z | | z + Tz |
| 0 0 0 1 | | 1 | | 1 |javascript
// 创建平移矩阵
function createTranslationMatrix(tx, ty, tz) {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
]);
}
// 使用示例
const translateMatrix = createTranslationMatrix(1.0, 2.0, 3.0);旋转矩阵
绕 X 轴旋转
code
| 1 0 0 0 | | x | | x |
| 0 cos(θ) -sin(θ) 0 | × | y | = | y·cos- z·sin |
| 0 sin(θ) cos(θ) 0 | | z | | y·sin+ z·cos |
| 0 0 0 1 | | 1 | | 1 |绕 Y 轴旋转
code
| cos(θ) 0 sin(θ) 0 |
| 0 1 0 0 |
| -sin(θ) 0 cos(θ) 0 |
| 0 0 0 1 |绕 Z 轴旋转
code
| cos(θ) -sin(θ) 0 0 |
| sin(θ) cos(θ) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |javascript
// 绕 X 轴旋转
function createRotationXMatrix(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return new Float32Array([
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
]);
}
// 绕 Y 轴旋转
function createRotationYMatrix(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return new Float32Array([
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
]);
}
// 绕 Z 轴旋转
function createRotationZMatrix(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return new Float32Array([
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
}缩放矩阵
code
| Sx 0 0 0 | | x | | x·Sx |
| 0 Sy 0 0 | × | y | = | y·Sy |
| 0 0 Sz 0 | | z | | z·Sz |
| 0 0 0 1 | | 1 | | 1 |javascript
// 创建缩放矩阵
function createScaleMatrix(sx, sy, sz) {
return new Float32Array([
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
]);
}四元数
四元数(Quaternion)是一种用于表示 3D 旋转的数学工具,避免了万向节锁问题。
四元数基础
四元数表示为 q = w + xi + yj + zk,其中 w, x, y, z 是四个分量。
javascript
// 四元数类
class Quaternion {
constructor(x = 0, y = 0, z = 0, w = 1) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
// 从欧拉角创建
static fromEuler(x, y, z) {
const c1 = Math.cos(x / 2);
const c2 = Math.cos(y / 2);
const c3 = Math.cos(z / 2);
const s1 = Math.sin(x / 2);
const s2 = Math.sin(y / 2);
const s3 = Math.sin(z / 2);
return new Quaternion(
s1 * c2 * c3 + c1 * s2 * s3,
c1 * s2 * c3 - s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
c1 * c2 * c3 - s1 * s2 * s3
);
}
// 从轴角创建
static fromAxisAngle(axis, angle) {
const halfAngle = angle / 2;
const s = Math.sin(halfAngle);
return new Quaternion(
axis[0] * s,
axis[1] * s,
axis[2] * s,
Math.cos(halfAngle)
);
}
// 四元数乘法
multiply(q) {
return new Quaternion(
this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y,
this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x,
this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w,
this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z
);
}
// 转换为旋转矩阵
toMatrix() {
const xx = this.x * this.x;
const yy = this.y * this.y;
const zz = this.z * this.z;
const xy = this.x * this.y;
const xz = this.x * this.z;
const yz = this.y * this.z;
const wx = this.w * this.x;
const wy = this.w * this.y;
const wz = this.w * this.z;
return new Float32Array([
1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy), 0,
2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx), 0,
2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy), 0,
0, 0, 0, 1
]);
}
// 球面线性插值
static slerp(q1, q2, t) {
let dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
if (dot < 0) {
q2 = new Quaternion(-q2.x, -q2.y, -q2.z, -q2.w);
dot = -dot;
}
if (dot > 0.9995) {
return new Quaternion(
q1.x + t * (q2.x - q1.x),
q1.y + t * (q2.y - q1.y),
q1.z + t * (q2.z - q1.z),
q1.w + t * (q2.w - q1.w)
).normalize();
}
const theta0 = Math.acos(dot);
const theta = theta0 * t;
const sinTheta = Math.sin(theta);
const sinTheta0 = Math.sin(theta0);
const s0 = Math.cos(theta) - dot * sinTheta / sinTheta0;
const s1 = sinTheta / sinTheta0;
return new Quaternion(
s0 * q1.x + s1 * q2.x,
s0 * q1.y + s1 * q2.y,
s0 * q1.z + s1 * q2.z,
s0 * q1.w + s1 * q2.w
);
}
normalize() {
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
return new Quaternion(this.x / len, this.y / len, this.z / len, this.w / len);
}
}四元数优势
| 特性 | 欧拉角 | 四元数 |
|---|---|---|
| 万向节锁 | 存在 | 无 |
| 插值平滑 | 不稳定 | 稳定(SLERP) |
| 存储效率 | 3 个浮点数 | 4 个浮点数 |
| 计算效率 | 较低 | 较高 |
| 可读性 | 直观 | 不直观 |
矩阵运算
矩阵乘法
WebGL 使用列主序(Column-Major)存储矩阵:
javascript
// 4×4 矩阵乘法
function multiplyMatrix(a, b) {
const result = new Float32Array(16);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
result[i * 4 + j] =
a[i * 4 + 0] * b[0 * 4 + j] +
a[i * 4 + 1] * b[1 * 4 + j] +
a[i * 4 + 2] * b[2 * 4 + j] +
a[i * 4 + 3] * b[3 * 4 + j];
}
}
return result;
}组合变换
多个变换按从右到左的顺序应用:
javascript
// 先缩放,再旋转,最后平移
const scale = createScaleMatrix(2.0, 2.0, 2.0);
const rotate = createRotationYMatrix(Math.PI / 4);
const translate = createTranslationMatrix(1.0, 0.0, 0.0);
// 注意顺序:从右到左
const modelMatrix = multiplyMatrix(
multiplyMatrix(translate, rotate),
scale
);模型视图投影矩阵
MVP 矩阵概念
MVP 矩阵将顶点从模型空间变换到裁剪空间:
code
模型空间 → 世界空间 → 观察空间 → 裁剪空间
↓ ↓ ↓ ↓
Model × View × Projection = MVP模型矩阵(Model Matrix)
将模型空间变换到世界空间:
javascript
const modelMatrix = new Float32Array([
// 缩放 2 倍,旋转 45 度,平移到 (1, 0, 0)
1.414, 0, -1.414, 0,
0, 2, 0, 0,
1.414, 0, 1.414, 0,
1, 0, 0, 1
]);视图矩阵(View Matrix)
定义相机位置和方向:
javascript
// lookAt 矩阵
function createLookAtMatrix(eye, center, up) {
const z = normalize([eye[0] - center[0], eye[1] - center[1], eye[2] - center[2]]);
const x = normalize(cross(up, z));
const y = cross(z, x);
return new Float32Array([
x[0], y[0], z[0], 0,
x[1], y[1], z[1], 0,
x[2], y[2], z[2], 0,
-dot(x, eye), -dot(y, eye), -dot(z, eye), 1
]);
}
// 辅助函数
function normalize(v) {
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / len, v[1] / len, v[2] / len];
}
function cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
// 使用示例:相机在 (0, 0, 5),看向原点
const viewMatrix = createLookAtMatrix(
[0, 0, 5], // 相机位置
[0, 0, 0], // 看向的目标点
[0, 1, 0] // 上方向
);投影矩阵(Projection Matrix)
透视投影
模拟人眼视觉效果,近大远小:
javascript
function createPerspectiveMatrix(fov, aspect, near, far) {
const f = 1.0 / Math.tan(fov / 2);
const nf = 1 / (near - far);
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) * nf, -1,
0, 0, 2 * far * near * nf, 0
]);
}
// 使用示例
const projectionMatrix = createPerspectiveMatrix(
Math.PI / 4, // 视角(45 度)
600 / 400, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);正交投影
平行投影,无透视效果:
javascript
function createOrthographicMatrix(left, right, bottom, top, near, far) {
const lr = 1 / (left - right);
const bt = 1 / (bottom - top);
const nf = 1 / (near - far);
return new Float32Array([
-2 * lr, 0, 0, 0,
0, -2 * bt, 0, 0,
0, 0, 2 * nf, 0,
(left + right) * lr, (top + bottom) * bt, (far + near) * nf, 1
]);
}
// 使用示例
const orthoMatrix = createOrthographicMatrix(
-5, 5, // 左右
-5, 5, // 上下
0.1, 100 // 近远
);使用矩阵库
手动计算矩阵容易出错,推荐使用成熟的数学库。
gl-matrix
强大的矩阵和向量运算库:
bash
npm install gl-matrixjavascript
import { mat4, vec3 } from 'gl-matrix';
// 创建单位矩阵
const modelMatrix = mat4.create();
// 应用变换
mat4.translate(modelMatrix, modelMatrix, [1, 2, 3]);
mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 4);
mat4.scale(modelMatrix, modelMatrix, [2, 2, 2]);
// 视图矩阵
const viewMatrix = mat4.create();
mat4.lookAt(viewMatrix,
[0, 0, 5], // 相机位置
[0, 0, 0], // 目标位置
[0, 1, 0] // 上方向
);
// 投影矩阵
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix,
Math.PI / 4, // 视角
600 / 400, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
// MVP 矩阵
const mvpMatrix = mat4.create();
mat4.multiply(mvpMatrix, projectionMatrix, viewMatrix);
mat4.multiply(mvpMatrix, mvpMatrix, modelMatrix);
// 传递给着色器
gl.uniformMatrix4fv(u_MVPMatrix, false, mvpMatrix);在着色器中使用矩阵
顶点着色器
glsl
attribute vec3 a_Position;
uniform mat4 u_ModelMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjectionMatrix;
void main() {
// 方式一:分别计算
vec4 worldPos = u_ModelMatrix * vec4(a_Position, 1.0);
vec4 viewPos = u_ViewMatrix * worldPos;
gl_Position = u_ProjectionMatrix * viewPos;
// 方式二:使用预计算的 MVP 矩阵
// gl_Position = u_MVPMatrix * vec4(a_Position, 1.0);
}法线变换
法线需要使用法线矩阵变换:
glsl
attribute vec3 a_Normal;
uniform mat4 u_ModelMatrix;
uniform mat3 u_NormalMatrix; // 法线矩阵
varying vec3 v_Normal;
void main() {
v_Normal = u_NormalMatrix * a_Normal;
// 或者
// v_Normal = mat3(transpose(inverse(u_ModelMatrix))) * a_Normal;
}JavaScript 中计算法线矩阵
javascript
import { mat4, mat3 } from 'gl-matrix';
const normalMatrix = mat3.create();
mat3.normalFromMat4(normalMatrix, modelMatrix);
gl.uniformMatrix3fv(u_NormalMatrix, false, normalMatrix);相机系统详解
相机类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 第一人称 | 固定在角色视角 | FPS 游戏 |
| 第三人称 | 跟随角色后上方 | TPS 游戏 |
| 轨道相机 | 围绕目标旋转 | 模型查看器 |
| 正交相机 | 无透视效果 | 2D 游戏、CAD |
| 自由相机 | 任意移动 | 场景编辑器 |
第一人称相机
javascript
class FirstPersonCamera {
constructor() {
this.position = [0, 0, 5];
this.pitch = 0; // 俯仰角
this.yaw = 0; // 偏航角
this.speed = 0.1;
}
moveForward() {
const direction = this.getDirection();
this.position[0] += direction[0] * this.speed;
this.position[2] += direction[2] * this.speed;
}
moveBackward() {
const direction = this.getDirection();
this.position[0] -= direction[0] * this.speed;
this.position[2] -= direction[2] * this.speed;
}
moveLeft() {
const right = this.getRight();
this.position[0] -= right[0] * this.speed;
this.position[2] -= right[2] * this.speed;
}
moveRight() {
const right = this.getRight();
this.position[0] += right[0] * this.speed;
this.position[2] += right[2] * this.speed;
}
look(dx, dy) {
this.yaw += dx * 0.01;
this.pitch -= dy * 0.01;
this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch));
}
getDirection() {
return [
Math.sin(this.yaw) * Math.cos(this.pitch),
Math.sin(this.pitch),
-Math.cos(this.yaw) * Math.cos(this.pitch)
];
}
getRight() {
return [
Math.cos(this.yaw),
0,
Math.sin(this.yaw)
];
}
getViewMatrix() {
const target = [
this.position[0] + Math.sin(this.yaw) * Math.cos(this.pitch),
this.position[1] + Math.sin(this.pitch),
this.position[2] - Math.cos(this.yaw) * Math.cos(this.pitch)
];
const viewMatrix = mat4.create();
mat4.lookAt(viewMatrix, this.position, target, [0, 1, 0]);
return viewMatrix;
}
}轨道相机
javascript
class OrbitCamera {
constructor() {
this.target = [0, 0, 0];
this.radius = 5;
this.theta = 0; // 水平角度
this.phi = Math.PI / 2; // 垂直角度
this.minRadius = 1;
this.maxRadius = 100;
}
rotate(dx, dy) {
this.theta -= dx * 0.01;
this.phi -= dy * 0.01;
this.phi = Math.max(0.01, Math.min(Math.PI - 0.01, this.phi));
}
zoom(delta) {
this.radius *= delta > 0 ? 1.1 : 0.9;
this.radius = Math.max(this.minRadius, Math.min(this.maxRadius, this.radius));
}
pan(dx, dy) {
const right = this.getRight();
const up = [0, 1, 0];
this.target[0] -= right[0] * dx * 0.01;
this.target[1] -= up[1] * dy * 0.01;
this.target[2] -= right[2] * dx * 0.01;
}
getRight() {
return [
Math.cos(this.theta),
0,
Math.sin(this.theta)
];
}
getPosition() {
return [
this.target[0] + this.radius * Math.sin(this.phi) * Math.sin(this.theta),
this.target[1] + this.radius * Math.cos(this.phi),
this.target[2] + this.radius * Math.sin(this.phi) * Math.cos(this.theta)
];
}
getViewMatrix() {
const viewMatrix = mat4.create();
mat4.lookAt(viewMatrix, this.getPosition(), this.target, [0, 1, 0]);
return viewMatrix;
}
getProjectionMatrix(aspect) {
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI / 4, aspect, 0.1, 1000);
return projectionMatrix;
}
}相机控制器
javascript
class CameraController {
constructor(camera, canvas) {
this.camera = camera;
this.canvas = canvas;
this.isDragging = false;
this.lastX = 0;
this.lastY = 0;
this.setupEventListeners();
}
setupEventListeners() {
// 鼠标事件
this.canvas.addEventListener('mousedown', (e) => {
this.isDragging = true;
this.lastX = e.clientX;
this.lastY = e.clientY;
});
this.canvas.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - this.lastX;
const dy = e.clientY - this.lastY;
this.camera.rotate(dx, dy);
this.lastX = e.clientX;
this.lastY = e.clientY;
});
this.canvas.addEventListener('mouseup', () => {
this.isDragging = false;
});
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
this.camera.zoom(e.deltaY);
});
// 键盘事件
document.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case 'w':
this.camera.moveForward?.();
break;
case 's':
this.camera.moveBackward?.();
break;
case 'a':
this.camera.moveLeft?.();
break;
case 'd':
this.camera.moveRight?.();
break;
}
});
// 触摸事件
this.canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
this.isDragging = true;
this.lastX = e.touches[0].clientX;
this.lastY = e.touches[0].clientY;
}
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length === 1 && this.isDragging) {
const dx = e.touches[0].clientX - this.lastX;
const dy = e.touches[0].clientY - this.lastY;
this.camera.rotate(dx, dy);
this.lastX = e.touches[0].clientX;
this.lastY = e.touches[0].clientY;
}
});
this.canvas.addEventListener('touchend', () => {
this.isDragging = false;
});
}
}实例:旋转立方体
完整示例:
javascript
import { mat4 } from 'gl-matrix';
// 顶点着色器
const vertexShaderSource = `
attribute vec3 a_Position;
attribute vec3 a_Color;
uniform mat4 u_MVPMatrix;
varying vec3 v_Color;
void main() {
gl_Position = u_MVPMatrix * vec4(a_Position, 1.0);
v_Color = a_Color;
}
`;
// 片段着色器
const fragmentShaderSource = `
precision mediump float;
varying vec3 v_Color;
void main() {
gl_FragColor = vec4(v_Color, 1.0);
}
`;
function main() {
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
// ... 创建着色器程序 ...
// 立方体顶点数据
const vertices = new Float32Array([
// 前面
-1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1,
// 后面
-1, 1, -1, -1, -1, -1, 1, -1, -1, 1, 1, -1,
// 其他面 ...
]);
// 创建矩阵
const modelMatrix = mat4.create();
const viewMatrix = mat4.create();
const projectionMatrix = mat4.create();
const mvpMatrix = mat4.create();
// 设置视图矩阵
mat4.lookAt(viewMatrix, [0, 0, 5], [0, 0, 0], [0, 1, 0]);
// 设置投影矩阵
mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 0.1, 100);
// 动画循环
function render() {
// 旋转模型
mat4.rotateY(modelMatrix, modelMatrix, 0.01);
mat4.rotateX(modelMatrix, modelMatrix, 0.005);
// 计算 MVP 矩阵
mat4.multiply(mvpMatrix, projectionMatrix, viewMatrix);
mat4.multiply(mvpMatrix, mvpMatrix, modelMatrix);
// 清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 传递矩阵
gl.uniformMatrix4fv(u_MVPMatrix, false, mvpMatrix);
// 绘制
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
render();
}小结
核心概念
- 矩阵变换:平移、旋转、缩放
- 四元数:避免万向节锁,平滑插值
- MVP 矩阵:模型、视图、投影矩阵的组合
- 相机系统:第一人称相机、轨道相机
- 法线变换:法线矩阵的正确使用
最佳实践
- 使用数学库:gl-matrix 等成熟库
- 预计算矩阵:减少着色器计算
- 矩阵缓存:避免重复计算
- 精度控制:选择合适的浮点精度
下一步
继续学习高级特性,探索 WebGL 2.0 的新功能。