{T}

矩阵变换

变换基础

在 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-matrix
javascript
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 矩阵:模型、视图、投影矩阵的组合
  • 相机系统:第一人称相机、轨道相机
  • 法线变换:法线矩阵的正确使用

最佳实践

  1. 使用数学库:gl-matrix 等成熟库
  2. 预计算矩阵:减少着色器计算
  3. 矩阵缓存:避免重复计算
  4. 精度控制:选择合适的浮点精度

下一步

继续学习高级特性,探索 WebGL 2.0 的新功能。