Deep Read

双线性插值算法

双线性插值(Bilinear Interpolation)主要用来解决纹理采样中的图像失真。纹理被放大(Magnification)或缩小(Minification)贴到模型表面时,如果不做合适的插值,就会出现明显的像素化(马赛克)或锯齿(走样的成因见 锯齿和抗锯齿锯齿和抗锯齿锯齿产生的原因 锯齿(Aliasing)的根本原因是采样频率不足以还原原始信号的全部信息,在信号处理领域这叫"走样"。放到计算机图形学里,可以拆成三层来看。 连续的场景 vs. 离散的像素。 我们想渲染的 3D 场景在数学上是连续的:一个三角形的边缘是一条无限细的直线,两种颜色的交界是瞬时突变,这种突变包含了无限高的频率。而屏幕是一张离散的像素网格,光栅化(Rasterization,它在管线中的位置见 [[渲染流水线完全解析]])就是在这张离散网格上对连续场景做采样——每个像素中心可以看作一个采样点,它判断自己是否被三角形覆盖,然后取一个单一颜色。 违反奈奎斯特-香农采样定理。 要无损还原一个信号,采样频率必须至少是信号最高频率的两倍。而理想几何边缘包含无限高频,屏幕分辨率(采样频率)却是有限的,所以无论分辨率多高都无法满足该定理。采样率不足时,捕捉不到的高频信息会"折叠"回低频域,产生原信号里并不存在的错误低频——视觉上就表现为阶梯状的像素块,也就是锯齿。 说得更直白些:光栅化时一个像素只能有一个颜色,当三角形边缘穿过某个像素,这个像素必须做"非黑即白"的决定——要么三角形)。

问题来源:UV 坐标与纹素的错位

渲染管线里,我们为模型每个顶点指定 UV 坐标(纹理坐标),它对应纹理图上的一个点。三角形被光栅化成屏幕像素(Fragment)时,每个像素的 UV 是由三角形顶点 UV 插值得到的,结果通常是浮点数,例如 uv(0.35, 0.72)

而纹理图是由离散的纹素(Texel)组成的,纹素坐标是整数。于是就产生了矛盾:我们必须决定如何用一个浮点 UV 去整数网格的纹理上"取色"。双线性插值正是对这个问题的一种回答。

对照:最近邻采样

最直接的做法是把浮点 UV 四舍五入到最近的整数坐标,直接取那个纹素的颜色,即最近邻采样(Nearest-neighbor Sampling)。

  • 优点:极快,只需一次取整和一次内存读取。
  • 缺点:纹理放大时,多个屏幕像素会映射到同一个纹素、取到完全相同的颜色,屏幕上就形成大块色块,也就是马赛克/像素化。

双线性插值的思想

双线性插值不再只取"最近"的一个纹素,而是取浮点采样位置周围最邻近的 4 个纹素,按采样点到它们的距离加权平均,算出最终颜色。因为要在两个方向上各做一次线性插值,所以叫"双线性":

  • 第一次线性插值(竖直方向):用 0 号点和 2 号点插值得到 left 的颜色,用 1 号点和 3 号点插值得到 right 的颜色。
  • 第二次线性插值(水平方向):再用 leftright 插值,得到目标点 p 的颜色。

推导

设 p 点坐标为 (xp, yp),周围四个纹素坐标如图:

先在竖直方向求出 left、right 两个中间颜色。以 left 为例,按 p 到上下两点的距离比例加权:

d0     = y0 - yp
d      = y0 - y2
yScale = d0 / d

leftColor  = c2 · yScale + c0 · (1 - yScale)
rightColor = c3 · yScale + c1 · (1 - yScale)

再在水平方向,用 left、right 按 p 的横向位置插值,得到最终颜色:

dl     = xp - x2
d      = x3 - x2
xScale = dl / d

colorP = rightColor · xScale + leftColor · (1 - xScale)

两次线性插值之后,就用 p 点周围的四个纹素"合成"出了 p 点的新颜色。相比最近邻,颜色在纹素之间实现了平滑过渡,从而消除了像素化。