《计算机图形学》卷一《几何、变换与光栅化》目录

《计算机图形学》卷一《几何、变换与光栅化》目录:16 章 48 节,把渲染从 API 调用还原成可算的整数——Q16.16 定点顶点变换、整数边函数覆盖判定、定点重心插值、深度缓冲、裁剪、抗锯齿与纹理映射全部定点实现,让 Python 与 TypeScript 产出逐字节相同的 PNG。本页给出与站内既有内容的分工、六档可运行性口径、《烛影山谷》场景总览与全书 16 章 48 节完整清单。

这是《计算机图形学》三卷本的第一卷。它回答一个问题:渲染能不能被还原成整数,而且这种还原是可证伪的?

站内的图形学内容已经很多,但基本落在现代图形 API 的实时渲染工程这一侧:graphics/ 专题 45 篇讲管线理论、MSAA/FXAA/TAA、延迟与集群前向、GPU Driven、光追混合;《渲染引擎》 55 篇用 TypeScript 从零搭一个可跑的 helio;《技术美术与管线工程》 50 篇做 LOD 预算、三角预算、显存账与 mip 字节账。它们都跑在 GPU 上,产出的是帧率、draw call 与预算。

「同一条管线,如果只用整数跑在 CPU 上,它到底在算什么、误差从哪来、能不能被逐字节证明」——这一段没有人占。 而这一段恰恰是可计算、可跑、可证伪的一段。这一卷补的就是这一段的地基。

做法只有一句话:一份场景规格,三种宿主消费,两条一致性纪律。config/scene_spec.json 是唯一真源(相机 / 光源 / 材质 / 网格 / 渲染参数),它同时驱动三处——S 层 Python + numpy 的 CPU 定点软光栅化器(基准真源)、P 层 TypeScript 的定点光栅化内核(node --test)、W 层浏览器里的 WebGL2 着色器(Playwright 读像素)。三处读的是同一个文件。

两条纪律是这一卷最硬的部分:① 整数与定点逐位一致——顶点变换走 Q16.16、三角形覆盖走整数边函数、重心坐标与深度走定点、子像素用 6 位精度,于是同一份规格在 Python 与 TypeScript 下产出逐字节相同的像素缓冲(sha256 比对)。这条是可证伪的:任何一处运算顺序或舍入不同,摘要立刻不同。② 浮点按声明容差——GPU 的光栅化规则(top-left rule、子像素精度、插值方式)与 CPU 不同,逐位一致在数学上不可能,故 W 层一律用声明容差断言(不一致像素占比、MAE、PSNR),正文不写「逐位相等」。

贯穿全书的是虚构场景 《烛影山谷》(工程名 lantern)与同名工具链:config/ 放规格,sim/ 是 Python 仿真,core/ 是 TypeScript 定点内核,web/ 把内核内联进单文件 HTML 交给无头浏览器真渲染。场景全部由程序生成,不引入任何第三方素材。

这套书与站内既有内容的分工

站内已经有大量相邻内容,写清分工比重复写一遍更有价值。下面这几处边界是本卷最容易混淆的,务必分清:

  • 图形渲染管线理论 讲的是现代可编程管线与 API 阶段(顶点/几何/片元、命令缓冲、同步)。本卷第 1 章讲的是同一条管线在 CPU 上的整数实现:没有 API,只有四个步骤与它们的定点算术。
  • 图形学数学基础 是浮点数学库(向量、矩阵、四元数,用 float)。本卷第 2–4 章是定点数学与可复现性:同一个点积,Q16.16 与 double 给出的答案不同,而前者可以被两种语言逐位复现。
  • OpenGL 核心模式管线 与 Vulkan 初始化与渲染管线 是 API 用法(创建对象、绑定状态、提交命令)。本卷第 3 章与第 8 章是矩阵链的整数化推导与 Sutherland–Hodgman 的整数实现——同一个裁剪算法,去掉 API 之后剩下什么。
  • 抗锯齿方法 讲 MSAA / FXAA / TAA 的 API 实现与性能。本卷第 6 章与第 11 章从整数边函数出发推覆盖率,把「覆盖率」写成一个可以逐像素断言的整数,再看超采样与 MSAA 在 CPU 上怎么实现。
  • 纹理显存管理 讲纹理的显存占用、格式、压缩与流式加载。本卷第 10 章与第 12 章讲深度缓冲的定点精度分布与采样、过滤、mipmap 的定点实现——关注的是数值,不是显存。
  • 剔除与可见性系统 是 GPU 上的视锥剔除、BVH 与 Hi-Z。本卷第 5 章与第 9 章讲视锥的解析构造、近平面的数值与朝向符号的精确判定:剔除为什么能成立,靠的是边函数的符号。
  • GPU 驱动渲染与 LOD 是把剔除搬到 GPU 上。本卷第 9 章只做一件事:证明环绕顺序与法线朝向的关系在定点下是可判定的。
  • 后处理 是后处理链的 API 与性能。本卷第 7 章讲的是线段的整数判定与误差界(DDA 与 Bresenham)——一个几乎没有浮点的算法。
  • GPU 剖析与调试 是GPU 侧的计时与抓帧工具。本卷第 14 章讲中间缓冲(深度图、法线图、覆盖率图、线框图)的可视化与断言:在没有 GPU 抓帧器的情况下,怎么用 CPU 的中间缓冲定位问题。
  • glTF 资产管线 是资产导入导出。本卷第 15 章讲程序化网格生成、网格表示与场景图遍历——几何从哪来,而不是文件怎么读。
  • 骨骼动画 用四元数驱动蒙皮。本卷第 4 章把四元数当可断言的整数旋转:万向节死锁可以演示、朝向插值可以量化、双覆盖可以验证。
  • 《渲染引擎》 (TS 可跑引擎 helio)是引擎架构:RHI 抽象、命令队列、双后端、帧图。本卷第 13 章是同一算法跨语言的逐字节证明——同一个内核,两种语言,一个摘要。
  • 《技术美术与管线工程》 是管线闸门与预算。本卷第 16 章是可执行规格的三消费联调:不是预算够不够,而是三处产出的图像与中间缓冲是否互相自洽。
  • Unity/SRP 口径的渲染管线 讲的是引擎侧怎么配管线。本卷第 16 章讲的是一份规格怎么被三种宿主消费并互相校验。

一句话:本卷做的是「渲染的每一步能不能写成整数、这些整数能不能被两种语言复现、误差从哪来」,不是「怎么用图形 API 把画面画出来」,也不是「怎么让画面更好看」。

明确不在本卷范围内

为了让边界清楚,下面这些这一卷不讲:

  • 图形 API 的用法与工程实践(OpenGL / Vulkan / WebGPU / Metal 的对象模型、命令缓冲、同步、资源状态)——见 graphics/ 专题与《渲染引擎》。
  • 着色模型、BRDF、阴影、色彩管理与后处理链——这些是**卷二《着色、光照与纹理》**的内容。
  • 光线求交、加速结构、蒙特卡洛与全局光照——这些是**卷三《光线追踪与全局光照》**的内容。
  • 性能优化与移动端适配——见 移动端渲染优化 等篇。
  • 真实 GPU 上的性能剖析与真实驱动的浮点差异:本机 headless 的 WebGL2 后端是 ANGLE/SwiftShader 软件实现,不是真实 GPU。凡涉及真实硬件的结论,正文逐处显式标注「需真实 GPU 环境,命令行不可复现」。
  • 任何第三方素材:贯穿案例为虚构场景,所有 PNG / SVG / GIF 由程序化生成。

可运行性六档口径

本卷每一节的代码块头部注释里都写着它属于哪一档。所有代码块都在本机实跑过,正文数字等于实跑输出;但「整卷项目复现」(把全部配置与共享模块一次性组装跑通)由第 16 章交付,本页不单独声称。

档数据源算什么(真跑)产出断言
Sconfig/scene_spec.jsonnumpy 向量化软光栅化(顶点变换 → 边函数覆盖 → 重心插值 → 深度测试 → 着色)+ 中间缓冲PNG + JSON + CSV + 纯 xml.etree SVG脚本 exit 0;SVG ET.fromstring 合法;定点不变量:重心权重和 == 面积(残差恒为 0)、深度单调
P同一 specTypeScript 定点光栅化内核(整数边函数 + 定点插值,BigInt)core/test/cg_raster_*.test.tsnode --test '<glob>',fail == 0;与 S 层的 PNG sha256 逐字节相同
W同一 spec 内联进单文件 HTML浏览器 WebGL2 顶点/片元着色器真渲染web/dist/raster<NN>.html + out/shots/*.pngPlaywright 无头:断言全绿 + canvas 已绘制 + 与 S 层的容差比对(占比 / MAE / PSNR)
D场景规格schema、字段表、不变量清单、检查清单JSON / Markdown 表lint;必须与 S/P/W 同节搭配
X真实 GPU真实硬件上的性能剖析、真实驱动的浮点差异无正文逐处显式标注「需真实 GPU 环境,命令行不可复现」
B可扰动参数分辨率 / 采样数 / 光照强度 / FOV / mipmap 开关扰动 → 走样与相变阈值JSON软告警不硬失败;exit 0;数字来自本机实跑

「渲染正确」在本卷被操作化成几个可算的代理指标(覆盖率一致性、图像误差 MAE 与 PSNR、定点不变量残差、走样阈值),只用于相对比较,禁绝对化——它们证明的是「这套实现自洽、可控、可分辨」,不是「画质更好」。

贯穿案例《烛影山谷》场景总览

全书只用一份规格 config/scene_spec.json 驱动。它是一个程序化生成的小场景:一片起伏地形(16×16 格点的双线性高度场 + smoothstep 噪声)、三个不同材质与位置的球体(八面体细分)、一盏方向光与一个环境光项(点光源与更复杂的光照模型留到卷二),以及一条可以逐帧推进的相机路径。所有几何与颜色都由规格里的整数生成。

渲染参数

字段值作用
width / height320 / 240画布尺寸(像素)
subpixelBits6子像素精度,1 像素 = 64 单位
backgroundQ8[12,14,20]背景色(8 位通道)
ambientQ1613107环境光项(Q16.16,约 0.2)
seed20261015噪声与场景生成的种子

相机与光照

字段值作用
eyeQ16[0, 393216, 1572864]相机位置(世界单位 0 / 6 / 24)
targetQ16[0, 131072, 0]注视点
upQ16[0, 65536, 0]上方向
tanHalfFovYQ1637837半视场角的正切(约 0.577,即 60° 视场)
nearQ16 / farQ1632768 / 4194304近远平面(0.5 / 64)
directionalLight.dirQ16[-37837, 37837, 37837]方向光朝向
intensityQ1658982方向光强度(约 0.9)

基线一帧的实测结果(S 层 sim/render_raster.py 与 P 层 core/tools/render_raster_ts.ts 产出逐字节相同的像素缓冲):

canvas        : 320x240
vertices      : 487
triangles     : 896 drawn / 896 total (0 dropped, 290 culled)
pixels covered: 43731 (569‰)
sha256(rgb)   : bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced
elapsed       : 86.4 ms

读法:场景共 487 个顶点、896 个三角形;背面剔除掉 290 个(屏幕空间逆时针为正面),其余 896 个全部通过相机(0 dropped);覆盖 43731 个像素,占画布 320×240 = 76800 像素的 569‰。这一行 sha256 就是「两种语言逐字节一致」的可验证点——它是全书最硬的一条断言。

W 层(WebGL2,同一份规格)与 S 层的容差比对实测:

GL 后端         : ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)
GL 版本         : WebGL 2.0 (OpenGL ES 3.0 Chromium)
覆盖归属不同    : 6 / 76800 (0.08‰)   ← 主判据
显著差异像素    : 28 / 76800 (0.36‰)  ← 共享边归属差异
MAE             : 0.2848
PSNR            : 47.79 dB

读法:35112 个像素的通道差恰好是 1(纯舍入),只有 28 个像素差异超过 2——它们全部落在共享边上,来自 CPU 的 >= 0 与 GPU 的 top-left rule 对边像素归属的不同判定。这正是「浮点按声明容差」的含义。

底座:三种消费的公共入口

三种消费共享同一套底座:S 层公共库 sim/cglib.py,P 层内核 core/src/lantern/{fixed,vec,mat,project,noise,mesh,scene,raster,pipeline}.ts,W 层打包器 web/pack.mjs 与验证器 web/verify_raster01.mjs。底座只读,各章只在它之上写自己那一节的脚本与断言。

下面这段是全部定点算术的根,也是两条纪律里第一条的落点。它是 core/src/lantern/fixed.ts 的完整内容,与工作区文件逐字节一致:

// file: lantern/core/src/lantern/fixed.ts
// tier: P
// verify: node --test 'core/test/cg_raster_*.test.ts'
// Q16.16 定点标量(BigInt 实现):与 Python 侧 sim/cglib.py 的 fmul/fdiv/fsqrt 逐字对齐。
//
// 为什么用 BigInt 而不是 Number:
//   Number 的 `>>` 只对 32 位有效,矩阵链的中间量可达 2^34 量级,
//   一旦溢出会静默给出错误结果。BigInt 是无限精度,与 Python 的 int 语义一致。
//   逐像素层(raster.ts)量级 <= 2^49.6 < 2^53,可以用 Number + Math.floor。
//
// 舍入铁律:
//   - 降精度一律用 `>>`(算术右移 = 向下取整),Python 与 BigInt 两侧同义
//   - 定点除法用 fdiv(向零截断),禁止裸 `/`:Python 的 `//` 是 floor,
//     与 BigInt 的 `/`(向零截断)在负数上不一致,冒烟阶段实测过这个坑

export const ONE = 65536n; // Q16.16 的 1.0
export const SUB = 6; // 屏幕空间子像素位数
export const SUB1 = 64; // 1 像素 = 64 单位

/** Q16.16 乘法:降精度用算术右移(负数向下取整,两侧一致)。 */
export function fmul(a: bigint, b: bigint): bigint {
  return (a * b) >> 16n;
}

/** Q16.16 除法:向零截断(与 Python 侧显式实现一致)。 */
export function fdiv(a: bigint, b: bigint): bigint {
  if (b === 0n) throw new Error("fdiv by zero");
  const q = ((a < 0n ? -a : a) << 16n) / (b < 0n ? -b : b);
  return a < 0n !== b < 0n ? -q : q;
}

/** BigInt 精确整数平方根:floor(sqrt(n)),与 Python 的 math.isqrt 同义。 */
export function isqrt(n: bigint): bigint {
  if (n < 0n) throw new Error("isqrt of negative");
  if (n < 2n) return n;
  let x = n;
  let y = (x + 1n) >> 1n;
  while (y < x) {
    x = y;
    y = (x + n / x) >> 1n;
  }
  return x;
}

/** Q16.16 平方根:floor(sqrt(x << 16)),与 Python 侧 math.isqrt(x << 16) 一致。 */
export function fsqrt(x: bigint): bigint {
  if (x <= 0n) return 0n;
  return isqrt(x << 16n);
}

/** 整数 -> Q16.16。 */
export function fFromInt(n: number | bigint): bigint {
  return BigInt(n) << 16n;
}

/** Q16.16 -> 整数,向零截断。 */
export function fToInt(a: bigint): bigint {
  const q = (a < 0n ? -a : a) >> 16n;
  return a < 0n ? -q : q;
}

三条跨语言纪律(违反其一,逐字节一致必然崩):① 降精度一律用算术右移,定点除法用显式向零截断的实现,禁用裸 / 与 //——Python 的 // 是 floor,与 BigInt 的 /(向零截断)在负数上不一致;② config/ 禁浮点,全部走定点整数或整数千分比,矩阵链的中间量必须在 BigInt 里算(Number 的 >> 只对 32 位有效);③ 整数类可以写「逐字节相同」,感知类派生量按声明容差断言,正文不写「GPU 与 CPU 逐位相等」。

全书目录(16 章 48 节)

第一部分 · 把渲染还原成整数(第 1–4 章)

第 1 章 渲染的最小模型(D+S)

第 2 章 向量、点与齐次坐标(S+P)

第 3 章 矩阵与变换管线(S+P)

第 4 章 旋转与朝向(S+P)

第二部分 · 光栅化(第 5–8 章)

第 5 章 相机与投影(S+P)

第 6 章 三角形的光栅化(S+P+W)

第 7 章 直线的光栅化(S+P)

第 8 章 裁剪(S+P)

第三部分 · 深度、采样与纹理(第 9–12 章)

第 9 章 背面剔除与朝向(S+P)

第 10 章 深度与可见性(S+P)

第 11 章 抗锯齿与采样(S+P+W)

第 12 章 纹理映射(S+P+W)

第四部分 · 一致性与装配(第 13–16 章)

第 13 章 光栅化的数值一致(S+P)

第 14 章 渲染器的调试视图(S+W)

第 15 章 场景与几何的生成(S+P)

第 16 章 装配:从零到第一帧(S+P+W+B)

怎么读

三条路径,按你的目的挑一条:

可运行性总表(各章档位分布)

章主题档位
1渲染的最小模型D + S
2向量、点与齐次坐标S + P
3矩阵与变换管线S + P
4旋转与朝向S + P
5相机与投影S + P
6三角形的光栅化S + P + W
7直线的光栅化S + P
8裁剪S + P
9背面剔除与朝向S + P
10深度与可见性S + P
11抗锯齿与采样S + P + W
12纹理映射S + P + W
13光栅化的数值一致S + P
14渲染器的调试视图S + W
15场景与几何的生成S + P
16装配:从零到第一帧S + P + W + B

整卷口径:每一节至少有一个 S / P / W 档的可跑代码块;S 层脚本逐个跑(/opt/homebrew/bin/python3 sim/cg_raster_*.py),P 层写全 glob(node --test 'core/test/cg_raster_*.test.ts'),W 层用 web/verify_raster<NN>.mjs 无头断言;B 层软告警不硬失败。完整字段表、定点约定与数据集索引见附录 A 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cg」更多文章

  1. 14.3 中间缓冲的可视化
  2. 14.2 覆盖率与线框
  3. 14.1 深度图与法线图