- 先用直觉回顾一下“为什么要
/w,w 大概干嘛的”,把脑子热起来; - 然后从一个最简单的 2D→1D 透视投影例子开始推,先在一维上看 w 从哪来;
- 再上升到 3D 透视,从 Camera Space 到 NDC(忽略 near/far、只看 x/y 与 z 的关系)推一遍;
- 再引入完整的 near / far,推标准的 4×4 透视投影矩阵,分析它每个元素怎么来的;
- 最后总结:投影矩阵的第几行第几列如何决定 w,w 和 z 的关系到底是什么。
全程会尽量避免晦涩的线性代数术语,但公式会写完整,你可以对着纸推一遍。
一、热身:再用超简短的大白话回顾一下“透视除法在干嘛”
在摄像机空间(Camera Space)里,一个点是(x, y, z):
- x、y:横向、纵向偏移;
- z:离相机的深度。
透视的直觉需求只有一句话:
这个点投影到屏幕上之后,
它的“屏幕坐标(x_screen, y_screen)”应该跟x/z,y/z成正比。
也就是:
x_screen ∝ x / z y_screen ∝ y / z这就是“近大远小”的本质 —— z 越大(越远),x/z 越小,投影越靠近中心,看起来越小。
问题来了:矩阵乘法是线性的,/z是非线性的,怎么统一?
答案就是:
把坐标升到四维
(x, y, z, 1),
设计一个 4×4 投影矩阵 P,
让乘完以后得到(x', y', z', w'),
然后再做一次:x_ndc = x' / w' y_ndc = y' / w' z_ndc = z' / w'通过“除以 w”去实现原来想要的 “除以 z”。
那这个 w’ 到底是怎么算出来的?
——就是投影矩阵 P 的最后一行和原始 (x,y,z,1) 点的点积。
我们下面就从最简情况开始推:让你看见 w 的来源,而不是“记住有个 w”。
二、从最简单的 2D 透视推起:一维上的 w 从哪来?
先别上来就 3D,我们先搞一个简化版:
- 只有一个二维空间:横轴 x,深度轴 z(可想成“平面世界”);
- 我们要把 (x, z) 投影到一条线(屏幕)上,只关注 x_screen;
- 透视要求:
x_screen ∝ x / z。
2.1 直接用齐次坐标来搞
我们把 2D 点 (x, z) 写成齐次坐标(x, z, 1)。
设计一个 3×3 的投影矩阵 P,使得:
[x', w']^T = P * [x, z, 1]^T 的前两维 最后 x_screen = x' / w'为简单起见,先让我们只看第一行和第三行(x’, w’),假定:
x' = a * x + b * z + c * 1 w' = d * x + e * z + f * 1我们希望:
x' / w' = k * x / z (k 是比例常数,表示最终映射屏幕的缩放)这要求对所有 x,z 都成立,推导很复杂。
但我们可以直接“聪明设计”一个简单的形式:
让 w’ 只和 z 有关,比如:
w' = z再让 x’ 只和 x 有关:
x' = x这样:
x_screen = x' / w' = x / z是不是刚好满足我们的要求?是的。
那对应的矩阵是什么?
矩阵 P 作用在(x, z, 1)上:
x' = 1 * x + 0 * z + 0 * 1 z' = 0 * x + 1 * z + 0 * 1 (我们先不管) w' = 0 * x + 1 * z + 0 * 1也就是说:
P = | 1 0 0 | | 0 1 0 | | 0 1 0 |这是个非常粗糙的例子,但它告诉你两件事:
- w’ 完全可以被设计成“等于 z”,这样
/w等价/z; - w’ 的值其实就是“投影矩阵最后一行·原始向量”的结果,是矩阵设计出来的,而不是凭空冒出来。
3D 透视投影也是同理,只是矩阵换成 4×4,事情复杂一点。
三、上到 3D 透视:先忽略 near/far,只关心横纵与 z 的关系
我们现在进入真实的摄像机空间:
- 一个点在 Camera Space 中是
(x, y, z); - 把它写成齐次坐标:
(x, y, z, 1); - 我们要构造一个 4×4 的 Projection 矩阵,使得:
ndc.x ∝ x / z ndc.y ∝ y / zNDC 是做完透视除法后的坐标:
(x_ndc, y_ndc, z_ndc) = (x', y', z') / w'3.1 先只看 x、y 与 w 的关系
我们先不管 z’,也忽略 near/far:
设 Projection 矩阵(简化版,只看和 x/w, y/w 有关的部分):
| A 0 0 0 | | 0 B 0 0 | | 0 0 ? ? | | 0 0 ? 0 |作用在(x, y, z, 1)上:
x' = A * x y' = B * y z' = ... (先不管) w' = α * z + β * 1 (由第四行决定)我们希望:
x_ndc = x' / w' = (A * x) / (α * z + β) y_ndc = y' / w' = (B * y) / (α * z + β)如果我们想“本质上像 x/z”的形式,最简单的做法是:
- 令 β = 0(w’ 不依赖常数,只依赖 z);
- 令 α ≠ 0,使得
w' = α * z;
那么:
x_ndc = (A * x) / (α * z) = (A/α) * (x / z) y_ndc = (B * y) / (α * z) = (B/α) * (y / z)这就是我们想要的形式 —— 比例 x/z、y/z,只是多了个缩放系数(A/α)和(B/α)。
这个缩放系数最终用于控制视野角(FOV)和画面宽高比(aspect),我们马上再讲。
关键点是:
w’ 可以简单设计成 “某个常数 × z”,
于是/w'这一步就包含了/z的效果。
这就是那个神秘 w 的来源之一:
Projection 矩阵第四行,让 w’ 和原始 z 挂钩。
3.2 把 FOV 和宽高比塞进 A 和 B 里
透视相机有两个重要参数:
- 垂直视野角
fovY; - 宽高比
aspect = width / height;
直观上:
- y 方向 FOV 越大,同样 z 深度下,能看到的 y 范围越广;
- x 方向和 y 方向的关系由
aspect决定。
经过一番推导(这里不展开细节,只说结果和直觉),我们会得到:
A = 1 / (tan(fovY/2) * aspect) B = 1 / tan(fovY/2)这两个值控制横、纵两个方向的“压缩程度”。
tan(fovY/2)决定了“在 z=1 的那一截平面上,能看到多高的 y”;- 取倒数就是“把这个高度映射回 [-1,1] 的范围”。
此时,我们已经有了:
x' = A * x y' = B * y w' = α * z (为了简单现在先让 α = 1,也就是 w' = z)则:
x_ndc = x' / w' = A * x / z = x / (z * tan(fovY/2) * aspect) y_ndc = y' / w' = B * y / z = y / (z * tan(fovY/2))这就是:视野角 + 透视 + 宽高比一起生效的结果。
你可以把它理解为:x/z 再除以个“窗口高度”,缩放到 [-1,1]。
此时 w’ 的来源已经非常清晰:
w’ 就是投影矩阵第四行和原始向量 (x, y, z, 1) 做点积得到的。
在这个简化里,它就是 z(或者某个常数乘以 z)。
四、加上 near / far:完整透视投影矩阵是怎么来的?
刚才我们只关心了 x/z、y/z,现在要把 z(深度)也搞清楚。
真实的透视投影还关心:
- 近平面
near = n; - 远平面
far = f。
我们期望:
- 在 Camera Space 中,z = n 的点映射到 NDC 的某个值(OpenGL 是 -1,DirectX 是 0);
- z = f 的点映射到另一个值(OpenGL 是 1,DirectX 是 1);
- 中间是某种“非线性插值” —— 这就是深度缓冲为什么近处密、远处稀的原因。
为了简单起见,我先以OpenGL 标准右手系、z∈[-1,1]的透视矩阵为例推一遍大致结构:
最终目标形式(OpenGL 常见):
P = | A 0 0 0 | | 0 B 0 0 | | 0 0 (f+n)/(n-f) 2fn/(n-f) | | 0 0 -1 0 |这里:
- A = 1 / (tan(fovY/2) * aspect)
- B = 1 / tan(fovY/2)
看 w 行:(0, 0, -1, 0)
作用在(x, y, z, 1)上:
w' = 0*x + 0*y + (-1)*z + 0*1 = -z这就是OpenGL 透视矩阵中的 w 来源,它就是-z。
再看 z 行:(0, 0, (f+n)/(n-f), 2fn/(n-f)):
z' = (f+n)/(n-f) * z + 2fn/(n-f) * 1透视除法后:
z_ndc = z' / w' = [ (f+n)/(n-f) * z + 2fn/(n-f) ] / (-z)整理一下:
z_ndc = - (f+n)/(n-f) - 2fn/[(n-f) * z]你会发现这是跟 1/z 有关的非线性函数,这就是深度的非线性分布来源。
现在我们重点看 w’ 的部分即可:
在这个 OpenGL 透视矩阵里,w’ 始终等于 -z(Camera Space 中的 z 深度的相反数)。
所以:
- x_ndc = x’ / w’ = (A*x) / (-z)
- y_ndc = y’ / w’ = (B*y) / (-z)
注意 OpenGL 的 Camera Space 是看向 -Z,z 是负值,用 -z 相当于用深度的绝对值。
换个坐标系(比如 DirectX 左手系看 +Z),符号会变,但“w 和 z 强相关”这件事不会变。
你可以记住一句特别关键的话:
对于标准的透视投影矩阵,w’ 要么等于 z,要么等于 -z,要么等于乘了个常数的 z。
所以透视除法本质上就是:x_ndc ≈ (某个常数) * x / z y_ndc ≈ (某个常数) * y / z那个“某个常数”里藏着 FOV 和 aspect 的信息。
五、再用 DirectX 风格(0~1 深度)的矩阵看一眼 w 的来源
为了让你对照两个风格,再看一个常见的 DirectX 左手系透视矩阵(z∈[0,1]):
一个典型版本是(看+Z):
P = | A 0 0 0 | | 0 B 0 0 | | 0 0 f/(f-n) 1 | | 0 0 -n*f/(f-n) 0 | ← 注意这里很多教材写法不同,我这里给的是一种常见变种更常见的 Unity / D3D 风格(简化一下写):
Unity 官方文档里(左手坐标)Projection 矩阵的第三、四行经常是:
[ 0 0 1 0 ] // 这里行列顺序要注意具体实现,我用的是“列主序” vs “行主序”略有差别 [ 0 0 ? 0 ]因为不同引擎内存布局和约定有所不同,具体符号你不用死记,只抓大框架:
- 最后一行(决定 w)的那行,一定是:
“某个常数 × z + 某个常数 × 1”,
但在大多数标准透视矩阵里,都是简单到:
“w’ = z” 或 “w’ = -z”。
这一点是核心:w 直接取自摄像机空间 z。
六、把全过程拉直:从 (x, y, z, 1) 到/w的显式公式
我们以 OpenGL 标准右手系透视矩阵为例,完整写一遍:
投影矩阵 P:
P = | A 0 0 0 | | 0 B 0 0 | | 0 0 (f+n)/(n-f) 2fn/(n-f) | | 0 0 -1 0 |顶点在摄像机空间:v = (x, y, z, 1)。
6.1 乘投影矩阵:v’ = P * v
得到 Clip Space 坐标(x', y', z', w'):
x' = A * x y' = B * y z' = (f+n)/(n-f) * z + 2fn/(n-f) * 1 w' = -1 * z注意:就是这么直接,w’ 就是 -z。
6.2 透视除法:NDC = v’ / w’
x_ndc = x' / w' = (A * x) / (-z) y_ndc = y' / w' = (B * y) / (-z) z_ndc = z' / w' = [ (f+n)/(n-f) * z + 2fn/(n-f) ] / (-z)你可以看到:
- x_ndc 与 x/z 成正比;
- y_ndc 与 y/z 成正比;
- z_ndc 是一个关于 1/z 的函数(非线性深度)。
所以:
那个神秘的 w 分量,在 OpenGL 标准透视矩阵中,就是 Camera Space z 的相反数:w’ = -z。
它一半参加裁剪(Clip),一半参加透视除法,/w'这一步真正完成了“近大远小”。
在 DirectX / Unity 风格下,你会看到有的实现是w' = z而不是-z,
只是坐标系和矩阵的符号习惯不同,但核心关系不变:w ~ z。
七、再把问题原话完全对上:
“那个神秘的 w 分量具体数值来自哪里?具体数学推导是什么?”
现在可以给你一个高度精炼、但对应你问题的答案:
7.1 w 的数值来自哪里?
从数学上说:
w’ = (投影矩阵的第 4 行) · (原始齐次坐标向量)
对点
(x, y, z, 1)来说:w' = p40 * x + p41 * y + p42 * z + p43 * 1在标准透视投影矩阵的设计中,这一行通常是:
[0, 0, ±1, 0]于是:
w' = ± z从直觉上说:
w 就是 Camera Space 中 z 深度的一个线性函数(常常就是 z 或 -z),
这样/w就变成/z,实现透视缩放。
7.2 数学推导的主干逻辑是什么?
目标:我们想要最终的 NDC 满足
x_ndc ∝ x / z y_ndc ∝ y / z用齐次坐标:
把原始点写成(x, y, z, 1),用矩阵 P 变成(x', y', z', w')。
NDC =(x'/w', y'/w', z'/w')。为了让
x'/w'出现x/z的形式,我们设计:w’ 跟 z 成比例,即
w' = α * z;
x’ 跟 x 成比例,即x' = A * x;
同理 y’ 跟 y 成比例。这样:
x_ndc = x'/w' = (A/α) * (x / z) y_ndc = y'/w' = (B/α) * (y / z)再根据 FOV 和 aspect 的几何关系,确定 A、B 的具体值:
A = 1/(tan(fovY/2) * aspect)
B = 1/tan(fovY/2)再根据希望 z_ndc 在 near, far 映射到 -1/1 或 0/1 的要求,
推出 P 的第三行(z’ 的线性组合),得到完整的四行四列。
整个推导过程中的“关键一步”就是:
我们强行把 w’ 设计成 z 的线性函数(最简单是 w’=z 或 w’=-z),
这样透视除法/w就自然而然变成了/z的透视缩放。
八、最后用非常粗暴的几句话,帮你把这套东西永久刻进脑子里
w 是谁?
- 它不是凭空冒出来的,它就是:
“Projection 矩阵最后一行 × (x, y, z, 1)” 的结果;
- 在常规透视矩阵里,这一行是
[0, 0, ±1, 0],
所以 w’ = ±z。
- 它不是凭空冒出来的,它就是:
为什么要
/w?- 因为我们想要
/z,但矩阵乘法不会自动带/z; - 把“深度 z”灌进 w’,之后统一
/w',就等价/z了。
- 因为我们想要
透视除法的公式就是:
v_clip = P * v_camera // 得到 (x', y', z', w') v_ndc = v_clip / w' // (x'/w', y'/w', z'/w')在标准透视矩阵中,
w' = z或w' = -z,
所以:x_ndc = const * x / z y_ndc = const * y / z投影矩阵的推导思路:
- 先从平面几何出发,确立:FOV、aspect 决定 A、B;
- 再确定 near、far 映射到 z_ndc 的目标范围;
- 设计 z’ 的线性表达;
- 把 w’ 定成与 z 线性相关;
- 最后得到完整的 4×4 矩阵。
所以,w 其实一点也不神秘:
- 它就是“为了方便 GPU 用矩阵乘法做透视,把深度 z 暂存到第四个分量里”;
- 透视除法
/w就是“把 z 这层透视账结清”的那一下; - 你看到的那一行
ndc = clipPos / clipPos.w,
就是全世界图形学工程师共同约定俗成的:“好,现在咱们把透视真的做出来吧”。
如果你愿意,你甚至可以在纸上写一个简单的 Projection 矩阵(比如 OpenGL 那个),
取几组 (x,y,z),自己算一遍P * (x,y,z,1),再除以 w,
你会非常直观地看到:点越远,结果越靠近中心,
同时 w 每次真的就是 z 或 -z,这样 w 的来源就不再是“玄学”,而是能算的数。