Interpolation & texture mapping插值与纹理映射
Lesson 04 handed us barycentric coordinates at every covered pixel; now we use them to interpolate vertex attributes and paint textures — and confront the perspective-correction trap that makes naive interpolation visibly wrong. 第 04 课在每个被覆盖的像素上交给了我们重心坐标;现在我们用它来插值顶点属性、绘制纹理——并正面迎战那个让朴素插值肉眼可见地出错的透视校正陷阱。
Five moves. (1) Use barycentric weights to interpolate any vertex attribute — color, normal, UV — which is how smooth shading and texture lookup are fed. (2) Spring the perspective trap: interpolating linearly in screen space is wrong under perspective because projection is projective, not affine; the fix is to interpolate a/w and 1/w and divide — and this is why lesson 02 kept w in clip space. (3) Define texture mapping and UVs: the [0,1]² image, per-vertex coordinates, texel sampling, and wrap modes. (4) Meet minification & aliasing, where a distant surface crams many texels into one pixel, and the mipmap pyramid that prefilters it — trilinear picks the level from the screen-space derivative of UV. (5) Fix the oblique case with anisotropic filtering, then hand off to lesson 06's general sampling theory.
五步走。(1) 用重心权重插值任意顶点属性——颜色、法线、UV——平滑着色与纹理查找都由此喂入。(2) 触发透视陷阱:在屏幕空间做线性插值,在透视下是错的,因为投影是射影而非仿射;修法是插值 a/w 与 1/w 再相除——而这正是第 02 课在裁剪空间保留 w 的原因。(3) 定义纹理映射与 UV:[0,1]² 图像、逐顶点坐标、纹素采样、以及环绕模式。(4) 迎接缩小与走样——远处表面把许多纹素塞进一个像素——以及对它预滤波的 mipmap 金字塔;三线性用 UV 的屏幕空间导数挑选层级。(5) 用各向异性过滤修好斜视这一情形,再交棒给第 06 课的一般采样理论。
1 · Barycentric interpolation of vertex attributes顶点属性的重心插值
Lesson 04 ended with a gift: for every pixel inside a triangle, the edge functions already give three weights (α, β, γ) that sum to 1 and locate the pixel as a blend of the three vertices — the barycentric coordinates. A triangle carries data at its vertices: a color, a surface normal, a texture coordinate. To shade the interior we simply blend that per-vertex data by those same weights.
第 04 课以一份礼物收尾:对三角形内的每个像素,边函数已经给出三个和为 1 的权重 (α, β, γ),把像素定位成三个顶点的一次混合——这就是重心坐标。一个三角形把数据带在它的顶点上:一个颜色、一条表面法线、一个纹理坐标。要给内部着色,我们只需用同一组权重去混合这些逐顶点数据。
Set ai to the three vertex colors and you get a smoothly graded (Gouraud-shaded) triangle. Set them to the three vertex normals and you get a normal that varies across the face, so a faceted mesh can look curved (lesson 08 dots that interpolated normal with a light). Set them to the three UV coordinates and you get, at each pixel, the exact point in a texture image to sample. One operation, three uses — interpolation is the bridge between "data known only at corners" and "a value at every pixel."
把 ai 设为三个顶点颜色,你就得到一个平滑过渡(Gouraud 着色)的三角形。设为三条顶点法线,你就得到在整个面上变化的法线,于是一个刻面网格能显得弯曲(第 08 课会把这条插值法线与光照做点积)。设为三个 UV 坐标,你就在每个像素上得到——要在纹理图像中采样的那个精确位置。同一种操作,三种用途——插值正是“只在角点已知的数据”与“每个像素上的一个值”之间的桥梁。
The weights are cheap: from the same edge functions of lesson 04, α = A0/A, β = A1/A, γ = A2/A, where A is the whole triangle's (signed) area and Ai is the sub-triangle area opposite vertex i. They are computed once per pixel and reused for every attribute. But there is a catch, and it is the whole reason this lesson is not a one-liner.
权重很便宜:由第 04 课同样的边函数,α = A0/A、β = A1/A、γ = A2/A,其中 A 是整个三角形的(有向)面积,Ai 是与顶点 i 相对的子三角形面积。它们每像素只算一次,供每个属性复用。但有个陷阱,而它正是本课不止一行字的全部原因。
2 · The perspective trap透视陷阱
Interpolating an attribute linearly in screen space — plug the screen-space (α, β, γ) straight into the blend above — is wrong the moment perspective is involved. The reason traces back to lesson 02: the perspective projection is a projective map, not an affine one. Affine maps preserve equal spacing; projective maps do not. Equal steps across a triangle on the screen correspond to unequal steps across the same triangle in the world, because the perspective divide by w compressed far geometry more than near geometry. So a value that is genuinely linear over the world surface becomes a nonlinear function of screen position — and blending it linearly in screen space bends it.
把一个属性在屏幕空间做线性插值——把屏幕空间的 (α, β, γ) 直接代进上面的混合——一旦牵涉透视就是错的。原因可追回第 02 课:透视投影是一个射影映射,而非仿射映射。仿射映射保持等间距;射影映射不保持。在屏幕上跨三角形走等步长,对应到世界里同一三角形上却是不等步长,因为除以 w 的透视除法把远处几何压缩得比近处更狠。于是一个在世界表面上确实线性的值,变成了屏幕位置的非线性函数——在屏幕空间线性混合它,就把它掰弯了。
The fix is exact and cheap, and it is the reason lesson 02 was so careful to keep w through clip space instead of dividing early. The quantities that are linear in screen space are the attribute divided by w and the reciprocal 1/w itself. So we interpolate those with the screen-space barycentrics, then divide at the end:
修法既精确又便宜,也正是第 02 课如此小心地在裁剪空间保留 w、而不早早相除的原因。真正在屏幕空间线性的量,是属性除以 w,以及倒数 1/w 本身。所以我们用屏幕空间的重心坐标去插值这些量,最后再相除:
Read it as: interpolate the numerator (attribute over w) linearly, interpolate the denominator (1/w) linearly, divide. The two w-weightings cancel exactly when the vertices are equidistant, and otherwise re-weight each vertex by how near it is — a near vertex (small w, large 1/w) pulls harder, which is precisely the correction perspective demands. This is what clip-space w was for.
读作:把分子(属性除以 w)线性插值,把分母(1/w)线性插值,相除。当顶点等距时两个 w 加权恰好抵消,否则就按每个顶点的远近重新加权——近的顶点(w 小、1/w 大)拉力更强,这正是透视所要求的校正。这就是裁剪空间那个 w 的用途。
A worked mini-example makes the size of the error concrete. Take one texture coordinate u along an edge whose two endpoints are at depths giving w0 = 1 (near, u0 = 0) and w1 = 4 (far, u1 = 1). Sample the screen-space midpoint, α = β = 0.5:
一个小算例让误差的大小变得具体。取一条边上的一个纹理坐标 u,其两端点深度给出 w0 = 1(近,u0 = 0)与 w1 = 4(远,u1 = 1)。在屏幕空间中点采样,α = β = 0.5:
| Method | Computation | u at screen midpoint |
|---|---|---|
| Affine (wrong) | 0.5·0 + 0.5·1 | 0.500 |
| Perspective-correct | (0.5·0/1 + 0.5·1/4) / (0.5/1 + 0.5/4) | 0.200 |
| 方法 | 计算 | 屏幕中点处的 u |
|---|---|---|
| 仿射(错误) | 0.5·0 + 0.5·1 | 0.500 |
| 透视校正 | (0.5·0/1 + 0.5·1/4) / (0.5/1 + 0.5/4) | 0.200 |
The correct answer is 0.2, not 0.5: the screen midpoint of this receding edge is much closer, in texture space, to the near end — a 0.3 error in u, easily a third of the way across the image. That gap is exactly the classic PlayStation-1 texture wobble: that console interpolated UVs affinely, so textures visibly warped and swam as the geometry moved. The widget below lets you flip between the two and watch straight checker lines bend under affine and snap straight under perspective-correct.
正确答案是 0.2,不是 0.5:这条后退边的屏幕中点,在纹理空间里离近端要近得多——u 上 0.3 的误差,轻易就跨过图像的三分之一。这道缝正是经典的 PlayStation-1 纹理抖动:那台主机用仿射方式插值 UV,所以随着几何移动,纹理会肉眼可见地扭曲、游动。下面的控件让你在两者间切换,看棋盘直线在仿射下弯曲、在透视校正下猛地变直。
3 · Texture mapping & UV coordinates纹理映射与 UV 坐标
A texture is an image we glue onto a surface to supply detail — a wood grain, a brick pattern, a face — that would be absurd to model as geometry. The gluing is a coordinate assignment. We give every vertex a UV coordinate: a 2D point in the texture's own space, conventionally the unit square [0,1]², with (0,0) one corner and (1,1) the opposite. Interpolate UV across the triangle (perspective-correctly, per section 2), and each pixel gets a (u,v) that names where in the image to look.
纹理是我们贴到表面上以提供细节的一张图像——木纹、砖块图案、一张脸——若用几何去建模会荒谬至极。这种粘贴是一次坐标指派。我们给每个顶点一个 UV 坐标:纹理自身空间里的一个二维点,惯例是单位正方形 [0,1]²,(0,0) 是一角、(1,1) 是对角。把 UV 跨三角形插值(按第 2 节,用透视校正),每个像素就得到一个 (u,v),指明去图像的何处查看。
A crucial distinction: a texel is one cell of the texture image; a pixel is one cell of the screen. They are almost never the same size on screen. A UV of, say, (0.5, 0.5) lands in the middle of the image; to turn it into a texel address you scale by the texture resolution, (u·W, v·H). Because that address is generally fractional and rarely one-to-one with pixels, sampling is its own problem — the heart of sections 4–5.
一个关键区分:纹素(texel)是纹理图像的一个单元;像素(pixel)是屏幕的一个单元。它们在屏幕上几乎从不同样大。比如 UV 为 (0.5, 0.5) 落在图像正中;要把它变成纹素地址,你按纹理分辨率缩放,(u·W, v·H)。因为这个地址通常是小数、且极少与像素一一对应,采样本身就是一个问题——正是第 4–5 节的核心。
What if UV strays outside [0,1] — a vertex at u = 2.3, or a texture meant to tile? The wrap mode decides:
若 UV 越出 [0,1] 该怎么办——一个顶点在 u = 2.3,或一张本就打算平铺的纹理?由环绕模式决定:
| Wrap mode | Rule for out-of-range u | Use |
|---|---|---|
| Repeat | fractional part: u − ⌊u⌋ | tiling patterns (brick, grass) |
| Clamp | pin to [0,1]: min(max(u,0),1) | stretch edge pixel; avoid seams |
| Mirror | reflect each unit: 1.3 → 0.7 | seamless tiles without a hard repeat |
| 环绕模式 | 越界 u 的规则 | 用途 |
|---|---|---|
| Repeat 重复 | 取小数部分:u − ⌊u⌋ | 平铺图案(砖、草) |
| Clamp 钳制 | 钳到 [0,1]:min(max(u,0),1) | 拉伸边缘像素;避免接缝 |
| Mirror 镜像 | 每一单元反射:1.3 → 0.7 | 无硬性重复的无缝平铺 |
4 · Minification & texture aliasing缩小与纹理走样
Now the sampling problem bites. Consider a textured floor stretching to the horizon. Near the camera, one texel covers many pixels — magnification, easy. Far away, one pixel covers many texels — minification. If we point-sample a single texel per pixel, we ignore all the others under that pixel's footprint, and which texel we happen to hit lurches as the camera moves a hair. The result is the shimmering, crawling aliasing you see on distant fences and roof tiles — a sampling failure previewed here and treated in full by lesson 06's sampling theory.
现在采样问题咬人了。设想一片延伸到地平线的带纹理地板。相机附近,一个纹素覆盖许多像素——放大,容易。远处,一个像素覆盖许多纹素——缩小。若每像素只点采一个纹素,我们就忽略了该像素足迹下的所有其他纹素,而恰好命中哪个纹素会随相机挪动一丝而剧烈跳变。结果就是你在远处栅栏与屋瓦上看到的那种闪烁、爬行的走样——一次采样失败,这里先预览,第 06 课的采样理论完整处理。
The right answer would be to average every texel under the pixel's footprint, every frame — far too expensive at runtime. The classic solution precomputes it: the mipmap, a pyramid of prefiltered copies. Level 0 is the full texture; each higher level is a ½×½ box-filtered (averaged) shrink, down to a single texel. Storage costs only 1 + 1/4 + 1/16 + … ≈ 1/3 extra — a third more memory buys a whole prefiltered hierarchy.
正确答案本应是——每一帧都把像素足迹下的每个纹素平均掉——运行时太贵了。经典解法把它预计算:mipmap,一座预滤波副本的金字塔。第 0 层是完整纹理;每往上一层是 ½×½ 盒式滤波(求平均)的缩小,一直缩到单个纹素。存储只多花 1 + 1/4 + 1/16 + … ≈ 1/3——多三分之一的内存,就买下一整座预滤波层级。
At runtime we pick the level whose texels are about pixel-sized. "How many texels does this pixel span?" is answered by the screen-space derivative of UV — how fast (u,v) changes from one pixel to the next, (∂u/∂x, ∂v/∂x, ∂u/∂y, ∂v/∂y). The longest of those gradients gives a texel span ρ, and the level is L = log2 ρ: span two texels → level 1, span four → level 2. A steeply receding floor has a large derivative and reaches for coarse levels; the near floor uses level 0.
运行时我们挑选纹素约为像素大小的那一层。“这个像素跨越多少纹素?”由 UV 的屏幕空间导数回答——(u,v) 从一个像素到下一个变化多快,即 (∂u/∂x, ∂v/∂x, ∂u/∂y, ∂v/∂y)。这些梯度中最长的那个给出纹素跨度 ρ,层级为 L = log2 ρ:跨两纹素 → 第 1 层,跨四 → 第 2 层。一片陡峭后退的地板导数很大、伸向粗层;近处地板用第 0 层。
L is generally fractional (say 2.4), and snapping to the nearest whole level makes a visible seam where the level switches. Trilinear filtering removes it with two blends: do a bilinear sample (the 2×2 texel neighborhood, blended by the fractional texel position) within level 2 and again within level 3, then linearly blend those two results by the 0.4 fraction. "Tri" = two dimensions within a level, plus one across levels. Magnification, where we are below level 0, just uses bilinear on level 0 — there is no coarser detail to reach for.
L 通常是小数(比如 2.4),而硬取到最近的整数层,会在层级切换处留下肉眼可见的接缝。三线性过滤用两次混合消除它:在第 2 层之内做一次双线性采样(2×2 纹素邻域,按小数纹素位置混合),在第 3 层内再做一次,然后用那 0.4 的小数线性混合这两个结果。“三”= 层内两维,加上跨层一维。放大时我们在第 0 层之下,就只在第 0 层做双线性——没有更精细的细节可取。
5 · Anisotropic filtering各向异性过滤
Mipmaps assume the pixel's footprint in texture space is roughly square — isotropic, the same in every direction. On a surface viewed obliquely — that receding floor at a shallow grazing angle — the footprint is not square at all: it is a long, thin sliver, stretched far along the direction of recession and narrow across it. Picking one mipmap level must serve the longest axis to avoid aliasing, but that same coarse level then over-blurs the short axis. This is why plain trilinear makes grazing floors and roads look muddy and smeared into the distance.
mipmap 假设像素在纹理空间中的足迹大致是正方形——各向同性,各个方向都一样。在斜视的表面上——那片以浅掠射角后退的地板——足迹根本不是正方形:它是一道又长又细的薄片,沿后退方向被拉得很长、横向很窄。挑一个 mipmap 层级必须迁就最长的轴以避免走样,但同一粗层随即把短轴过度模糊。这就是为什么单纯的三线性会让掠射的地板和道路看起来浑浊、向远处糊成一片。
Anisotropic filtering ("aniso") fixes it by respecting the footprint's shape. Instead of one sample from an over-coarse level, it takes several trilinear samples spaced along the footprint's long axis and averages them — using a finer level appropriate to the short axis. "16× aniso" means up to sixteen samples along that axis. The cost is those extra taps, spent only where the footprint is elongated; the payoff is crisp, readable texture on grazing surfaces — the single most visible texture-quality setting in games.
各向异性过滤(简称 aniso)通过尊重足迹的形状来修复它。它不取过粗层级的一个样本,而是沿足迹长轴等距取若干次三线性样本再平均——使用适配短轴的更精细层级。“16× aniso”意为沿该轴最多取十六个样本。代价是那些额外的采样,只花在足迹被拉长之处;回报是掠射表面上清晰可读的纹理——这是游戏里最显眼的纹理质量设置。
Cranking the mip level coarser to kill shimmer, or forcing a global mip bias, trades aliasing for blur — the wrong knob. The footprint problem is anisotropy, not overall size, and only anisotropic sampling reads the right texels. Likewise, forgetting perspective-correct interpolation (section 2) is invisible on a flat wall facing the camera and glaring on that same wall seen edge-on — the two failures both bite hardest exactly where a surface recedes.
为了消灭闪烁而把 mip 级别一味调粗、或强行加一个全局 mip 偏置,是用锯齿换模糊——拧错了旋钮。足迹(footprint)问题的本质是各向异性、而非整体尺寸,只有各向异性采样才会读到正确的纹素。同理,忘记透视校正插值(第 2 节)在正对相机的平墙上看不出来,在同一面墙侧看时却刺眼——这两种失败都恰恰在表面向远处退去的地方咬得最狠。
Where this points next接下来指向何处
Every problem in the back half of this lesson — shimmering minification, mipmap level choice, trilinear blends, anisotropic taps — was a workaround for the same underlying fact: a pixel is an area, but we kept sampling it at a point. We patched it per-texture with prefiltering. Lesson 06 states the general theory those patches approximate: signals, frequencies, the Nyquist limit, why point-sampling any high-frequency signal aliases, and how anti-aliasing — supersampling, MSAA, prefiltering — is the principled fix for edges and shading, not just textures. Mipmaps will reappear there as exactly "prefiltering before you undersample."
本课后半的每个问题——闪烁的缩小、mipmap 层级选择、三线性混合、各向异性采样——都是对同一个底层事实的绕行:像素是一块面积,我们却一直在一个点上采样它。我们用预滤波逐纹理打了补丁。第 06 课陈述这些补丁所近似的一般理论:信号、频率、奈奎斯特极限、为什么点采样任何高频信号都会走样,以及抗锯齿——超采样、MSAA、预滤波——为何是对边缘与着色(而不仅是纹理)的有原则的修法。mipmap 会在那里以“欠采样之前先预滤波”的身份原样重现。
Barycentric weights interpolate any vertex attribute — attr(P) = α·a0 + β·a1 + γ·a2 — feeding smooth shading, per-pixel normals, and UVs. But screen-space linear interpolation is wrong under perspective because projection is projective, not affine; the fix is perspective-correct interpolation, attr = (Σ αiai/wi) / (Σ αi/wi) — which is precisely why lesson 02 kept w. Textures attach via UVs in [0,1]² with repeat/clamp/mirror wrap; texels are not pixels. Minification packs many texels into one pixel and aliases; mipmaps prefilter a ½-pyramid (~⅓ extra memory), and trilinear picks the level from the screen-space UV derivative and blends across two levels (bilinear picks texels within a level). Anisotropic filtering takes several samples along an oblique footprint's long axis so grazing surfaces stay crisp instead of over-blurred.
重心权重插值任意顶点属性——attr(P) = α·a0 + β·a1 + γ·a2——喂给平滑着色、逐像素法线与 UV。但屏幕空间线性插值在透视下是错的,因为投影是射影而非仿射;修法是透视校正插值,attr = (Σ αiai/wi) / (Σ αi/wi)——这正是第 02 课保留 w 的原因。纹理通过 [0,1]² 中的 UV 附着,配以重复/钳制/镜像环绕;纹素不是像素。缩小把许多纹素塞进一个像素并走样;mipmap 预滤波出一座 ½ 金字塔(约多 ⅓ 内存),三线性用屏幕空间 UV 导数挑选层级并跨两层混合(双线性在层内挑纹素)。各向异性过滤沿斜足迹的长轴取若干样本,让掠射表面保持清晰、而非被过度模糊。
Interview prompts面试题
- How do you shade the interior of a triangle given only per-vertex data? (§1 — barycentric interpolation: attr(P) = α·a0 + β·a1 + γ·a2 with α+β+γ=1, the same weights reused for color, normal, and UV.) 只给逐顶点数据,你如何给三角形内部着色?(§1 — 重心插值:attr(P) = α·a0 + β·a1 + γ·a2,α+β+γ=1,同一组权重供颜色、法线、UV 复用。)
- Why is interpolating UV linearly in screen space wrong under perspective? (§2 — perspective projection is a projective, not affine, map; equal screen steps are unequal world steps, so a world-linear attribute is nonlinear in screen position and blends bent.) 为什么在屏幕空间线性插值 UV 在透视下是错的?(§2 — 透视投影是射影而非仿射映射;等屏幕步长是不等世界步长,故一个世界线性的属性在屏幕位置上非线性,混合会被掰弯。)
- State the perspective-correct interpolation formula and say why clip-space w is kept. (§2 — attr = (Σ αiai/wi)/(Σ αi/wi); only a/w and 1/w are screen-linear, so lesson 02 defers the ÷w so w survives to reweight by depth.) 写出透视校正插值公式,并说明为何保留裁剪空间 w。(§2 — attr = (Σ αiai/wi)/(Σ αi/wi);只有 a/w 与 1/w 在屏幕上线性,故第 02 课推迟 ÷w,让 w 存活下来按深度重新加权。)
- What problem do mipmaps solve, and roughly what do they cost? (§4 — minification aliasing: many texels per pixel point-sampled shimmer; mipmaps prefilter a ½-each pyramid picked by the UV derivative, costing ~⅓ extra memory.) mipmap 解决什么问题,大约花多少代价?(§4 — 缩小走样:每像素许多纹素被点采样而闪烁;mipmap 预滤波出一座每层减半、由 UV 导数挑选的金字塔,约多 ⅓ 内存。)
- Distinguish bilinear from trilinear filtering. (§4 — bilinear blends the 2×2 texel neighborhood within one level; trilinear does bilinear in the two bracketing levels and blends across them by frac(L), removing the level-switch seam.) 区分双线性与三线性过滤。(§4 — 双线性在一层内混合 2×2 纹素邻域;三线性在夹住的两层内各做双线性、再按 frac(L) 跨层混合,消除层级切换的接缝。)
- Why is anisotropic filtering needed when trilinear already exists? (§5 — an oblique surface's footprint is a long thin sliver; one isotropic mip level over-blurs the short axis, so aniso takes several samples along the long axis to keep grazing surfaces crisp.) 既然已有三线性,为什么还需要各向异性过滤?(§5 — 斜视表面的足迹是又长又细的薄片;单个各向同性 mip 层会过度模糊短轴,故 aniso 沿长轴取若干样本,让掠射表面保持清晰。)