GPU Gems 1 · Chapter 25. Fast Filter-Width Estimates with Texture Maps
用编码 mipmap 的 filterMap 借助纹理硬件估计任意程序量的变化宽度,在不支持 ddx/ddy 的 fragment profile 上实现可控的程序抗锯齿。
学习目标
- 能解释程序纹理为什么需要 filter width,并区分单点采样、区域平均与硬件纹理过滤
- 能实现标准
ddx/ddy版 filter width,并说明它在不支持 derivative 的 fragment profile 上为何不可用 - 能构造每级保存 level 常量的 filterMap,解码纹理硬件返回的 mip 级别并恢复 width
- 能识别 mipmap 上下限、过度估计、纹理内存和压缩格式对发布质量与性能的影响
程序纹理把 checkerboard、条纹、噪声和其他函数直接写进 fragment shader,省掉了预制纹理,却也失去了纹理硬件自动提供的 filter 信息。当镜头远离表面或物体运动时,一个屏幕像素覆盖越来越大的程序坐标范围;如果 shader 仍只在一个点计算函数,高频细节就会折叠成 alias。
↡一个屏幕像素覆盖的程序坐标变化范围,用于决定函数需要被预过滤或平均的区域 是把采样率带入程序纹理的接口。对二维 uv,可以用相邻像素差的最大分量得到一个各向同性近似;它通常宁可略微高估,也不应低估到让高频细节穿过像素采样。
1. 先理解 filter width:程序 checker 为什么会 alias
假设 checker 函数根据 floor(uv.x) + floor(uv.y) 返回零或一。单点版本只知道当前坐标落在哪个格子,却不知道一个屏幕像素跨过了多少个格子。理想结果应该是对 pixel footprint 内的 checker 面积求平均;当 footprint 横跨黑白边界时,输出就应该是中间灰度而不是突然跳变。
float checker(float2 uv) {
return (fmod(floor(uv.x) + floor(uv.y), 2) < 1) ? 0 : 1;
}↡用相邻像素的值差近似当前值相对屏幕 x 与 y 的变化量的 fragment shader 函数 可以从这个变化量估计窗口。现代 Cg/HLSL profile 通常提供 ddx() 和 ddy():
float filterwidth(float2 value) {
float2 fw = max(abs(ddx(value)), abs(ddy(value)));
return max(fw.x, fw.y);
}这个宽度是 ↡用一个标量近似 x、y 方向最大变化范围的过滤宽度,可能比真实的各向异性 footprint 更宽,但它倾向于避免低估。得到 width 后,可以把 checker 函数变成对区间的平均,或者把它交给一个已有的 mip、lookup 或 analytical filter。
↡通过预过滤或区域平均把程序函数中超过像素采样率的高频细节移除的过程 的关键是先确定变化速度,再决定要保留多少细节。只在结果上加一个固定 blur 并不能适应物体缩放;width 随屏幕坐标变化,过滤半径也应该变化。
2. 用编码 mipmap 让纹理硬件“报告”变化范围
在不支持 derivatives 的 profile 上,章节利用纹理采样硬件的一个特性:普通 texture lookup 会根据相邻像素的纹理坐标自动选择 ↡纹理 mip 链中代表特定分辨率与过滤范围的层级编号。只要构造一个特殊的 ↡每级 mipmap 存放可解码 level 值、用于让 texture lookup 间接报告 filter width 的辅助纹理,让 level 0 的值为零、level 1 的值为一,以此类推,返回值就携带了硬件选择的 mip 级别信息。
构造完成后,纹理硬件并不知道自己在做“导数估计”,它只是按通常的过滤规则在相邻 mip 之间插值。我们读取的 fractional level 近似于 log2 的纹理 footprint,再通过指数运算恢复 width:
uniform sampler2D filterMap;
float filterwidthFromTexture(float2 uv) {
float log2width = (255.0 / 16.0) * tex2D(filterMap, uv).x;
return exp2(-log2width);
}这里的比例因子来自资源编码:纹理通道通常归一化到零到一,写入 mip 的 level 还可以额外乘一个常数以扩大有效范围。实际工程要把 filterMap 的尺寸、mip 数、格式、过滤模式、寻址模式和解码系数作为同一个资源契约版本化。
3. 用 width 过滤程序函数,而不是只画得更模糊
得到 width 后,最简单的 checker 近似是围绕当前坐标采样两个边界并计算平均;更复杂的程序函数可以选择一个预过滤 LUT、积分表达式或按 width 调整采样数量。目标是让函数变化的频率不超过当前像素能够表示的频率。
float width = filterwidthFromTexture(uv);
float2 lo = uv - 0.5 * width;
float2 hi = uv + 0.5 * width;
float average = checkerIntegral(hi) - checkerIntegral(lo);
return average / max(width, 0.0001);过滤函数的误差方向很重要:width 略高会产生过度模糊,但通常比 width 过低造成 alias 更可接受。对各向异性表面,单个 isotropic width 会丢失方向信息;可以分别保存 x、y 变化,或使用多个样本估计椭圆 footprint,但那会增加带宽和 shader 成本。
4. 上下限、压缩与 profile 兼容性
编码 mipmap 的最高级别限制了最大可分辨 width:当函数变化快到需要比顶层还大的过滤范围时,多个不同的大 width 都会返回同一个顶层值,结果可能重新 alias。反过来,base level 限制了最小 width:缓慢变化的值会被当成同一个最细 level,造成轻微过度过滤或 width 高估。
这种方法的性能接近 derivative 版本,却需要一个 texture unit 和额外纹理内存。因为每一级的纹理值是常量,它很适合压缩格式;但压缩、通道归一化和 mip 过滤不能改变 level 的单调关系。发布前应在 profile 支持、资源大小、缓存命中、最大最小变化速度和 fallback 行为之间做完整测量。
5. 动手实验:比较两条 width 路径
先预测四个变化:提高 pattern scale 或 screen motion 会扩大真实 width;降低 mipmap range 会让 texture method 在极端快变或慢变处失真;prefilter quality 降低会提高 checker alias;ddx/ddy 与 encoded filterMap 的 width 应在覆盖范围内接近,但不必逐像素完全相等。
Filter Width Lab
先预测:mipmap 层数不足时,width 会在哪一端失真?
观察:texture method 用一次硬件查找换取 profile 兼容性;mipmap 范围太窄会夹断 width,quality 低会增加 checker alias。
这个实验用确定性的 checker 柱状图表达估计关系,不替代 GPU profiler。发布前要对近景、远景、掠射角、快速运动、静止平面、不同纹理坐标函数、资源压缩和不支持 derivatives 的目标 profile 回归,并保存 reference width、alias、ALU、texture unit 和显存数据。
小结:用硬件选择 mip 反推出程序变化率
- filter width 描述一个像素覆盖的程序坐标范围,是 procedural antialiasing 的输入。
- derivative 版用
ddx/ddy直接估计局部变化,但并非所有 fragment profile 都支持。 - filterMap 为每级 mip 写入可解码 level 值,利用纹理硬件返回的 mip 选择近似
log2 width,再用exp2恢复宽度。 - 这个方法成本接近 derivative 路径,却消耗 texture unit 和纹理内存;常量 mip 很适合压缩,但资源契约必须保持单调。
- mipmap 上下限会夹断极快或极慢的变化,发布实现要有覆盖范围测试和 fallback。
练习
问题 1|改 Demo 代码 标准 profile 中 ddx(uv) 的绝对值最大分量为 0.125,ddy(uv) 的最大分量为 0.25。按 filterwidth() 的各向同性定义,width 是多少?它变成原来的两倍时,checker 应如何调整过滤范围?
问题 2|推导题 filterMap 查表返回归一化值 0.25,解码公式是 log2width = (255 / 16) × value,随后 width = exp2(-log2width)。写出解码过程的近似值,并说明为何资源归一化不能遗漏。
问题 3|独立实现题 为不支持 ddx/ddy 的旧 fragment profile 设计 checkerboard 的抗锯齿方案。请说明 filterMap 的制作、运行时解码、覆盖范围限制和至少五个测试场景。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- filter width
- 一个屏幕像素覆盖的程序坐标变化范围,用于决定过滤区域。
- derivative
- 用相邻像素值差近似 fragment 中局部变化量的函数。
- procedural antialiasing
- 通过预过滤或区域平均移除程序纹理中超过采样率的高频细节。
- filterMap
- 每级 mip 存放可解码 level 值、用于间接估计 filter width 的辅助纹理。
- mipmap level
- 纹理 mip 链中代表特定分辨率与过滤范围的层级编号。
- isotropic filter width
- 用一个标量近似 x、y 方向最大变化范围的过滤宽度。