课程主讲:闫令琪 (Lingqi Yan) | UCSB B站课程链接:https://www.bilibili.com/video/BV1X7411F744 本章对应课件:第 7 讲(Illumination, Shading and Graphics Pipeline)、第 8 讲(Shading, Pipeline and Texture Mapping)、第 9 讲(Texture Mapping cont.)
说明:官方精简版课件中,纹理应用(环境贴图、凹凸/法线贴图、位移贴图、3D 纹理)位于第 10 讲、阴影贴图位于第 12 讲;本文按 B 站完整版课程结构将其归入着色主题之下。
着色 (Shading) 是将材质属性应用到物体上的过程,决定了物体在不同光照条件下呈现的颜色和外观。本文涵盖深度缓冲(可见性/遮挡)、Blinn-Phong 反射模型、着色频率、图形渲染管线、纹理映射及其相关技术。
1 深度缓冲 (Z-Buffer)
问题

多个三角形可能覆盖同一个像素,如何确定哪个三角形在前面?
画家算法 (Painter’s Algorithm)

思路:先画远处的物体,再画近处的物体(远的被近的覆盖,绘制时覆盖帧缓冲中的颜色)。
问题:
- 需要对三角形按深度排序($O(n \log n)$,n 为三角形数),计算量大
- 可能出现循环遮挡的情况,无法确定深度顺序(unresolvable depth order)
Z-Buffer 算法

核心思想:为每个采样点(像素)存储当前最小的深度值 $z$,渲染时比较深度(这也是最终胜出的可见性算法)。
简化假设(课件强调):规定 $z$ 总为正值,且 $z$ 越小越近、越大越远(smaller z → closer, larger z → further)。
数据结构:
- 帧缓冲 (Framebuffer):存储每个像素的颜色值
- 深度缓冲 (Depth Buffer / Z-Buffer):存储每个像素的深度值
算法流程:
- 初始化:将深度缓冲中每一个像素设为无穷大(最远),帧缓冲设为背景色
- 遍历三角形:逐个处理场景中的每个三角形
- 光栅化:对当前三角形的每个采样点 $(x, y, z)$,计算其屏幕空间位置与深度值
- 深度测试:比较当前点的深度 $z$ 与深度缓冲中该像素已存储的深度值
- 更新条件:仅当 $z$ 更小(更靠近相机)时,才替换该像素的颜色与深度值
- 重复:对全部三角形执行上述步骤,最终帧缓冲即为可见图像
// 初始化:深度缓冲置为无穷大
for each pixel (x, y):
zbuffer[x][y] = ∞ // 初始化为最大深度
framebuffer[x][y] = background_color
// 渲染
for each triangle T:
for each sample (x, y, z) in T:
if z < zbuffer[x, y]: // 目前最近的采样点
framebuffer[x, y] = rgb // 更新颜色
zbuffer[x, y] = z // 更新深度
else:
; // 什么都不做,该采样点被遮挡

关键特性:
- 时间复杂度:$O(n)$,n 为三角形数(假设每个三角形覆盖恒定数量的像素 constant coverage)
- 无需对三角形排序
- 每个三角形独立处理,与绘制顺序无关
- 最重要的可见性算法,所有 GPU 都在硬件层面实现
注意:
- 深度值通常不是线性的(透视投影后)
- 深度精度有限,可能导致 Z-fighting(深度冲突)
2 着色基础
在韦氏词典中,shading 的定义是"用平行线或色块使插图或图表变暗或着色"。
在图形学中,着色是指将材质 (material) 应用到物体上的过程。
着色考虑三个关键要素:
| 要素 | 说明 |
|---|---|
| 材质属性 | 物体表面的颜色、光泽度、反射率等 |
| 光照 | 光源的强度、方向、颜色 |
| 观察方向 | 相机看向物体的方向 |
3 Blinn-Phong 反射模型
模型概述
Blinn-Phong 反射模型是一个简单但实用的经验光照模型,将光照分为三个分量:

| 分量 | 中文名 | 特点 |
|---|---|---|
| Specular highlights | 高光反射 | 镜面反射,与观察方向相关 |
| Diffuse reflection | 漫反射 | 光线均匀散射,与观察方向无关 |
| Ambient lighting | 环境光 | 常数项,模拟间接光照 |
着色是局部的
着色在一个特定的着色点 (shading point) 上计算朝相机反射的光,输入包括:

| 输入量 | 符号 | 说明 |
|---|---|---|
| 观察方向 | $\vec{v}$ | 从着色点指向相机的单位向量 |
| 表面法线 | $\vec{n}$ | 着色点处垂直于表面的单位向量 |
| 光照方向 | $\vec{l}$ | 从着色点指向光源的单位向量(每个光源一个) |
| 表面参数 | — | 颜色 (color)、光泽度 (shininess) 等 |

关键特性:着色是局部的——它只考虑该点自身的信息,不会产生阴影(shading ≠ shadow),因为着色时不知道其他物体的存在。
漫反射 (Diffuse Reflection)
漫反射是光学中的一个基本概念,指的是光线照射到粗糙表面时,向各个方向均匀散射的现象。

基本原理
- 各向同性散射:入射光被分散到半球空间内的各个方向
- 与视角无关:观察者在不同位置看到的亮度基本一致(遵循朗伯余弦定律)
- 无镜像:不像镜面反射那样形成清晰的像
漫反射的核心特点:光线被均匀地散射到各个方向,因此表面颜色与观察方向无关。
Lambert 余弦定律
光线到达表面的能量与光照方向和法线之间的夹角有关。

- 正方体顶面垂直于光线时,接收到全部光照
- 旋转 60 度后,同样面积只接收到一半的光照
- 一般地,单位面积接收的光能与 $\cos\theta = \vec{l} \cdot \vec{n}$ 成正比
其中 $\theta$ 是光照方向 $\vec{l}$ 和表面法线 $\vec{n}$ 之间的夹角。
光线衰减
光从点光源发出后,能量随距离衰减。

在距离 $r$ 处,光强为 $\frac{I}{r^2}$(点光源向四周辐射,能量分布在更大的球面上):
$$ I(r) = \frac{I}{r^2} $$其中 $I$ 是光源在单位距离处的强度。
漫反射公式
综合 Lambert 余弦定律和光线衰减,漫反射的着色公式为:
$$ L_d = k_d \cdot \frac{I}{r^2} \cdot \max(0, \vec{n} \cdot \vec{l}) $$| 符号 | 含义 |
|---|---|
| $L_d$ | 漫反射光(最终看到的颜色) |
| $k_d$ | 漫反射系数(漫反射率,表示表面颜色) |
| $I$ | 光源强度 |
| $r$ | 着色点到光源的距离 |
| $\vec{n} \cdot \vec{l}$ | 法线与光照方向的点积($\cos\theta$) |
| $\max(0, \cdot)$ | 防止负值(背光面不接收光照) |
漫反射系数 $k_d$ 的效果:$k_d$ 越大,表面越亮;$k_d = 0$ 表示完全不反射漫反射光。

注意:漫反射与观察方向 $\vec{v}$ 无关——无论从哪个方向看,漫反射颜色都一样。
高光反射 (Specular)
基本原理
高光强度依赖于观察方向:在镜面反射方向附近看起来最亮。

半程向量 (Half Vector)
Blinn-Phong 模型引入了半程向量来简化计算:
$$ \vec{h} = \text{bisector}(\vec{v}, \vec{l}) = \frac{\vec{v} + \vec{l}}{\|\vec{v} + \vec{l}\|} $$当 $\vec{h}$ 恰好等于表面法线 $\vec{n}$ 时,入射光和观察方向关于法线对称。

核心思想:观察方向 $\vec{v}$ 接近镜面反射方向,等价于半程向量 $\vec{h}$ 接近法线 $\vec{n}$。用单位向量的点积来衡量接近程度:$\vec{n} \cdot \vec{h} = \cos\alpha$。
为什么用半程向量而不是直接计算镜面反射方向?因为半程向量的计算更简单(只需向量加法和归一化),而计算反射方向需要更多运算。
指数 p 的作用
直接使用 $\cos\alpha$ 作为高光强度会导致高光范围过大,不符合真实情况。通过引入指数 $p$,可以缩小高光范围(收窄反射波瓣):
$$ \max(0, \cos\alpha)^p $$
| 指数 $p$ | 效果 |
|---|---|
| $p = 1$ | 高光范围很大,不自然 |
| $p = 64$ | 高光范围适中 |
| $p = 128$ | 高光范围很小,接近真实镜面 |
| $p \to \infty$ | 趋近完美镜面反射 |
增加 $p$ 会收窄反射波瓣 (reflection lobe),使高光更集中。
课件还给出了 $k_s$ 从 0 到 1、$p$ 从 1 逐渐增大的一组对比:$k_s$ 越大高光越亮,$p$ 越大高光范围越小、越接近镜面效果;注意该对比图显示的是 $L_d + L_s$ 的合成结果,而非单独的高光项。

高光公式
$$ L_s = k_s \cdot \frac{I}{r^2} \cdot \max(0, \vec{n} \cdot \vec{h})^p $$| 符号 | 含义 |
|---|---|
| $L_s$ | 高光反射光 |
| $k_s$ | 高光系数(镜面反射率) |
| $\vec{h}$ | 半程向量 |
| $p$ | 高光指数(控制高光大小) |
环境光 (Ambient)
环境光是不依赖于任何因素的着色:添加一个常数颜色来近似被忽略的间接光照,并填充黑色阴影区域。

| 符号 | 含义 |
|---|---|
| $L_a$ | 环境光(反射的环境光) |
| $k_a$ | 环境光系数 |
| $I_a$ | 环境光强度 |
特点:
- 添加一个常数颜色来近似被忽略的间接光照
- 填充阴影区域,避免完全黑暗
- 这是一种近似/假的方法,不是真正的全局光照
完整 Blinn-Phong 模型
将三个分量相加,得到完整的 Blinn-Phong 反射模型:
$$ L = L_a + L_d + L_s = k_a I_a + k_d \frac{I}{r^2} \max(0, \vec{n} \cdot \vec{l}) + k_s \frac{I}{r^2} \max(0, \vec{n} \cdot \vec{h})^p $$
4 着色频率 (Shading Frequencies)
三种着色频率
下面图中三个球体有什么着色差异?

同样的几何模型,使用不同的着色频率会产生截然不同的效果。图中从左向右依次为,着色应用在面、顶点、像素。
Flat Shading(平面着色)

- 着色对象:每个三角形(面)
- 方法:每个三角形面有一个法线向量,整个面使用同一个颜色
- 特点:三角形面是平的,适合平面物体
- 缺点:不适合光滑表面,能看到明显的三角形边界
Gouraud Shading(顶点着色)

- 着色对象:每个顶点
- 方法:每个顶点有法线向量,在顶点处计算着色,然后通过插值得到三角形内部的颜色
- 特点:颜色在三角形内平滑过渡
- 问题:高光可能被丢失(如果高光恰好落在三角形内部而非顶点处)
Phong Shading(像素着色)

- 着色对象:每个像素
- 方法:通过插值得到每个像素的法线向量,在每个像素上计算完整的着色模型
- 特点:最平滑的效果
- 注意:Phong Shading(着色频率)≠ Blinn-Phong Reflectance Model(反射模型),两者是不同的概念
三种频率对比

| 着色频率 | 着色对象 | 法线来源 | 效果 | 计算量 |
|---|---|---|---|---|
| Flat | 三角形面 | 面法线 | 有棱角 | 最小 |
| Gouraud | 顶点 | 顶点法线 | 较平滑 | 中等 |
| Phong | 像素 | 插值法线 | 最平滑 | 最大 |
何时选择哪种频率? 当几何模型足够密集(三角形足够小)时,Flat Shading 也能产生很好的效果。着色频率的选择取决于几何精度和性能需求的平衡。
顶点法线的计算
顶点法线的最佳来源是底层几何体本身(例如球体的顶点法线就是从球心指向顶点的方向)。
如果无法从几何体获取,则需要从相邻三角形面推断:

即对顶点周围所有相邻面的法线求平均(或加权平均),然后归一化。
像素法线的计算

通过重心坐标插值(见第 7 节)从顶点法线获得每个像素的法线向量。
重要提醒:插值后必须归一化法线向量。
5 图形渲染管线 (Graphics Pipeline)
完整流程
图形渲染管线描述了从 3D 顶点输入到 2D 图像输出的完整过程:

| 阶段 | 输入 | 处理内容 | 输出 |
|---|---|---|---|
| 顶点处理 (Vertex Processing) | 3D 空间顶点 | MVP 变换、顶点着色 | 屏幕空间顶点 |
| 三角形处理 (Triangle Processing) | 顶点流 | 组装三角形 | 三角形流 |
| 光栅化 (Rasterization) | 三角形 | 采样三角形覆盖 | 片段流(每覆盖一个采样点产生一个片段) |
| 片段处理 (Fragment Processing) | 片段 | Z-Buffer 深度测试、着色(Blinn-Phong)、纹理映射 | 着色后的片段 |
| 帧缓冲操作 (Framebuffer Operations) | 着色片段 | 写入帧缓冲 | 最终图像 |
下面是一些图形渲染管线的实际在处理的案例:





Shader 程序
现代图形 API(如 OpenGL)允许程序员通过 Shader 自定义管线中顶点处理和片段处理阶段的操作。
Shader 描述的是对单个顶点(或单个片段)的操作,每个顶点或像素片段都会执行。
示例 GLSL 片段着色器(执行一次 per fragment):
uniform sampler2D myTexture; // 程序参数:纹理
uniform vec3 lightDir; // 程序参数:光照方向
varying vec2 uv; // 每片段值(由光栅器插值)
varying vec3 norm; // 每片段值(由光栅器插值)顶点法线
void diffuseShader()
{
vec3 kd; // 系数
kd = texture2d(myTexture, uv); // 从纹理获取材质颜色
kd *= clamp(dot(-lightDir, norm), 0.0, 1.0); // Lambertian 着色
gl_FragColor = vec4(kd, 1.0); // 输出片段颜色
}
说明:
uniform:程序级参数,所有片段共享,全局变量varying:每片段值,由光栅器从顶点插值得到- 输出颜色存储在
gl_FragColor中 - Shader 函数对每个片段执行一次,输出该片段屏幕采样位置处的表面颜色

Shadertoy 在线着色器网站,上图案例访问地址:https://www.shadertoy.com/view/ld3Gz2 。遗憾的是课件提供的外网链接视频已经不在了,这里就不提供了。
一些游戏引擎内部已经做好了很多算法来实现模型渲染、阴影、实时全局光照等等,游戏开发只需要关注游戏开发本身,不需要太多关注计算机图形学背后的算法实现。但是对于我们想要更进一步了解图形渲染,理解背后的原理还是很重要的。
GPU
GPU(图形处理器)是专门用于执行图形渲染管线计算的处理器,GPU 分为两种,一种独立显卡,另外一种是集成显卡。

| 特点 | 说明 |
|---|---|
| 异构多核 | 数千个并行处理核心 |
| 专用设计 | 针对图形管线优化 |
| 硬件实现 | Z-Buffer 等算法在硬件层面实现 |

现代 GPU 目标:实时渲染数十万至数百万三角形的复杂场景,高分辨率(2-4 百万像素 + 超采样),30-60 FPS(VR 更高)。
随着 AI 的发展,GPU 在大模型计算方面发光发热。这背后正是得益于上文提到的 GPU 异构多核、大规模并行架构:图形渲染需要对成百上千万的顶点与像素执行相同运算,而大模型训练与推理(矩阵乘法、卷积、注意力机制)同样可拆分为海量可并行的任务,恰好能将 GPU 数千个计算核心全部利用起来。厂商也为 AI 计算专门设计了硬件单元与软件栈,例如 NVIDIA 的 Tensor Core(张量核心)与 CUDA 编程框架,使 H100/A100 等 GPU 成为大模型训练与推理的主力算力。可以说,GPU 诞生于图形学,却在 AI 时代大放异彩——理解图形学中"并行处理每个像素/顶点"的思路,恰恰是理解 GPU 如何高效并行计算大模型的关键。
6 纹理映射 (Texture Mapping)
为什么需要纹理映射

不同位置应该有不同的颜色,但不能为每个三角形指定不同的材质属性。纹理映射通过将一张 2D 图像"贴"到 3D 表面上来解决这个问题。
基本原理
3D 表面上的每个点都对应 2D 纹理图像上的一个位置。

- 每个 3D 表面点都有一个在 2D 纹理(图像)中对应的位置
- 每个三角形从纹理图像中"复制"一块到表面上
解决如何将图片贴到 3D 物品身上:
- 艺术设计师创造。
- 任意一个模型,展开为平面,并且保持产生的三角形尽可能的少扭曲,将三角形映射到纹理上。

每个三角形顶点被分配一个纹理坐标 (texture coordinate) $(u, v)$,称为 UV。
UV 坐标
把任意一个三角形映射到纹理上的做法。

| 属性 | 说明 |
|---|---|
| 坐标范围 | $u \in [0, 1]$,$v \in [0, 1]$ |
| 可视化 | 可以将 $u$ 和 $v$ 作为颜色显示,用于调试纹理映射 |
| 纹理空间 | $(u, v)$ 定义了在纹理图像中的位置 |
纹理的重复使用
纹理可以应用到任何模型上面,下面图中只使用到一块纹理进行重复,但是纹理重复之后,中间的接缝很突兀。

同一张纹理可以在物体表面多次重复使用。

例如地面砖墙:一张砖块纹理可以在整个墙面上平铺,节省内存。像这种无缝贴合的纹理叫 Tiled,游戏开发场景会常遇到。
纹理查询与应用
应用纹理的基本流程:
for (each rasterized screen sample (x, y)): // 遍历每个屏幕像素
(u, v) = evaluate texture coordinate at (x, y) // 用重心坐标插值得到 UV
texcolor = texture.sample(u, v); // 采样纹理颜色
set sample's color to texcolor; // 通常作为漫反射系数 k_d
纹理颜色通常用作 Blinn-Phong 模型中的漫反射系数 $k_d$(漫反射反照率 albedo)。
7 重心坐标 (Barycentric Coordinates)
为什么需要插值
在三角形内部插值是为了:
| 目的 | 说明 |
|---|---|
| 为什么要插值 | 获取在顶点处指定值;在三角形内部获得平滑过渡变化的值 |
| 插值什么 | 纹理坐标、颜色、法线向量等 |
| 如何插值 | 使用重心坐标 |
定义

重心坐标是三角形的一种坐标系统。对于三角形 ABC 和三角形内任意一点 $(x, y)$:
$$ (x, y) = \alpha A + \beta B + \gamma C $$其中 $(\alpha, \beta, \gamma)$ 就是该点的重心坐标,满足:
$$ \alpha + \beta + \gamma = 1 $$判定在三角形内:当 $\alpha \geq 0$、$\beta \geq 0$、$\gamma \geq 0$ 三个坐标都非负时,点在三角形内部。
特殊点的重心坐标:

| 点 | 重心坐标 |
|---|---|
| 顶点 $A$ | $(1, 0, 0)$ |
| 顶点 $B$ | $(0, 1, 0)$ |
| 顶点 $C$ | $(0, 0, 1)$ |
| 重心 (centroid) | $(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$ |
几何意义——面积比
重心坐标可以用子三角形的面积比来理解:

其中 $A_A$ 是点 $(x,y)$ 与顶点 BC 构成的子三角形面积(对边于 $A$),以此类推。
计算公式
已知三角形顶点 $A(x_A, y_A)$、$B(x_B, y_B)$、$C(x_C, y_C)$ 和点 $(x, y)$,重心坐标为:
$$ \alpha = \frac{-(x-x_B)(y_C-y_B) + (y-y_B)(x_C-x_B)}{-(x_A-x_B)(y_C-y_B) + (y_A-y_B)(x_C-x_B)} $$$$ \beta = \frac{-(x-x_C)(y_A-y_C) + (y-y_C)(x_A-x_C)}{-(x_B-x_C)(y_A-y_C) + (y_B-y_C)(x_A-x_C)} $$$$ \gamma = 1 - \alpha - \beta $$插值应用

使用重心坐标对顶点值进行线性插值:
$$ V = \alpha V_A + \beta V_B + \gamma V_C $$$V_A$、$V_B$、$V_C$ 可以是顶点的位置、纹理坐标、颜色、法线、深度、材质属性等。
重要注意:重心坐标在投影变换下不是不变的!因此深度插值等需要在正确的空间中(三维空间)进行(通常在屏幕空间中插值深度时需要透视校正)。
8 纹理放大 (Texture Magnification)
问题:当纹理太小(分辨率不足),需要将其放大映射到更大的屏幕区域时,目标像素位置很少恰好落在原始像素的整数格点上。比如:
- 放大 2 倍后,目标像素
(1.5, 2.3)对应原始图像的哪个颜色? - 纹理映射时,UV 坐标
(0.37, 0.82)落在四个像素之间,取哪个?
最近邻插值(Nearest Neighbor)直接取最近的整数像素,简单粗暴,但会产生明显的锯齿和块状感。
双线性插值(Bilinear Interpolation)则利用周围 4 个像素的加权平均,让过渡更平滑。
双线性插值用 4 个像素(一阶多项式),双三次插值用 16 个像素(三阶多项式),拟合更复杂的局部曲面。
Lanczos 插值基于 Sinc 函数(理想低通滤波器的时域响应),是双三次插值的"升级版",追求理论上的最优重建。(课件中未提及,暂不过多讲述)
纹理上的一个像素称为 纹素 (texel)(纹理元素、纹素)。
以上方法对比
| 维度 | 最近邻 | 双线性 | 双三次 | Lanczos |
|---|---|---|---|---|
| 参与像素 | 1 | 4 | 16 | 36 (Lanczos-3) |
| 多项式阶数 | 0阶(常数) | 1阶(线性) | 3阶(三次) | sinc 函数 |
| 连续性 | 不连续 | C⁰ 连续 | C¹ 连续 | C¹ 连续 |
| 边缘效果 | 锯齿 | 模糊 | 锐利 | 最锐利 |
| 振铃效应 | 无 | 无 | 轻微 | 明显 |
| 计算量 | 1x | 3x | 10x | 20x+ |
| 典型用途 | 像素艺术 | 游戏纹理 | 照片放大 | 专业图像 |

双线性插值 (Bilinear Interpolation)
假设 4×4 纹理中要在红色点 $(x, y)$ 处采样纹理值,黑点是已知的纹理采样点位置。

步骤 1:找到最近的 4 个纹素 $u_{00}$、$u_{10}$、$u_{01}$、$u_{11}$

步骤 2:计算小数偏移量 $(s, t)$(采样点相对于左下角纹素的位置,$s, t \in [0, 1]$)

步骤 3:定义一维线性插值函数:

对 $x$ 在 $v_0$ 和 $v_1$ 之间的线性插值。当 $x$ 为 0 的时候,结果为 $v_0$;当 $x$ 为 1 的时候,结果为 $v_1$。
步骤 4:两次水平插值:

步骤 5:将上面获得两个水平插值,进行一次垂直插值(得到最终结果):

双线性插值总结:共 3 次线性插值(2 次水平 + 1 次垂直,水平垂直可以互换顺序),在合理开销下获得较好的平滑效果。
双三次插值 (Bicubic)
取周围 16 个纹素(4×4),使用三次多项式进行插值。效果比双线性更好,但计算量更大。
9 纹理缩小 (Texture Minification)
问题:当纹理太大(分辨率过高),多个纹素映射到同一个屏幕像素时,怎么办?
像素的重心 → 纹理坐标 → 求出纹理坐标的值。
问题分析

点采样会导致严重的走样 (aliasing) 问题:远处地面的纹理出现摩尔纹 (Moire) 和锯齿 (Jaggies) 等伪影。

原因:一个屏幕像素在纹理空间中覆盖了多个纹素(像素足迹 footprint 过大),但点采样只取其中一个纹素的值,丢失了大量信息。
超采样的局限
超采样(如 512x SSAA)可以解决走样问题,但:
- 计算代价非常高
- 当纹理高度缩小时,一个像素足迹内可能有大量纹素,信号频率过高,需要更高的采样频率
换一个思路:既然问题在于一个像素覆盖了多个纹素,那我们不需要采样——只需要获取一个范围内的平均值即可。
这就是范围查询 (Range Query) 问题:
- 点查询:在 $(u, v)$ 处获取纹理值
- 范围查询:在 $(u, v)$ 附近的区域内获取平均值
Mipmap
Mipmap 允许进行快速、近似、正方形的范围查询。
“Mip” 来自拉丁语 “multum in parvo”,意为"小空间中包含大量信息"。
构建方法
从原始纹理(Level 0)开始,每级将边长减半,取 4 个相邻纹素的平均值:

| 层级 | 分辨率 | 说明 |
|---|---|---|
| Level 0 | $128 \times 128$ | 原始纹理 |
| Level 1 | $64 \times 64$ | 每 2x2 求平均 |
| Level 2 | $32 \times 32$ | 每 4x4 求平均 |
| Level 3 | $16 \times 16$ | 每 8x8 求平均 |
| Level 4 | $8 \times 8$ | 每 16x16 求平均 |
| Level 5 | $4 \times 4$ | 每 32x32 求平均 |
| Level 6 | $2 \times 2$ | 每 64x64 求平均 |
| Level 7 | $1 \times 1$ | 全局平均 |

存储开销:Mipmap 的额外存储开销仅为原始纹理的 $\frac{1}{3}$。
推导:Level 0 大小为 $N \times N$,各级大小为 $\frac{N}{2} \times \frac{N}{2}$, $\frac{N}{4} \times \frac{N}{4}$, …, 总和为 $N^2 \cdot (\frac{1}{4} + \frac{1}{16} + \frac{1}{64} + \cdots) = N^2 \cdot \frac{1/4}{1 - 1/4} = N^2 \cdot \frac{1}{3}$(课件外补充)
计算 Mipmap 层级 D
不同位置的像素在纹理空间中的覆盖范围大小不同,需要选择合适的 Mipmap 层级。近处的像素映射到纹理空间后足迹很小,远处的像素足迹则大得多——即不同像素有不同大小的足迹,因此需要逐像素地估计其查询范围。
方法:利用相邻屏幕像素的纹理坐标变化率来估计纹理足迹大小。

- $L$ 表示一个屏幕像素在纹理空间中覆盖的近似边长
- $D$ 是对应的 Mipmap 层级
- $L = 1$ 时 $D = 0$(使用原始纹理),$L = 4$ 时 $D = 2$(使用 Level 2)
获取正方形四个顶点映射过去,计算出边长也是可以的。
三线性插值 (Trilinear Interpolation)

在上面场景图片中,距离我们很近,存在很多细节,需要在很低的层去查询计算;距离我们很远,不需要显示更多细节,层级越往上。这是一个渐变的过程,需要一个平滑过渡,就需要用到插值计算。
$D$ 通常不是整数。为了得到平滑的结果,在两个相邻的整数层级之间再做一次线性插值:
- 在 Level $D$(向下取整)上做双线性插值,得到结果 $A$
- 在 Level $D+1$(向上取整)上做双线性插值,得到结果 $B$
- 在 $A$ 和 $B$ 之间根据 $D$ 的小数部分做线性插值

比如想要查询 1.8 层,将第一层和第二层各自内部进行双线性插值,得到结果后进行层与层之间的一次线性插值。
三线性插值 = 2 次双线性插值 + 1 次线性插值,结果平滑且开销小。
Mipmap 的局限
Mipmap 只能查询正方形区域的平均值,但实际的像素足迹可能是细长的矩形,导致:

过度模糊 (Overblur):Mipmap 将非正方形足迹近似为正方形,使用了过大的区域求平均,导致远处纹理过度模糊。可以使用各向异性过滤部分解决。
各向异性过滤 (Anisotropic Filtering)
各向异性过滤可以处理轴对齐的矩形区域查询,比 Mipmap 效果更好。

Mipmap 本质上是将图长宽各缩小一半,对应的图上对角线的图形的变化。不同的长宽比的计算,可以看到图中每一列竖直方向做了压缩,每一行做了水平方向的压缩,比 Mipmap 多了不均匀的水平竖直方向的压缩。第一列第三行的图形恢复到原来的图片对应的是一个细长的矩形。通过这样的查询就可以计算压扁的矩形区域。
各向异性过滤多少 X,相当于多少层级,2x 相当于图上左上角两行两列区域进行了一次压缩。4x 相当于三行三列区域,再次进行一次压缩。
- 可以查询轴对齐矩形区域的平均值
- 解决了 Mipmap 的过度模糊问题
- 存储开销:各向异性过滤需要存储多个不同长宽比的层级,开销为原始纹理的 3 倍(课件外补充)
- 局限:对角斜方向的足迹仍然是问题
各向异性指在不同方向上表现各不相同,水平和垂直方向表现都相同为各向同性。
不规则像素足迹
屏幕像素在纹理空间中的足迹可能是不规则的形状(特别是斜视角下):

但是各向异性过滤对于对角斜方向区域,使用矩形区域是无法完全覆盖的,所以就有 EWA 过滤出现。
EWA 过滤 (Elliptical Weighted Average)

- 使用多次查找和加权平均
- 用椭圆来近似像素足迹
- 可以处理不规则的足迹形状
- Mipmap 层级仍然有帮助(加速查询)
- 代价最高,质量最好
10 纹理应用 (Texture Applications)
纹理不仅可以存储颜色,还可以存储各种数据来影响渲染效果。在现代 GPU 中,纹理 = 存储 + 范围查询(过滤),是一种将数据带入片段计算的通用方法。

环境贴图 (Environment Mapping)
用纹理来存储环境光照信息,用于模拟反射效果。

球面环境贴图 (Spherical Environment Map)

- 将环境映射到球面上
- 问题:球面两极处会产生严重的扭曲变形 (distortion)
立方体环境贴图 (Cube Map)

- 将环境映射到立方体的 6 个面上
- 优点:扭曲更少,各方向均匀
- 使用方法:一个向量沿其方向映射到立方体上的点,根据反射方向确定采样的面和坐标(需要 dir → face 计算)

凹凸贴图 / 法线贴图 (Bump / Normal Mapping)

纹理不只是定义颜色,它可以定义任何属性。可以定义模型表面沿着法线方向向上向下各走多少,上图中可以认定为黑色为 0,白色为 1。不需要重新定义模型细节,可以根据相对应的纹理进行处理。
核心思想
通过扰动法线向量来模拟表面细节,而不增加更多的三角形、不改变实际几何形状。

原理
- 纹理存储的是高度变化(凹凸贴图)或法线方向(法线贴图),即"存储高度/法线"来伪装出精细几何
- 每个纹素定义了一个"高度偏移",在着色时,使用扰动后的法线代替原始法线进行光照计算(仅用于着色计算)
- 不改变实际的几何形状(顶点位置不变)
- 效果:在视觉上产生凹凸感,但物体边缘仍然是光滑的(因为是"假的")
法线计算(2D 情况)
假设原始法线为 $\vec{n} = (0, 1)$,凹凸贴图定义了高度变化 $h(p)$,则扰动后的法线为:

其中 $dp = c \cdot [h(p+1) - h(p)]$,$c$ 是控制凹凸程度的常数。
获得蓝色切线,求垂直于切线的法线:蓝色水平移动距离为 1,垂直距离为 $dp$,对应的切向量为 $(1, dp)$,法线为 $(-dp, 1)$。
法线计算(3D 情形)
3D 情形下(原始法线为 $\vec{n} = (0, 0, 1)$),扰动后的法线为:
$$ \vec{n}' = \text{normalize}\left(-\frac{dp}{du}, \; -\frac{dp}{dv}, \; 1\right) $$其中 $\frac{dp}{du} = c_1 \cdot [h(u+1) - h(u)]$、$\frac{dp}{dv} = c_2 \cdot [h(v+1) - h(v)]$。注意该公式定义在局部坐标系下。
凹凸贴图 vs 法线贴图
| 对比项 | 凹凸贴图 (Bump Map) | 法线贴图 (Normal Map) |
|---|---|---|
| 存储内容 | 高度值(灰度图) | 法线向量(RGB 通道) |
| 计算方式 | 从高度计算梯度,再求法线 | 直接读取法线 |
| 精度 | 较低 | 较高 |
位移贴图 (Displacement Mapping)
与凹凸贴图的区别
| 对比项 | 凹凸贴图 (Bump Mapping) | 位移贴图 (Displacement Mapping) |
|---|---|---|
| 几何改变 | 不改变 | 真实改变顶点位置 |
| 边缘效果 | 边缘仍然光滑 | 边缘也呈现凹凸 |
| 阴影 | 无法产生自阴影 | 可以产生自阴影 |
| 要求 | 任意模型 | 模型需要足够细分 |
| 性能 | 快 | 较慢 |

位移贴图使用与凹凸贴图相同的纹理,但真正移动了顶点的位置,因此效果更真实,但要求模型的三角形足够细密,否则无法表现高频细节。
阴影贴图 (Shadow Mapping)
该主题与几何(第 12 讲 Geometry 3 后半)内容对应,可互相参照。
核心思想
如果一个点不在光源的"视线"中,那么它在阴影中。这是一种基于图像空间的两趟 (two-pass) 算法,计算阴影时不需要场景的几何信息,但必须处理走样伪影。
算法步骤
第一步:从光源视角渲染 (Pass 1: Render from Light)
- 将相机放在光源位置,渲染场景
- 只记录深度信息(不记录颜色),得到阴影贴图 (Shadow Map)
- 阴影贴图存储了从光源看去的最近深度(深度图像)

第二步:从相机视角渲染 (Pass 2: Render from Eye)
- 从实际相机视角渲染场景,得到标准图像(含深度)
- 对每个可见点,将其投影回光源空间,计算其到光源的深度 $d$
- 查询阴影贴图中对应位置的深度 $d_{\text{shadow}}$
- 比较:如果两者深度不一致($d > d_{\text{shadow}}$),该点在阴影中;否则不在阴影中

阴影判断
| 条件 | 结论 |
|---|---|
| $d \approx d_{\text{shadow}}$ | 不在阴影中(光源能看到该点) |
| $d > d_{\text{shadow}}$ | 在阴影中(被其他物体遮挡) |
阴影贴图的问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 阴影锯齿 | 阴影贴图分辨率不足(图像空间方法通病) | 提高阴影贴图分辨率、级联阴影(课件外补充) |
| 自阴影 (Shadow Acne) | 深度比较的浮点数值误差 | 添加偏移量 (bias) |
| 仅产生硬阴影 | 点光源假设 | 使用 PCF、PCSS 等软阴影算法(课件外补充) |
特点
- 优点:不需要场景几何信息,只依赖图像空间
- 缺点:会产生走样(阴影贴图的分辨率限制),只能产生硬阴影
3D 纹理与过程化纹理
3D 纹理 (3D Texture / Solid Texture)
- 纹理定义在整个 3D 空间中
- 物体像从实心材质中"雕刻"出来
- 适合木纹、大理石等材质
过程化纹理 (Procedural Texture)
- 不存储纹理图像,而是通过函数计算纹理值(如 Perlin 噪声)
- 节省存储空间
- 可以实现无限分辨率
- 常用噪声函数(如 Perlin Noise)
预计算着色与体积渲染
- 预计算着色 / 环境光遮蔽 (Precomputed Shading / Ambient Occlusion):纹理还可以存储预先计算好的着色结果,例如用 AO 贴图记录各处被环境遮蔽的程度,叠加后能让简单着色的场景呈现丰富的明暗细节

- 3D 纹理与体积渲染 (3D Textures and Volume Rendering):将密度等属性定义在整个 3D 空间中(如医学 CT 数据),沿视线对体积数据采样并积分,即可渲染出人体组织、烟雾等效果(Marc Levoy 的经典工作)

纹理的其他应用
| 应用 | 说明 |
|---|---|
| 环境光照贴图 | 存储预计算的全局光照信息 |
| 细节贴图 | 在基础纹理上叠加细节纹理 |
| 查找表 (LUT) | 存储预计算的光照查找表 |
11 复盘自测
Z-Buffer 算法相比画家算法的优势是什么?为什么它不需要排序?
Z-Buffer 为每个像素独立维护最小深度,逐采样点做深度测试与更新,三角形之间互不依赖、与绘制顺序无关,因此无需全局深度排序(画家算法必须排序且存在循环遮挡无法排序的问题),时间复杂度降为 $O(n)$。
为什么说 Blinn-Phong 着色"局部",shading 为什么产生不了阴影?
着色只在着色点上依据该点的 $\vec{v}$、$\vec{n}$、$\vec{l}$ 和材质参数计算,不知道场景中其他物体的存在,无法判断"光源是否被遮挡",所以环境光项只是近似填充暗部。真正的阴影需要额外的可见性计算(如阴影贴图)。
半程向量 $\vec{h}$ 的作用是什么?指数 $p$ 如何影响高光?
$\vec{h} = \frac{\vec{v} + \vec{l}}{\|\vec{v} + \vec{l}\|}$,用 $\vec{n} \cdot \vec{h}$ 衡量观察方向与镜面反射方向的接近程度,比直接求反射向量更省计算。增大 $p$ 会收窄反射波瓣,使高光更集中、更接近真实镜面。
Flat / Gouraud / Phong 三种着色频率各自在什么位置计算颜色?Gouraud 的缺陷是什么?
分别在面、顶点、像素上着色。Gouraud 在顶点算颜色再插值,若高光恰好落在三角形内部而不在顶点上,高光可能被"丢失"。
双线性插值的步骤是怎样的?为什么需要三线性插值?
找最近 4 个纹素 → 计算偏移 $(s,t)$ → 两次水平 lerp + 一次垂直 lerp。Mipmap 层级 $D = \log_2 L$ 通常不是整数,三线性插值在相邻两层各做一次双线性插值后再按小数部分线性插值,消除层级切换的突变。
Mipmap 为什么会带来过度模糊?各向异性过滤和 EWA 分别如何解决?
Mipmap 只能做正方形范围查询,细长(压扁)的像素足迹被近似成更大的正方形区域求平均。各向异性过滤支持轴对齐矩形查询;EWA 用椭圆近似任意形状的足迹,多次查找加权平均,质量最好但代价最高。
凹凸贴图与位移贴图的本质区别是什么?
凹凸贴图只扰动法线、不改变几何(边缘依然光滑、无法自阴影);位移贴图真正移动顶点位置,轮廓与阴影都正确,但要求模型足够细分、开销更大。
阴影贴图两趟渲染分别做了什么?shadow acne 如何缓解?
Pass 1 从光源渲染并只保存深度(阴影贴图);Pass 2 从相机渲染,把每个可见点投影回光源空间比较深度,$d > d_{\text{shadow}}$ 即在阴影中。浮点误差导致的自阴影条纹(shadow acne)通过添加深度偏移量 (bias) 缓解。
本文是 GAMES101 - 现代计算机图形学学习系列的第 5 篇笔记。