logo
Published on

第一章 序言 & 介绍

Authors

序言

本版《计算机图形学基础》对有关着色、光反射和路径追踪的材料进行了大量的重写,并对全文进行了许多修正。

本书现在更好地介绍了以物理为基础的材料和基于物理的渲染为名的技术,这些技术在实际实践中正变得越来越主流。这些材料现在整合得更好,我们认为这本书很好地反映了许多教师目前组织图形课程的方式。

本书的组织结构与第四版基本相似。多年来,我们一直在修订这本书,努力保留早期版本所特有的非正式、直观的呈现风格,同时提高一致性、准确性和完整性。我们希望读者发现, 本书将会是学习计算机图形学非常好的基石.

在线资源查看: https://www.cs.cornell.edu/

第一章——介绍

计算机图形学这一术语描述了使用计算机创建和处理图像的任何方式。

本书介绍了可用于创建各种图像(逼真的视觉效果、信息丰富的技术插图或精美的计算机动画)的算法和数学工具。图形可以是二维或三维的;图像可以是完全合成的,也可以是通过处理照片生成的。本书介绍了基本算法和数学,尤其是用于生成三维物体和场景合成图像的算法和数学。

实际上,进行计算机图形学不可避免地需要了解特定的硬件、文件格式,通常还需要了解一个或两个图形 API(参见第 1.3 节)。

计算机图形学是一个快速发展的领域,因此该知识的具体内容是一个不断变化的目标。因此,在本书中,我们尽力避免依赖任何特定的硬件或 API。鼓励读者使用与其软件和硬件环境相关的文档来补充文本。幸运的是,计算机图形学文化具有足够多的标准术语和概念,本书中的讨论应该可以很好地映射到大多数环境。 本章定义了一些基本术语,并提供了一些历史背景,以及与计算机图形学相关的信息来源。

1.1 图形学分类

分类是有时是不准确的, 但是大多数图形从业者都同意计算机图形学的以下主要领域:

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

还有许多其他领域涉及计算机图形学,至于它们是否是核心图形学领域,则见仁见智。本文至少会涉及这些领域。这些相关领域包括:

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

1.2 主要的应用

几乎任何工作都可以使用计算机图形,但计算机图形技术的主要消费者包括以下行业:

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

1.3图形学API

使用图形库的一个关键部分是处理图形 API。

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

每个图形程序都需要能够使用两个相关的 API:用于视觉输出的图形 API 和用于从用户获取输入的用户界面 API。目前,图形和用户界面 API 有两种主流范例。

第一种是集成方法,以 Java 为例,其中图形和用户界面工具包是集成的、可移植的软件包,它们完全标准化并作为语言的一部分得到支持。

第二种以 Direct3D 和 OpenGL 为代表,其中绘图命令是与 C++ 等语言绑定的软件库的一部分,而用户界面软件是一个独立的实体,可能因系统而异。在后一种方法中,编写可移植代码是有问题的,尽管对于简单的程序,可以使用可移植库层来封装系统特定的用户界面代码。无论您选择哪种 API,基本的图形调用大致相同,并且本书的概念也是适用的。

1.4图形学管线pipline

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

尽管以有效的从后到前的顺序绘制三角形曾经是计算机图形学中最重要的研究问题,但现在几乎总是使用 z 缓冲区来解决,它使用特殊的内存缓冲区以蛮力的方式解决问题。事实证明,图形管道中使用的几何操作几乎完全可以在由三个传统几何坐标和第四个有助于透视观察的齐次坐标组成的 4D 坐标空间中完成。

这些 4D 坐标使用 4 × 4 矩阵和 4维向量进行操作。因此,图形管道包含许多用于有效处理和组合此类矩阵和向量的机制。

这个 4D 坐标系是计算机科学中最微妙和最美丽的构造之一,它无疑是学习计算机图形学时最大的智力障碍。每本图形书的第一部分都涉及这些坐标。生成图像的速度在很大程度上取决于绘制的三角形数量。因为在许多应用中,交互性比视觉质量更重要,所以有必要尽量减少用于表示模型的三角形数量。

此外,如果从远处查看模型,则所需的三角形数量会比从近处查看模型时少。这表明,使用不同的细节级别 (LOD) 来表示模型很有用。

1.5 数值问题

许多图形程序实际上只是 3D 数字代码。数字问题在此类程序中通常至关重要。

在“过去”,很难以稳健和可移植的方式处理此类问题,因为机器对数字有不同的内部表示,更糟糕的是,以不同且不兼容的方式处理异常。

幸运的是,几乎所有现代计算机都符合 IEEE 浮点标准(IEEE 标准协会,1985 年)。这使得程序员可以对如何处理某些数字条件做出许多方便的假设。

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

  1. 无穷大 (∞)。这是大于所有其他有效数字的有效数字。
  2. 负无穷大 (−∞)。这是小于所有其他有效数字的有效数字。
  3. 非数字 (NaN)。这是由具有未定义结果的运算产生的无效数字,例如零除以零。

IEEE 浮点数的设计者做出了一些对程序员来说极为方便的决定。其中许多与上述处理异常(例如除以零)的三个特殊值有关。在这些情况下,会记录异常,但在许多情况下,程序员可以忽略它。

具体来说,对于任何正实数 a,以下涉及除以无穷值的规则成立:

+a/(+∞) = +0,

−a/(+∞) = −0,

+a/(−∞) = −0,

−a/(−∞) = +0.

对于涉及到∞值到运算, 行为如下(a > 0):

∞ + ∞ = +∞,

∞−∞ = NaN,

∞×∞ = ∞,

∞/∞ = NaN,

∞/a = ∞,

∞/0 = ∞,

0/0 = NaN.

涉及到判断无穷大的值的规则如下:

  1. 所有有限有效数字都小于 +∞。
  2. 所有有限有效数字都大于 −∞。
  3. −∞ 小于 +∞。 涉及具有 NaN 值的表达式的规则很简单:
  4. 任何包含 NaN 的算术表达式都会产生 NaN。
  5. 任何涉及 NaN 的布尔表达式都为假。

IEEE 浮点数最有用的方面可能是如何处理除以零的情况;对于任何正实数 a,以下涉及除以零值的规则都成立:

+a/ +0 = +∞,

−a/ +0 = −∞.

遵循上述IEEE 规则,许多数值计算就会变得简单得多。例如,考虑以下表达式:

a=11b+1ca = \frac{1}{\frac{1}{b} + \frac{1}{c}}

上面的表达式,如果除以零导致程序崩溃(在 IEEE 浮点之前的许多系统中都是如此),则需要两个 if 语句来检查 b 或 c 是否为小值或零值。

但是,对于 IEEE 浮点,如果 b 或 c 为零,我们将得到所需的 a=0。避免特殊检查的另一种常用方法是利用 NaN 的布尔属性。请考虑以下代码段:

a = f(x)
if (a > 0) then
do something

这里,函数 f 可能会返回“不正常”的数值,例如 ∞ 或 NaN, 但 if 条件仍然定义明确:对于 a = NaN 或 a = −∞,它为假;对于 a = +∞,它为真。

如果仔细决定返回哪些值,if 通常可以做出正确的选择,而无需进行特殊检查。这使得程序更小、更强大、更高效。

1.6 效率

没有任何可以让代码更高效的魔法。效率是通过仔细的权衡利弊得失来实现的,而这些权衡对于不同的架构是不同的。但是,在未来,一个好的启发式方法是程序员应该更多地关注内存访问模式而不是操作计数。

这与二十年前最好的启发式方法正好相反。这种转变的发生是因为内存的速度没有跟上处理器的速度。

由于这种趋势持续下去,有限和一致的内存访问对于优化的重要性只会增加。一个让代码运行得更快的合理方法是按照以下顺序进行,就是只采取那些必要的步骤:

  1. 尽可能以最直接的方式编写代码。根据需要即时计算中间结果,而不是存储它们。
  2. 以优化模式编译。
  3. 使用任何现有的分析工具来查找关键瓶颈。
  4. 检查数据结构以寻找改善局部性的方法。如果可能,使数据单元大小与目标架构上的缓存/页面大小相匹配。
  5. 如果分析发现数值计算存在瓶颈,请检查编译器生成的汇编代码以查找效率缺失。重写源代码以解决您发现的任何问题。

这些步骤中最重要的是第一步。 大多数“优化”都会使代码更难阅读,而不会加快速度。 此外,花在前期优化代码上的时间通常比花在纠正错误或添加功能上的时间更好。 另外,要小心旧文本中的建议;一些经典技巧,例如使用整数代替实数,可能不再能提高速度,因为现代 CPU 通常可以像执行整数运算一样快地执行浮点运算。在所有情况下,都需要进行分析以确保针对特定机器和编译器的任何优化的优点。

1.7 图形学程序设计和编程

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

1.7.1 类的设计

任何图形程序的一个关键部分是拥有良好的类或实例, 用于几何实体(例如矢量和矩阵)以及图形实体(例如 RGB 颜色和图像)。 这些实例应尽可能简洁高效。

一个通用的设计问题是,位置和位移是否应该作为单独的类,因为它们具有不同的操作; 例如,位置乘以一半没有几何意义,而位移的一半有几何意义(Goldman,1985;DeRose,1989) 这个问题几乎没有一致意见,这可能会引发图形从业者数小时的激烈争论,但为了举例说明,我们假设我们不会做出区分。

这意味着要编写一些基本类,包括:

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

此外,您可能想要或可能不想为区间、正交基和坐标框架添加类。

1.7.2 浮点数与双精度数

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

1.7.3 调试图形学程序

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

  • 基于科学方法的调试: 在图形程序中,有一种替代传统调试的方法,通常非常有用。 这种调试方法的缺点是,它与计算机程序员在职业生涯早期被教导不要做的事情非常相似,所以如果你这样做,你可能会觉得这有一点奇怪: 我们创建一个图像并观察它有什么问题。然后,我们提出一个关于导致问题的原因的假设并对其进行测试。

    例如,在光线追踪程序中,我们可能有许多看起来有些随机的暗像素。这是大多数人在编写光线追踪器时遇到的经典“阴影痤疮”问题。 传统的调试在这里没有帮助;相反,我们必须意识到阴影光线正在照射被阴影的表面。 我们可能会注意到,黑点的颜色是环境光的颜色,所以直接照明是缺失的。直接照明可以在阴影中关闭,因此您可能会假设这些点被错误地标记为阴影中,

    但实际上并非如此。为了验证这个假设,我们可以关闭阴影检查并重新编译。这将表明这些是错误的阴影测试,我们可以继续我们的侦查工作。 这种方法有时可以成为良好实践的关键原因是,我们从来不需要发现错误值或真正确定我们的概念错误。 相反,我们只是通过实验缩小了我们的概念错误范围。通常,只需要几次试验就可以追踪到问题,这种调试方式是令人愉快的。

  • 在调试过程中输出图片: 在许多情况下,从图形程序中获取调试信息的最简单渠道是输出图像本身。 如果您想知道针对每个像素运行的计算的一部分中某个变量的值,您可以临时修改程序以将该值直接复制到输出图像并跳过通常会进行的其余计算。

    例如,如果您怀疑表面法线的问题导致了阴影问题,您可以将法线向量直接复制到图像(x 变为红色,y 变为绿色,z 变为蓝色),从而产生实际用于计算的向量的彩色编码说明。 或者,如果您怀疑某个特定值有时超出其有效范围,请让您的程序在发生这种情况的地方写入亮红色像素。 其他常见技巧包括用明显的颜色绘制表面的背面(当它们不应该可见时),根据对象的 ID 号为图像着色,或根据它们计算的工作量为像素着色。

  • 使用调试器

    仍然存在一些情况,特别是当基于科学方法似乎导致矛盾时,观察到底发生了什么是关键, 问题在于,图形程序通常涉及多次执行相同的代码(例如,每个像素一次,或每个三角形一次), 因此从一开始就在调试器中逐步执行完全不切实际。

    而且最困难的错误通常只发生在复杂的输入中。一个有用的方法是“为错误设置陷阱”。 首先,确保您的程序是确定性的 - 在单个线程中运行它,并确保所有随机数都是从固定种子计算出来的。 然后,找出哪个像素或三角形表现出错误,并在您怀疑不正确的代码之前添加一个语句,该语句将仅在可疑情况下执行。 例如,如果您发现像素(126,247)表现出错误,则添加

        if x = 126 and y = 247 then
        print “blarg!

    如果您在打印语句上设置断点,则可以在计算您感兴趣的像素之前进入调试器。一些调试器具有“条件断点”功能,可以在不修改代码的情况下实现相同的功能。 在程序崩溃的情况下,传统的调试器可用于 精确定位崩溃的位置。然后,您应该开始在程序中回溯,使用断言和重新编译,以查找程序出错的位置。这些 断言应该留在程序中,以防您将来添加的潜在错误。这再次意味着避免了传统的逐步过程,因为那不会为您的程序添加有价值的断言。

  • 调试时查看数据

    通常,很难理解程序在做什么,因为它在最终出错之前会计算大量中间结果。 这种情况类似于测量大量数据的科学实验,而一个解决方案是相同的:为自己制作好的图表和插图,以了解数据的含义。

    例如,在光线追踪器中,您可以编写代码来可视化光线树,以便您可以看到哪些路径对像素有贡献,或者在图像重采样例程中,您可以制作图表来显示从输入中获取样本的所有点。花费在编写代码以可视化程序内部状态上的时间也会在优化程序时更好地理解其行为。