1.3 与 graphics 专题的分工表
前两节把一帧的时间线和渲染器的分层讲清楚了,这一节回答一个更现实的问题:本卷和站内已有的 graphics/ 专题,到底谁写什么? 站内已经有 45 篇图形学文章,其中 16 篇和渲染引擎直接相关。如果分工不清,读者会在「渲染原理」和「引擎实现」之间反复横跳,作者也会把别人讲透的算法再抄一遍。
本节的立场和卷一一致:别人讲过的「是什么、为什么」,我们全部外链出去;本卷只写「怎么在 helio 的 RHI 上搭出来、跑出什么真实结果」。 这不是偷懒,而是对读者负责——你花钱买的是「从零实现」,不是「渲染概论」的第十个版本。把边界划清楚,双方都省事。
读完这一节,你应该能回答三个问题:某个渲染话题该看谁、本卷的 18 章各自负责什么、以及为什么我们敢说「算法不重推」。这三个问题的答案,最后都会收敛到同一张表上。
顺带说一句:这张表也是给未来的你用的。当你自己动手写一个渲染器、卡在某个算法上时,回来查一查这张表,就能立刻分清「这是我没读懂的原理,还是我没写对的代码」——前者去 graphics/,后者留在本卷。
为什么要有这张表
渲染是「原理」和「实现」最容易糊在一起的一块。比如「级联阴影」,站内文章会把视锥分割、光锥拟合、PCF 采样讲得很透;但怎么把这几步接到一个真实引擎里——阴影贴图资源从哪来、Pass 怎么排序、级联切换怎么和相机绑定——这些是 graphics/ 文章不负责的,正是本卷要补的。
再比如「GPU 剔除」。graphics-gpu-driven-rendering-lod 会讲 Hi-Z 金字塔怎么建、怎么用上一帧的深度做遮挡测试;而本卷要回答的是:剔除结果怎么塞进间接绘制参数缓冲、什么时候读回来、和帧图的哪个 Pass 相邻。原理是别人的强项,接线是本卷的本职。
一句话总结这两个例子:graphics/ 回答「为什么要这样算」,本卷回答「算出来之后往哪放」。前者是数学,后者是工程。
于是分工就落在一条线上:
- 算法层(为什么这样算、有几种流派、各自的数学推导)→ 外链
graphics/。 - 实现层(在 RHI 上怎么建资源、怎么录命令、怎么验收)→ 本卷。
这条线对应上一节的三层结构:graphics/ 讲的是「原理」,本卷把原理落到 RHI 与渲染器两层上。场景层则负责把数据准备好——它是原理和实现之间的输入。
还有一个更实际的理由:避免重复劳动带来的错误。 如果本卷也去推导一遍 PBR 的 BRDF,一来篇幅翻倍,二来推导一旦有笔误,读者跟着错。不如把「推导」这件站内已经做好的事交给既有文章,本卷把精力全花在「代码能不能跑、像素对不对」上。下面这张表是本节的核心,逐条列出 16 篇文章各自负责什么、本卷只做什么。
16 篇文章逐条对照
| 既有文章 | 它负责什么 | 本卷只做什么 |
|---|---|---|
/graphics-render-graph/ | 帧图与帧调度架构:Pass 依赖、自动屏障、异步计算 | 第 14 章用 helio 实现一个帧图并跑出结果 |
/graphics-culling-visibility-systems/ | 可见性剔除:视锥、BVH、Hi-Z、GPU Driven 剔除 | 第 8 章在 RHI 上实现视锥/BVH 剔除与 GPU 剔除 |
/graphics-lighting-shadows/ | 光照模型与实时阴影:Phong、PBR、阴影贴图、级联阴影 | 第 12–13 章实现光照与级联阴影的引擎侧装配 |
/graphics-post-processing/ | 后处理:HDR、Bloom、色调映射、抗锯齿 | 第 15 章实现全屏 Pass 链、色调映射与抗锯齿 |
/graphics-skeletal-animation/ | 骨骼动画与蒙皮:层级、混合、GPU 蒙皮 | 第 6–7 章接入骨骼数据的顶点格式与场景变换 |
/graphics-texture-memory/ | 纹理采样与 GPU 内存优化:mipmap、压缩、寻址 | 第 6 章实现纹理格式、压缩与 mipmap 上传 |
/graphics-shader-compilation-pipeline/ | 着色器编译与 SPIR-V 工具链:源码到指令的链路 | 第 11 章做 WGSL 组织、反射驱动绑定与编译调试 |
/graphics-color-management-hdr-pipeline/ | 色彩管理与 HDR:线性工作流、ACES、宽色域 | 第 15 章实现色调映射 Pass 与线性工作流 |
/graphics-anti-aliasing-methods/ | 抗锯齿全解析:MSAA、FXAA、TAA、时序超分 | 第 15 章在后处理链里实现一种可跑的 AA |
/graphics-clustered-forward-rendering/ | 集群前向渲染与光源管理:Forward+、Tiled、Clustered | 第 12 章实现 clustered 光源装配 |
/graphics-deferred-tiled-rendering/ | 延迟渲染与 Tiled/Clustered 光照剔除 | 第 12/14 章用帧图组织 G-Buffer 与光照 Pass |
/graphics-gpu-driven-rendering-lod/ | GPU Driven 渲染与 LOD:从 CPU 瓶颈到 GPU 剔除 | 第 8 章实现 GPU 剔除与 LOD 的引擎侧 |
/graphics-gpu-profiling-debugging/ | GPU 剖析与调试工具链:帧捕获、计数器、着色器调试 | 第 18 章在 helio 里做帧耗时统计与剖析收尾 |
/graphics-gltf-asset-pipeline/ | glTF 资产格式与场景加载管线 | 第 6–7 章把网格与材质数据接进场景 |
/graphics-gpu-particles-vfx/ | GPU 粒子与特效:计算着色器、十万级粒子 | 第 18 章把粒子接进《微光岛》场景 |
/graphics-math-foundations/ | 图形学数学基础:向量、矩阵、坐标变换、投影推导 | 卷一已实现数学库,本卷只调用 |
这 16 篇可以分成三组
表是按章号排的,但按「本卷怎么用」看,它们其实分成三组:
- 第一组:帧与可见性(render-graph、culling、gpu-driven-rendering-lod、gpu-profiling-debugging)。它们讲的是「一帧里哪些东西要画、以什么顺序画」。本卷的第 8、14、18 章直接把这三篇的算法接到 RHI 与帧图上,产出的是「跑起来的帧」和「真实帧耗时」。
- 第二组:资源与几何(texture-memory、gltf-asset-pipeline、skeletal-animation、shader-compilation-pipeline)。它们讲的是「像素从哪来」。本卷的第 6、7、11 章负责把这些数据接进场景、传上 GPU,产出的是「一张真的画出来的纹理」和「一段真的编译过的 WGSL」。
- 第三组:光照与后处理(lighting-shadows、post-processing、color-management-hdr-pipeline、anti-aliasing-methods、clustered-forward-rendering、deferred-tiled-rendering、gpu-particles-vfx)。它们讲的是「怎么好看」。本卷的第 12、13、15、18 章负责在帧图里组织这些 Pass,产出的是「可截图的画面」和「可对账的像素值」。
这么分组的意义是:你读某一章时,能一眼看出它对应哪几篇既有文章。 读到第 13 章级联阴影卡住了,回头看 graphics-lighting-shadows 补原理;读到第 6 章纹理上传卡住了,回头看 graphics-texture-memory。左边是「为什么」,右边是「怎么做」,两边合起来才完整。
三组之间还有一条隐含的依赖链:先有资源(第二组),才有可见性(第一组),最后才是好看(第三组)。这也是本卷的章节顺序——第 6–7 章先把网格与纹理接进场景,第 8 章才谈剔除,第 12 章以后才谈光照与后处理。你如果跳过第二组直接读第三组,会发现光照 Pass 根本拿不到可用的输入。
换一个角度看,这三组也对应渲染器要回答的三个问题:画什么(可见性)、拿什么画(资源)、怎么画才好看(光照与后处理)。graphics/ 把这三个问题各自的「算法答案」讲透了,本卷把它们的「工程答案」补上——用哪张纹理、哪个 Pass、多少显存、几毫秒。
原则与用法:算法不重推,怎么读这套书
把这张表压缩成一条原则,就是:本卷不重新推导任何渲染算法,只负责把它在 helio 的 RHI 上搭出来,并跑出可复现的像素或计时结果。
这条原则听上去简单,执行起来却要克制:每次写到某个算法,总会忍不住想「顺便把原理也讲了吧」。本书的纪律是——只给一句结论加一个外链,然后立刻转到代码上。原理留给 graphics/,代码留给自己,各司其职。
拿「级联阴影」举例。graphics-lighting-shadows 会告诉你为什么要分几级、怎么拟合光锥、PCF 怎么采样;本卷第 13 章则要回答完全不同的三个问题:
- 资源:级联阴影贴图是几张纹理?格式选什么?在 RHI 上怎么建、怎么在每帧更新?
- 装配:谁负责算光源的视锥?它和场景里的相机、光源数据怎么对接?
- 验收:跑起来之后,阴影边界在哪、几级之间怎么过渡、帧耗时涨了多少——这些都要有真实数字。
同理,「延迟渲染」本卷不重推 G-Buffer 的压缩技巧,而是用第 14 章的帧图把 G-Buffer Pass 和光照 Pass 组织起来;「GPU 剔除」本卷不重推 Hi-Z 的构造,而是把剔除结果接到第 8 章的间接绘制上。
再看一个「后处理」的例子。graphics-post-processing 会讲 Bloom 的高斯模糊怎么分离、色调映射有哪些算子、抗锯齿各有什么代价;本卷第 15 章要做的是:把这些效果组织成一条全屏 Pass 链,用帧图管好每张中间纹理的生命周期,让每个 Pass 的输入输出都对得上,最后给出「加一条 Bloom 多花几毫秒」的真实数字。同一件事,别人讲原理,本卷讲接线与验收。
算法看别人,实现看本卷——这条线就是三列表的右列。
具体来说,本卷不重复造轮子的边界是:
- 概念层不重复:一个算法「是什么、有几种流派」,只给一句结论 + 外链,不展开论证。
- 推导层不重复:公式怎么来的、矩阵为什么这样乘,指向既有文章;本卷只给可编译实现和数值/像素验证。
- 选型层不重复:用前向还是延迟、要不要上 TAA,属于项目决策,不属于本书;本卷只保证「你自己实现时知道每个取舍的代价」。
- 实现层不外包:只要一段代码能从零写出来并跑出产物,本卷就自己写——这是本书唯一不可让渡的部分。
有人会问:既然别人写过,为什么还要自己实现一遍?因为读得懂和写得出是两种能力。看懂一篇 TAA 综述,和亲手处理历史缓冲的重投影、抖动矩阵、与帧图的生命周期,中间隔着一整个调试周期。本卷要补的正是这一段。
把「左列 / 右列」再对照一次,边界就更清楚了:
| 问题 | 去哪看 |
|---|---|
| 这个算法的数学推导 | 对应的 graphics/ 文章 |
| 有哪几种流派、怎么选 | 对应的 graphics/ 文章 |
| 在 helio RHI 上怎么建资源 | 本卷对应章节 |
| Pass 怎么排序、和帧图怎么接 | 本卷对应章节 |
| 跑出来是几帧、像素对不对 | 本卷对应章节 + 附录 C |
左列是「读什么」,右列是「写什么」。本卷唯一不可让渡的,就是右列。
「验收」这一栏值得单独强调。本卷每一节的产物,最后都要落成一个可复现的结果:要么是一组跑得过的单测,要么是一张可截图的画面,要么是一串带环境的计时数字。以第 13 章级联阴影为例,验收清单大概是这样的:
- 阴影贴图能建出来,格式和分辨率在能力表允许范围内;
- 三个级联的覆盖范围随相机远近自动切换,切缝处没有明显跳变;
- 关掉阴影与打开阴影相比,帧耗时增加多少,有具体数字;
- 这些数字写进附录 C,标注运行环境。
没有这条清单,级联阴影就只是「看起来能跑」;有了它,读者才能判断自己的实现到底对不对。
读者常见问题
几个读者最常问的问题,先在这里一次性回答:
- 「我该按什么顺序读?」 顺序读第 1–18 章。本卷是一条依赖链,从 RHI 到后端、资源、场景、剔除、光照、帧图、后处理,跳读会在后面章节付出返工代价。
- 「需要装游戏引擎吗?」 不需要。L1 代码只需要 Node,L2 代码只需要一个支持 WebGPU 的浏览器(headless 也行)。
- 「数字能信吗?」 所有性能与像素数字都在附录 C 里,标注了运行环境,只做同机对比,不跨语言或跨设备外推。
- 「和卷一怎么衔接?」 卷一交付了内核(时间、内存、数学、ECS、任务系统)与《微光岛》的场景数据;本卷在它之上把 GPU 那一半补上,最后把《微光岛》真正渲染出来。
- 「能只读某一章吗?」 可以当参考手册用,但要跑通代码仍建议先读 RHI 与资源两章打好地基。
- 「为什么不直接抄现成引擎的代码?」 因为本书的目的是让你能自己写。抄一份渲染器你只会得到一份能跑的代码;从零搭一遍,你才会知道每个取舍的代价在哪。
- 「跨专题的外链会不会失效?」 本卷所有外链都用站内相对路径,并且指向已经上线的既有文章;书内链接统一用
relref,目标都在本卷的 slug 白名单里。
这些回答其实都能从「写什么 / 不写什么」推出来,之所以单列,是因为它们决定了你怎么分配时间——先读哪章、哪些可以跳过、哪些必须动手敲一遍。
怎么用这套书
这套书对代码的要求是「可复现」而不是「看起来对」。每节的可运行代码都按层级标注,你要先知道自己这节能跑到什么程度:
- L1(纯 TS,Node 直跑):不需要浏览器、不需要 GPU。用
npx tsc --noEmit做类型检查,用npx vitest run <file>跑单测。本卷的剔除、排序、帧图拓扑、批次键这些「纯算法」都是 L1。 - L2(渲染,headless 浏览器):需要 WebGPU。经本地 http 服务在 headless Chromium 里跑,命令行不可直接复现——本卷的每一个 Pass 都落在这一层。
- L3(仅类型):只做类型层面的验证,不产生运行期行为,比如本节上面那三份接口草图。
- L4(命令行不可复现):需要在真实设备上手动验证,比如不同 GPU 驱动的差异、交互式编辑器视口。
一个粗略的比例:本卷大约一半的代码是 L1(剔除、排序、帧图拓扑、批次键、材质参数计算这些纯算法),另一半是 L2(每一个真正碰 GPU 的 Pass)。L3 只出现在接口定义处,L4 只在少数「必须真机」的讨论里点到。这个分布是刻意的——凡是能脱离 GPU 验证的逻辑,我们都尽量做成 L1,这样你即便没有能跑 WebGPU 的设备,也能跑通本卷相当一部分内容。
每段代码的第一行注释都会写明 // file:(还原路径)、// tier:(层级)和 // verify:(验证命令)。这意味着你可以把全书代码抽出来,一条命令复现每一节的结果:L1 块跑 npx vitest run,L2 块经本地 http 服务在 headless 浏览器里跑,L3 块只做类型检查。
抽出代码时注意一点:带 // file: 的块都是完整可编译的文件,不做跨节拼接。同一个文件在多节里出现时,以书中最后一次为准——这意味着你可以放心地按 // file: 的路径把每个块单独存盘,不用担心半截代码。
最后强调一遍本卷对「可复现」的要求:每个 L2 渲染块都必须能在 headless 浏览器里经本地 http 服务跑出真实像素或计时,而不是「在我机器上看着对」。附录 C 会汇总所有性能数字及其运行环境,方便你对照自己的机器复现。
换句话说,本卷的每个「可运行产物」小节,都是一份可以照着敲、照着跑、照着对的清单。你不需要相信我写的数字,跑一遍就知道对不对。
好,分工讲完了,下面把这张表变成代码。
前置知识方面:会写 TypeScript、知道 ArrayBuffer 和 typed array 是什么、大致听说过矩阵乘法,就够了。不需要预先懂 GPU 编程,也不需要装任何游戏引擎——本卷所有 L1 代码只需要 Node,L2 代码只需要一个支持 WebGPU 的浏览器。
那为什么用 TypeScript 而不是 C++?工业引擎确实用 C++ 或 Rust,但本书的目的是讲清机制,不是做出最快的二进制。用 TypeScript 有三个实打实的好处:零门槛复现(Node 直接跑 .ts,不用配 CMake);内存模型显式(ArrayBuffer 逼着你把字节偏移、对齐、视图写出来);类型系统当教具(上一节的依赖方向守卫就是例子)。代价也要说清楚:本书的数字不能当成 C++ 的性能预期,我们验证的是「取舍方向」,不是绝对速度。
贯穿项目依然是低多边形 3D 收集游戏**《微光岛》**(glimmer-isle)。卷一已经交付了它的场景数据(实例、变换、材质 id);本卷的任务是把这些数据变成屏幕上的像素。18 章大致按「从硬件到画面」排列:
- 第 1 章 总览:一帧的时间线、渲染器分层、与既有内容的分工(也就是本章)。
- 第 2–5 章 RHI:为什么要 RHI、资源对象、管线状态;命令录制、提交与栅栏、三缓冲与生命周期;绑定组、上传与 mipmap、显存预算;WebGPU / WebGL2 / NullRHI 三个后端。
- 第 6–7 章 资源与场景:顶点格式与程序化网格、纹理格式与压缩、采样器;场景图变换、脏标记世界矩阵、相机与视锥。
- 第 8–9 章 剔除与批次:视锥剔除、空间划分与 BVH、GPU 剔除与 LOD;排序键、状态缓存、实例化与批次。
- 第 10–11 章 材质与着色器:材质模型与参数、着色器变体与宏、变体爆炸与缓存;WGSL 组织、反射驱动绑定、编译信息调试。
- 第 12–13 章 光照与阴影:光照模型与路径、光源类型与衰减、clustered 光源装配;阴影贴图的引擎侧、级联拟合、性能与质量取舍。
- 第 14–15 章 帧图与后处理:Pass 依赖与生命周期、WebGPU 约束、帧图编译与执行;全屏 Pass 链、色调映射与抗锯齿、后处理预算。
- 第 16–18 章 2D/UI 与多线程与收官:精灵批次与图集、SDF 文本与矢量 UI、UI 与 3D 的合成顺序;渲染线程与命令缓冲、离屏画布并行录制、与任务系统集成;最后把《微光岛》整体渲染出来并做性能剖析。
这条路线本身也是一条依赖链:你几乎不会在第 3 章用到第 13 章的东西,反过来却会。所以本卷建议顺序读。
上面这张表和这套读法,其实也是本卷的写作纪律:每写一节,先问「这一节的原理是不是已经在 graphics/ 里讲过」,如果是,就只留一句结论加一个外链,把篇幅让给代码。下面我们把这套纪律变成一段可运行的代码。
可运行产物:把分工编码成路由表
表格是给人看的,但「不越界」如果只靠自觉,迟早会漂。helio 的做法是把这张表编码成一个可查询的路由函数:给它一个渲染话题,它告诉你该由谁负责——是站内 graphics/ 文章,还是本卷自己动手。这样「越界」就变成了一条可以断言的失败用例。
这个路由表和卷一 1.2 的 scope-router 是同一个思路,但换成了渲染话题的关键词表:每条规则一个关键词,命中就返回对应的归属。它有两条不变量——恰好 16 条 external 规则对应 16 篇文章,且所有 external 路径都是站内根级文章路径。测试会把这两条钉死。
// file: src/render/scope-router.ts
// tier: L1
// verify: npx vitest run src/render/scope-router.test.ts
//
// helio 渲染:把「与 graphics 专题的分工」编码成可查询的路由表。
// 给一个渲染话题,返回它归谁:站内 graphics 文章(讲原理),还是本卷(在 RHI 上实现)。
// 16 条 external 规则一一对应 graphics 专题的 16 篇文章。
export type RenderOwner =
| { readonly kind: 'external'; readonly path: string }
| { readonly kind: 'this-book'; readonly chapter: number };
export interface RenderScopeRule {
readonly topic: string;
readonly owner: RenderOwner;
}
/** 顺序即优先级:先命中的规则先返回。 */
export const RENDER_SCOPE: readonly RenderScopeRule[] = [
{ topic: '帧图', owner: { kind: 'external', path: '/graphics-render-graph/' } },
{ topic: '剔除', owner: { kind: 'external', path: '/graphics-culling-visibility-systems/' } },
{ topic: '光照', owner: { kind: 'external', path: '/graphics-lighting-shadows/' } },
{ topic: '后处理', owner: { kind: 'external', path: '/graphics-post-processing/' } },
{ topic: '骨骼动画', owner: { kind: 'external', path: '/graphics-skeletal-animation/' } },
{ topic: '纹理内存', owner: { kind: 'external', path: '/graphics-texture-memory/' } },
{ topic: '着色器编译', owner: { kind: 'external', path: '/graphics-shader-compilation-pipeline/' } },
{ topic: '色彩管理', owner: { kind: 'external', path: '/graphics-color-management-hdr-pipeline/' } },
{ topic: '抗锯齿', owner: { kind: 'external', path: '/graphics-anti-aliasing-methods/' } },
{ topic: '集群前向', owner: { kind: 'external', path: '/graphics-clustered-forward-rendering/' } },
{ topic: '延迟渲染', owner: { kind: 'external', path: '/graphics-deferred-tiled-rendering/' } },
{ topic: 'GPU Driven', owner: { kind: 'external', path: '/graphics-gpu-driven-rendering-lod/' } },
{ topic: 'GPU 剖析', owner: { kind: 'external', path: '/graphics-gpu-profiling-debugging/' } },
{ topic: 'glTF', owner: { kind: 'external', path: '/graphics-gltf-asset-pipeline/' } },
{ topic: 'GPU 粒子', owner: { kind: 'external', path: '/graphics-gpu-particles-vfx/' } },
{ topic: '数学推导', owner: { kind: 'external', path: '/graphics-math-foundations/' } },
{ topic: 'RHI 抽象', owner: { kind: 'this-book', chapter: 2 } },
{ topic: '命令缓冲', owner: { kind: 'this-book', chapter: 3 } },
];
/** 兜底:没命中任何规则的话题判给本卷——「从零实现」是本书的本职。 */
const FALLBACK: RenderOwner = { kind: 'this-book', chapter: 2 };
export function routeRender(topic: string): RenderOwner {
for (const rule of RENDER_SCOPE) if (topic.includes(rule.topic)) return rule.owner;
return FALLBACK;
}
export function isExternalRender(owner: RenderOwner): owner is Extract<RenderOwner, { kind: 'external' }> {
return owner.kind === 'external';
}
配套测试验证:恰好 16 条 external 规则、路径两两不同;原理类话题外链,实现类话题留本卷。本机实测输出:
✓ render scope-router > 恰好 16 条 external 规则,路径两两不同
✓ render scope-router > 渲染原理类话题外链到 graphics 文章
✓ render scope-router > RHI 实现类话题留在本卷
✓ render scope-router > 未命中规则默认本卷
✓ render scope-router > 所有外链都指向站内根级文章路径
Test Files 1 passed (1)
Tests 5 passed (5)
把几个「读者真会问的问题」丢进路由表,直接看它判给谁:
帧图与自动屏障 -> /graphics-render-graph/
光照与阴影 -> /graphics-lighting-shadows/
TAA 抗锯齿 -> /graphics-anti-aliasing-methods/
glTF 场景加载 -> /graphics-gltf-asset-pipeline/
RHI 抽象怎么搭 -> 本卷第 2 章
命令缓冲与提交 -> 本卷第 3 章
最后两行值得注意:它们没有命中任何 external 规则,落到了本卷。这正是我们想要的不变量——「要不要自己实现」的答案永远偏向自己实现。
第 1 章到此结束。我们搞清楚了「一帧里谁在等谁」「渲染器怎么分层」「和既有文章怎么分工」这三件事。从下一章开始,我们正式动手写 RHI 的第一行代码,并把它接到 WebGPU、WebGL2、NullRHI 三个后端上——那才是本卷真正的起点。
阅读导航:上一节:1.2 渲染器的分层:RHI / 渲染器 / 场景 · 下一节:2.1 为什么要 RHI:多后端与平台差异 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。