像素可见性

三角形投影到屏幕后,同一个像素可能同时被几个三角形覆盖。覆盖测试只能告诉我们这些三角形都经过这里,还要从中找出相机真正能看见的表面。

画家算法 Painter’s Algorithm 先绘制远处的表面,再让近处的表面覆盖已经写入的颜色。绘制立方体时,先画背面、再画侧面、最后画正面,通常能够得到正确遮挡。但是侧面的远近关系不能只由一个代表深度决定:某一侧面在一部分位置较近,在另一部分位置却可能较远,错误顺序会留下不应露出的边缘。排序不仅需要约 O(nlog⁡n)O(n\log n) 的代价,有时还不存在能够满足所有遮挡关系的顺序。

三个三角形之间存在循环遮挡

图中 PP 遮住 QQ 的一部分,QQ 遮住 RR,RR 又遮住 PP。若按整个三角形排序,三个条件无法同时满足。问题出在把整个图元作为排序单位;在每个屏幕位置上,最近表面仍然可以单独判断。

因此,深度缓冲 Depth Buffer,也称 Z-Buffer,为每个采样点保存当前最小深度,并让颜色缓冲 Framebuffer 保存与该深度对应的颜色。下面先约定深度 dd 为正,且越小越近;它不等同于相机坐标中朝向负 zz 的带符号坐标。

1
2
3
4
5
6
将所有采样点的深度初始化为无穷大
对每个三角形:
对它覆盖的每个采样点:
计算该点的深度 d 和颜色 C
若 d 小于已保存的深度:
同时更新深度和颜色

例如某点先收到深度为 5 的片元,就保存它;后来收到深度 3,则替换深度和颜色;再收到深度 8,两个缓冲都保持不变。处理任意数量的图元之后,总有

dbuffer(x,y)=min⁡{di∣i 覆盖该采样点}.d_{\mathrm{buffer}}(x,y)=\min\{d_i\mid i\text{ 覆盖该采样点}\}.

片元 Fragment 是光栅化生成的候选记录,包含位置、深度以及插值属性。只有通过深度等测试的片元才可能影响最终像素,一个像素因而可以先后收到多个片元。这个最小值关系说明:Z-Buffer 无须对图元排序;若每个三角形的覆盖样本数视为常数,nn 个三角形的比较总量是 O(n)O(n)。一般情况下更准确的工作量是所有候选片元数的总和,较大的三角形和重复遮挡都会增加它。

颜色图与对应的深度图

左图保存可见颜色,右图把最近深度显示成灰度:靠近相机的立方体角较暗,地板向远处逐渐变亮。两图位置一一对应,深度图的明暗只是数值的可视化,不是光照结果。

实际深度缓冲通常保存投影后的规范化深度。沿用透视投影中近平面对应 zndc=1z_{\mathrm{ndc}}=1、远平面对应 −1-1 的约定,可令

d=1−zndc2∈[0,1].d=\frac{1-z_{\mathrm{ndc}}}{2}\in[0,1].

这样仍可用“小于”判断更近。硬件可把缓冲清为 1;严格小于测试会拒绝恰好位于最远边界、深度也为 1 的片元,边界是否绘制取决于所选比较规则。概念伪代码中的无穷大避免了这个细节。不同 API 或 reversed-Z 实现可能采用不同映射和比较方向,必须整套保持一致。

不透明表面、没有深度并列且比较规则一致时,交换图元提交顺序不会改变结果。共面或近共面表面可能出现深度并列与 Z-fighting,有限精度会使它们争夺同一位置。透明表面还要与后面的颜色混合,仅留下最近一层无法表示这种累积过程;通常需结合不透明深度测试、透明排序和混合,或专门的顺序无关透明技术。使用 MSAA 时深度也按覆盖样本分别记录:2×22\times2 样本是 4 份深度,4×44\times4 是 16 份,并非一个像素只存一份就足够。

局部着色

可见性正确之后,若把立方体的所有面填成同一种颜色,画面仍只有外轮廓,转动时不容易区分哪个面朝向哪里。给不同朝向的面计算不同亮度,才会显出表面的方向和转折。着色 Shading 研究表面在给定照明和观察条件下呈现什么颜色。

先只考虑一个表面点 QQ。计算需要该点的单位法线 n\mathbf n、指向光源的单位向量 l\mathbf l、指向相机的单位向量 v\mathbf v,以及颜色、反射系数、Shininess 等材质参数。对点光源,另需光源强度 II 和光源到 QQ 的距离 rr。向量必须归一化并放在同一坐标系里;否则点积既不再等于夹角余弦,也无法正确比较方向。若程序把光方向定义成光线传播方向,它与这里的 l\mathbf l 相反,使用时需取负号。

这是一种局部着色 Local Shading:只根据 QQ 的属性和这些方向计算响应,还没有检查 QQ 到光源之间是否被其他物体挡住。因而表面可以有明暗和高光,却不自动产生投射阴影。阴影 Shadow 需要额外的可见性判断;间接光还需要计算别的表面如何反射光。先把这三类问题分清,后面的模型才不会被赋予它没有计算的效果。

以一张被照亮的表面为例,可以区分较宽的底色变化、局部较亮的高光,以及背光侧仍然保留的亮度。Blinn–Phong 模型 用漫反射、镜面反射和环境光三个分量近似它们。下面先从较宽的底色变化开始。

漫反射

对哑光表面,移动观察者时,同一个点的亮度可以近似不变。这种理想化行为称为朗伯漫反射 Lambertian Reflection:固定入射照明下,表面沿不同观察方向的出射辐亮度相同。辐亮度描述沿某个方向看到的表面亮度,严格定义见辐射度量。此处“各方向相同”不表示单位立体角的总功率相同,后者还与投影面积有关。

要算漫反射亮度,先看表面实际接收了多少光。把一小块表面正对平行光束,原来落入它的六条等能量光线都被接收;将表面旋转 60∘60^\circ 后,它在光束垂直截面上的面积只剩一半,因此同样的表面积只接收到约三条光线。这个变化由朗伯余弦定律 Lambert’s Cosine Law 表示:

dA⊥=dAcos⁡θ=dA(n⋅l).\mathrm dA_\perp=\mathrm dA\cos\theta =\mathrm dA(\mathbf n\cdot\mathbf l).

这里 θ\theta 是光源方向与法线夹角。正对光源时投影面积最大,接近掠射时投影面积趋近零。季节中的太阳高度变化也有这一因素:斜射时同样一束光分布在更大的地面面积上;季节本身还涉及日照时长等条件。单面不透明表面的背侧不接收这束直接光,因此使用 max⁡(0,n⋅l)\max(0,\mathbf n\cdot\mathbf l)。

再看光源距离。假定点光源向各方向均匀发光,半径 rr 的球面截获同样总功率,而球面面积为 4πr24\pi r^2。若单位距离处单位正交面积接收的值为 II,则

4πI=4πr2Ir⟹Ir=Ir2.4\pi I=4\pi r^2 I_r \quad\Longrightarrow\quad I_r=\frac{I}{r^2}.

这里解释的是接收光随距离的变化,光源自身的辐射强度并未变小。更严格地把 II 定义为每单位立体角的辐射强度,也得到正对光源时的辐照度 I/r2I/r^2。平行光源不使用这个点光源距离衰减。

最后乘以表面颜色系数,得到经验模型中的漫反射项:

Ld=kdIr2max⁡(0,n⋅l).L_d=k_d\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf l).

三个因子分别控制材质响应、到达的光、表面对这束光的接收量。kdk_d 取 RGB 三分量时,每个颜色通道有自己的响应比例;取零会让这一项为零,增大它会让这一项更亮。这里的 kdk_d 可吸收归一化常数,不能直接把 kd=1k_d=1 当成严格的“全部入射能量被反射”。若采用物理上的反照率 Albedo ρd\rho_d,即总反射能量占入射能量的比例,朗伯 BRDF 为 ρd/π\rho_d/\pi,对应

Ld=ρdπIr2max⁡(0,n⋅l).L_d=\frac{\rho_d}{\pi}\frac{I}{r^2} \max(0,\mathbf n\cdot\mathbf l).

公式不含 v\mathbf v,所以固定点的漫反射亮度不随观察方向改变;也不含相机距离。在没有吸收和散射的传播介质中,辐亮度沿光线保持不变,远处物体主要是占据的图像面积变小。光源到表面的距离衰减与表面到相机的距离是两个不同问题。

镜面高光

光滑表面把反射集中在镜面方向附近。观察方向越靠近这个方向,看到的高光通常越亮。Phong 反射模型 直接比较观察方向 v\mathbf v 和理想反射方向

r=2(n⋅l)n−l.\mathbf r=2(\mathbf n\cdot\mathbf l)\mathbf n-\mathbf l.

Blinn–Phong 采用另一种方向比较。若 l\mathbf l 与 v\mathbf v 恰好满足镜面反射定律,法线正好平分两者夹角。两单位向量相加后指向角平分线,因此定义半程向量 Halfway Vector

h=l+v∥l+v∥.\mathbf h=\frac{\mathbf l+\mathbf v}{\|\mathbf l+\mathbf v\|}.

半程向量与法线的夹角

图中 h\mathbf h 平分 l\mathbf l 与 v\mathbf v,α\alpha 是 h\mathbf h 与 n\mathbf n 的夹角。理想反射条件下它们重合,n⋅h=1\mathbf n\cdot\mathbf h=1;偏离时点积下降。Blinn–Phong 用这个点积计算镜面项:

Ls=ksIr2max⁡(0,n⋅h)p.L_s=k_s\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf h)^p.

为什么还要取 pp 次幂?仅用余弦时,夹角达到 45∘45^\circ,数值仍有 cos⁡45∘≈0.707\cos45^\circ\approx0.707,亮区过宽。将它取高次幂后,除了接近 1 的数值,其余很快下降:0.98≈0.430.9^8\approx0.43,0.964≈0.00120.9^{64}\approx0.0012。指数 pp 即 Shininess,控制高光分布宽度。

余弦的幂次增大时,曲线峰值区域收窄

四幅图从 cos⁡α\cos\alpha 到 cos⁡64α\cos^{64}\alpha,峰值都为 1,但显著非零的角度范围逐渐缩小。p=64p=64 时在约 20∘20^\circ 到 30∘30^\circ 的范围已经明显衰减;p=100p=100 到 200 会得到更窄的高光。这些角度不是模型的硬边界:半高宽可由 cos⁡pα=1/2\cos^p\alpha=1/2 求出 α=arccos⁡(2−1/p)\alpha=\arccos(2^{-1/p}),可见范围还取决于亮度尺度和显示条件。

ksk_s 控制镜面分量的颜色和强度。许多非金属表面的镜面反射近似不改变入射光颜色,白光下高光常接近白色;金属高光可以具有明显颜色。Phong 与 Blinn–Phong 都是经验模型,指数不能直接等同于物理粗糙度。半程方向提供了方便的计算方式,实际性能仍取决于实现,不能据此断言它在所有硬件上都更快。单面模型还应要求光源和相机位于表面正侧;l+v=0\mathbf l+\mathbf v=\mathbf0 时半程向量未定义。上式省略了更完整物理模型中的若干几何和能量约束,因此适合用来理解高光,不保证能量守恒。

环境光

只计算直接光照,背向光源的表面会变黑。然而真实场景中,别的表面还会把光反射到这里。暂时不计算这些反弹,可以加入环境光 Ambient Lighting 常数项:

La=kaIa,L=La+Ld+Ls.L_a=k_a I_a,\qquad L=L_a+L_d+L_s.

它与光源方向、观察方向和法线都无关,相当于给同一材质各点加一个相同的亮度基底。真实间接光会随遮挡、位置和其他材质改变,常数项只是近似。

环境、漫反射、镜面分量以及它们的和

图中环境项只有均匀底色;漫反射项随朝向形成大范围渐变;镜面项大部分位置较暗,仅在满足高光方向的位置出现亮斑。相加后,背光侧保留底色,面向光源的位置更亮,高光叠加在少数位置。完整经验公式为

L=kaIa+kdIr2max⁡(0,n⋅l)+ksIr2max⁡(0,n⋅h)p.L=k_aI_a+k_d\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf l) +k_s\frac{I}{r^2}\max(0,\mathbf n\cdot\mathbf h)^p.

若有多个直接光源,可以分别计算它们的漫反射和镜面贡献后相加,环境项不需要随每个光源重复添加。

着色频率

现在已经能够求一个点的颜色,还要决定在哪些点求解光照方程。着色频率 Shading Frequency 指评估光照的空间粒度:每个面、每个顶点,或每个片元。

网格密度与 Flat、Gouraud、Phong 着色的对照

先看第一行:左侧球的每个面内部颜色相同,面与面之间亮度突变;中间球通过插值变平滑,但高光较宽或可能被漏掉;右侧在表面内部重新计算光照,高光更集中。向下看,网格逐渐变密,三种方法之间的差别减小。两种变量需分别比较:横向改变着色频率,纵向改变几何采样密度。

  • Flat Shading 为一个三角形选一个法线和代表点,只计算一次颜色,整个面使用同一结果。三角形法线可由两条边叉积后归一化得到,适合表达折面。
  • Gouraud Shading 在顶点计算颜色,再插值到三角形内部。若亮峰位于面内部而三个顶点都不亮,颜色插值无法凭空生成这个峰,因此可能漏掉高光。
  • Phong Shading 先把顶点法线插值到片元,再在片元位置归一化法线并计算光照。这里的 Phong 指评估位置,与使用 v⋅r\mathbf v\cdot\mathbf r 的 Phong 反射模型不同;Phong Shading 同样可以使用 Blinn–Phong 模型。

顶点法线从哪里来?若网格来自球面等解析曲面,可以使用原曲面的法线,例如球心指向球面点的方向。只有网格时,可以由相邻面法线估计:

nv=normalize⁡(∑iwini).\mathbf n_v=\operatorname{normalize}\left(\sum_i w_i\mathbf n_i\right).

最简单令权重相同,也可按面积或顶点夹角加权。这样相邻三角形共享顶点处的法线,插值后方向较平滑;机械硬边两侧则应保留独立法线,不能平均成圆滑过渡。插值一般会改变向量长度,参与点积前必须再次归一化。

更细的着色不改变实际轮廓和遮挡关系,粗球的边缘仍然是多边形。成本也不能只凭“逐面便宜”判断:当面片小于一个像素时,面片数量可能超过像素数量;粗网格上则常由逐片元计算得到更细的光照变化。

图形管线

变换、覆盖测试、可见性和着色各自处理不同对象。把它们连起来,就是图形管线 Graphics Pipeline。

顶点流、三角形流和片元流的处理过程

沿图中的主干向下读,输入先是三维顶点;顶点处理改变它们的位置和属性;三角形处理根据连接关系形成图元;光栅化产生覆盖记录;片元处理计算颜色;帧缓冲操作决定这些颜色怎样写入图像。每个阶段接收的对象不同,所以同一段程序可以分别描述“一个顶点”或“一个片元”的计算。

具体实现中,应用在 CPU 上更新场景、相机和动画,并提交绘制指令。顶点着色器执行 MVP 等变换,输出齐次裁剪坐标;图元装配后在裁剪空间裁掉视锥外部分,进行透视除法与视口映射;光栅化执行覆盖测试和属性插值;片元着色器执行纹理查询及光照;逐样本操作执行深度、模板和混合,再形成输出图像。深度测试可能在满足条件时提前进行以避免无用着色,但其含义仍是保留可见样本。

着色器 Shader 是可编程阶段的程序。下面用 GLSL 展示一个片元的漫反射计算;纹理提供此点的颜色,光照项随后调节它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uniform sampler2D materialTexture;
uniform vec3 lightDir;
in vec2 uv;
in vec3 normal;
out vec4 fragmentColor;

void main()
{
vec3 kd = texture(materialTexture, uv).rgb;
vec3 n = normalize(normal);
// lightDir 采用光线传播方向,取负后才指向光源。
vec3 l = normalize(-lightDir);
float cosine = clamp(dot(n, l), 0.0, 1.0);
fragmentColor = vec4(kd * cosine, 1.0);
}

uniform 是一次绘制中共享的参数,in 接收从顶点属性插值得到的片元输入,texture 查询纹理,dot 求点积,clamp 限定数值范围,输出中的第 4 分量是 Alpha。这里省略了点光源距离衰减,按方向照明演示。硬件把程序应用到大量片元,不需要应用程序逐个遍历它们;着色器内部仍可包含循环、分支和函数。

GPU 为这种大量相似计算提供并行执行和固定功能单元。它既可作为独立显卡,也可集成在处理器中。以 2015 年的 Unreal Engine Kite Demo 为例,复杂场景包含数十万至数百万三角形,输出约 200 万至 400 万像素并进行抗锯齿,目标为每秒 30 至 60 帧,VR 往往要求更高刷新率。课件所举约 2–4 TFLOPS 是当时设备的计算量级,不是当前 GPU 的统一性能。几何着色器还可生成或丢弃图元,计算着色器可处理通用并行任务。Inigo Quilez 的约 800 行蜗牛 Shader 演示了由程序定义形状、外观和动画的用法。

OpenGL、DirectX 等 API 让应用配置这些阶段和数据接口;相机参数、变换矩阵和绘制状态仍由应用或引擎决定并提供,驱动不会替程序自动选择场景所需的 MVP。

纹理坐标

同一个球面既可以是一种纯色,也可以带有星形图案;同一块地板可以有木纹。它们使用同一光照方程,只需让 kdk_d 随位置变化。纹理映射 Texture Mapping 用一张图或其他可查询数据,为表面各点提供属性。

屏幕、三维表面与二维纹理中的对应点

中间是世界空间中的地球表面,右侧是二维地图,左侧是投影后的屏幕图像。映射 ϕ\phi 关联表面点与地图位置,投影 π\pi 关联表面点与屏幕位置。图像中的同一个点经历两套不同映射,不能把屏幕坐标直接当作纹理坐标。

三维表面局部只有两个自由度,因此可用二维参数 UV 标记位置。球面可以用经纬参数,复杂模型则常剪开成若干片再展开。这个建立表面与二维域对应关系的过程称为参数化 Parameterization。美术人员可手工展开,也可由算法尽量减少拉伸、压缩和接缝;曲面并不一定能无失真地展开成一张平面。

模型上的三角形与 UV 平面上的对应网格

图中左右颜色对应相同位置,三维模型上的顶点在右侧都有 UV 坐标。渲染阶段已知三角形三个顶点的对应关系,再求内部点的 UV,而不需要重新进行整个模型的展开。查询纹理得到 RGB 后,可以把它用作这个点的 kdk_d,再计算光照;纹理颜色不替代光照结果。

纹素 Texel 是纹理数组的一个离散元素,像素是输出图像的元素,两者分辨率可以不同。常用归一化 UV 表示一张图的范围 [0,1]2[0,1]^2。对宽高 Wt,HtW_t,H_t 的纹理,纹素 (i,j)(i,j) 的中心可写为 ((i+0.5)/Wt,(j+0.5)/Ht)((i+0.5)/W_t,(j+0.5)/H_t);具体上下方向约定需与图像加载保持一致。

UV 也可超过 [0,1][0,1]:Repeat 重复平铺,Clamp 固定在边界,Mirror 交替镜像。要让重复纹理没有可见接缝,左右边界和上下边界需匹配,称为无缝纹理 Seamless Texture。Wang Tiles 用带边缘约束的多种小块拼接,可减轻单张图反复重复的规则感。法线、粗糙度、透明度等属性也能放进纹理,进一步用途见纹理作为数据。

重心插值

顶点有了颜色、法线和 UV,三角形内部应该怎样取值?先让两个顶点之间按位置比例过渡,再把这种想法推广到三角形。重心坐标 Barycentric Coordinates 为平面点 PP 指定三个顶点的权重:

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

三个权重只有两个自由度,符合平面点用两个参数表示的要求。A,B,CA,B,C 自身的权重分别是 (1,0,0)(1,0,0)、(0,1,0)(0,1,0)、(0,0,1)(0,0,1),三个权重都非负时点位于三角形内或边界;有负权重时则位于外部。

内部点把三角形分成三个子三角形

图中 AAA_A 是位于顶点 AA 对面的子三角形面积,其他标记同理。对应权重正好是面积比例:

α=SPBCSABC,β=SPCASABC,γ=SPABSABC.\alpha=\frac{S_{PBC}}{S_{ABC}},\qquad \beta=\frac{S_{PCA}}{S_{ABC}},\qquad \gamma=\frac{S_{PAB}}{S_{ABC}}.

可把这一关系理解为点到对边的相对高度:PBCPBC 与 ABCABC 共用底边 BCBC,面积比等于高度比;PP 越靠近 AA,对边的相对高度越大,AA 的权重越大。重心把三角形等分成三个面积相等的部分,因此三个权重都是 1/31/3。对外部点要使用有向面积,直接取绝对面积无法识别负权重。

在屏幕二维坐标中,也可直接解三元线性方程。令 A=(xA,yA)A=(x_A,y_A),其他顶点同理,非退化三角形有

Δ=(yB−yC)(xA−xC)+(xC−xB)(yA−yC),\Delta=(y_B-y_C)(x_A-x_C)+(x_C-x_B)(y_A-y_C),

α=(yB−yC)(x−xC)+(xC−xB)(y−yC)Δ,β=(yC−yA)(x−xC)+(xA−xC)(y−yC)Δ,γ=1−α−β.\alpha=\frac{(y_B-y_C)(x-x_C)+(x_C-x_B)(y-y_C)}{\Delta}, \qquad \beta=\frac{(y_C-y_A)(x-x_C)+(x_A-x_C)(y-y_C)}{\Delta}, \qquad \gamma=1-\alpha-\beta.

若属性 VV 在三角形上按仿射方式变化,就用 V(P)=αVA+βVB+γVCV(P)=\alpha V_A+\beta V_B+\gamma V_C。它既能表示标量深度,也能按分量插值颜色、法线和 UV;法线还要归一化。Δ=0\Delta=0 时三角形退化,不能通过上述除法取得唯一坐标。

还有一个关键限制:重心权重在仿射变换下保持,却不在透视投影下保持。远端等长线段在屏幕上更短,屏幕中点一般不是三维线段中点。直接用屏幕权重插值 UV,会使同一表面的纹理在不同三角形间呈现错误变化,必须做透视矫正插值 Perspective-Correct Interpolation。

设投影前权重为 λi\lambda_i,齐次裁剪顶点为 P~i\widetilde P_i,第四分量为 wiw_i。投影前的线性组合和投影后坐标满足

P~=∑iλiP~i,wP=∑iλiwi,Pscreen=∑iλiwiwPPi,screen.\widetilde P=\sum_i\lambda_i\widetilde P_i, \qquad w_P=\sum_i\lambda_iw_i, \qquad P_{\mathrm{screen}}=\sum_i\frac{\lambda_iw_i}{w_P}P_{i,\mathrm{screen}}.

因此屏幕权重 αi=λiwi/wP\alpha_i=\lambda_iw_i/w_P。反解并用 ∑iλi=1\sum_i\lambda_i=1 消掉 wPw_P,得到

λi=αi/wi∑jαj/wj,V(P)=∑iαiVi/wi∑iαi/wi.\lambda_i=\frac{\alpha_i/w_i}{\sum_j\alpha_j/w_j}, \qquad V(P)=\frac{\sum_i\alpha_i V_i/w_i}{\sum_i\alpha_i/w_i}.

例如屏幕线段中点的两端权重都是 1/21/2,两端 ww 分别为 1 和 4,投影前权重为 0.80.8 和 0.20.2。近端在屏幕上占据更大的比例,所以同一屏幕中点的三维位置更靠近近端。实现中可分别线性插值 Vi/wiV_i/w_i 和 1/wi1/w_i,再做一次除法。普通属性和相机空间深度需要这种校正;已经完成除法的 NDC 深度 zc/wcz_c/w_c 则在屏幕三角形内线性插值,不能再重复校正。

三角形内部的纹理查询把权重、属性和着色连接为下面的过程。w 来自齐次裁剪坐标,插值后的法线还需要归一化:

1
2
3
4
5
6
7
对每个通过覆盖测试的片元:
计算屏幕重心权重 alpha、beta、gamma
用各顶点的属性 / w 和 1 / w 计算透视矫正 UV
查询纹理,得到该点的 kd
同样插值法线并重新归一化
用 kd、法线、光源方向和视线方向计算颜色
结合深度测试,把可见样本写入缓冲

纹理放大

查询得到的 UV 一般落在纹素中心之间。例如用 256×256256\times256 的图覆盖 4K 屏幕上的大块表面,一个纹素会对应多个像素,称为纹理放大 Texture Magnification。这时要从离散纹素重建连续颜色。

最近邻 Nearest Neighbor 选择离查询点最近的纹素,相当于把纹素中心坐标四舍五入。一个区域都返回同一个值,跨过中间边界时突然跳变,放大后便看到方块。双线性插值 Bilinear Interpolation 改用围住查询点的四个纹素中心。

双线性插值的四个邻点与查询点

图中红点是查询位置,粉色区域标出参与计算的四个纹素,黑点 u00,u10,u01,u11u_{00},u_{10},u_{01},u_{11} 保存已知颜色。s,ts,t 表示相对于左下中心的水平和垂直偏移,并以相邻中心距离为 1,所以都在 [0,1][0,1] 内。定义线性插值 Lerp

lerp⁡(s,a,b)=(1−s)a+sb.\operatorname{lerp}(s,a,b)=(1-s)a+sb.

先沿水平方向算出上下两个中间值,再沿垂直方向求红点:

u0=(1−s)u00+su10,u1=(1−s)u01+su11,u_0=(1-s)u_{00}+su_{10},\qquad u_1=(1-s)u_{01}+su_{11},

c=(1−t)u0+tu1=(1−s)(1−t)u00+s(1−t)u10+(1−s)tu01+stu11.c=(1-t)u_0+tu_1 =(1-s)(1-t)u_{00}+s(1-t)u_{10}+(1-s)tu_{01}+st u_{11}.

四个权重非负且和为 1。正中心 s=t=1/2s=t=1/2 时就是四者平均,靠近某个纹素中心时,它的权重趋近 1;交换水平和垂直插值顺序,展开后仍得到同一公式。此处图中 uiju_{ij} 是颜色值标记,不能与归一化 UV 坐标中的 uu 混淆。

最近邻、双线性与双三次放大效果

眼角和发丝附近,Nearest 显示方块边界,Bilinear 把相邻值连续连接,但边缘较柔和。双三次插值 Bicubic Interpolation 使用 4×44\times4 的 16 个邻点,通过三次核重建,可改善平滑性,也增加查询和计算成本;具体连续性取决于核,带负权重的核还可能出现过冲。过滤能够改善重建,纹理原本没有采到的细节无法由插值唯一恢复。

纹理缩小

远处地板的情况相反:一个屏幕像素对应许多纹素,称为纹理缩小 Texture Minification。此时即使准确插值了像素中心的颜色,也只代表覆盖范围内的一个点。

同一纹理的参考结果与单点采样结果

右图近处的斜线出现锯齿,远处格子产生摩尔纹;左图细线随着距离自然汇合。越远处,每个像素跨过的黑白变化越多,单次查询容易只取到其中一个颜色,采样位置的微小变化就会引起较大的结果变化。这是纹理信号的欠采样,不是简单增加纹理原图分辨率就能解决的问题。

一个屏幕像素在纹理空间中的覆盖范围

蓝点表示查询中心,灰点表示纹素中心,带色平行四边形表示一个像素投回纹理后的覆盖范围 Footprint。从左向右,范围由小于一个纹素变为覆盖多个纹素。放大时主要需要重建一个点的值;缩小时则需要整个范围经过滤波的平均。

每像素取 512 个样本再平均的演示可以明显减少这些伪影,但成本随样本数量增长,512 也不是所有纹理都会满足的固定阈值。问题于是转为:能否不在运行时逐个遍历范围内纹素,而直接查询预先算好的平均?这区分了点查询 Point Query 与范围查询 Range Query。范围查询还可能求最大值或最小值,这里特指颜色平均,需要与所求量匹配的数据结构。

Mipmap

Mipmap 预先保存原图以及一系列低分辨率版本。第 0 层是原图,第 1 层宽高各减半,每个值由前一层相邻 2×22\times2 纹素平均而来,后续重复这个过程。第 DD 层的一个纹素代表原图约 2D×2D2^D\times2^D 的方形范围。

Mipmap 的多级图像

图中 128×128128\times128 的原图逐步减到 1×11\times1,连同第 0 层共 8 层;越高层越少细节,最后一个纹素保存全图平均。它也称为图像金字塔 Image Pyramid;Mipmap 来自 multum in parvo,意为在小空间中保存大量信息。它支持快速、近似的方形范围平均,既不精确积分任意边界,也不直接处理任意形状。

宽高都减半,每层存储量是前一层的 1/41/4。对较大的 2 的幂次方形纹理,总量接近

1+14+116+⋯=43,1+\frac14+\frac1{16}+\cdots=\frac43,

即额外约 1/31/3。有限纹理的总和略小于这一极限。也可以把各层各复制三份:原图三份填满大正方形的三个象限,剩余象限放下一层三份,如此递归填满,得到 3S=4A3S=4A,其中 AA 为原图存储量。非 2 的幂次纹理也可生成 Mipmap,但每层尺寸取整和边界滤波需按实现规则处理,不要求所有实际纹理先补成 2 的幂。

运行时要回答的下一个问题是选哪一层。先把当前像素中心及其屏幕右侧、上侧的邻居映射到纹理。

由屏幕邻居估计纹理覆盖跨度

左图的蓝色与橙色点分别表示两组相邻屏幕采样位置,右图显示它们在纹理空间中的对应位置。橙色点的纹理间距比蓝色点大,所以橙色点位置需要更高的层级。将 UV 距离换成原图纹素单位,记两个方向跨度为 ℓx,ℓy\ell_x,\ell_y,取 L=max⁡(ℓx,ℓy)L=\max(\ell_x,\ell_y)。也可把像素四角映射后用边长估计,二者都是局部近似。用偏导数写为

ℓx=(Wt∂u∂x)2+(Ht∂v∂x)2,ℓy=(Wt∂u∂y)2+(Ht∂v∂y)2.\ell_x=\sqrt{\left(W_t\frac{\partial u}{\partial x}\right)^2+ \left(H_t\frac{\partial v}{\partial x}\right)^2},\quad \ell_y=\sqrt{\left(W_t\frac{\partial u}{\partial y}\right)^2+ \left(H_t\frac{\partial v}{\partial y}\right)^2}.

这里偏导数描述屏幕位移如何转为纹理位移,组合成局部 Jacobian 雅可比矩阵。每升一层,原图覆盖边长翻倍,所以求 2D=L2^D=L,得到

D=log⁡2L.D=\log_2 L.

L=4L=4 时用第 2 层,L=8L=8 时用第 3 层;L<1L<1 属于放大,使用原图进行重建。实际还应把层级限制在 [0,Dmax⁡][0,D_{\max}],零跨度取一个正下限后再求对数。

把层级取整后的可视化

这幅伪彩色图用颜色编码查询层级 DD,颜色只表示层级数值。同一连续墙面上的大块色带表示层级取整导致的跳变;若直接把整数层级用于纹理颜色查询,跨层时可能出现接缝。三线性过滤 Trilinear Filtering 在相邻两层分别双线性查询,再沿层级做一次线性混合:令 D0=⌊D⌋D_0=\lfloor D\rfloor、t=D−D0t=D-D_0,则

c=(1−t)cD0(u,v)+tcD0+1(u,v).c=(1-t)c_{D_0}(u,v)+t c_{D_0+1}(u,v).

例如 D=1.8D=1.8 时两层权重为 0.20.2 和 0.80.8。这里第三个维度是层级,并非纹理的第三个空间坐标。

连续层级的可视化

同一墙面上的层级编码现在连续过渡。柱子与背景交界处仍有颜色跳变,是因为它们属于不同表面、覆盖尺度也不同,不能把所有跳变都归咎于过滤。一次三线性过滤包含相邻两层的双线性查询,再做一次层级混合;每层使用四邻点,合计最多八个纹素,硬件常能把这些操作作为一次过滤查询完成。它减少层间突变,未改变方形范围近似的限制。

各向异性

斜视地板时,footprint 往往细长。如果按长边 LL 选择方形 Mipmap 范围,短边也被扩大到 LL,许多本不该参与的纹素被平均,产生过度模糊 Overblur。需要在长轴上充分滤波,同时尽量保留短轴的细节,这就是各向异性过滤 Anisotropic Filtering 的动机。

各向异性过滤下的斜视地面

图中远处的地面线条仍可辨认。改善来自过滤范围与斜视覆盖形状更接近,不是增加了原图细节。普通 Mipmap 各层宽高同步缩小;Ripmap 分别预计算横向与纵向缩小,得到可查询轴对齐矩形的二维层级表。

Ripmap 层级表及 EWA 椭圆权重示意

上半图把卫星纹理的不同横向、纵向缩放组合排成一个表格:向右宽度减小,向下高度减小;第一行保持原高度,第一列保持原宽度。从左上到右下的对角线就是普通 Mipmap 的同步缩小序列。它能适应轴对齐的长条,但与坐标轴斜交的 footprint 仍可能被不必要地扩宽。完整 Ripmap 的存储总量接近

(1+12+14+⋯ )2=4,\left(1+\frac12+\frac14+\cdots\right)^2=4,

即总量约原图 4 倍、额外约 3 倍,不能把额外存储量当成总量。

下半图椭圆的等权曲线具有不同长短轴和方向。EWA Elliptical Weighted Average 用椭圆高斯权重近似 footprint,通过多次加权纹理查询近似范围积分,并可借助 Mipmap 缩小查询工作量。靠近核中心的样本权重较大,沿椭圆长轴较远的样本仍可能参与平均。它适应旋转、拉伸的局部覆盖范围,查询次数随范围和精度需求增加,结果仍是近似值。

实际 GPU 的各向异性过滤也常沿长轴组合多个较小范围查询,而不存完整 Ripmap。设置中的 2×、4×、8×、16× 通常表示允许的最大各向异性程度,限制可支持的长短轴尺度比和相应采样预算,具体策略依硬件而异;它们不表示保存了多少层 Mipmap。提高等级可能改善斜视纹理,也会增加查询、带宽和计算开销,收益仍受原始分辨率、footprint 与实现限制。