光栅化是将连续的几何图元转换为离散像素的过程,是现代实时渲染的核心技术。本章深入探讨光栅化的数学原理、算法实现和硬件优化策略。我们将从三角形的离散化开始,逐步深入到深度测试、抗锯齿技术,最后探讨现代GPU的光栅化管线设计。通过本章学习,读者将掌握从几何到像素的完整转换过程,理解现代图形硬件的设计哲学,并能够分析和优化光栅化性能。
三角形是计算机图形学中最基本的图元,其优势包括:
三角形的数学表示形式多样,可以用参数方程、隐式方程或重心坐标表示: \(\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),这意味着它是能够张成二维空间的最小凸集。
仿射不变性:重心坐标在仿射变换下保持不变 \(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)\)
这个性质保证了我们可以在任意坐标系中进行插值计算。
四边形:
多边形:
曲面片(如Bézier):
在实际应用中,三角形通常组织成网格(mesh)结构:
三角形可以通过三个顶点 $\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2$ 表示,每个顶点包含:
在屏幕空间中,经过投影变换后的顶点坐标为: \(\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)决定了三角形的正面:
叉积与缠绕顺序的关系: \(\mathbf{n} = (\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0)\)
对于复杂模型,三角形常组织成层次结构:
对于屏幕空间中的点 $\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(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_{01}(\mathbf{p}) = 2 \cdot \text{SignedArea}(\mathbf{v}_0, \mathbf{v}_1, \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分量。
当像素中心恰好落在三角形边界上时,需要明确的规则避免:
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);
}
优势:
重心坐标 $(\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 = \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)$ 提供了三角形内部的一种自然参数化。
当三角形接近退化时,重心坐标计算可能不稳定。稳健的实现需要:
条件数检查: \(\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);
角度法:计算点对三角形三条边的张角之和:
| 方法 | 计算复杂度 | 精度 | 硬件适合性 | 备注 |
|---|---|---|---|---|
| 边函数 | O(1) | 高 | 极好 | GPU标准方法 |
| 重心坐标 | O(1) | 高 | 好 | 同时用于插值 |
| 面积法 | O(1) | 中 | 中 | 浮点误差累积 |
| 角度法 | O(1) | 低 | 差 | 需要反三角函数 |
最简单的光栅化算法:
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)
效率分析:
// 考虑像素中心偏移
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}}\)
不同形状三角形的填充率:
低填充率意味着大量无效测试,需要更精细的算法。
边函数具有线性性质: \(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
相比朴素算法:
现代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;
}
现代GPU采用分块策略:
层次化测试策略:
经典的光栅化方法,特别适合软件实现:
扫描线转换的优势:
一种高效的并行光栅化算法:
为避免浮点误差导致的裂缝,现代光栅化器使用定点数表示:
定点数转换: \(x_{fixed} = \lfloor x_{float} \times 2^{subpixel\_bits} + 0.5 \rfloor\)
定点数是一种用整数表示小数的方法。对于m.n格式:
定点数运算:
// 加法:直接相加
fixed_c = fixed_a + fixed_b;
// 乘法:需要右移
fixed_c = (fixed_a * fixed_b) >> n;
// 除法:需要左移
fixed_c = (fixed_a << n) / fixed_b;
浮点运算的不精确性会导致:
考虑两个共享边的三角形:
如果使用浮点计算:
// 三角形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填充规则:
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) {
// 像素在三角形内
}
定点数运算可能溢出,需要谨慎选择位宽:
在现代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中的定点数单元:
Stage 1: 坐标转换 (float -> fixed)
Stage 2: 边函数计算
Stage 3: 覆盖测试
Stage 4: 属性插值设置
随着浮点单元的进步,一些新技术正在出现:
对于三角形内部的点,所有顶点属性需要进行插值:
屏幕空间的线性插值不等于世界空间的线性插值。透视正确插值公式: \(\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采用的优化策略:
插值器硬件设计:
// 每个属性的插值
for each attribute:
I = a0/w0 * α + a1/w1 * β + a2/w2 * γ
attribute = I * w
深度缓冲是解决可见性问题的标准方法:
深度测试算法:
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}\)
透视投影导致深度精度分布极不均匀:
精度分析: \(\frac{dz_{window}}{dz_{eye}} = \frac{fn}{(f-n)z_{eye}^2}\)
这表明精度随距离平方反比下降。
现代GPU在片段着色前进行深度测试:
传统管线:光栅化 → 片段着色 → 深度测试 Early Z管线:光栅化 → 深度测试 → 片段着色(仅通过的片段)
启用条件:
Early Z的效果取决于:
使用深度金字塔加速剔除:
自底向上构建深度金字塔:
// 对于每个上层像素
z_max = max(child[0], child[1], child[2], child[3])
z_min = min(child[0], child[1], child[2], child[3])
根据奈奎斯特-香农采样定理,采样频率必须大于信号最高频率的两倍才能完美重建信号。图形学中的锯齿本质是采样不足导致的混叠(aliasing)。
频域分析:
图形学中的高频来源:
理想的抗锯齿是预滤波,但计算成本高。实践中常采用超采样近似预滤波。
常用的重建滤波器:
| 盒式滤波器(Box):$w(x) = 1, | x | < 0.5$ |
| 三角滤波器(Tent):$w(x) = 1 - | x | , | x | < 1$ |
| Lanczos滤波器:$w(x) = \text{sinc}(x)\text{sinc}(x/a), | x | < a$ |
最直接的抗锯齿方法:
计算成本:$O(n^2)$,其中 $n$ 是超采样倍数。
MSAA优化了SSAA的性能:
采样模式:
覆盖计算:
coverage = 0
for each sample in pixel:
if sample inside triangle:
coverage += 1/num_samples
final_color = coverage * fragment_color + (1-coverage) * background
NVIDIA的优化技术:
利用时间域的信息:
时间积累公式: \(color_{new} = \alpha \cdot color_{current} + (1-\alpha) \cdot color_{history}\)
其中 $\alpha$ 通常为 0.1-0.2。
后处理抗锯齿技术:
FXAA的亮度边缘检测: \(edge = |L_N - L_S| + |L_E - L_W| > threshold\)
其中 $L$ 表示像素亮度。
现代GPU采用大规模并行架构:
将顶点组装成图元:
背面剔除: \(\mathbf{n} \cdot \mathbf{v} = (v_1 - v_0) \times (v_2 - v_0) \cdot \mathbf{view} > 0\)
将图元分配到屏幕分块:
分块大小权衡:
每个分块独立处理:
2×2像素块(Quad)的重要性:
减少带宽需求的关键技术:
对于平面区域,存储基准值和差分: \(z_{compressed} = z_{base} + \Delta z_x \cdot x + \Delta z_y \cdot y\)
存储块的最小/最大值和相对偏移: \(z_{normalized} = \frac{z - z_{min}}{z_{max} - z_{min}}\)
负责最终的像素操作:
原子操作保证:
优化的缓存设计:
带宽优化技术:
新一代几何管线:
允许不同区域使用不同的着色率:
现代GPU集成了光线追踪单元:
光栅化是将连续几何转换为离散像素的核心过程。本章涵盖了:
关键概念:
重要公式:
性能考虑:
3.1 边函数计算 给定三角形顶点 $\mathbf{v}_0 = (0, 0)$, $\mathbf{v}_1 = (4, 0)$, $\mathbf{v}_2 = (2, 3)$,计算点 $\mathbf{p} = (2, 1)$ 的三个边函数值,并判断该点是否在三角形内部。
提示:注意顶点的顺序和边函数的符号。
3.2 重心坐标插值 三角形三个顶点的颜色分别为红色 $(1,0,0)$、绿色 $(0,1,0)$ 和蓝色 $(0,0,1)$。如果某点的重心坐标为 $(\alpha, \beta, \gamma) = (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$,再进行透视除法。
3.4 MSAA覆盖计算 使用4×MSAA,采样点位置为像素中心偏移 $(-0.25, -0.25)$、$(0.25, -0.25)$、$(-0.25, 0.25)$ 和 $(0.25, 0.25)$。如果三角形边界恰好通过像素中心(从左下到右上的对角线),计算该像素的覆盖率。
提示:判断每个采样点相对于边界的位置。
3.5 保守光栅化 设计一个保守光栅化算法,确保所有与三角形有任何重叠的像素都被标记。描述你的算法并分析其与标准光栅化的差异。考虑:如何扩展三角形边界?如何处理极小的三角形?
提示:考虑将三角形边界向外扩展半个像素。
3.6 自适应超采样 设计一个自适应超采样系统,只在需要的地方(如边缘)使用高采样率。你的系统应该包括:边缘检测标准、采样模式选择、以及性能与质量的权衡分析。
提示:可以基于深度或颜色的梯度来检测边缘。
3.7 深度缓冲精度优化 推导对数深度缓冲的数学公式,并分析其相对于标准深度缓冲的精度分布。实现一个深度值编码/解码方案,在24位定点数中最大化可用精度。
提示:考虑人眼对近处细节的敏感度。
3.8 GPU分块大小优化 分析不同分块大小(8×8, 16×16, 32×32)对GPU光栅化性能的影响。考虑:缓存命中率、负载均衡、几何复杂度的影响。设计一个实验来确定特定场景的最优分块大小。
提示:考虑三角形大小分布和屏幕空间局部性。
浮点精度导致的裂缝
深度精度不足
屏幕空间线性插值
属性插值溢出
过度绘制(Overdraw)
小三角形问题
MSAA与透明度
TAA鬼影
Warp发散
纹理缓存未命中
视觉化调试
// 可视化重心坐标
color = vec3(alpha, beta, gamma);
// 可视化mipmap级别
color = heat_map(mipmap_level);
// 可视化overdraw
color = vec3(overdraw_count / max_overdraw);
性能分析要点