INSIDE ONE FRAME
一帧画面,16.7 毫秒
你转一下鼠标,画面刷新一次,中间只有十六点七毫秒。这段时间里引擎要读输入、解物理、找路、剔除、变换、打光、采样、压缩、发包——20 步,每一步都能指回馆里的某件展品。一帧画面用掉的数学,从公元前三世纪一直排到 1918 年,二十一位一个不落。
左边是帧内的毫秒刻度。它是示意的——真实分配随场景、显卡与引擎天差地别, 这里只用来说清顺序,以及「总共就这么多时间」。
- 0.0
鼠标动了三个像素
一帧的开始是一对数。鼠标报上来的位移、你在世界里的位置、屏幕上每个像素的地址——全都是「用一组数说清一个位置」。这件事 1637 年才有;在那之前,几何是画出来的,不是算出来的。
- 0.3
把这两个数变成一次转身
引擎不会把姿态记成三个角度:机头朝天时那三个角会塌成两个,飞机再也转不出某些方向。它存的是四个数,而且乘法不可交换——正好对应「先偏航再俯仰」和反过来本来就是两个姿势。镜头从一个角度摇到另一个角度,中间那些帧由 slerp 沿球面补出来。
- 1.1
物理:速度和位置各推一步
手雷在飞,箱子在塌。引擎要把加速度积分成速度、速度积分成位置,这正是流数法当年要算的东西。顺序还不能写反:先更新速度、再用新速度更新位置,能量才不会一路漏光。至于角动量凭什么守恒,答案是 1918 年给的——因为把整个系统转过任意角度,规则不变。
- 2.6
这一枪打中了谁
一条射线和一个球会不会相交,写下来就是一个一元二次方程:判别式小于零是没打中,两个根是进和出。九世纪那本书教的配方法,如今每秒要在显卡上跑几百万次。碰上不是球的形状,就改成迭代逼近——沿切线滑一步,再滑一步。
- 3.4
船入水了
浮力等于排开水的重量,这条两千两百年没变。难的是「排开了多少」:船体是一张三角网格,要算它水下那部分的体积,用的是把体积分换成边界上的面积分的那条定理。同一条定理还顺手给出质心和惯性张量——也就是它翻起来有多费劲。
- 4.2
敌人开始找路
关卡被切成一张图:路点是顶点,走得通的地方是边。「从这里到那里怎么走」是图论问题,而图论的第一篇论文写于 1736 年,为的是七座桥。要是改成「把所有巡逻点各走一次再回来」,那就换了一道题——那道题至今没有好办法。
- 5.0
先把看不见的扔掉
最终留在屏幕上的不到场景的百分之一,所以第一件事是剔除。要快就得让「位置相近的东西在内存里也相近」,办法是拿一条能走遍三维空间又从不乱跳的曲线给格子编号。这种曲线 1891 年就造出来了,当时纯粹是为了回答一个问题:一根线能不能填满一个正方形。
- 5.9
模型 → 世界 → 相机 → 屏幕
每个顶点要连过四套坐标系,每一次都是一个矩阵。矩阵的行列式告诉你这次变换把体积放大了几倍;要是它是负的,说明模型被镜像了,法线得跟着翻面,否则光会从物体里面照出来。行列式第一次被写下来,是在 1693 年的一封信里。
- 7.0
- 8.0
曲面其实没有曲面
显卡只会画三角形。一个球、一根管子、一张脸,都是用足够多的平面片凑出来的;远了少凑几片,近了再割细,这就是 LOD。「割之又割,以至于不可割」是三世纪写下的八个字,说的正是同一件事:边数够多,多边形与圆之间就无所失矣。
- 8.8
把三维的表面摊成一张二维的图
贴图之前要展 UV:曲面上的每个点对应图上的一个点。这样的一一对应必然存在——1878 年就证明了,线段上的点与正方形里的点一样多。可那个对应不连续,而贴图要的是连续的,于是代价躲不掉:曲面只要有弯曲,摊平就一定会拉伸,这是 1827 年那条定理的结论。美术把拉伸赶到看不见的地方,那些地方就叫接缝。
- 10.0
这个面朝不朝着光
一次光照计算的核心只是一个内积:面的法线与光的方向差多少。正因为光只看朝向,才骗得过去——法线贴图把一张平三角形的朝向逐像素改掉,眼睛就以为那里坑坑洼洼。而「朝向与弯曲可以只在面内测量、不必参照外部空间」,本身就是那门几何的起点。
- 11.0
四面八方的环境光,压成九个数
天光从每个方向来,不可能逐个方向存下来。办法是把「方向 → 亮度」这个函数摊在一组正交基上,只留前九项:低频留下,细节丢掉,反正环境光本来就柔和。这与把方波拆成正弦波是同一招;而「把函数看成无穷维空间里的一个向量」这个说法,是希尔伯特给的。
- 12.0
软阴影:往光源上撒一把点
光源不是一个点,阴影才有软边。算法是在光源表面撒若干采样点,逐个看挡没挡住,再平均——把一个积分换成有限次采样的平均值,正是当年给积分下定义的那套办法反过来用。点还不能乱撒也不能撒成方格,要彼此不许太近;这种撒法用的是另一位的名字。
- 13.0
烟雾和水花
流体每推进一步都要解一次方程,把速度场里凭空冒出来的那部分压掉——那就是泊松方程。离散到网格上,它说的是每个格子的值等于四邻的平均,再减去源项。所以一次流体解算的内循环,其实是在反复求一个调和函数。
- 14.0
泛光与景深:给画面上一层模糊
亮处发光、远处虚化,用的是同一个核:一口以像素为中心的钟。它还能拆成横一遍、竖一遍——两次一维模糊等于一次二维模糊,代价从 N² 降到 2N。这是这条钟形曲线独有的特权,换个形状就拆不开。
- 14.4
枪声、脚步、混响
声音在内存里是一串采样点,在算法眼里是一堆频率。要压缩就把人听不见的那些扔掉,要做混响就把房间的冲激响应与它卷在一起。拆和拼用的是同一把刀;而这把刀的语言——把旋转写成一个指数——出自另一位。
- 15.0
发出去,路上丢了一个包
联机对战每秒要发几十个包,丢包是常态。不靠重传也能补回来的办法,是发出去之前先掺进冗余:把数据当成有限域上一个多项式的取值,丢掉几个点,剩下的点仍然把那个多项式定死。有限域是一个二十岁的人在决斗前夜整理出来的。
- 15.6
两台机器的同一帧,为什么会不一样
锁步同步赌的是决定论:同样的初始状态加同样的输入,必然算出同样的结果——两百年前有人把这个信念推到过极端。可浮点数只有有限位,两台机器在最后一位上差了一点点,几百帧之后就是两个世界。这不是 bug,是对初值的敏感依赖,1890 年在三体问题里第一次被看清。
- 16.7
交给显示器:为什么偏偏是 60
一秒切成 60 份,掉帧时能干净地降成 30、20、15——因为 60 的因数多得离谱:2、3、4、5、6、10、12、15、20、30 全都整除得了。这种「因数特别多」的数有个名字,叫高度合成数,由一个从未受过正规训练的人在 1915 年系统研究。(60 这个数最初来自电网频率,但它这么好用,是因数说了算。)