需要预先学习:


1. 插值

在渲染管线中,光栅化阶段(见 着色与渲染管线)需要为三角形内部的每个片元确定颜色、法线、纹理坐标等属性,而顶点只提供了属性在三个顶点处的值。三角形内部的属性要靠插值(interpolation)推算:已知离散的顶点值,估计连续区域内的值。


1.1 线性插值

线性插值(linear interpolation)是最基本的插值方式:在两个已知值之间按比例取中间值。一维情况下,在 aa、bb 之间按参数 t∈[0,1]t \in [0, 1] 插值:

lerp(a,b,t)=(1−t)⋅a+t⋅blerp(a, b, t) = (1 - t) \cdot a + t \cdot b

t=0t = 0 得到 aa,t=1t = 1 得到 bb,t=0.5t = 0.5 得到中点。这个式子对二维点、三维向量同样成立,逐分量计算即可。


1.2 重心坐标

三角形内部任意一点的位置可以用重心坐标(barycentric coordinates)表示:用三个非负权重 (α,β,γ)(\alpha, \beta, \gamma) 描述点相对三个顶点的占比:

P=αA+βB+γC,α+β+γ=1P = \alpha A + \beta B + \gamma C, \quad \alpha + \beta + \gamma = 1

其中 AA、BB、CC 是三角形三个顶点。点 PP 在三角形内部,当且仅当 α,β,γ≥0\alpha, \beta, \gamma \ge 0。

重心坐标有直观的几何意义——面积比:α\alpha 等于点 PP 与边 BCBC 围成的小三角形面积占整个三角形的比例,其余两个同理:

α=S△PBCS△ABC,β=S△PCAS△ABC,γ=S△PABS△ABC\alpha = \frac{S_{\triangle PBC}}{S_{\triangle ABC}}, \quad \beta = \frac{S_{\triangle PCA}}{S_{\triangle ABC}}, \quad \gamma = \frac{S_{\triangle PAB}}{S_{\triangle ABC}}

实际计算时用向量叉积求面积,或者直接解线性方程组。值得一提的关联:光栅化 中判断"点是否在三角形内"的叉积法,其实就是在判断三个权重是否同号——三个叉积的符号统一,等价于 α,β,γ\alpha, \beta, \gamma 都非负。

有了重心坐标,三角形内部任意属性都可以插值。已知三个顶点处的属性值 VAV_A、VBV_B、VCV_C,点 PP 处的属性为:

VP=αVA+βVB+γVCV_P = \alpha V_A + \beta V_B + \gamma V_C

这就是属性插值。光栅化阶段用它插值颜色、法线,纹理映射中的 UV 坐标也靠它插值。

注:屏幕空间的重心坐标不能直接套用。透视投影下,屏幕上的插值比例与三维空间中的插值比例不一致,直接插值会产生透视扭曲。


2. 纹理映射(Texture Mapping)

回顾 着色与渲染管线 中的 Blinn-Phong 模型:漫反射系数 kdk_d 是一个常数,整个物体只有一个颜色,无法表现斑马条纹、木纹这类表面细节。纹理映射(texture mapping)解决这个问题——把一张图片(纹理,texture)贴到三维表面上,让着色器从纹理中查询每个点的颜色,用它作为漫反射系数。


2.1 纹理与纹理坐标

纹理是一张二维图片,由纹素(texel,texture element,纹理的基本单元,对应屏幕上的像素)组成。要把纹理贴到表面,每个顶点需要指定一个纹理坐标(texture coordinate,也称 UV 坐标),记作 (u,v)(u, v),取值范围约定为 [0,1][0, 1]:

坐标 含义
uu 纹理横向位置,0 到 1 从左到右
vv 纹理纵向位置,0 到 1 从下到上

(0,0)(0, 0) 对应纹理左下角,(1,1)(1, 1) 对应右上角。

注:vv 轴方向在不同 API 中可能有差异(有的以左上角为原点),具体以所用图形 API 的约定为准。


2.2 纹理映射的实现流程

纹理映射贯穿渲染管线的几个阶段:

  1. 建模阶段:在顶点上指定 UV 坐标,相当于把模型表面"展开"后对应到纹理图片上;
  2. 光栅化阶段:三角形内部每个片元的 UV 由三个顶点的 UV 用重心坐标插值得到;
  3. 片元处理阶段:用片元的 UV 到纹理中采样(sample)颜色,得到该点的漫反射系数 kdk_d,代入着色模型计算颜色。

第三步的采样本质是查表:UV 落在哪个纹素附近,就取哪个纹素的颜色。

// 伪代码:片元着色中用纹理颜色替代常数漫反射系数
Vec3 fragment_shading(const Vec2& uv, ...)
{
    Vec3 k_d = texture.sample(uv);   // 从纹理采样漫反射颜色
    return blinn_phong(k_d, ...);    // 代入 Blinn-Phong 计算
}

2.3 纹理采样与双线性插值

UV 是连续值,很少恰好落在某个纹素中心。最简单的最近邻采样(nearest neighbor)直接取距离最近的纹素,速度快但画面会出现明显的块状锯齿,纹理被放大时尤其严重。

双线性插值(bilinear interpolation)改善这种情况:取 UV 周围 2×2 的四个纹素,先在水平方向做两次线性插值,再在竖直方向做一次,得到平滑的过渡:

设 u 落在纹素列 u0 与 u1 之间,v 落在纹素行 v0 与 v1 之间

水平方向:   c0 = lerp(tex(u0, v0), tex(u1, v0), u - u0)
            c1 = lerp(tex(u0, v1), tex(u1, v1), u - u0)
竖直方向:color = lerp(c0, c1, v - v0)

2.4 MipMap

纹理缩小(minification)时,多个纹素挤进一个像素,密集细节在采样下出现摩尔纹(见 光栅化 的走样),时间上还会表现为闪烁——不同帧的采样位置不同,画面抖动。问题根源与 光栅化 中的走样相同:采样频率不足。

MipMap(multum in parvo,拉丁语"许多东西放在一个小地方")是预生成的纹理金字塔:原始纹理作为第 0 级,每一级把边长缩小一半(面积缩小到四分之一),直到 1×1,级别数约为 log⁡2S\log_2 S(SS 为最长边):

级别 分辨率 适用场景
level 0 原始纹理 像素覆盖的纹素很少(近处)
level 1 边长减半 稍远
level 2 边长再减半 更远
⋯\cdots ⋯\cdots ⋯\cdots

核心思想:根据像素覆盖的纹理区域大小选择合适分辨率的级别——远处一个像素覆盖很多纹素,就去低分辨率级别采样,等效于预先完成了低通滤波(呼应 光栅化 中"先模糊再采样"的反走样思路)。

级别选择:用屏幕相邻像素的 UV 差分,估计当前像素在纹理空间覆盖区域的大小。设 UV 对屏幕坐标的偏导数为 dudx\frac{du}{dx}、dvdx\frac{dv}{dx}、dudy\frac{du}{dy}、dvdy\frac{dv}{dy},像素覆盖区域近似为边长 LL 的正方形:

L=max⁡((dudx)2+(dvdx)2, (dudy)2+(dvdy)2)L = \max\left(\sqrt{\left(\frac{du}{dx}\right)^2 + \left(\frac{dv}{dx}\right)^2},\ \sqrt{\left(\frac{du}{dy}\right)^2 + \left(\frac{dv}{dy}\right)^2}\right)

再取对数得到级别 D=log⁡2LD = \log_2 L。这些偏导数 GPU 硬件可以直接计算。

三线性插值(trilinear interpolation):DD 通常不是整数,先在 DD、D+1D+1 两级内分别做双线性插值,再按小数部分对两个结果做一次线性插值,得到平滑的过渡,这就是三线性过滤(trilinear filtering)。

存储代价:所有级别加起来比原始纹理多约 13\frac{1}{3} 的存储量(14+116+164+⋯=13\frac{1}{4} + \frac{1}{16} + \frac{1}{64} + \cdots = \frac{1}{3})。


2.5 各向异性过滤

MipMap 假设像素在纹理空间覆盖的是正方形区域,用边长 LL 一个数描述。但斜着看表面时(比如俯视地面),像素覆盖的纹理区域其实是细长的长方形——沿某个方向拉伸得很厉害,正方形假设失效,MipMap 只能取较模糊的级别,远处纹理依然发糊。

各向异性过滤(anisotropic filtering)针对这种情况:不再按单一的正方形采样,而是沿拉伸方向多次采样(次数称各向异性程度,常见 2x、4x、8x、16x)后取平均,让斜向表面也保持清晰。代价是采样次数增加、性能开销变大,是目前 GPU 上最常用的高质量纹理过滤方式。