除以深度,瞬间得到 3D
文章摘要
游戏开发者 Gabriel O’Flaherty-Chan 在 2026 年 9 月 15 日写的一篇图形学入门短文,副标题是”一窥 3D 图形中相机的工作原理”。文章的亮点在于每个概念都配了可以实时运行的着色器小例子,读者可以拖动滑块调整参数,并随时展开完整的着色器代码。
缘起。 作者回忆,刚开始做游戏时用的是高层框架,里面有一个”Camera”对象,开箱即用。可当他想尝试更有创意的技巧时,连该用什么词去搜索都不知道;直到写了更底层的图形代码,他才意识到相机背后其实是非常简单的数学。他引用了 Tsoding 推荐的一段视频《一个揭开 3D 图形神秘面纱的公式》,公式只有两行:x’ = x/z,y’ = y/z。也就是说,如果规定 y 轴朝上、z 轴朝前,三维坐标 (x, y, z) 只要把 x 和 y 都除以 z,就能投影到二维平面上。比如一组只有深度不同的点 (2,1,2)、(2,1,4)、(2,1,8),投影后分别是 (1, 0.5)、(0.5, 0.25)、(0.25, 0.125),深度越大,越靠近消失点 (0, 0)。
两个小演示。 第一个演示是一个绕相机竖轴公转的小球,它在屏幕上的位置是 xy 除以 z,半径是 0.5 除以 z,于是越远越小、越靠近中心,读者可以用滑块调节它沿 z 轴前移的距离。第二个演示用同样的原理画出一个旋转的线框立方体:8 个顶点、12 条边,每条边的两个端点先旋转、再沿 z 轴推远,然后各自除以 z 得到屏幕坐标,再用一个线段距离函数画出来。几十行着色器代码,一个有透视感的 3D 立方体就转起来了。
透视投影矩阵。 作者也承认,这些例子非常受限,只适合最简单的场景。真正做 3D 时,需要考虑相机朝向、位置、视野(FOV)等因素。理论上可以硬改这些着色器来支持,但更省力、更实用的办法是透视投影矩阵,也就是”强大的 Camera 背后的魔法”。构造这个矩阵有不同的方式,计算机视觉和图形学的约定就不太一样,各家对哪根轴朝上、朝右、朝前也没有共识,所以很难指着某一篇维基百科条目说这是通用形式。但在基于三角形的图形学里,常见做法是用视野角、宽高比、近裁剪面和远裁剪面四个参数来定义相机。这些参数还能顺便告诉你哪些东西在画面里、哪些不在,从而剔除屏幕外的几何体,只渲染可见部分。矩阵的结构大致是:第一行是 f/a,第二行是 f,第三行负责深度映射的 A 和 B,第四行把 z 搬到 w 分量上。其中焦距系数 f = 1/tan(θ/2)(θ 是垂直视野角),a 是宽高比,A = (F+n)/(F−n),B = −2Fn/(F−n),n 和 F 分别是近、远裁剪面。这一节的交互演示左边画出视锥体的等轴侧视图,右边是相机实际看到的画面,读者可以拖动深度、视野、宽高比、近远裁剪面五个滑块,观察小球何时进入或离开视锥。
与”除以深度”的关系。 作者接着说明,开头那个技巧其实就是透视投影矩阵的一个特例。一个点先从世界空间(整个场景共享的坐标系)变换到视图空间(相机位于原点),再乘以投影矩阵,得到裁剪空间坐标。把 (x, y, z, 1) 乘上矩阵,结果是 (f/a·x, f·y, Az+B, z),第四个分量 w 正好就是原来的深度 z。文中有一个可折叠的小注解释那个”多出来的 1”:4×4 矩阵需要四分量的齐次坐标,对于任意非零的 w,(x, y, z, w) 表示三维点 (x/w, y/w, z/w),所以写成 w=1 不改变原来的点;w=0 则表示方向,不受平移影响,”那是另一个话题”。
把裁剪空间前三个分量除以 w 之后,坐标就不再表示场景中的距离,而是表示点落在相机可见范围内的什么位置,这就是标准化设备坐标(NDC),之后再映射到屏幕分辨率。前面的例子省略了 f 和 a,把它们都设为 1,就恰好得到 x/z 和 y/z。完整的渲染流程于是可以写成:世界空间 → 视图空间 → 裁剪空间 →(除以 w)→ NDC → 像素 → 光栅化。
结论。 作者希望读者带走的一点是:相机只是在做几步简单的变换,只要明白每一步是干什么的,就可以根据自己的需求和限制挑选要自己实现的部分。有时你需要整条管线,有时你只需要那一次除以深度。
HN 评论精华
本帖 219 分、约 40 条评论,气氛友好。作者 gabrieloc 本人在帖子里现身,说他写了些关于 3D 相机原理的笔记,配了交互示例,希望能为这个他自己曾经困惑过的话题”去神秘化”,”就算没用,也有好玩的滑块可以拖”。dyarosla 称赞了可视化和滑块,同时建议在视锥体的侧面以及近、远裁剪面上正确地裁掉小球,虽然数学上不一定如此,但这是 3D 应用里用户期待的效果。TN1ck 特别夸了滑块在手机上的体验。anitil 说一看到标题就想起了 Tsoding 的视频,很高兴作者提到了它;throwaway219450 也贴出了那段视频,说”大约从第 7 分钟开始精彩,一切拼到一起的方式近乎魔法”。
怎么教更直观。 aappleby 分享了他上”图形学 101”短课时的做法:先说”视锥体是一个 90 度的金字塔,尖端在 z=1 处被切掉,另一端延伸到无穷远”,再推导怎样用矩阵和透视除法把它映射到 NDC,这比通常那种”这是透视矩阵的计算公式,别问这些数字从哪来”直观得多。psvv 说自己自学时,故意不设近裁剪面,看到相机背后的东西出现在前面,反而很有启发;他认为很多 3D 图形学知识都能从”除以深度”这类基础推出来,再加上”如何从一组坐标系基向量构造变换矩阵”,就不用去查透视矩阵怎么写了,可以重走早期 3D 先驱的发现之路,自己想出来的东西远比读来的记得牢。Lerc 用窗户来讲:学生很容易想象从眼睛到窗框的金字塔一直延伸出去,再想象窗外某个物体的各个角拉出绳子连到眼睛,绳子会穿过窗户,3D 图形学就是算出绳子穿过窗户的位置,把窗外的东西画在窗户上,看起来和真的一模一样。
那个”多出来的 1”引发的数学讨论。 tmoertel 认为文中关于额外的 1 的解释可以补充一点:平移不是线性变换(原点不映射到自身),而是仿射变换,所以无法用 3×3 矩阵表示三维平移。办法是把三维空间嵌入四维空间、固定在 w=1 处,这样原空间的平移就变成四维空间里的线性变换,可以用 4×4 矩阵乘法表示,从而让包括平移在内的所有常见 3D 操作都能借助现代硬件上矩阵乘法的”蛮力”。globalnode 以二维嵌入三维为例追问,sheept 解释仿射矩阵本质上是剪切变换,在三维空间中剪切 z=1 的平面就相当于在平面内平移;tmoertel 又给了一个具体推导:把平面放在 z=1,变换 f(x, y, z) = (x + s·z, y, z) 是线性的,却能把平面原点 (0,0,1) 移到 (s,0,1),平面若放在 z=0 则这个方案失效。globalnode 后来自嘲说重读时发现自己大部分都想偏了,可惜删不掉。ggambetta(《Computer Graphics from Scratch》的作者)补充说,不用齐次坐标也能做旋转和平移,向量加法本身也是线性代数,齐次坐标真正带来的是把多个变换合成一次矩阵乘法,这在古老的硬件上尤其重要,”那时候每一次乘法都要抢”。tmoertel 回应说合成线性变换不需要齐次坐标,需要齐次坐标的是把平移也纳入其中;yeoyeo42 则强调”这是线性代数,但不是线性变换”。gcr 推荐了 Szeliski 的《计算机视觉:算法与应用》,其中提到在二维齐次坐标里两个点的叉积就是过它们的直线,两条直线的叉积就是它们的交点。LPisGood 问这是不是四元数在图形学里很重要的原因,yunruse 解释了四元数的好处在于把旋转轴和角度合成一个对象,避免万向节锁,并讲了个掌故:阿波罗 11 号登月后不久,Collins 开玩笑说希望圣诞节能收到第四个万向节。
怀旧与”重新发明”。 好几位读者回忆起自己少年时代摸索出同样公式的经历。lolakutty 说 90 年代中期他还是个用 486 的少年,没有互联网也没有书,靠观察光把三维物体的影子投到墙上,自己推出了 x/z、y/z,在 14 英寸单色显示器的 QBasic 里转起一个立方体,至今记得那种兴奋;他感叹如今的孩子把电脑看成”高级电视”,不再把它当作可编程的设备。rjrodger 说 1989 年他就是这样在 ZX Spectrum 上做出 3D 线框的,”线性代数是什么?”,全凭试错,此后再没有哪次编程的快感能比得上。rhyperior 说这些东西曾经是软件圈的常识,因为 Carmack、Abrash、Hecker 等人当年在公开地探索游戏技术,现在又要有人从第一性原理重新推一遍,有点好笑。socalgal2 认为问题在于很难找到合适的简单解释,搜到的要么讲得差、要么太数学化、要么预设了太多背景,现在或许可以让 LLM 按你的水平讲解。fsloth 则说计算机图形学本来就建立在数学上,但真正需要的只是一小部分概念,比如 2×2、3×3、4×4 矩阵怎么工作,学会了受用终身;相机变换非常反直觉,最好让学生自己推导一遍。nik282000 的反应很简短:”什么?我一直以为这就是做 3D 的便宜办法。”
作品分享。 skzv 用同样的数学,配合深度图把公开的二维照片变成三维场景;JKCalhoun 写了一个在 HTML5 Canvas 上渲染平面着色多边形的老派 3D 引擎,还让 Claude 帮他排查异常,找出了一两处数学错误;IvanK_net 贴出了他的”47 行 JavaScript 写成的 3D 引擎”,只能画线段。
别忘了其他深度线索。 jmiskovic 提醒,在二维屏幕上呈现 3D,关键是给大脑足够的深度线索:近大远小是重要的一个,第二重要的是遮挡,而这些线框渲染完全跳过了遮挡;另一个是雾,也就是光穿过介质造成的影响,实现起来很简单,越远的颜色越往灰色褪。只靠遮挡和雾,甚至只靠雾,不模拟透视也能做出令人信服的 3D 效果。account42 补充了阴影的重要性,还举了一张正午太阳直射下的夏威夷街景照片为例,没有阴影的真实照片看起来也很扁平;jmiskovic 又补充了环境光遮蔽,可以看作许多微小的阴影。