第3章:光栅化
章节概述
光栅化是将连续的几何图元转换为离散像素的过程,是现代实时渲染的核心技术。本章深入探讨光栅化的数学原理、算法实现和硬件优化策略。我们将从三角形的离散化开始,逐步深入到深度测试、抗锯齿技术,最后探讨现代GPU的光栅化管线设计。通过本章学习,读者将掌握从几何到像素的完整转换过程,理解现代图形硬件的设计哲学,并能够分析和优化光栅化性能。
3.1 三角形的离散化
3.1.1 为什么选择三角形
三角形是计算机图形学中最基本的图元,其优势包括:
- 平面性保证:三个不共线的点唯一确定一个平面,避免了非平面多边形的歧义性
- 凸性质:三角形必定是凸多边形,简化了内外判断和光栅化算法
- 插值简单:重心坐标提供了自然的线性插值框架,保证了C⁰连续性
- 硬件友好:固定的顶点数量便于并行处理和硬件优化
- 数学优雅:仿射不变性和投影不变性使得变换计算简洁高效
三角形的数学表示形式多样,可以用参数方程、隐式方程或重心坐标表示: $$\mathbf{p}(u,v) = (1-u-v)\mathbf{v}_0 + u\mathbf{v}_1 + v\mathbf{v}_2, \quad u,v \geq 0, u+v \leq 1$$
三角形的几何性质
三角形具有许多重要的几何性质,这些性质是光栅化算法的理论基础:
-
单纯形性质:三角形是二维空间中的单纯形(simplex),这意味着它是能够张成二维空间的最小凸集。
-
重心坐标的几何意义: - 重心坐标 $(\alpha, \beta, \gamma)$ 表示点到对边的有向距离比 - 物理意义:如果在三个顶点放置质量为 $(\alpha, \beta, \gamma)$ 的质点,则质心位于点 $\mathbf{p}$ - 面积比:$\alpha = \frac{\text{Area}(\mathbf{p}\mathbf{v}_1\mathbf{v}_2)}{\text{Area}(\mathbf{v}_0\mathbf{v}_1\mathbf{v}_2)}$
-
仿射不变性:重心坐标在仿射变换下保持不变 $$T(\alpha\mathbf{v}_0 + \beta\mathbf{v}_1 + \gamma\mathbf{v}_2) = \alpha T(\mathbf{v}_0) + \beta T(\mathbf{v}_1) + \gamma T(\mathbf{v}_2)$$ 这个性质保证了我们可以在任意坐标系中进行插值计算。
-
欧拉公式的应用:对于三角形网格,顶点数V、边数E、面数F满足: $$V - E + F = 2 - 2g$$ 其中g是亏格(genus)。对于简单连通网格,$g=0$,因此 $F \approx 2V$。
三角形与其他图元的比较
四边形:
- 优点:更少的图元数量,某些情况下更自然(如地形网格)
- 缺点:可能非平面,需要分割成三角形,插值更复杂
多边形:
- 优点:更灵活的建模
- 缺点:需要三角化,凹多边形处理复杂,硬件支持有限
曲面片(如Bézier):
- 优点:更高的几何精度,更少的存储
- 缺点:计算复杂,需要细分(tessellation)成三角形
三角形的拓扑表示
在实际应用中,三角形通常组织成网格(mesh)结构:
-
独立三角形:每个三角形存储三个顶点 - 优点:简单,无依赖 - 缺点:顶点重复,内存浪费
-
索引三角形列表:顶点数组 + 索引数组 - 优点:共享顶点,节省内存 - 缺点:额外的间接访问
-
三角形条带(Triangle Strip): - 表示:$v_0, v_1, v_2, v_3, ...$ 形成三角形 $(v_0,v_1,v_2), (v_1,v_2,v_3), ...$ - 优点:最少的索引数据 - 缺点:需要退化三角形连接不同条带
-
三角形扇(Triangle Fan): - 表示:中心点 $v_0$ 与边界点 $v_1, v_2, ...$ 形成三角形 - 优点:适合圆形或扇形区域 - 缺点:应用场景有限
3.1.2 三角形的表示
三角形可以通过三个顶点 $\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2$ 表示,每个顶点包含:
- 位置:$\mathbf{p} = (x, y, z, w)^T$(齐次坐标)
- 属性:颜色、法线、纹理坐标等
在屏幕空间中,经过投影变换后的顶点坐标为: $$\mathbf{p}_{\text{screen}} = \begin{pmatrix} x_s \ y_s \ z_s \end{pmatrix} = \begin{pmatrix} \frac{x_{ndc} + 1}{2} \cdot \text{width} \ \frac{y_{ndc} + 1}{2} \cdot \text{height} \ z_{ndc} \end{pmatrix}$$ 其中NDC(Normalized Device Coordinates)坐标范围为$[-1, 1]^3$。需要注意的是,$z_s$通常会经过非线性变换以优化深度精度分布。
三角形的隐式表示使用平面方程: $$\mathbf{n} \cdot (\mathbf{p} - \mathbf{v}_0) = 0$$ 其中法向量$\mathbf{n} = (\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0)$决定了三角形的朝向。
坐标系变换链
三角形顶点经历的完整变换链:
-
模型空间(Model Space) → 世界空间(World Space) $$\mathbf{p}_{world} = \mathbf{M}_{model} \mathbf{p}_{model}$$
-
世界空间 → 观察空间(View Space) $$\mathbf{p}_{view} = \mathbf{V} \mathbf{p}_{world}$$
-
观察空间 → 裁剪空间(Clip Space) $$\mathbf{p}_{clip} = \mathbf{P} \mathbf{p}_{view}$$ 此时坐标为齐次坐标 $(x_c, y_c, z_c, w_c)$
-
透视除法 → NDC空间 $$\mathbf{p}_{ndc} = \begin{pmatrix} x_c/w_c \ y_c/w_c \ z_c/w_c \end{pmatrix}$$
-
视口变换 → 屏幕空间 $$x_s = (x_{ndc} + 1) \cdot \frac{width}{2} + x_{viewport}$$ $$y_s = (y_{ndc} + 1) \cdot \frac{height}{2} + y_{viewport}$$
三角形的数据结构
高效的三角形表示需要考虑内存布局和缓存友好性:
struct Vertex {
float3 position; // 12 bytes
float3 normal; // 12 bytes
float2 texcoord; // 8 bytes
float4 tangent; // 16 bytes (含手性)
// 总计 48 bytes,对齐到 64 bytes
};
struct Triangle {
uint3 indices; // 顶点索引
uint materialID; // 材质索引
float area; // 预计算的面积
float3 normal; // 预计算的法线
};
退化三角形的处理
退化三角形是指面积为零或接近零的三角形,可能由以下原因产生:
- 共线顶点:三个顶点在一条直线上
- 重复顶点:两个或更多顶点位置相同
- 数值精度:浮点误差导致的近似共线
检测方法: $$\text{area} = \frac{1}{2}||(\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0)|| < \epsilon$$ 处理策略:
- 预处理剔除:在网格处理阶段删除
- 运行时跳过:光栅化时检测并跳过
- 顶点合并:将距离过近的顶点合并
三角形的方向与缠绕顺序
缠绕顺序(winding order)决定了三角形的正面:
-
逆时针(CCW):OpenGL默认 - 从观察者角度看,顶点按逆时针排列为正面 - 法线指向观察者
-
顺时针(CW):DirectX默认 - 从观察者角度看,顶点按顺时针排列为正面
叉积与缠绕顺序的关系: $$\mathbf{n} = (\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0)$$
- CCW:法线指向外(正面)
- CW:法线指向内(背面)
三角形的层次表示
对于复杂模型,三角形常组织成层次结构:
-
BVH(Bounding Volume Hierarchy): - 每个节点包含子三角形的包围盒 - 用于加速光线追踪和碰撞检测
-
LOD(Level of Detail): - 不同距离使用不同精度的三角形网格 - 减少远处物体的渲染开销
-
空间划分: - Octree、KD-Tree等结构 - 加速空间查询和剔除
3.1.3 点在三角形内的判断
叉积法(Edge Function)
对于屏幕空间中的点 $\mathbf{p} = (x, y)$,判断其是否在三角形内部最常用的方法是边函数(edge function): $$E_{01}(\mathbf{p}) = (x - x_0)(y_1 - y_0) - (y - y_0)(x_1 - x_0)$$ 边函数的几何意义是有向面积的两倍,符号表示点在边的哪一侧。点 $\mathbf{p}$ 在三角形内部当且仅当:
- $E_{01}(\mathbf{p}) \geq 0$
- $E_{12}(\mathbf{p}) \geq 0$
- $E_{20}(\mathbf{p}) \geq 0$
(假设三角形顶点按逆时针顺序排列)
边函数的优势在于:
- 增量计算:相邻像素的边函数值可通过简单加法获得
- 并行友好:不同像素的计算完全独立
- 精确性:使用整数运算可避免浮点误差
边函数的数学性质
边函数具有以下重要性质:
-
线性性:边函数是位置的线性函数 $$E(x+\Delta x, y+\Delta y) = E(x,y) + A\Delta x + B\Delta y$$ 其中 $A = y_1 - y_0$, $B = x_0 - x_1$
-
符号几何意义: - $E > 0$:点在边的左侧(逆时针方向) - $E < 0$:点在边的右侧 - $E = 0$:点在边上
-
与有向面积的关系: $$E_{01}(\mathbf{p}) = 2 \cdot \text{SignedArea}(\mathbf{v}_0, \mathbf{v}_1, \mathbf{p})$$
-
缩放不变性: $$E_{01}(s\mathbf{p}) = s^2 E_{01}(\mathbf{p})$$
边函数的向量形式
使用向量表示可以更清晰地理解边函数: $$E_{01}(\mathbf{p}) = \det\begin{pmatrix} \mathbf{p} - \mathbf{v}_0 & \mathbf{v}_1 - \mathbf{v}_0 \end{pmatrix} = (\mathbf{p} - \mathbf{v}_0) \times (\mathbf{v}_1 - \mathbf{v}_0)$$ 这表明边函数实际上是两个向量的叉积的z分量。
精确边界处理(Edge Rules)
当像素中心恰好落在三角形边界上时,需要明确的规则避免:
- 像素被多个三角形同时覆盖
- 像素未被任何三角形覆盖
Top-Left规则:
bool is_top_edge(v0, v1) {
return v0.y == v1.y && v0.x < v1.x; // 水平且向右
}
bool is_left_edge(v0, v1) {
return v0.y < v1.y; // 向上
}
// 边界测试
if (E == 0) {
accept = is_top_edge(v0, v1) || is_left_edge(v0, v1);
}
定点数实现
为了保证精确性,实际实现中使用定点数:
// 转换为定点数(16.8格式)
int32_t to_fixed(float f) {
return (int32_t)(f * 256.0f + 0.5f);
}
// 定点数边函数
int32_t edge_fixed(int32_t x, int32_t y,
int32_t x0, int32_t y0,
int32_t x1, int32_t y1) {
return (x - x0) * (y1 - y0) - (y - y0) * (x1 - x0);
}
优势:
- 精确:无浮点舍入误差
- 快速:整数运算
- 一致:相邻三角形共享边产生相同结果
重心坐标(Barycentric Coordinates)
重心坐标 $(\alpha, \beta, \gamma)$ 定义了点相对于三角形的位置: $$\mathbf{p} = \alpha \mathbf{v}_0 + \beta \mathbf{v}_1 + \gamma \mathbf{v}_2$$ 其中 $\alpha + \beta + \gamma = 1$。计算公式为: $$\alpha = \frac{E_{12}(\mathbf{p})}{E_{12}(\mathbf{v}_0)}$$ $$\beta = \frac{E_{20}(\mathbf{p})}{E_{20}(\mathbf{v}_1)}$$ $$\gamma = \frac{E_{01}(\mathbf{p})}{E_{01}(\mathbf{v}_2)}$$ 点在三角形内部当且仅当 $\alpha, \beta, \gamma \geq 0$。
重心坐标的性质:
- 仿射不变性:在仿射变换下保持不变
- 插值性质:任何顶点属性都可以通过重心坐标线性插值
- 归一化:$\alpha + \beta + \gamma = 1$ 确保了插值的正确性
重心坐标的计算方法
-
面积比方法: $$\alpha = \frac{\text{Area}(\triangle \mathbf{p}\mathbf{v}_1\mathbf{v}_2)}{\text{Area}(\triangle \mathbf{v}_0\mathbf{v}_1\mathbf{v}_2)}$$ 使用叉积计算面积: $$\text{Area} = \frac{1}{2}|(\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0)|$$
-
线性系统方法: 解线性方程组: $$\begin{pmatrix} x \ y \ 1 \end{pmatrix} = \begin{pmatrix} x_0 & x_1 & x_2 \ y_0 & y_1 & y_2 \ 1 & 1 & 1 \end{pmatrix} \begin{pmatrix} \alpha \ \beta \ \gamma \end{pmatrix}$$ 使用克拉默法则: $$\alpha = \frac{\det\begin{pmatrix} x & x_1 & x_2 \ y & y_1 & y_2 \ 1 & 1 & 1 \end{pmatrix}}{\det\begin{pmatrix} x_0 & x_1 & x_2 \ y_0 & y_1 & y_2 \ 1 & 1 & 1 \end{pmatrix}}$$
-
边函数方法(最常用): $$\alpha = \frac{E_{12}(\mathbf{p})}{2A}, \quad \beta = \frac{E_{20}(\mathbf{p})}{2A}, \quad \gamma = \frac{E_{01}(\mathbf{p})}{2A}$$ 其中 $A$ 是三角形面积。
重心坐标的几何解释
-
质心解释:如果在三个顶点分别放置质量为 $(\alpha, \beta, \gamma)$ 的质点,系统的质心就在点 $\mathbf{p}$。
-
垂直距离解释:重心坐标与点到对边的垂直距离成比例。
-
参数化解释:$(\alpha, \beta, \gamma)$ 提供了三角形内部的一种自然参数化。
重心坐标的特殊点
- 重心:$(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$ - 三条中线的交点
- 外心:到三个顶点距离相等的点
- 内心:$(\frac{a}{a+b+c}, \frac{b}{a+b+c}, \frac{c}{a+b+c})$ - 其中$a,b,c$是边长
- 垂心:三条高线的交点
重心坐标的数值稳定性
当三角形接近退化时,重心坐标计算可能不稳定。稳健的实现需要:
-
条件数检查: $$\kappa = \frac{\max(|\mathbf{v}_0|, |\mathbf{v}_1|, |\mathbf{v}_2|) \cdot \max(|\mathbf{v}_1-\mathbf{v}_0|, |\mathbf{v}_2-\mathbf{v}_1|, |\mathbf{v}_0-\mathbf{v}_2|)}{2A}$$ 当 $\kappa$ 很大时,计算不稳定。
-
退化处理:
if (area < epsilon) { // 退化为线段或点 // 使用投影到最长边的方法 } -
双精度计算:对于大型场景,使用双精度避免精度损失。
其他判断方法
同侧法:检查点是否与对面顶点在每条边的同一侧。
对于每条边 $(\mathbf{v}_i, \mathbf{v}_j)$,检查: $$\text{sign}(E_{ij}(\mathbf{p})) = \text{sign}(E_{ij}(\mathbf{v}_k))$$ 其中 $\mathbf{v}_k$ 是第三个顶点。
面积法:比较子三角形面积之和与原三角形面积: $$\text{Area}(PAB) + \text{Area}(PBC) + \text{Area}(PCA) = \text{Area}(ABC)$$ 由于浮点误差,实际判断时需要容差: $$|\sum \text{Area}_i - \text{Area}_{total}| < \epsilon$$ 向量法:使用向量叉积的方向一致性:
vec2 v0v1 = v1 - v0;
vec2 v1v2 = v2 - v1;
vec2 v2v0 = v0 - v2;
vec2 v0p = p - v0;
vec2 v1p = p - v1;
vec2 v2p = p - v2;
float cross0 = v0v1.x * v0p.y - v0v1.y * v0p.x;
float cross1 = v1v2.x * v1p.y - v1v2.y * v1p.x;
float cross2 = v2v0.x * v2p.y - v2v0.y * v2p.x;
// 所有叉积同号则在内部
return (cross0 >= 0 && cross1 >= 0 && cross2 >= 0) ||
(cross0 <= 0 && cross1 <= 0 && cross2 <= 0);
角度法:计算点对三角形三条边的张角之和:
- 内部点:张角之和 = $2\pi$
- 外部点:张角之和 < $2\pi$
- 边界点:张角之和 = $\pi$
各种方法的比较
| 方法 | 计算复杂度 | 精度 | 硬件适合性 | 备注 |
| 方法 | 计算复杂度 | 精度 | 硬件适合性 | 备注 |
|---|---|---|---|---|
| 边函数 | O(1) | 高 | 极好 | GPU标准方法 |
| 重心坐标 | O(1) | 高 | 好 | 同时用于插值 |
| 面积法 | O(1) | 中 | 中 | 浮点误差累积 |
| 角度法 | O(1) | 低 | 差 | 需要反三角函数 |
3.1.4 光栅化算法
包围盒算法(Bounding Box)
最简单的光栅化算法:
- 计算三角形的轴对齐包围盒(AABB)
- 遍历包围盒内的所有像素
- 对每个像素进行内外测试
xmin = floor(min(x0, x1, x2))
xmax = ceil(max(x0, x1, x2))
ymin = floor(min(y0, y1, y2))
ymax = ceil(max(y0, y1, y2))
for y in [ymin, ymax]:
for x in [xmin, xmax]:
if (x, y) inside triangle:
shade_pixel(x, y)
效率分析:
- 时间复杂度:$O((x_{max}-x_{min})(y_{max}-y_{min}))$
- 对于细长三角形效率低下(大量像素在三角形外)
- 适合小三角形或接近正方形的三角形
包围盒优化
-
像素中心对齐:
// 考虑像素中心偏移 xmin = floor(min(x0, x1, x2) - 0.5) xmax = ceil(max(x0, x1, x2) - 0.5) -
保守包围盒: 为了处理浮点误差,稍微扩大包围盒:
const float epsilon = 0.001f; xmin = floor(min(x0, x1, x2) - epsilon); xmax = ceil(max(x0, x1, x2) + epsilon); -
空包围盒检查:
if (xmin > xmax || ymin > ymax) { return; // 退化三角形,跳过 }
填充率分析
定义填充率(fill rate)为: $$\text{Fill Rate} = \frac{\text{Triangle Area}}{\text{Bounding Box Area}}$$ 不同形状三角形的填充率:
- 正三角形:约50%
- 直角三角形:50%
- 细长三角形:可能低至1%
低填充率意味着大量无效测试,需要更精细的算法。
增量算法(Incremental Algorithm)
边函数具有线性性质: $$E(x+1, y) = E(x, y) + (y_1 - y_0)$$ $$E(x, y+1) = E(x, y) - (x_1 - x_0)$$ 利用这一性质可以避免重复计算:
- 计算起始点的边函数值
- 使用增量更新相邻像素的值
- 减少乘法运算,提高效率
增量算法的优化版本:
// 预计算增量
A01 = y0 - y1, B01 = x1 - x0
A12 = y1 - y2, B12 = x2 - x1
A20 = y2 - y0, B20 = x0 - x2
// 起始点边函数值
w0_row = E01(xmin, ymin)
w1_row = E12(xmin, ymin)
w2_row = E20(xmin, ymin)
for y in [ymin, ymax]:
w0 = w0_row
w1 = w1_row
w2 = w2_row
for x in [xmin, xmax]:
if (w0 >= 0 && w1 >= 0 && w2 >= 0):
shade_pixel(x, y)
w0 += A01
w1 += A12
w2 += A20
w0_row += B01
w1_row += B12
w2_row += B20
增量算法的性能分析
相比朴素算法:
- 乘法操作:从每像素 6 次减少到 0 次
- 加法操作:每像素 9 次(三个边函数,每个 3 次)
- 内存访问:顺序访问,缓存友好
并行增量算法
现代GPU使用2×2像素块(quad)为单位:
// 计算quad左上角的边函数值
float3 w_base = compute_edge_functions(quad_x, quad_y);
// 并行计算四个像素
float3 w00 = w_base;
float3 w10 = w_base + float3(A01, A12, A20);
float3 w01 = w_base + float3(B01, B12, B20);
float3 w11 = w_base + float3(A01+B01, A12+B12, A20+B20);
// SIMD处理
bool4 inside = (w00 >= 0) & (w10 >= 0) & (w01 >= 0) & (w11 >= 0);
定点数增量算法
为了保证精度,使用定点数运算:
// 16.16定点数格式
typedef int32_t fixed16;
// 转换为定点数
fixed16 to_fixed16(float f) {
return (fixed16)(f * 65536.0f + 0.5f);
}
// 定点数增量
fixed16 w0 = to_fixed16(E01(xmin, ymin));
fixed16 A01_fixed = to_fixed16(y0 - y1);
fixed16 B01_fixed = to_fixed16(x1 - x0);
// 内循环
for (int x = xmin; x <= xmax; x++) {
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
shade_pixel(x, y);
}
w0 += A01_fixed;
w1 += A12_fixed;
w2 += A20_fixed;
}
分块光栅化(Tiled Rasterization)
现代GPU采用分块策略:
- 将屏幕划分为固定大小的块(如8×8或16×16)
- 首先进行块级别的粗测试
- 只对可能相交的块进行像素级测试
层次化测试策略:
- 粗粒度测试:使用块的四个角点快速排除完全在外的块
- 细粒度测试:对通过粗测试的块进行逐像素测试
- 早期退出:一旦确定块完全在内,跳过像素级测试
扫描线算法(Scanline Algorithm)
经典的光栅化方法,特别适合软件实现:
- 找出三角形与每条扫描线的交点
- 对每条扫描线,填充交点之间的像素
- 使用活动边表(AET)维护当前相交的边
扫描线转换的优势:
- 内存访问模式友好(行优先)
- 易于实现反走样(通过子像素精度)
- 适合并行化(不同扫描线独立)
Pineda算法
一种高效的并行光栅化算法:
- 同时计算三个边函数
- 使用边函数的符号判断像素位置
- 支持任意遍历顺序(如Hilbert曲线)
3.1.5 亚像素精度与定点数
为避免浮点误差导致的裂缝,现代光栅化器使用定点数表示:
- 典型精度:16.8或24.8格式(整数部分.小数部分位数)
- 子像素精度:1/256像素(8位小数)或1/65536像素(16位小数)
- 保证相邻三角形的边界精确对齐
定点数转换: $$x_{fixed} = \lfloor x_{float} \times 2^{subpixel_bits} + 0.5 \rfloor$$
定点数的数学基础
定点数是一种用整数表示小数的方法。对于m.n格式:
- m位表示整数部分
- n位表示小数部分
- 总位数 = m + n
- 精度 = $2^{-n}$
- 范围 = $[-2^{m-1}, 2^{m-1} - 2^{-n}]$
定点数运算:
// 加法:直接相加
fixed_c = fixed_a + fixed_b;
// 乘法:需要右移
fixed_c = (fixed_a * fixed_b) >> n;
// 除法:需要左移
fixed_c = (fixed_a << n) / fixed_b;
为什么需要亚像素精度
-
消除裂缝: - 浮点误差可能导致相邻三角形之间出现缝隙 - 定点数保证共享边的计算结果完全一致
-
精确的边界处理: - 像素中心可能恰好落在三角形边上 - 需要一致的规则决定归属
-
反走样支持: - 计算部分覆盖的像素 - 需要亚像素级别的精度
裂缝问题的根源
浮点运算的不精确性会导致:
- T型接缝:共享边的两个三角形在光栅化时产生缝隙
- 重复像素:同一像素被多个三角形覆盖
- 丢失像素:边界上的像素未被任何三角形覆盖
裂缝问题的具体例子
考虑两个共享边的三角形:
- 三角形A:$(0,0), (10,0), (5,10)$
- 三角形B:$(10,0), (5,10), (10,10)$
- 共享边:$(10,0) - (5,10)$
如果使用浮点计算:
// 三角形A计算共享边函数
E_A = (x - 10) * (10 - 0) - (y - 0) * (5 - 10)
= 10x - 100 + 5y
// 三角形B计算同一条边(反向)
E_B = (x - 5) * (0 - 10) - (y - 10) * (10 - 5)
= -10x + 50 - 5y + 50
= -10x - 5y + 100
// 理论上:E_A + E_B = 0
// 实际上:浮点误差可能导致 E_A + E_B ≠ 0
裂缝的视觉影响
裂缝在以下情况下尤为明显:
- 高对比度场景:亮色背景上的暗色物体
- 运动场景:裂缝会随着视角变化而闪烁
- 反走样后:裂缝可能被放大
定点数运算规则
为保证水密性(watertight),必须遵循:
- 顶点捕捉:共享顶点必须捕捉到相同的定点坐标
- 填充规则:采用统一的边界归属规则(如top-left rule)
- 边函数一致性:共享边的边函数计算必须产生相同结果
Top-left填充规则:
- 像素中心恰好在边上时,只有当边是"top"或"left"边时才包含该像素
- Top边:水平且位于三角形上方
- Left边:非水平且位于三角形左侧
Top-Left规则的实现
bool is_top_left_edge(vec2 v0, vec2 v1) {
vec2 edge = v1 - v0;
// Top edge: horizontal and going right
if (edge.y == 0 && edge.x > 0) return true;
// Left edge: going up
if (edge.y > 0) return true;
return false;
}
// 在边函数测试中应用
float bias = is_top_left_edge(v0, v1) ? 0 : -1;
if (edge_function + bias >= 0) {
// 像素在三角形内
}
定点数的溢出处理
定点数运算可能溢出,需要谨慎选择位宽:
-
边函数计算: - 输入:m.n格式的坐标 - 乘积:需要2m位 - 差值:需要2m+1位(考虑符号)
-
安全位宽选择: - 屏幕坐标:16.8(支持65536×65536) - 边函数:32位有符号整数 - 插值参数:16.16(更高精度)
定点数与浮点数的混合使用
在现代GPU中,通常混合使用:
- 定点数:用于光栅化阶段(保证精确性)
- 浮点数:用于着色计算(需要大范围)
转换点:
// 光栅化后,转换回浮点
float x_float = fixed_x / (float)(1 << FRAC_BITS);
float y_float = fixed_y / (float)(1 << FRAC_BITS);
// 进行着色计算
color = shade_pixel(x_float, y_float, attributes);
定点数的性能优势
- 确定性:相同输入总是产生相同输出
- 并行性:无需处理浮点异常
- 硬件效率:整数运算单元更简单、更快
定点数硬件实现
现代GPU中的定点数单元:
-
专用定点数ALU: - 定点乘法器 - 移位器(用于除法) - 饱和算术(防止溢出)
-
流水线设计:
Stage 1: 坐标转换 (float -> fixed) Stage 2: 边函数计算 Stage 3: 覆盖测试 Stage 4: 属性插值设置 -
SIMD优化: - 4个像素同时处理 - 向量化的定点数运算
未来趋势
随着浮点单元的进步,一些新技术正在出现:
-
混合精度模式: - 边界测试使用定点数 - 内部填充使用浮点数
-
可配置精度: - 根据场景需求调整 - VR需要更高精度 - 移动设备可以降低精度
-
AI辅助裂缝检测: - 机器学习检测潜在裂缝 - 自动修复算法
3.1.6 属性插值
对于三角形内部的点,所有顶点属性需要进行插值:
透视正确插值
屏幕空间的线性插值不等于世界空间的线性插值。透视正确插值公式: $$\frac{1}{w} = \frac{\alpha}{w_0} + \frac{\beta}{w_1} + \frac{\gamma}{w_2}$$ $$attribute = \frac{\frac{\alpha \cdot attr_0}{w_0} + \frac{\beta \cdot attr_1}{w_1} + \frac{\gamma \cdot attr_2}{w_2}}{\frac{1}{w}}$$ 其中 $w$ 是齐次坐标的第四分量。
透视插值的必要性
考虑一条在3D空间中均匀分布的纹理坐标线,投影到屏幕后:
- 近处的间隔在屏幕上显得大
- 远处的间隔在屏幕上显得小
- 直接的屏幕空间插值会导致纹理扭曲
数学推导: 设世界空间中的线性插值参数为 $t$,则: $$\mathbf{p}_{world} = (1-t)\mathbf{p}_0 + t\mathbf{p}_1$$ 投影后在屏幕空间: $$t_{screen} = \frac{t/w_1}{(1-t)/w_0 + t/w_1}$$ 这表明屏幕空间的参数与世界空间的参数呈非线性关系。
插值优化
现代GPU采用的优化策略:
- 预计算倒数:顶点着色器输出 $1/w$ 而非 $w$
- 增量插值:利用重心坐标的线性性质
- SIMD并行:同时插值多个属性
插值器硬件设计:
// 每个属性的插值
for each attribute:
I = a0/w0 * α + a1/w1 * β + a2/w2 * γ
attribute = I * w
特殊属性的处理
- 深度值:通常线性插值 $z/w$,因为深度缓冲存储的就是这个值
- 法线向量:插值后需要重新归一化
- 颜色:可以选择是否进行透视校正(Gouraud着色通常不需要)
3.2 深度测试与抗锯齿
3.2.1 深度缓冲(Z-Buffer)
深度缓冲是解决可见性问题的标准方法:
- 为每个像素存储当前最近的深度值
- 新片段只有在更近时才会更新
- 与绘制顺序无关(order-independent)
深度测试算法:
for each fragment at (x, y) with depth z:
if z < depth_buffer[x][y]:
depth_buffer[x][y] = z
color_buffer[x][y] = fragment_color
深度值的表示
从眼空间到NDC空间的深度变换: $$z_{ndc} = \frac{f+n}{f-n} + \frac{2fn}{f-n} \cdot \frac{1}{z_{eye}}$$ 归一化到[0,1]的窗口空间深度: $$z_{window} = \frac{z_{ndc} + 1}{2}$$
深度精度问题
透视投影导致深度精度分布极不均匀:
- 近处精度高,远处精度低
- 50%的精度集中在[n, 2n]范围内
- Z-fighting:深度值接近时的闪烁
精度分析: $$\frac{dz_{window}}{dz_{eye}} = \frac{fn}{(f-n)z_{eye}^2}$$ 这表明精度随距离平方反比下降。
深度精度优化方案
- 对数深度缓冲: $$z_{log} = \frac{\log(z/n)}{\log(f/n)}$$
- 优点:精度分布更均匀
- 缺点:需要着色器计算,不兼容硬件深度测试
-
反向Z(Reversed-Z): - 使用 $1-z$ 作为深度值 - 配合浮点深度缓冲,利用浮点数在0附近的高精度 - DirectX 12和Vulkan的推荐做法
-
多级深度缓冲(Cascaded Depth): - 将场景分为多个深度范围 - 每个范围使用独立的深度缓冲 - 常用于大规模场景(如飞行模拟器)
-
W缓冲: - 直接存储线性深度 $w = z_{eye}$ - 精度分布均匀但总体精度较低 - 早期硬件支持,现已少见
3.2.2 早期深度测试(Early Z)
现代GPU在片段着色前进行深度测试:
- 减少无用的片段着色计算
- 要求深度写入是单调的
- 某些操作会禁用Early Z
Early Z的工作原理
传统管线:光栅化 → 片段着色 → 深度测试 Early Z管线:光栅化 → 深度测试 → 片段着色(仅通过的片段)
启用条件:
- 片段着色器不修改深度值
- 不使用discard或clip
- 不启用alpha test
- 深度测试函数是单调的(如LESS、GREATER)
性能影响
Early Z的效果取决于:
- 绘制顺序:前到后绘制可最大化Early Z效益
- 场景复杂度:overdraw越严重,Early Z收益越大
- 着色器复杂度:片段着色器越复杂,节省越多
3.2.3 层次深度缓冲(Hierarchical Z)
使用深度金字塔加速剔除:
- 存储块级别的最大/最小深度
- 快速剔除被遮挡的大块区域
- 与分块光栅化配合使用
Hi-Z的构建
自底向上构建深度金字塔:
// 对于每个上层像素
z_max = max(child[0], child[1], child[2], child[3])
z_min = min(child[0], child[1], child[2], child[3])
Hi-Z测试流程
- 计算图元的保守深度范围[z_prim_min, z_prim_max]
- 从粗到细进行层次测试: - 如果 z_prim_min > z_tile_max:图元被遮挡,剔除 - 如果 z_prim_max < z_tile_min:图元在前,快速通过 - 否则:细分到下一层继续测试
Z-Cull vs Z-Pass
- Z-Cull:Hi-Z测试失败,整块剔除
- Z-Pass:Hi-Z测试通过,跳过像素级深度测试
- 两者结合可显著减少深度缓冲带宽
3.2.4 抗锯齿基础理论
采样理论
根据奈奎斯特-香农采样定理,采样频率必须大于信号最高频率的两倍才能完美重建信号。图形学中的锯齿本质是采样不足导致的混叠(aliasing)。
频域分析:
- 原始信号:$f(x)$
- 采样过程:$f_s(x) = f(x) \cdot \sum_{n} \delta(x - nT)$
- 频谱混叠:当 $f_s < 2f_{max}$ 时,高频成分折叠到低频
图形学中的高频来源:
- 几何边界:阶跃函数包含无限高频
- 纹理细节:高频纹理模式
- 镜面高光:急剧变化的光照
- 几何细节:细小的几何特征
预滤波vs后滤波
-
预滤波(Pre-filtering): - 在采样前去除高频成分 - 理论上正确但计算困难 - 需要解析计算覆盖面积
-
后滤波(Post-filtering): - 对采样结果进行重建 - 实际常用但理论不完美 - 包括各种图像空间技术
理想的抗锯齿是预滤波,但计算成本高。实践中常采用超采样近似预滤波。
滤波核函数
常用的重建滤波器:
- 盒式滤波器(Box):$w(x) = 1, |x| < 0.5$
- 三角滤波器(Tent):$w(x) = 1 - |x|, |x| < 1$
- 高斯滤波器:$w(x) = e^{-x^2/2\sigma^2}$
- Lanczos滤波器:$w(x) = \text{sinc}(x)\text{sinc}(x/a), |x| < a$
3.2.5 超采样抗锯齿(SSAA)
最直接的抗锯齿方法:
- 以更高分辨率渲染
- 对结果进行下采样
计算成本:$O(n^2)$,其中 $n$ 是超采样倍数。
3.2.6 多重采样抗锯齿(MSAA)
MSAA优化了SSAA的性能:
- 每个像素存储多个采样点
- 片段着色只执行一次
- 深度和覆盖测试对每个采样点独立进行
采样模式:
- 2×MSAA:旋转网格模式
- 4×MSAA:旋转盒模式或Quincunx
- 8×MSAA:优化的Poisson分布
覆盖计算:
coverage = 0
for each sample in pixel:
if sample inside triangle:
coverage += 1/num_samples
final_color = coverage * fragment_color + (1-coverage) * background
3.2.7 覆盖采样抗锯齿(CSAA)
NVIDIA的优化技术:
- 分离覆盖采样和颜色/深度采样
- 16×覆盖配合4×颜色/深度
- 减少内存带宽需求
3.2.8 时间抗锯齿(TAA)
利用时间域的信息:
- 每帧使用不同的采样偏移(jittering)
- 将多帧结果进行时间积累
- 使用运动向量处理动态场景
时间积累公式: $$color_{new} = \alpha \cdot color_{current} + (1-\alpha) \cdot color_{history}$$ 其中 $\alpha$ 通常为 0.1-0.2。
3.2.9 形态学抗锯齿(MLAA/FXAA/SMAA)
后处理抗锯齿技术:
- 边缘检测:识别几何边界
- 模式匹配:确定边缘形状
- 混合:根据模式进行像素混合
FXAA的亮度边缘检测: $$edge = |L_N - L_S| + |L_E - L_W| > threshold$$ 其中 $L$ 表示像素亮度。
3.3 现代GPU光栅化管线
3.3.1 GPU架构概述
现代GPU采用大规模并行架构:
- 流多处理器(SM):独立的计算单元
- SIMT执行模型:单指令多线程
- Warp/Wavefront:32/64线程的执行组
3.3.2 图元装配(Primitive Assembly)
将顶点组装成图元:
- 索引缓冲读取
- 图元拓扑解释(点、线、三角形)
- 图元剔除(背面、视锥体外)
背面剔除: $$\mathbf{n} \cdot \mathbf{v} = (v_1 - v_0) \times (v_2 - v_0) \cdot \mathbf{view} > 0$$
3.3.3 图元分配(Primitive Distribution)
将图元分配到屏幕分块:
- 计算图元的屏幕空间包围盒
- 确定覆盖的分块列表
- 将图元引用添加到分块队列
分块大小权衡:
- 小分块:更好的负载均衡,更多的管理开销
- 大分块:更少的重复工作,可能的负载不均
3.3.4 片段生成与调度
每个分块独立处理:
- 从队列读取图元
- 生成片段(2×2像素块为单位)
- 调度到着色单元
2×2像素块(Quad)的重要性:
- 计算屏幕空间导数(ddx、ddy)
- 纹理LOD计算
- 保证SIMD执行效率
3.3.5 深度缓冲压缩
减少带宽需求的关键技术:
差分压缩
对于平面区域,存储基准值和差分: $$z_{compressed} = z_{base} + \Delta z_x \cdot x + \Delta z_y \cdot y$$
深度范围压缩
存储块的最小/最大值和相对偏移: $$z_{normalized} = \frac{z - z_{min}}{z_{max} - z_{min}}$$
3.3.6 渲染输出单元(ROP)
负责最终的像素操作:
- 深度测试
- 模板测试
- 混合操作
- 写入帧缓冲
原子操作保证:
- 同一像素的操作串行化
- 不同像素可以并行
3.3.7 内存层次结构
优化的缓存设计:
- L1缓存:纹理缓存、常量缓存
- L2缓存:统一缓存,所有单元共享
- 帧缓冲缓存:专用于ROP操作
带宽优化技术:
- Delta色彩压缩:存储与参考值的差
- 快速清除:标记而非实际写入
- 事务消除:检测并跳过冗余写入
3.3.8 可编程管线扩展
网格着色器(Mesh Shader)
新一代几何管线:
- 替代传统的顶点/几何着色器
- 支持GPU生成几何体
- 更灵活的图元剔除
可变率着色(VRS)
允许不同区域使用不同的着色率:
- 1×1、1×2、2×2、4×4等
- 基于内容的自适应
- 注视点渲染优化
3.3.9 光线追踪硬件集成
现代GPU集成了光线追踪单元:
- RT Core:硬件加速的BVH遍历
- 光栅化与光追混合:主要场景用光栅化,反射/阴影用光追
- 时间去噪:利用TAA框架降噪
本章小结
光栅化是将连续几何转换为离散像素的核心过程。本章涵盖了:
关键概念:
- 三角形离散化的数学基础:边函数、重心坐标
- 透视正确插值:$\frac{1}{w}$ 的线性插值
- 深度缓冲与可见性:Z-Buffer算法及其优化
- 抗锯齿理论:采样定理与各种AA技术
- GPU管线架构:从图元到像素的并行处理
重要公式:
- 边函数:$E(\mathbf{p}) = (\mathbf{p} - \mathbf{v}_0) \times (\mathbf{v}_1 - \mathbf{v}_0)$
- 重心坐标:$\mathbf{p} = \alpha \mathbf{v}_0 + \beta \mathbf{v}_1 + \gamma \mathbf{v}_2$
- 透视插值:$attr = \frac{\sum \frac{\alpha_i \cdot attr_i}{w_i}}{\sum \frac{\alpha_i}{w_i}}$
- 深度映射:$z_{ndc} = \frac{f+n}{f-n} + \frac{2fn}{f-n} \cdot \frac{1}{z_{eye}}$
性能考虑:
- 分块光栅化减少overdraw
- Early-Z避免无用着色
- 压缩技术降低带宽需求
- SIMT执行模型的并行效率
练习题
基础题
3.1 边函数计算 给定三角形顶点 $\mathbf{v}_0 = (0, 0)$, $\mathbf{v}_1 = (4, 0)$, $\mathbf{v}_2 = (2, 3)$,计算点 $\mathbf{p} = (2, 1)$ 的三个边函数值,并判断该点是否在三角形内部。
提示:注意顶点的顺序和边函数的符号。
答案
计算三个边函数:
- $E_{01}(\mathbf{p}) = (2-0)(0-0) - (1-0)(4-0) = 0 - 4 = -4$
- $E_{12}(\mathbf{p}) = (2-4)(3-0) - (1-0)(2-4) = -6 - (-2) = -4$
- $E_{20}(\mathbf{p}) = (2-2)(0-3) - (1-3)(0-2) = 0 - 4 = -4$
由于所有边函数值都为负,说明三角形顶点是顺时针排列。对于顺时针排列,点在内部的条件是所有边函数值都小于等于0,因此点 $(2,1)$ 在三角形内部。
验证:可以计算重心坐标 $\alpha = \beta = \gamma = 1/3$,均为正值,确认点在内部。
3.2 重心坐标插值 三角形三个顶点的颜色分别为红色 $(1,0,0)$、绿色 $(0,1,0)$ 和蓝色 $(0,0,1)$。如果某点的重心坐标为 $(\alpha, \beta, \gamma) = (0.5, 0.3, 0.2)$,计算该点的插值颜色。
提示:直接应用重心坐标的定义。
答案
使用重心坐标进行线性插值: $$\mathbf{color} = \alpha \cdot \mathbf{c}_0 + \beta \cdot \mathbf{c}_1 + \gamma \cdot \mathbf{c}_2$$ $$= 0.5 \cdot (1,0,0) + 0.3 \cdot (0,1,0) + 0.2 \cdot (0,0,1)$$ $$= (0.5, 0, 0) + (0, 0.3, 0) + (0, 0, 0.2)$$ $$= (0.5, 0.3, 0.2)$$ 因此该点的颜色为 RGB = $(0.5, 0.3, 0.2)$。
3.3 透视正确插值 在屏幕空间,三角形顶点的深度值($1/w$)分别为 0.5、0.25 和 0.125。某点的重心坐标为 $(0.4, 0.4, 0.2)$。如果顶点的纹理坐标分别为 $(0,0)$、$(1,0)$ 和 $(0.5,1)$,计算该点的透视正确纹理坐标。
提示:先插值 $1/w$,再进行透视除法。
答案
首先插值 $1/w$: $$\frac{1}{w} = 0.4 \times 0.5 + 0.4 \times 0.25 + 0.2 \times 0.125 = 0.2 + 0.1 + 0.025 = 0.325$$ 然后计算每个分量的权重: $$\text{weight}_0 = \frac{0.4 \times 0.5}{0.325} = \frac{0.2}{0.325} = \frac{8}{13}$$ $$\text{weight}_1 = \frac{0.4 \times 0.25}{0.325} = \frac{0.1}{0.325} = \frac{4}{13}$$ $$\text{weight}_2 = \frac{0.2 \times 0.125}{0.325} = \frac{0.025}{0.325} = \frac{1}{13}$$ 最后插值纹理坐标: $$u = \frac{8}{13} \times 0 + \frac{4}{13} \times 1 + \frac{1}{13} \times 0.5 = \frac{4.5}{13} \approx 0.346$$ $$v = \frac{8}{13} \times 0 + \frac{4}{13} \times 0 + \frac{1}{13} \times 1 = \frac{1}{13} \approx 0.077$$
透视正确的纹理坐标为 $(0.346, 0.077)$。
3.4 MSAA覆盖计算 使用4×MSAA,采样点位置为像素中心偏移 $(-0.25, -0.25)$、$(0.25, -0.25)$、$(-0.25, 0.25)$ 和 $(0.25, 0.25)$。如果三角形边界恰好通过像素中心(从左下到右上的对角线),计算该像素的覆盖率。
提示:判断每个采样点相对于边界的位置。
答案
三角形边界是从左下到右上的对角线,可以表示为:$y = x$
对于每个采样点,检查是否在边界下方(假设三角形在边界下方):
- $(-0.25, -0.25)$:$-0.25 < -0.25$ 不成立,不被覆盖
- $(0.25, -0.25)$:$-0.25 < 0.25$ 成立,被覆盖
- $(-0.25, 0.25)$:$0.25 < -0.25$ 不成立,不被覆盖
- $(0.25, 0.25)$:$0.25 < 0.25$ 不成立,不被覆盖
覆盖率 = 1/4 = 25%
注意:如果三角形在边界上方,则采样点1和3会被覆盖,覆盖率为50%。
挑战题
3.5 保守光栅化 设计一个保守光栅化算法,确保所有与三角形有任何重叠的像素都被标记。描述你的算法并分析其与标准光栅化的差异。考虑:如何扩展三角形边界?如何处理极小的三角形?
提示:考虑将三角形边界向外扩展半个像素。
答案
保守光栅化算法设计:
-
边界扩展方法: - 将每条边沿法线方向向外扩展 $\frac{\sqrt{2}}{2}$ 个像素(最坏情况下的像素对角线长度) - 边函数修改:$E'(p) = E(p) - \frac{\sqrt{2}}{2} \cdot ||\mathbf{n}||$
-
实现步骤:
for each edge (v0, v1): normal = perpendicular(v1 - v0) offset = 0.5 * sqrt(2) * normalize(normal) expanded_edge = translate edge by offset -
极小三角形处理: - 如果三角形面积小于一个像素,至少光栅化其质心所在的像素 - 使用包围球测试:如果像素中心到三角形的距离小于 $\frac{\sqrt{2}}{2}$,则光栅化
-
优化考虑: - 使用分离轴定理进行精确的像素-三角形相交测试 - 对于轴对齐的边,扩展可以简化为0.5像素
-
应用场景: - 体素化 - 碰撞检测 - 阴影体生成
3.6 自适应超采样 设计一个自适应超采样系统,只在需要的地方(如边缘)使用高采样率。你的系统应该包括:边缘检测标准、采样模式选择、以及性能与质量的权衡分析。
提示:可以基于深度或颜色的梯度来检测边缘。
答案
自适应超采样系统设计:
-
边缘检测标准:
edge_score = max( |depth_center - depth_neighbor| / depth_center, |color_center - color_neighbor|, |normal_center · normal_neighbor - 1| ) -
采样策略分级: - Level 0 (1×): edge_score < 0.1 - Level 1 (4×): 0.1 ≤ edge_score < 0.3 - Level 2 (8×): 0.3 ≤ edge_score < 0.6 - Level 3 (16×): edge_score ≥ 0.6
-
实现流程: - Pass 1: 以1×采样率渲染,同时输出G-buffer - Analysis: 分析G-buffer,标记需要超采样的像素 - Pass 2: 对标记的像素进行超采样 - Resolve: 合并结果
-
优化技术: - 时间复用:结合TAA,减少当前帧的采样需求 - 预测:基于前一帧的边缘信息预测当前帧 - 分块处理:以2×2或4×4块为单位决定采样率
-
性能分析: - 最佳情况:大部分平滑区域,接近1×性能 - 最坏情况:全是边缘,退化为完全超采样 - 典型场景:20-40%的像素需要超采样,性能提升2-3倍
3.7 深度缓冲精度优化 推导对数深度缓冲的数学公式,并分析其相对于标准深度缓冲的精度分布。实现一个深度值编码/解码方案,在24位定点数中最大化可用精度。
提示:考虑人眼对近处细节的敏感度。
答案
- 对数深度推导:
标准深度:$z_{std} = \frac{f+n}{f-n} - \frac{2fn}{(f-n)z}$
对数深度:$z_{log} = \frac{\log(z/n)}{\log(f/n)}$
精度分析:
- 标准深度:$\frac{dz_{std}}{dz} = \frac{2fn}{(f-n)z^2}$(随z²下降)
- 对数深度:$\frac{dz_{log}}{dz} = \frac{1}{z\log(f/n)}$(随z线性下降)
-
24位编码方案:
encode(z_eye): // 使用伪对数编码 if z_eye < split_distance: // 近处使用线性 encoded = (z_eye - n) / (split - n) * 0.5 else: // 远处使用对数 encoded = 0.5 + 0.5 * log(z_eye/split) / log(f/split) return uint24(encoded * (2^24 - 1)) -
精度分布优化: - 分割点选择:$split = \sqrt{nf}$(几何平均) - 近处精度:~0.01mm at 1m - 远处精度:~1m at 1000m
-
实际考虑: - 反向Z + 浮点:利用浮点数的指数特性 - W缓冲:直接存储线性深度1/w - 多级深度:近景和远景使用独立缓冲
3.8 GPU分块大小优化 分析不同分块大小(8×8, 16×16, 32×32)对GPU光栅化性能的影响。考虑:缓存命中率、负载均衡、几何复杂度的影响。设计一个实验来确定特定场景的最优分块大小。
提示:考虑三角形大小分布和屏幕空间局部性。
答案
- 理论分析:
8×8分块:
- 优点:细粒度负载均衡,小三角形效率高
- 缺点:管理开销大,大三角形跨越多个块
16×16分块:
- 优点:平衡的选择,适合中等大小三角形
- 缺点:某些场景可能不够灵活
32×32分块:
- 优点:管理开销小,大三角形效率高
- 缺点:负载不均衡风险,小三角形浪费
- 性能模型: ``` Cost = N_tiles × (C_setup + N_prims × C_raster + N_pixels × C_shade)
其中:
- N_tiles = ceil(W/tile_size) × ceil(H/tile_size)
- N_prims = avg_prims_per_tile(与分块大小相关)
- N_pixels = tile_size²(实际着色的像素可能更少) ```
-
实验设计: ``` for tile_size in [8, 16, 32]: for scene in [many_small_tris, few_large_tris, mixed]: measure:
- Frame time - Tile occupancy - Cache miss rate - Warp divergence```
-
自适应策略: - 基于场景统计动态选择 - 混合分块:不同区域使用不同大小 - 预测模型:基于前几帧的统计信息
-
实际建议: - UI渲染:8×8(many small triangles) - 游戏场景:16×16(平衡选择) - CAD/建筑:32×32(大型三角形)
常见陷阱与错误(Gotchas)
1. 精度相关问题
浮点精度导致的裂缝
- 问题:相邻三角形的共享边在光栅化时可能产生裂缝
- 原因:浮点运算的舍入误差
- 解决:使用定点数运算,确保共享顶点的坐标完全相同
深度精度不足
- 问题:Z-fighting,远处物体深度分辨率低
- 原因:透视投影的非线性深度分布
- 解决:调整near/far平面,使用对数深度或反向Z
2. 插值错误
屏幕空间线性插值
- 问题:纹理扭曲,光照不正确
- 原因:忘记进行透视校正
- 解决:始终使用 $1/w$ 进行插值权重计算
属性插值溢出
- 问题:颜色值超出[0,1]范围
- 原因:重心坐标计算错误或数值精度问题
- 解决:验证重心坐标和为1,添加范围检查
3. 性能陷阱
过度绘制(Overdraw)
- 问题:同一像素被多次着色
- 原因:绘制顺序不当,缺少早期剔除
- 解决:前向后渲染不透明物体,使用Early-Z
小三角形问题
- 问题:GPU利用率低
- 原因:小于2×2像素的三角形导致quad利用率低
- 解决:使用LOD,合并小三角形
4. 抗锯齿相关
MSAA与透明度
- 问题:Alpha-tested几何体边缘仍有锯齿
- 原因:MSAA只对几何边缘有效,不处理着色器discard
- 解决:使用Alpha to Coverage或专门的透明度AA
TAA鬼影
- 问题:移动物体出现拖尾
- 原因:历史帧权重过高或运动向量不准确
- 解决:降低历史帧权重,改进运动向量计算
5. GPU特定问题
Warp发散
- 问题:性能显著下降
- 原因:同一warp内的线程执行不同分支
- 解决:排序绘制调用,减少动态分支
纹理缓存未命中
- 问题:带宽瓶颈
- 原因:纹理访问模式随机,mipmap选择不当
- 解决:改善UV布局,正确计算LOD
6. 调试技巧
视觉化调试
// 可视化重心坐标
color = vec3(alpha, beta, gamma);
// 可视化mipmap级别
color = heat_map(mipmap_level);
// 可视化overdraw
color = vec3(overdraw_count / max_overdraw);
性能分析要点
- 使用GPU profiler识别瓶颈
- 监控各阶段的吞吐量
- 检查缓存命中率
最佳实践检查清单
光栅化设计审查
几何处理
- [ ] 三角形顶点按一致顺序(通常逆时针)
- [ ] 共享顶点使用索引缓冲
- [ ] 避免退化三角形(面积接近0)
- [ ] 合理的三角形大小(避免过小)
精度管理
- [ ] 定点数用于光栅化坐标
- [ ] 适当的子像素精度(通常8-16位)
- [ ] Near/far平面合理设置
- [ ] 深度缓冲格式选择得当
性能优化
- [ ] 实施背面剔除
- [ ] 启用Early-Z(避免禁用它的操作)
- [ ] 合理的绘制顺序(不透明物体前到后)
- [ ] 批处理相似的绘制调用
抗锯齿策略
- [ ] 根据目标平台选择AA方法
- [ ] MSAA采样模式优化
- [ ] TAA的运动向量正确
- [ ] 后处理AA作为备选方案
内存与带宽
- [ ] 深度缓冲压缩启用
- [ ] 颜色缓冲压缩启用
- [ ] 合理的渲染目标格式
- [ ] 避免不必要的缓冲清除
着色器最佳实践
- [ ] 避免着色器中的discard(影响Early-Z)
- [ ] 减少动态分支
- [ ] 利用插值器硬件
- [ ] 正确的导数计算(维持2×2 quad)
现代特性利用
- [ ] VRS用于性能优化
- [ ] Mesh shader用于几何优化
- [ ] 硬件光追的选择性使用
- [ ] 多分辨率渲染技术
调试与验证
- [ ] 无渲染错误(裂缝、闪烁)
- [ ] 性能指标达标
- [ ] 内存使用合理
- [ ] 支持必要的调试可视化
代码质量
- [ ] 光栅化参数可配置
- [ ] 适当的错误处理
- [ ] 性能计数器集成
- [ ] 清晰的管线状态管理