像素画纹理过滤:为什么必须用 Nearest
像素画游戏在放大渲染时,必须使用 Nearest(最近邻)过滤而非 Bilinear(双线性插值),原因涉及 GPU 纹理采样管线、像素画的离散美学特征、以及双线性插值的数学本质。
GPU 纹理采样底层原理
Nearest(最近邻)— 1 次采样
texel_x = floor(u * width)
texel_y = floor(v * height)
return texture[texel_x][texel_y]
将 normalized UV 坐标映射到纹素范围并选取最近的纹素,读取 1 个纹素直接返回。若用 texel-center 坐标描述,也可写作 x = u * width - 0.5 后选择最近的整数中心。结果永远是原始纹理中存在的颜色值,绝不创造新颜色。
Bilinear(双线性插值)— 4 次采样
1. 转换到 texel-center 坐标: x = u * width - 0.5, y = v * height - 0.5
2. floor 得到左上角纹素索引: i = floor(x), j = floor(y)
3. 计算小数偏移: fx = fract(x), fy = fract(y)
4. 读取 2×2 纹素四元组: c00, c10, c01, c11
5. 两次线性插值:
top = lerp(c00, c10, fx)
bottom = lerp(c01, c11, fx)
result = lerp(top, bottom, fy)
读取 4 个纹素,通过加权平均在颜色边界处通常会产生原始纹理中不存在的新颜色。
来源:Vulkan 1.3 规范 §16.8、《Real-Time Rendering》4th Ed. §13.4-13.5
为什么双线性插值摧毁像素画
像素画的视觉灵魂建立在三个原则上:
- 锐利的颜色边界 — 每个像素的颜色是精心选择的,边界处无渐变
- 严格的颜色调色板 — 通常只有 4~32 种颜色
- 轮廓由像素边界定义 — 形状辨识度依赖像素网格
双线性插值对每一条原则都是致命的:
- 在颜色边界创造不存在的中间色(如黑色/白色边界处产生灰色”幽灵像素”)
- 软化轮廓,使形状变得模糊
- 在非整数缩放时(如 2.5×),让边界落在屏幕像素之间,从而产生混色和非像素精确的模糊边缘
开发者社区的经典描述:
“当你不小心对像素画用了双线性过滤,看起来就像有人在屏幕上涂了凡士林。每个锐利的像素边缘都变成了模糊的渐变。“
纹理游走(Texture Swimming)
双线性插值在动态场景中最致命的问题:
当摄像机以亚纹素精度(sub-texel)移动时,小数偏移 (fx, fy) 持续变化,导致:
- 相邻屏幕像素采样到略有不同的混合比例
- 产生微妙的闪烁/游走/扭曲效果
- 对只有 4-32 种颜色的像素画,这种色彩污染会被立刻感知
Nearest 是阶跃函数 — 在一定范围的坐标内始终选中同一个整数纹素,边界是硬切换,没有中间混色状态。因此它不会产生 Bilinear 那种连续混色的 texture swimming;但在亚像素移动或非 pixel-perfect 摄像机下,仍可能出现离散跳变、抖动或闪烁。
GPU 硬件管线完整流程
UV 坐标 → [地址模式] → [Mip 级别选择] → [过滤模式] → 输出颜色
Mip 级别选择机制(quad 执行模型,简化表达):
- GPU 2×2 像素 quad 中,水平和垂直相邻片元用于估计 UV 对屏幕坐标的导数
- 常见近似可写作:
ρx = sqrt((du/dx * width)^2 + (dv/dx * height)^2),ρy = sqrt((du/dy * width)^2 + (dv/dy * height)^2),λ = log₂(max(ρx, ρy)) - 实际实现还会受到显式 LOD、LOD bias、clamp、各向异性过滤和 GPU 实现细节影响
- Nearest + Mip = 选 1 个 mip 级别 + 取 1 个纹素
- Linear + Mip(三线性)= 选 2 个 mip 级别 × 每级 4 个纹素 = 8 次采样
来源:SPIR-V 规范 §3.23(Derivative Instructions)、NVIDIA Ada 架构白皮书
根本权衡
| 维度 | Nearest | Bilinear |
|---|---|---|
| 采样次数 | 1 | 4 |
| 创造新颜色 | 绝不 | 在颜色边界处通常会 |
| 边缘外观 | 锐利精确 | 模糊抗锯齿 |
| 非整数缩放 | 可能出现不均匀像素宽度 / snapping | 边界模糊、混色、非像素精确 |
| 时间稳定性 | 阶跃切换 | 平滑但有游走 |
| 最适合 | 像素画、UI、文字 | 照片、3D 纹理、渐变 |
各向异性过滤(AF)与像素画
AF 对像素画无帮助:
- AF 解决的是透视角度下的各向异性投影问题(如远处的地板)
- 像素画通常使用正交投影(各向异性比接近 1:1),AF 的收益很小
- AF 通常仍依赖线性/多点采样,无法保留像素画的硬边界;具体采样次数和 mip 参与方式取决于 GPU 与采样器实现
各种过滤模式对比
| 过滤类型 | 纹素采样数 | Mip 级别数 | 最适合场景 |
|---|---|---|---|
| Nearest | 1 | 1 | 像素画、复古图形 |
| Bilinear | 4 | 1 | 3D 放大 |
| Trilinear | 8 | 2 | 3D + Mipmaps |
| Anisotropic (16×) | 实现相关,可能显著多于 Bilinear | 实现相关 | 透视角度表面 |
业界实践
所有顶级像素画游戏都使用 Nearest 过滤:
- Celeste — 内部分辨率 320×180,Nearest 放大到屏幕
- Stardew Valley — 16×16 瓦片,固定虚拟分辨率 + Nearest 缩放
- Shovel Knight — NES 风格 400×240,严格 Nearest
- Undertale — 简洁像素画,锐利渲染
- Dead Cells — 像素精灵 + 流畅动画,严格 Nearest
结论
像素画的每一个像素都是艺术决策。双线性插值在颜色边界处通常会创造不存在的中间色,摧毁这种精心设计的离散性。Nearest 是唯一能在放大时保持原始像素 1:1 忠实还原的基础过滤方式。这不只是”好看不好看”的问题,而是正确性问题 — 线性插值从数学上就不是为离散的、像素级精确的美术资源设计的。
参考来源
- Vulkan 1.3 规范 §16.8:Image Operations — https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap16.html
- D3D12 Texture Filtering — https://learn.microsoft.com/en-us/windows/win32/direct3d11/resource-binding-in-d3d11#texture-filtering
- SPIR-V 规范 §3.23:Derivative Instructions — https://registry.khronos.org/SPIR-V/specs/1.2/SPIR-V.html#Derivatives
- 《Real-Time Rendering》4th Ed. §13.4-13.7(Akenine-Möller, Haines, Hoffman)
- GPU Gems Ch.14:各向异性过滤 — https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch14.html
- GPU Gems 3 Ch.22:快速纹理过滤 — http://developer.nvidia.com/gpugems/gpugems3/part-iv-image-processing/chapter-22-fast-filtering-texture-gpu
- GPUOpen:AF Explained — https://gpuopen.com/learn/anisotropic-filtering-explained/
- NVIDIA Ada 架构白皮书
- Wikipedia:Pixel-art scaling algorithms — https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms
相关页面
- bevy-image-filtering-nearest-vs-linear — Bevy API 层面的对比
- bevy-pixel-perfect-rendering — Bevy 像素完美渲染方案(双相机管线 + 整数倍缩放)
- bevy — Bevy 引擎实体页