1. 渲染管线总览
1.1 管线各阶段
现代实时渲染管线(以 OpenGL/Vulkan 为参照):
顶点数据
→ 顶点着色器(Vertex Shader)
→ 图元装配(Primitive Assembly)
→ 几何着色器(可选)
→ 裁剪与透视除法
→ 视口变换
→ 光栅化(Rasterization)
→ 片元着色器(Fragment Shader)
→ 逐片元测试(深度/模板)
→ 混合(Blending)
→ 帧缓冲
1.2 可编程与固定阶段
- 可编程:顶点着色器、片元着色器(以及可选几何/曲面细分/计算着色器)。
- 固定功能:裁剪、透视除法、视口变换、光栅化、深度测试、混合。
关键认知:光栅化本身是固定硬件,程序员只能通过着色器影响「每个顶点」和「每个片元」的行为。
1.3 数据流视角
管线本质是把 顶点流 变成 片元流 再变成 像素。中间所有几何阶段都在 3D 空间运算,光栅化那一刻起进入 2D 屏幕空间。
2. 坐标系与变换
2.1 五个坐标空间
模型空间 Model
│ 模型矩阵 M
世界空间 World
│ 视图矩阵 V
观察空间 View/Camera
│ 投影矩阵 P
裁剪空间 Clip
│ 透视除法
NDC(-1..1)
│ 视口变换
屏幕空间 Screen
2.2 齐次坐标
用 4 维向量 (x, y, z, w) 表示 3D 点,好处是平移也能写成矩阵乘法:
// 平移矩阵(4x4,列主序)
float T[16] = {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
};
w=1 表示点,w=0 表示方向向量(平移对其无效)。透视投影后 w 携带深度信息,透视除法 (x/w, y/w, z/w) 把远处的物体缩小。
2.3 MVP 矩阵
最终变换是三者复合:
clip = P · V · M · vertex
顺序不可交换:先模型、再视图、最后投影。矩阵乘法满足结合律,所以可以先在 CPU 算好 MVP = P·V·M 一次,逐顶点只做一次矩阵乘。
3. 投影与齐次坐标
3.1 正交投影
平行投影,无近大远小,用于 CAD 与 2D。矩阵只做线性缩放与平移。
3.2 透视投影
模拟人眼,远处变小。核心是用 w 分量存储 -z:
x' = x
y' = y
z' = a·z + b
w' = -z
透视除法后 x'/w' 自然带上 1/z 的缩放。透视矩阵由 fov(视场角)、aspect(宽高比)、near、far 决定。
import numpy as np
def perspective(fov_deg, aspect, near, far):
f = 1.0 / np.tan(np.radians(fov_deg) / 2)
return np.array([
[f/aspect, 0, 0, 0],
[0, f, 0, 0],
[0, 0, (far+near)/(near-far), -1],
[0, 0, (2*far*near)/(near-far), 0],
])
3.3 深度精度问题
透视投影下深度是非线性的,near 平面越近,远处精度越差。设 near 过小(如 0.001)会让远处 Z-fighting 严重,通常 near 应尽量大(0.1~1)。
4. 光照模型
4.1 Lambert 漫反射
漫反射强度正比于光线与法线夹角的余弦:
I_diffuse = k_d · I_light · max(0, N · L)
N 是表面法线,L 是指向光源的单位向量。max(0, ·) 保证背光面不出现负光照。
4.2 Phong 镜面反射
R = reflect(-L, N)
I_specular = k_s · I_light · max(0, R · V)^shininess
V 是视线方向,shininess 越大高光越锐利。计算 reflect 需要一次向量反射。
4.3 Blinn-Phong 改进
用半程向量 H 替代反射向量,省一次反射计算且高光更自然:
H = normalize(L + V)
I_specular = k_s · I_light · max(0, N · H)^shininess
Blinn-Phong 是实时渲染的事实标准,几乎所有引擎默认使用。
vec3 N = normalize(vNormal);
vec3 L = normalize(lightPos - vFragPos);
vec3 V = normalize(cameraPos - vFragPos);
vec3 H = normalize(L + V);
float diff = max(dot(N, L), 0.0);
float spec = pow(max(dot(N, H), 0.0), 32.0);
vec3 color = (kAmbient + kDiffuse * diff + kSpecular * spec) * lightColor;
4.4 环境光与衰减
- 环境光:常数项近似间接光,避免背光面全黑。
- 衰减:点光源强度按
1/d²衰减,工程上常加常数与线性项平滑近处。
5. BRDF 入门
5.1 定义
**BRDF(双向反射分布函数)**描述「从入射方向 ωi 来的光有多少反射到出射方向 ωo」:
f_r(ωi, ωo) = dL_o(ωo) / dE_i(ωi)
5.2 渲染方程
L_o(x, ωo) = L_e(x, ωo) + ∫ f_r(x, ωi, ωo) · L_i(x, ωi) · (N · ωi) dωi
这是全局光照的数学基础。实时渲染只求近似解:只算直接光,间接光靠环境贴图或探针。
5.3 物理正确模型
- 微表面理论:表面由微小镜面组成,BRDF 由 法线分布 D、几何遮蔽 G、菲涅尔 F 三项构成(Cook-Torrance)。
- 金属度/粗糙度工作流:PBR 材质用 baseColor、metallic、roughness 三张图描述,取代传统的高光/漫反射贴图。
- 能量守恒:反射的光不能超过入射光,这是 PBR 与「调参数好看」的本质区别。
6. 纹理映射与采样
6.1 UV 与纹理坐标
每个顶点带一对 (u, v) 坐标,光栅化时在三角形内重心插值得到片元的 UV。
6.2 采样滤波
- 最近邻:取最近纹素,像素化明显,但适合像素风。
- 双线性:取周围 4 个纹素加权平均,平滑。
- 三线性:在双线性基础上再对两个 mipmap 层插值。
6.3 Mipmap
预先算好一系列逐级减半的纹理副本:
Level 0: 1024x1024
Level 1: 512x512
...
Level 10: 1x1
远处物体一个像素覆盖很多纹素,不降采样会严重走样(摩尔纹)。Mipmap 用约 33% 额外显存换来正确的降采样。
6.4 各向异性过滤
Mipmap 假设投影近似各向同性,斜视地面时会过度模糊。各向异性过滤沿主方向做多次采样,按压缩比取值(如 16x),显著改善地面纹理清晰度。
6.5 寻址模式
GL_REPEAT(平铺)、GL_CLAMP_TO_EDGE(钳制到边缘)、GL_MIRRORED_REPEAT(镜像)。默认 REPEAT 时 UV 超出会平铺,天空盒等场景需显式改钳制。
7. 深度缓冲与可见性
7.1 Z-Buffer 算法
每个像素维护一个深度值,片元深度小于已存值才写入颜色:
if (fragDepth < depthBuffer[x][y]) {
depthBuffer[x][y] = fragDepth;
colorBuffer[x][y] = fragColor;
}
复杂度 O(像素数),与三角形数量无关,是 GPU 默认的可见性方案。代价是逐像素写深度,需要额外带宽。
7.2 Early-Z
现代 GPU 在片元着色器之前先做深度测试,被遮挡的片元直接丢弃不执行着色器,大幅省算力。但若着色器里写了 gl_FragDepth,Early-Z 失效。
7.3 Z-Fighting
两个面深度极接近时,逐像素深度精度不足导致闪烁条纹。原因与对策:
- 深度精度不足:加大 near 平面距离。
- 共面几何:用 深度偏移(polygon offset) 把其中一个稍微前移。
- 对数深度缓冲:用
log分布重映射深度,改善远处精度。
7.4 透明度与排序
半透明物体不能只靠 Z-Buffer,必须从远到近排序再混合(画家算法),且关闭深度写入只保留测试。
8. 剔除与裁剪
8.1 背面剔除
三角形顶点按逆时针为正面(OpenGL 默认),法线朝外的面才渲染,背面直接丢弃,省掉约一半三角形。
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
8.2 视锥裁剪
只保留与视锥体(六个平面围成)相交的物体,其余不进入管线。视锥裁剪可在物体级(包围盒)与三角形级做。
8.3 裁剪与透视除法顺序
必须在透视除法之前裁剪(裁剪空间下做),否则 w 为负的顶点除法后坐标翻转,产生错误几何。
8.4 遮挡剔除
用**层次 Z 缓冲(Hi-Z)**或 GPU 查询判断物体是否完全被遮挡,省掉整批绘制调用,是复杂场景的关键优化。
9. 光栅化与光线追踪
9.1 光栅化的特点
- 逐三角形遍历像素,硬件高度并行,实时(60~240fps)可行。
- 全局效果(反射、折射、软阴影)需要 hack 或屏幕空间近似。
- 复杂度随三角形数与分辨率增长。
9.2 光线追踪的特点
- 逐像素发射光线求交,天然支持反射、折射、阴影、全局光照。
- 朴素实现复杂度高,靠 BVH / KD-Tree 加速结构降到 O(log n)。
- 传统上离线渲染用,如今 RTX 等硬件加速让实时光追成为可能。
for each pixel:
ray = camera.generateRay(x, y)
color = trace(ray, depth)
9.3 混合渲染
现代引擎常用光栅化做主渲染 + 光线追踪做反射/阴影/全局光的混合管线,兼顾性能与画质。
10. 着色器
10.1 顶点着色器职责
- 应用 MVP 变换,把顶点从模型空间送到裁剪空间。
- 计算法线、切线、UV 等传给后续阶段的数据。
- 每个顶点执行一次,输出裁剪空间坐标。
10.2 片元着色器职责
- 逐片元计算颜色(光照、纹理、效果)。
- 输出到帧缓冲,参与深度测试与混合。
- 每个片元执行一次,是最耗算力的阶段。
10.3 插值限定符
out vec3 vNormal; // 默认平滑插值
flat out int vMaterialId; // 不插值,取 provoking vertex
noperspective out vec2 vUV; // 屏幕空间线性插值
flat 对整数与索引至关重要——插值整数会得到无意义的值。
10.4 计算着色器
计算着色器脱离图形管线,做通用并行计算(后处理、粒子、GPU 剔除),是 GPGPU 在图形 API 里的入口。
11. 颜色空间与伽马校正
11.1 线性与 sRGB
光照计算必须在线性空间做,而纹理与显示器用 sRGB(近似 gamma 2.2)编码。
线性 → sRGB: c_srgb = c_linear^(1/2.2)
sRGB → 线性: c_linear = c_srgb^2.2
11.2 常见错误
- 忘记在采样后转线性:光照计算在 sRGB 空间做,结果偏暗且高光发灰。
- 忘记在输出前转 sRGB:画面整体过亮、发白。
- 现代管线通常让 GPU 用 sRGB 纹理格式自动转换,或在着色器首尾各转一次。
11.3 HDR 与色调映射
HDR 用浮点缓冲保存超过 1.0 的亮度,最后用 Reinhard、ACES 等曲线映射到 [0,1] 显示。ACES 是影视与游戏的工业标准。
12. 抗锯齿
12.1 走样的来源
光栅化是点采样,高频几何边缘采样不足产生锯齿(jaggies),纹理高频产生摩尔纹。
12.2 SSAA
超采样:以 2x/4x 分辨率渲染再降采样,质量最好但成本按面积平方增长。
12.3 MSAA
多重采样:只对几何边缘做多采样,内部像素仍单次着色。大幅省算力且效果接近 SSAA,是实时渲染主流。
glEnable(GL_MULTISAMPLE);
12.4 后处理抗锯齿
- FXAA:后处理检测边缘模糊,极快但会糊细节。
- TAA:利用时间累积多帧抖动采样,质量高,但快速运动会产生拖影(需运动向量修正)。
- DLSS / FSR:基于 AI 或空间算法的超分抗锯齿,同时提升性能。
13. 常见陷阱
- MVP 矩阵乘法顺序写反:必须是
P·V·M,顺序颠倒会得到扭曲或不可见的结果。 - 在 sRGB 空间做光照:颜色发灰、过渡不自然,务必转线性再算。
- near 平面设得过小:远处深度精度崩塌,Z-Fighting 遍布全场。
- 忘了开深度测试:后画的物体直接覆盖前面的,画面像涂鸦。
- 半透明物体不排序:混合结果错误,需从远到近绘制并关闭深度写入。
- 整数属性未加
flat:插值出无意义的材质索引,导致随机错色。 - 法线未归一化:非等比缩放会破坏法线方向,必须用法线矩阵(模型矩阵逆转置)变换。
- 纹理未生成 Mipmap:远处纹理严重摩尔纹与闪烁,应开启
GL_LINEAR_MIPMAP_LINEAR。 - 裁剪放在透视除法之后:
w为负时坐标翻转,产生飞出屏幕的错误三角形。 - 过度依赖 TAA 掩盖低分辨率:拖影与细节丢失在高动态场景下明显,需权衡。
参考文章
- 计算机组成原理 — GPU 硬件结构与 SIMD 执行模型
- 并行计算与 GPU 编程模型 — warp、共享内存与 kernel 编写
- 线性代数与矩阵 — 变换矩阵与齐次坐标的数学基础
- 数据结构 — BVH 与空间加速结构的组织方式
- 算法复杂度 — 光栅化与光线追踪的复杂度对比
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。