GPU Gems 2 · Chapter 28. Mipmap-Level Measurement
用 alpha 标定纹理、遮挡查询与参考计数测量实际可见的 mip 层,再以显著性阈值和分摊调度驱动纹理内存管理。
学习目标
- 能解释 Mipmap-Level Measurement 为什么让真实 GPU 采样路径回答“哪些 mip 层可见”,而不是由 CPU 猜测
- 能实现 calibration alpha、reference/query ratio、显著性阈值与分摊调度,并核对状态覆盖和内存结果
- 能回答:距离、表面朝向、阈值和测量间隔改变时,应保留哪一层、会节省多少内存,又可能出现什么画质或延迟风险
先测量用户真正看见的细节
一张大图为远近不同的物体准备了多档清晰度。最高档往往占据最多空间,却可能只在极少视角中出现;永远把它留在显存里,会挤掉其他角色、地形和特效的预算。
问题在于,最终选择不仅由距离决定,还会受到表面朝向、屏幕设置和硬件内部策略影响。与其在主处理器上重建一套不完整的猜测,不如让图形硬件按真实状态再画一次,再把“有多少片元需要某档细节”读成管理决策。先预测:在实验里同时增加距离和掠射角,首个可见层会往 L0 还是 L3 移动?
Mipmap-Level Measurement 实验
先预测:距离和掠射角同时增大时,首个可见层会向更细还是更粗移动?把阈值提高又会释放哪些顶层?
查询比例以 reference fragments 为分母;距离、朝向和真实采样状态共同决定可见层。
本章的 Mipmap-Level Measurement 不是自己实现一套 mip 选择公式,而是把 GPU 已经隐式做出的选择转成可查询的片元数量,再决定哪些高分辨率层值得常驻。
1. 从人工 false color 到 alpha 标定
美术检查常把每个 mip 层涂成强烈不同的颜色:看到哪种颜色,就知道硬件正在采哪一层。这个方法直观,却需要人在画面里寻找少量像素。自动化的关键是把“颜色识别”改写为 GPU 已能高效完成的通过/拒绝测试。
↡为测量而准备的特殊 mipmap;每层用不同 alpha 区间编码,让真实 sampler 把层选择带入像素输出。标定纹理尺寸应与被测纹理一致,采样状态也必须一致。最简单的最高层测试在 L0 写 alpha 1,其余层写 alpha 0;替换目标纹理后重画对象,只让 alpha 大于零的片元通过。RGB 不参与判定,可用于 false-color 调试,或填入有代表性的图案,避免驱动把异常纯色纹理走入不同优化路径。
以未压缩 512×512 RGBA8 为例,L0、L1、L2、L3 分别约占 1024、256、64、16 KB。每降一级边长减半,面积与内存变成四分之一,因此确认 L0/L1 不可见,收益远大于继续分析 mip chain 尾部。
2. 把层选择变成 query count
↡让 GPU 报告一次 draw call 中有多少样本或片元通过可见性测试的异步查询机制。遮挡查询原本用于判断物体是否被挡住;它统计通过深度测试的片元,也会遵守该 draw call 的其他管线状态。原章利用这一点:重放物体、替换标定纹理、关闭颜色写入,再让 alpha 阶段只保留正在测试的 mip 区间。这样 query count 就成为该层对当前画面的可见证据。
↡按照像素输出的 alpha 与参考值决定片元继续还是被丢弃的固定功能测试;现代着色器可用 discard 模拟同一判定。原始 Direct3D 路径会覆盖 pixel shader、alpha-test states 与 texture,并使用 D3DRS_COLORWRITEENABLE 禁止标定颜色写进画面。现代 API 可在 fragment shader 中执行等价阈值判断;但 WebGL2 的 ANY_SAMPLES_PASSED 只提供布尔结果,不直接返回原章所需的精确计数,移植时要选择支持 samples-passed 计数的宿主 API,或改用分级布尔策略。
float encoded = texture(calibrationMip, uv).a;
float remapped = clamp(4.0 * (encoded - levelOffset), 0.0, 1.0);
if (remapped <= alphaReference) discard;
// color attachment writes disabled; depth test remains identical
outColor = vec4(0.0);单独重放一次无 alpha test 的 reference query,得到当前物体所有可见片元数。它自动包含遮挡、裁剪和视口变化;若省略分母,同样的 650 个片元在近景小物体与满屏地形上会被错误解释为同等重要。
3. 一张纹理测多个 mip 区间
不必为每个被测层创建一张标定纹理。原章把 L0 到 L3 的 alpha 依次设为 1、0.75、0.5、0.25;trilinear filtering 会在相邻层之间产生连续梯度。第 次重放仅改变 offset,把目标区间缩放回 alpha test 的完整动态范围。
本节符号如下:
- :标定纹理采样得到的 alpha
- :测试第 层时使用的偏移
- :重映射后送入 alpha test 的值
- :第 层查询计数
- :参考查询计数
- :第 层相对可见比例
这个式子在说:四倍缩放把每个四分之一宽的 alpha 区间扩展到零至一,使 alpha test 对四个层级都使用同样的精度和参考值。
查询必须异步读取,不能在提交后立刻等待,否则 CPU/GPU pipeline 会被同步点卡住。实践中把 query handle 与对象、frame index、被测层一起放进 pending 队列,后续帧确认结果可用后再解释和更新资源状态。
for (const level of measuredLevels) {
beginSamplesPassedQuery(object.id, level);
replayWithCalibration(object, levelOffsets[level]);
endSamplesPassedQuery();
}
beginReferenceQuery(object.id);
replayWithoutAlphaReject(object);
endReferenceQuery();4. reference ratio 与显著性阈值
↡把某层查询数除以 reference query 后,与之比较的画面占比下限;低于它的细层可视为视觉上不显著。先把绝对计数归一化:
这个式子在说:从最细层开始寻找第一个达到阈值 的层 ;比它更细的层只影响过少片元,可以从当前纹理预算中去掉。必须保护 的完全不可见对象,并把它交给不可见资源策略,而不是除零。
原章给出的典型计数为 reference 13000、L0 0、L1 650(5%)、L2 10530(81%)、L3 13000(100%)。采用 15% 阈值时,L2 是首个显著可见层,因此 L0/L1 可移除。阈值越高,内存越省,但局部高细节表面会更早变软;它是明确的画质—空间旋钮,不是越大越好。
5. 在引擎里分摊五次附加绘制
↡把待测对象排成循环队列,每帧只测一个或少量对象,使多次重放成本分散到多个帧。测四个层加一个 reference,需要五次附加 draw call。若每帧对所有对象执行,draw 数会膨胀到不可接受;但纹理层通常随相对位置缓慢变化,没有必要每个对象每帧重测。原章在 Leviathan 引擎中每帧从循环队列取一个对象,因此固定只增加五次 draw calls。
每个 buffered draw call 要能重放并局部覆盖状态:pixel shader、标定 texture、alpha 判定与 color-write mask;几何、depth、采样状态和通常的 view transform 保持一致。如果希望测量视锥外对象,可以临时重设 view,但这回答的是另一个问题,结果不可与当前画面直接混用。
简单循环队列会产生延迟:对象快速接近相机时,新高分辨率层可能在若干帧后才被重新请求,出现 resolution pop。改进方法是让速度大、投影变化快或刚进入视野的对象提高优先级,并设置升清晰度快、降清晰度慢的 hysteresis,避免阈值附近反复换层。
6. 朝向、阈值与实际内存收益
相同距离不能推出相同层级。平坦地形以掠射角进入视野时,一个屏幕像素覆盖很长的纹理区域,常落到较粗层;正对相机的崖壁即使等距,也可能暴露更细层。这正是 CPU 距离近似最容易漏掉、而真实 sampler 已自然纳入的变量。
原章在 Leviathan terrain 的样本中报告:不测量时使用 4.333 MB;0%、15%、30% 阈值分别降到 0.864、0.538、0.464 MB,对应 80%、88%、89% 节省。数字属于当时的特定场景与引擎,不应当作现代项目保证;可迁移的是测量方法,以及“高阈值继续省内存但逐步降低画质”的趋势。
扩展方向有三类。第一,调整标定纹理与 viewport 的相对尺寸,可把 magnification 也编码进测量范围。第二,给每层 alpha 加入同源、缩放偏置的确定性纹理,使小幅距离变化也有片元穿过阈值,可观察层级上升、下降或静止的趋势。第三,支持导数指令的 shader 可用 ddx/ddy 直接估计 mip level,省去标定纹理带宽,但结果回读仍需 query 或 reduction 机制。
三步验收:编码、解释、调度
三个阶段使用同一 mesh、相机路径和 sampler preset。先证明 alpha 编码隔离了目标层,再证明 ratio 与阈值结论正确,最后才把结果接入纹理常驻策略。
第一步:核对 calibration alpha 与状态覆盖
逐层检查 1、0.75、0.5、0.25 的 alpha,记录 offset 与 alpha-test 输出;重放时仅覆盖 shader、texture、alpha 判定和 color-write,保留真实 depth、geometry 与 sampler。
本章小结
- calibration texture 让真实 sampler 把层选择编码进 alpha。
- occlusion query 与 reference query 把可见证据变成相对比例。
- significance threshold 决定首个值得常驻的细节层。
- round-robin 将四层加参考值的五次重放分摊到多帧。
- 距离、朝向、过滤状态与调度延迟必须共同进入资源决策。
练习
问题 1|解释查询结果。 某对象的 reference 为 20000,L0 到 L3 查询计数依次为 400、2400、15000、20000。阈值为 15% 时首个显著层是哪一层?L0/L1 能否同时移除?
问题 2|设计调度策略。 场景有 600 个稳定地形块和 20 个快速角色,四层测量每对象需要五次附加 draw。怎样控制成本并减少角色靠近镜头时的 resolution pop?
问题 3|修改实验并提交证据。 在 MeasurementLab 中把 distance、grazing angle、threshold 和 cadence 分别推到高值。你应保存哪些状态,如何判断“省内存但不可交付”?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- calibration texture
- occlusion query
- alpha test
- reference query
- significance threshold
- round-robin measurement