GPU Gems 2 · Chapter 41. Deferred Filtering: Rendering from Difficult Data Formats

用两遍 deferred filtering 把压缩、稀疏或 flat 3D 数据重建为可被硬件插值的局部纹理,并在质量、指令与驻留内存之间做选择。

学习目标

  • 能解释为什么困难数据布局要先局部重建,再交给纹理硬件完成过滤
  • 能修改 Deferred Filtering Lab 的重建成本、采样密度和切片数,比较两遍与单遍路径的指令和驻留内存
  • 能回答:面对压缩体数据、动态 flat 3D texture 和普通 3D texture,什么场景值得采用 deferred filtering?

先把“难搬的货”放到传送带上

想象仓库里有一批货物:为了节省空间,货物被装进带编号的箱子,编号还指向另一排说明卡。仓库机器人可以很快做计算,却不懂这套间接编号;如果每次取一件货都重新查八个邻居,传送带会被重复搬运堵住。

本章解决的问题是:如何先把需要的局部货物整理成标准小托盘,再让后面的机器用现成的平滑搬运能力反复取样?没有这一步,画面放大或做体渲染时,重复解码会把片元指令和纹理读取推到不可交互的程度。

计算友好的布局,不一定是采样友好的布局flat 3D texturepacked slices / sparse tileskey texturekey = 7lookup codebookcodebookreconstruct sample硬件的线性过滤器看不懂这条间接地址链

1. 困难格式:计算时省空间,采样时多一步寻址

GPU 上的数据不一定是一张规则的 2D 或 3D 纹理。云、火焰和隐式表面常把三维域压成二维切片、稀疏 tile,或者把一组相似块共享到一个压缩表里。这样的布局更适合更新和计算,但渲染器无法把一个坐标直接交给硬件过滤器。

在原书的例子里,每个数据块保存一个 key;key 指向 codebook 中的代表值。这样相似的块可以共用存储,但一次采样要先读 key,再读代表值,最后才能得到一个可过滤的样本。

直接让片元程序手写三线性过滤,会把一次采样变成八个邻域重建和多次插值。放大体数据时,许多相邻片元还会重复重建完全相同的 texel,计算量随着画面片元数一起膨胀。

计算友好的布局,不一定是采样友好的布局flat 3D texturepacked slices / sparse tileskey texturekey = 7lookup codebookcodebookreconstruct sample硬件的线性过滤器看不懂这条间接地址链

2. deferred filtering:把重建与过滤拆成两遍

第一遍只负责读取困难格式并重建当前需要的 data slice;它不必知道最终画面会从这一层采样多少次。第二遍读取相邻的重建 slice,做最后一次 LERP、着色和光照。重建成本因此按 texel 支付一次,而不是按每个画面 fragment 重复支付。

先重建一次,再让硬件反复过滤Pass Akey + codebook重建 slicenative-resolution slicePass Bslice i + slice i+1输出图像hardware trilinear每个 texel 只付一次 reconstruction代价从“每个 fragment 解码 8 次”移到“每个 texel 解码 1 次”两张相邻 slice 足够支撑当前采样窗口,内存随切片增量推进

下面的 WebGL2 片元函数对应第二遍的核心:textureItextureJ 已经是普通切片,axisLerp 对当前轴向切片的所有片元都相同,因此只需要两次读取和一次插值。

vec4 deferredTex3D(
  vec2 texCoord2D,
  float axisLerp,
  sampler2D textureI,
  sampler2D textureJ
) {
  vec4 valueI = texture(textureI, texCoord2D);
  vec4 valueJ = texture(textureJ, texCoord2D);
  return mix(valueI, valueJ, axisLerp);
}

这段代码刻意不包含 key、codebook 或八邻域的解码逻辑。它的职责是把已经整理好的局部数据交给纹理读取和硬件插值;第一遍的复杂性被隔离在重建 pass 内。

3. 三线性过滤:沿主要体轴滑动窗口

原书不使用任意方向的 slice plane,而是选择最接近观察方向的体轴做 axis-aligned slicing。每次先重建 i + 1,然后在 ii + 1 之间绘制 sample slices;平面内的过滤交给纹理硬件,轴向的最后一步由 axisLerp 完成。

这个选择让当前切片上的轴向坐标对所有 fragment 都相同,axisLerp 可以作为常量传入 shader。视线与某个体轴越接近,所需的切片顺序越自然;视线变化时,只需切换主要体轴和对应的存储访问方式。

deferred filtering 的四个关键时刻1decodekey → codebook2reconstructnative slice3LERPi / i + 14shadefiltered sample只让两个相邻 slice 驻留,sample window 随切片向前滑动点击播放、暂停、单步或拖动进度,观察 reconstruction 与 filtering 的边界

第 1 / 4 步 · 读取困难格式

第一遍负责理解困难格式,第二遍负责把过滤交给固定功能单元。

让切片方向与观察方向尽量对齐volume slicescurrent windowii + 1axisLerp 在两层之间取样不画任意 slice plane,而是选择主要体轴并逐层推进
// 每个当前 sample slice 只在相邻重建层之间插值
float axisLerp = slicePosition - floor(slicePosition);
vec4 lower = texture(reconstructedI, texCoord2D);
vec4 upper = texture(reconstructedJ, texCoord2D);
vec4 sample = mix(lower, upper, axisLerp);
vec3 shaded = shadeVolume(sample, gradient(sample));
outColor = vec4(shaded, sample.a);

这里的 axisLerp 不是对整段体数据一次性插值,而是当前相邻切片窗口内的局部比例。完成这一段窗口后,旧的 i 可以被丢弃,i + 1 成为下一轮的 i

4. flat 3D texture:动态数据也能保持增量渲染

动态体数据经常由 GPU 自己生成:模拟、图像处理、分割或形变会持续更新它们。受限于当时的内存模型,三维域可能被存成多张 2D slice,也可能被压成一张 flat 3D texture。deferred filtering 把“计算布局”和“渲染布局”解耦,所以渲染器不用强迫计算阶段改用另一种存储。

用两张 slice 推动整段体数据slice ireconstructedsample slicesfinal LERP + shadeslice i + 1becomes next iwindow = 2 slices,空间成本与体数据深度无关

关键是增量窗口:不必同时重建整个体,只需要当前的两张 slice。空间开销主要跟切片分辨率和两层窗口有关,而不是跟体数据的深度线性增长;时间上则把每个 texel 的解码成本支付一次。

5. 何时延后:用一次额外 pass 换掉大量重复读取

把重复的重建从 fragment 热点拿走单遍手写 filtering8 个邻居 × 4 条重建指令+ 8 次读取 + 14 次 LERP/ADD54instructions / filtered readdeferred filtering每个 texel 只重建一次两次读取 + 一次最终 LERP3instructions / filtered read额外的一遍 pass 换来更少的重复 texture reads 和算术

官方示例给出一个直观数量级:单遍手写三线性过滤需要重建 8 个 texel、8 次纹理读取和 14 次 LERP/ADD,总计约 54 条片元指令;deferred 路径在最终读取中约为两次纹理读取加一次 LERP,即 3 条指令,另加一次性的重建 pass。

先预测:在下面的实验中,把 reconstruction cost 调高,哪一种路径的估算指令增长更快?把 samples per voxel 调高,为什么单遍路径更容易失控?打开误区开关,观察“直接打开线性过滤”为什么不能替代第一遍重建。

当前路径:两遍 deferred
单遍估算指令6,912
两遍估算指令640
单次过滤读取3
驻留 slice 数2
估算收益10.8×
推荐路径:每个 texel 重建一次,当前窗口只保留两张 slice。

三步验收:从间接地址到可过滤的体采样

分步1 / 3

第一步:识别困难格式并隔离重建

先画出 key、codebook、稀疏 tile 或 flat 3D 地址之间的关系,再把重建限制在当前需要的原生分辨率区域。不要让最终画面中的每个 fragment 都重复追踪同一条地址链。

计算友好的布局,不一定是采样友好的布局flat 3D texturepacked slices / sparse tileskey texturekey = 7lookup codebookcodebookreconstruct sample硬件的线性过滤器看不懂这条间接地址链
先重建一次,再让硬件反复过滤Pass Akey + codebook重建 slicenative-resolution slicePass Bslice i + slice i+1输出图像hardware trilinear每个 texel 只付一次 reconstruction代价从“每个 fragment 解码 8 次”移到“每个 texel 解码 1 次”两张相邻 slice 足够支撑当前采样窗口,内存随切片增量推进

本章小结

  • 困难格式适合计算,不一定适合硬件过滤。
  • deferred filtering 把重建和最终过滤拆成两遍。
  • axis-aligned slicing 用两张相邻 slice 完成轴向 LERP。
  • 每个 texel 一次重建,避免每个 fragment 重复解码。
  • 滑动窗口让动态体数据只驻留两层。

练习

问题 1|排出两遍路径。 请写出从压缩体数据到最终着色的步骤,并说明为什么第一遍应该在原生分辨率重建,而不是直接按最终画面分辨率重建。

问题 2|修改 Demo 代码。 为 Deferred Filtering Lab 增加 windowSlices 控件,比较驻留 2 张、3 张和完整体数据三种窗口,并把“估算指令”与“驻留 slice 数”分开显示。你需要防止哪种误导?

问题 3|场景选型。 压缩云体、GPU 动态生成的 flat 3D texture、普通规则 3D texture,哪些最适合 deferred filtering?如果重建完全免费,是否仍然有理由使用它?

名词解释

本章出现的专业名词,用大白话再讲一遍。

difficult data format
vector quantization
codebook
deferred filtering
trilinear filtering
axis-aligned slicing
flat 3D texture

讨论

评论区加载中…