《游戏引擎实现原理》2.1 资源导入器与离线处理
美术交给引擎的是源资源,运行时却不能直接吃:本节用 TypeScript 从零实现 helio 的资源导入器与离线处理——注册表按扩展名分派、版本号驱动失效、PPM 解码、BC1 块压缩、mip 链生成、自描述二进制产物与元数据,并实测真实压缩比与 BC1 往返误差。
category
美术交给引擎的是源资源,运行时却不能直接吃:本节用 TypeScript 从零实现 helio 的资源导入器与离线处理——注册表按扩展名分派、版本号驱动失效、PPM 解码、BC1 块压缩、mip 链生成、自描述二进制产物与元数据,并实测真实压缩比与 BC1 往返误差。
资源加载进来之后,谁拥有它、什么时候可以扔?本节从零实现资源级的引用计数表:acquire 加引用、release 减引用、归零不立刻释放而是挂进待释放队列、帧末 collect 统一释放。用一个「材质释放时级联释放它持有的 mesh」的场景证明延迟释放如何避开 re-entrant 释放,并量出本机约 2100 万次 acquire/release 每秒。
依赖图告诉引擎「先加载谁」,但真正的加载是异步并发的,还要让 UI 看到进度。本节从零实现带并发上限、优先级队列、in-flight 去重与字节进度回调的异步加载器,用注入的假 IO 在 Node 直跑,量出并发从 1 到 8 时加载 64 个资源的墙钟耗时几乎线性下降,并解释为什么进度条要按字节而不是按个数。
资源管线的第一课是把「资源」本身建模清楚:一个 GUID、一个类型、一条路径、一张依赖表。本节从零实现 helio 的资源模型与依赖图,用 Kahn 算法算出「依赖在前」的拓扑加载序,用 DFS 三色标记检测循环依赖,再给出反向边供后续热重载定位受影响资源。全部代码在 Node 直跑,11 条用例锁住行为。
画面能画、能动了,但「动得好不好」要用数字说话。本节给 18.1 的装配流水线做 CPU 逐帧计时,再用 WebGPU 的 timestamp-query 在渲染通道两端打时间戳、读出真实 GPU 纳秒数,跑出可复现的毫秒数字,最后为整卷《渲染引擎》收口。
把「一天中的时刻」映射成一组可直接喂给光照的参数:太阳方向、太阳颜色、天空天顶与地平线颜色、环境光强度。本节在 helio 里从零实现这套映射,再把它原样搬进 WGSL,渲染一天三个时刻的天空并回读像素,要求 CPU 与 GPU 逐像素对齐。
前十七章把 RHI、资源、场景、剔除、排序、光照、帧图、后处理、2D 与多线程各自做成了零件。本节是综合章的第一节,把这些零件装配成一条「纯数据场景 → 帧计划」的流水线,并在真实 WebGPU 设备上把这份帧计划画出来、回读像素,验证整条链路能跑出真实结果。
命令缓冲通道有了、独立编码器拆分也验证了,但谁来切块、谁来合并还没着落。本节把「分块 → 并行录 → 合并」接到卷一那套 Job System 上:用确定性调度模型算 makespan,再用 worker_threads 真跑,实测 4 worker 把 4096 个绘制项的录制墙钟压到约 2.7 分之一,且校验和与串行完全一致。
上一节把命令缓冲做成了一条跨线程通道,但录制本身还是串行的。本节用 OffscreenCanvas 把渲染表面搬离主线程,再把一帧的命令拆成多条独立编码器分别录制、按提交顺序合并,在真实 WebGPU 设备上验证「两条命令缓冲」与「单编码器两趟」逐字节一致。
主线程一边跑游戏逻辑一边录渲染命令,逻辑一卡帧就掉。本节把录制与提交拆到渲染线程上:主线程只往命令缓冲里塞命令,渲染线程按帧取走执行,用一个双缓冲的生产消费环把「还没消费完就覆写」变成一条确定性异常,并跑出背压的真实时间线,看清三缓冲比双缓冲多出的那一份缓冲垫换来了什么。
精灵和矢量图形都能画了,但它们和 3D 世界还没有规矩:谁先画、谁压在谁上面、半透明怎么混。本节把一帧的绘制定成三段——3D 不透明近到远、3D 透明远到近、UI 永远在最上——再在同一个 render pass 里先画 3D 绿底、再用 alpha 混合叠上左半屏的红色 UI,回读出 0.5 红加 0.5 绿的真实像素。
文字和矢量图形由曲线定义,位图放大会糊、图集也装不下任意字号。本节用有向距离场(SDF)把形状变成一段距离函数:距离直接转成抗锯齿覆盖率,一个全屏三角形就能画出边缘半透明的圆角矩形,任意放大都不糊。CPU 参考与 WGSL 逐字对齐,跑出中心全白、角外全黑、边界 128 的真实像素。
2D 与 UI 渲染的第一个问题是「一张界面里成百上千个精灵怎么画」。本节先把小图打进一张 2 次幂图集(货架装箱),再用一条精灵批次键把同层同深度的同页精灵并成一次绘制:10 万个精灵在单页图集下只剩 16 次绘制,最后在真实 WebGPU 设备上跑出一次 draw(6,2) 从图集采出红蓝两个精灵。
后处理链的代价可以提前算出来:每个全屏 pass 都要把纹理读一遍、写一遍,这就是带宽。本节在 helio 里实现一份带宽账本,把 pass 的读写字节按格式与分辨率算清楚,再在真实 WebGPU 设备上实测 1/5/13 抽样三个全屏 pass 的每帧耗时,看成本到底由「固定开销」还是「像素数」主导。
场景 pass 输出的是线性 HDR,显示器要的是 [0,1] 的 LDR,中间隔着色调映射;锯齿则要在边缘上做文章。本节在 helio 里把 Reinhard、ACES、filmic 三个算子与一次 5 邻域 FXAA 接成后处理链上的两个 pass,并在真实 WebGPU 设备上验证 GPU 结果与 CPU 算子逐像素一致。
后处理是一串全屏 pass:每个 pass 读若干张纹理、写一张纹理。本节在 helio 上从零搭出全屏 Pass 链的数据模型——覆盖整个视口的全屏三角形、pass 描述符、链合法性校验,以及用「最后一次使用」回收中间纹理的物理纹理规划,再在真实 WebGPU 设备上跑一条两段链、回读像素逐通道验证。
帧图编译成执行计划只有三步:拓扑排序定顺序、给每个资源分配槽位、把 Pass 的读写原样带上。本节在 helio 里把这三步写成纯 CPU 的编译器,再让浏览器侧的执行器只按计划录命令,把一条乱序声明的三 Pass 帧图跑出正确像素,验证「声明序」与「执行序」彻底解耦。
Vulkan 类 API 的帧图要替开发者插屏障、把瞬态资源别名进共享显存;WebGPU 把这两件事整个从 API 里拿掉了——同步在 pass 边界隐式完成,纹理也没有别名入口。本节在 helio 里把这个约束差异量化:同一批瞬态资源,别名规划器给出两种显存占用,再在真实设备上跑通一条三 Pass 链。
帧图不是一张画出来的图,而是一组 Pass 与它们读写资源的声明。本节在 helio 里从零实现帧图的 IR:把「谁读什么、谁写什么」翻译成数据流依赖与三类 hazard,跑拓扑排序排出执行序,再算出每个资源的生命周期区间,为后面的内存规划与执行打底。
阴影的两大旋钮——每像素采样数与深度偏置——分别换质量和换成本。PCF 半径从 1 到 3 让采样数按平方增长,把硬边软化成柔化像素;偏置从 0 往上加能压住自遮挡粉刺,但加过头阴影就脱离接触点。本节在 helio 里实现采样核与斜率缩放偏置,并在 GPU 上量化这两条权衡曲线。