一、前言:90% 的 WebGL 新手,都栽在了 MVP 矩阵上
1.1 这篇文章,专治你的 3D 恐惧症
你是不是也这样?✅ 能画三角形,却搞不定 3D 立方体旋转✅ 复制粘贴矩阵代码,改个参数就崩✅ 对着满屏矩阵公式,越看越懵,直接放弃
如果答案是 YES,恭喜你刷到了救命文!本文不讲晦涩的数学推导(用初中知识讲透),只教你能直接落地的 3D 实战技巧,看完就能做出旋转的 3D 立方体。

1.2 为什么 MVP 矩阵是 WebGL 3D 的 "通关密码"?
新手最容易踩的坑:"能跑就行,不懂也没关系"。但现实是 ------想做摄像机环绕?卡壳!想让物体绕自身轴旋转?卡壳!想加透视效果让 3D 更真实?还是卡壳!
说白了:不懂 MVP 矩阵,你永远只能做 WebGL 的 "抄代码仔",成不了真正的 3D 开发者。
1.3 看完这篇,你能直接拿捏这些技能
✅ 秒懂模型 / 视图 / 投影矩阵:用 "拍电影" 比喻,10 秒记住各自作用✅ 告别死记硬背:用初中数学推导出平移 / 旋转 / 缩放矩阵✅ 实战落地:30 分钟做出可旋转的彩色 3D 立方体,代码直接复制能用✅ 避坑指南:解决深度测试、矩阵乘法顺序等新手必踩的 5 个坑
二、核心概念:用 "拍电影" 讲透 MVP 矩阵(看完忘不掉)
新手最怕的就是抽象,咱用 "拍电影" 的逻辑,把 MVP 矩阵刻进脑子里:
表格
| 阶段 | 对应矩阵 | 拍电影比喻 | 核心任务 |
|---|---|---|---|
| 模型变换 | 模型矩阵 | 演员在片场走位、摆姿势 | 把物体从 "自己的小空间" 放到 3D 世界里 |
| 视图变换 | 视图矩阵 | 摄影师移动摄像机、调角度 | 确定 "观众" 的视角,让画面以摄像机为中心 |
| 投影变换 | 投影矩阵 | 摄像机镜头把 3D 画面压成 2D | 给画面加 "立体感",定义哪些内容能被看到 |
💡 划重点:顶点最终位置公式(抄在笔记本上!)gl_Position = 投影矩阵 × 视图矩阵 × 模型矩阵 × 原始顶点坐标记住:矩阵乘法是 "从右到左",先动模型,再调视角,最后加透视!
三、保姆级解析:矩阵怎么用?
3.1 先搞懂:为啥非要用 4x4 矩阵?
新手灵魂拷问:3D 坐标只有 x/y/z,为啥矩阵要搞 4x4?答案:为了齐次坐标------ 简单说,3x3 矩阵搞不定 "平移",加个第四维,平移、旋转、缩放就能用一套公式搞定,这是 WebGL 的 "偷懒小技巧"!
3.2 模型矩阵:让物体 "动起来"(代码直接用)
- 平移矩阵(让物体挪位置):
javascript
运行
// 列主序,WebGL专用格式,Tx/Ty/Tz是平移距离
const translateMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
Tx, Ty, Tz, 1
];
- 旋转矩阵(让物体转起来):绕 Z 轴旋转的核心代码,改角度就能调转速
- 缩放矩阵(让物体变大变小):改数值就能控制长宽高
3.3 视图矩阵:做 "上帝视角" 的摄影师
不用自己写矩阵!用 gl-matrix 库的lookAt函数,1 行代码搞定摄像机位置:
// 摄像机在(3,3,5),盯着原点看,Y轴向上(符合人眼习惯)
mat4.lookAt(viewMatrix, [3.0, 3.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
改第一个数组,就能让摄像机 "挪位置",比如移到 (0,5,8),就是俯视视角!

函数参数详解
你代码中的这行调用,包含了三个最重要的信息:
javascript
mat4.lookAt(viewMatrix, // 参数1:输出的视图矩阵
[3.0, 3.0, 5.0], // 参数2:摄像机位置 (eye)
[0.0, 0.0, 0.0], // 参数3:观察目标点 (center/target)
[0.0, 1.0, 0.0] // 参数4:上方向 (up)
);
-
参数1
viewMatrix:这是一个已经创建好的矩阵变量(mat4.create())。函数执行后,计算好的视图矩阵就会存入这个变量,供后续渲染使用。 -
参数2
[3.0, 3.0, 5.0](摄像机位置) :想象你的眼睛就放在这个坐标点上。x=3, y=3, z=5意味着摄像机在世界坐标系的右方、上方、前方。这正是我们常说的"斜45度俯瞰视角"的经典位置,能很好地展现立方体的三维形态。 -
参数3
[0.0, 0.0, 0.0](观察目标点) :这是你眼睛盯着看的那个点 。这里设为原点(0,0,0),而我们的立方体顶点范围是-1到1,所以立方体的中心正好在原点。这就保证了立方体始终在视野中央。 -
参数4
[0.0, 1.0, 0.0](上方向) :这是一个至关重要的概念。它定义了你头顶的朝向 。这里设为(0,1,0),代表世界坐标系的Y轴正方向是"向上"。这个向量确保了摄像机是"正着"的,不会歪斜。你可以试着把它改成(1,0,0)(让X轴向上),想象一下整个观察的世界会怎样旋转90度。
3.4 投影矩阵:给画面加 "立体感"
透视投影是 3D 的灵魂!一行代码搞定:
javascript
运行
// 45度视野+自适应宽高比+近0.1远100,不会出现"穿模"
mat4.perspective(projMatrix, Math.PI / 4, canvas.width/canvas.height, 0.1, 100.0);
四、实战炸场:30 分钟做出旋转的彩色立方体(代码可直接复制)
光说不练假把式!下面是完整代码,复制保存为.html 文件,浏览器打开就能看到效果 ------彩色立方体自动旋转,立体感拉满!
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL MVP矩阵实战:旋转的彩色立方体</title>
<style>
body { margin: 0; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; }
#info {
position: absolute;
top: 20px;
left: 20px;
color: white;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 8px;
pointer-events: none;
z-index: 100;
font-size: 14px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.note {
position: absolute;
bottom: 20px;
left: 20px;
color: #aaa;
font-size: 12px;
background: rgba(0,0,0,0.4);
padding: 5px 12px;
border-radius: 20px;
}
</style>
</head>
<body>
<div id="info">
<h2>✨ MVP矩阵变换实战</h2>
<p>模型矩阵(旋转) × 视图矩阵(摄像机) × 投影矩阵(透视) = 最终变换</p>
<p>📐 当前状态:彩色立方体自动旋转 | 视角:斜上方45°</p>
</div>
<div class="note">🔥 爆文配套代码 · WebGL 矩阵变换详解 · 即拿即用</div>
<!-- 引入 gl-matrix 库,用于矩阵计算 (CDN) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<!-- 使用 WebGL 的着色器脚本模板 -->
<script id="vertex-shader" type="x-shader/x-vertex">
// 顶点着色器
attribute vec4 aPosition; // 顶点位置 (本地坐标)
attribute vec4 aColor; // 顶点颜色
uniform mat4 uMVP; // 模型-视图-投影 联合矩阵
varying vec4 vColor; // 传递给片元着色器的颜色
void main() {
// MVP矩阵变换顶点坐标 (关键步骤!)
gl_Position = uMVP * aPosition;
// 传递颜色 (不变)
vColor = aColor;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
// 片元着色器
precision mediump float;
varying vec4 vColor; // 从顶点着色器接收的颜色
void main() {
gl_FragColor = vColor; // 直接输出颜色
}
</script>
<script>
(function() {
// --- 初始化 WebGL 上下文 ---
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
const gl = canvas.getContext('webgl');
if (!gl) {
alert('你的浏览器不支持 WebGL!');
return;
}
// --- 启用深度测试 (非常重要! 否则立方体面片顺序会错乱) ---
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.1, 0.1, 0.15, 1.0); // 深灰色背景
// --- 编译着色器 ---
function compileShader(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 vertexShaderSource = document.getElementById('vertex-shader').text;
const fragmentShaderSource = document.getElementById('fragment-shader').text;
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
// --- 创建程序并链接 ---
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));
return;
}
gl.useProgram(program);
// --- 获取 attribute 和 uniform 变量位置 ---
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aColor = gl.getAttribLocation(program, 'aColor');
const uMVP = gl.getUniformLocation(program, 'uMVP');
// ========== 第一步:定义顶点数据 (8个顶点:位置 + 颜色) ==========
// 立方体的8个顶点坐标 (本地坐标,范围为 -1 到 1)
const vertexPositions = new Float32Array([
// 前面四个顶点 (z = +1)
-1.0, -1.0, 1.0, // 0: 前下左
1.0, -1.0, 1.0, // 1: 前下右
1.0, 1.0, 1.0, // 2: 前上右
-1.0, 1.0, 1.0, // 3: 前上左
// 后面四个顶点 (z = -1)
-1.0, -1.0, -1.0, // 4: 后下左
1.0, -1.0, -1.0, // 5: 后下右
1.0, 1.0, -1.0, // 6: 后上右
-1.0, 1.0, -1.0 // 7: 后上左
]);
// 每个顶点的颜色 (为了鲜艳,我们让每个角颜色不同)
const vertexColors = new Float32Array([
// 前面四个顶点颜色 (红、黄、绿、蓝)
1.0, 0.2, 0.2, // 0: 红色调
1.0, 1.0, 0.2, // 1: 黄色调
0.2, 1.0, 0.2, // 2: 绿色调
0.2, 0.6, 1.0, // 3: 蓝色调
// 后面四个顶点颜色 (品红、青、橙、紫)
1.0, 0.4, 0.8, // 4: 粉红色
0.2, 1.0, 1.0, // 5: 青色
1.0, 0.6, 0.2, // 6: 橙色
0.8, 0.4, 1.0 // 7: 紫色
]);
// ========== 第二步:定义索引数据 (12个三角形,共36个索引) ==========
// 每个面由两个三角形组成,这里按逆时针顺序排列(面向外面)
const indices = new Uint16Array([
// 前面 (0,1,2, 0,2,3)
0, 1, 2, 0, 2, 3,
// 右面 (1,5,6, 1,6,2)
1, 5, 6, 1, 6, 2,
// 后面 (5,4,7, 5,7,6)
5, 4, 7, 5, 7, 6,
// 左面 (4,0,3, 4,3,7)
4, 0, 3, 4, 3, 7,
// 上面 (3,2,6, 3,6,7)
3, 2, 6, 3, 6, 7,
// 下面 (4,5,1, 4,1,0)
4, 5, 1, 4, 1, 0
]);
// ========== 第三步:创建缓冲区并上传数据 ==========
// 1. 创建位置缓冲区
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexPositions, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// 2. 创建颜色缓冲区
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexColors, gl.STATIC_DRAW);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor);
// 3. 创建索引缓冲区 (ELEMENT_ARRAY_BUFFER)
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// 解绑 ARRAY_BUFFER (避免后续误操作)
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// ========== 第四步:矩阵变换相关代码 ==========
// 创建矩阵存储对象
const modelMatrix = mat4.create(); // 模型矩阵
const viewMatrix = mat4.create(); // 视图矩阵
const projMatrix = mat4.create(); // 投影矩阵
const mvpMatrix = mat4.create(); // 最终的 MVP 矩阵
// 初始化投影矩阵 (透视投影)
// 参数:输出矩阵, 视野角度(弧度), 宽高比, 近平面, 远平面
mat4.perspective(projMatrix,
Math.PI / 4, // 45度视野
canvas.width / canvas.height, // 宽高比
0.1, // 近平面
100.0 // 远平面
);
// 初始化视图矩阵 (摄像机位置)
// 参数:输出矩阵, 摄像机位置, 观察目标点, 上方向
mat4.lookAt(viewMatrix,
[3.0, 3.0, 5.0], // 眼睛(摄像机)在右上方
[0.0, 0.0, 0.0], // 观察原点
[0.0, 1.0, 0.0] // Y轴向上
);
// ========== 动画循环 ==========
let angle = 0; // 旋转角度
function animate() {
// 更新旋转角度 (随时间增加)
angle += 0.01;
// --- 1. 更新模型矩阵 (绕Y轴旋转) ---
mat4.identity(modelMatrix); // 重置为单位矩阵
mat4.rotateY(modelMatrix, modelMatrix, angle); // 绕Y轴旋转
// 可以尝试同时绕X轴旋转: mat4.rotateX(modelMatrix, modelMatrix, angle * 0.5);
// --- 2. 计算最终的 MVP 矩阵: MVP = 投影 × 视图 × 模型 ---
// 先计算 视图 × 模型
mat4.multiply(mvpMatrix, viewMatrix, modelMatrix);
// 再乘投影矩阵
mat4.multiply(mvpMatrix, projMatrix, mvpMatrix);
// --- 3. 将MVP矩阵传递给着色器 ---
gl.uniformMatrix4fv(uMVP, false, mvpMatrix);
// --- 4. 清屏并绘制 ---
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 使用索引缓冲区绘制立方体
// 参数:绘制模式, 索引数量, 索引数据类型, 偏移量
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
// 请求下一帧动画
requestAnimationFrame(animate);
}
// 启动动画
animate();
// ========== 窗口大小自适应 ==========
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
// 更新投影矩阵的宽高比
mat4.perspective(projMatrix,
Math.PI / 4,
canvas.width / canvas.height,
0.1, 100.0
);
});
})();
</script>
</body>
</html>
当你运行这段代码,会看到一个色彩鲜艳的立方体在深色背景中平滑地绕Y轴自动旋转。它的每一个面都有渐变的色彩,立体感非常强。


代码中添加了详细的注释,并特别突出了以下几个关键点,你可以直接在文章中引用:
1. MVP矩阵的完整计算流程
代码中清晰地展示了每一步:
// 先计算 视图 × 模型
mat4.multiply(mvpMatrix, viewMatrix, modelMatrix);
// 再乘投影矩阵
mat4.multiply(mvpMatrix, projMatrix, mvpMatrix);
重点强调:矩阵乘法顺序是从右到左的,即先应用模型变换,再应用视图变换,最后应用投影变换。
2. 索引缓冲区的使用
代码使用了36个索引来绘制12个三角形(6个面 × 2个三角形),避免了重复定义顶点数据,这正是你最初提供的参考文章中所讲的性能优化技巧。还记得上一篇索引复用缓存的教程吗?这里我们用36个索引复用了8个顶点,内存效率提升了4.5倍!"
3. 深度测试的启用
gl.enable(gl.DEPTH_TEST); 这行代码至关重要。如果不启用深度测试,立方体背面的面可能会绘制在前面,导致视觉错误。这是很多初学者容易忽略的点,提出来可以展现你的专业度。
4. 响应式画布
代码监听了 resize 事件,当窗口大小改变时,自动更新画布尺寸和投影矩阵的宽高比,确保立方体永远不变形。
五、新手必看:5 个踩坑点,避开直接少走 1 个月弯路
- **矩阵乘法顺序别搞反!**错:投影 × 模型 × 视图 → 立方体直接 "飞出屏幕"对:投影 × 视图 × 模型 → 先动物体,再调视角,最后加透视
- 一定要开深度测试! 漏了
gl.enable(gl.DEPTH_TEST),立方体背面会盖在前面,直接变成 "纸片" - **索引缓冲区超省内存!**8 个顶点 + 36 个索引,比直接画 36 个顶点省 4.5 倍内存,大厂都这么用!
- **窗口缩放要更更新投影矩阵!**不然窗口变大 / 变小,立方体就会 "拉伸变形",像哈哈镜一样
- 角度更新要加时间因子! 用
requestAnimationFrame更新角度,旋转速度不卡帧,丝滑到离谱
六、进阶挑战:从 "会用" 到 "精通",秀翻面试官
- 摄像机漫游:加键盘监听,按 WASD 控制摄像机移动,实现 "在 3D 世界走路"
- 多物体动画:给 2 个立方体加不同的模型矩阵,一个绕 Y 轴转,一个绕 X 轴转
- 加光照效果:基于模型矩阵推导法线矩阵,让立方体有 "光影感",瞬间变高级
七、结语:MVP 矩阵,是你进 WebGL 3D 大门的钥匙
看完这篇,你已经搞定了 WebGL 3D 的核心 ------ 不用再抄别人的代码,不用再怕改参数崩掉,你可以自己控制物体的旋转、视角的切换、画面的透视。
从画三角形到做 3D 立方体,你只缺这一步:吃透 MVP 矩阵。现在代码给你了,思路给你了,剩下的就是动手改一改、玩一玩 ------ 毕竟,WebGL 的乐趣,就在于把脑子里的 3D 世界,亲手画在屏幕上!
感谢阅读! 喜欢本文请不要吝啬你的 一键三连:点赞、收藏、留言,让更多小伙伴看到这份干货!