第一章 介绍


此翻译自学使用,才疏学浅,如有错误还望指正。

概览


img

正文


计算机图形学[computer graphics]这一术语描述了任何使用计算机来创建和编辑图像的方法。这本书介绍了能够用于创造各种图片(真实的视觉效果、信息技术插图、和美丽的计算机动画)的算法和数学工具。图形学可以是二维或者三维;图像能过够完全的合成或者由不断变化的照片制作。这本书是关于算法原理和数学的,尤其是那些用于制作三维物体和场景去合成图像的。

实际上做图形学不可避免的需要关于特定硬件文件格式、通常还有一两个图形API(application program interface,应用程序接口)(见1.3节)的知识。计算机图形学是一个快速发展的领域,所以这些知识的细节是一个移动靶。因此,在这本书中我们尽我们最大的努力去避免依赖任何特定的硬件或API。鼓励读者为他们的软件和硬件环境的相关文件补充文本。幸运的是,计算机图形学的文化有足够的标准术语和概念,本书中的讨论应该很好地映射到大多数环境中。

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

1.1 图形领域

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

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

还有许多其他领域也涉及到计算机图形,至于它们是否为核心图形领域还有待商榷。这些相关领域在课文中至少会涉及到,包括:

  • 用户交互(User interaction) 用户交互处理输入设备(如鼠标和平板电脑)之间的界面,应用程序,以图像形式反馈给用户,以及其他临时反馈。在历史上,这个领域主要与图形有关,因为图形研究人员最早使用了现在无处不在的输入/输出设备。
  • 虚拟现实(User interaction,VR) VR试图让用户沉浸在3D虚拟世界中。这通常需要至少立体图形和对头部运动的响应。对于真实的VR,还应该提供声音和力量反馈。因为这个领域需要先进的3D图形和先进的显示技术,所以经常与图形学紧密关联。
  • 可视化(Visualization) 可视化试图通过可视化显示让用户了解复杂的信息。在视觉化的问题中,经常有图形的问题需要解决。
  • 图像处理(Image processing) 图像处理处理的是二维图像的操作,在图形和视觉领域都有应用。
  • 3D扫描(3D scanning) 3D扫描使用测距[range-finding]技术创建测量的3D模型。这样的模型对于创建丰富的视觉图像非常有用,而这些模型的处理通常需要图形算法。
  • 计算摄影学(Computational photography) 计算摄影是利用计算机图形学、计算机视觉和图像处理方法来实现摄影捕捉物体、场景和环境的新方法。

1.2 主要应用

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

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

1.3 图形API

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

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

无论你选择什么样的API,基本的图形调用大致上都是相同的,本书的概念都适用。

1.4 图形管线(Graphics Pipeline)

今天的每台台式电脑都有一个强大的3D图形管线。这是一个特殊的有效绘制3D图元[primitives]在透视图上的软件/硬件子系统。通常这些系统都是为处理共享顶点的3D三角形而优化的。管线中的基本操作将3D顶点位置映射到2D屏幕位置,并为三角形着色,以便它们看起来都真实,并按照正确的前后顺序显示。

虽然以有效的前后顺序绘制三角形曾经是计算机图形学中最重要的研究问题,但现在几乎都是使用Z-buffer来解决,它使用一种特殊的内存缓冲区来以一种粗暴的方式解决问题。

事实证明,在图形管线中使用的几何操作可以几乎完全在4D坐标空间中完成,该空间由三个传统几何坐标和第四个齐次坐标组成,有助于透视。这些4D坐标是用4×4矩阵和4矢量来操作的。因此,图形管线包含许多有效处理和组合这些矩阵和向量的机器。这个4D坐标系是计算机科学中最精巧和最美丽的构造之一,它无疑是学习计算机图形学时要跨越的最大智力障碍[ intellectual hurdle]。每本图形学书籍的第一部分都有很大一部分涉及这些坐标。

生成图像的速度很大程度上取决于绘制的三角形的数量。因为交互性[interactivity]在许多应用程序中比视觉质量更重要,所以有必要最小化[minimize]用于表示模型的三角形数量。此外,如果在远处查看模型,则需要的三角形比从较近距离查看模型时要少。这个建议用不同的多层次细节(level of detail,LOD)来表示模型是有用的。

1.5 数值问题

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

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

  1. 无穷大[Infinity] (∞)。这是一个大于所有其他有效数字的有效数字。
  2. 负无穷大[Minus infinity] (-∞)。这是一个小于所有其他有效数字的有效数字。
  3. 非数字[Not a number] (NaN)。这是一个无效的数字,产生于具有未定义结果的操作,例如零除以零。

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

img

EEE浮点数有两种表示0的方式,一种被视为正的,另一种被视为负的。之间的区别-0和+0只是偶尔会起作用,但当它起作用的时候,我们需要记住它。

其他涉及无穷大值的操作的行为方式与人们预期的一样。同样对于正a,其行为如下:

img

布尔表达式中包含无穷大值的规则与预期的一样:

  1. 所有有限有效数都小于+∞。
  2. 所有有限有效数都大于-∞。
  3. −∞ 小于 +∞。

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

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

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

img

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

img

这种表达出现在电阻和透镜上。如果被零除导致程序崩溃(在IEEE浮点之前的许多系统中都是这样),那么需要两个If语句来检查b或c的小值或零值。相反,对于IEEE浮点,如果b或c为零,我们将根据需要获得a的零值。另一种避免特殊检查的常用技术是利用NaN的布尔属性。考虑下面的代码段:

img

在这里,函数f可能返回“丑陋[ugly]”的值,例如∞或NaN,但是if条件仍然定义良好:对于a=NaN或a=-∞为false,对于a=+∞为true。在决定返回哪些值时,if通常可以做出正确的选择,而不需要进行特殊检查。这使得程序更小、更健壮、更高效

1.6 效能

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

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

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

这些步骤中最重要的是第一步。大多数“优化”在不加快速度的情况下使代码更难阅读。此外,预先优化代码所花费的时间通常更好地用于纠正错误或添加特性。另外,要注意来自旧文本的建议;一些经典的技巧,如使用整数而不是实数,可能不再产生速度,因为现代CPU通常可以像执行整数一样快速地执行浮点运算操作。在所有情况下,都需要通过分析来确定针对特定机器和编译器的任何优化是值得的

1.7 设计和编码图形程序

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

我坚信KISS(“keep it simple,stuble”[保持简单、傻瓜])原则,从这个角度来看,两个类的争论并不足以证明增加的复杂性是合理的。 -P.S. 我喜欢将点和向量分开,因为这样可以使代码更具可读性,并且可以让编译器捕捉到一些错误。 -S.M.

1.7.1 类的设计

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

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

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

此外,你可能想也可能不想为间隔[intervals]、正交基[orthonormal bases]和坐标系[coordinate frames]添加类。

你也可以考虑为单位长度向量创建一个特殊的类,虽然我发现他们的痛苦超过了他们的价值。 -P.S.

1.7.2 Float vs. Double

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

我建议在几何计算中使用双精度,在颜色计算中使用浮点。对于占用大量内存的数据,例如三角形网格,我建议存储float数据,但在通过成员函数访问数据时,将其转换为double。 -P.S.

我主张使用float进行所有计算,直到你发现代码的特定部分需要双精度。 -S.M.

1.7.3 调试图形程序

如果你四处打听,你可能会发现随着程序员越来越有经验,他们使用传统调试器的次数越来越少。造成这种情况的一个原因是,使用这种调试器对复杂的程序比对简单的程序更困难。另一个原因是,最困难的错误是概念性错误,在这些错误的情况下执行了错误的操作,而且很容易浪费大量时间来遍历变量值而不检测到这种情况。我们发现有几种调试策略在图形方面特别有用。

科学方法(The Scientific Method)

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

图像作为编码调试输出(Images as Coded Debugging Output)

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

使用调试器(Using a Debugger)

还有一些情况,特别是当科学方法似乎导致了矛盾,没有什么可以代替观察到底发生了什么的时候。问题在于,图形程序通常涉及到同一代码的多次执行(例如,逐像素执行一次,或者逐三角形执行一次),因此从一开始就在调试器中逐步执行是完全不现实的。而最困难的错误通常只发生在复杂的输入上。

一个有用的方法是为bug“设置一个陷阱”首先,确保你的程序是确定性的,在一个线程中运行它,并确保所有的随机数都是从固定的种子中计算出来的。然后,找出哪个像素或三角形显示了错误,并在你怀疑的不正确代码之前添加一个语句,该语句将仅针对可疑实例[case]执行。例如,如果你发现像素(126,247)显示了错误,那么添加:

img

使用固定随机数种子的特殊调试模式很有用。

如果在print语句上设置断点,则可以在计算感兴趣的像素之前将其放入调试器。有些调试器有一个“条件断点[conditional breakpoint]”特性,可以在不修改代码的情况下实现相同的功能。

在程序崩溃的情况下,传统的调试器对于查明崩溃的位置非常有用。然后,你应该在程序中开始回溯,使用断言[asserts]和重新编译,找出程序哪里出了问题。这些断言应该保留在程序中,以备将来添加的潜在bug。这再次意味着传统的逐步执行过程被避免了,因为这将不会向程序添加有价值的断言。

调试数据可视化(Data Visualization for Debugging)

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

我喜欢设置调试print语句的格式,这样输出恰好是一个Matlab或Gnuplot脚本,可以生成一个有用的绘图[plot] -S.M.

注意事项

软件工程的讨论受到Effective C++ 系列(Meyers,1995, 1997),Extreme Programming movement(Beck和Andres,2004)和 The Practice of Programming(Kernighan和Pike,1999)的影响。实验调试的讨论是基于与Steve Parker的讨论。

有许多与计算机图形学相关的年度会议,包括ACM SIGGRAPH和SIGGRAPH Asia、Graphics Interface、游戏开发者大会(Game Developers Conference,GDC)、欧洲图形(Eurographics)、太平洋图形(Pacific Graphics)、高性能图形(High Performance Graphics)、欧洲图形绘制研讨会(the Eurographics Symposium on Rendering)和IEEE VisWeek。这些在网上搜索他们的名字很容易找到。