还在为 WebGL 中物体"近大远小"的效果感到神奇?或者苦恼于正交投影和透视投影到底该怎么选?为什么 3D 物体总被裁剪看不见?
今天这篇教程,带你彻底搞懂 WebGL 中的投影矩阵 。我们将从数学公式推导 开始,一步步拆解正交投影和透视投影的矩阵来源,再到实战代码,并详细解析每个参数的含义与调试技巧,让你不仅会用,更懂其原理。
今天这篇教程,带你彻底搞懂 WebGL 中的投影矩阵。我们从数学原理到实战代码,手把手教你区分正交与透视投影,并理解 MVP 矩阵中投影矩阵的核心作用。

左图为正交,六个面为平行四边形,右图为透视,六个面近大远小

左图为正交,六个面为平行四边形,右图为透视,六个面近大远小
1.先搞懂:为什么需要投影矩阵?
在 WebGL 中,顶点着色器输出的 gl_Position 坐标范围是 裁剪空间 (四个分量 x,y,z,w 都在 -w 到 w 之间)。但我们的世界是三维的,屏幕是二维的,投影矩阵 的作用就是:
将 3D 空间中的物体,变换到 2D 屏幕上,并决定"近大远小"的视觉感。
没有投影矩阵,你只能看到物体在 [-1,1] 范围内的正交投影,毫无立体感。投影矩阵是连接 3D 世界与 2D 屏幕的魔法桥梁。
核心优势(投影矩阵版)
-
透视投影:产生景深感,符合人眼视觉,是 3D 游戏/场景的首选。
-
正交投影:保持物体实际大小,无透视变形,适合 2D 游戏、UI 界面、工程制图。
-
GPU 统一处理:无论哪种投影,都通过 4x4 矩阵乘法完成,性能高效。
2.WebGL + 投影矩阵工作原理
投影矩阵是 MVP 矩阵(模型-视图-投影)中的最后一环:
-
模型矩阵:将物体从局部坐标变换到世界坐标(平移/旋转/缩放)。
-
视图矩阵:将世界坐标变换到观察者(相机)坐标(定义相机位置和朝向)。
-
投影矩阵 :将观察坐标变换到裁剪坐标,并定义可视范围(视景体)。
关键公式:
glsl
gl_Position = 投影矩阵 * 视图矩阵 * 模型矩阵 * 顶点坐标;
3.投影矩阵数学推导(重点!)
3.1 透视投影矩阵推导
透视投影的目标:将视景体(平截头体)映射到裁剪空间立方体 [-1,1]^3,并实现"近大远小"。
步骤 1:建立坐标系
-
相机位于原点,看向 -Z 方向(OpenGL 惯例)
-
近平面在
z = -near,远平面在z = -far -
近平面上可视范围:
[left, right] × [bottom, top]

步骤 2:透视除法原理
透视的核心是通过 z 坐标控制缩放。对于空间中一点 (x, y, z),投影到近平面上的坐标为:
text
x' = (near * x) / (-z)
y' = (near * y) / (-z)
这里 -z 是点到相机的距离,距离越大,投影坐标越小------这就是"近大远小"的数学本质。
步骤 3:构建齐次坐标变换
我们希望用一个 4x4 矩阵 M_proj 使得:
text
M_proj * (x, y, z, 1)^T = (x * near, y * near, A*z + B, -z)^T
其中 A 和 B 用于将 z 映射到 [-1,1],而 -z 作为 w 分量,为后续透视除法做准备。
根据近平面和远平面的映射条件:
-
当
z = -near时,输出z_clip = -1 -
当
z = -far时,输出z_clip = 1
代入 z_clip = (A*z + B) / (-z),得到方程组:
text
(A*(-near) + B) / near = -1 → -A*near + B = -near
(A*(-far) + B) / far = 1 → -A*far + B = far
解得:
text
A = -(far + near) / (far - near)
B = -2*far*near / (far - near)
步骤 4:考虑视野和宽高比
通常我们不直接指定 left/right/bottom/top,而是通过:
-
视野角 fov(垂直方向)
-
宽高比 aspect
关系为:
text
top = near * tan(fov/2)
bottom = -top
right = top * aspect
left = -right
代入步骤 3 的矩阵,得到最终透视投影矩阵:
text
[ 1/(aspect*tan(fov/2)) 0 0 0 ]
[ 0 1/tan(fov/2) 0 0 ]
[ 0 0 -(far+near)/(far-near) -2*far*near/(far-near) ]
[ 0 0 -1 0 ]
3.2 正交投影矩阵推导
正交投影的目标:将长方体视景体映射到裁剪空间立方体 [-1,1]^3,无透视缩放。
步骤 1:建立映射关系
视景体:[left, right] × [bottom, top] × [-near, -far](注意 Z 轴负方向)
我们需要将 x ∈ [left, right] 线性映射到 x' ∈ [-1, 1]:
text
x' = (2 / (right - left)) * x - (right + left) / (right - left)
同理 y 和 z。
步骤 2:写成矩阵形式
正交投影矩阵为:
text
[ 2/(right-left) 0 0 -(right+left)/(right-left) ]
[ 0 2/(top-bottom) 0 -(top+bottom)/(top-bottom) ]
[ 0 0 -2/(far-near) -(far+near)/(far-near) ]
[ 0 0 0 1 ]
注意 Z 轴负方向导致 -2/(far-near) 和 -(far+near)/(far-near) 的出现,确保 z = -near 映射到 -1,z = -far 映射到 1。
4.投影矩阵参数详解(调试必读)
4.1 透视投影参数
透视投影矩阵通过 mat4.perspective(fov, aspect, near, far) 创建,各参数含义如下:
| 参数 | 类型 | 含义 | 取值范围 | 调试建议 |
|---|---|---|---|---|
| fov | number(弧度) |
垂直视野角(Field of View) | 0° ~ 180°,常用 45°~60° | fov 越大,视野越广,物体越小(广角效果);fov 越小,视野越窄,物体越大(长焦效果)。fov > 120° 会产生明显畸变。 |
| aspect | number |
宽高比 = 画布宽度/高度 | 通常 >0 | 必须与 canvas 的 width/height 一致,否则画面拉伸。窗口 resize 时需重新计算。 |
| near | number |
近平面距离(相机到近平面的距离) | >0,通常 0.01~1.0 | 过小 (如 0.0001)会导致深度缓冲精度不足,引起 Z-fighting(画面闪烁);过大(如 10)会使近处物体被裁剪。 |
| far | number |
远平面距离 | > near,通常 100~1000 | 过小 会使远处物体被裁剪;过大会浪费深度精度,同样引起 Z-fighting。建议 far/near ≤ 1000 以保证深度精度。 |
调试技巧:
-
物体被裁剪:增大 far 或减小 near
-
画面闪烁/深度冲突:缩小 far/near 比值,或将 near 适当调大
-
物体太小/太大:调整 fov(增大 fov 物体变小,减小 fov 物体变大)
4.2 正交投影参数
正交投影矩阵通过 mat4.ortho(left, right, bottom, top, near, far) 创建,各参数含义如下:
| 参数 | 类型 | 含义 | 取值范围 | 调试建议 |
|---|---|---|---|---|
| left | number |
视景体左边界 X 坐标 | left < right | 范围越小,物体在屏幕上显示越大;范围越大,物体越小。类似于"缩放"。 |
| right | number |
视景体右边界 X 坐标 | right > left | 与 left 共同决定水平可视范围。通常保持 right = -left 使物体居中。 |
| bottom | number |
视景体下边界 Y 坐标 | bottom < top | 与 top 共同决定垂直可视范围。通常保持 top = -bottom 使物体居中。 |
| top | number |
视景体上边界 Y 坐标 | top > bottom | 控制垂直方向显示范围。 |
| near | number |
近平面距离 | 通常 0.1~10 | 物体必须在 near 和 far 之间才能显示。与透视投影不同,正交投影的 near/far 不影响物体大小,只决定深度测试范围。 |
| far | number |
远平面距离 | > near | 同样只影响深度测试范围。建议保持 far/near 比值在合理范围(如 1000 以内)以保证深度精度。 |
调试技巧:
-
物体被裁剪:检查物体是否在
[left,right] × [bottom,top] × [near,far]范围内 -
物体太小/太大:调整 left/right/bottom/top 的范围(范围越小,物体越大)
-
画面拉伸:检查 aspect 是否通过 left/right/bottom/top 正确反映(通常设置
right/left = aspect * (top/bottom))
5.实战:透视投影 vs 正交投影(同屏对比)
以下代码在一个页面中左右分屏展示两种投影效果,并支持鼠标拖拽同步旋转视角,让你直观感受差异。
html
const proj = mat4.create();
const aspect = width / height;
// 使用可调的 fov 参数
mat4.perspective(proj, perspFov * Math.PI / 180, aspect, 0.3, 30);
const mvp = mat4.create();
mat4.multiply(mvp, proj, view);
perspGL.gl.uniformMatrix4fv(perspGL.mvpLoc, false, mvp);
perspGL.gl.clearColor(0.08, 0.08, 0.12, 1);
perspGL.gl.clear(perspGL.gl.COLOR_BUFFER_BIT | perspGL.gl.DEPTH_BUFFER_BIT);
perspGL.gl.drawArrays(perspGL.gl.TRIANGLES, 0, 36);
6.正交 vs 透视:核心区别一览表
| 对比维度 | 正交投影 | 透视投影 |
|---|---|---|
| 视觉效果 | 物体大小不随距离变化,无"近大远小" | 近大远小,符合人眼视觉 |
| 视景体 | 长方体(平行投影) | 平截头体(锥体被切去顶部) |
| 平行线 | 始终保持平行 | 在远处交汇于灭点 |
| 数学核心 | 线性映射,无除法 | 透视除法(除以 -z)实现缩放 |
| 参数控制 | left/right/bottom/top 直接控制显示范围 | fov 控制视野广度,aspect 防拉伸 |
| 深度精度 | near/far 影响深度精度,但不影响大小 | near/far 影响深度精度,且 far/near 比值过大会导致 Z-fighting |
| 典型用途 | 2D游戏、UI界面、工程制图、CAD | 3D游戏、虚拟场景、模型展示 |
7.投影矩阵参数调优指南
透视投影参数调优
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 物体被裁剪 | near 太大 或 far 太小 | 减小 near(如 0.1),增大 far(如 1000) |
| 远处物体闪烁 | far/near 比值过大(>1000) | 减小 far 或增大 near,压缩比值 |
| 物体太小/太大 | fov 不合适 | 减小 fov 物体变大,增大 fov 物体变小 |
| 画面拉伸 | aspect 与 canvas 不匹配 | 窗口 resize 时重新计算 aspect |
正交投影参数调优
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 物体被裁剪 | left/right/bottom/top 范围过小,或 near/far 不合适 | 扩大范围,或检查物体坐标是否在范围内 |
| 物体太小/太大 | left/right/bottom/top 范围不合适 | 缩小范围物体变大,扩大范围物体变小 |
| 画面拉伸 | left/right 与 bottom/top 比例与 canvas 宽高比不一致 | 保持 (right-left)/(top-bottom) = aspect |
🎁 新手投影避坑指南
-
物体看不见:检查物体是否在 near/far 范围内。可以临时将 near 设小、far 设大来测试。
-
画面拉伸:透视投影中,aspect 必须与 canvas 的宽高比一致,并在窗口改变时更新。
-
深度冲突(Z-fighting):near/far 范围太大或物体靠太近时,表面会闪烁。尽量缩小范围,或提高深度缓冲区精度。
-
矩阵顺序 :
投影 × 视图 × 模型的顺序千万不能搞反,矩阵乘法不满足交换律。 -
透视投影 fov 陷阱 :fov 是垂直视野角,不是水平。宽屏下水平视野会自动更宽,这是正常的。
8.总结
-
投影矩阵 是 WebGL 3D 渲染的最后一步,决定了视觉效果是"近大远小"还是"保持真实大小"。
-
透视投影 通过透视除法和平截头体实现景深,正交投影 通过线性映射和长方体实现精确尺寸。
-
从公式推导可以看出,两者本质区别在于是否引入与 z 相关的除法,这也决定了它们完全不同的视觉特性。
-
参数调试时,near/far 的比值 是影响深度精度的关键,fov 和 left/right/bottom/top 分别控制视野范围。
9.完整代码,关注后可以下载。
💡 下期预告
投影矩阵学完,MVP 矩阵就完整了!下期我们将深入 光照模型,教你如何让物体拥有明暗变化、阴影和高光,彻底告别扁平感,打造真正逼真的 3D 场景。
关注我,下期手把手教你用矩阵+光照,新手也能轻松拿捏🎊!