• 翻译人员:StubbornHuang
  • 翻译程度:机翻+部分调整
  • 翻译时间:2021年11月8日

1 介绍

术语计算机图形描述了使用计算机来创建和处理图像的任何用途。 本书介绍了可用于创建各种图像的算法和数学工具——逼真的视觉效果、信息丰富的技术插图或精美的计算机动画。 图形可以是二维或三维的; 图像可以是完全合成的,也可以通过处理照片产生。 这本书是关于基本算法和数学的,尤其是那些用于生成 3D 对象和场景的合成图像的算法和数学。

实际做计算机图形不可避免地需要了解特定的硬件、文件格式,通常还需要了解一到两个图形 API(参见第 1.3 节)。计算机图形学是一个快速发展的领域,因此该知识的细节是不断发展变换的。 因此,在本书中,我们尽量避免依赖任何特定的硬件或 API。 鼓励读者使用适用于其软件和硬件环境的相关文档来补充文本。 幸运的是,计算机图形文化有足够的标准术语和概念,本书中的讨论应该很好地映射到大多数环境。

本章定义了一些基本术语并提供了一些历史背景以及与计算机图形相关的信息来源。

1.1 图形领域

在任何领域强加类别都是危险的,但大多数图形从业者都会同意以下是计算机图形的主要领域:

  • 建模是一种可以存储在计算机上的方式并用于处理形状和外观属性的数学规范。 例如,一个咖啡杯可能被描述为一组有序的 3D 点以及一些用于连接这些点的插值规则和一个描述光如何与杯子相互作用的反射模型。
  • 渲染是一个继承自艺术的术语,用于从3D 计算机模型创建阴影图像。
  • 动画是一种通过图像序列创造运动错觉的技术。 动画使用建模和渲染,但添加了随时间移动的关键问题,这在基本建模和渲染中通常不会处理。

涉及计算机图形的其他领域还有很多,是否属于图形领域核心仁者见仁智者见智。 这些都至少会在文中有所涉及。 此类相关领域包括以下:

  • 用户交互处理输入设备(如鼠标和平板电脑)、应用程序、图像中对用户的反馈以及其他感官反馈之间的接口。 从历史上看,这个领域与图形有关,主要是因为图形研究人员最早接触到了现在无处不在的输入/输出设备。
  • 虚拟现实试图让用户沉浸在 3D 虚拟世界中。 这通常至少需要立体图形和对头部运动的响应。 对于真正的虚拟现实,还应该提供声音和力反馈。 由于该领域需要先进的 3D 图形和先进的显示技术,因此往往与图形密切相关。
  • 可视化试图通过视觉显示让用户洞察复杂的信息。 通常在可视化问题中需要解决图形问题。
  • 图像处理处理 2D 图像的处理,用于图形和视觉领域。
  • 3D 扫描使用测距技术创建测量的 3D 模型。此类模型对于创建丰富的视觉图像很有用,并且此类模型的处理通常需要图形算法。
  • 计算摄影是使用计算机图形学、计算机视觉和图像处理方法来实现以摄影方式捕捉物体、场景和环境的新方法。

1.2 主要应用

几乎任何努力都可以使用计算机图形,但是计算机图形主要应用于以下消费行业:

  • 电子游戏越来越多地使用复杂的 3D 模型和渲染算法。
  • 动画片通常直接从 3D 模型渲染。 许多传统的 2D 卡通使用从 3D 模型渲染的背景,这允许在没有大量艺术家时间的情况下连续移动视点。
  • 视觉效果使用几乎所有类型的计算机图形技术。 几乎每部现代电影都使用数字合成来将背景与单独拍摄的前景叠加在一起。 许多电影还使用 3D 建模和动画来创建合成环境、物体,甚至大多数观众永远不会怀疑不是真实的角色。
  • 动画电影使用了许多用于视觉效果的相同技术,但不一定针对看起来真实的图像。
  • CAD/CAM 代表计算机辅助设计和计算机辅助制造。 这些领域使用计算机技术在计算机上设计零件和产品,然后使用这些虚拟设计来指导制造过程。 例如,许多机械零件是在 3D 计算机建模包中设计的,然后在计算机控制的铣削设备上自动生产。
  • 模拟可以被认为是准确的视频游戏。 例如,飞行模拟器使用复杂的 3D 图形来模拟驾驶飞机的体验。 此类模拟对于安全关键领域(例如驾驶)的初始培训以及对有经验的用户的情景培训(例如成本太高或无法物理创建的特定消防情况)非常有用。
  • 医学成像为扫描的患者数据创建有意义的图像。 例如,计算机断层扫描 (CT) 数据集由密度值的大型 3D 矩形阵列组成。 计算机图形用于创建阴影图像,帮助医生从这些数据中提取最显著的信息。
  • 信息可视化创建的数据图像不一定具有“自然”的视觉描述。 例如,十只不同股票价格的时间趋势没有明显的视觉描绘,但巧妙的绘图技术可以帮助人类看到这些数据中的模式。

1.3 图形APIs

使用图形库的一个关键部分是处理图形 API。 应用程序接口 (API) 是执行一组相关操作的标准函数集合,图形 API 是一组执行基本操作的函数,例如将图像和 3D 表面绘制到屏幕上的窗口中。

每个图形程序都需要能够使用两个相关的 API:用于视觉输出的图形 API 和用于从用户获取输入的用户界面 API。 目前有两种主要的图形和用户界面 API 范式。 第一种是集成方法,以 Java 为例,其中图形和用户界面工具包是集成的和可移植的包,完全标准化并作为语言的一部分得到支持。 第二种由 Direct3D 和 OpenGL 表示,其中绘图命令是与 C++ 等语言相关的软件库的一部分,而用户界面软件是一个独立的实体,可能因系统而异。 在后一种方法中,编写可移植代码是有问题的,尽管对于简单的程序,可以使用可移植库层来封装系统特定的用户界面代码。

无论您选择哪种 API,基本的图形调用都将大体相同,本书的概念也将适用。

1.4 图形管线

今天的每台台式计算机都拥有强大的 3D 图形管道。 这是一个特殊的软件/硬件子系统,可以有效地从透视图中绘制 3D 基元。 通常,这些系统针对处理具有共享顶点的 3D 三角形进行了优化。 管道中的基本操作将 3D 顶点位置映射到 2D 屏幕位置并对三角形进行着色,使它们看起来逼真并以适当的从后到前的顺序出现。

尽管有效的从后到前的顺序绘制三角形曾经是计算机图形学中最重要的研究问题,但现在几乎总是使用 z-buffer 来解决,它使用特殊的内存缓冲区暴力解决该问题。

事实证明,图形管道中使用的几何操作几乎完全可以在由三个传统几何坐标和第四个齐次坐标组成的 4D 坐标空间中完成,这有助于透视查看。 这些 4D 坐标使用 4 × 4 矩阵和 4 向量进行操作。 因此,图形管道包含许多用于有效处理和组合此类矩阵和向量的机制。 这个 4D 坐标系是计算机科学中使用的最微妙、最漂亮的结构之一,它无疑是学习计算机图形时要跨越的最大智力障碍。 每本图形学相关的书的第一部分的大部分内容都涉及这些内容。

生成图像的速度在很大程度上取决于绘制的三角形数量。 由于在许多应用程序中交互性比视觉质量更重要,因此尽量减少用于表示模型的三角形数量是值得的。 此外,如果在远处查看模型,与从近处查看模型时相比,需要的三角形更少。 这表明用不同的细节级别 (LOD) 表示模型很有用。

1.5 数学问题

许多图形程序实际上只是3D数字代码。在这类程序中,数值问题通常是至关重要的。在“旧时代”,很难以健壮且可移植的方式处理此类问题,因为机器对数字有不同的内部表示,更糟糕的是,以不同且不兼容的方式处理异常。幸运的是,几乎所有现代计算机都符合IEEE浮点标准(IEEE标准协会,1985)。这允许程序员对如何处理某些数值条件做出许多方便的假设。

虽然IEEE浮点运算有许多在编码数值算法时很有价值的特性,但对于图形中遇到的大多数情况,只有少数几个特性是至关重要的。首先,也是最重要的一点,是了解IEEE浮点运算中实数有三个“特殊”值:

  • 无穷大(\infty) :这是一个大于所有其他有效数字的有效数字。
  • 负无穷大(-\infty) :这是一个小于所有其他有效数字的有效数字。
  • 不是数字(NaN):这是一个无效的数字,由具有未定义结果的操作产生,例如零除以零。

IEEE浮点运算的设计者做出了一些对程序员来说非常方便的决定。其中许多与上面处理异常(如除零)时的三个特殊值有关。在这些情况下,会记录异常,但在许多情况下,程序员可以忽略该异常。具体来说,对于任何正实数a,以下涉及无穷大除法的规则:

\begin{aligned}
+a/\left ( + \infty \right ) = +0 \\
- a/\left ( + \infty \right ) = - 0 \\
+a/\left ( - \infty \right ) = -0 \\
- a/\left ( - \infty \right ) = +0 \\
\end{aligned}

其他涉及无穷大值的操作的行为方式与预期相同。同样,对于正值a,运算如下所示:

\begin{aligned}
\infty + \infty &=+\infty \\
\infty - \infty &=\mathrm{NaN} \\
\infty \times \infty &=\infty \\
\infty / \infty &=\mathrm{NaN} \\
\infty / a &=\infty \\
\infty / 0 &=\infty \\
0 / 0 &=\mathrm{NaN}
\end{aligned}

布尔表达式中涉及无限值的规则与预期的一样

  1. 所有有限的有效数字都小于+\infty.
  2. 所有有限有效数字都大于-\infty.
  3. -\infty小于+\infty.

涉及具有NaN值的表达式的规则很简单

  1. 任何包含NaN的算术表达式都会产生NaN。
  2. 任何涉及NaN的布尔表达式都是false。

也许IEEE浮点运算最有用的方面是如何处理被零除的问题;对于任何正实数a,以下涉及零值除的规则适用:

\begin{aligned} + a/+ 0=+ \infty \\
- a/+ 0=- \infty \\
\end{aligned}

如果程序员利用IEEE规则,许多数值计算会变得简单得多。例如,考虑表达式:

\begin{aligned}
a = \frac{1}{\frac{1}{b}+\frac{1}{c} }
\end{aligned}

电阻和透镜会产生这样的表达式。如果除以零导致程序崩溃(在IEEE浮点之前的许多系统中都是如此),则需要两个If语句来检查b或c的小值或零值。
相反,对于IEEE浮点,如果b或c为零,我们将根据需要得到a的零值。避免特殊检查的另一种常见技术是利用NaN的布尔属性。

考虑下面的代码段:

a = f(x)
if(a > 0) then
    dosomething

上面的代码中,函数f可能返回丑陋值,例如\infty或者NaN,当a=NaN或者a=-\infty时值为false,当a=+\infty时值为true。在决定返回哪些值时要小心,if通常可以做出正确的选择,而无需进行特殊检查。这使得程序更小、更健壮、更高效。

1.6 效率

没有什么神奇的规则可以让代码更高效。效率是通过谨慎的权衡来实现的,对于不同的体系结构,这些权衡是不同的。然而,在可预见的未来,一个很好的启发是程序员应该更多地关注内存访问模式,而不是操作计数。这与二十年前的最佳启发相反。发生此切换是因为内存的速度没有跟上处理器的速度。由于这一趋势持续下去,有限和一致的内存访问对于优化的重要性只会增加。

使代码快速的合理方法是按以下顺序进行,需要采取的步骤为:

  1. 以最简单的方式编写代码。根据需要动态计算中间结果,而不是存储它们;
  2. 以优化模式编译;
  3. 使用现有的任何分析工具查找关键瓶颈;
  4. 检查数据结构以寻找改进局部性的方法。如果可能,使数据单元大小与目标体系结构上的缓存/页面大小匹配;
  5. 如果分析揭示了数值计算中的瓶颈,请检查编译器生成的汇编代码是否有遗漏的效率。重写源代码以解决您发现的任何问题;

不会加快速度。此外,预先优化代码所花费的时间通常最好花在纠正错误或添加功能上。此外,要注意来自旧文本的建议;一些经典的技巧,如使用整数代替实数,可能不再产生速度,因为现代CPU通常可以执行浮点运算,其速度与执行整数运算的速度一样快。在所有情况下,都需要对特定机器和编译器进行分析,以确保任何优化的优点。

1.7 图形程序的设计与编码

某些常用的策略在图形编程中通常很有用。在本节中,我们提供一些建议,您可能会发现这些建议对实现本书中学习的方法很有帮助。

1.7.1 类设计

任何图形程序的关键部分都是为几何实体(如向量和矩阵)以及图形实体(如RGB颜色和图像)提供良好的类或例程。这些例程应该尽可能的简单和有效。一个普遍的设计问题是位置和位移是否应该是单独的类别,因为它们具有不同的操作,例如,位置乘以一半没有几何意义,而位移的一半有几何意义(Goldman,1985;DeRose,1989)。在这个问题上几乎没有一致意见,这可能会在图形从业者中引发数小时的激烈辩论,但为了举例说明,我们假设我们不会做出区分。

一些要编写的基本类包括:

  • vector2:存储x和y分量的二维向量类。它应该将这些组件存储在长度为2的数组中,以便能够很好地支持索引操作符。还应该包括向量加法、向量减法、点积、叉积、标量乘法和标量除法的操作。
  • vector3:类似于vector2的3D向量类。
  • hvector:四分量齐次向量(见第7章)。
  • rgb:存储三个组件的RGB颜色。还应包括RGB加法、RGB减法、RGB乘法、标量乘法和标量除法的操作。
  • transform:用于变换的4×4矩阵。应包括矩阵乘法和成员函数,以应用于位置、方向和曲面法向量。如第6章所示,这些都是不同的。
  • image:具有输出操作的RGB像素的2D阵列。

此外,您可能希望也可能不希望为间隔、正交基和坐标系添加类。

1.7.2 Float类型 vs Double类型

现代结构表明,降低内存使用和保持一致的内存访问是提高效率的关键。建议使用单精度数据。然而,为了避免数值问题,建议使用双精度类型。权衡取决于程序,但最好在类定义中有一个默认值。

1.7.3 调试图形程序

如果你四处打听,你可能会发现,随着程序员越来越有经验,他们越来越少使用传统的调试器。这样做的一个原因是,对于复杂的程序来说,使用这样的调试器比对于简单的程序来说更难。另一个原因是,最困难的错误是概念错误,在概念错误中实现了错误的东西,很容易浪费大量时间在变量值中单步执行,而没有检测到这种情况。我们发现有几种调试策略在图形中特别有用。

科学方法

在图形程序中,有一种替代传统调试的方法,这种方法通常非常有用。它的缺点是,它与计算机程序员在职业生涯早期被教导不要做的事情非常相似,因此如果你这样做,你可能会感到“淘气”:我们创建一个图像并观察它有什么问题。然后,我们提出一个关于问题起因的假设,并对其进行检验。例如,在光线跟踪程序中,我们可能有许多看起来有些随机的暗像素。这是典型的“暗影粉刺”问题,大多数人在写光线跟踪器时都会遇到。传统的调试在这里没有帮助;相反,我们必须意识到阴影光线正在撞击正在着色的曲面。我们可能会注意到,暗点的颜色是环境色,因此缺少的是直接照明。可以在阴影中关闭直接照明,因此您可能会假设这些点未正确标记为阴影中的点时,它们被错误标记为阴影中的点。为了验证这个假设,我们可以关闭阴影检查并重新编译。这表明这些是假阴影测试,我们可以继续我们的侦查工作。这种方法有时可以成为良好实践的关键原因是,我们从不需要发现错误的值或真正确定我们的概念错误。相反,我们只是在实验上缩小了概念错误的范围。通常情况下,只需进行几次试验就可以跟踪情况,这种类型的调试是令人愉快的。

作为编码调试输出的图像

在许多情况下,从图形程序中获取调试信息的最简单渠道是输出图像本身。如果您想知道针对每个像素运行的部分计算的某个变量的值,您可以临时修改程序,将该值直接复制到输出图像,并跳过通常会完成的其余计算。例如,如果怀疑曲面法线的问题导致着色问题,则可以将法线向量直接复制到图像(x变为红色,y变为绿色,z变为蓝色),从而生成计算中实际使用的向量的颜色编码图示。或者,如果您怀疑某个特定值有时超出其有效范围,请让您的程序在发生这种情况的地方写入亮红色像素。其他常见技巧包括用明显的颜色(当它们不应该是可见的)绘制表面的背面,根据对象的ID号为图像着色,或根据计算所需的工作量为像素着色。

使用调试器

仍然有一些例子,特别是当科学方法似乎导致了一种矛盾时,没有什么可以替代精确观察正在发生的事情。问题在于,图形程序通常涉及对同一代码的多次执行(例如,每像素一次,或每三角形一次),这使得从一开始就在调试器中单步执行完全不切实际。最困难的错误通常只发生在复杂的输入上。

一种有用的方法是为bug“设置陷阱”。首先,确保您的程序是确定性的,在单个线程中运行它,并确保所有随机数都是从固定种子计算出来的。然后,找出哪个像素或三角形显示了使用固定随机数种子的特殊调试模式。在您怀疑的不正确代码之前添加一条语句,该语句将仅针对可疑情况执行。例如,如果您发现像素(126247)显示错误,则添加:

if x = 126 and y = 247 then
    print "blarg"

如果在print语句上设置断点,则可以在计算感兴趣的像素之前进入调试器。有些调试器具有“条件断点”功能,可以在不修改代码的情况下实现相同的功能。

在程序崩溃的情况下,传统的调试器有助于确定崩溃的位置。然后,您应该开始在程序中使用断言和重新编译进行回溯,以找到程序出错的地方。这些断言应该留在程序中,以便将来添加潜在的bug。这同样意味着避免了传统的单步执行过程,因为这不会将有价值的断言添加到程序中。

用于调试的数据可视化

通常很难理解你的程序在做什么,因为它在最终出错之前计算了很多中间结果。 这种情况类似于一个测量大量数据的科学实验,一个解决方案是一样的:为自己制作好的图表和插图,以了解数据的含义。例如,在光线追踪器中,您可以编写代码来可视化光线树,以便您可以看到哪些路径对像素有贡献,或者在图像重采样例程中,您可以绘制显示从输入中获取样本的所有点的图。在编写代码以可视化程序内部状态上的所花的时间也会在优化程序时更好地理解其行为。