跳到主要内容WebGL 矩阵基础:平移、旋转、缩放与复合变换实战 | 极客日志JavaScript大前端算法
WebGL 矩阵基础:平移、旋转、缩放与复合变换实战
WebGL 渲染依赖矩阵乘法处理顶点变换。本文详解平移、旋转、缩放的 4x4 矩阵原理及实现,涵盖纯手写矩阵与 gl-matrix 库的复合变换实战。重点解析变换顺序对结果的影响、单位矩阵作用及列主序格式要求,帮助开发者掌握 GPU 高效并行计算的核心逻辑,避免手动修改顶点坐标的性能陷阱。
leon17 浏览 在 WebGL 开发中,矩阵是驱动 3D 图形的核心引擎。想要实现流畅的动画效果,理解矩阵乘法及其在 GPU 中的运作方式至关重要。
为什么矩阵是 WebGL 的灵魂
WebGL 基于 OpenGL ES,其渲染管线高度依赖着色器。矩阵作为连接 JavaScript 逻辑与 GPU 渲染的桥梁,负责处理所有空间变换。
- 统一逻辑:平移、旋转、缩放均通过「矩阵 × 顶点坐标」完成,符合 GPU 规范。
- 性能优势:矩阵运算可被 GPU 高效并行处理,确保动画丝滑。
- 灵活组合:多个矩阵相乘即可实现复杂的复合变换。
关键原则:WebGL 中所有变换的标准写法是 gl_Position = 变换矩阵 * 顶点坐标,而非直接修改顶点的 x/y 分量。
1. 平移变换
平移是最基础的仿射变换。在计算机图形学中,它通过齐次坐标和 4x4 矩阵实现。
原理
三维点表示为齐次坐标 (x, y, z, 1)。平移矩阵的形式如下:
[ 1 0 0 tx ]
[ 0 1 0 ty ]
[ 0 0 1 tz ]
[ 0 0 0 1 ]
执行乘法后,新坐标 x' = x + tx,以此类推。即使处理 2D 图形,也统一使用 4x4 矩阵(z/tz 设为 0)。
实战代码
下面是一个纯矩阵实现的平移动画示例,红色三角形沿 X 轴往复运动。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebGL 矩阵实战:平移的红色三角形</title>
<style>
canvas { border: 2px solid #ff4400; display: block; margin: 20px auto; border-radius: 8px; }
</style>
</head>
<body>
<canvas id="glCanvas">
</canvas>
<script>
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('您的浏览器不支持 WebGL,请尝试 Chrome 或 Firefox');
throw new Error('WebGL not supported');
}
gl.clearColor(0.0, 0.0, 0.0, 1.0);
const vertexShaderSource = `
attribute vec4 a_Position;
uniform mat4 u_TranslateMatrix;
void main() {
gl_Position = u_TranslateMatrix * a_Position;
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
function createShader(gl, source, type) {
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;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([
-0.2, -0.2, 0.0,
0.2, -0.2, 0.0,
0.0, 0.2, 0.0
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
const u_TranslateMatrix = gl.getUniformLocation(program, 'u_TranslateMatrix');
const translateMatrix = new Float32Array(16);
let tx = 0.0;
const step = 0.02;
function setTranslateMatrix(matrix, tx, ty, tz) {
matrix.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1]);
}
function animate() {
tx += step;
if (tx > 1.0) tx = -1.0;
setTranslateMatrix(translateMatrix, tx, 0.0, 0.0);
gl.uniformMatrix4fv(u_TranslateMatrix, false, translateMatrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>
2. 旋转变换
旋转的核心在于构建包含三角函数的 4x4 矩阵。二维点绕原点逆时针旋转 θ 角的矩阵如下:
[ cosθ -sinθ 0 0 ]
[ sinθ cosθ 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]
计算过程等价于 x' = x*cosθ - y*sinθ。将完整矩阵传递给着色器,GPU 会自动完成运算,无需在 Shader 中重复计算三角函数。
实战代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebGL 矩阵实战:旋转的红色三角形</title>
<style>
canvas { border: 2px solid #ff4400; display: block; margin: 20px auto; border-radius: 8px; }
</style>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('您的浏览器不支持 WebGL');
throw new Error('WebGL not supported');
}
const vertexShaderSource = `
attribute vec4 a_Position;
uniform mat4 u_RotateMatrix;
void main() {
gl_Position = u_RotateMatrix * a_Position;
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
function createShader(gl, source, type) {
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;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
const u_RotateMatrix = gl.getUniformLocation(program, 'u_RotateMatrix');
const rotateMatrix = new Float32Array(16);
let angle = 0.0;
function setRotateZMatrix(matrix, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
matrix.set([cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
}
function animateRotate() {
angle += 0.02;
setRotateZMatrix(rotateMatrix, angle);
gl.uniformMatrix4fv(u_RotateMatrix, false, rotateMatrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animateRotate);
}
animateRotate();
</script>
</body>
</html>


3. 缩放变换
缩放通过改变对角线上的元素来实现。二维点沿 x/y 轴缩放的矩阵如下:
[ sx 0 0 0 ]
[ 0 sy 0 0 ]
[ 0 0 sz 0 ]
[ 0 0 0 1 ]
其中 sx, sy, sz 分别为各轴缩放因子。负数可实现翻转效果。
实战代码
此示例让三角形在 0.5 倍到 2 倍之间来回缩放。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebGL 矩阵实战:缩放的红色三角形</title>
<style>
canvas { border: 2px solid #ff4400; display: block; margin: 20px auto; border-radius: 8px; }
</style>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('您的浏览器不支持 WebGL');
throw new Error('WebGL not supported');
}
const vertexShaderSource = `
attribute vec4 a_Position;
uniform mat4 u_ScaleMatrix;
void main() {
gl_Position = u_ScaleMatrix * a_Position;
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
function createShader(gl, source, type) {
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;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
const u_ScaleMatrix = gl.getUniformLocation(program, 'u_ScaleMatrix');
const scaleMatrix = new Float32Array(16);
let scaleFactor = 1.0;
const step = 0.01;
let isEnlarging = true;
function setScaleMatrix(matrix, sx, sy, sz) {
matrix.set([sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1]);
}
function animateScale() {
if (isEnlarging) {
scaleFactor += step;
if (scaleFactor >= 2.0) isEnlarging = false;
} else {
scaleFactor -= step;
if (scaleFactor <= 0.5) isEnlarging = true;
}
setScaleMatrix(scaleMatrix, scaleFactor, scaleFactor, 1.0);
gl.uniformMatrix4fv(u_ScaleMatrix, false, scaleMatrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animateScale);
}
animateScale();
</script>
</body>
</html>


4. 复合变换:平移 + 旋转 + 缩放
手动组合矩阵容易出错且难以维护。在实际开发中,推荐使用 gl-matrix 库来处理复杂的矩阵运算。
gl-matrix 的优势
- 封装了所有 4x4 矩阵运算方法。
- 自动处理矩阵乘法顺序。
- 针对 WebGL 做了优化,性能更优。
实战代码
以下示例展示了如何使用 gl-matrix 实现同时进行的平移、旋转和缩放动画。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebGL 矩阵进阶:复合动画</title>
<style>
canvas { border: 2px solid #ff4400; display: block; margin: 20px auto; border-radius: 8px; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('您的浏览器不支持 WebGL');
throw new Error('WebGL not supported');
}
const vertexShaderSource = `
attribute vec4 a_Position;
uniform mat4 u_ModelMatrix;
void main() {
gl_Position = u_ModelMatrix * a_Position;
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
function createShader(gl, source, type) {
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;
}
const vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
const u_ModelMatrix = gl.getUniformLocation(program, 'u_ModelMatrix');
const modelMatrix = mat4.create();
let tx = 0.0;
let angle = 0.0;
let scaleFactor = 1.0;
let isEnlarging = true;
function animateWithMatrix() {
mat4.identity(modelMatrix);
mat4.scale(modelMatrix, modelMatrix, [scaleFactor, scaleFactor, 1.0]);
mat4.rotateZ(modelMatrix, modelMatrix, angle);
mat4.translate(modelMatrix, modelMatrix, [tx, 0.0, 0.0]);
tx += 0.015;
if (tx > 1.0) tx = -1.0;
angle += 0.02;
if (isEnlarging) {
scaleFactor += 0.005;
if (scaleFactor >= 1.5) isEnlarging = false;
} else {
scaleFactor -= 0.005;
if (scaleFactor <= 0.7) isEnlarging = true;
}
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animateWithMatrix);
}
animateWithMatrix();
</script>
</body>
</html>


核心知识点总结
- 变换顺序决定结果:矩阵乘法不满足交换律。推荐顺序为:缩放 → 旋转 → 平移。若先平移再旋转,会导致物体围绕原点旋转而非自身中心。
- 单位矩阵是基础:
mat4.identity() 生成无变换的单位矩阵,是所有操作的起点。
- 格式要求严格:WebGL 中矩阵必须是
Float32Array 类型的 16 个元素(列主序),传递时 gl.uniformMatrix4fv 的第二个参数必须为 false。
避坑指南
- 不要死磕数学公式:先跑通代码,观察
tx、angle、scaleFactor 变化对动画的影响,直观理解矩阵作用。
- 拒绝手动改顶点:始终遵循「矩阵 × 顶点」的标准写法,直接修改坐标会丧失 GPU 并行计算优势。
- 优先使用库:实际开发中避免手动拼凑矩阵,减少错误率,专注业务逻辑。
- 注意缩放中心:默认缩放绕原点进行。若需绕自定义点缩放,需先平移到原点→缩放→平移回原位置。
掌握这些基础后,后续可以进一步探索透视矩阵、视图矩阵以及 MVP 矩阵模型,构建真正的 3D 场景。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online