核心定义
模型矩阵,Model Matrix,作用是把模型的顶点从局部坐标系变换到世界坐标系。- 它回答的是 3 个问题:物体
放在哪里、朝向哪里、有多大。 - 在 WebGL 顶点着色器里,常见写法是:
glsl
gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
- 这里的
uModelMatrix,就是你要创建的模型矩阵。
先建立正确直觉
- 模型原始顶点通常是"建模时的坐标",比如一个立方体中心在
(0, 0, 0)。 - 但场景里真正显示时,这个立方体可能要:
- 移动到
(10, 2, -5) - 绕
y轴旋转45度 - 缩放成原来的
2倍
- 移动到
- 这些变化不会直接改顶点数据本身,而是通过一个
模型矩阵统一描述。 - GPU 在处理每个顶点时,用这个矩阵把局部点变成世界空间中的点。
模型矩阵本质上怎么创建
- 模型矩阵本质上是把多个基础变换矩阵
乘起来。 - 最常见的基础变换有 3 个:
平移矩阵 T旋转矩阵 R缩放矩阵 S
通常写成:
text
M = T * R * S
或者更完整一点:
text
M = T * Rz * Ry * Rx * S
这表示:
- 先缩放
- 再旋转
- 最后平移
虽然你写的是 T * R * S,但当它作用到顶点上时,真正执行顺序是从右往左。
为什么先从单位矩阵开始
创建任何模型矩阵,第一步几乎都是从单位矩阵开始:
text
I =
| 1 0 0 0 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
- 它相当于"什么都不做"。
- 一个物体如果还没平移、没旋转、没缩放,那么它的模型矩阵就是单位矩阵。
- 所有后续变换,都是在这个基础上叠加出来的。
1. 创建平移矩阵
如果物体要移动到 (tx, ty, tz),平移矩阵写成:
text
T =
| 1 0 0 tx |
| 0 1 0 ty |
| 0 0 1 tz |
| 0 0 0 1 |
作用:
- 不改变物体形状和朝向
- 只改变物体在世界中的位置
比如移动到 (3, 2, -5):
text
T =
| 1 0 0 3 |
| 0 1 0 2 |
| 0 0 1 -5 |
| 0 0 0 1 |
2. 创建缩放矩阵
如果物体沿 x/y/z 方向分别缩放 sx/sy/sz:
text
S =
| sx 0 0 0 |
| 0 sy 0 0 |
| 0 0 sz 0 |
| 0 0 0 1 |
作用:
- 改变物体大小
sx = sy = sz是等比缩放- 不相等时是非均匀缩放
比如放大 2 倍:
text
S =
| 2 0 0 0 |
| 0 2 0 0 |
| 0 0 2 0 |
| 0 0 0 1 |
3. 创建旋转矩阵
在 3D 中,旋转通常按坐标轴分别创建。
绕 x 轴旋转角度 rx:
text
Rx =
| 1 0 0 0 |
| 0 cos(rx) -sin(rx) 0 |
| 0 sin(rx) cos(rx) 0 |
| 0 0 0 1 |
绕 y 轴旋转角度 ry:
text
Ry =
| cos(ry) 0 sin(ry) 0 |
| 0 1 0 0 |
| -sin(ry) 0 cos(ry) 0 |
| 0 0 0 1 |
绕 z 轴旋转角度 rz:
text
Rz =
| cos(rz) -sin(rz) 0 0 |
| sin(rz) cos(rz) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
如果物体同时有多个旋转,通常组合成:
text
R = Rz * Ry * Rx
或者按你的项目约定来。这里一定要注意:
旋转顺序会影响结果Rx * Ry * Rz和Rz * Ry * Rx通常不是一个效果
4. 把它们乘起来,得到模型矩阵
假设一个物体:
- 缩放:
(2, 2, 2) - 绕
y轴旋转45度 - 平移到
(5, 0, -8)
那么模型矩阵常写作:
text
M = T * Ry * S
顶点变换时:
text
worldPosition = M * localPosition
含义是:
- 顶点先被放大
- 再围绕自身局部原点旋转
- 最后整体移动到世界中的目标位置
这就是最典型的模型矩阵创建方式。
最重要的一点:顺序为什么不能乱
矩阵乘法不满足交换律:
text
A * B != B * A
所以:
text
M = T * R * S
和
text
M = S * R * T
结果完全不同。
举个直觉例子:
T * R * S:像"先调整模型姿态和大小,再把它放到世界里"R * T * S:像"先把模型移走,再绕世界原点转",轨迹会变成绕圈
在做模型矩阵时,最常见也最符合直觉的是:
text
M = T * R * S
也就是:
- 先缩放
- 再旋转
- 最后平移
手写创建模型矩阵的思路
如果你不用库,逻辑一般是这样:
- 先创建单位矩阵
I - 根据位置创建
T - 根据旋转创建
Rx、Ry、Rz - 根据缩放创建
S - 按顺序相乘得到最终
M
伪代码:
js
const modelMatrix = multiply(
translation(tx, ty, tz),
multiply(
rotationY(ry),
multiply(
rotationX(rx),
scaling(sx, sy, sz)
)
)
);
如果还有 z 轴旋转,就继续插进去。
一个更完整的 JavaScript 示例
下面用最容易理解的方式说明,不依赖框架:
js
function degToRad(deg) {
return deg * Math.PI / 180;
}
function createIdentityMatrix() {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
}
function createTranslationMatrix(tx, ty, tz) {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1,
]);
}
function createScaleMatrix(sx, sy, sz) {
return new Float32Array([
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1,
]);
}
function createRotationXMatrix(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
return new Float32Array([
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
]);
}
function createRotationYMatrix(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
return new Float32Array([
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1,
]);
}
function createRotationZMatrix(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
return new Float32Array([
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
}
你还需要一个矩阵乘法函数:
js
function multiplyMatrix4(a, b) {
const out = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
out[col * 4 + row] =
a[0 * 4 + row] * b[col * 4 + 0] +
a[1 * 4 + row] * b[col * 4 + 1] +
a[2 * 4 + row] * b[col * 4 + 2] +
a[3 * 4 + row] * b[col * 4 + 3];
}
}
return out;
}
最后创建模型矩阵:
js
const tx = 5;
const ty = 0;
const tz = -8;
const rx = degToRad(30);
const ry = degToRad(45);
const rz = degToRad(0);
const sx = 2;
const sy = 2;
const sz = 2;
const T = createTranslationMatrix(tx, ty, tz);
const Rx = createRotationXMatrix(rx);
const Ry = createRotationYMatrix(ry);
const Rz = createRotationZMatrix(rz);
const S = createScaleMatrix(sx, sy, sz);
const R = multiplyMatrix4(Rz, multiplyMatrix4(Ry, Rx));
const modelMatrix = multiplyMatrix4(T, multiplyMatrix4(R, S));
这就是一个完整的模型矩阵构造流程。
然后怎么传给 WebGL
创建好后,传给 shader:
js
gl.uniformMatrix4fv(uModelMatrixLocation, false, modelMatrix);
顶点着色器里:
glsl
attribute vec3 aPosition;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
void main() {
gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
}
为什么有的人矩阵看起来不一样
你会发现很多教程里矩阵摆放位置不一样,这通常有 3 个原因:
- 有的人按
行向量思维讲 - 有的人按
列向量思维讲 - WebGL / OpenGL 常见的是
列主序存储和列向量乘法约定
所以你经常会看到:
- 平移量写在最后一列
- 或者写在数组最后一行位置
这不是一定谁错了,而是数学表示方式和内存排列方式不同。
真正重要的是 3 件事保持一致:
- 你的矩阵构造方式
- 你的乘法函数
- 你的 shader 乘法约定
只要这三者统一,结果就对。
推荐的实际工程思路
在真实 WebGL 项目里,通常不会每次都手写 16 个数,而是维护对象的几个基础属性:
positionrotationscale
然后每一帧重新生成模型矩阵:
js
modelMatrix = T * R * S
比如对象类里常见会这样设计:
js
class Transform {
constructor() {
this.position = [0, 0, 0];
this.rotation = [0, 0, 0];
this.scale = [1, 1, 1];
this.modelMatrix = createIdentityMatrix();
}
updateMatrix() {
const T = createTranslationMatrix(
this.position[0],
this.position[1],
this.position[2]
);
const Rx = createRotationXMatrix(this.rotation[0]);
const Ry = createRotationYMatrix(this.rotation[1]);
const Rz = createRotationZMatrix(this.rotation[2]);
const S = createScaleMatrix(
this.scale[0],
this.scale[1],
this.scale[2]
);
const R = multiplyMatrix4(Rz, multiplyMatrix4(Ry, Rx));
this.modelMatrix = multiplyMatrix4(T, multiplyMatrix4(R, S));
}
}
这样你的渲染对象就有了一个很清晰的变换系统。
父子层级时怎么创建模型矩阵
如果场景里有层级关系,比如:
- 车身是父节点
- 车轮是子节点
那车轮的变换分两层:
本地模型矩阵:车轮相对车身的位置世界模型矩阵:车轮最终在世界中的位置
计算方式通常是:
text
childWorldMatrix = parentWorldMatrix * childLocalMatrix
所以更准确地说:
- 单个物体的模型矩阵,往往是
localMatrix - 真正送入渲染管线时,经常使用它的
worldMatrix
这也是很多引擎里 matrix 和 matrixWorld 分开的原因。
旋转的一个难点:欧拉角和万向节锁
如果你用的是:
rotationXrotationYrotationZ
这种方式,本质是欧拉角旋转。它简单直观,但有两个问题:
- 旋转顺序敏感
- 可能出现
万向节锁
所以复杂项目里,有时会用:
四元数来生成旋转矩阵
但对"怎么创建模型矩阵"这个问题来说,先掌握 T * R * S 已经足够了。
最常见的错误
错误 1:把顺序写反:结果是物体围着世界原点乱转,或者缩放方向不对错误 2:角度没有转弧度:Math.sin和Math.cos要吃弧度,不是角度错误 3:矩阵乘法函数与存储约定不一致:看起来矩阵没问题,但结果完全错位错误 4:模型矩阵没每帧更新:位置变了,但画面不动错误 5:父子关系里直接把 localMatrix 当 worldMatrix:子物体位置会不对错误 6:把平移放进 3x3 里:3D 里平移通常要靠4x4齐次矩阵
怎么判断自己的模型矩阵对不对
你可以按这个顺序自查:
- 先只用单位矩阵,确认模型能画出来
- 再只加缩放,确认大小变化正确
- 再只加平移,确认位置变化正确
- 再只加单轴旋转,确认方向变化正确
- 最后再组合
T * R * S
这样最容易定位问题。
一句话记忆
- 创建模型矩阵,本质就是把物体的
平移、旋转、缩放写成矩阵,然后按固定顺序相乘。 - 最常见的工程写法是:
text
ModelMatrix = Translation * Rotation * Scale
- 它把模型从
局部空间送到世界空间。
你可以这样理解到位
S决定"模型本身有多大"R决定"模型在自己原点附近怎么转"T决定"模型整体搬到世界哪里去"
所以创建模型矩阵,就是在回答:
- 这个物体原地有多大
- 它在原地转成什么朝向
- 最后它被放到场景哪个位置