39. 计算机图形学基础

沿渲染管线逐级展开图形学核心概念:从模型视图投影矩阵与齐次坐标的变换链,到 Lambert、Phong、Blinn-Phong 光照模型与 BRDF 入门,再讲纹理映射与 Mipmap 采样、深度缓冲与 Z-fighting、背面剔除与视锥裁剪,并对比光栅化与光线追踪两种范式,最后落到着色器职责、伽马校正与 MSAA 抗锯齿。

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 与空间加速结构的组织方式
  • 算法复杂度 — 光栅化与光线追踪的复杂度对比

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 46. 排队论与容量估算:利特尔法则与尾延迟
  2. 45. 编译器优化与中间表示:SSA、内联与循环优化
  3. 44. 并发模型对比:Actor、CSP 与数据并行