/ Ch11 纹理映射

第11章:纹理映射 — 零基础讲义

讲义说明
本讲义基于 Steve Marschner & Peter Shirley 所著《虎书5》第5版第11章(p.255-290)。
本章是图形学的"皮肤"——它告诉你:怎么把一张 2D 图像"贴"到 3D 物体表面。
学完本章,你将真正理解 mipmap、立方体贴图、阴影贴图、Bump Map、过程纹理等核心概念。
本版插画采用 Guizang 材质插画风格重新绘制。

学习目标

  1. 理解纹理查找的"3 个空间":物体/世界/相机/纹理空间的关系
  2. 掌握纹理坐标函数——怎么把 3D 点映射到 2D 纹理
  3. 掌握插值纹理坐标——三角形内部怎么插值
  4. 理解抗锯齿纹理查找——为什么纹理会出现锯齿
  5. 掌握mipmap——纹理的金字塔采样
  6. 理解各向异性过滤——为什么远处的地板要特别处理
  7. 掌握常见纹理应用:Bump/Normal Map、Displacement、Shadow、Environment
  8. 理解3D 过程纹理:noise、turbulence、stripe

11.0 本章动机:为什么需要纹理?

11.0.1 一切从"丑陋的色块"开始

想象你渲染了一个 3D 球。光照、阴影都对——但球的表面就是一个均匀的红色,没有任何细节。你永远不会相信这是一个"真实"的球。为什么会这样?因为现实世界的物体表面几乎都不是单一颜色——木头有纹路、布料有纤维、皮肤有毛孔、石头有凹凸。

最简单的解决办法是什么?

:把一个高分辨率的 2D 图像("纹理图")"贴"到 3D 物体表面。这个过程就叫纹理映射(Texture Mapping)

11.0.2 三个空间的"翻译游戏"

纹理映射本质上是一个翻译游戏——把 3D 世界坐标翻译成 2D 纹理坐标。这个翻译函数的名字叫 φ(phi):

UV坐标映射
图11-1:UV坐标映射——2D纹理平面的格子通过 φ 函数"贴"到 3D 三角形表面
世界物体 S → 视图投影 π → 屏幕 → 纹理坐标函数 φ → 纹理 T

    3D 点 (x,y,z)  ───π───→  屏幕像素 (i,j)
                          │
                          └──φ──→  纹理坐标 (u,v) → 颜色 RGB
符号名称含义
S场景物体3D 世界中的几何体(球、人物、地面)
π视图投影把 3D 点投影到 2D 屏幕的相机变换(已知)
φ纹理坐标函数把 3D 点映射到 2D 纹理坐标的函数(待设计)
T纹理2D 图像(一堆像素,每个像素有颜色)
核心洞察π 是已知的(视图投影),φ待设计的(纹理坐标)。纹理映射的核心问题就是:怎么把表面点 (x, y, z) 映射到 (u, v)?

11.0.3 简单映射为什么不够用?

最笨的办法:直接把 3D 点的 x, y 当成纹理坐标 u, v。

// 简单映射
u = x
v = y

这个方法在轴对齐的平面上能用,但只要物体倾斜一下,纹理就会"拉伸"——比如一面斜放的墙上,砖块都被拉成了平行四边形。弯曲表面就更没救了。

根本原因:简单映射只对"轴对齐的平面"有效。任意朝向的表面,需要更聪明的 φ 函数。

🤔 想一想:为什么你小时候玩的"贴纸"贴在曲面上会起皱?
因为贴纸的格子是正方形的,但曲面(如球面)的"自然网格"是弯曲的。强行把方形格子贴到曲面上,就会有"拉伸"和"褶皱"。纹理映射本质上就是解决"如何把平的东西贴到曲的东西上"的数学问题。

11.1 查找纹理值

11.1.1 纹理查找的代码

假设我们已经知道表面点的纹理坐标 (u, v),怎么从纹理图上取出对应的颜色?

// 最简单的纹理查找(不考虑抗锯齿)
function texture_lookup(Texture t, float u, float v):
  // 把 [0,1] 的纹理坐标映射到像素索引
  // 减 0.5 是因为像素中心位于 i+0.5
  int i = round(u * t.width()  - 0.5)
  int j = round(v * t.height() - 0.5)
  return t.get_pixel(i, j)
符号含义
u, v归一化的纹理坐标,范围通常 [0, 1]
t.width(), t.height()纹理的像素分辨率(如 1024×1024)
i, j像素的整数索引(从 0 开始)
round(... - 0.5)四舍五入到最近的像素中心
get_pixel(i, j)取出该像素的颜色(RGB)
🤔 想一想:为什么是 u * W - 0.5,而不是 u * W
因为像素是一个"区域",不是"点"。像素 (i, j) 覆盖的范围是 [i, i+1) × [j, j+1),中心位于 (i+0.5, j+0.5)。当 u=0 时,对应纹理图最左边一列像素的中心 (0.5/W);当 u=1 时对应最右边一列像素的中心 (1-0.5/W)。减 0.5 保证这个对应关系正确。如果不减 0.5,u=0 会取到像素 (0, j) 的左边缘(而不是中心)。

11.1.2 这是"最近邻采样"——会有锯齿

上面这段代码就是点采样(Point Sampling),也叫最近邻采样(Nearest Neighbor)。它有两个致命问题:

  1. 锯齿(Aliasing):远处的物体会出现明显的"马赛克",因为一个像素只采了 1 个纹素(texel)
  2. 摩尔纹(Moiré Patterns):当纹理的频率接近屏幕采样频率时,会出现奇怪的干涉条纹

为什么会有锯齿? Ch10 已经讲过——采样率不足。当 1 个屏幕像素覆盖了多个纹素(远处物体)时,我们只取了 1 个纹素的值,丢掉了其他纹素的信息。这种"欠采样"是所有视觉锯齿的根源。

💡 关键术语
- texel(纹素):纹理图像中的单个像素,区别于"屏幕像素"(pixel)
- 采样(sampling):从连续函数中取出离散值
- 重建(reconstruction):从离散样本恢复连续信号

11.2 纹理坐标函数

11.2.1 几何确定的坐标——φ 的 5 种实现

φ 函数的 5 种"现成"实现方式——每种都基于物体的几何形状:

方法 1:平面投影(Planar Projection)

适用:表面大致平面的物体(地板、墙、广告牌)。

// 把 3D 点直接当成 2D 坐标
φ(x, y, z) = (u, v) where [u, v, *, 1]ᵀ = M_t · [x, y, z, 1]ᵀ
UV坐标映射示例
图11-2:UV坐标映射的实际例子——2D 网格上的每个格子对应 3D 表面上的一个小面片
致命问题对闭合物体不单射——前后两个点会映射到同一位置(一个篮球的"前"和"后"会被贴上同一张图的同一块)。

方法 2:投影纹理(Projective Texture)

核心思想:用 4×4 矩阵(投影矩阵)做透视投影——和相机投影场景的原理完全一样!

// 4×4 投影矩阵
φ(x, y, z) = (u/w, v/w) where [u, v, *, w]ᵀ = P_t · [x, y, z, 1]ᵀ

应用:投影纹理(projector)、阴影贴图(shadow map)。

场景用途
投影仪把一张图"投影"到任何表面上
阴影贴图从光源视角"看"场景,记录深度
环境光照从某点"看"环境贴图,模拟间接光

方法 3:球面坐标(Spherical Coordinates)

适用:球形 / 近球形物体(地球仪、星球、灯泡)。

// 把 3D 点转换成 (经度, 纬度)
φ(x, y, z) = ((π + atan2(y, x)) / 2π,    // u = 经度 [0,1]
              (π - acos(z / ||x||)) / π)   // v = 纬度 [0,1]
符号含义
atan2(y, x)点 (x,y) 的极角(范围 [-π, π])
acos(z / ||x||)从 z 轴算起的极角(范围 [0, π])
||x||向量长度 sqrt(x² + y² + z²)
问题极点处有严重失真(一片区域压缩到点)。地球的南极附近,国家会被压扁成奇怪的形状。

方法 4:圆柱坐标(Cylindrical Coordinates)

适用:柱形物体(杯子、树干、铅笔)。

φ(x, y, z) = ((1 / 2π) · (π + atan2(y, x)),    // u = 角度 [0,1]
              ½ · [1 + z])                       // v = 高度 [0,1]

圆柱坐标和球面坐标很像,但 v 方向直接用 z(线性),不会有极点失真问题。

方法 5:立方体贴图(Cubemap)

适用:环境光照、反射(最常用)。

核心思想:用 6 个正方形纹理分别对应立方体的 6 个面。查找时,根据光线方向判断用哪个面:

// 6 个面的投影
φ_±x(x, y, z) = ½ [1 ± (-y, -z) / |x|]
φ_±y(x, y, z) = ½ [1 ± (x, -z) / |y|]
φ_±z(x, y, z) = ½ [1 ± (x, -y) / |z|]

// 选择 |·| 最大的那一面
chosen_face = argmax(|x|, |y|, |z|)
💡 OpenGL 约定:u 顺时针,v 反时针。环境贴图、光照探针、反射贴图都用 cubemap。

11.2.2 插值纹理坐标(最常用的方法)

对于复杂的任意形状物体(如恐龙模型),几何方法难以构造纹理坐标。怎么办?

答案在顶点存储纹理坐标,三角形内部用重心插值。这就是 OpenGL/UE/Unreal 的标准做法。

// 三角形三个顶点 (u₀, v₀), (u₁, v₁), (u₂, v₂)
// 内部点 (α, β, γ) 的纹理坐标
u = α · u₀ + β · u₁ + γ · u₂
v = α · v₀ + β · v₁ + γ · v₂

// 其中 α + β + γ = 1,且都 ≥ 0
符号含义
(u₀, v₀), (u₁, v₁), (u₂, v₂)三个顶点的纹理坐标(顶点属性)
α, β, γ重心坐标(决定内部点位置)
α + β + γ = 1重心坐标的归一化条件
🏠 生活类比:贴地砖

想象你在贴地砖。每块地砖的"位置"就是 (u, v) 坐标——左下角是 (0,0),右上角是 (1,1)。当你知道地砖的四个角落在房间的什么位置时,房间内任何一点的地砖颜色都可以通过插值算出来。
重心插值 = "我离这个角 30%,离那个角 50%,离第三个角 20%,所以颜色 = 30%×角1 + 50%×角2 + 20%×角3"

11.2.3 UV 展开:把 3D 摊成 2D

对于任意形状的 3D 物体,怎么决定每个顶点的 UV 坐标?这就是 UV 展开(UV Unwrapping)——把 3D 表面"剪开"成 2D 平面。

例子:20 面体(Icosahedron)的 UV 展开

  • 优点:失真为零(每个三角形大小相同)
  • 缺点:接缝多(每个三角形都要接到下一行)
接缝处理的关键:接缝处复制顶点(同位置不同 UV)。这样能避免接缝两侧用"错误路径"插值,导致纹理扭曲。

UV 展开品质的 3 个评估指标:

指标含义
连续性(continuity)相邻三角形共享 UV → 自动满足
单射性(injectivity)UV 区域内不重叠(无"自交")
失真最小3D 和 2D 三角形面积/形状比例接近 1

11.2.4 平铺、包裹和纹理变换

3 种 wrap mode(包裹模式)

模式行为应用
Tiling(平铺)超出 [0,1] 后取模(重复)棋盘、墙砖、地面
Clamping(钳制)超出 [0,1] 后固定为 0 或 1单一图像(贴 logo)
Hybrid(混合)边缘 wrap + 内部 tile棋盘的边框

纹理变换:在原 UV 上叠加 2D 仿射变换

φ'(x) = M_T · φ_model(x)

其中 M_T 是一个 3×3 矩阵:
| cosθ  -sinθ  tx |
| sinθ   cosθ  ty |
|  0      0     1 |
核心洞察M_T 是一个 3×3 矩阵(2D 仿射变换),让你可以动态调整 UV 而不改原模型。在游戏里这非常常用——比如水面纹理流动、UI 动画。

11.3 抗锯齿纹理查找

11.3.1 像素的"足迹"(Footprint)

抗锯齿的核心思想:不要只采 1 个纹素,要采一片纹素并加权平均。这一片纹素就叫像素的"足迹"(footprint)。

纹理过滤对比
图11-3:三种纹理过滤——点采样(像素化)、双线性(模糊)、三线性(清晰)

关键问题:不同位置的像素,对应的纹理区域大小和形状都不同

位置足迹大小形状
近处物体小(只覆盖少量 texel)近似正方形
远处物体大(覆盖很多 texel)近似正方形
斜视的地面/墙壁长条形

足迹的数学表达

ψ = φ ∘ π⁻¹     // 把屏幕坐标映射回纹理坐标
footprint(x) = ψ(像素面积)   // 像素的 1×1 正方形 → 纹理的某个区域

11.3.2 足迹的线性近似(Jacobian 矩阵)

Jacobian 矩阵 J 近似足迹:

J = [ ∂u/∂x  ∂u/∂y ]
    [ ∂v/∂x  ∂v/∂y ]

几何意义:单位像素面积 → J 决定的平行四边形(足迹)

filter 宽度 D

D = max{ ||u_x||, ||u_y|| }
符号含义
u_x, u_yJ 的两列向量(足迹的两个主方向)
||·||向量的长度
D足迹的"直径"——需要采样的纹素数
核心洞察J 是 GPU 抗锯齿纹理查找的核心。在着色器里,GPU 自动计算屏幕空间 (x, y) 对纹理 (u, v) 的导数(也叫 screen-space derivatives)。

11.3.3 重建(Reconstruction)——放大情况

当足迹 < 1 个 texel(放大):

重建 = 插值 = 卷积 with reconstruction filter。最常用:双线性插值(4 个 texel 加权平均)

def tex_sample_bilinear(Texture t, float u, float v):
    # 1. 把 [0,1] 纹理坐标缩放到像素坐标
    u_p = u * t.width  - 0.5
    v_p = v * t.height - 0.5
    
    # 2. 找到周围 4 个像素
    iu0 = floor(u_p); iu1 = iu0 + 1   # 左、右
    iv0 = floor(v_p); iv1 = iv0 + 1   # 下、上
    
    # 3. 计算权重(距离 4 个像素的距离)
    a_u = iu1 - u_p; b_u = 1 - a_u     # 水平权重
    a_v = iv1 - v_p; b_v = 1 - a_v     # 垂直权重
    
    # 4. 加权平均 4 个 texel
    return a_u * a_v * t[iu0][iv0]   # 左下 \
         + a_u * b_v * t[iu0][iv1]   # 左上 \
         + b_u * a_v * t[iu1][iv0]   # 右下 \
         + b_u * b_v * t[iu1][iv1]   # 右上
变量含义
u_p, v_p像素空间的浮点坐标
iu0, iu1左、右两个像素索引(整数)
a_u, b_u对左、右两个像素的权重(距离反比)
a_u + b_u = 1权重和为 1(凸组合)
💡 重要性能考虑:双线性插值 = 4 次纹理采样。在现代 GPU 中,纹理采样是性能瓶颈(受 memory latency 限制)。尽量避免不必要的纹理读取。

11.3.4 Mipmap —— 缩小情况

问题:当足迹 > 1 个 texel 时(缩小),直接采样会锯齿/混叠

Mipmap 解决方案 = 预计算的多分辨率纹理。每层都是上一层的 1/2 缩放。

Mipmap金字塔
图11-4:Mipmap金字塔——从 1024×1024(底层)到 1×1(顶层),每层都是上一层的 1/2 缩放
M0:  1024×1024   (原图)
M1:   512×512    (1/2)
M2:   256×256
M3:   128×128
...
M10:    1×1

Mipmap 的 3 大优势

  1. 内存可控:多 33% 内存(1 + 1/4 + 1/16 + ... < 4/3)
  2. 采样时根据足迹大小选择层:自动适配远近物体
  3. 抗锯齿效果立竿见影:比点采样好 10 倍

Mipmap 层选择公式

k = log₂(D) (D = filter 宽度)
k0 = floor(k); k1 = k0 + 1
# 在两层间线性插值(trilinear 过滤)
result = a · M[k0] + b · M[k1]
参数含义
Dfilter 宽度(足迹直径)
k目标 mipmap 层级(连续值)
k0, k1夹住 k 的两个整数层
a, b两层间的插值权重,a + b = 1
🤔 想一想:为什么 Mipmap 只用 33% 额外内存?
几何级数:1 + 1/4 + 1/16 + 1/64 + ... = 1 / (1 - 1/4) = 4/3。也就是原图内存的 1.33 倍。这是 Mipmap 在内存上"经济"的数学原因。

11.3.5 各向异性过滤

问题:当足迹是长条形(如远处的地面),mipmap 会过度模糊

原因:mipmap 只考虑方形区域。对于长条形足迹,它只能选一个"最坏"的方形 mip 层,结果是垂直方向模糊、水平方向也模糊——但我们其实只想要垂直方向模糊。

各向异性过滤 = 沿长条形方向多次采样

# 各向异性过滤伪代码
# 1. 计算足迹的长宽比
λ = max(||u_x||, ||u_y||) / min(||u_x||, ||u_y||)

# 2. 沿长边方向采 N 次(向上取整到 1, 2, 4, 8, 16)
N = clamp(round(λ), 1, 16)

# 3. 在长条形上等距采 N 个样本并平均
color = 0
for i in 0..N-1:
    t = (i + 0.5) / N
    pos = center + t * (long_axis)
    color += sample_mipmap(pos)
return color / N
质量样本数适用
point1最差,1 个纹素
bilinear4基本无锯齿
trilinear8(2 层 × 4 像素)远近过渡平滑
anisotropic 2x2 × 4 = 8地面/墙面的标准
anisotropic 16x16 × 4 = 64极限质量
💡 质量对比point < bilinear < trilinear < anisotropic。质量越好,代价是采样数增加。现代游戏一般开 4x~8x 各向异性。

11.4 纹理映射的应用

11.4.1 控制着色参数

纹理不只能控制漫反射颜色——它能控制几乎任何着色参数:

用途纹理内容图11.x
漫反射颜色砖墙、木纹、布料图案图11.18
镜面反射率抛光区(白色=高光,黑色=粗糙)图11.20
粗糙度磨砂区
环境贴图模拟天空/灯光图11.32
凹凸/法线表面细节(不真改几何)图11.24
位移真凹凸(改顶点)图11.25
阴影光源视角的深度图图11.27

例子:图11.20 陶瓷马克杯的镜面粗糙度由反相的漫反射纹理控制——白色部分代表高光,黑色部分代表粗糙。这种"用一个纹理的不同颜色通道"的做法在游戏里非常常见。

11.4.2 法线贴图和凹凸贴图

法线贴图 = 存储 3D 法线(扰动后的)。

法线贴图原理
图11-5:法线贴图原理——左边是"假凹凸"(平面 + 扰动法线),右边是"真几何"(实际 3D 凹凸)

为什么需要法线贴图?

直接在着色时改 normal = 看起来像有凹凸。比真实几何便宜 1000 倍。一块砖墙的法线贴图只需要 4 KB,但 1 千万个真实的砖头三角形要 1 GB。

凹凸贴图(Bump Map) = 高度场(1D 标量)

从高度场到法线的推导

∂h/∂x → 沿 x 方向的斜率
∂h/∂y → 沿 y 方向的斜率

扰动后的法线:
n_perturbed = n + (-∂h/∂x) · t + (-∂h/∂y) · b

其中 t 和 b 是切线空间基向量(TBN 矩阵)
符号含义
h(x, y)高度场(凹凸贴图)
∂h/∂x, ∂h/∂y沿两个切线方向的斜率
n原始法线(未扰动)
t, b切线空间的两个基向量(tangent, bitangent)
n_perturbed扰动后的法线
关键洞察法线贴图是"假凹凸"——边缘还是平的。真正的凹凸需要 displacement mapping(修改顶点)。

切线空间(TBN 矩阵)

  • 存储在切线空间(相对表面)而非世界空间
  • 原因:法线贴图需要随物体表面"一起动"
  • 需要切线向量(T, B, N)

11.4.3 位移贴图(Displacement Maps)——真凹凸

真凹凸 = 移动顶点位置

vertex.position += h(uv) · normal

优点

  1. 边缘和轮廓都正确(不是平面)
  2. 能做自阴影(凹凸贴图做不到)
  3. 光线交互是真实的

缺点

  1. 需要高密度网格(否则形变不够)
  2. 内存和计算成本高
  3. 不适合实时渲染(常用在离线渲染)
🤔 想一想:法线贴图 vs 位移贴图如何选择?
实时(游戏)→ 法线贴图。物体不需要太复杂的几何细节,但表面光照要"看起来"对。离线(电影)→ 位移贴图。物体边缘的轮廓必须真实,即使需要几亿三角形。

11.4.4 阴影贴图(Shadow Maps)

核心思想从光源视角渲染一次深度图

# Pass 1: 从光源看
shadow_map[每个像素] = 最近表面的深度

# Pass 2: 从相机看
for each fragment:
    # 1. 把 fragment 投影到光源的"屏幕"
    light_space_pos = light_proj × fragment_pos
    
    # 2. 查 shadow_map
    closest = shadow_map[light_space_pos]
    
    # 3. 比较
    if fragment.depth > closest + bias:
        in_shadow = True
参数含义
light_proj光源的视图-投影矩阵(和相机类似)
light_space_posfragment 在光源"屏幕"上的位置
closestshadow_map 在该位置记录的最近深度
bias阴影偏移(避免自影)

关键技术点

  • shadow bias:避免自影(self-shadowing)
  • percentage closer filtering (PCF):多次采样平均,让阴影边缘更柔和
  • shadow acne:bias 不够大会出现斑点

💡 实战:UE / Unity 的实时光照基本都是用 shadow map(加 PCF)。

11.4.5 环境贴图(Environment Maps)

问题:漫反射光来自所有方向——不能用点光源或面光源近似。

环境贴图 = 整个 360° 颜色图

2 种表示

类型存储优点缺点
球面贴图(Spherical)6 个面展开成"十字"形连续极点失真
立方体贴图(Cubemap)6 个正方形无失真需要 6 张图

应用

  1. 反射贴图(Reflection Mapping):镜面反射
  2. 环境光照(Environment Lighting):漫反射光
# 反射贴图伪代码
def shade_fragment(view_dir, normal):
    reflect = reflect(view_dir, normal)
    (u, v) = spheremap_coords(reflect)
    return texture_lookup(env_map, u, v)
核心洞察环境贴图 = 烘焙的环境光照——不需要实时计算每个方向的漫反射光。这是离线渲染的"圣杯"——把整个光照预算成一张 6 面图。

11.5 过程化 3D 纹理

11.5.1 为什么需要 3D 纹理?

2D 纹理的限制

  • 必须有 UV 参数化(很多时候很复杂)
  • 有接缝和失真
  • 不能"雕刻"立体感

3D 纹理 = 在 3D 空间定义的纹理

关键优势直接在 3D 坐标取样——不需要 UV!把一个 3D 纹理函数 f(x, y, z) 想象成一个"无限延伸的纹理云",无论你的表面在哪里,都直接用世界坐标取样。

11.5.2 3D 条纹

最简单的 3D 纹理 = 周期性切换两种颜色:

def RGB_stripe(point p):
    if sin(x_p) > 0:
        return c0    # 第一种颜色
    else:
        return c1    # 第二种颜色

可控宽度

def RGB_stripe(point p, real w):
    if sin(π · x_p / w) > 0:
        return c0
    else:
        return c1

其中 w 控制条纹的"宽度"——w 越大,条纹越宽。

11.5.3 Perlin 噪声——"平滑的随机"

什么是"平滑噪声"?

特性描述
在每个点是伪随机看起来杂乱无章
相邻点相似(不跳跃)看起来"自然"
类似 TV static 但更平滑有"质感"

Perlin 噪声的实现

  • 3D 网格,每个格点有随机梯度
  • 用 Hermite 三次插值(不是线性)
# Perlin 噪声数学公式
n(x, y, z) = Σᵢ Σⱼ Σₖ ω(x-i, y-j, z-k) · Γᵢⱼₖ

其中:
ω(t) = 2|t|³ - 3|t|² + 1   # Hermite 三次权重
Γᵢⱼₖ = 预计算的随机单位向量
符号含义
ω(t)Hermite 三次权重函数(保证 C¹ 连续)
Γᵢⱼₖ网格点 (i, j, k) 的随机单位梯度向量
n(x, y, z)Perlin 噪声函数值(通常 [-1, 1])

应用

  • 石头、木纹、云、皮肤
  • modular noise = 同一函数,不同参数

11.5.4 湍流(Turbulence)——多个频率的噪声叠加

湍流 = 多个频率的噪声叠加

# 湍流公式
n_t(x) = Σ |n(2ⁱ · x)| / 2ⁱ

i 从 0 到 N(典型 N = 6 ~ 8)
符号含义
2ⁱ · x第 i 层频率翻倍(2×, 4×, 8×, ...)
|n(...)|取绝对值(避免负值抵消)
1 / 2ⁱ每层的权重(高频贡献小)
n_t(x)叠加后的湍流值

为什么用湍流?

单个 noise 太"平滑"(像鹅卵石)。湍流有"分形"感(细节无处不在)——像云、烟、火焰这种自然现象。原因是它把多个尺度的细节叠加起来:从大轮廓到小细节都有。

应用:云、烟、火焰、地形

Turbstripe 例子

def RGB_turbstripe(point p, double w):
    double t = (1 + sin(k1 · z_p + turbulence(k2 · p))) / w
    return t · s0 + (1-t) · s1

把湍流作为正弦函数的相位扰动——条纹变得"扭曲",模拟自然纹理(如大理石、木纹)。

🏠 生活类比:做云

想象你在画一朵云。如果你只画一个大圆形,它会显得很"卡通"(像 Photoshop 的圆)。如果你先画几个大圆形作为云的"骨架",再在每个大圆形上画几个中圆形,最后在每个中圆形上画几个小圆点——这种"层层叠叠"就是湍流的思路。每一层都是噪声,但频率不同,叠加后才有"云"的质感。


全章总结

讲义核心洞察:纹理映射是图形学的"皮肤"——

  • 3 个空间:物体/世界/相机/纹理空间,φ 函数是核心
  • 纹理坐标函数:平面/球面/圆柱/立方体/插值 UV
  • 抗锯齿:足迹(footprint)→ mipmap → 各向异性
  • 应用:Bump/Normal/Displacement/Shadow/Environment
  • 3D 纹理:noise、turbulence、stripe
主题核心公式/方法关键洞察
纹理查找t.get_pixel(round(u*W - 0.5), round(v*H - 0.5))中心点采样,减 0.5 校正
几何 φ平面 / 球面 / 圆柱 / 立方体各有限制,闭合物体不单射
重心插值u = α·u₀ + β·u₁ + γ·u₂三角形内任意点的纹理坐标
双线性插值4 texel 加权平均放大情况的标准方案
Mipmap 层k = log₂(D)33% 内存换抗锯齿
各向异性沿长边多次采样长条形足迹的最佳方案
Bump 法线n' = n + (-∂h/∂x)t + (-∂h/∂y)b假凹凸,边缘还是平面
阴影判定if (d > shadow_map[uv] + bias) 阴影shadow bias 避免自影
Turbulencen_t(x) = Σ |n(2ⁱ x)| / 2ⁱ多个频率的噪声叠加
📚 关键术语速查
  • φ (phi):纹理坐标函数,3D → 2D 的映射
  • Texel:纹素(纹理图像中的单个像素)
  • Footprint:足迹(屏幕像素覆盖的纹理区域)
  • Mipmap:多分辨率纹理金字塔
  • Trilinear:在两层 mipmap 间线性插值
  • Anisotropic:各向异性过滤(针对长条形足迹)
  • TBN:切线空间基向量(Tangent, Bitangent, Normal)
  • Perlin Noise:伪随机但平滑的噪声函数
  • Turbulence:多频率噪声叠加

一句话总结:纹理映射 = 用 φ 函数把 3D 表面点映射到 2D 纹理 → 抗锯齿采样(双线性 + mipmap + 各向异性)→ 应用于漫反射/法线/位移/阴影/环境光 → 高级用 3D 过程纹理(noise + turbulence)。

下一步:第 12 章将讲解图形学的数据结构——mesh 怎么存?空间查询怎么做?这是所有游戏引擎的基础。

目录 X
章节 X