《游戏引擎实现原理》14.1 Pass 依赖与资源生命周期
帧图不是一张画出来的图,而是一组 Pass 与它们读写资源的声明。本节在 helio 里从零实现帧图的 IR:把「谁读什么、谁写什么」翻译成数据流依赖与三类 hazard,跑拓扑排序排出执行序,再算出每个资源的生命周期区间,为后面的内存规划与执行打底。
tag
帧图不是一张画出来的图,而是一组 Pass 与它们读写资源的声明。本节在 helio 里从零实现帧图的 IR:把「谁读什么、谁写什么」翻译成数据流依赖与三类 hazard,跑拓扑排序排出执行序,再算出每个资源的生命周期区间,为后面的内存规划与执行打底。
阴影的两大旋钮——每像素采样数与深度偏置——分别换质量和换成本。PCF 半径从 1 到 3 让采样数按平方增长,把硬边软化成柔化像素;偏置从 0 往上加能压住自遮挡粉刺,但加过头阴影就脱离接触点。本节在 helio 里实现采样核与斜率缩放偏置,并在 GPU 上量化这两条权衡曲线。
一张固定分辨率的阴影贴图罩住整个视锥,近处必然糊、远处必然浪费。级联阴影把视锥沿深度切成几级,每级单独拟合一个光源空间正交盒,并在相机移动时把正交盒对齐到 texel 网格。本节在 helio 里实现切分、角点提取与拟合,再在 GPU 上复算逐级联的包围盒。
光照决定物体长什么样,阴影决定它站在哪。本节在 helio 上从零实现阴影贴图:先从光源视角把遮挡物渲染成一张光空间深度图,再把相机视角的地面点投影回光源空间做深度比较,最后在真实 WebGPU 设备上回读像素,与 CPU 解析参考逐点比对。
上一节把单个光源的衰减算清楚,但场景里往往有上百个光源,逐物体遍历全部光源太贵。集群光照把视图空间切成三维网格,每个集群预存一份命中的光源列表。本节在 helio 里实现集群网格与光源装配的 CPU 参考,再用 compute 着色器在真实 WebGPU 设备上复算并逐集群比对。
上一节把着色路径打通,这一节解决「光从哪来、怎么变弱」。方向光没有距离衰减,点光用 UE4 的窗口化反平方,聚光再叠一个锥形衰减。本节在 helio 里实现三类光源的衰减公式,并用一个 compute 着色器在真实 WebGPU 设备上回读每个距离的衰减值,与 CPU 参考逐样本比对。
光照不是一句「乘以点积」,而是「法线 + 光方向 + 视线 + 材质」到一根颜色的映射。本节在 helio 里从零实现 Lambert、Blinn-Phong、GGX 三条着色路径的 CPU 参考,再原样搬进 WGSL,在真实 WebGPU 设备上渲染球体并回读像素,要求 CPU 与 GPU 逐个字节对齐。
着色器写错时报错只有一行天书,定位全靠猜:本节真跑 getCompilationInfo(),拿到带行号、列号与字符偏移的诊断,并用 11.1 的 include 行映射把它还原成原始文件的文件:行:列,让编辑器能一键跳到出错的那一行源码。
uniform 结构体在 CPU 侧要按完全一致的内存布局填充,手写偏移是 bug 温床:本节从 WGSL 的 struct 反射出每个成员的偏移与整个缓冲区大小,按该布局自动打包参数,并真跑一次渲染回读像素,验证绑定确实生效,偏移对了像素才会对。
WGSL 没有 #include,但引擎要把公共函数拆成多个文件复用:本节在 createShaderModule() 之前做一层文本级 include 展开,处理递归、去重与循环检测,并真跑一次编译与像素回读,验证拼接后的模块确实可编译可用。
六个开关朴素地给出 2^6=64 个着色器变体,但特性之间有依赖,真实可达集只有 32 个。这一节按依赖枚举可达变体、用内容寻址(源码 + 开关集合的哈希)给编译结果去重,并在 headless Chromium 里把全部可达变体编译一遍,验证 0 error、非法组合会被拒。
WGSL 没有预处理器,材质却要在多种开关组合下生成不同程序。这一节实现一个只认 #ifdef / #ifndef / #else / #endif 的行级宏展开器,把一份模板裁剪成具体变体,并在 headless Chromium 里逐个编译验证 0 error。
把材质拆成「模型(参数声明)+ 实例(参数值)」两层,按 std140 算出每个参数的字节偏移并打包成 uniform buffer;再在 headless Chromium 里把它绑进管线、渲染一帧、回读中心像素,断言它就是 baseColor。
《游戏引擎实现原理》卷二《渲染引擎》全书导航页:说明本卷定位与卷一《引擎内核》的关系、本卷差异化(在 RHI 上从零实现、每节跑出真实产物、真实像素与计时结果),给出顺序通读、只读 RHI 四章、直奔帧图与后处理三条阅读路径,讲清 L1–L2 可运行性口径,并按五个部分列出 18 章 54 节的完整章节索引,另含周计划表与常见问题速查,方便读者按需定位、跳读与随时查阅。
《游戏引擎实现原理》卷二《渲染引擎》全 55 篇,在卷一《引擎内核》的 helio 之上用 TypeScript 从零接出渲染引擎:从渲染架构总览与一帧的 CPU / GPU 时间线起步,经 RHI 抽象设计、命令与队列、GPU 资源与内存、WebGPU 与 WebGL2 多后端实现,再到几何与纹理资源、场景变换相机、可见性剔除与 LOD、排序与批次、材质与着色器、光照与阴影、帧图、后处理与全屏特效、2D 与 UI、多线程渲染,最后把全部零件装配成能画的渲染器,让《微光岛》在真实 GPU 上被完整画出来。
9.2 的状态缓存砍不掉顶点缓冲的频繁切换,因为它按材质而非网格分组。这一节换一条把网格提到高位的批次键,把连续同网格的绘制并成一次实例化绘制:20 万次绘制在 64 网格场景降到 64 次,并跑出一次 draw(6,4) 画出四个彩色实例的真实像素。
写一个后端无关的状态缓存,记住当前管线、绑定组、顶点与索引缓冲,只在真正变化时下发;在 20 万次绘制的乱序与排序两种流上跑出「应用 / 跳过」两路计数,看排序能把管线切换从 199157 次砍到 512 次、又砍不掉什么,为下一节的实例化合批埋下引子。
把剔除后的可见项压进一条 32 位排序键:高 18 位决定批次归属、低 14 位决定前后顺序,再用稳定的 LSD 基数排序替代多字段比较器。在 20 万项的确定性场景上跑出三种排序的真实耗时对比,并正面校验不透明项确实近→远、透明项远→近。
把视锥剔除与 LOD 选择整体搬到 GPU:一个 compute 通道按包围球剔除、按距离选档,用原子计数器追加进对应 LOD 的实例缓冲,第二个通道写出三组 indirect 绘制参数;CPU 每帧只发三个 drawIndirect,并回读掩码与 CPU 逐对象比对验证一致。
用最长轴中位数分割把十万个 AABB 建成扁平二叉 BVH,把逐盒的线性剔除换成层次遍历;再把叶节点 AABB 与图元 AABB 摊平上传,用两个 compute 通道做两级视锥剔除,回读掩码与 CPU 的 bvhCull 逐位比对,验证完全一致。