少女祈祷中...

前言

  • 第四版更新的思考与挑战

    • 作者们最初认为过去八年变化不大,更新工作会相对轻松。
    • 然而,实际花费了一年半时间,并增加了三位专家参与,才完成任务。
    • 即便如此,仍感觉可以继续编辑和阐述,因为领域内不断有新的文章和成果涌现。
    • 参考资料的Google Doc已超过170页,每页约20条参考文献及笔记,体现了资料的丰富性。
    • 书中引用的某些参考文献,其内容足以在其他书籍中构成独立章节。例如阴影章节,其主题本身就有专门的书籍论述。
    • 这种信息的丰富性对从业者是好消息,本书会经常指向这些主要信息源。
  • 本书核心内容

    • 专注于创建合成图像的算法,这些算法速度足够快,以支持用户与虚拟环境的交互(即实时渲染)。
    • 重点是三维渲染,并有限地涉及用户交互机制。
    • 建模、动画等对于实时应用同样重要,但超出了本书的讨论范围。
  • 读者基础与本书定位

    • 期望读者具备计算机图形学、计算机科学及编程的基础知识。
    • 本书关注算法本身,而非特定的API。
    • 若部分章节难以理解,建议快速浏览或查阅参考文献。
    • 本书最有价值的服务是帮助读者认识到知识的未知领域:一个想法的核心、他人已有的发现以及深入学习的途径。
  • 参考文献的使用方式

    • 尽可能引用相关材料,并在多数章节末尾提供进一步阅读和资源的总结。
    • 与前几版试图囊括所有相关信息不同,本版更像一本指南(guidebook)而非百科全书(encyclopedia),因为该领域已发展到无法详尽列出所有技术变种。
    • 通过描述少数代表性方案、用更新更广泛的综述替代原始来源,并依赖读者从引用文献中获取更多信息,以更好地服务读者。
  • 在线资源

    • 大多数参考文献可通过 realtimerendering.com 网站链接访问。
    • 鼓励读者花时间查看相关参考文献,即使只是对某个主题浅尝辄止,也能欣赏到其中精彩的图像。
    • 该网站还包含资源、教程、演示程序、代码示例、软件库、书籍校正等链接。
  • 作者的目标

    • 编写一本作者们初入行时希望拥有的书:既统一又有深度,包含入门书籍中找不到的细节和参考文献。
    • 希望这本书,以及作者们对这个领域的看法,能对读者的学习和工作有所裨益。

第一章 引言

  • 实时渲染 (Real-time rendering) 致力于在计算机上快速生成图像。

  • 它是计算机图形学中交互性最强的领域。图像显示在屏幕上,观看者行动或反应,这种反馈会影响接下来生成的内容。

  • 这种反应和渲染的循环以足够快的速率发生,使观看者不会感知到单个图像,而是沉浸在一个动态过程中。

  • 帧率 (Frames Per Second, FPS) 或赫兹 (Hertz, Hz):衡量图像显示速率的单位。

    • 1 FPS:几乎没有交互感;用户能明显感知到每张新图像的出现。
    • 约 6 FPS:开始产生交互感。
    • 视频游戏目标帧率:30, 60, 72 FPS 或更高;在这些速度下,用户专注于动作和反应。
  • 刷新率 (Refresh Rate) vs. 显示速率 (Display Rate)

    • 电影放映机以 24 FPS 显示帧,但使用快门系统将每帧显示两到四次以避免闪烁。此刷新率独立于显示速率,以赫兹 (Hz) 表示。例如,将帧照亮三次的快门具有 72 Hz 的刷新率。
    • LCD 显示器也将刷新率与显示速率分开。
    • 以 24 FPS 观看屏幕上的图像可能尚可接受,但更高的速率对于最小化响应时间至关重要。
    • 短至15毫秒的时间延迟就可能减慢并干扰交互。
    • 例如,用于虚拟现实的头戴式显示器通常需要 90 FPS 以最小化延迟。
  • 实时渲染不仅仅是交互性

    • 如果速度是唯一标准,任何能快速响应用户命令并在屏幕上绘制任何内容的应用程序都符合条件。
    • 实时渲染通常意味着生成三维图像。
  • 实时渲染定义的关键要素

    1. 交互性 (Interactivity)
    2. 三维空间 (three-dimensional space) 的某种联系。
    3. 图形加速硬件 (graphics acceleration hardware)
      • 许多人认为1996年3Dfx Voodoo 1卡的推出是消费级三维图形的真正开端。
      • 随着市场的飞速发展,现在每台计算机、平板电脑和手机都内置了图形处理器 (GPU)。
      • 图1.1 (Forza Motorsport 7) 和图1.2 (The Witcher 3) 展示了硬件加速实现的优秀实时渲染效果。
  • 图形硬件的进步推动了交互式计算机图形学研究的爆炸式增长。本书将专注于提供提高速度和改善图像质量的方法,同时描述加速算法和图形API的特性与局限性。

  • 本书目标:呈现关键概念和术语,解释领域中最稳健和实用的算法,并为获取更多信息提供指引。

1.1 内容概览

以下是各章节的简要概述:

  • 第2章 图形渲染管线 (The Graphics Rendering Pipeline):实时渲染的核心,描述了将场景描述转换成可见图像的一系列步骤。
  • 第3章 图形处理单元 (The Graphics Processing Unit):现代GPU如何使用固定功能和可编程单元组合来实现渲染管线的各个阶段。
  • 第4章 变换 (Transforms):操纵物体位置、方向、大小、形状以及相机位置和视图的基本工具。
  • 第5章 着色基础 (Shading Basics):讨论材质和光源的定义及其在实现所需表面外观(写实或风格化)中的应用。介绍其他与外观相关的议题,如通过反走样、透明度和伽马校正提高图像质量。
  • 第6章 纹理映射 (Texturing):实时渲染中最强大的工具之一,能够在表面上快速访问和显示图像。
  • 第7章 阴影 (Shadows):向场景中添加阴影可增强真实感和场景理解。介绍流行的快速计算阴影的算法。
  • 第8章 光与颜色 (Light and Color):在进行基于物理的渲染之前,需要理解如何量化光和颜色。物理渲染过程完成后,需要将结果量转换为显示值,同时考虑屏幕和观看环境的特性。
  • 第9章 基于物理的着色 (Physically Based Shading):从头开始构建对基于物理的着色模型的理解。从底层物理现象开始,涵盖各种渲染材质的模型,最后介绍材质混合与滤波方法,以避免走样并保持表面外观。
  • 第10章 局部光照 (Local Illumination):探索用于表现更复杂光源的算法。表面着色考虑到光是由具有特定形状的物理对象发出的。
  • 第11章 全局光照 (Global Illumination):模拟光与场景之间多次相互作用的算法,进一步提高图像的真实感。讨论环境光遮蔽、方向性遮蔽,以及漫反射和镜面反射表面的全局光照效果渲染方法,还有一些有前景的统一方法。
  • 第12章 图像空间效果 (Image-Space Effects):图形硬件擅长快速执行图像处理。首先讨论图像滤波和重投影技术,然后概述几种流行的后处理效果:镜头光晕、运动模糊和景深。
  • 第13章 超越多边形 (Beyond Polygons):三角形并非总是描述对象最快或最真实的方式。介绍基于图像、点云、体素和其他样本集的替代表示方法的各自优势。
  • 第14章 体积与半透明渲染 (Volumetric and Translucency Rendering):重点是体积材质表示的理论与实践及其与光源的相互作用。模拟的现象范围从大规模大气效应到细小毛发纤维内的光散射。
  • 第15章 非真实感渲染 (Non-Photorealistic Rendering):尝试使场景看起来逼真只是渲染方式的一种。概述其他风格,如卡通着色和水彩效果。还讨论线条和文本生成技术。
  • 第16章 多边形技术 (Polygonal Techniques):几何数据来源广泛,有时需要修改才能快速良好地渲染。介绍多边形数据表示和压缩的多个方面。
  • 第17章 曲线和曲面 (Curves and Curved Surfaces):更复杂的表面表示提供了诸多优势,如能够在质量和渲染速度之间进行权衡、更紧凑的表示以及平滑表面生成。
  • 第18章 管线优化 (Pipeline Optimization):应用程序运行并使用高效算法后,可通过各种优化技术使其更快。主题是找到瓶颈并决定如何处理。也讨论了多处理技术。
  • 第19章 加速算法 (Acceleration Algorithms):让它运行起来后,让它运行得更快。涵盖各种形式的剔除和细节层次渲染。
  • 第20章 高效着色 (Efficient Shading):场景中大量光源会显著降低性能。在片段已知可见之前对其进行完全着色是另一种浪费计算周期的来源。探讨解决这些及其他着色效率低下问题的多种方法。
  • 第21章 虚拟现实与增强现实 (Virtual and Augmented Reality):这些领域在以快速且一致的速率高效生成逼真图像方面面临特定的挑战和技术。
  • 第22章 相交测试方法 (Intersection Test Methods):相交测试对于渲染、用户交互和碰撞检测非常重要。此处深入介绍各种常见几何相交测试的最有效算法。
  • 第23章 图形硬件 (Graphics Hardware):重点是颜色深度、帧缓冲和基本架构类型等组件。提供代表性GPU的案例研究。
  • 第24章 未来 (The Future):作者们的一些展望。
  • 在线内容:由于空间限制,《碰撞检测》章节以及关于线性代数和三角学的附录可在 realtimerendering.com 免费下载。

1.2 符号和定义

首先,解释本书中使用的数学符号。

1.2.1 数学符号

表1.1总结了将使用的大部分数学符号。

  • 例外情况:主要存在于着色方程中,使用文献中已非常成熟的符号,例如:$L$ 表示辐射率 (radiance),$E$ 表示辐照度 (irradiance),$\sigma_s$ 表示散射系数 (scattering coefficient)。

  • 角度和标量:取自 $\mathbb{R}$ (实数)。

    • 角度 (angle):小写希腊字母,如 $\alpha_i, \phi, \rho, \eta, \gamma_{242}, \theta$。
    • 标量 (scalar):小写斜体字母,如 $a, b, t, u_k, v, w_{ij}$。
  • $$\mathbf{v} = \begin{pmatrix} v_x \\ v_y \\ v_z \end{pmatrix}$$
    • 有时为了易读性,使用 $(v_x, v_y, v_z)$ 而非更正式的 $(v_x \ v_y \ v_z)^T$。
  • 齐次坐标 (homogeneous notation):坐标用四个值表示 $\mathbf{v} = (v_x \ v_y \ v_z \ v_w)^T$。

    • 向量:$\mathbf{v} = (v_x \ v_y \ v_z \ 0)^T$。
    • 点:$\mathbf{p} = (v_x \ v_y \ v_z \ 1)^T$。
    • 有时也使用三维向量和点,会尽量避免歧义。
    • 对于矩阵操作,向量和点使用相同表示法非常有利。
    • 有时会用数字索引代替 $x, y, z$,例如 $\mathbf{v} = (v_0 \ v_1 \ v_2)^T$。
    • 这些规则也适用于二维向量(忽略三维向量的最后一个分量)。
  • 矩阵 (matrix):大写粗体字母,如 $\mathbf{T}(t), \mathbf{X}, \mathbf{R}_x(\rho)$。

    • 常用尺寸为 $2 \times 2, 3 \times 3, 4 \times 4$。
    • $3 \times 3$ 矩阵 $\mathbf{M}$ 的元素表示为 $m_{ij}$,$0 \le (i, j) \le 2$,$i$ 表示行,$j$ 表示列: $$\mathbf{M} = \begin{pmatrix} m_{00} & m_{01} & m_{02} \\ m_{10} & m_{11} & m_{12} \\ m_{20} & m_{21} & m_{22} \end{pmatrix} \quad (1.1)$$
    • 从矩阵 $\mathbf{M}$ 中分离向量的表示法(以 $3 \times 3$ 矩阵为例):$\mathbf{m}_{,j}$ 表示第 $j$ 列向量,$\mathbf{m}_{i,}$ 表示第 $i$ 行向量(以列向量形式写作 $\mathbf{m}_{i,}^T$)。列向量索引也可用 $x, y, z, w$: $$\mathbf{M} = (\mathbf{m}_{,0} \ \mathbf{m}_{,1} \ \mathbf{m}_{,2}) = (\mathbf{m}_x \ \mathbf{m}_y \ \mathbf{m}_z) = \begin{pmatrix} \mathbf{m}_{0,}^T \\ \mathbf{m}_{1,}^T \\ \mathbf{m}_{2,}^T \end{pmatrix} \quad (1.2)$$
  • 平面 (plane):$\pi: \mathbf{n} \cdot \mathbf{x} + d = 0$,包含其数学表达式,平面法向量 $\mathbf{n}$ 和标量 $d$。

    • 法向量描述平面朝向。对于曲面,法向量描述特定点的朝向。对于平面,所有点法向量相同。
    • 平面 $\pi$ 将空间分为正半空间 ($\mathbf{n} \cdot \mathbf{x} + d > 0$) 和负半空间 ($\mathbf{n} \cdot \mathbf{x} + d < 0$),其余点在平面上。
  • 三角形 (triangle):由三个点 $\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2$ 定义,记作 $\triangle \mathbf{v}_0 \mathbf{v}_1 \mathbf{v}_2$ 或 $\triangle \mathbf{cba}$。

  • 线段 (line segment):两点确定,如 $\mathbf{uv}, \mathbf{a}_i\mathbf{b}_j$。

  • 几何实体 (geometric entity):大写斜体字母,如 $AOBB, T, BAABB$。

  • 其他数学运算符 (表1.2)

    1. 点积 (dot product):$\cdot$
    2. 叉积 (cross product):$\times$
    3. 向量转置 (transpose of the vector v):$\mathbf{v}^T$。列向量可在文本中紧凑表示为 $\mathbf{v} = (v_x \ v_y \ v_z)^T$。
    4. 二维向量的垂直向量 (“perp dot product operator”):对于向量 $\mathbf{v} = (v_x \ v_y)^T$,其垂直向量为 $\mathbf{v}^{\perp} = (-v_y \ v_x)^T$。
    5. 矩阵行列式 (determinant of a matrix):$|\mathbf{A}|$。有时也用 $|\mathbf{A}| = |\mathbf{a} \ \mathbf{b} \ \mathbf{c}| = \det(\mathbf{a}, \mathbf{b}, \mathbf{c})$,其中 $\mathbf{a}, \mathbf{b}, \mathbf{c}$ 是矩阵 $\mathbf{A}$ 的列向量。
    6. 标量的绝对值 (absolute value of a scalar):$|a|$。
    7. 参数的长度或范数 (length (or norm) of argument):$\|\cdot\|$。
    8. 将 $x$ 限制到0以上 (clamping $x$ to 0):$x^+$ $$x^+ = \begin{cases} x, & \text{if } x > 0, \\ 0, & \text{otherwise.} \end{cases} \quad (1.3)$$
    9. 将 $x$ 限制在0和1之间 (clamping $x$ between 0 and 1):符号为 $x$ 上方带一个加号和一个横线 (原文为 $x$ with a plus sign and a horizontal bar on top of the plus)。 $$x_{\text{clamped to [0,1]}} = \begin{cases} 1, & \text{if } x \ge 1, \\ x, & \text{if } 0 < x < 1, \\ 0, & \text{otherwise.} \end{cases} \quad (1.4)$$
    10. 阶乘 (factorial):$n! = n(n-1)(n-2) \cdots 3 \cdot 2 \cdot 1$,且 $0! = 1$。 (1.5)
    11. 二项式系数 (binomial coefficients): $$\binom{n}{k} = \frac{n!}{k!(n-k)!} \quad (1.6)$$
  • 专用数学函数 (表1.3)

    1. atan2(y, x):双参数反正切函数。与 arctan(x) 的主要区别在于:$-\frac{\pi}{2} < \arctan(x) < \frac{\pi}{2}$,而 $0 \le \text{atan2}(y, x) < 2\pi$ (根据本书文本,实际实现可能不同),且 atan2 增加了额外参数以避免 arctan(y/x) 中 $x=0$ 时除零错误。
    2. log(n):表示自然对数 $\log_e(n)$,而非以10为底的对数 $\log_{10}(n)$。
  • 坐标系与颜色

    • 坐标平面 (coordinate planes) 或轴对齐平面 (axis-aligned planes):$x=0, y=0, z=0$。
    • 主轴 (main axes) 或主方向 (main directions):$\mathbf{e}_x = (1 \ 0 \ 0)^T, \mathbf{e}_y = (0 \ 1 \ 0)^T, \mathbf{e}_z = (0 \ 0 \ 1)^T$,分别称为x轴、y轴、z轴。这组轴常被称为标准基 (standard basis)。
    • 除非另有说明,使用标准正交基 (orthonormal bases)(由相互垂直的单位向量组成)。
    • 范围表示:$[a, b]$ 包含 $a,b$ 及其中间所有数;$(a, b)$ 不含 $a,b$;$[a, b)$ 包含 $a$ 不含 $b$。
    • 使用右手坐标系 (right-hand coordinate system)。
    • 颜色 (Colors):用三元素向量表示,如 (红, 绿, 蓝),每个元素范围为 $[0, 1]$。

1.2.2 几何定义

  • 基本渲染图元 (basic rendering primitives / drawing primitives):几乎所有图形硬件都使用点、线和三角形。
    • 例外:Pixel-Planes 可以绘制球体,NVIDIA NV1 芯片可以绘制椭球体。
  • 模型 (model) 或对象 (object):几何实体的集合。例如汽车、建筑,甚至一条线。
    • 实践中,对象通常由一组绘制图元组成,但也可能具有更高阶的几何表示,如贝塞尔曲线/曲面或细分曲面。
    • 对象也可以由其他对象组成,例如,汽车对象包含四个车门对象、四个车轮对象等。
  • 场景 (scene):构成待渲染环境中所有事物的模型的集合。场景还可以包括材质描述、光照和视图规格。

1.2.3 着色 (Shading)

  • 遵循计算机图形学中已确立的用法,本书中源自 “shading”、“shader” 及相关词汇的术语用于指代两个不同但相关的概念:
    1. 计算机生成的视觉外观 (例如,“着色模型 (shading model)”、“着色方程 (shading equation)”、“卡通着色 (toon shading)”)。
    2. 渲染系统的可编程组件 (例如,“顶点着色器 (vertex shader)”、“着色语言 (shading language)”)。
  • 在两种情况下,其预期含义应能从上下文中清晰理解。

延伸阅读与资源

  • 本书最重要的参考资源是其官方网站:realtimerendering.com
  • 该网站包含与每章相关的最新信息和网站链接。
  • 实时渲染领域正以“实时”的速度变化。书中试图关注基础概念和不易过时的技术。网站则有机会呈现与当今软件开发者相关的信息,并能保持更新。

第二章 图形渲染管线

“链条的强度取决于其最薄弱的环节。”

本章介绍了实时图形的核心组成部分——图形渲染管线,有时也简称为“管线”。管线的主要功能是给定虚拟相机、三维对象、光源等,生成或渲染一张二维图像。因此,渲染管线是实时渲染的基础工具。使用管线的过程如图2.1所示。图像中对象的位置和形状由其几何形状、环境特征以及相机在环境中的位置决定。对象的外观受材质属性、光源、纹理(应用于表面的图像)和着色方程的影响。

  • 图2.1说明:左图中,虚拟相机位于棱锥体的顶点(四条线汇聚处)。只有视体(view volume)内的图元会被渲染。对于透视渲染的图像(如此处所示),视体是一个平截头体(frustum),即一个底面为矩形的截断棱锥体。右图显示了相机“看到”的内容。注意,左图中的红色甜甜圈形状在右侧的渲染中没有出现,因为它位于视锥体之外。此外,左图中的蓝色扭曲棱柱被视锥体的顶面裁剪。

2.1 架构

物理世界中,管线概念以多种形式存在,从工厂装配线到快餐厨房。它同样适用于图形渲染。

  • 管线由若干阶段组成,每个阶段执行更大任务的一部分。
  • 管线阶段并行执行,每个阶段依赖于前一阶段的结果。
  • 理想情况下,一个非管线系统被划分为n个管线阶段后,可以获得n倍的加速。这种性能提升是使用管线的主要原因。
  • 示例:制作三明治的流水线——一人准备面包,一人加肉,一人加配料。每个人将结果传递给下一个人,并立即开始下一个三明治的工作。如果每人耗时20秒,则最大速率为每20秒一个三明治,即每分钟三个。
  • 管线阶段并行执行,但它们会一直停顿,直到最慢的阶段完成其任务。例如,如果加肉阶段更复杂,耗时30秒,那么能达到的最佳速率是每分钟两个三明治。此特定管线中,加肉阶段是瓶颈。
  • 实时渲染管线的粗略划分(如图2.2所示):
    • 应用阶段 (Application Stage)
    • 几何处理阶段 (Geometry Processing Stage)
    • 光栅化阶段 (Rasterization Stage)
    • 像素处理阶段 (Pixel Processing Stage)
  • 图2.2说明:渲染管线的基本构造,包括四个阶段:应用、几何处理、光栅化和像素处理。这些阶段中的每一个本身都可能是一个管线,如几何处理阶段下方所示;或者一个阶段可能(部分)并行化,如像素处理阶段下方所示。此图中,应用阶段是单个过程,但此阶段也可以是管线式或并行化的。注意,光栅化找到图元(例如三角形)内部的像素。
  • 这些阶段中的每一个通常本身就是一个管线,意味着它由几个子阶段组成。
  • 功能阶段(functional stages)与其实现结构有所区别。功能阶段有特定任务,但不指定任务在管线中的执行方式。
  • 渲染速度可以用每秒帧数 (FPS) 表示,即每秒渲染的图像数量。也可以用赫兹 (Hz) 表示,即1/秒,更新频率。还常用毫秒 (ms) 表示渲染一帧所需时间。
  • 应用阶段:由应用程序驱动,通常在通用CPU上以软件形式实现。CPU通常包含多个核心,能够并行处理多个执行线程。CPU负责的任务包括碰撞检测、全局加速算法、动画、物理模拟等。
  • 几何处理阶段:处理变换、投影和所有其他类型的几何操作。该阶段计算绘制什么、如何绘制以及在哪里绘制。通常在图形处理单元 (GPU) 上执行,GPU包含许多可编程核心以及固定功能硬件。
  • 光栅化阶段:通常输入三个顶点(构成一个三角形),并找到所有被认为在该三角形内部的像素,然后将这些像素转发到下一阶段。
  • 像素处理阶段:为每个像素执行一个程序以确定其颜色,并可能执行深度测试以判断其是否可见。它还可能执行逐像素操作,例如将新计算的颜色与先前颜色混合。光栅化和像素处理阶段也完全在GPU上处理。

2.2 应用阶段

开发者对应用阶段有完全控制权,因为它通常在CPU上执行。因此,开发者可以完全决定实现方式,并可以后续修改以提高性能。此处的更改也会影响后续阶段的性能。

  • 例如,应用阶段的算法或设置可以减少要渲染的三角形数量。
  • 部分应用工作可以通过GPU的计算着色器 (compute shader) 模式执行。此模式将GPU视为高度并行的通用处理器,忽略其专门用于渲染图形的特殊功能。
  • 应用阶段结束时,要渲染的几何图形被馈送到几何处理阶段。这些是渲染图元,即点、线和三角形,它们最终可能会显示在屏幕上。这是应用阶段最重要的任务。
  • 由于此阶段基于软件实现,它不像几何处理、光栅化和像素处理阶段那样划分为子阶段。但为提高性能,此阶段常在多个处理器核心上并行执行(超标量构造)。
  • 此阶段通常实现的过程:
    • 碰撞检测 (Collision Detection):检测到两个对象碰撞后,可能会生成响应。
    • 处理其他来源的输入:键盘、鼠标、头戴式显示器等。
    • 加速算法 (Acceleration Algorithms):如特定的剔除算法(第19章)。
    • 处理管线其余部分无法处理的其他任何事务。

2.3 几何处理

GPU上的几何处理阶段负责大部分逐三角形和逐顶点的操作。该阶段进一步划分为以下功能阶段(图2.3):顶点着色 (Vertex Shading)、投影 (Projection)、裁剪 (Clipping) 和屏幕映射 (Screen Mapping)。

  • 图2.3说明:几何处理阶段划分为一个功能阶段管线。

2.3.1 顶点着色 (Vertex Shading)

顶点着色的两个主要任务:

  1. 计算顶点的位置。
  2. 评估程序员希望作为顶点输出的任何数据,如法线和纹理坐标。
  • 传统上,对象的大部分阴影是通过将光照应用于每个顶点的位置和法线,并仅在顶点处存储结果颜色来计算的。然后这些颜色在三角形内插。因此,这个可编程顶点处理单元被称为顶点着色器 (vertex shader)
  • 随着现代GPU的出现,部分或全部着色发生在每像素阶段,顶点着色阶段变得更通用,可能根本不评估任何着色方程,具体取决于程序员的意图。顶点着色器现在是一个更通用的单元,专用于设置与每个顶点相关的数据。
  • 例如,顶点着色器可以使用第4.4和4.5节中的方法为对象制作动画。
  • 顶点位置计算
    • 模型最初位于其自身的模型空间 (model space)
    • 每个模型可关联一个模型变换 (model transform),以定位和定向。一个模型可有多个模型变换,允许同一模型的多个副本(称为实例)在场景中具有不同位置、方向和大小。
    • 模型的顶点和法线通过模型变换进行变换。对象的坐标称为模型坐标 (model coordinates)
    • 应用模型变换后,模型位于世界坐标 (world coordinates)世界空间 (world space)。世界空间是唯一的,所有模型变换后都存在于此同一空间中。
    • 只有相机(观察者)看到的模型才会被渲染。相机在世界空间中有位置和方向。
    • 为便于投影和裁剪,相机和所有模型都通过视图变换 (view transform) 进行变换。
    • 视图变换的目的是将相机置于原点,使其朝向负z轴方向,y轴朝上,x轴朝右(不同约定可能存在,如朝向+z轴)。
    • 变换后的空间称为相机空间 (camera space),或更常见的视图空间 (view space)观察空间 (eye space)。图2.4展示了视图变换如何影响相机和模型。
    • 模型变换和视图变换通常实现为4×4矩阵(第4章)。顶点的位置和法线可以按程序员喜欢的方式计算。
  • 顶点着色的第二类输出
    • 为了产生逼真的场景,不仅要渲染对象的形状和位置,还要模拟其外观。这包括每个对象的材质以及任何光源对对象的影响。
    • 这种确定光对材质影响的操作称为着色 (shading)。它涉及在对象的不同点计算着色方程。
    • 通常,一些计算在几何处理期间对模型的顶点执行,其他计算可能在逐像素处理期间执行。
    • 每个顶点可以存储各种材质数据,如点的位置、法线、颜色或评估着色方程所需的任何其他数值信息。
    • 顶点着色结果(可以是颜色、向量、纹理坐标以及任何其他类型的着色数据)随后被发送到光栅化和像素处理阶段,进行插值并用于计算表面着色。
  • 投影 (Projection)
    • 作为顶点着色的一部分,渲染系统执行投影,然后进行裁剪,这将视图体变换为一个单位立方体,其极值点在(-1, -1, -1)和(1, 1, 1)处(也可能使用其他范围,如 $0 \le z \le 1$)。该单位立方体称为规范视体 (canonical view volume)
    • 投影首先进行,在GPU上由顶点着色器完成。
    • 两种常用的投影方法:
      • 正交投影 (Orthographic Projection)(也称平行投影):如图2.5左侧。正交投影的视体通常是一个矩形框,正交投影将此视体变换为单位立方体。其主要特征是平行线在变换后仍保持平行。此变换是平移和缩放的组合。
      • 透视投影 (Perspective Projection):如图2.5右侧。在此类投影中,对象离相机越远,投影后显得越小。此外,平行线可能在地平线处汇聚。透视变换因此模仿了我们感知对象大小的方式。几何上,视体(称为平截头体/视锥体, frustum)是一个底面为矩形的截断棱锥体。平截头体也被变换为单位立方体。
    • 正交和透视变换都可以用4×4矩阵构建(第4章)。
    • 任一变换后,模型被称为处于裁剪坐标 (clip coordinates)。这些实际上是齐次坐标(第4章讨论),因此这发生在除以w之前。GPU的顶点着色器必须始终输出这种类型的坐标,以便下一功能阶段(裁剪)正确工作。
    • 虽然这些矩阵将一个体积变换为另一个体积,但它们被称为投影,因为显示后,z坐标不存储在生成的图像中,而是存储在z缓冲(第2.5节描述)中。这样,模型就从三维投影到了二维。

2.3.2 可选顶点处理 (Optional Vertex Processing)

在完成上述顶点处理后,GPU上可以按顺序进行一些可选阶段:曲面细分 (tessellation)、几何着色 (geometry shading) 和流输出 (stream output)。它们的使用取决于硬件能力和程序员需求,通常不常用。

  • 曲面细分 (Tessellation)
    • 解决问题:固定数量的三角形难以平衡质量和性能(例如,弹跳的球近看粗糙,远看浪费性能)。
    • 作用:可以为曲面生成适当数量的三角形。
    • 基于面片 (patches) 定义曲面,每个面片由一组顶点构成。
    • 曲面细分阶段本身包含一系列子阶段:外壳着色器 (hull shader)、细分器 (tessellator) 和域着色器 (domain shader)。它们将这些面片顶点集转换为(通常)更大的顶点集,然后用于制作新的三角形集。
    • 可以根据相机距离决定生成多少三角形:近处多,远处少。
  • 几何着色器 (Geometry Shader)
    • 出现早于曲面细分着色器,因此在GPU上更常见。
    • 与曲面细分着色器类似,它接收各种类型的图元并可以产生新的顶点。
    • 更简单,创建范围有限,输出图元类型也更受限。
    • 用途之一:粒子生成。例如,烟花爆炸中的每个火球可以表示为一个点(单个顶点),几何着色器可以将每个点变成一个面向观察者的正方形(由两个三角形组成),覆盖多个像素,提供更具说服力的图元进行着色。
  • 流输出 (Stream Output)
    • 使GPU能作为几何引擎。
    • 可以选择将处理后的顶点输出到一个数组以供进一步处理,而不是发送到管线的其余部分渲染到屏幕。
    • 这些数据可供CPU或GPU本身在后续处理遍(pass)中使用。
    • 通常用于粒子模拟,如烟花示例。
  • 这三个阶段按此顺序执行——曲面细分、几何着色、流输出——每个都是可选的。无论使用哪个(或不使用),如果继续沿管线向下,我们都会得到一组具有齐次坐标的顶点,这些顶点将被检查相机是否能看到它们。

2.3.3 裁剪 (Clipping)

只有完全或部分在视体内的图元需要传递到光栅化阶段(以及后续的像素处理阶段)。

  • 完全在视体内的图元将按原样传递到下一阶段。
  • 完全在视体外的图元不会进一步传递,因为它们不被渲染。
  • 部分在视体内的图元需要裁剪。例如,一条线段一个顶点在内一个顶点在外,则应根据视体进行裁剪,使外部顶点被替换为线段与视体边界相交处的新顶点。
  • 使用投影矩阵意味着变换后的图元是相对单位立方体进行裁剪的。在裁剪前进行视图变换和投影的优点是使裁剪问题保持一致:图元总是相对单位立方体进行裁剪。图2.6描绘了裁剪过程。
  • 除了视体的六个裁剪平面外,用户还可以定义额外的裁剪平面以可视地切割对象(称为剖切, sectioning)。
  • 裁剪步骤使用投影产生的4值齐次坐标执行裁剪。在透视空间中,值通常不会跨三角形线性插值。需要第四个坐标(w),以便在使用透视投影时正确插值和裁剪数据。
  • 最后,执行透视除法 (perspective division),将结果三角形的位置置于三维归一化设备坐标 (Normalized Device Coordinates, NDC) 中。如前所述,此视体范围从(-1, -1, -1)到(1, 1, 1)。
  • 几何阶段的最后一步是将此空间转换为窗口坐标。

2.3.4 屏幕映射 (Screen Mapping)

只有视体内的(裁剪后的)图元会传递到屏幕映射阶段,进入此阶段时坐标仍是三维的 (NDC)。

  • 每个图元的x和y坐标被变换以形成屏幕坐标 (screen coordinates)
  • 屏幕坐标连同z坐标也称为窗口坐标 (window coordinates)
  • 假设场景应渲染到一个窗口,其最小角点为 $(x_1, y_1)$,最大角点为 $(x_2, y_2)$,其中 $x_1 < x_2$ 且 $y_1 < y_2$。
  • 屏幕映射是一个平移操作后跟一个缩放操作。新的x和y坐标称为屏幕坐标。
  • z坐标(OpenGL为$[-1, +1]$,DirectX为$[0, 1]$)也被映射到 $[z_1, z_2]$,默认值为 $z_1 = 0$ 和 $z_2 = 1$。这些可以通过API更改。
  • 窗口坐标以及这个重新映射的z值被传递到光栅化器阶段。屏幕映射过程如图2.7所示。
  • 像素和浮点坐标的关系
    • 对于水平像素阵列,使用笛卡尔坐标,最左侧像素的左边缘是浮点坐标0.0。OpenGL一直使用此方案,DirectX 10及其后续版本也使用它。
    • 此像素的中心在0.5处。
    • 因此,像素范围[0, 9]覆盖的浮点范围是[0.0, 10.0)。
    • 转换公式:
      • $d = \text{floor}(c)$ (2.1) (c是连续浮点值,d是离散整数索引)
      • $c = d + 0.5$ (2.2) (d是像素的离散索引,c是像素内的连续浮点值)
    • API差异:
      • 所有API的像素位置值都是从左到右增加。
      • 顶部和底部边缘的零点位置在OpenGL和DirectX之间有时不一致。
      • OpenGL倾向于始终使用笛卡尔坐标系,将左下角视为值最低的元素。
      • DirectX有时根据上下文将左上角定义为此元素。例如,图像的(0,0)在OpenGL中是左下角,在DirectX中是左上角。

2.4 光栅化 (Rasterization)

给定经过变换和投影的顶点及其相关的着色数据(全部来自几何处理),下一阶段的目标是找到所有在被渲染图元(例如三角形)内部的像素(picture elements的缩写)。

  • 这个过程称为光栅化,它分为两个功能子阶段(图2.8左侧):三角形设置 (Triangle Setup)(也称图元组装, Primitive Assembly)和三角形遍历 (Triangle Traversal)。这些阶段也能处理点和线。
  • 光栅化,也称扫描转换 (scan conversion),是将屏幕空间中的二维顶点(每个顶点都有一个z值/深度值和各种相关的着色信息)转换为屏幕上的像素的过程。
  • 光栅化也可以被认为是几何处理和像素处理之间的同步点,因为正是在这里,由三个顶点形成的三角形最终被发送到像素处理。
  • 像素是否被认为与三角形重叠取决于GPU管线的设置:
    • 点采样 (Point Sampling):最简单的情况是在每个像素中心使用单个采样点,如果中心点在三角形内,则相应像素被认为在三角形内。
    • 可以使用每个像素多于一个样本的超级采样 (Supersampling)多重采样抗锯齿 (Multisampling Antialiasing, MSAA) 技术(第5.4.2节)。
    • 保守光栅化 (Conservative Rasterization):如果像素的至少一部分与三角形重叠,则该像素“在”三角形内(第23.1.2节)。

2.4.1 三角形设置 (Triangle Setup)

在此阶段,计算三角形的微分、边方程和其他数据。这些数据可用于三角形遍历(第2.4.2节),以及插值由几何阶段产生的各种着色数据。此任务使用固定功能硬件。

2.4.2 三角形遍历 (Triangle Traversal)

在这里,检查其中心(或样本)被三角形覆盖的每个像素,并为与三角形重叠的像素部分生成一个片元 (fragment)

  • 更精细的采样方法见第5.4节。
  • 找到哪些样本或像素在三角形内部通常称为三角形遍历。
  • 每个三角形片元的属性是通过在三个三角形顶点之间插值数据生成的(第5章)。这些属性包括片元的深度以及来自几何阶段的任何着色数据。
  • 在此处执行三角形上的透视校正插值 (perspective-correct interpolation)(第23.1.1节)。
  • 所有在图元内部的像素或样本随后被发送到下一阶段——像素处理阶段。

2.5 像素处理 (Pixel Processing)

此时,通过前面所有阶段的组合,所有被认为在三角形或其他图元内部的像素都已被找到。像素处理阶段分为像素着色 (Pixel Shading)合并 (Merging)(图2.8右侧)。像素处理是在位于图元内部的像素或样本上执行逐像素或逐样本计算和操作的阶段。

2.5.1 像素着色 (Pixel Shading)

任何逐像素的着色计算都在这里执行,使用插值的着色数据作为输入。最终结果是一个或多个颜色,传递到下一阶段。

  • 与通常由专用硬连线硅片执行的三角形设置和遍历阶段不同,像素着色阶段由可编程GPU核心执行。
  • 为此,程序员为像素着色器 (pixel shader)(在OpenGL中称为片元着色器, fragment shader)提供一个程序,该程序可以包含任何期望的计算。
  • 这里可以使用多种技术,其中最重要的一种是纹理映射 (texturing)(第6章详细介绍)。简单地说,对对象进行纹理映射意味着将一个或多个图像“粘贴”到该对象上,以达到各种目的。图2.9描绘了这个过程的一个简单示例(龙模型,纹理图像的各个部分“粘贴”到龙身上)。图像可以是一维、二维或三维的,二维图像最常见。
  • 最简单的情况下,最终产物是每个片元的颜色值,这些值被传递到下一个子阶段。

2.5.2 合并 (Merging)

每个像素的信息存储在颜色缓冲 (color buffer) 中,这是一个颜色的矩形数组(每种颜色都有红、绿、蓝三个分量)。

  • 合并阶段负责将像素着色阶段产生的片元颜色与当前存储在缓冲中的颜色相结合。
  • 此阶段也称为ROP,代表“光栅操作管线 (raster operations pipeline)”或“渲染输出单元 (render output unit)”,取决于提问者。
  • 与着色阶段不同,执行此阶段的GPU子单元通常不是完全可编程的,但是高度可配置的,可以实现各种效果。
  • 此阶段还负责可见性解析 (resolving visibility)。这意味着当整个场景渲染完毕后,颜色缓冲应包含场景中从相机视角可见的图元的颜色。
  • 对于大多数甚至所有图形硬件,这是通过 z缓冲 (z-buffer)(也称深度缓冲, depth buffer)算法完成的。
    • z缓冲的大小和形状与颜色缓冲相同,它为每个像素存储到当前最近图元的z值。
    • 当一个图元被渲染到某个像素时,计算该图元在该像素处的z值,并与z缓冲中同一像素的内容进行比较。
    • 如果新的z值小于z缓冲中的z值,则正在渲染的图元比先前在该像素处最接近相机的图元更近。因此,该像素的z值和颜色将用正在绘制的图元的z值和颜色更新。
    • 如果计算出的z值大于z缓冲中的z值,则颜色缓冲和z缓冲保持不变。
    • z缓冲算法简单,具有O(n)收敛性(n是要渲染的图元数量),并且适用于任何可以为每个(相关)像素计算z值的绘图图元。
    • 此算法允许大多数图元以任何顺序渲染,这是其流行的另一个原因。
    • 但是,z缓冲在屏幕上的每个点只存储一个深度,因此不能用于部分透明 (partially transparent) 的图元。这些图元必须在所有不透明图元之后,并按从后到前的顺序渲染,或使用独立的顺序无关算法(第5.5节)。透明度是基本z缓冲的主要弱点之一。
  • 其他通道和缓冲
    • Alpha通道 (alpha channel):与颜色缓冲相关联,为每个像素存储相关的不透明度值(第5.5节)。在旧API中,Alpha通道也用于通过Alpha测试功能选择性地丢弃像素。如今,可以在像素着色器程序中插入丢弃操作,并可以使用任何类型的计算来触发丢弃。这种类型的测试可用于确保完全透明的片元不影响z缓冲(第6.6节)。
    • 模板缓冲 (stencil buffer):一个离屏缓冲,用于记录渲染图元的位置。它通常每个像素包含8位。可以使用各种函数将图元渲染到模板缓冲中,然后可以使用该缓冲的内容来控制到颜色缓冲和z缓冲的渲染。例如,假设已将填充的圆形绘制到模板缓冲中。这可以与一个操作符结合,该操作符仅允许在圆形存在的地方将后续图元渲染到颜色缓冲中。模板缓冲是生成某些特殊效果的强大工具。
  • 管线末端的这些功能称为光栅操作 (raster operations, ROP) 或混合操作 (blend operations)。可以将当前颜色缓冲中的颜色与正在处理的三角形内部像素的颜色混合。这可以实现诸如透明度或颜色样本累积之类的效果。如前所述,混合通常使用API进行配置,而不是完全可编程的。但是,某些API支持光栅顺序视图 (raster order views),也称为像素着色器排序 (pixel shader ordering),可实现可编程混合功能。
  • 帧缓冲 (framebuffer) 通常由系统上的所有缓冲组成。
  • 为避免让观看者看到图元在光栅化并发送到屏幕的过程,使用了双缓冲 (double buffering)。这意味着场景的渲染在离屏的后置缓冲 (back buffer) 中进行。一旦场景在后置缓冲中渲染完毕,后置缓冲的内容将与先前显示在屏幕上的前置缓冲 (front buffer) 的内容进行交换。交换通常在垂直回扫 (vertical retrace) 期间发生。

2.6 穿越管线 (Through the Pipeline)

点、线和三角形是构建模型或对象的渲染图元。假设应用程序是一个交互式计算机辅助设计 (CAD) 应用程序,用户正在检查一个华夫饼机的设计。我们将跟随这个模型完整地走过图形渲染管线,包括四个主要阶段:应用、几何、光栅化和像素处理。场景以透视方式渲染到屏幕上的一个窗口中。在这个简单示例中,华夫饼机模型既包含线条(显示零件边缘)也包含三角形(显示表面)。华夫饼机有一个可以打开的盖子。一些三角形通过带有制造商徽标的二维图像进行纹理映射。在此示例中,表面着色完全在几何阶段计算,除了纹理的应用,它发生在光栅化阶段。

  • 应用阶段 (Application)
    • CAD应用程序允许用户选择和移动模型的部件。例如,用户可能选择盖子,然后移动鼠标打开它。应用阶段必须将鼠标移动转换为相应的旋转矩阵,然后确保在渲染盖子时正确应用此矩阵。
    • 另一个例子:播放一个动画,使相机沿预定义路径移动,以从不同视角显示华夫饼机。然后,应用阶段必须根据时间更新相机参数,如位置和观察方向。
    • 对于要渲染的每一帧,应用阶段将相机位置、光照和模型的图元馈送到管线的下一个主要阶段——几何阶段。
  • 几何处理阶段 (Geometry Processing)
    • 对于透视观察,我们假设应用程序已提供投影矩阵。此外,对于每个对象,应用程序已计算出一个矩阵,该矩阵描述了视图变换以及对象本身的位置和方向。在我们的示例中,华夫饼机的底座会有一个矩阵,盖子会有另一个。
    • 在几何阶段,对象的顶点和法线通过此矩阵进行变换,将对象置于视图空间中。
    • 然后,可以使用材质和光源属性在顶点处计算着色或其他计算。
    • 然后使用用户提供的单独投影矩阵执行投影,将对象变换到表示眼睛所见的单位立方体空间中。
    • 所有在该立方体外部的图元都被丢弃。所有与此单位立方体相交的图元都相对于该立方体进行裁剪,以获得一组完全位于单位立方体内部的图元。
    • 然后,顶点被映射到屏幕上的窗口中。
    • 在所有这些逐三角形和逐顶点的操作执行完毕后,结果数据被传递到光栅化阶段。
  • 光栅化阶段 (Rasterization)
    • 在上一阶段裁剪后存活的所有图元随后被光栅化,这意味着找到所有在图元内部的像素,并将其进一步向下发送到像素处理阶段。
  • 像素处理阶段 (Pixel Processing)
    • 此处的目的是计算每个可见图元的每个像素的颜色。
    • 那些已与任何纹理(图像)关联的三角形将按需应用这些图像进行渲染。
    • 通过z缓冲算法以及可选的丢弃和模板测试来解析可见性。
    • 每个对象依次处理,最终图像然后显示在屏幕上。

结论 (Conclusion)

  • 该管线是数十年来针对实时渲染应用的API和图形硬件演进的结果。
  • 值得注意的是,这并非唯一可能的渲染管线;离线渲染管线经历了不同的演进路径。电影制作的渲染通常使用微多边形管线 (micropolygon pipelines),但最近光线追踪 (ray tracing) 和路径追踪 (path tracing) 已占据主导地位。这些技术(在第11.2.2节中介绍)也可用于建筑和设计预可视化。
  • 多年来,应用程序开发者使用此处描述过程的唯一方式是通过图形API定义的固定功能管线 (fixed-function pipeline)。固定功能管线因此得名,因为实现它的图形硬件包含无法灵活编程的元件。最后一个主要的固定功能机器的例子是任天堂于2006年推出的Wii。
  • 另一方面,可编程GPU (Programmable GPUs) 使得可以精确确定在整个管线的各个子阶段中应用哪些操作。本书第四版假设所有开发都使用可编程GPU完成。

进一步阅读和资源 (Further Reading and Resources)

  • Blinn的书《A Trip Down the Graphics Pipeline》是一本较早的关于从头开始编写软件渲染器的书。它是学习实现渲染管线的一些微妙之处的好资源,解释了裁剪和透视插值等关键算法。
  • 历史悠久(但经常更新)的《OpenGL Programming Guide》(又名“红宝书”)对图形管线及其相关算法进行了详尽的描述。
  • 本书的网站 realtimerendering.com 提供了各种管线图、渲染引擎实现等链接。

第三章 图形处理单元

“显示器就是计算机。” ——黄仁勋

历史上,图形加速始于在覆盖三角形的每个像素扫描线上插值颜色然后显示这些值。包含访问图像数据的能力使得纹理可以应用于表面。为插值和测试z深度添加硬件提供了内置的可见性检查。由于它们频繁使用,这些过程被提交给专用硬件以提高性能。在后续的几代产品中,渲染管线的更多部分以及每个部分的更多功能被添加进来。专用图形硬件相对于CPU的唯一计算优势是速度,但速度至关重要。

在过去的二十年中,图形硬件经历了令人难以置信的转变。第一个包含硬件顶点处理的消费级图形芯片(NVIDIA的GeForce256)于1999年出货。NVIDIA创造了术语图形处理单元(GPU)以区分GeForce 256与之前仅支持光栅化的芯片,这个术语沿用至今。在接下来的几年里,GPU从一个复杂固定功能管线的可配置实现演变为高度可编程的白板,开发者可以在上面实现自己的算法。各种可编程着色器是控制GPU的主要方式。为了效率,管线的某些部分仍然是可配置的,而不是可编程的,但趋势是向可编程性和灵活性发展[175]。

GPU通过专注于一组狭窄且高度可并行的任务来获得其高速。它们拥有专用的硅片来实现z缓冲、快速访问纹理图像和其他缓冲区,以及例如查找哪些像素被三角形覆盖。这些元素如何执行其功能将在第23章中介绍。更重要的是要早期了解GPU如何为其可编程着色器实现并行性。

第3.3节解释了着色器如何工作。目前,你需要知道的是,着色器核心是一个小型处理器,它执行一些相对独立的任务,例如将顶点从其在世界中的位置转换到屏幕坐标,或者计算被三角形覆盖的像素的颜色。由于每帧每秒有数千或数百万个三角形被发送到屏幕,因此每秒可能有数十亿次着色器调用,即着色器程序运行的独立实例。

首先,延迟是所有处理器都面临的问题。访问数据需要一定的时间。思考延迟的一个基本方法是,信息离处理器越远,等待时间就越长。第23.3节更详细地讨论了延迟。存储在内存芯片中的信息比存储在本地寄存器中的信息需要更长的访问时间。第18.4.1节更深入地讨论了内存访问。关键点是等待数据检索意味着处理器停滞,这会降低性能。

3.1 数据并行架构

  • 不同的处理器架构使用各种策略来避免停顿。
    • CPU优化用于处理各种数据结构和大型代码库。CPU可以有多个处理器,但每个处理器主要以串行方式运行代码,受限的SIMD向量处理是次要例外。
    • 为了最小化延迟的影响,CPU芯片的很大一部分由快速本地缓存组成,这些内存中填充了接下来可能需要的数据。
    • CPU还通过使用诸如分支预测、指令重排、寄存器重命名和缓存预取等巧妙技术来避免停顿[715]。
  • GPU采用不同的方法。
    • GPU芯片的大部分区域专用于大量的处理器,称为着色器核心,通常数以千计。
    • GPU是一个流处理器,其中有序的相似数据集依次处理。
    • 由于这种相似性(例如,一组顶点或像素),GPU可以以大规模并行的方式处理这些数据。
    • 另一个重要因素是这些调用尽可能独立,这样它们就不需要来自相邻调用的信息,也不共享可写内存位置。这条规则有时会被打破以允许新的有用功能,但这种例外会带来潜在延迟的代价,因为一个处理器可能等待另一个处理器完成其工作。
  • GPU针对吞吐量进行了优化,吞吐量定义为可以处理数据的最大速率。
    • 然而,这种快速处理是有代价的。由于较少的芯片面积专用于缓存和控制逻辑,每个着色器核心的延迟通常远高于CPU处理器遇到的延迟[462]。
  • 延迟隐藏举例
    • 假设一个网格被光栅化,有两千个像素有片段需要处理;一个像素着色器程序将被调用两千次。
    • 想象只有一个着色器处理器(世界上最弱的GPU)。它开始为两千个片段中的第一个执行着色器程序。
    • 着色器处理器对寄存器中的值执行一些算术运算。寄存器是本地的,访问速度快,因此不会发生停顿。
    • 然后着色器处理器遇到一条指令,如纹理访问;例如,对于给定的表面位置,程序需要知道应用于网格的图像的像素颜色。纹理是一个完全独立的资源,不是像素程序本地内存的一部分,纹理访问可能有些复杂。内存提取可能需要数百到数千个时钟周期,在此期间GPU处理器什么也不做。此时,着色器处理器将停顿,等待纹理的颜色值返回。
    • 为了使这个糟糕的GPU变得更好,给每个片段一点存储空间用于其本地寄存器。现在,着色器处理器不再在纹理提取时停顿,而是被允许切换并执行另一个片段,即两千个片段中的第二个。这种切换非常快,第一个或第二个片段中的任何内容都不会受到影响,除了记录第一个片段正在执行哪个指令。现在执行第二个片段。与第一个片段相同,执行一些算术函数,然后再次遇到纹理提取。着色器核心现在切换到另一个片段,即第三个。最终所有两千个片段都以这种方式处理。此时着色器处理器返回到第一个片段。到这个时候,纹理颜色已经被提取并可供使用,因此着色器程序可以继续执行。处理器以相同的方式进行,直到遇到另一个已知会暂停执行的指令,或者程序完成。
    • 单个片段的执行时间会比着色器处理器专注于它时更长,但片段整体的执行时间会大大减少。
  • 在这种架构中,延迟通过让GPU通过切换到另一个片段来保持繁忙而得以隐藏。
  • SIMD(单指令,多数据):
    • GPU通过将指令执行逻辑与数据分离,进一步发展了这种设计。
    • 这种安排在固定数量的着色器程序上以锁步方式执行相同的命令。
    • SIMD的优势在于,与使用单独的逻辑和分派单元来运行每个程序相比,处理数据和切换所需的硅片(和功率)要少得多。
  • 现代GPU术语和Warp执行:
    • 每个片段的像素着色器调用称为一个线程 (thread)。它包含着色器输入值的一些内存,以及着色器执行所需的任何寄存器空间。
    • 使用相同着色器程序的线程被捆绑成组,NVIDIA称之为Warp,AMD称之为Wavefront。
    • 一个Warp/Wavefront由一定数量的GPU着色器核心(8到64个)使用SIMD处理方式进行调度执行。每个线程映射到一个SIMD通道。
    • 假设有2000个线程要执行。NVIDIA GPU上的Warp包含32个线程。这会产生 $2000/32 = 62.5$ 个Warp,意味着分配了63个Warp,其中一个Warp是半空的。
    • Warp的执行类似于我们单个GPU处理器的例子。着色器程序在所有32个处理器上锁步执行。
    • 当遇到内存提取时,所有线程同时遇到它,因为对所有线程执行相同的指令。提取信号表明该Warp的线程将停顿,所有线程都在等待它们(不同的)结果。
    • Warp不会停顿,而是被换出,换入一个不同的包含32个线程的Warp,然后由32个核心执行。这种交换与我们单个处理器系统一样快,因为在Warp换入或换出时,每个线程内的数据都不会被触动。每个线程都有自己的寄存器,每个Warp都跟踪它正在执行哪个指令。换入一个新的Warp只是将核心集指向一组不同的线程来执行;没有其他开销。Warp执行或换出,直到所有Warp都完成。 (参见图3.1)
  • 延迟隐藏机制:
    • Warp交换是所有GPU使用的主要延迟隐藏机制。
    • 实际中,由于交换成本极低,Warp可能会因为更短的延迟而被换出。
  • 影响效率的因素:
    • 线程数量:如果线程很少,那么只能创建很少的Warp,使得延迟隐藏出现问题。
    • 着色器程序结构:
      • 每个线程的寄存器使用量:着色器程序每个线程所需的寄存器越多,GPU上能驻留的线程就越少,因此Warp也越少。Warp短缺可能意味着停顿无法通过交换来缓解。
      • 占用率 (Occupancy):驻留的Warp被称为“飞行中”(in flight),这个数量称为占用率。高占用率意味着有许多Warp可供处理,因此处理器空闲的可能性较小。低占用率通常会导致性能不佳。
      • 内存提取频率:也会影响需要多少延迟隐藏。Lauritzen [993]概述了占用率如何受寄存器数量和着色器使用的共享内存影响。Wronski [1911, 1914]讨论了理想占用率如何根据着色器执行的操作类型而变化。
    • 动态分支 (Dynamic branching):由“if”语句和循环引起。
      • 如果一个Warp中的所有线程都评估并采用相同的分支,Warp可以继续执行而无需担心其他分支。
      • 但是,如果某些线程(甚至一个线程)采用备用路径,则Warp必须执行两个分支,并丢弃每个特定线程不需要的结果[530, 945]。这个问题称为线程分化 (thread divergence),少数线程可能需要执行循环迭代或执行“if”路径,而Warp中的其他线程则不需要,导致它们在此期间空闲。
  • 所有GPU都实现了这些架构思想,从而产生了具有严格限制但每瓦计算能力巨大的系统。

3.2 GPU管线概述

  • GPU实现了第二章中描述的概念上的几何处理、光栅化和像素处理管线阶段。
  • 这些阶段被划分为几个硬件阶段,具有不同程度的可配置性或可编程性。图3.2根据可编程或可配置程度对各个阶段进行了颜色编码。
    • 绿色阶段:完全可编程。
    • 黄色阶段:可配置但不可编程(例如,合并阶段可以设置各种混合模式)。
    • 蓝色阶段:功能完全固定。
    • 虚线:表示可选阶段。
  • 注意这些物理阶段的划分与第二章中介绍的功能阶段有所不同。
  • 逻辑模型 vs. 物理模型:
    • 这里描述的是GPU的逻辑模型,即API向程序员展示的模型。
    • 物理模型(硬件供应商的实现)可能有所不同。逻辑模型中固定功能的阶段可能通过向相邻可编程阶段添加命令在GPU上执行。管线中的单个程序可能被拆分为由不同子单元执行的元素,或者完全通过单独的通道执行。
    • 逻辑模型可以帮助你推断影响性能的因素,但不应将其误认为是GPU实际实现管线的方式。
  • 管线阶段:
    • 顶点着色器 (Vertex Shader):完全可编程,用于实现几何处理阶段。
    • 几何着色器 (Geometry Shader):完全可编程,对图元(点、线或三角形)的顶点进行操作。可用于执行逐图元着色操作、销毁图元或创建新图元。
    • 曲面细分阶段 (Tessellation Stage) 和几何着色器都是可选的,并非所有GPU都支持它们,尤其是在移动设备上。
    • 裁剪 (Clipping)、三角形设置 (Triangle Setup) 和三角形遍历 (Triangle Traversal) 阶段由固定功能硬件实现。
    • 屏幕映射 (Screen Mapping) 受窗口和视口设置影响,内部形成简单的缩放和重新定位。
    • 像素着色器 (Pixel Shader):完全可编程。
    • 合并阶段 (Merger Stage):虽然不可编程,但高度可配置,可以设置为执行各种操作。它实现“合并”功能阶段,负责修改颜色、z缓冲、混合、模板以及任何其他与输出相关的缓冲区。
    • 像素着色器执行与合并阶段一起构成了第二章中介绍的概念性像素处理阶段。
  • GPU管线随着时间的推移,已经从硬编码操作向着日益增强的灵活性和控制力发展。引入可编程着色器阶段是这一演进中最重要的一步。

3.3 可编程着色器阶段

  • 统一着色器设计 (Unified Shader Design):
    • 现代着色器程序使用统一着色器设计。这意味着顶点、像素、几何和曲面细分相关的着色器共享一个共同的编程模型。
    • 内部它们具有相同的指令集架构 (ISA)。实现此模型的处理器在DirectX中称为通用着色器核心 (common-shader core),具有此类核心的GPU被称为具有统一着色器架构。
    • 这种架构背后的思想是着色器处理器可用于多种角色,GPU可以根据需要分配这些处理器。例如,一组具有微小三角形的网格比由两个三角形组成的巨大正方形需要更多的顶点着色器处理。具有独立顶点和像素着色器核心池的GPU意味着保持所有核心繁忙的理想工作分配是严格预先确定的。有了统一的着色器核心,GPU可以决定如何平衡此负载。
  • 着色器编程语言:
    • 着色器使用类似C的着色语言编程,如DirectX的High-Level Shading Language (HLSL) 和OpenGL Shading Language (GLSL)。
    • DirectX的HLSL可以编译为虚拟机字节码,也称为中间语言 (IL 或 DXIL),以提供硬件无关性。中间表示还可以允许着色器程序离线编译和存储。该中间语言由驱动程序转换为特定GPU的ISA。控制台编程通常避免中间语言步骤,因为系统只有一个ISA。
  • 数据类型:
    • 基本数据类型是32位单精度浮点标量和向量(尽管向量只是着色器代码的一部分,并且如上所述硬件不支持)。
    • 现代GPU上本地也支持32位整数和64位浮点数。
    • 浮点向量通常包含诸如位置 ($xyzw$)、法线、矩阵行、颜色 ($rgba$) 或纹理坐标 ($uvwq$) 之类的数据。
    • 整数最常用于表示计数器、索引或位掩码。
    • 还支持聚合数据类型,如结构体、数组和矩阵。
  • 输入与输出:
    • 一次绘制调用 (draw call) 会调用图形API来绘制一组图元,从而导致图形管线执行并运行其着色器。
    • 每个可编程着色器阶段有两种类型的输入:
      • 统一输入 (uniform inputs):其值在整个绘制调用期间保持不变(但可以在绘制调用之间更改)。
      • 可变输入 (varying inputs):来自三角形顶点或光栅化的数据。
      • 例如,像素着色器可以提供光源颜色作为统一值,而三角形表面位置每个像素都不同,因此是可变的。
      • 纹理 (texture) 是一种特殊的统一输入,曾经总是一个应用于表面的彩色图像,但现在可以被认为是任何大型数据数组。
  • 虚拟机寄存器 (Shader Model 4.0, 图3.3):
    • 底层虚拟机为不同类型的输入和输出提供特殊寄存器。
    • 可用于统一输入的常量寄存器数量远大于可用于可变输入或输出的寄存器数量。这是因为可变输入和输出需要为每个顶点或像素单独存储,因此需要多少数量是有限的。统一输入存储一次,并在绘制调用中的所有顶点或像素中重复使用。
    • 虚拟机还有通用临时寄存器,用作暂存空间。
    • 所有类型的寄存器都可以使用临时寄存器中的整数值进行数组索引。
    • 着色器虚拟机输入输出(Shader Model 4.0下,括号内为顶点/几何/像素着色器的最大可用数):
      • 可变输入寄存器 (Varying Input Registers): 16 / 16 / 32 个寄存器
      • 输出寄存器 (Output Registers): 16 / 32 / 8 个寄存器
      • 常量寄存器 (Constant Registers): 16个缓冲区,每个4096个寄存器
      • 临时寄存器 (Temporary Registers): 4096 个寄存器
      • 纹理 (Textures): 128个数组,每个512个纹理
  • 操作:
    • 图形计算中常见的操作在现代GPU上高效执行。
    • 着色语言通过运算符(如 *+)公开最常见的这些操作。
    • 其余的通过内部函数 (intrinsic functions) 公开,例如 atan()sqrt()log() 等,这些函数针对GPU进行了优化。
    • 还存在用于更复杂操作的函数,例如向量归一化和反射、叉积以及矩阵转置和行列式计算。
  • 流控制 (Flow Control):
    • 指使用分支指令来改变代码执行流程。
    • 与流控制相关的指令用于实现高级语言结构,如 “if” 和 “case” 语句以及各种类型的循环。
    • 着色器支持两种类型的流控制:
      • 静态流控制 (Static flow control):分支基于统一输入的值。这意味着代码的流程在绘制调用期间是恒定的。静态流控制的主要好处是允许在各种不同情况下使用相同的着色器(例如,不同数量的光源)。由于所有调用都采用相同的代码路径,因此不存在线程分化。
      • 动态流控制 (Dynamic flow control):基于可变输入的值,意味着每个片段可以不同地执行代码。这比静态流控制强大得多,但可能会牺牲性能,尤其是在着色器调用之间代码流不规律变化的情况下。

3.4 可编程着色与API的演进

  • 早期概念:
    • 可编程着色的框架思想可以追溯到1984年Cook的“着色树”(shade trees) [287]。图3.4展示了一个简单的着色器及其对应的着色树。
    • RenderMan着色语言[63, 1804]于20世纪80年代末基于此思想发展而来。它至今仍用于电影制作渲染,并与其他不断发展的规范(如Open Shading Language (OSL) 项目[608])一起使用。
  • 消费级硬件与早期GPU:
    • 1996年10月1日,3dfx Interactive首次成功推出消费级图形硬件(见图3.5时间线)。其Voodoo显卡以高质量和高性能渲染游戏《Quake》的能力使其迅速被采用。此硬件全程实现固定功能管线。
    • 在GPU本身支持可编程着色器之前,曾有几次尝试通过多次渲染通道实时实现可编程着色操作。《Quake III: Arena》脚本语言是1999年在这方面第一个获得广泛商业成功的。
    • 如本章开头所述,NVIDIA的GeForce256是第一个被称为GPU的硬件,但它是不可编程的,是可配置的。
  • DirectX 8.0 与早期可编程性 (2001年):
    • 2001年初,NVIDIA的GeForce 3是第一个支持可编程顶点着色器的GPU[1049],通过DirectX 8.0和OpenGL的扩展公开。这些着色器用一种类汇编语言编程,由驱动程序动态转换为微码。
    • DirectX 8.0中也包含了像素着色器,但像素着色器未能实现真正的可编程性——支持的有限“程序”被驱动程序转换成纹理混合状态,进而连接硬件“寄存器组合器”。这些“程序”不仅长度有限(12条指令或更少),而且缺乏重要功能。
    • Peercy等人[1363]通过对RenderMan的研究,指出依赖纹理读取和浮点数据对于真正的可编程性至关重要。
    • 此时的着色器不允许流控制(分支),因此条件必须通过计算两个分支的结果并在它们之间选择或插值来模拟。
  • DirectX 9.0 与 Shader Model 2.0 (2002年):
    • DirectX定义了着色器模型 (Shader Model, SM) 的概念,以区分具有不同着色器功能的硬件。
    • 2002年发布了DirectX 9.0,包括Shader Model 2.0,它具有真正可编程的顶点和像素着色器。类似的功能也通过各种扩展在OpenGL下公开。
    • 添加了对任意依赖纹理读取和16位浮点值存储的支持,最终完成了Peercy等人确定的要求集。
    • 对着色器资源(如指令、纹理和寄存器)的限制增加了,因此着色器能够实现更复杂的效果。还添加了对流控制的支持。
    • 着色器长度和复杂性的增长使得汇编编程模型日益繁琐。幸运的是,DirectX 9.0还包括了HLSL。这种着色语言由Microsoft与NVIDIA合作开发。大约在同一时间,OpenGL ARB(架构审查委员会)发布了GLSL,这是一种用于OpenGL的相当相似的语言[885]。这些语言深受C编程语言的语法和设计理念的影响,并包含了RenderMan着色语言的元素。
  • Shader Model 3.0 (2004年):
    • 于2004年推出,增加了动态流控制,使着色器功能更加强大。
    • 它还将可选功能转变为必需功能,进一步增加了资源限制,并增加了对顶点着色器中纹理读取的有限支持。
    • 当新一代游戏机于2005年底(微软的Xbox 360)和2006年底(索尼计算机娱乐的PLAYSTATION 3系统)推出时,它们配备了Shader Model 3.0级别的GPU。任天堂的Wii主机是最后一个值得注意的固定功能GPU之一,最初于2006年底发货。纯粹的固定功能管线此时早已不复存在。
    • 着色语言已经发展到使用各种工具来创建和管理它们的程度。图3.6显示了这样一个使用Cook着色树概念的工具的屏幕截图。
  • Shader Model 4.0 (DirectX 10.0, 2006年底):
    • 可编程性的下一个重大进步也出现在2006年底。DirectX 10.0 [175] 中包含的Shader Model 4.0引入了几个主要功能,例如几何着色器和流输出。
    • Shader Model 4.0为所有着色器(顶点、像素和几何)引入了统一编程模型,即前面描述的统一着色器设计。
    • 资源限制进一步增加,并增加了对整数数据类型(包括位运算)的支持。OpenGL 3.3中GLSL 3.30的引入提供了类似的着色器模型。
  • DirectX 11 与 Shader Model 5.0 (2009年):
    • 2009年,DirectX 11和Shader Model 5.0发布,增加了曲面细分阶段着色器和计算着色器(也称为DirectCompute)。该版本还侧重于更有效地支持CPU多处理(第18.5节讨论的主题)。
    • OpenGL在4.0版中添加了曲面细分,在4.3版中添加了计算着色器。
  • API演进模式 (DirectX vs OpenGL):
    • DirectX和OpenGL的演进方式不同。两者都为特定版本发布设定了一定的硬件支持级别。
    • 微软控制DirectX API,因此直接与AMD、NVIDIA和Intel等独立硬件供应商(IHV)以及游戏开发商和计算机辅助设计软件公司合作,以确定要公开哪些功能。
    • OpenGL由硬件和软件供应商联盟开发,由非营利组织Khronos Group管理。由于涉及的公司数量众多,API功能通常在DirectX中引入一段时间后才出现在OpenGL的某个版本中。然而,OpenGL允许扩展(特定于供应商或更通用的扩展),从而可以在版本中正式支持之前使用最新的GPU功能。
  • 低开销API时代:
    • Mantle (2013年): AMD引入Mantle API,与视频游戏开发商DICE合作开发,旨在剥离大部分图形驱动程序开销,将控制权直接交给开发人员。同时进一步支持有效的CPU多处理。这类新API专注于大幅减少CPU在驱动程序中花费的时间,以及更高效的CPU多处理器支持(第18章)。
    • DirectX 12 (2015年): 微软采纳了Mantle中开创的思想,并于2015年发布为DirectX 12。注意DirectX 12并不专注于公开新的GPU功能——DirectX 11.3公开了相同的硬件特性。这两个API都可用于将图形发送到Oculus Rift和HTC Vive等虚拟现实系统。然而,DirectX 12是对API的彻底重新设计,更好地映射到现代GPU架构。低开销驱动程序对于CPU驱动程序成本造成瓶颈的应用程序,或者使用更多CPU处理器进行图形处理可以提高性能的应用程序非常有用[946]。从早期API移植可能很困难,并且天真的实现可能导致性能降低[249, 699, 1438]。
    • Metal (2014年): 苹果于2014年发布了自己的低开销API,名为Metal。Metal首先在iPhone 5S和iPad Air等移动设备上可用,一年后通过OS X El Capitan提供给较新的Macintosh。除了效率之外,减少CPU使用量还可以节省电力,这在移动设备上是一个重要因素。该API有自己的着色语言,用于图形和GPU计算程序。
    • Vulkan (2016年初): AMD将其Mantle工作捐赠给Khronos Group,后者于2016年初发布了自己的新API,名为Vulkan。与OpenGL一样,Vulkan可在多种操作系统上运行。Vulkan使用一种名为SPIR-V的新型高级中间语言,用于着色器表示和通用GPU计算。预编译的着色器是可移植的,因此可以在任何支持所需功能的GPU上使用[885]。Vulkan也可用于非图形GPU计算,因为它不需要显示窗口[946]。Vulkan与其他低开销驱动程序的一个显着区别是它旨在与从工作站到移动设备的各种系统配合使用。
  • 移动端API (OpenGL ES):
    • 在移动设备上,通常使用OpenGL ES。“ES”代表嵌入式系统 (Embedded Systems),因为该API是针对移动设备开发的。当时的OpenGL标准在其某些调用结构中相当臃肿和缓慢,并且需要支持很少使用的功能。
    • OpenGL ES 1.0 (2003年): 是OpenGL 1.3的精简版,描述了一个固定功能管线。虽然DirectX的发布与支持它们的图形硬件的发布同步,但移动设备的图形支持开发并非如此。例如,2010年发布的第一代iPad实现了OpenGL ES 1.1。
    • OpenGL ES 2.0 (2007年): 规范发布,提供可编程着色。它基于OpenGL 2.0,但没有固定功能组件,因此与OpenGL ES 1.1不向后兼容。
    • OpenGL ES 3.0 (2012年): 提供诸如多重渲染目标、纹理压缩、变换反馈、实例化以及更广泛的纹理格式和模式等功能,以及着色器语言的改进。
    • OpenGL ES 3.1: 添加了计算着色器。
    • OpenGL ES 3.2: 添加了几何和曲面细分着色器等功能。
    • 第23章更详细地讨论了移动设备架构。
  • 浏览器API (WebGL):
    • WebGL是OpenGL ES的一个分支,是基于浏览器的API,通过JavaScript调用。
    • WebGL 1.0 (2011年): 该API的第一个版本可在大多数移动设备上使用,因为它在功能上等同于OpenGL ES 2.0。与OpenGL一样,扩展提供了对更高级GPU功能的访问。
    • WebGL 2: 假设支持OpenGL ES 3.0。
    • WebGL的优势:
      • 跨平台,可在所有个人电脑和几乎所有移动设备上运行。
      • 驱动程序批准由浏览器处理。即使一个浏览器不支持特定的GPU或扩展,通常另一个浏览器也支持。
      • 代码是解释执行的,而不是编译的,开发只需要一个文本编辑器。
      • 大多数浏览器都内置了调试器,并且可以检查任何网站上运行的代码。
      • 程序可以通过上传到网站或Github等方式进行部署。
    • 更高级别的场景图和效果库(如three.js [218])可以轻松访问各种更复杂效果的代码,如阴影算法、后处理效果、基于物理的着色和延迟渲染。

(图3.5: API和图形硬件发布时间线简要)

  • 1996: 3dfx Voodoo
  • 1999: GeForce256 “GPU”
  • 2001: DirectX 8.0
  • 2002: DX 9.0 SM 2.0
  • 2004: SM 3.0
  • 2005-2006: Xbox 360, Wii, PS3
  • 2006: DX 10.0 SM 4.0
  • 2007: OpenGL ES 2.0
  • 2009: DX 11.0 SM 5.0
  • 2010: iPad (OpenGL ES 1.1)
  • 2011: WebGL
  • 2012: OpenGL ES 3.0
  • 2013: Mantle, PS4, Xbox One
  • 2014: Metal
  • 2015: DX 12.0
  • 2016: Vulkan, Rift, Vive
  • … WebGL 2

3.5 顶点着色器

  • 顶点着色器是图3.2所示功能管线中的第一个阶段。
  • 输入装配器 (Input Assembler):
    • 虽然这是程序员直接控制的第一个阶段,但值得注意的是,在此阶段之前会发生一些数据操作。
    • 在DirectX称为输入装配器[175, 530, 1208]的阶段,可以将多个数据流编织在一起,形成发送到管线的顶点和图元集。
    • 例如,一个对象可以由一个位置数组和一个颜色数组表示。输入装配器将通过创建具有位置和颜色的顶点来创建该对象的三角形(或线或点)。第二个对象可以使用相同的位置数组(以及不同的模型变换矩阵)和不同的颜色数组来表示。
    • 数据表示在第16.4.5节中详细讨论。
    • 输入装配器还支持执行实例化 (instancing)。这允许通过一次绘制调用多次绘制一个对象,每次实例具有一些变化的数据。实例化的使用在第18.4.2节中介绍。
  • 顶点属性与网格:
    • 三角形网格由一组顶点表示,每个顶点与模型表面上的特定位置相关联。
    • 除了位置,每个顶点还有其他可选属性,例如颜色或纹理坐标。
    • 表面法线也在网格顶点处定义,这似乎是一个奇怪的选择。数学上,每个三角形都有一个明确定义的表面法线,直接使用三角形的法线进行着色似乎更有意义。然而,在渲染时,三角形网格通常用于表示底层的曲面,而顶点法线用于表示该曲面的方向,而不是三角形网格本身的方向。第16.3.4节将讨论计算顶点法线的方法。
    • 图3.7显示了代表曲面的两个三角形网格的侧视图,一个平滑,一个带有锐利折痕。平滑的顶点法线用于表示平滑表面。在右侧,中间的顶点被复制并赋予了两个法线,表示一个折痕。
  • 顶点着色器功能:
    • 顶点着色器是处理三角形网格的第一个阶段。描述三角形如何形成的数据对顶点着色器是不可用的。顾名思义,它只处理传入的顶点。
    • 顶点着色器提供了一种修改、创建或忽略与每个三角形顶点相关的值(如颜色、法线、纹理坐标和位置)的方法。
    • 通常,顶点着色器程序将顶点从模型空间转换到齐次裁剪空间(第4.7节)。至少,顶点着色器必须始终输出此位置。
    • 顶点着色器与前面描述的统一着色器非常相似。传入的每个顶点都由顶点着色器程序处理,然后输出许多在三角形或线上插值的值。
    • 顶点着色器既不能创建也不能销毁顶点,一个顶点产生的结果也不能传递给另一个顶点。由于每个顶点都是独立处理的,GPU上的任意数量的着色器处理器都可以并行应用于传入的顶点流。
  • 物理与逻辑模型的差异: 输入装配通常被呈现为在顶点着色器执行之前发生的过程。这是一个物理模型通常不同于逻辑模型的例子。物理上,获取数据以创建顶点可能发生在顶点着色器中,驱动程序会悄悄地在每个着色器前面加上适当的指令,这对程序员是不可见的。
  • 顶点着色器的用途:
    • 关节动画的顶点混合。
    • 轮廓线渲染。
    • 对象生成:通过仅创建一次网格,然后由顶点着色器使其变形。
    • 使用蒙皮 (skinning) 和变形 (morphing) 技术制作角色身体和面部动画。
    • 过程变形:例如旗帜、布料或水的运动[802, 943]。
    • 粒子创建:通过将退化(无面积)网格发送到管线,并根据需要赋予它们面积。
    • 镜头畸变、热浪、水波纹、翻页等效果:通过在经历过程变形的屏幕对齐网格上使用整个帧缓冲区内容作为纹理。
    • 通过顶点纹理拾取 (vertex texture fetch) 应用地形高度场[40, 1227]。
    • 图3.8显示了一些使用顶点着色器完成的变形效果(法线茶壶、剪切变换的茶壶、噪声函数扭曲的茶壶)。
  • 顶点着色器的输出:
    • 顶点着色器的输出可以通过几种不同的方式消耗。
    • 通常的路径是为每个实例的图元(例如三角形)生成并光栅化,然后将产生的各个像素片段发送到像素着色器程序进行后续处理。
    • 在某些GPU上,数据也可以发送到曲面细分阶段或几何着色器,或存储在内存中。这些可选阶段将在以下各节中讨论。

3.6 曲面细分阶段 (The Tessellation Stage)

  • 曲面细分阶段允许我们渲染曲面。GPU的任务是将每个表面描述转换为一组具有代表性的三角形。
  • 这是GPU的一个可选功能,最早在DirectX 11中可用(并且是其必需部分)。OpenGL 4.0和OpenGL ES 3.2也支持它。
  • 优势:
    • 曲面描述通常比直接提供相应的三角形更紧凑。除了节省内存外,此功能还可以防止CPU和GPU之间的总线成为每帧形状都在变化的动画角色或对象的瓶颈。
    • 通过为给定视图生成适当数量的三角形,可以有效地渲染表面。例如,如果一个球离相机很远,只需要几个三角形。近距离时,用数千个三角形表示可能看起来最好。这种控制细节级别 (level of detail, LoD) 的能力还允许应用程序控制其性能,例如,在较弱的GPU上使用较低质量的网格以维持帧率。
    • 通常由平面表示的模型可以转换为精细的三角形网格,然后根据需要进行扭曲[1493],或者可以对其进行细分以减少昂贵着色计算的频率[225]。
  • 曲面细分阶段总是由三个元素组成。使用DirectX的术语,它们是:外壳着色器 (hull shader)、细分器 (tessellator) 和域着色器 (domain shader)。
    • 在OpenGL中,外壳着色器是曲面细分控制着色器 (tessellation control shader),域着色器是曲面细分评估着色器 (tessellation evaluation shader),这些名称更具描述性,尽管冗长。
    • 固定功能的细分器在OpenGL中称为图元生成器 (primitive generator)。
  • 关于如何指定和细分曲线和曲面的详细内容在第17章讨论。这里简要概述每个细分阶段的目的。
  • 外壳着色器 (Hull Shader / Tessellation Control Shader):
    • 输入:一个特殊的面片图元 (patch primitive)。它由定义细分曲面、贝塞尔面片或其他类型曲面元素的若干控制点组成。
    • 两个功能:
      1. 告诉细分器应该生成多少三角形,以及以何种配置生成(曲面细分因子,Tessellation Factors - TFs)。
      2. 对每个控制点执行处理。
    • 可选地,外壳着色器可以修改传入的面片描述,根据需要添加或删除控制点。
    • 输出:其控制点集,以及曲面细分控制数据,发送给域着色器。(见图3.9)
  • 细分器 (Tessellator / Primitive Generator):
    • 管线中的一个固定功能阶段,仅与曲面细分着色器一起使用。
    • 任务:为域着色器添加若干新顶点以供处理。
    • 从外壳着色器接收信息:期望的曲面细分表面类型:三角形、四边形或等值线 (isoline)。等值线是一组线带,有时用于头发渲染[1954]。
    • 从外壳着色器接收的重要值:曲面细分因子 (TFs) / 曲面细分级别 (OpenGL中的tessellation levels)。
      • 两种类型:内部 (inner) 和外部边缘 (outer edge)。
      • 两个内部因子决定三角形或四边形内部发生多少细分。
      • 外部因子决定每个外部边缘如何分割(第17.6节)。图3.10显示了增加细分因子的示例。
      • 通过允许单独控制,我们可以使相邻曲面边缘的细分匹配,无论内部如何细分。匹配边缘可以避免面片相接处的裂缝或其他着色瑕疵。
    • 顶点被赋予重心坐标 (barycentric coordinates)(第22.8节),这些值指定了期望表面上每个点的相对位置。
  • 外壳着色器总是输出一个面片,即一组控制点位置。但是,它可以通过向细分器发送零或更小(或非数字,NaN)的外部细分级别来表示要丢弃一个面片。
  • 域着色器 (Domain Shader / Tessellation Evaluation Shader):
    • 如果面片未被丢弃,细分器会生成一个网格并将其发送到域着色器。
    • 来自外壳着色器的曲面控制点被域着色器的每次调用用来计算每个顶点的输出值。
    • 域着色器的数据流模式类似于顶点着色器,来自细分器的每个输入顶点都被处理并生成相应的输出顶点。
    • 形成的三角形随后向下传递到管线中。
  • 效率和结构:
    • 这个系统听起来复杂,但其结构是为了效率,每个着色器都可以相当简单。
    • 传入外壳着色器的面片通常几乎不作修改,或只做少量修改。该着色器也可能使用面片的估计距离或屏幕尺寸来动态计算细分因子,如地形渲染[466]。或者,外壳着色器可能只是传递一组由应用程序计算并提供的固定值给所有面片。
    • 细分器执行一个复杂但固定功能的过程,即生成顶点、赋予它们位置,并指定它们形成的三角形或线。这个数据放大步骤在着色器之外执行,以提高计算效率[530]。
    • 域着色器获取为每个点生成的重心坐标,并在面片的评估方程中使用这些坐标来生成期望的位置、法线、纹理坐标和其他顶点信息。
    • 图3.11显示了一个例子:底层约6000个三角形的网格,右侧每个三角形都使用PN三角形细分进行了细分和位移。

3.7 几何着色器 (The Geometry Shader)

  • 几何着色器可以将图元转换为其他图元,这是曲面细分阶段无法做到的。
    • 例如,一个三角形网格可以通过让每个三角形创建线框边缘来转换为线框视图。或者,这些线可以被替换为面向观察者的四边形,从而制作出边缘更粗的线框渲染[1492]。
  • 几何着色器于2006年底随着DirectX 10的发布被添加到硬件加速图形管线中。它位于管线中曲面细分着色器之后,并且其使用是可选的。虽然它是Shader Model 4.0的必需部分,但在早期的着色器模型中并未使用。OpenGL 3.2和OpenGL ES 3.2也支持这种类型的着色器。
  • 输入:
    • 单个对象及其关联的顶点。该对象通常由三角形带、线段或仅一个点组成。
    • 可以定义和处理扩展图元。特别是,可以传入三角形外部的三个附加顶点,并且可以使用多段线上的两个相邻顶点。(见图3.12)
    • 在DirectX 11和Shader Model 5.0中,可以传入更复杂的面片,最多包含32个控制点。但即便如此,曲面细分阶段在面片生成方面更有效[175]。
  • 输出:
    • 几何着色器处理此图元并输出零个或多个顶点,这些顶点被视作点、多段线或三角形带。
    • 注意,几何着色器可以不生成任何输出。通过这种方式,可以通过编辑顶点、添加新图元和移除其他图元来选择性地修改网格。
  • 用途:
    • 几何着色器设计用于修改传入数据或进行有限数量的复制。
    • 例如,一种用途是生成数据的六个变换副本,以同时渲染立方体贴图的六个面;参见第10.4.3节。
    • 它还可以用于高效创建级联阴影贴图 (cascaded shadow maps) 以生成高质量阴影。
    • 其他利用几何着色器的算法包括:从点数据创建可变大小的粒子,沿轮廓挤压鳍片以进行毛发渲染,以及为阴影算法查找对象边缘。
    • 图3.13展示了更多示例(GS动态元球等值面细分、GS分形细分线段并生成公告板显示闪电、GS与流输出结合进行布料模拟)。
  • 特性:
    • DirectX 11增加了几何着色器使用实例化的能力,即几何着色器可以在任何给定图元上运行固定次数[530, 1971]。在OpenGL 4.0中,这通过调用计数 (invocation count) 指定。
    • 几何着色器还可以输出最多四个流。一个流可以向下发送到渲染管线进行进一步处理。所有这些流都可以选择性地发送到流输出渲染目标。
  • 性能考量:
    • 几何着色器保证按输入顺序输出图元的结果。这会影响性能,因为如果多个着色器核心并行运行,结果必须被保存和排序。
    • 此因素及其他因素使得几何着色器不适用于在单次调用中复制或创建大量几何体[175, 530]。
    • 在管线中,只有三个地方可以在GPU上创建工作:光栅化、曲面细分阶段和几何着色器。其中,几何着色器的行为在考虑所需资源和内存时最不可预测,因为它是完全可编程的。
    • 实践中,几何着色器通常很少使用,因为它不能很好地映射到GPU的优势。在某些移动设备上,它是通过软件实现的,因此不鼓励在那里使用[69]。

3.7.1 流输出 (Stream Output)

  • GPU管线的标准用途是将数据通过顶点着色器发送,然后光栅化产生的三角形并在像素着色器中处理这些三角形。过去,数据总是通过管线传递,中间结果无法访问。
  • 流输出的概念是在Shader Model 4.0中引入的。
  • 在顶点由顶点着色器(以及可选地,曲面细分和几何着色器)处理后,这些顶点可以以流(即有序数组)的形式输出,此外还可以发送到光栅化阶段。实际上,光栅化可以完全关闭,然后管线纯粹用作非图形流处理器。
  • 以这种方式处理的数据可以送回管线,从而允许迭代处理。这种类型的操作对于模拟流动的水或其他粒子效果很有用(如第13.8节所述)。它也可以用于对模型进行蒙皮,然后使这些顶点可供重用(第4.4节)。
  • 流输出仅以浮点数的形式返回数据,因此可能会有明显的内存成本。
  • 流输出作用于图元,而不是直接作用于顶点。如果网格被发送到管线,每个三角形都会生成其自己的一组三个输出顶点。原始网格中的任何顶点共享都会丢失。因此,更典型的用途是将顶点作为点集图元仅通过管线发送。
  • 在OpenGL中,流输出阶段称为变换反馈 (transform feedback),因为其许多用途的重点是变换顶点并将其返回以进行进一步处理。
  • 图元保证按其输入顺序发送到流输出目标,这意味着顶点顺序将得到维护[530]。

3.8 像素着色器 (The Pixel Shader)

  • 在顶点、曲面细分和几何着色器执行其操作后,图元被裁剪并设置为进行光栅化,如前一章所述。管线的这一部分在其处理步骤中相对固定,即可配置但不可编程。
  • 每个三角形都被遍历以确定它覆盖哪些像素。光栅器还可能粗略计算三角形覆盖每个像素单元区域的程度(第5.4.2节)。这个部分或完全与像素重叠的三角形片段称为片段 (fragment)。
  • 插值:
    • 三角形顶点处的值,包括z缓冲中使用的z值,会在三角形表面上为每个像素进行插值。这些值被传递给像素着色器,然后由像素着色器处理该片段。
    • 在OpenGL中,像素着色器被称为片段着色器 (fragment shader),这也许是一个更好的名称。本书通篇使用“像素着色器”以保持一致性。
    • 发送到管线的点和线图元也会为覆盖的像素创建片段。
    • 跨三角形执行的插值类型由像素着色器程序指定。
      • 通常我们使用透视校正插值 (perspective-correct interpolation),这样当对象在远处后退时,像素表面位置之间的世界空间距离会增加。一个例子是渲染延伸到地平线的铁轨。铁轨枕木在铁轨较远的地方更密集,因为接近地平线的每个连续像素都经过了更多的距离。
      • 其他插值选项也可用,例如屏幕空间插值,其中不考虑透视投影。
      • DirectX 11对插值执行的时间和方式提供了进一步的控制[530]。
  • 输入与输出:
    • 在编程术语中,顶点着色器程序的输出,在三角形(或线)上插值后,有效地成为像素着色器程序的输入。
    • 随着GPU的发展,其他输入也已公开。例如,从Shader Model 3.0及更高版本开始,片段的屏幕位置可用于像素着色器。此外,三角形的哪一面可见是一个输入标志。此知识对于在单次传递中为三角形的正面和背面渲染不同材质非常重要。
    • 有了输入,像素着色器通常计算并输出片段的颜色。它还可以产生一个不透明度值,并可选地修改其z深度。在合并期间,这些值用于修改像素处存储的内容。光栅化阶段生成的深度值也可以由像素着色器修改。模板缓冲值通常不可修改,而是传递到合并阶段。DirectX 11.3允许着色器更改此值。
    • 诸如雾计算和alpha测试之类的操作已从合并操作转移到SM 4.0中的像素着色器计算[175]。
  • 片段丢弃 (Fragment Discard):
    • 像素着色器还具有丢弃传入片段(即不生成输出)的独特能力。
    • 图3.14显示了如何使用片段丢弃的一个示例。用户定义的裁剪平面。左侧,单个水平裁剪平面切割对象。中间,嵌套球体被三个平面裁剪。右侧,仅当球体表面位于所有三个裁剪平面之外时才被裁剪。
    • 裁剪平面功能曾经是固定功能管线中的可配置元素,后来在顶点着色器中指定。有了片段丢弃功能,此功能随后可以在像素着色器中以任何期望的方式实现,例如决定裁剪体应如何进行与(AND)或或(OR)运算。
  • 多重渲染目标 (Multiple Render Targets, MRT):
    • 最初,像素着色器只能输出到合并阶段,以供最终显示。像素着色器可以执行的指令数量随着时间的推移已大大增加。
    • 这种增加催生了多重渲染目标(MRT)的想法。像素着色器程序的结果不再仅仅发送到颜色和z缓冲,而是可以为每个片段生成多组值并保存到不同的缓冲区中,每个缓冲区称为一个渲染目标。
    • 渲染目标通常具有相同的x和y维度;某些API允许不同的大小,但渲染区域将是这些维度中最小的。某些架构要求每个渲染目标具有相同的位深度,甚至可能具有相同的数据格式。根据GPU的不同,可用的渲染目标数量为四个或八个。
    • 即使存在这些限制,MRT功能在更有效地执行渲染算法方面也是一个强大的辅助工具。单次渲染过程可以在一个目标中生成彩色图像,在另一个目标中生成对象标识符,在第三个目标中生成世界空间距离。
    • 这种能力也催生了一种不同类型的渲染管线,称为延迟着色 (deferred shading),其中可见性和着色在不同的通道中完成。第一个通道在每个像素处存储有关对象位置和材质的数据。后续通道可以有效地应用光照和其他效果。这类渲染方法在第20.1节中描述。
  • 像素着色器限制与例外:
    • 像素着色器的局限性在于它通常只能在其被赋予的片段位置写入渲染目标,并且不能读取邻近像素的当前结果。也就是说,当像素着色器程序执行时,它不能将其输出直接发送到邻近像素,也不能访问其他像素的最近更改。相反,它计算仅影响其自身像素的结果。
    • 然而,这种限制并不像听起来那么严重。在一个通道中创建的输出图像的任何数据都可以被后续通道中的像素着色器访问。可以使用图像处理技术处理邻近像素,如第12.1节所述。
    • 像素着色器不能知道或影响邻近像素结果的规则存在例外。
      • 梯度计算: 一个例外是像素着色器可以在计算梯度或导数信息期间立即(尽管是间接地)访问相邻片段的信息。像素着色器被提供任何插值沿x和y屏幕轴每像素变化的量。这些值对于各种计算和纹理寻址很有用。这些梯度对于诸如纹理过滤(第6.2.2节)之类的操作尤其重要,在这些操作中我们想知道图像覆盖像素的程度。所有现代GPU都通过以 $2 \\times 2$ 的组(称为quad)处理片段来实现此功能。当像素着色器请求梯度值时,将返回相邻片段之间的差异。参见图3.15。统一核心具有访问邻近数据(保存在同一warp上的不同线程中)的能力,因此可以计算用于像素着色器的梯度。这种实现的一个后果是,梯度信息不能在受动态流控制影响的着色器部分(即具有可变迭代次数的“if”语句或循环)中访问。组中的所有片段必须使用相同的指令集进行处理,以便所有四个像素的结果对于计算梯度都有意义。这是一个基本限制,即使在离线渲染系统中也存在[64]。
      • 无序访问视图 (UAV) / 着色器存储缓冲对象 (SSBO): DirectX 11引入了一种允许对任何位置进行写访问的缓冲区类型,即无序访问视图 (UAV)。最初仅用于像素和计算着色器,对UAV的访问在DirectX 11.1中扩展到所有着色器[146]。OpenGL 4.3称之为着色器存储缓冲对象 (SSBO)。像素着色器并行运行,顺序任意,此存储缓冲区在它们之间共享。
        • 通常需要某种机制来避免数据竞争条件(也称为数据冒险),即两个着色器程序“竞争”影响相同的值,可能导致任意结果。例如,如果像素着色器的两个调用试图大约同时对检索到的相同值进行加法,则可能会发生错误。两者都会检索原始值,两者都会在本地修改它,但随后无论哪个调用最后写入其结果,都会清除另一个调用的贡献——只会发生一次加法。GPU通过具有着色器可以访问的专用原子单元 (atomic units) 来避免此问题[530]。然而,原子操作意味着某些着色器可能会在等待访问正在被另一个着色器进行读/修改/写操作的内存位置时停顿。
      • 光栅器顺序视图 (ROV): 虽然原子操作避免了数据冒险,但许多算法需要特定的执行顺序。例如,您可能希望在覆盖一个更远的透明蓝色三角形之前绘制它,然后用红色透明三角形覆盖它,将红色混合在蓝色之上。一个像素可能有两个像素着色器调用,每个三角形一个,其执行方式可能导致红色三角形的着色器在蓝色的着色器之前完成。在标准管线中,片段结果在合并阶段进行排序,然后才进行处理。光栅器顺序视图 (ROV) 在DirectX 11.3中引入,以强制执行顺序。它们类似于UAV;它们可以以相同的方式被着色器读取和写入。关键区别在于ROV保证数据按正确的顺序访问。这大大增加了这些着色器可访问缓冲区的实用性[327, 328]。例如,ROV使像素着色器能够编写自己的混合方法成为可能,因为它可以直接访问和写入ROV中的任何位置,因此不需要合并阶段[176]。代价是,如果检测到乱序访问,像素着色器调用可能会停顿,直到较早绘制的三角形被处理完毕。

3.9 合并阶段 (The Merging Stage)

  • 如第2.5.2节所述,合并阶段是将各个片段(在像素着色器中生成)的深度和颜色与帧缓冲结合的地方。
  • DirectX称此阶段为输出合并器 (output merger);OpenGL称之为逐样本操作 (per-sample operations)。
  • 在大多数传统管线图(包括我们自己的)上,此阶段是模板缓冲和z缓冲操作发生的地方。
  • 如果片段可见,则在此阶段发生的另一个操作是颜色混合 (color blending)。
    • 对于不透明表面,没有真正的混合,因为片段的颜色只是替换先前存储的颜色。
    • 片段和存储颜色的实际混合通常用于透明度和合成操作(第5.5节)。
  • Early-Z (早期Z测试):
    • 想象一下,由光栅化生成的片段通过像素着色器运行,然后在应用z缓冲时发现被某个先前渲染的片段隐藏。那么在像素着色器中完成的所有处理都是不必要的。
    • 为了避免这种浪费,许多GPU在像素着色器执行之前执行一些合并测试[530]。片段的z深度(以及正在使用的任何其他内容,如模板缓冲或裁剪)用于测试可见性。如果隐藏,则剔除片段。此功能称为Early-Z [1220, 1542]。
    • 像素着色器能够更改片段的z深度或完全丢弃片段。如果发现像素着色器程序中存在任一类型的操作,则Early-Z通常不能使用并被关闭,这通常会使管线效率降低。
    • DirectX 11和OpenGL 4.2允许像素着色器强制开启Early-Z测试,尽管有许多限制[530]。有关Early-Z和其他z缓冲优化的更多信息,请参见第23.7节。有效使用Early-Z会对性能产生很大影响,这在第18.4.5节中详细讨论。
  • 可配置性:
    • 合并阶段介于固定功能阶段(如三角形设置)和完全可编程着色器阶段之间。虽然它不可编程,但其操作是高度可配置的。
    • 特别是颜色混合可以设置为执行大量不同的操作。最常见的是涉及颜色和alpha值的乘法、加法和减法的组合,但其他操作也是可能的,例如最小值和最大值,以及位逻辑运算。
    • DirectX 10增加了将像素着色器中的两种颜色与帧缓冲颜色混合的功能。此功能称为双源颜色混合 (dual source-color blending),不能与多重渲染目标(MRT)一起使用。MRT确实支持混合,DirectX 10.1引入了在每个单独缓冲区上执行不同混合操作的功能。
  • 顺序保证:
    • 如上一节末尾所述,DirectX 11.3提供了一种通过ROV使混合可编程的方法,尽管会牺牲性能。
    • ROV和合并阶段都保证绘制顺序,也称为输出不变性 (output invariance)。无论像素着色器结果生成的顺序如何,API都要求结果按输入顺序(逐对象、逐三角形)排序并发送到合并阶段。

3.10 计算着色器 (The Compute Shader)

  • GPU不仅可以用于实现传统的图形管线。在从计算股票期权估值到训练用于深度学习的神经网络等各种领域都有许多非图形用途。以这种方式使用硬件称为GPU计算 (GPU computing)。
  • 像CUDA和OpenCL这样的平台用于将GPU作为大规模并行处理器进行控制,而无需真正需要或访问图形特定功能。这些框架通常使用带有扩展的C或C++等语言,以及为GPU制作的库。
  • 计算着色器特性:
    • 计算着色器在DirectX 11中引入,是GPU计算的一种形式,因为它是一个未锁定在图形管线中某个位置的着色器。
    • 它与渲染过程紧密相关,因为它由图形API调用。它与顶点、像素和其他着色器一起使用。
    • 它利用与管线中使用的相同的统一着色器处理器池。
    • 它与其他着色器一样,具有一组输入数据,并且可以访问缓冲区(如纹理)进行输入和输出。
    • Warp和线程在计算着色器中更为明显。例如,每次调用都会获得一个它可以访问的线程索引。
    • 还有一个线程组 (thread group) 的概念,在DirectX 11中由1到1024个线程组成。这些线程组由x、y和z坐标指定,主要是为了简化在着色器代码中的使用。每个线程组都有少量在线程之间共享的内存。在DirectX 11中,这相当于32KB。计算着色器按线程组执行,因此可以保证组中的所有线程同时运行[1971]。
  • 优势与用途:
    • 计算着色器的一个重要优势是它们可以访问在GPU上生成的数据。将数据从GPU发送到CPU会产生延迟,因此如果处理和结果可以保留在GPU上,则可以提高性能[1403]。
    • 后处理 (Post-processing),即以某种方式修改渲染图像,是计算着色器的常见用途。共享内存意味着对图像像素进行采样的中间结果可以与邻近线程共享。例如,已发现使用计算着色器确定图像的分布或平均亮度比在像素着色器上执行此操作快两倍[530]。
    • 计算着色器还可用于粒子系统、网格处理(如面部动画[134])、剔除[1883, 1884]、图像过滤[1102, 1710]、提高深度精度[991]、阴影[865]、景深[764]以及任何其他可以利用一组GPU处理器来处理的任务。
    • Wihlidal [1884]讨论了计算着色器如何比曲面细分外壳着色器更有效。
    • 图3.16展示了其他用途(计算着色器模拟受风影响的头发,头发本身使用曲面细分阶段渲染;计算着色器执行快速模糊操作;模拟海浪)。

本章结束了我们对GPU实现渲染管线的概述。GPU的功能可以通过多种方式使用和组合以执行各种与渲染相关的过程。利用这些功能的相关理论和算法是本书的核心主题。我们的重点现在转向变换和着色。

进一步阅读和资源

  • Giesen的图形管线之旅[530]详细讨论了GPU的许多方面,解释了元素为何如此工作。
  • Fatahalian和Bryant的课程[462]在一系列详细的讲座幻灯片中讨论了GPU并行性。
  • 虽然Kirk和Hwa的书[903]专注于使用CUDA进行GPU计算,但其引言部分讨论了GPU的演进和设计理念。
  • 学习着色器编程的正式方面需要一些努力。像《OpenGL Superbible》[1606]和《OpenGL Programming Guide》[885]这样的书籍包含了着色器编程的材料。较早的书籍《OpenGL Shading Language》[1512]没有涵盖较新的着色器阶段,如几何和曲面细分着色器,但确实特别关注与着色器相关的算法。
  • realtimerendering.com,获取最新和推荐的书籍。

第四章 变换

“何必恐惧愤怒的矢量 在你沉睡的头颅周围转向,形成。 永远无需畏惧 这个贫瘠世界抽象风暴的暴力。” ——罗伯特·潘·沃伦

变换是一种操作,它接受点、向量或颜色等实体,并以某种方式转换它们。对于计算机图形从业者来说,掌握变换至关重要。通过变换,你可以定位、重塑和动画化对象、光源和相机。你还可以确保所有计算都在同一坐标系中进行,并以不同方式将对象投影到平面上。这些只是变换可以执行操作中的一小部分,但足以证明变换在实时图形乃至任何类型计算机图形中的重要作用。

线性变换是保持向量加法和标量乘法的变换。具体来说: $f(\mathbf{x}) + f(\mathbf{y}) = f(\mathbf{x} + \mathbf{y})$ (4.1) $kf(\mathbf{x}) = f(k\mathbf{x})$ (4.2)

例如,$f(\mathbf{x}) = 5\mathbf{x}$ 是一个将向量每个元素乘以五的变换。这是一个线性变换,因为它满足上述两个条件。这种函数称为缩放变换。旋转变换是另一个线性变换。所有三元素向量的线性变换(如缩放和旋转)都可以用 $3 \times 3$ 矩阵表示。

然而,$3 \times 3$ 矩阵通常不够用。例如,$f(\mathbf{x}) = \mathbf{x} + (7, 3, 2)$ 不是线性的。这种将固定向量加到另一个向量上的操作执行平移。我们希望组合各种变换,例如,先将对象缩小一半,然后将其移动到不同位置。

使用仿射变换可以组合线性变换和平移,通常存储为 $4 \times 4$ 矩阵。仿射变换执行线性变换然后进行平移。为了表示四元素向量,我们使用齐次坐标表示法,以相同方式表示点和方向。方向向量表示为 $\mathbf{v} = (v_x, v_y, v_z, 0)^T$,点表示为 $\mathbf{p} = (p_x, p_y, p_z, 1)^T$。

所有平移、旋转、缩放、反射和错切矩阵都是仿射的。仿射矩阵的主要特性是它保持线的平行性,但不一定保持长度和角度。仿射变换也可以是各个仿射变换的任意序列的串联。

本章将从最基本、最核心的仿射变换开始。然后描述更专门的矩阵,接着讨论和描述四元数。然后是顶点混合和变形。最后,描述投影矩阵。这些变换、它们的符号、函数和属性大部分总结在表4.1中。

理解变换矩阵及其相互作用至关重要。例如,了解正交矩阵(其逆是其转置)可以加快矩阵求逆。

表4.1 本章讨论的大多数变换的总结

符号 名称 特性
$\mathbf{T}(\mathbf{t})$ 平移矩阵 移动一个点。仿射。
$\mathbf{R}_x(\rho)$ 旋转矩阵 围绕x轴旋转 $\rho$ 弧度。y轴和z轴有类似符号。正交且仿射。
$\mathbf{R}$ 旋转矩阵 任何旋转矩阵。正交且仿射。
$\mathbf{S}(\mathbf{s})$ 缩放矩阵 根据 $\mathbf{s}$ 沿x、y和z轴缩放。仿射。
$\mathbf{H}_{ij}(s)$ 错切矩阵 相对于分量 $j$,将分量 $i$ 错切因子 $s$。$i,j \in \{x,y,z\}$。仿射。
$\mathbf{E}(h,p,r)$ 欧拉变换 由欧拉角航向(yaw)、俯仰(pitch)、滚转(roll)给出的方位矩阵。正交且仿射。
$\mathbf{P}_o$ 正交投影 平行投影到某个平面或体积。仿射。
$\mathbf{P}_p$ 透视投影 带透视地投影到某个平面或体积。
slerp($\hat{\mathbf{q}}$, $\hat{\mathbf{r}}$, t) slerp变换 (四元数球面线性插值) 根据四元数 $\hat{\mathbf{q}}$ 和 $\hat{\mathbf{r}}$ 以及参数 $t$ 创建插值四元数。

(注意:原表中的 Po(s) 和 Pp(s) 中的 (s) 参数在文本描述中通常不这样体现,而是通过其他参数定义,如视景体范围。此处简化为 Po 和 Pp)

4.1 基本变换

本节描述最基本的变换,如平移、旋转、缩放、错切、变换串联、刚体变换、法线变换和逆的计算。

4.1.1 平移

  • 从一个位置到另一个位置的改变由平移矩阵 $\mathbf{T}$ 表示。
  • 该矩阵通过向量 $\mathbf{t} = (t_x, t_y, t_z)$ 平移实体。
  • $\mathbf{T}(\mathbf{t})$ 由公式4.3给出: $$\mathbf{T}(\mathbf{t}) = \mathbf{T}(t_x, t_y, t_z) = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}$$
  • 点 $\mathbf{p} = (p_x, p_y, p_z, 1)^T$ 与 $\mathbf{T}(\mathbf{t})$ 相乘得到新点 $\mathbf{p}' = (p_x+t_x, p_y+t_y, p_z+t_z, 1)^T$。
  • 方向向量 $\mathbf{v} = (v_x, v_y, v_z, 0)^T$ 不受平移影响。
  • 平移矩阵的逆是 $\mathbf{T}^{-1}(\mathbf{t}) = \mathbf{T}(-\mathbf{t})$。
  • 计算机图形学中存在另一种有效的符号方案,将平移向量放在矩阵的底行(行主序,例如DirectX)。本书使用列主序。内存存储中,矩阵的最后四个值是三个平移值后跟一个1。

4.1.2 旋转

  • 旋转变换将向量(位置或方向)围绕通过原点的给定轴旋转给定角度。
  • 它是一种刚体变换,即保留点之间的距离并保留手性(handedness)。
  • 方位矩阵是与相机视图或对象关联的旋转矩阵,定义其在空间中的方位。
  • 二维旋转:向量 $\mathbf{v} = (r \cos\theta, r \sin\theta)$ 旋转 $\phi$ 弧度(逆时针)得到 $\mathbf{u} = (r \cos(\theta+\phi), r \sin(\theta+\phi))$。 $$\mathbf{u} = \begin{pmatrix} \cos\phi & -\sin\phi \\ \sin\phi & \cos\phi \end{pmatrix} \mathbf{v} = \mathbf{R}(\phi)\mathbf{v}$$ (源自公式 4.4)
  • 三维中常用的旋转矩阵是 $\mathbf{R}_x(\phi)$, $\mathbf{R}_y(\phi)$, 和 $\mathbf{R}_z(\phi)$,分别围绕x, y, z轴旋转 $\phi$ 弧度: $$\mathbf{R}_x(\phi) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\phi & -\sin\phi & 0 \\ 0 & \sin\phi & \cos\phi & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.5) $$\mathbf{R}_y(\phi) = \begin{pmatrix} \cos\phi & 0 & \sin\phi & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\phi & 0 & \cos\phi & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.6) $$\mathbf{R}_z(\phi) = \begin{pmatrix} \cos\phi & -\sin\phi & 0 & 0 \\ \sin\phi & \cos\phi & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.7)
  • 对于任何围绕轴旋转 $\phi$ 弧度的 $3 \times 3$ 旋转矩阵 $\mathbf{R}$,其迹(trace)为: $$\text{tr}(\mathbf{R}) = 1 + 2 \cos\phi$$ (4.8)
  • 旋转矩阵 $\mathbf{R}_i(\phi)$ 的特性是它使旋转轴 $i$ 上的所有点保持不变。
  • 所有旋转矩阵的行列式为1并且是正交的。其逆矩阵为 $\mathbf{R}_i^{-1}(\phi) = \mathbf{R}_i(-\phi)$ 或 $\mathbf{R}_i^T(\phi)$。
  • 示例:绕点旋转
    • 要将对象绕z轴旋转 $\phi$ 弧度,旋转中心为点 $\mathbf{p}$。
    • 变换步骤:
      1. 将对象平移使 $\mathbf{p}$ 与原点重合:$\mathbf{T}(-\mathbf{p})$。
      2. 执行旋转:$\mathbf{R}_z(\phi)$。
      3. 将对象平移回其原始位置:$\mathbf{T}(\mathbf{p})$。
    • 最终变换 $\mathbf{X}$ 为: $$\mathbf{X} = \mathbf{T}(\mathbf{p})\mathbf{R}_z(\phi)\mathbf{T}(-\mathbf{p})$$ (4.9) (注意矩阵顺序)

4.1.3 缩放

  • 缩放矩阵 $\mathbf{S}(\mathbf{s}) = \mathbf{S}(s_x, s_y, s_z)$ 分别沿x, y, z方向缩放实体。
  • $$\mathbf{S}(\mathbf{s}) = \begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.10)
  • 如果 $s_x = s_y = s_z$,则称为均匀缩放(isotropic scaling),否则称为非均匀缩放(anisotropic scaling)。
  • 逆矩阵是 $\mathbf{S}^{-1}(\mathbf{s}) = \mathbf{S}(1/s_x, 1/s_y, 1/s_z)$。
  • 使用齐次坐标,通过操作矩阵位置 (3,3) 的元素(即右下角元素)可以创建均匀缩放矩阵。例如,均匀缩放因子为5,可以将 (0,0), (1,1), (2,2) 元素设为5,或者将 (3,3) 元素设为 1/5。 $$\mathbf{S} = \begin{pmatrix} 5 & 0 & 0 & 0 \\ 0 & 5 & 0 & 0 \\ 0 & 0 & 5 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}, \quad \mathbf{S}' = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1/5 \end{pmatrix}$$ (4.11) 使用 $\mathbf{S}'$ 必须进行齐次化(除以w分量),如果系统总是执行此除法,则没有额外成本。
  • $\mathbf{s}$ 的一个或三个分量为负值会产生反射矩阵(镜像矩阵)。若仅两个缩放因子为-1,则相当于旋转 $\pi$ 弧度。
  • 旋转矩阵与反射矩阵的串联也是反射矩阵。
  • 通过计算矩阵左上 $3 \times 3$ 部分的行列式来检测矩阵是否反射。若值为负,则矩阵是反射的。
  • 示例:沿特定方向缩放
    • 如果缩放应沿正交的、右手系向量 $\mathbf{f}_x, \mathbf{f}_y, \mathbf{f}_z$ 的轴进行。
    • 首先,构建基变换矩阵 $\mathbf{F}$: $$\mathbf{F} = \begin{pmatrix} f_{x_x} & f_{y_x} & f_{z_x} & 0 \\ f_{x_y} & f_{y_y} & f_{z_y} & 0 \\ f_{x_z} & f_{y_z} & f_{z_z} & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (基于公式4.13的解释,其中 $\mathbf{f}_x=(f_{x_x}, f_{x_y}, f_{x_z})^T$ 等是列向量)
    • 变换思想:将坐标系与标准轴对齐,然后使用标准缩放矩阵,再变换回来。
    • 变换 $\mathbf{X}$ 为: $$\mathbf{X} = \mathbf{F}\mathbf{S}(\mathbf{s})\mathbf{F}^T$$ (4.14) (因为 $\mathbf{F}$ 是正交的,$\mathbf{F}^{-1} = \mathbf{F}^T$)

4.1.4 错切

  • 错切矩阵可用于扭曲整个场景或模型的形状。
  • 六种基本错切矩阵:$\mathbf{H}_{xy}(s), \mathbf{H}_{xz}(s), \mathbf{H}_{yx}(s), \mathbf{H}_{yz}(s), \mathbf{H}_{zx}(s), \mathbf{H}_{zy}(s)$。第一个下标表示被改变的坐标,第二个下标表示进行错切的坐标。
  • $\mathbf{H}_{xz}(s)$ 示例 (将x坐标根据z坐标进行错切): $$\mathbf{H}_{xz}(s) = \begin{pmatrix} 1 & 0 & s & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.15) 此矩阵作用于点 $\mathbf{p}$ 产生点 $(p_x+sp_z, p_y, p_z)^T$。
  • $\mathbf{H}_{ij}(s)$ 的逆是 $\mathbf{H}_{ij}^{-1}(s) = \mathbf{H}_{ij}(-s)$。
  • 另一种错切矩阵 $\mathbf{H}'_{xy}(s,t)$ (z坐标同时错切x和y坐标): $$\mathbf{H}'_{xy}(s,t) = \begin{pmatrix} 1 & 0 & s & 0 \\ 0 & 1 & t & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (基于公式4.16,但原文公式似乎有误,应该是第z列影响x和y,即 H’xz(s)Hyz(t) 形式,或者如原文的 H’{xy}(s,t) = H{xk}(s)H_{yk}(t) for k=z。若按原文 H’_{xy}(s,t),则s在(0,2)位置,t在(1,2)位置) 根据文本描述 “Here, however, both subscripts are used to denote that these coordinates are to be sheared by the third coordinate.” 指的是前两个下标的坐标被第三个坐标错切。若 $i,j,k$ 分别为 $x,y,z$, 那么 $H'_{xy}(s,t)$ 表示x和y被z错切,所以 s 在(0,2) t 在(1,2)。 $$\mathbf{H}'_{xy}(s,t) = \begin{pmatrix} 1 & 0 & s & 0 \\ 0 & 1 & t & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (修正了原公式4.16,使其符合描述,原公式的s,t位置在z列)
  • 任何错切矩阵的行列式 $|\mathbf{H}| = 1$,因此这是保体积变换。

4.1.5 变换的串联

  • 由于矩阵乘法不满足交换律,矩阵出现的顺序很重要。变换的串联是顺序相关的。
  • 例如,$\mathbf{S}\mathbf{R}$ 与 $\mathbf{R}\mathbf{S}$ 的结果通常不同。
  • 串联一系列矩阵为一个单一矩阵是为了提高效率。
  • 例如,如果所有对象必须先缩放,然后旋转,最后平移,则复合矩阵 $\mathbf{C} = \mathbf{T}\mathbf{R}\mathbf{S}$。应用于顶点 $\mathbf{p}$ 时为 $\mathbf{T}(\mathbf{R}(\mathbf{S}\mathbf{p}))$。
  • 矩阵串联满足结合律,例如 $(\mathbf{T}\mathbf{R})(\mathbf{S}\mathbf{p})$。

4.1.6 刚体变换

  • 仅由平移和旋转串联组成的变换称为刚体变换。

  • 它保持长度、角度和手性。

  • $$\mathbf{X} = \begin{pmatrix} r_{00} & r_{01} & r_{02} & t_x \\ r_{10} & r_{11} & r_{12} & t_y \\ r_{20} & r_{21} & r_{22} & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}$$

    (4.17) 其中 $r_{ij}$ 是旋转矩阵 $\mathbf{R}$ 的元素。

  • $\mathbf{X}$ 的逆计算为 $\mathbf{X}^{-1} = (\mathbf{T}(\mathbf{t})\mathbf{R})^{-1} = \mathbf{R}^{-1}\mathbf{T}(\mathbf{t})^{-1} = \mathbf{R}^T \mathbf{T}(-\mathbf{t})$。

  • 另一种计算逆的方法($\bar{R}$是R的3x3部分): 如果 $\mathbf{X} = \begin{pmatrix} \bar{\mathbf{R}} & \mathbf{t} \\ \mathbf{0}^T & 1 \end{pmatrix}$ 则 $\mathbf{X}^{-1} = \begin{pmatrix} \bar{\mathbf{R}}^T & -\bar{\mathbf{R}}^T \mathbf{t} \\ \mathbf{0}^T & 1 \end{pmatrix}$ (4.19)

  • 示例:相机定向 (gluLookAt)

    • 相机位于 $\mathbf{c}$,观察目标 $\mathbf{l}$,给定相机向上方向为 $\mathbf{u}'$。
    • 计算新基向量 $\{\mathbf{r}, \mathbf{u}, \mathbf{v}\}$:
      1. 观察向量 (view vector) $\mathbf{v} = (\mathbf{c} - \mathbf{l}) / ||\mathbf{c} - \mathbf{l}||$ (从目标指向相机位置的归一化向量)。
      2. 右向量 (right vector) $\mathbf{r} = -(\mathbf{v} \times \mathbf{u}') / ||\mathbf{v} \times \mathbf{u}'||$ (注意这里的负号是为了使r指向右侧,如果v是从相机到目标,则不需要负号,但这里v是从目标到相机)。更常见的定义是先算lookDir = (l-c).normalize(), 然后 r = (lookDir x u’).normalize(), u = r x lookDir。但书中定义v=(c-l)/||c-l||。所以r = (u’ x v).normalize() (若u’xv,则r是屏幕右方)。书中是r=-(v x u’) / ||v x u’||。如果v是c-l(目标到相机),u’是近似up,那么 v x u’ 指向左边,所以 - (v x u’) 指向右边。这是对的。
      3. 最终向上向量 (final up vector) $\mathbf{u} = \mathbf{v} \times \mathbf{r}$ (保证归一化且垂直)。
    • 相机变换矩阵 $\mathbf{M}$ 首先将所有物体平移,使相机位置在原点,然后改变基,使 $\mathbf{r}$ 与 (1,0,0) 对齐,$\mathbf{u}$ 与 (0,1,0) 对齐,$\mathbf{v}$ 与 (0,0,1) 对齐。 $$\mathbf{M} = \begin{pmatrix} r_x & r_y & r_z & 0 \\ u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} 1 & 0 & 0 & -c_x \\ 0 & 1 & 0 & -c_y \\ 0 & 0 & 1 & -c_z \\ 0 & 0 & 0 & 1 \end{pmatrix} = \begin{pmatrix} r_x & r_y & r_z & -\mathbf{r} \cdot \mathbf{c} \\ u_x & u_y & u_z & -\mathbf{u} \cdot \mathbf{c} \\ v_x & v_y & v_z & -\mathbf{v} \cdot \mathbf{c} \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (基于公式4.20的解释, $\mathbf{t}$ 在书中是 $\mathbf{c}$) (注意:平移矩阵 $\mathbf{T}(-\mathbf{c})$ 在右边,因为它首先作用于顶点。)

4.1.7 法线变换

  • 用于变换点、线、三角形的矩阵不能总是用于变换曲面法线(或顶点光照法线)。
  • 如果模型被非均匀缩放,使用相同矩阵变换法线会导致法线不再垂直于曲面。
  • 正确的方法是使用原变换矩阵的伴随矩阵的转置。伴随矩阵总能保证存在。
  • 传统答案是使用逆矩阵的转置 $(\mathbf{M}^{-1})^T$。这通常有效。
  • 如果矩阵是奇异的(行列式为零),则逆不存在。
  • 由于法线是向量,平移不影响它。大多数建模变换是仿射的,不改变w分量。在这种常见情况下,只需要计算左上 $3 \times 3$ 部分的伴随矩阵。
  • 如果变换矩阵仅由平移、旋转和均匀缩放操作串联而成:
    • 平移不影响法线。
    • 均匀缩放因子仅改变法线长度。
    • 旋转序列仍是旋转。
    • 在这种情况下,$(\mathbf{M}^{-1})^T = (\mathbf{S}^{-1}\mathbf{R}^{-1})^T = (\mathbf{S}^{-1}\mathbf{R}^T)^T = (\mathbf{R}^T)^T (\mathbf{S}^{-1})^T = \mathbf{R} (\mathbf{S}^{-1})^T$。由于均匀缩放 $S$ 是对角阵,$(S^{-1})^T = S^{-1}$。所以是 $\mathbf{R}S^{-1}$。
    • 如果仅有旋转,$(\mathbf{R}^{-1})^T = (\mathbf{R}^T)^T = \mathbf{R}$。所以原变换矩阵本身可用于变换法线。
    • 如果包含均匀缩放 $S_u$,则法线变换为 $S_u \mathbf{R}$。法线长度会改变。
  • 变换后的法线通常需要重新归一化。
  • 如果仅串联平移和旋转,法线长度不变,无需重新归一化。
  • 如果还串联了均匀缩放,可以用整体缩放因子直接归一化法线。
  • 切向量(Tangent vectors)总是直接由原始矩阵变换。

4.1.8 逆的计算

  • 计算矩阵逆的三种方法:
    1. 参数反转:如果矩阵是单个变换或简单变换序列且参数已知,例如 $\mathbf{M} = \mathbf{T}(\mathbf{t})\mathbf{R}(\phi)$,则 $\mathbf{M}^{-1} = \mathbf{R}(-\phi)\mathbf{T}(-\mathbf{t})$。简单且保持精度。
    2. 正交矩阵:如果矩阵已知是正交的(例如,任何旋转序列),则 $\mathbf{M}^{-1} = \mathbf{M}^T$。
    3. 通用方法:如果一无所知,则使用伴随矩阵法、克莱默法则、LU分解或高斯消元法。伴随矩阵法和克莱默法则因分支操作较少而更可取。
  • 优化:如果逆用于变换向量,则通常只需对矩阵的左上 $3 \times 3$ 部分求逆。

4.2 特殊矩阵变换和操作

4.2.1 欧拉变换

  • 一种直观的构造矩阵以确定方位的方法。
  • 默认视图方向通常沿负z轴,头部沿y轴。
  • 欧拉变换是三个旋转矩阵的乘积。一个常用顺序是: $$ \mathbf{E}(h, p, r) = \mathbf{R}_z(r)\mathbf{R}_x(p)\mathbf{R}_y(h) $$ (4.21) 其中 $h$ (head/yaw, 航向/偏航), $p$ (pitch, 俯仰), $r$ (roll, 滚转) 是欧拉角。
  • 顺序有24种可能。$\mathbf{E}$ 是正交的,其逆为 $\mathbf{E}^{-1} = \mathbf{E}^T = (\mathbf{R}_z\mathbf{R}_x\mathbf{R}_y)^T = \mathbf{R}_y^T \mathbf{R}_x^T \mathbf{R}_z^T$。
  • 欧拉角直观易懂,但有局限性(例如,难以在两组欧拉角之间插值,可能出现万向节死锁)。
  • 世界坐标系中,y轴向上(media-related modeling)或z轴向上(manufacturing, architecture, GIS)均常见。这只是一个90度旋转(可能还有反射)的差别。
  • 相机视图空间中的向上方向与其在世界空间中的向上方向没有必然联系。

4.2.2 从欧拉变换中提取参数

  • 从正交矩阵 $\mathbf{E}$ 中提取欧拉参数 $h, p, r$。设 $\mathbf{E}$ 为 $3 \times 3$ 旋转矩阵: $$ \mathbf{E}(h,p,r) = \begin{pmatrix} e_{00} & e_{01} & e_{02} \\ e_{10} & e_{11} & e_{12} \\ e_{20} & e_{21} & e_{22} \end{pmatrix} = \mathbf{R}_z(r)\mathbf{R}_x(p)\mathbf{R}_y(h) $$ (4.22)
  • 展开 $\mathbf{R}_z(r)\mathbf{R}_x(p)\mathbf{R}_y(h)$ 得到: $$ \mathbf{E} = \begin{pmatrix} \cos r \cos h - \sin r \sin p \sin h & -\sin r \cos p & \cos r \sin h + \sin r \sin p \cos h \\ \sin r \cos h + \cos r \sin p \sin h & \cos r \cos p & \sin r \sin h - \cos r \sin p \cos h \\ -\cos p \sin h & \sin p & \cos p \cos h \end{pmatrix} $$ (4.23)
  • 从 $e_{21} = \sin p$ 可得 $p$。
  • 从 $e_{01}/e_{11} = -\tan r$ 和 $e_{20}/e_{22} = -\tan h$ 可得 $r$ 和 $h$。
  • 提取公式(使用 atan2(y,x)): $$ h = \text{atan2}(-e_{20}, e_{22}) $$ $$ p = \arcsin(e_{21}) $$ $$ r = \text{atan2}(-e_{01}, e_{11}) $$ (4.25)
  • 万向节死锁 (Gimbal Lock):当 $\cos p = 0$ (即 $p = \pm \pi/2$) 时发生。此时 $r$ 和 $h$ 绕同一轴旋转。
    • 若 $\cos p = 0$,则 $e_{21} = \sin p = \pm 1$。
    • 可任意设 $h=0$。此时矩阵变为: $$\mathbf{E} = \begin{pmatrix} \cos r & \mp \sin r & 0 \\ \sin r & \pm \cos r & 0 \\ 0 & \pm 1 & 0 \end{pmatrix} \text{ (if } \sin p = \pm 1, \cos p = 0 \text{)}$$ (原书Eq 4.26似乎基于特定假设简化,这里按 $h=0, p=\pm\pi/2$ 重构) 书中的 Eq 4.26 是: $$\mathbf{E} = \begin{pmatrix} \cos r & \sin r \sin p & \sin r \cos p \\ \sin r & -\cos r \sin p & -\cos r \cos p \\ 0 & \cos p & -\sin p \end{pmatrix} \text{ (after setting h=0 in original E from 4.23, not simplified for cos p = 0 yet)}$$ 当 $\cos p = 0$, $\sin p = \pm 1$: $$\mathbf{E} = \begin{pmatrix} \cos r & \pm \sin r & 0 \\ \sin r & \mp \cos r & 0 \\ 0 & 0 & \mp 1 \end{pmatrix} \text{ (for } h=0, p=\pm\pi/2 \text{ from 4.23)}$$ 在这种情况下,可以使用 $r = \text{atan2}(e_{10}, e_{00})$。
  • $\arcsin$ 的定义域导致 $-\pi/2 \le p \le \pi/2$。如果原始 $p$ 超出此范围,无法提取原始参数。
  • 欧拉角不唯一。
  • 万向节死锁:当旋转使得一个自由度丢失时发生。例如,x/y/z顺序,若y轴旋转 $\pi/2$,则局部z轴与原始x轴对齐,导致最后绕z的旋转变得冗余。
  • 不同的旋转顺序(如z/x/y或z/x/z)是可行的。z/x/z只有当绕x旋转 $\pi$ 弧度时才发生万向节死锁。没有完美的序列能避免万向节死锁。
  • 示例:约束变换
    • 将输入设备提供的任意旋转矩阵 $\mathbf{P}$ 约束为仅绕x轴旋转。
    • 从 $\mathbf{P}$ 中提取欧拉角 $h, p, r$。
    • 创建一个新矩阵 $\mathbf{R}_x(p')$(这里的$p'$是提取出的俯仰角,如果希望是绕物体局部x轴,则需要选择合适的欧拉角分量)。

4.2.3 矩阵分解

  • 从串联矩阵中检索各种变换(如缩放、旋转、平移、错切)的任务。
  • 用途:
    • 提取对象的缩放因子。
    • 获取特定系统所需的变换。
    • 确定模型是否仅经历了刚体变换。
    • 在动画关键帧之间插值。
    • 从旋转矩阵中移除错切。
  • 平移很容易检索(4x4矩阵的最后一列)。通过检查行列式是否为负来确定是否发生反射。
  • 分离旋转、缩放和错切更复杂。
  • Thomas、Goldman、Shoemake 等人提出了相关算法。Shoemake的算法适用于仿射矩阵,与参考系无关,并尝试分解矩阵以获得刚体变换。

4.2.4 绕任意轴旋转

  • 目标:创建一个变换,使实体绕归一化轴 $\mathbf{r}$ 旋转 $\alpha$ 弧度。

  • 方法1:基变换

    1. 构造旋转矩阵 $\mathbf{M}$,将轴 $\mathbf{r}$ 变换为x轴。
      • 找到与 $\mathbf{r}$ 正交的第二个轴 $\mathbf{s}$。一个数值稳定的方法是:找到 $\mathbf{r}$ 中绝对值最小的分量,将其设为0,交换另两个分量,然后将第一个非零分量取反。 $$\mathbf{\bar{s}} = \begin{cases} (0, -r_z, r_y), & \text{if } |r_x| \le |r_y| \text{ and } |r_x| \le |r_z| \\ (-r_z, 0, r_x), & \text{if } |r_y| \le |r_x| \text{ and } |r_y| \le |r_z| \\ (-r_y, r_x, 0), & \text{if } |r_z| \le |r_x| \text{ and } |r_z| \le |r_y| \end{cases}$$ (4.27)
      • $\mathbf{s} = \mathbf{\bar{s}} / ||\mathbf{\bar{s}}||$
      • 第三个轴 $\mathbf{t} = \mathbf{r} \times \mathbf{s}$。
      • $(\mathbf{r}, \mathbf{s}, \mathbf{t})$ 构成正交基。
      • 矩阵 $\mathbf{M}$ 的行是 $\mathbf{r}^T, \mathbf{s}^T, \mathbf{t}^T$ (如果M将r变换到x轴,则M的逆的列是r,s,t,或者说M的行是r,s,t) 书中给出: $$\mathbf{M} = \begin{pmatrix} \mathbf{r}^T \\ \mathbf{s}^T \\ \mathbf{t}^T \end{pmatrix} \quad \text{(This matrix transforms (r,s,t) basis to (x,y,z) basis if applied to coordinates in (r,s,t) basis.)}$$ 或者,如果 M 的列是 r, s, t,则它将 (x,y,z) 基向量变换为 r,s,t。为了将 r 对齐到 x 轴,我们需要 $M \mathbf{r} = \mathbf{e}_x$。所以 $M$ 的第一行应该是 $\mathbf{r}^T$ (如果 $\mathbf{r}$ 是单位向量)。 The text says: “This matrix transforms the vector r into the x-axis, s into the y-axis, and t into the z-axis.” This means $M\mathbf{r} = (1,0,0)^T$, $M\mathbf{s} = (0,1,0)^T$, $M\mathbf{t} = (0,0,1)^T$. This is achieved if the rows of $M$ are $\mathbf{r}, \mathbf{s}, \mathbf{t}$ (assuming r,s,t are row vectors) or if the columns of $M^{-1}$ are $\mathbf{r}, \mathbf{s}, \mathbf{t}$ (column vectors). The matrix given by Eq 4.28 is: $$\mathbf{M} = \begin{pmatrix} r_x & r_y & r_z \\ s_x & s_y & s_z \\ t_x & t_y & t_z \end{pmatrix}$$ (This means $\mathbf{r}^T$ is the first row, etc.)
    2. 执行实际旋转 $\mathbf{R}_x(\alpha)$。
    3. 使用 $\mathbf{M}^{-1}$ (即 $\mathbf{M}^T$ 因为 $\mathbf{M}$ 是正交的) 变换回来。
    • 最终变换 $\mathbf{X}$: $$\mathbf{X} = \mathbf{M}^T \mathbf{R}_x(\alpha) \mathbf{M}$$ (4.29) (Note: In step 1, M transforms from world to a space where r is x-axis. So you apply M first, then Rx, then M_inv. So $X = M^{-1} R_x M = M^T R_x M$)
  • $$\mathbf{R} = \begin{pmatrix} \cos\phi + (1-\cos\phi)r_x^2 & (1-\cos\phi)r_x r_y - r_z \sin\phi & (1-\cos\phi)r_x r_z + r_y \sin\phi \\ (1-\cos\phi)r_y r_x + r_z \sin\phi & \cos\phi + (1-\cos\phi)r_y^2 & (1-\cos\phi)r_y r_z - r_x \sin\phi \\ (1-\cos\phi)r_z r_x - r_y \sin\phi & (1-\cos\phi)r_z r_y + r_x \sin\phi & \cos\phi + (1-\cos\phi)r_z^2 \end{pmatrix}$$

    (这是一个3x3矩阵,需要扩展到4x4用于齐次坐标) (基于公式4.30)

4.3 四元数

  • 1843年由哈密顿发明,1985年由Shoemake引入计算机图形学。
  • 用于表示旋转和方向。优于欧拉角和矩阵:
    • 任何三维方向可表示为绕特定轴的单次旋转,易于与四元数相互转换。
    • 可用于方向的稳定和恒定插值 (SLERP)。
  • 四元数有四个部分,通常表示为 $\hat{\mathbf{q}}$。

4.3.1 数学背景

  • 定义:一个四元数 $\hat{\mathbf{q}}$ $$\hat{\mathbf{q}} = (\mathbf{q}_v, q_w) = iq_x + jq_y + kq_z + q_w = \mathbf{q}_v + q_w$$ 其中 $\mathbf{q}_v = (q_x, q_y, q_z)$ 是虚部,$q_w$ 是实部。 $i, j, k$ 是虚数单位:$i^2 = j^2 = k^2 = -1$,$jk = -kj = i$, $ki = -ik = j$, $ij = -ji = k$。(4.31)
  • 乘法:$\hat{\mathbf{q}}\hat{\mathbf{r}} = (\mathbf{q}_v \times \mathbf{r}_v + r_w\mathbf{q}_v + q_w\mathbf{r}_v, q_w r_w - \mathbf{q}_v \cdot \mathbf{r}_v)$ (4.32)
  • 加法:$\hat{\mathbf{q}} + \hat{\mathbf{r}} = (\mathbf{q}_v + \mathbf{r}_v, q_w + r_w)$
  • 共轭:$\hat{\mathbf{q}}^* = (-\mathbf{q}_v, q_w)$
  • 范数 (norm):$n(\hat{\mathbf{q}}) = \sqrt{\hat{\mathbf{q}}\hat{\mathbf{q}}^*} = \sqrt{\mathbf{q}_v \cdot \mathbf{q}_v + q_w^2} = \sqrt{q_x^2+q_y^2+q_z^2+q_w^2}$ (4.33)
  • 单位元 (Identity):$\hat{\mathbf{i}} = (\mathbf{0}, 1)$
  • (Inverse):$\hat{\mathbf{q}}^{-1} = \frac{1}{n(\hat{\mathbf{q}})^2} \hat{\mathbf{q}}^*$ (4.35)
  • 规则
    • 共轭规则:$(\hat{\mathbf{q}}^*)^* = \hat{\mathbf{q}}$, $(\hat{\mathbf{q}} + \hat{\mathbf{r}})^* = \hat{\mathbf{q}}^* + \hat{\mathbf{r}}^*$, $(\hat{\mathbf{q}}\hat{\mathbf{r}})^* = \hat{\mathbf{r}}^*\hat{\mathbf{q}}^*$ (4.36)
    • 范数规则:$n(\hat{\mathbf{q}}^*) = n(\hat{\mathbf{q}})$, $n(\hat{\mathbf{q}}\hat{\mathbf{r}}) = n(\hat{\mathbf{q}})n(\hat{\mathbf{r}})$ (4.37)
    • 乘法律:线性性,结合律 (4.38)
  • 单位四元数:$n(\hat{\mathbf{q}}) = 1$。可写为 $\hat{\mathbf{q}} = (\sin\phi \mathbf{u}_q, \cos\phi) = \sin\phi \mathbf{u}_q + \cos\phi$,其中 $||\mathbf{u}_q||=1$。(4.39)
  • 指数形式:$\hat{\mathbf{q}} = \sin\phi \mathbf{u}_q + \cos\phi = e^{\phi \mathbf{u}_q}$ (4.41)
  • 对数:$\log(\hat{\mathbf{q}}) = \phi \mathbf{u}_q$ (4.42)
  • :$\hat{\mathbf{q}}^t = \sin(\phi t)\mathbf{u}_q + \cos(\phi t)$ (4.42)

4.3.2 四元数变换

  • 单位四元数可以表示任何三维旋转。

  • 将点/向量 $\mathbf{p}=(p_x, p_y, p_z, p_w)^T$ 的坐标放入四元数 $\hat{\mathbf{p}}$ (通常 $p_w=0$ for vector, $p_w=1$ for point in terms of what it conceptually represents, but for rotation, the point is treated as pure quaternion $\hat{\mathbf{p}}=(p_x,p_y,p_z,0)$)。

  • 对于单位四元数 $\hat{\mathbf{q}} = (\sin\phi \mathbf{u}_q, \cos\phi)$,变换 $\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^{-1}$ (或 $\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^*$ 因为 $\hat{\mathbf{q}}$ 是单位的) 将 $\hat{\mathbf{p}}$ (以及点 $\mathbf{p}$) 绕轴 $\mathbf{u}_q$ 旋转 $2\phi$ 弧度。(4.43)

  • $\hat{\mathbf{q}}$ 和 $-\hat{\mathbf{q}}$ 表示相同的旋转。

  • 串联:先应用 $\hat{\mathbf{q}}$ 再应用 $\hat{\mathbf{r}}$ 于 $\hat{\mathbf{p}}$:$\hat{\mathbf{r}}(\hat{\mathbf{q}}\hat{\mathbf{p}}\hat{\mathbf{q}}^*)\hat{\mathbf{r}}^* = (\hat{\mathbf{r}}\hat{\mathbf{q}})\hat{\mathbf{p}}(\hat{\mathbf{r}}\hat{\mathbf{q}})^* = \hat{\mathbf{c}}\hat{\mathbf{p}}\hat{\mathbf{c}}^*$,其中 $\hat{\mathbf{c}} = \hat{\mathbf{r}}\hat{\mathbf{q}}$。(4.44)

  • 矩阵转换

    • 四元数 $\hat{\mathbf{q}}=(q_x,q_y,q_z,q_w)$ 到矩阵 $\mathbf{M}_q$ ($s=2/(n(\hat{\mathbf{q}}))^2$,对于单位四元数 $s=2$): $$\mathbf{M}_q = \begin{pmatrix} 1 - s(q_y^2+q_z^2) & s(q_x q_y - q_w q_z) & s(q_x q_z + q_w q_y) & 0 \\ s(q_x q_y + q_w q_z) & 1 - s(q_x^2+q_z^2) & s(q_y q_z - q_w q_x) & 0 \\ s(q_x q_z - q_w q_y) & s(q_y q_z + q_w q_x) & 1 - s(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ 对于单位四元数 ($s=2$): $$\mathbf{M}_q = \begin{pmatrix} 1 - 2(q_y^2+q_z^2) & 2(q_x q_y - q_w q_z) & 2(q_x q_z + q_w q_y) & 0 \\ 2(q_x q_y + q_w q_z) & 1 - 2(q_x^2+q_z^2) & 2(q_y q_z - q_w q_x) & 0 \\ 2(q_x q_z - q_w q_y) & 2(q_y q_z + q_w q_x) & 1 - 2(q_x^2+q_y^2) & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.46)
    • 正交矩阵 $\mathbf{M}_q$ (取其左上3x3部分 $m_{ij}$) 到单位四元数 $\hat{\mathbf{q}}$: $m_{21}-m_{12} = 4q_w q_x$, $m_{02}-m_{20} = 4q_w q_y$, $m_{10}-m_{01} = 4q_w q_z$ (4.47) $tr(\mathbf{M}_q) = m_{00}+m_{11}+m_{22}+m_{33}$ (对于4x4)。对于单位四元数的旋转矩阵(4.46), $m_{33}=1$, 且 $tr(M_{3x3}) = 4q_w^2-1$, 所以 $tr(M_q) = 4q_w^2$. $$q_w = \frac{1}{2}\sqrt{tr(\mathbf{M}_q)}$$ $$q_x = (m_{21}-m_{12})/(4q_w)$$ $$q_y = (m_{02}-m_{20})/(4q_w)$$ $$q_z = (m_{10}-m_{01})/(4q_w)$$ (4.49) 为数值稳定,避免除以小的 $q_w$: 如果 $q_w$ 最大,使用上述公式。否则,基于以下关系(其中 $m_{33}$ 是 $M_q$ 的(3,3)元素,即1): $4q_x^2 = +m_{00} - m_{11} - m_{22} + m_{33}$ $4q_y^2 = -m_{00} + m_{11} - m_{22} + m_{33}$ $4q_z^2 = -m_{00} - m_{11} + m_{22} + m_{33}$ $4q_w^2 = tr(\mathbf{M}_q)$ (实际上是 $m_{00}+m_{11}+m_{22}+m_{33}$,而 $m_{33}=1$ 对于旋转矩阵的4x4形式) (基于公式 4.51) 先计算 $q_x, q_y, q_z, q_w$ 中最大的一个,然后用 (4.47) 计算其余分量。
  • 球面线性插值 (Slerp)

    • 在两个单位四元数 $\hat{\mathbf{q}}$ 和 $\hat{\mathbf{r}}$ 之间,根据参数 $t \in [0,1]$ 计算插值四元数。
    • 代数形式:$\hat{\mathbf{s}}(\hat{\mathbf{q}}, \hat{\mathbf{r}}, t) = (\hat{\mathbf{r}}\hat{\mathbf{q}}^{-1})^t \hat{\mathbf{q}}$ (4.52)
    • 实现形式: $$\text{slerp}(\hat{\mathbf{q}}, \hat{\mathbf{r}}, t) = \frac{\sin(\phi(1-t))}{\sin\phi}\hat{\mathbf{q}} + \frac{\sin(\phi t)}{\sin\phi}\hat{\mathbf{r}}$$ (4.53) 其中 $\cos\phi = q_x r_x + q_y r_y + q_z r_z + q_w r_w = \hat{\mathbf{q}} \cdot \hat{\mathbf{r}}$。
    • Slerp 计算的是四维单位球面上从 $\hat{\mathbf{q}}$ 到 $\hat{\mathbf{r}}$ 的最短弧(测地线)。
    • Slerp 计算昂贵。有快速增量方法或近似方法。
  • 球面样条插值 (Squad)

    • 用于平滑插值一系列方向 $\hat{\mathbf{q}}_0, \hat{\mathbf{q}}_1, \dots, \hat{\mathbf{q}}_{n-1}$,避免Slerp连接处的急转。
    • 在 $\hat{\mathbf{q}}_i$ 和 $\hat{\mathbf{q}}_{i+1}$ 之间引入辅助四元数 $\hat{\mathbf{a}}_i, \hat{\mathbf{a}}_{i+1}$。 $$\hat{\mathbf{a}}_i = \hat{\mathbf{q}}_i \exp\left[-\frac{\log(\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i-1}) + \log(\hat{\mathbf{q}}_i^{-1}\hat{\mathbf{q}}_{i+1})}{4}\right]$$ (4.54)
    • Squad 函数: $$\text{squad}(\hat{\mathbf{q}}_i, \hat{\mathbf{q}}_{i+1}, \hat{\mathbf{a}}_i, \hat{\mathbf{a}}_{i+1}, t) = \text{slerp}(\text{slerp}(\hat{\mathbf{q}}_i, \hat{\mathbf{q}}_{i+1}, t), \text{slerp}(\hat{\mathbf{a}}_i, \hat{\mathbf{a}}_{i+1}, t), 2t(1-t))$$ (4.55)
  • 从一个向量旋转到另一个向量

    • 将方向 $\mathbf{s}$ 旋转到方向 $\mathbf{t}$ (最短路径)。归一化 $\mathbf{s}, \mathbf{t}$。
    • 旋转轴 $\mathbf{u} = (\mathbf{s} \times \mathbf{t}) / ||\mathbf{s} \times \mathbf{t}||$。
    • $\mathbf{s} \cdot \mathbf{t} = \cos(2\phi)$,$||\mathbf{s} \times \mathbf{t}|| = \sin(2\phi)$,其中 $2\phi$ 是 $\mathbf{s}$ 和 $\mathbf{t}$ 之间的夹角。
    • 旋转四元数 $\hat{\mathbf{q}} = (\sin\phi \mathbf{u}, \cos\phi)$。
    • 简化形式 (令 $e = \mathbf{s} \cdot \mathbf{t}$): $$\hat{\mathbf{q}} = (\mathbf{q}_v, q_w) = \left( \frac{1}{\sqrt{2(1+e)}}(\mathbf{s} \times \mathbf{t}), \frac{\sqrt{2(1+e)}}{2} \right)$$ (4.56) 避免 $\mathbf{s}, \mathbf{t}$ 方向相近时的数值不稳定。若方向相反 ($e \approx -1$),则需特殊处理。
    • 对应的旋转矩阵 $\mathbf{R}(\mathbf{s}, \mathbf{t})$ (3x3): 令 $\mathbf{v} = \mathbf{s} \times \mathbf{t}$, $e = \mathbf{s} \cdot \mathbf{t}$, $h = (1-e)/(\mathbf{v} \cdot \mathbf{v}) = 1/(1+e)$ (若 $\mathbf{v} \cdot \mathbf{v} \neq 0$) $$\mathbf{R}(\mathbf{s}, \mathbf{t}) = \begin{pmatrix} e+hv_x^2 & hv_x v_y - v_z & hv_x v_z + v_y \\ hv_y v_x + v_z & e+hv_y^2 & hv_y v_z - v_x \\ hv_z v_x - v_y & hv_z v_y + v_x & e+hv_z^2 \end{pmatrix}$$ (4.57) 当 $\mathbf{s}, \mathbf{t}$ 平行或近乎平行时需注意。

4.4 顶点混合 (Vertex Blending)

  • 也称线性混合蒙皮 (linear-blend skinning),包络 (enveloping),骨骼子空间变形 (skeleton-subspace deformation)。
  • 解决刚性部件在关节处动画不自然的问题(如肘部)。
  • 方法:
    • 模型具有骨骼 (bones)。
    • 每个顶点可以受多个骨骼变换的影响,通过权重进行混合。
    • 整个网格通常称为皮肤 (skin)。
  • 数学公式:变换后的顶点 $\mathbf{u}(t)$ $$\mathbf{u}(t) = \sum_{i=0}^{n-1} w_i \mathbf{B}_i(t) \mathbf{M}_i^{-1} \mathbf{p}$$ 其中 $\sum_{i=0}^{n-1} w_i = 1, w_i \ge 0$。(4.59)
    • $\mathbf{p}$ 是原始顶点(通常在模型/绑定姿势坐标中)。
    • $w_i$ 是骨骼 $i$ 对顶点 $\mathbf{p}$ 的权重。
    • $\mathbf{M}_i$ 是从初始骨骼 $i$ 的坐标系到世界(或模型)坐标的变换(绑定矩阵的逆)。它将顶点从世界/模型空间转换到骨骼 $i$ 的初始(绑定)姿势空间。$\mathbf{M}_i^{-1}$ 将顶点从骨骼的绑定空间转换到模型空间。
    • $\mathbf{B}_i(t)$ 是骨骼 $i$ 随时间 $t$ 变化的当前世界变换矩阵。
    • $\mathbf{B}_i(t)\mathbf{M}_i^{-1}$ 将原始顶点 $\mathbf{p}$ (在模型空间) 变换到骨骼 $i$ 当前姿态下的位置。
  • 实践中,$\mathbf{B}_i(t)$ 和 $\mathbf{M}_i^{-1}$ 为每个骨骼每帧预先串联。
  • 顶点 $\mathbf{p}$ 被不同骨骼的串联矩阵变换,然后使用权重 $w_i$ 进行混合。
  • 法线通常也可以用相同公式变换,但如果骨骼有非均匀缩放,可能需要 $(\mathbf{B}_i(t)\mathbf{M}_i^{-1})^{-T}$。
  • 优点:非常适合GPU。顶点数据只需发送一次。每帧只需更新骨骼矩阵。
  • 缺点:可能出现不必要的折叠、扭曲和自相交(如“糖果包装纸”效应)。
  • 改进:双四元数 (Dual Quaternions) 蒙皮。有助于保持原始变换的刚性,避免扭曲。计算成本略高于线性混合。但可能导致膨胀效应。
  • 其他替代方案:旋转中心蒙皮 (center-of-rotation skinning)。

4.5 变形 (Morphing)

  • 从一个三维模型平滑过渡到另一个三维模型。

  • 涉及两个主要问题:

    1. 顶点对应问题:为两个可能具有不同拓扑、顶点数和连接性的模型建立顶点间的对应关系。这是一个难题。
    2. 插值问题
  • 如果模型间存在一对一的顶点对应关系,则插值可以在逐顶点基础上进行。

  • 线性插值:对于时间 $t \in [t_0, t_1]$,计算 $s = (t-t_0)/(t_1-t_0)$。 变形后的顶点 $\mathbf{m} = (1-s)\mathbf{p}_0 + s\mathbf{p}_1$ (4.60) 其中 $\mathbf{p}_0, \mathbf{p}_1$ 是同一顶点在不同时间(或不同模型中)的位置。

  • 变形目标 (Morph Targets) 或 混合形状 (Blend Shapes)

    • 提供更直观的控制。
    • 有一个中性模型 $\mathbf{N}$ (neutral model)。
    • 有一组 $k$ 个不同的姿势模型 $\mathbf{P}_i$。
    • 预处理:计算“差异模型” $\mathbf{D}_i = \mathbf{P}_i - \mathbf{N}$。
    • 变形后的模型 $\mathbf{M}$: $$\mathbf{M} = \mathbf{N} + \sum_{i=1}^k w_i \mathbf{D}_i$$ (4.61) 其中 $w_i$ 是权重,可以为负或大于1。
    • 允许独立操纵模型的不同特征。
    • 姿势空间变形 (Pose-space deformation) 结合了顶点混合和变形目标。

4.6 几何缓存回放 (Geometry Cache Playback)

  • 用于过场动画等需要极高质量动画的场景。
  • 朴素方法:存储所有帧的所有顶点,数据量巨大。
  • Gneiting 提出的减少内存成本的方法(可达10%):
    1. 量化 (Quantization):例如,位置和纹理坐标用16位整数存储。有损压缩。
    2. 空间和时间预测 (Spatial and Temporal Prediction)
      • 空间压缩:如平行四边形预测。对于三角带,下一个顶点的预测位置是当前三角形在其平面内围绕当前三角形边的反射。编码与此预测位置的差异。
      • 时间压缩:类似MPEG。每n帧进行一次空间压缩。中间帧使用时间预测(例如,基于前一帧的运动增量)。
    • 这些技术足以用于实时流式传输数据。

4.7 投影 (Projections)

  • 在渲染场景之前,所有相关对象必须投影到某个平面或简单体积中。
  • 透视投影矩阵会影响w分量,并且其底行通常不是 (0 0 0 1)。通常需要齐次化。
  • 本节假设观察者沿相机负z轴观看,y轴向上,x轴向右(右手坐标系)。

4.7.1 正交投影 (Orthographic Projection)

  • 特点:平行线在投影后仍然平行。对象大小不随与相机距离而改变。
  • 简单正交投影矩阵 $\mathbf{P}_o$ (投影到 $z=0$ 平面): $$\mathbf{P}_o = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.62) 此矩阵不可逆。
  • 更常用的正交投影矩阵由六元组 $(l, r, b, t, n, f)$ 定义(左、右、底、顶、近、远平面)。它将这些平面形成的轴对齐包围盒 (AABB) 缩放和平移到规范视景体 (canonical view volume)。
    • AABB 最小角 $(l,b,n)$,最大角 $(r,t,f)$。注意 $n > f$ (沿负z轴看)。
    • OpenGL规范视景体:最小角 $(-1,-1,-1)$,最大角 $(1,1,1)$。
    • DirectX规范视景体:最小角 $(-1,-1,0)$,最大角 $(1,1,1)$。
    • 变换到规范视景体是为了更有效地进行裁剪。
  • OpenGL的正交投影矩阵 (将AABB映射到 $[-1,1]^3$): $$\mathbf{P}_o = \mathbf{S}(\mathbf{s})\mathbf{T}(\mathbf{t}) = \begin{pmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.63) (注意 $f-n$ 在这里应该是 $n-f$ 以保持符号一致性,如果 $n>f$。或者用 $2/(n-f)$ 和 $-(n+f)/(n-f)$ for z. 书中用 $f-n$,这暗示$f$是较远的Z值,$n$是较近的Z值,且 $ff$ (例如 $n=1, f=-1$),那么 $f-n$ 是负数,这会翻转Z。) 假设 $n$ 和 $f$ 是沿负z轴的距离,所以 $n_{val} < f_{val}$ 但它们是负的,例如 $n=-1, f=-10$。那么 $n_{param}=n_{val}, f_{param}=f_{val}$。 $r-l, t-b$ 是正的。$f-n$ (例如 $-10 - (-1) = -9$)。 如果 $n$ 和 $f$ 代表到视点的距离(正值),且视点看向负Z,则近平面在 $z=-n$,远平面在 $z=-f$。此时要求 $0 < n < f$。那么AABB的z范围是 $[-f, -n]$。 此时 $z_{new} = \frac{2(z - (- (f+n)/2))}{(-n) - (-f)} = \frac{2z + (f+n)}{f-n}$ The matrix from text has $2/(f-n)$ and $-(f+n)/(f-n)$ for z. If $f$ (far plane) and $n$ (near plane) are distances along negative z-axis, e.g. $n=-1, f=-10$. Then $f-n = -10 - (-1) = -9$. This matches the text’s convention that $n > f$ for numerical values (e.g. $n=-1, f=-2$). The text states: “It is important to realize that $n > f$, because we are looking down the negative z-axis”. This implies $n$ is ’less negative’ than $f$. E.g., $n=-5, f=-100$. So $n > f$. Then $f-n$ is negative. This means the $z$ scaling factor $2/(f-n)$ is negative, causing a reflection on $z$. This changes handedness.
  • 这个变换通常包含一个镜像变换,将右手观察坐标系转换为左手规范化设备坐标。
  • DirectX正交投影矩阵 (映射到 $[-1,1]^2 \times [0,1]$): $$\mathbf{P}_{o[0,1]} = \begin{pmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{1}{f-n} & -\frac{n}{f-n} \\ 0 & 0 & 0 & 1 \end{pmatrix}$$ (4.66) (同样,如果 $n>f$, $f-n$ 为负。) (通常以转置形式显示)

4.7.2 透视投影 (Perspective Projection)

  • 平行线通常在投影后不再平行,可能汇聚到一点。物体越远显得越小。

  • $$\mathbf{P}_p = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 \end{pmatrix}$$$$\mathbf{P}_p = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & -d \\ 0 & 0 & -1/d & 0 \end{pmatrix}$$$$\mathbf{P}_p = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & -d \\ 0 & 0 & 1/(-d) & 1 \end{pmatrix} \text{ (If mapping z to -d and keeping z in numerator for depth)}$$$$ \mathbf{P}_p = \begin{pmatrix} -d & 0 & 0 & 0 \\ 0 & -d & 0 & 0 \\ 0 & 0 & -d & 0 \\ 0 & 0 & 1 & 0 \end{pmatrix} \text{ This is not a general perspective matrix, but results in correct x,y,z after divide by } p_z$$$$\mathbf{P}_p = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1/d & 0 \end{pmatrix}$$

    If this is applied to $(p_x, p_y, p_z, 1)^T$, we get $(p_x, p_y, p_z, -p_z/d)^T$. After homogenization: $(-d \cdot p_x/p_z, -d \cdot p_y/p_z, -d, 1)^T$. This interpretation of Eq 4.68, as used in Eq 4.69, is correct for $x,y,z$.

  • 视锥体 (View Frustum) 到规范视景体

    • 视锥体由 $(l, r, b, t, n, f)$ 定义,其中 $0 > n > f$ (或 $n < f < 0$ in actual values, like $n=-1, f=-10$).

    • OpenGL 规范视景体是 $[-1,1]^3$。

    • 视野 (Field of View, FOV): $\phi = 2 \arctan(w/(2d))$ (4.70)

    • $$ \mathbf{P}_p = \begin{pmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{pmatrix} $$

      (4.71) (The signs for $(r+l)/(r-l)$ and $(t+b)/(t-b)$ seem to be in the third column not fourth, suggesting it’s for $x_s = Ax_e + B z_e$, not $x_s = Ax_e + B$. This is common. $x_{clip} = \frac{2n}{r-l}x_e + \frac{r+l}{r-l}z_e$. $w_{clip} = -z_e$. So $x_{ndc} = \frac{2n}{r-l}\frac{x_e}{-z_e} + \frac{r+l}{r-l}\frac{z_e}{-z_e}$ )

      The $(r+l)/(r-l)$ term is typically $m_{02}$ (or $A_{20}$ if row major).

      The $(f+n)/(f-n)$ and $2fn/(f-n)$ are $m_{22}$ and $m_{23}$. The $-1$ is $m_{32}$. This matrix maps $z=n \to -1$ and $z=f \to 1$.

    • 应用此变换后,点为 $\mathbf{q}=(q_x,q_y,q_z,q_w)^T$。需除以 $q_w$ 进行齐次化:$\mathbf{p}=(q_x/q_w, q_y/q_w, q_z/q_w, 1)$。

    • $$ \mathbf{P}_p = \begin{pmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -1 & -2n \\ 0 & 0 & -1 & 0 \end{pmatrix} $$

      (4.73) (assuming $f+n \approx f$, $f-n \approx f$, $2fn/(f-n) \approx 2n$)

  • $$\mathbf{P}_{\text{OpenGL}} = \begin{pmatrix} \frac{2n'}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n'}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f'+n'}{f'-n'} & -\frac{2f'n'}{f'-n'} \\ 0 & 0 & -1 & 0 \end{pmatrix}$$

    (4.74)

  • $$\mathbf{P}_{\text{OpenGL}} = \begin{pmatrix} c/a & 0 & 0 & 0 \\ 0 & c & 0 & 0 \\ 0 & 0 & -\frac{f'+n'}{f'-n'} & -\frac{2f'n'}{f'-n'} \\ 0 & 0 & -1 & 0 \end{pmatrix}$$

    (4.75)

  • $$ \mathbf{P}_{p[0,1]} = \begin{pmatrix} \frac{2n'}{r-l} & 0 & -\frac{r+l}{r-l} & 0 \\ 0 & \frac{2n'}{t-b} & -\frac{t+b}{t-b} & 0 \\ 0 & 0 & \frac{f'}{f'-n'} & -\frac{f'n'}{f'-n'} \\ 0 & 0 & 1 & 0 \end{pmatrix} $$

    (4.76) (注意符号差异,因为DirectX是左手系,看向+Z,所以 (r+l)/(r-l) 项的符号可能不同或定义不同) The book has $-(r+l)/(r-l)$ for DirectX. This is correct for LH system if $l,r$ are defined symmetrically.

  • 深度值非线性

    • 变换后,规范化设备坐标 (NDC) 中的深度 $z_{NDC}$ 与输入深度 $p_z$ (相机空间z值) 成反比: $z_{NDC} = d - e/p_z$ (4.78) (对于OpenGL投影, $z_{NDC} \in [-1,1]$)
    • 这导致靠近远平面的深度值精度较低。
  • 提高深度精度的方法

    • 反向Z (Reversed Z):存储 $1.0 - z_{NDC}$。对于浮点或整数深度缓冲都有好处。浮点缓冲与反向Z提供最佳精度。
    • 对数深度 (Logarithmic Depth):例如,Kemen提出的 $z = w \log_2(\max(10^{-6}, 1+w))f_c/2$ (DirectX)。$f_c = 2/\log_2(f+1)$。(4.79)
    • 多视锥体 (Multiple Frusta):将视锥体在深度方向分割成多个子视锥体,分别渲染。

4.8 延伸阅读和资源

  • 交互式线性代数网站 [1718]。
  • Farin and Hansford《The Geometry Toolbox》[461]。
  • Lengyel《Mathematics for 3D Game Programming and Computer Graphics》[1025]。
  • 《Graphics Gems》系列。
  • Golub and Van Loan《Matrix Computations》[556]。
  • 关于四元数可视化和插值的文章。
  • 关于变形技术的综述。
  • Parent《Computer Animation》[1354]。