INSIDE ONE FRAME

一帧画面,16.7 毫秒

你转一下鼠标,画面刷新一次,中间只有十六点七毫秒。这段时间里引擎要读输入、解物理、找路、剔除、变换、打光、采样、压缩、发包——20 步,每一步都能指回馆里的某件展品。一帧画面用掉的数学,从公元前三世纪一直排到 1918 年,二十一位一个不落。

左边是帧内的毫秒刻度。它是示意的——真实分配随场景、显卡与引擎天差地别, 这里只用来说清顺序,以及「总共就这么多时间」。

  1. 0.0

    鼠标动了三个像素

    一帧的开始是一对数。鼠标报上来的位移、你在世界里的位置、屏幕上每个像素的地址——全都是「用一组数说清一个位置」。这件事 1637 年才有;在那之前,几何是画出来的,不是算出来的。

  2. 0.3

    把这两个数变成一次转身

    引擎不会把姿态记成三个角度:机头朝天时那三个角会塌成两个,飞机再也转不出某些方向。它存的是四个数,而且乘法不可交换——正好对应「先偏航再俯仰」和反过来本来就是两个姿势。镜头从一个角度摇到另一个角度,中间那些帧由 slerp 沿球面补出来。

  3. 1.1

    物理:速度和位置各推一步

    手雷在飞,箱子在塌。引擎要把加速度积分成速度、速度积分成位置,这正是流数法当年要算的东西。顺序还不能写反:先更新速度、再用新速度更新位置,能量才不会一路漏光。至于角动量凭什么守恒,答案是 1918 年给的——因为把整个系统转过任意角度,规则不变。

  4. 2.6

    这一枪打中了谁

    一条射线和一个球会不会相交,写下来就是一个一元二次方程:判别式小于零是没打中,两个根是进和出。九世纪那本书教的配方法,如今每秒要在显卡上跑几百万次。碰上不是球的形状,就改成迭代逼近——沿切线滑一步,再滑一步。

  5. 3.4

    船入水了

    浮力等于排开水的重量,这条两千两百年没变。难的是「排开了多少」:船体是一张三角网格,要算它水下那部分的体积,用的是把体积分换成边界上的面积分的那条定理。同一条定理还顺手给出质心和惯性张量——也就是它翻起来有多费劲。

  6. 4.2

    敌人开始找路

    关卡被切成一张图:路点是顶点,走得通的地方是边。「从这里到那里怎么走」是图论问题,而图论的第一篇论文写于 1736 年,为的是七座桥。要是改成「把所有巡逻点各走一次再回来」,那就换了一道题——那道题至今没有好办法。

  7. 5.0

    先把看不见的扔掉

    最终留在屏幕上的不到场景的百分之一,所以第一件事是剔除。要快就得让「位置相近的东西在内存里也相近」,办法是拿一条能走遍三维空间又从不乱跳的曲线给格子编号。这种曲线 1891 年就造出来了,当时纯粹是为了回答一个问题:一根线能不能填满一个正方形。

  8. 5.9

    模型 → 世界 → 相机 → 屏幕

    每个顶点要连过四套坐标系,每一次都是一个矩阵。矩阵的行列式告诉你这次变换把体积放大了几倍;要是它是负的,说明模型被镜像了,法线得跟着翻面,否则光会从物体里面照出来。行列式第一次被写下来,是在 1693 年的一封信里。

  9. 7.0

    这堵墙,真的是平的吗

    你的关卡默认是欧氏空间:平行的两堵墙永不相交,绕一圈回来朝向不变,三角形内角和是 180 度。这不是自然规律,是第五公设——一条两千三百年里没人证得出来的假设。传送门关卡之所以让人晕,正是因为它偷偷违反了这一条:那里的三角形内角和不再是 180 度。

  10. 8.0

    曲面其实没有曲面

    显卡只会画三角形。一个球、一根管子、一张脸,都是用足够多的平面片凑出来的;远了少凑几片,近了再割细,这就是 LOD。「割之又割,以至于不可割」是三世纪写下的八个字,说的正是同一件事:边数够多,多边形与圆之间就无所失矣。

  11. 8.8

    把三维的表面摊成一张二维的图

    贴图之前要展 UV:曲面上的每个点对应图上的一个点。这样的一一对应必然存在——1878 年就证明了,线段上的点与正方形里的点一样多。可那个对应不连续,而贴图要的是连续的,于是代价躲不掉:曲面只要有弯曲,摊平就一定会拉伸,这是 1827 年那条定理的结论。美术把拉伸赶到看不见的地方,那些地方就叫接缝。

  12. 10.0

    这个面朝不朝着光

    一次光照计算的核心只是一个内积:面的法线与光的方向差多少。正因为光只看朝向,才骗得过去——法线贴图把一张平三角形的朝向逐像素改掉,眼睛就以为那里坑坑洼洼。而「朝向与弯曲可以只在面内测量、不必参照外部空间」,本身就是那门几何的起点。

  13. 11.0

    四面八方的环境光,压成九个数

    天光从每个方向来,不可能逐个方向存下来。办法是把「方向 → 亮度」这个函数摊在一组正交基上,只留前九项:低频留下,细节丢掉,反正环境光本来就柔和。这与把方波拆成正弦波是同一招;而「把函数看成无穷维空间里的一个向量」这个说法,是希尔伯特给的。

  14. 12.0

    软阴影:往光源上撒一把点

    光源不是一个点,阴影才有软边。算法是在光源表面撒若干采样点,逐个看挡没挡住,再平均——把一个积分换成有限次采样的平均值,正是当年给积分下定义的那套办法反过来用。点还不能乱撒也不能撒成方格,要彼此不许太近;这种撒法用的是另一位的名字。

  15. 13.0

    烟雾和水花

    流体每推进一步都要解一次方程,把速度场里凭空冒出来的那部分压掉——那就是泊松方程。离散到网格上,它说的是每个格子的值等于四邻的平均,再减去源项。所以一次流体解算的内循环,其实是在反复求一个调和函数。

  16. 14.0

    泛光与景深:给画面上一层模糊

    亮处发光、远处虚化,用的是同一个核:一口以像素为中心的钟。它还能拆成横一遍、竖一遍——两次一维模糊等于一次二维模糊,代价从 N² 降到 2N。这是这条钟形曲线独有的特权,换个形状就拆不开。

  17. 14.4

    枪声、脚步、混响

    声音在内存里是一串采样点,在算法眼里是一堆频率。要压缩就把人听不见的那些扔掉,要做混响就把房间的冲激响应与它卷在一起。拆和拼用的是同一把刀;而这把刀的语言——把旋转写成一个指数——出自另一位。

  18. 15.0

    发出去,路上丢了一个包

    联机对战每秒要发几十个包,丢包是常态。不靠重传也能补回来的办法,是发出去之前先掺进冗余:把数据当成有限域上一个多项式的取值,丢掉几个点,剩下的点仍然把那个多项式定死。有限域是一个二十岁的人在决斗前夜整理出来的。

  19. 15.6

    两台机器的同一帧,为什么会不一样

    锁步同步赌的是决定论:同样的初始状态加同样的输入,必然算出同样的结果——两百年前有人把这个信念推到过极端。可浮点数只有有限位,两台机器在最后一位上差了一点点,几百帧之后就是两个世界。这不是 bug,是对初值的敏感依赖,1890 年在三体问题里第一次被看清。

  20. 16.7

    交给显示器:为什么偏偏是 60

    一秒切成 60 份,掉帧时能干净地降成 30、20、15——因为 60 的因数多得离谱:2、3、4、5、6、10、12、15、20、30 全都整除得了。这种「因数特别多」的数有个名字,叫高度合成数,由一个从未受过正规训练的人在 1915 年系统研究。(60 这个数最初来自电网频率,但它这么好用,是因数说了算。)

数学与生活同样的读法,换成一天的钟点 →展品总览这一帧只用到了一部分——全馆 118 件都在这里 →