INSIDE ONE FRAME
一帧画面,16.7 毫秒
你转一下鼠标,画面刷新一次,中间只有十六点七毫秒。这段时间里引擎要读输入、解物理、找路、剔除、变换、打光、采样、压缩、发包——21 步,每一步都能指回馆里的某件展品。一帧画面用掉的数学,从公元前四世纪一直排到二十世纪,120位一个不落。
左边是帧内的毫秒刻度。它是示意的——真实分配随场景、显卡与引擎天差地别, 这里只用来说清顺序,以及「总共就这么多时间」。
- 0.0
鼠标动了三个像素
一帧的开始是一对数。鼠标报上来的位移、你在世界里的位置、屏幕上每个像素的地址——全都是「用一组数说清一个位置」。这件事 1637 年才有;在那之前,几何是画出来的,不是算出来的。
- 0.3
把这两个数变成一次转身
引擎不会把姿态记成三个角度:机头朝天时那三个角会塌成两个,飞机再也转不出某些方向。它存的是四个数,而且乘法不可交换——正好对应「先偏航再俯仰」和反过来本来就是两个姿势。镜头从一个角度摇到另一个角度,中间那些帧由 slerp 沿球面补出来——它每一帧都要现算两个正弦,而数学库算正弦不查表,是把它展成一串幂次逐项加出来的:这条式子在十四世纪的喀拉拉就已经写下了。不过真正跑在芯片里的并不是那串级数:要把正弦压进四五项还保证误差够小,用的是「让最大偏差最小」的那一套——最好的那条多项式,标志是误差在整段区间上等幅地上下交替。而「多项式总归逼得上去」这件事本身,是 1885 年才被证成定理的:闭区间上的任何连续函数,都能被多项式一致逼近到你要的精度。这条线的起点在更早:喀拉拉那串级数是注在一本五世纪的书上的,而那本书里已经有一张二十四个值的半弦表,外加一条造表的差分规则——把正弦的二阶变化说成正比于它自己。表格与表格之间怎么补点,是另一件独立的事:唐代的历算碰上了它最别扭的一种——要补的两段本身不一样长,而补出来的那条曲线还得保证积回去分毫不差。镜头沿着一串关键帧移动时,位置在帧与帧之间怎么补,引擎里常用三次埃尔米特样条:每个关键帧上既给位置、也给速度,两段在接缝处连一阶导数也对得上,镜头不会在经过关键帧时顿一下——「连导数一起插值」是 1878 年一封信写成一条公式的。要是这一下转身来自手柄或头显,那两个数还得先滤一遍:陀螺仪会慢慢漂,加速度计会抖,两路读数按各自的不确定度加权揉成一个姿态,常用的正是 1960 年那个每来一个读数只改一次的递推。每一帧姿态只转一点点:引擎把角速度乘上帧长,当成一个无穷小的转动,再「重复无穷多次」取指数——一个连续的群由挨着恒等变换的那一小步决定,是 1874 年一篇十四页的短文里的话。反过来,要从位移算回一个角度就得求反正切,而它的级数在自变量大于 1 时根本不收敛;数学库的办法是先换成 π/2 减去倒数的那一个——1836 年一位中学教师正是拿这一对当例子,说明同一个超几何函数换一个自变量写,收敛就换一副样子。
- 1.1
物理:速度和位置各推一步
手雷在飞,箱子在塌。引擎要把加速度积分成速度、速度积分成位置,这正是流数法当年要算的东西。顺序还不能写反:先更新速度、再用新速度更新位置,能量才不会一路漏光。这个次序为什么管用,要从真实的运动说起:不加阻尼时,一团状态在(位置,速度)平面上怎么变形都行,面积却一点不变——1838 年有人证明了那个行列式恒等于 1;一加阻尼,面积就按指数缩小。而「这一刻走多快」这个问法本身也是有来历的:把一段时间里走过的距离除以那段时间,得到的是平均速度;要问此刻,就得把那段时间取到极短——十二世纪的印度历算为了算行星位置,已经把这两件事分开命名了。三百年后的撒马尔罕反过来问同一件事:表上每十天才有一行,中间那九天的速度怎么摊——答案是一列等差,公差由「摊出来的和要恰好等于表上那一段」定死,而且附了一条能当场验算的判据,与今天两个关键帧之间补间是同一个问题。更早的一张差分表在元代:1303 年一部书把「每天招多少兵」逐列相减,减到第四次为零,再拿每列的头一个数各乘一个垛积数相加,一次算出十五天的总数——今天补帧与外推里的前向差分公式,就是这张表。布娃娃的每一个关节、每一次接触都是一条约束,求解器这一步解的是「在满足约束的前提下让什么最小」——算出来的那个数就叫乘子。这一招的前身在 1743 年:把每一块「本来要怎么动」与「实际怎么动」之差单拿出来,要求这些差在约束上相互抵消,动力学就成了静力学。至于角动量凭什么守恒,答案是 1918 年给的——因为把整个系统转过任意角度,规则不变。箱子在空中翻滚的那几帧,引擎推的是欧拉写下的刚体转动方程;而这组方程一旦加上重力矩、再把一个点钉住,难度就换了个量级——能整个解出来的构形,一百多年里只找到三种,最后那一种是 1888 年找到的。至于这一步到底该怎么推:一步里多算几次斜率,就能换到更高的阶,这笔账 1901 年被算成了一组代数条件——四个函数值最多买到四阶,而那族里唯一只用四个的那条公式,一百多年来一直印在数值分析课本的同一页上。游戏引擎多半不买这笔账:它每帧只算一次力,图的是省和稳。手雷飞出去的那条弧,在 1537 年一本给炮手的书里被画成三段——先直线、再一段圆弧、最后竖直落下,作者还用一道二次方程算出了第一段有多长;九年后他自己改口:除了竖直上下,炮弹连一步也不走直线。每帧都让重力推一下速度,正是这句改口的意思。箱子与箱子碰没碰上,引擎问的常常是另一个形状:把 A 里每一点减去 B 里每一点,得到的仍是一个凸形,两块碰上当且仅当这个差里含着原点——GJK 算法就这么判。「两个形状逐点相加」这种运算,是二十世纪初拿来重新定义面积与表面积的。
- 2.6
这一枪打中了谁
一条射线和一个球会不会相交,写下来就是一个一元二次方程:判别式小于零是没打中,两个根是进和出。九世纪那本书教的配方法,如今每秒要在显卡上跑几百万次。换成甜甜圈、换成一根弯管,次数就升上去了:三次的公式十六世纪才解开,而且带着一个怪处——三个根全是实的时候,它偏要你先绕一趟虚数。显卡上不绕这一趟,用的是把方程换成一个角的那种解法,一个反余弦就把根报出来了。再不行就改成迭代逼近——沿切线滑一步,再滑一步。而「射线碰上一个锥面」截出来的,正是椭圆、抛物线、双曲线三者之一——聚光灯的光锥打在墙上是哪一条,两千两百年前就分好了类。求交这件事本身还有个更早的说法:十一世纪就有人指出,方程的根就是两条曲线的交点——只是那时是拿它来解方程,如今反过来,是拿方程去求交点。这一路能走通,还欠着更早的一步:要对多项式本身动手术——配方、约分、开方——得先承认它是一个可以这么摆弄的对象。十一世纪的巴格达做的正是这件事,《算术之妙》里连「一个多项式怎么开平方」都写了出来。顺带还有一件今天写着色器的人天天在做的事:同一个几何条件,换一种说法就化成另一个方程——十三世纪杭州那本讲田亩的书里,同一块八百六十四步的田被换着问了八遍,八个方程各配一档解法,练的正是「怎么把话化成方程」。求交还有一步更早的判断:两条多项式方程有没有公共根,不必先解出来——1840 年有人把两组系数错开排成一个方阵,它的值为零就有;今天求两条参数曲线的交点,常常先算的就是这个方阵。
- 3.4
船入水了
浮力等于排开水的重量,这条两千两百年没变。难的是「排开了多少」:船体是一张三角网格,要算它水下那部分的体积,用的是把体积分换成边界上的面积分的那条定理。同一条定理还顺手给出质心和惯性张量——也就是它翻起来有多费劲。另一条路是按层切:吃水线以下每隔一点取一个横截面,面积乘厚度累加起来就是体积——依据是五世纪那句「幂势既同,则积不容异」,两个立体每一层的截面积都相等,体积就必然相等。浮筒、油桶、轮胎这类绕轴转出来的形状还有一条近路:体积等于截面积乘以截面重心转过的那一圈,截面长什么样都不用管——这句话四世纪就写在亚历山大里亚的一部书里。切得粗了差一点怎么办,有一个省力的办法:1712 年刊行的一部书把一个球切了三遍,厚度一次次减半,拿三个结果的差外推一步,差的那一点整个补齐,一分不差。
- 4.2
敌人开始找路
关卡被切成一张图:路点是顶点,走得通的地方是边。可走的地面本身是一片片多边形铺出来的,而「这个角色此刻站在哪一片里」每帧都要判一次——判法是从他脚下向外发一条射线、数它穿过那片多边形的边几次,奇数就在里面;这句话是 1887 年为证明「一条闭曲线把平面分成内外两块」写下的。「从这里到那里怎么走」是图论问题,而图论的第一篇论文写于 1736 年,为的是七座桥。要是改成「把所有巡逻点各走一次再回来」,那就换了一道题——那道题至今没有好办法。至于「哪一种办法更快」这句话本身凭什么有意义:两条增长曲线最终一定分得出高下,绝不会你追我赶永不见分晓——这是 1910 年才被写成定理的,而它有时要到一个七百万位的数那里才兑现。「最短的那条路」本身也可以写成一个线性规划:把每条边上走不走当成运量,起点运出一件、终点收进一件——1963 年丹齐格那本书第 17 章就把它放在「网络与转运问题」一章里、用单纯形法来解。
- 5.0
先把看不见的扔掉
最终留在屏幕上的不到场景的百分之一,所以第一件事是剔除。要快就得让「位置相近的东西在内存里也相近」,办法是拿一条能走遍三维空间又从不乱跳的曲线给格子编号。这种曲线 1890 年就造出来了(皮亚诺先写出算式,第二年希尔伯特给了图),当时纯粹是为了回答一个问题:一根线能不能填满一个正方形。剔除本身则是一道是非题连着一道是非题:一个物体在不在视锥里,等于六个半空间判断全为真,而「把『且』写成乘法、把是非写成 0 和 1」是 1847 年定下来的规矩,显卡一帧要做上百万次。还有一半三角形不必等到视锥:背对着相机的那一面反正看不见。判法只要几次乘减——把三角形投到屏幕上,拿两条边做外积,带号的面积为正是正面、为负是背面。「面积带着正负号,因子一换号就反」,是 1844 年一本几乎没人读的书在序言里就讲明了的。
- 5.9
模型 → 世界 → 相机 → 屏幕
每个顶点要连过四套坐标系,每一次都是一个矩阵——引擎不会让顶点挨个过四道关,而是先把四个矩阵乘成一个,再拿这一个去乘几十万个顶点。「一方块数可以像一个量那样相乘,只是次序不能换」,这句话是 1858 年一位在伦敦做律师的数学家写下的。矩阵的行列式告诉你这次变换把体积放大了几倍;要是它是负的,说明模型被镜像了,法线得跟着翻面,否则光会从物体里面照出来。反过来,要把一个模型的体积恰好放大一倍,缩放矩阵对角线上填的不是 2,而是 2 的立方根 1.259921——公元前三世纪有人为这个数造过一件器具,几块板在轨道里推到四点共线,就把它夹了出来。行列式第一次被写下来,是在 1693 年的一封信里。矩阵里那些 sin 与 cos 则更老:它们最早是一张表,半度一行,公元二世纪在亚历山大里亚造出来的。而「负数本身算不算数」这件事,定下来比它们都难——628 年有人用「财产」与「债」讲清了正负相乘,负负得正从此有了规矩。关卡编辑器里并排的顶视、前视、侧视几个窗口则是另一种投影:不透视,只往互相垂直的几个面上垂直地投——一个立体用两张这样的图就完全定下来,同一点在两张图上落在一条垂直于折线的线上,这是 1795 年在巴黎一所新办的师范学校里当成一门课来教的。
- 7.0
这堵墙,真的是平的吗
你的关卡默认是欧氏空间:平行的两堵墙永不相交,绕一圈回来朝向不变,三角形内角和是 180 度。这不是自然规律,是第五公设——一条两千三百年里没人证得出来的假设。有人还为它找过一个只花半页纸的理由:两角夹一边,三角形就定了,所以第三个角只能是前两个角的函数,因为那条边是长度、不是数,进不了角的式子——这个论证 1794 年写在一本当了一百年课本的几何书里,而它恰好等于假定「等比放大不改变形状」。传送门关卡之所以让人晕,正是因为它偷偷违反了这一条:那里的三角形内角和不再是 180 度。而「不再是 180 度」差多少并不是随便定的:差掉的那一点正比于三角形的面积,1829 年就算清楚了。这件事对做关卡的人是有用的——在负曲率的空间里,同样的角度对应的边长是定死的,你没法把一张地图等比放大,空间本身给了你一个绝对的尺度。顺带一条给引擎写墙的人看的:古人早就想过把「平行」定义成「处处等距」,听着比「永不相交」实在得多;可在这样的关卡里,一排等长的垂线,顶端连起来根本不是一条直线,它比脚下那条长出 cosh 倍。
- 8.0
曲面其实没有曲面
显卡只会画三角形。一个球、一根管子、一张脸,都是用足够多的平面片凑出来的;远了少凑几片,近了再割细,这就是 LOD。「割之又割,以至于不可割」是三世纪写下的八个字,说的正是同一件事:边数够多,多边形与圆之间就无所失矣。要把这句话变成一条经得起查的定理,办法比它别扭:不能说多边形最后变成了圆,只能说两者之差压得比任何给定的量都小——公元前四世纪的走法就是这样,而引擎判断「割到几片够了」用的正是同一个念头:投到屏幕上的差额小于半个像素,就不必再割。至于该割多细,还要看它在屏幕上占多大:离得远一倍,面积只剩四分之一,这条「线度之比的平方」就是当年那条圆与圆之比的定理。而「差额小于半个像素」量的其实是弦与弧中间那一截的高——今天叫弦高,古人叫矢:十一世纪有人给过一条由弦与矢求弧长的近似,弧约等于弦加上两倍矢的平方除以直径,矢越小越准。美术把一个模型减面之后,要量减掉的那一版离原版差多少,常用的是两个曲面之间「各取最远、再取较大者」的那个距离——它是 1914 年为任意两个点集定下的。
- 8.8
把三维的表面摊成一张二维的图
贴图之前要展 UV:曲面上的每个点对应图上的一个点。这样的一一对应必然存在——1878 年就证明了,线段上的点与正方形里的点一样多。可那个对应不连续,而贴图要的是连续的,于是代价躲不掉:曲面只要有弯曲,摊平就一定会拉伸,这是 1827 年那条定理的结论。美术把拉伸赶到看不见的地方,那些地方就叫接缝。而「拉伸了多少」并不是凭感觉说的:把展开看成一个映射,每一小块面积被放大的倍数就是它的行列式,引擎里那张红蓝相间的「纹理密度图」量的正是这个数。那么摊法该怎么挑?常用的一招是把「拉伸」写成一个能量,在边界钉死的所有摊法里去找让它最小的那一张——「在边界值相同的函数里取能量最小的那个」这句话来自十九世纪的位势论,当年还因为「最小值凭什么一定取得到」出过一场风波。另有一条更讲究的路:既然保角摊平做不到,就退一步要求「每一个无穷小的圆都被拉成形状一样的椭圆」,而这句话正好是一个方程——1867 年有人为了把复变函数搬到任意曲面上写下了它,今天的低失真展开与医学影像里的皮层摊平解的都是它。还有一件事只有摊过 UV 的人会遇到:把一块贴图镜像着用,那一片的切线空间就换了手性,所以引擎里每个顶点的切线都要多带一个 ±1 的分量,专门记「这里是不是反的」。手性能不能全局说清,本来就是曲面的一条性质——有的曲面上根本说不清,1882 年有人缝出过一个这样的面。切线本身也有一处躲不掉的麻烦:给一个球形的模型在每一点配一条连续变化、处处不为零的切线,是做不到的——1912 年那条「偶数维的球梳不平」说的就是这个,所以球上的切线场总得留一两个奇点,美术把它们藏在看不见的两极。反过来,有一样东西无论怎么摊、怎么揉都不变:一整块封闭的网格,把每个顶点处的曲率加起来,像球那样的总是 4π,带一个洞的总是 0。1944 年有人证明这件事在任意偶数维都成立,证法是在面上铺一个向量场,借斯托克斯公式把曲率全赶到场的零点上。
- 10.0
这个面朝不朝着光
一次光照计算的核心只是一个内积:面的法线与光的方向差多少——而「一个粗糙表面看上去有多亮,正比于这个夹角的余弦」是 1760 年那本把「亮」第一次变成可量之物的书里的定律,所以引擎里那种不反光的漫射面至今叫朗伯面。正因为光只看朝向,才骗得过去——法线贴图把一张平三角形的朝向逐像素改掉,眼睛就以为那里坑坑洼洼。而「朝向与弯曲可以只在面内测量、不必参照外部空间」,本身就是那门几何的起点。镜面高光往哪个方向亮,按的是入射角等于反射角——公元一世纪就有人证明,这正是光能挑的最短的路。遇上玻璃和水就不止朝向了:光要在界面上拐一下,拐多少由两边的折射率定。这条定律不是从「路最短」推出来的,是从「最省时间」——1662 年有人这么算,理由是自然界不做无用功。今天渲染一块玻璃,逐像素算的就是这一条。那个内积本身也有来历:两条边与夹角定住第三条边,这件事《原本》第二卷早写成了面积,十五世纪的撒马尔罕才把它压成一条能直接代数字的式子——今天由两个向量求夹角,用的正是它的反解。真正的光还会在墙与墙之间来回弹:每弹一次都打一个折扣(表面的反射率小于 1),所以把「多弹一次」一遍遍算下去,一定收敛到唯一的一幅画面——这正是 1920 年那篇论文里「每一步都缩一截」的那条定理,同一篇还把这类方程的解写成了一个逐项衰减的级数,也就是今天全局光照按弹射次数展开的那一串。
- 11.0
四面八方的环境光,压成九个数
天光从每个方向来,不可能逐个方向存下来。办法是把「方向 → 亮度」这个函数摊在一组正交基上,只留前九项:低频留下,细节丢掉,反正环境光本来就柔和。这与把方波拆成正弦波是同一招;而「把函数看成无穷维空间里的一个向量」这个说法,是希尔伯特给的。
- 12.0
软阴影:往光源上撒一把点
光源不是一个点,阴影才有软边。算法是在光源表面撒若干采样点,逐个看挡没挡住,再平均——把一个积分换成有限次采样的平均值,正是当年给积分下定义的那套办法反过来用。而这个平均值凭什么靠得住,是一条 1713 年写下的定理:撒得越密,平均值越贴住那个真正的期望。「撒点」这件事本身还有一层麻烦:显卡里的随机数是算出来的,不是真随机——第一个这么干的人自己说过,用算术造随机数字的人「都处在犯罪状态」,因为这种数迟早会开始打转。点还不能乱撒也不能撒成方格,要彼此不许太近;这种撒法用的是另一位的名字。这里还藏着一件与直觉相反的事:前面那条 1713 年的定理管的是彼此独立的采样,而引擎里真正在用的几种撒法——低差异序列、按上一条光路改一改再试的那一类——相邻两次是有意做成相关的。相关了还算不算数?算,1906 年有人专门为此造了一串首尾相连的试验来证明;变的只是「要撒多少点才够」,那个倍数由相邻两次咬得有多紧定。还有一种撒法不必挨个检查距离:每放一个点就转过同一个角,137.5 度,转出来的螺旋永远不会自己叠成行——那个角是把一整圈按黄金分割切开得到的,而黄金分割正是那串兔子数列相邻两项之比一上一下、越夹越紧要去的地方。至于「最均匀」到底长什么样:同样大的圆在平面上排成六角格时最省地方,填充率 90.69%,比排成方格的 78.54% 高出一截——这个问题最早是 1611 年一本讲雪花的小册子问出来的。还有一问排在这一切后面:一串数到底「够不够乱」,怎么说?1965 年给出的答案不问它从哪个分布里来,只问能把它印出来的最短程序有多长——压得动的就不算乱,而长度一定的串里绝大多数一点也压不动。画面上的噪点,其实就是这个平均值的方差;同样一批采样,换一种算法去估,方差可以差出一截——1920 年代有人算过,估一批数有多散,拿平均偏差去估只用上了约八成八的信息,八百个样本只抵另一种办法的七百个。反过来的问题也在这里:撒了 n 个点、挡住 k 个,真正的遮挡比例落在哪个范围里、有多大把握——够不够就此收手,还是再撒一批?这是 1763 年一篇遗稿问的问题,用一张桌子和两只球讲清的。
- 13.0
烟雾和水花
流体每推进一步都要解一次方程,把速度场里凭空冒出来的那部分压掉——那就是泊松方程。离散到网格上,它说的是每个格子的值等于四邻的平均,再减去源项。所以一次流体解算的内循环,其实是在反复求一个调和函数。这个循环什么时候算完?代码里写的是「两次迭代之差小于一个阈值就停」——那一行正是 1821 年那条判据的工程版本:不必知道极限是什么,也能断定它收敛。解出来的压强场还有一条直觉可以对照:稳定地流着的水里,流得快的地方压强低——1738 年一本书先把这件事算清楚,它问的是一只大桶接一根管子,管口开得越大、管里流得越快,管壁就受得越轻,细到一定程度甚至往里吸。烟雾画出来还要再做一件事:沿每一条视线把密度加起来,算透过去还剩多少光。CT 机做的是同一个积分的反方向——量到的是每条直线上的积分,要求的是密度本身;1917 年有人证明,一个平面上所有直线的这种积分拿全了,密度就定得出来。
- 14.0
泛光与景深:给画面上一层模糊
真实的镜头为什么有景深?因为孔有大小:一个点穿过孔落下的是一块斑,孔越大斑越大——一千年前在暗室里做实验的人就记下过,墙上光斑的大小随孔而定。游戏里的相机本来是一个没有大小的针孔,处处清楚,要虚化就得自己补一道模糊。亮处发光、远处虚化,用的是同一个核:一口以像素为中心的钟。它还能拆成横一遍、竖一遍——两次一维模糊等于一次二维模糊,代价从 N² 降到 2N。这是这条钟形曲线独有的特权,换个形状就拆不开。落到像素上要的是一排整数权重,最省事的来源是那张三角形的某一行:1、4、6、4、1。行数往下走,它就越来越像那口钟——二项分布趋于正态,这件事 1733 年就写下来了,写在一份七页的拉丁文小册子里,只在朋友间传阅;那也是这条钟形曲线已知最早的一次露面。先模糊一遍再模糊一遍,半径不是相加、是平方相加——方差按次数累加,这正是布朗运动的一步一步:走了多久,方差就是多少。也正因为每一小步的平方加起来不趋于零,在布朗轨道上求导时要比链式法则多留一项。
- 14.2
色调映射:把一万倍的亮度差压进 256 级
渲染器算出来的亮度没有上限:阳光下的白墙比阴影里亮上千倍,屏幕每个通道却只有 256 级。按亮度平均切,暗部会挤成一团黑;得顺着眼睛切——眼睛判断亮暗看的是比例,暗处差一点就看得出,亮处差很多才察觉。色调映射和 sRGB 的伽马曲线做的都是这件事。最早按这个原则刻出来的一把尺子,是两千年前给星星分的六个等级:肉眼觉得每一等差得一样多,实际每一等亮两倍半。把「按比例」换成「按差」的那一步——比例相同的数,对应的数等差——是 1614 年一张正弦的对数表第一次印出来的。那条曲线在着色器里是逐像素算的,一帧就要算几百万次,写法一律是嵌套的:ax² + bx + c 写成 (ax + b)x + c,n 次多项式只要 n 次乘法,而不是 n(n+1)/2 次。这套写法今天叫霍纳法,中国的教科书叫它秦九韶算法——出处是 1247 年一本讲田亩、赋役与量雨的书。而秦九韶自己不是起点:再往上两百年,开封有人把开方摆成算筹上几行「乘下法入廉、乘廉入方」,逐位求根与嵌套求值本来就是同一套动作的两面。而那条曲线里的对数本身,着色器也是这样嵌套着算的:把数拆成 2 的若干次方乘一个接近 1 的数,后者用一条级数、从最高次那一项往回做「除了加、加了除」——1840 年代有人用二十层「尖锥」的面积这样算出了 ln 2 的前七位,每一步只用加法与除法。
- 14.4
枪声、脚步、混响
声音在内存里是一串采样点,在算法眼里是一堆频率。要压缩就把人听不见的那些扔掉,要做混响就把房间的冲激响应与它卷在一起。拆和拼用的是同一把刀;而这把刀的语言——把旋转写成一个指数——出自另一位。脚步声随机升降一两个半音免得听腻,升一个半音就是频率乘 2 的十二分之一次方:「一个比例的几分之几」,十四世纪才第一次被写成规则。至于声音怎么绕过柱子、拐过门框传到耳朵:把波前上每一点当成新的波源、再取它们的包络,这条画图的规矩是 1690 年写下的,今天的声学仿真与波束成形还在用。风声、雨声这类没完没了的环境音,调音色时看的是它的谱——一段不停、没有周期的声音本来没有傅里叶变换,它的谱是先算它与自己错开一段的平均、再变换出来的,1930 年才有人把这件事做严格。「冲激响应」也是同一类说法:房间对一声理想的拍手——一个 δ——怎么回答;可 δ 不是一个函数,它要到 1945 年才被说成「作用在光滑函数上的泛函」,从此卷积、求导对它都算得通。
- 15.0
发出去,路上丢了一个包
联机对战每秒要发几十个包,丢包是常态。掺多少冗余才够,这件事 1948 年就有了答案:一条会出错的线有一个容量,只要传得比它慢,就存在一种编码能把出错概率压到任意接近零——而且速率不必一路降到零。不靠重传也能补回来的办法,是发出去之前先掺进冗余:把数据当成有限域上一个多项式的取值,丢掉几个点,剩下的点仍然把那个多项式定死。有限域是一个二十岁的人在决斗前夜整理出来的。至于这条连接本身:握手时协商密钥用的多半是椭圆曲线,而那条曲线上的加法之所以成群、之所以绕得回来,追到底是「把椭圆积分反过来看」那一步——那是另一个二十几岁就走了的人。掺冗余还有一条更朴素的分寸:一个会声张的错误,远比一个不声不响被改坏的包便宜——1950 年有人只多加了一位校验,就让码从「能纠一位」变成「能纠一位、并且看得出什么时候错了两位」。线路坏得厉害时还有一种更舍得的办法:拿一族行两两正交的 ±1 方阵,每一行连同它的负行各当一个码字——32 阶那一个给出 64 个码字、最小距离 16,六位信息换三十二位,纠得了七位错。这一族方阵出自 1893 年一个关于行列式最大能有多大的问题,1971 年水手九号就是靠它把火星的照片发回地球的。还有一件事在发送端:该发多快,是照丢包与延迟一路调的,可它看到的丢包都是一个往返以前的事——用晚到的读数去调节,回路就成了带时滞的方程。这种方程的特征根有无穷多个,没法一个个求;1942 年有人把「稳不稳」搬到虚轴上去判:看两条曲线的零点是不是一个隔一个。
- 15.6
两台机器的同一帧,为什么会不一样
锁步同步赌的是决定论:同样的初始状态加同样的输入,必然算出同样的结果——两百年前有人把这个信念推到过极端。可浮点数只有有限位,两台机器在最后一位上差了一点点,几百帧之后就是两个世界。这不是 bug,是对初值的敏感依赖,1890 年在三体问题里第一次被看清。它还留下过一份白纸黑字的证据:1963 年有人把自己那台机器打出来的前六千步印成了表,今天照同一个格式、同一个步长、同一个起点重跑,头三十一圈绕哪一边全都对得上,从第三十二圈起两份各走各的。而浮点数要冒充的那个「实数」,本身也是有来历的:它长期只能靠画一条线来指认,直到 1869 至 1872 那几年才被几个人各自建在算术上——其中一种办法是把有理数切成两堆,缝里站不下有理数时就在那儿添一个。至于那些浮点数本身长什么样,也有人从机器的角度算过一笔:它们的尾数并不均匀,首位是 1 的差不多占三成,而且乘除法只会把分布越推越像这个样子。「几百帧之后」里的那个「几百」也不是随口说的:两条差之毫厘的轨道分开得多快,1892 年就有人给它下了定义——拿解乘上 e 的 λt 次方,看乘积从趋于零翻成无界的那个 λ;他同时举了个例子说明这个量连极限都不一定存在,只能取上极限。
- 16.7
交给显示器:为什么偏偏是 60
一秒切成 60 份,掉帧时能干净地降成 30、20、15——因为 60 的因数多得离谱:2、3、4、5、6、10、12、15、20、30 全都整除得了。这种「因数特别多」的数有个名字,叫高度合成数,由一个从未受过正规训练的人在 1915 年系统研究。(60 这个数最初来自电网频率,但它这么好用,是因数说了算。)可这一帧凭什么一定会结束?没有哪个办法能事先判断一段代码会不会停下来——这是 1936 年证明的;再往前一步,一套规则只要够用,就连「自己不会出矛盾」这句话也说不出口。所以驱动根本不去判断,只拿秒表看着:一段着色器超时没交卷,就把它连同画面一起杀掉,屏幕黑一下再回来。这类「算不出来」并不只关乎代码:连一个整系数方程有没有整数解,都没有通用的判定办法,而那种方程三世纪就在解了。