5月28日 01:32

WebGL 中的矩阵变换有哪些?MVP 矩阵是什么?

WebGL 中的矩阵变换

3D 渲染的本质是将顶点从模型局部坐标一步步变换到屏幕像素坐标,而矩阵就是描述这些变换的数学工具。WebGL 中所有坐标变换都通过 4×4 齐次矩阵完成,理解这些矩阵的含义和组合方式是掌握 3D 图形编程的基础。

三种基本变换矩阵

平移矩阵

平移将物体沿 X、Y、Z 轴移动指定距离,是最直观的变换:

shell
| 1 0 0 tx | | 0 1 0 ty | | 0 0 1 tz | | 0 0 0 1 |

由于 3×3 矩阵无法表示平移(线性变换不包含偏移),所以必须引入齐次坐标——用 4D 向量 (x, y, z, 1) 表示 3D 点,w=1 使平移分量 tx, ty, tz 参与运算。

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 ]); }

注意 WebGL 使用列主序存储,所以矩阵在数组中的排列与数学书写不同——第一列先填,再填第二列。

缩放矩阵

沿各轴独立缩放物体大小:

shell
| sx 0 0 0 | | 0 sy 0 0 | | 0 0 sz 0 | | 0 0 0 1 |

sx = sy = sz 时为等比缩放。如果某个分量为负值,会产生镜像翻转效果——这在法线计算时需要特别处理。

旋转矩阵

旋转矩阵比平移和缩放复杂,因为绕不同轴旋转的矩阵形式不同:

绕 X 轴旋转 θ:

shell
| 1 0 0 0 | | 0 cosθ -sinθ 0 | | 0 sinθ cosθ 0 | | 0 0 0 1 |

绕 Y 轴旋转 θ:

shell
| cosθ 0 sinθ 0 | | 0 1 0 0 | | -sinθ 0 cosθ 0 | | 0 0 0 1 |

绕 Z 轴旋转 θ:

shell
| cosθ -sinθ 0 0 | | sinθ cosθ 0 0 | | 0 0 1 0 | | 0 0 0 1 |

任意轴的旋转可以通过绕三个基本轴旋转的组合实现,但更优雅的方式是使用四元数(Quaternion),它避免了万向锁(Gimbal Lock)问题,且插值更平滑。面试中如果被追问"如何避免万向锁",四元数是标准答案。

MVP 矩阵详解

MVP 矩阵是渲染管线中最核心的概念,它将顶点从模型空间一路变换到裁剪空间:

MVP = P × V × M

关键点在于乘法顺序——右侧的矩阵先作用于顶点。所以实际变换过程是:顶点先被 M 变换,再被 V 变换,最后被 P 变换。写成作用于顶点的形式就是 P × V × M × vertex

M - 模型矩阵(Model Matrix)

模型矩阵将顶点从模型局部空间变换到世界空间。它描述的是物体在世界中的摆放方式——放在哪里、朝向哪个方向、多大尺寸。

javascript
const modelMatrix = mat4.create(); mat4.translate(modelMatrix, modelMatrix, [x, y, z]); mat4.rotateX(modelMatrix, modelMatrix, angleX); mat4.rotateY(modelMatrix, modelMatrix, angleY); mat4.scale(modelMatrix, modelMatrix, [sx, sy, sz]);

模型矩阵通常是平移、旋转、缩放的组合。变换的顺序影响最终结果:先缩放再旋转再平移,和先平移再旋转再缩放,得到的是完全不同的效果。一般推荐 SRT 顺序(Scale → Rotate → Translate),即先缩放调整大小,再旋转调整朝向,最后平移到目标位置。

V - 视图矩阵(View Matrix)

视图矩阵将顶点从世界空间变换到相机空间(观察空间)。它的本质是设置观察者的视角——相机在哪里、看向哪里、哪个方向朝上。

javascript
const viewMatrix = mat4.create(); mat4.lookAt( viewMatrix, [0, 0, 5], // 相机位置(eye) [0, 0, 0], // 观察目标点(center) [0, 1, 0] // 上方向向量(up) );

理解视图矩阵的一个思路:它并不移动相机,而是反向移动整个世界。将世界坐标系的原点搬到相机位置,并旋转坐标系使相机朝向 -Z 方向。这就是为什么 lookAt 矩阵是相机变换的逆矩阵。

P - 投影矩阵(Projection Matrix)

投影矩阵将顶点从相机空间变换到裁剪空间,是 3D 到 2D 映射的关键一步。

透视投影模拟人眼近大远小的效果,是 3D 游戏和可视化的默认选择:

javascript
mat4.perspective( projectionMatrix, Math.PI / 4, // 视野角度 FOV canvas.width / canvas.height, // 宽高比 0.1, // 近裁剪面 100.0 // 远裁剪面 );

FOV(Field of View)越大,看到的范围越广,但畸变越明显;近裁剪面和远裁剪面定义了可见深度范围,超出范围的片元会被剔除。注意近裁剪面不能设为 0,否则会导致深度缓冲精度问题。

正交投影没有近大远小的透视效果,物体大小不随距离变化,常用于 2D 游戏、UI 渲染和 CAD 软件:

javascript
mat4.ortho( projectionMatrix, -2, 2, // 左右边界 -2, 2, // 下上边界 0.1, 100 // 近远裁剪面 );

坐标空间变换全流程

一次完整的顶点变换经历以下坐标空间:

shell
模型空间(局部空间) ↓ 模型矩阵 M 世界空间 ↓ 视图矩阵 V 相机空间(观察空间) ↓ 投影矩阵 P 裁剪空间 ↓ 透视除法(除以 w 分量) 标准化设备坐标 NDC(-1 到 1 ↓ 视口变换 gl.viewport 屏幕空间

其中 MVP 矩阵负责前三步变换,裁剪空间之后由 GPU 硬件自动完成透视除法和视口变换。透视除法是理解投影的关键:裁剪空间的齐次坐标 (x, y, z, w) 除以 w 后得到 NDC 坐标,正是这个除法产生了近大远小的透视效果。

顶点着色器中的 MVP 应用

glsl
attribute vec3 a_position; uniform mat4 u_mvpMatrix; void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); }

实际开发中推荐在 JavaScript 端预计算 MVP 矩阵再传入着色器,而不是在着色器中分别接收三个矩阵相乘。原因有两点:一是减少 GPU 端的计算量(顶点着色器每个顶点执行一次),二是减少 uniform 变量的数量和上传次数。

常见面试追问

Q: 为什么 MVP 的乘法顺序是 P × V × M,而不是 M × V × P?

因为矩阵作用于顶点的顺序是从右到左。P × V × M × vertex 等价于先 M 再 V 再 P,即先从模型空间到世界空间,再到相机空间,最后到裁剪空间。这是渲染管线的自然顺序。

Q: WebGL 中矩阵是行主序还是列主序?

WebGL 使用列主序。gl.uniformMatrix4fvtranspose 参数必须为 false(WebGL 1 不支持转置上传)。这意味着在 Float32Array 中,矩阵的第一列四个元素排在最前面。

Q: 齐次坐标中 w 分量的作用是什么?

w=1 表示点(受平移影响),w=0 表示方向向量(不受平移影响)。方向向量只需要旋转和缩放,如法线向量和光照方向。透视除法中 w 还承担了产生近大远小效果的关键角色。

Q: 法线变换为什么不能直接用 MVP 矩阵?

法线是方向向量不是位置向量,且存在非等比缩放时直接用模型矩阵会导致法线不再垂直于表面。正确做法是使用模型矩阵左上 3×3 子矩阵的逆转置矩阵,即 (M⁻¹)ᵀ。gl-matrix 中的 mat3.normalFromMat4 就是做这件事。

标签:WebGL