【计算机图形学(译)】 一、介绍
- 1 介绍 Introduction
- 1.1 图形领域 (Graphics Areas)
- 1.2 主要应用 (Major Applications)
- 1.3 图形APls (Graphics APIs)
- 1.4 图形管线 (Graphics Pinpline)
- 1.5 数值问题 (Numerical Issues)
- 1.6 效率 (Efficiency)
- 1.7 设计和编写图形程序 (Designing and Coding Graphics Programs)
- 1.7.1 类设计 (Class Design)
- 1.7.2 Float vs. double
- 1.7.3 调试图形程序 (Debugging Graphics Programs)
1 介绍 Introduction
计算机图形学这个术语描述的是使用计算机来创建和操作图像。这本书介绍了算法和数学工具,可用于创建各种图像-现实的视觉效果,信息量大的技术插图,或好看的计算机动画。图形可以是二维或三维的;图像可以完全合成,也可以通过操纵照片来生成。这本书是关于基本的算法和数学,特别是那些用来产生三维物体和场景的合成图像。
实际上,做计算机图形处理不可避免地需要了解特定的硬件、文件格式,通常还需要一两个图形API(见1.3节)。计算机图形学是一个快速发展的领域,因此该知识的具体内容是不断变化的。因此,在本书中,我们尽量避免依赖于任何特定的硬件或API。鼓励读者为其软件和硬件环境补充相关文档。幸运的是,计算机图形学文化有足够多的标准术语和概念,本书中的讨论可以很好地映射到大多数环境中。
本章定义了一些基本术语,并提供了一些历史背景,以及与计算机图形学相关的信息来源。
1.1 图形领域 (Graphics Areas)
在任何领域强加分类都是危险的,但大多数图形从业者会同意以下计算机图形的主要领域:
- 建模 Modeling :是以一种可以存储在计算机上的方式处理形状和外观属性的数学规范。例如,一个咖啡杯可以被描述为一组有序的3D点,以及一些连接这些点的插值规则和一个描述光如何与杯子相互作用的反射模型。
- 渲染 Rending :是一个继承自艺术的术语,涉及从3D计算机模型中创建阴影图像。
- 动画 Animation : 是一种通过图像序列创造运动错觉的技术。动画使用建模和渲染,但增加了随时间移动的关键问题,这在基本的建模和渲染中通常不处理。
还有许多其他领域涉及计算机图形学,至于它们是否是核心图形学领域,则是见仁见智的问题。这些在本文中都会涉及到。这些相关领域包括:
- 用户交互 User interaction 处理输入设备(如鼠标和平板电脑)、应用程序、对用户的图像反馈和其他感官反馈之间的界面。从历史上看,这一领域主要与图形学有关,因为图形学研究人员最早接触到现在无处不在的输入/输出设备。
- 虚拟现实 Virtual reality 试图让用户沉浸在一个3D虚拟世界中。这通常至少需要立体图像和对头部运动的响应。对于真正的虚拟现实,声音和力的反馈也应该提供。由于这一领域需要先进的3D图形和先进的显示技术,它往往与图形密切相关。
- 可视化 Visualization 试图通过可视化显示让用户深入了解复杂的信息。通常,在可视化问题中需要解决图形问题。图像处理处理二维图像的操作,在图形学和视觉领域都有应用。
- 图像处理 Image processing 处理二维图像的操作,在图形学和视觉领域都有应用。
- 三维扫描 Three-dimensional scanning 使用测距技术来创建测量的三维模型。这样的模型对于创建丰富的视觉图像是有用的,并且这样的模型的处理通常需要图形算法。
- 计算摄影 Computational photography 是使用计算机图形学、计算机视觉和图像处理方法来实现以摄影方式捕捉物体、场景和环境的新方法。
1.2 主要应用 (Major Applications)
几乎任何行业都可以使用计算机图形学,但计算机图形学技术的主要用于以下消费行业:
- 电子游戏 Video games 越来越多地使用复杂的3D模型和渲染算法。
- 动画片 Cartoons 通常直接由3D模型渲染而成。许多传统的2D动画使用3D模型渲染的背景,这允许连续移动的视点,而不需要花费大量的美术师时间。
- 视觉效果 Visual effects 几乎使用了所有类型的计算机图形技术。几乎每一部现代电影都使用数字合成来叠加背景和单独拍摄的前景。许多电影还使用3D建模和动画来创造合成的环境、物体,甚至是大多数观众都不会怀疑是假的角色。
- 动画电影 Animated films 使用了许多与视觉效果相同的技术,但不一定以看起来真实的图像为目标。
- CAD/CAM 代表计算机辅助设计和计算机辅助制造。这些领域使用计算机技术在计算机上设计零件和产品,然后使用这些虚拟设计来指导制造过程。例如,许多机械零件是在3D计算建模包中设计的,然后在计算机控制的铣削设备上自动生产。
- 模拟 Simulation 可以被认为是精确的电子游戏。例如,飞行模拟器使用复杂的3D图形来模拟驾驶飞机的体验。这种模拟对于安全关键领域(如驾驶)的初始培训非常有用,对于有经验的用户(如成本太高或太危险而无法物理创建的特定消防情况)的场景培训也非常有用。
- 医学成像 Medical imaging 为扫描的患者数据创建有意义的图像。例如,计算机断层扫描(CT)数据集是由一个大型高密度值的三维矩形数组组成的。计算机图形学被用来创建阴影图像,帮助医生从这些数据中提取最显著的信息。
- 信息可视化 Information visualization 创建的数据图像不一定具有“自然”的视觉描述。例如,十种不同股票的价格的时间趋势没有明显的视觉描绘,但巧妙的绘图技术可以帮助人们看到这些数据中的模式。
1.3 图形APls (Graphics APIs)
使用图形库的一个关键部分是处理图形API。应用程序接口(API)是执行一组相关操作的标准函数集合,图形API是执行基本操作(如将图像和3D曲面绘制到屏幕上的窗口)的函数集。每个图形程序都需要能够使用两个相关的API:用于视觉输出的图形API和用于从用户获取输入的用户界面API。目前有两种主要的图形和用户界面应用程序。第一种是集成方法,以Java为例,其中图形和用户界面工具包是集成的,可移植的包是完全标准化的,并作为语言的一部分得到支持。第二种由Direct3D和OpenGL表示,其中绘图命令是绑定到语言(如c++)的软件库的一部分,用户界面软件是一个独立的实体,可能因系统而异。在后一种方法中,编写可移植代码是有问题的,尽管对于简单的程序,可以使用可移植库层来封装特定于系统的用户界面代码。
无论你选择什么API,基本的图形调用在很大程度上是相同的,本书的概念是通用的。
1.4 图形管线 (Graphics Pinpline)
今天的每台台式计算机都有一个强大的3D图形管线。这是一个特殊的软件/硬件子系统,可以有效地以透视方式绘制3D图元。通常,这些系统是为处理具有共享顶点的3D三角形而优化的。管线中的基本操作将3D顶点位置映射到2D屏幕位置,并为三角形加阴影,使它们看起来真实,并以正确的前后顺序出现。
虽然以有效的前后顺序绘制三角形曾经是计算机图形学中最重要的研究问题,但现在几乎总是使用z-buffer来解决,它使用一种特殊的内存缓冲区来以暴力方式解决问题。事实证明,图形管道中使用的几何操作几乎完全可以在由三个传统几何坐标和第四个有助于透视的齐次坐标组成的4D坐标空间中完成。这些4D坐标使用4 × 4矩阵和4向量进行操作。
因此,图形管道包含许多用于有效处理和组合这些矩阵和向量的机制。这个4D坐标系是计算机科学中使用的最微妙和最美丽的结构之一,它当然是学习计算机图形学时要跨越的最大障碍。每本图形书的第一部分都有很大一部分是处理这些坐标的。
生成图像的速度很大程度上取决于所绘制的三角形的数量。因为在许多应用程序中,交互性比视觉质量更重要,因此尽量减少用于表示模型的三角形数量是值得的。此外,如果从远处观看模型,需要的三角形比从较近的距离观看模型时要少。这表明用不同的细节级别(LOD)来表示模型是有用的。
1.5 数值问题 (Numerical Issues)
许多图形程序实际上只是3D数值代码。在这类程序中,数值问题通常是至关重要的。在“过去”,以健壮和可移植的方式处理此类问题非常困难,因为机器对数字有不同的内部表示,更糟糕的是,在不同机器上会以不同且不兼容的方式处理异常。幸运的是,几乎所有的现代计算机都符合IEEE浮点标准(IEEE Standards Association, 1985) 这允许程序员对如何处理某些数值条件做出许多方便的假设。
尽管IEEE浮点在编码数值算法时具有许多有价值的特性,但对于图形中遇到的大多数情况,只有少数几个是至关重要的。首先,也是最重要的,是要理解IEEE浮点中实数有三个“特殊”值:
- 无穷大 Infinity (∞) 这是一个大于所有其他有效数的有效数
- 无穷小 Minus Infinity (-∞)
- 非数 Not a number (NaN) 这是一个无效的数字,由具有未定义结果的操作产生,例如0除以0
IEEE浮点的设计者做出了一些对程序员来说极其方便的决定。其中许多与上面处理除零等异常时的三个特殊值有关。在这些情况下,会记录一个异常,但在许多情况下,程序员可以忽略它。具体来说,对于任何正实数a,以下涉及到被无限值除法的规则成立:
IEEE浮点数对零有两种表示,一种被视为正数,另一种被视为负数。
-0和+0之间的区别只是偶尔重要,但在某些情况下值得记住:
其他涉及无限值的操作的行为与我们所期望的一样。同样对于正a:
做布尔运算的时候,无穷数值的运算会有下面这样的规则:
- 所有有效的无穷数值都比+∞小
- 所有有效的无穷数值都比-∞大
- -∞比+∞小
对于哪些牵涉了NaN的表达式,规则比较简单:
- 任何牵涉了NaN的数值运算的结果是NaN
- 任何有NaN参与的布尔表达式的结果都是false
也许IEEE浮点运算最有用的方面是如何处理被零除的问题;对于任何正实数a,以下涉及零值除的规则适用:
如果程序员利用好IEEE的浮点数标准的话,很多数值运算会变得非常简单。比如,考虑到下面的这样一个表达式:
如果可能出现负零(- 0),则必须小心。
如果除零导致程序崩溃(在IEEE浮点之前的许多系统中都是如此),则需要两个If语句来检查b或c的小值或零值。相反,使用IEEE浮点,如果b或c为零,则我们将得到所需的a的零值。另一种避免特殊检查的常用技术是利用NaN的布尔属性。考虑以下代码:
a = f(x)
if (a > 0) then
do something
在这里,函数 f 可能返回错误的值,如∞或NaN,但if条件仍然定义良好:当a = NaN或a =-∞时为假,当a = +时为真。在决定返回哪些值时,通常if可以做出正确的选择,不需要进行特殊检查。这使得程序更小,更健壮,更高效。
1.6 效率 (Efficiency)
没有什么神奇的规则可以让代码更高效。效率是通过谨慎的权衡来实现的,这些权衡对于不同的体系结构是不同的。然而,在可预见的未来,一个很好的启发是程序员应该更加关注内存访问模式,而不是操作计数。这与20年前最好的启发式正好相反。出现这种切换是因为内存的速度跟不上处理器的速度。由于这种趋势的持续,有限的和连贯的内存访问对于优化的重要性只会增加。快速编写代码的合理方法是按照以下顺序进行,只执行需要的步骤:
- 以最直接的方式编写代码。根据需要实时计算中间结果,而不是存储它们。
- 以优化模式编译。
- 使用任何现有的分析工具来找到关键的瓶颈。
- 检查数据结构以寻找改进局部性的方法。如果可能的话,让数据单元大小与目标架构上的缓存/页面大小相匹配。
- 如果概要分析揭示了数值计算中的瓶颈,请检查编译器生成的程序集代码,以查找遗漏的效率。重写源代码以解决您发现的任何问题。
这些步骤中最重要的是第一步。大多数“优化”在没有加快速度的情况下使代码更难阅读。此外,花在优化代码上的时间通常比花在修改添加功能时的错误上更好。此外,要注意旧文本中的建议;一些经典技巧(例如使用整数而不是实数)可能不再能带来速度,因为现代cpu执行浮点运算的速度通常与执行整数运算一样快。在所有情况下,分析都是需要的,以确保对特定机器和编译器的任何优化的优点。
1.7 设计和编写图形程序 (Designing and Coding Graphics Programs)
坚信KISS(Keep it simple, stupid) 原则
(“保持简单和愚蠢”,意思是把事情弄得越简单、越傻瓜化越好) —— P.S
某些常见的策略在图形编程中通常是有用的。在本节中,我们提供了一些建议,你可能会发现实施书中学到的方法时会很有帮助。
我喜欢将点和向量分开,因为这使代码更具可读性,并且可以让编译器捕捉到一些错误。-S.M.
1.7.1 类设计 (Class Design)
任何图形程序的关键部分都是为几何实体(如向量和矩阵)以及图形实体(如RGB颜色和图像)提供良好的类或例程。这些例程应该尽可能地干净和高效。一个通用的设计问题是位置和位移是否应该是单独的类,因为它们有不同的操作:
例如,位置乘以1 / 2没有几何意义,而位移乘以1 / 2有几何意义(Goldman, 1985;DeRose, 1989)。对于这个问题几乎没有一致意见,这可能会在图形从业者之间引发数小时的激烈辩论,但为了举例,让我们假设我们不进行区分。这意味着要编写一些基本类比如:
- vector2 存储x和y分量的2D向量类。它应该将这些组件存储在一个长度为2的数组中,以便能够很好地支持索引操作符。还应该包括向量加法、向量减法、点积、叉乘、标量乘法和标量除法的运算。
- vector3 一个类似于vector2的3D向量类。
- hvector 有四个分量的齐次向量(见第八章)。
- rgb 存储三个组件的RGB颜色。您还应该包括RGB加法、RGB减法、RGB乘法、标量乘法和标量除法的操作。
- transform 一个用于变换的4 × 4矩阵。您应该包括一个矩阵乘法和成员函数,以应用于位置、方向和表面法向量。如第7章所示,这些都是不同的。
- image 带有输出操作的RGB像素的2D数组。
此外,你可以考虑为间隔、标准正交基和坐标框架添加类。
您还可以考虑为单位长度向量考虑一个特殊的类,尽管我发现它们的副作用超过了它们的价值。——P.S.
1.7.2 Float vs. double
现代架构建议保持低内存使用和保持一致的内存访问是提高效率的关键。这建议使用单精度数据。然而,避免数值问题建议使用双精度算法。折衷取决于程序,但是最好在类定义中有一个默认值。
1.7.3 调试图形程序 (Debugging Graphics Programs)
我建议使用double进行几何计算,使用float进行颜色计算。
对于占用大量内存的数据,例如三角形网格,我建议存储float数据,
但当通过成员函数访问数据时,可以将数据转换为double。
如果你四处打听,你可能会发现随着程序员越来越有经验,他们越来越少使用传统的调试器。其中一个原因是在复杂程序中使用这样的调试器比在简单程序中使用更尴尬。另一个原因是,最困难的错误是概念上的错误,即执行了错误的东西,并且很容易浪费大量的时间来逐步检查变量值而没有检测到这种情况。我们发现有几种调试策略在图形处理中特别有用。
我主张使用浮点数进行所有计算,直到发现在代码的特定部分需要双精度的证据。
科学方法(The Scientific Method)
在图形程序中,除了传统的调试之外,还有一种非常有用的方法。它的缺点是非常类似于程序员在职业生涯早期被教导不要做的事情,所以如果你这样做,你可能会觉得不合适:我们创建一个图像,观察它有什么问题。然后,我们提出一个关于问题原因的假设,并对其进行测试。
例如,在光线追踪程序中,我们可能有许多看起来有些随机的暗像素。这是大多数人在编写光线跟踪程序时遇到的经典的“阴影失真”(shadow acne)问题。
补充:阴影失真的根本原因是所生成的深度贴图的分辨率是有限的,就会造成在fragment位置处取深度图中的值发生采样就会出现问题。
传统的调试在这里没有帮助;相反,我们必须认识到阴影射线击中表面被遮蔽。我们可能会注意到黑点的颜色是环境色,所以直接照明是缺失的。直接照明可以在阴影中关闭,所以你可能会假设这些点被错误地标记为阴影中,而实际上它们不是。为了验证这个假设,我们可以关闭阴影检查并重新编译。这将表明这些是虚假的阴影测试,我们可以继续我们的工作。这种方法有时是很好的实践,我们从来不必发现错误的值或真正确定我们的概念错误。相反,我们只是缩小了实验上的概念误差。通常情况下,只需要进行几次试验就可以跟踪到问题所在,而且这种类型的调试是令人享受的。
图像作为编码调试输出(Image as Coded Debugging Output)
在许多情况下,从图形程序中获取调试信息的最简单的途径就是输出图像本身。如果你想知道某个变量的值,对于每个像素运行的部分计算,你可以暂时修改你的程序,直接复制该值到输出图像,并跳过其余通常会完成的计算。例如,如果您怀疑表面法线的问题导致了阴影问题,您可以直接将法线向量复制到图像中(x变为红色,y变为绿色,z变为蓝色),从而得到在计算中实际使用的向量的彩色编码插图。或者,如果您怀疑某个特定值有时超出了有效范围,那么让您的程序在发生这种情况的地方写入亮红色像素。其他常见的技巧包括用明显的颜色绘制表面的背面(当它们不应该是可见的时候),用物体的ID编号为图像着色,或者根据它们计算的工作量为像素着色。
使用调试器 (Using a Debugger)
仍然有一些情况,特别是当科学方法导致了不一致时,或者当没有什么可以替代观察正在发生的事情时。问题是图形程序经常涉及到相同代码的多次执行(例如,每个像素一次,或每个三角形一次),这使得从调试器开始逐步执行完全不切实际。最困难的错误通常只发生在复杂的输入中。一个有用的方法是为bug “Set a trap” 首先,确保您的程序是确定性的——在单个线程中运行它,并确保所有随机数都是从固定的种子计算出来的。然后,找出哪个像素或三角形显示了错误,并在您怀疑不正确的代码之前添加一条语句,该语句将仅针对可疑情况执行。例如,如果你发现像素(126,247)显示错误,
if x = 126 and y = 247 then
print "blarg!"
使用固定随机数种子的特殊调试模式非常有用。
如果在print语句上设置了断点,则可以在计算感兴趣的像素之前进入调试器。一些调试器具有“条件断点”特性,可以在不修改代码的情况下实现相同的功能。在程序崩溃的情况下,传统的调试器对于确定崩溃的位置非常有用。然后,您应该开始在程序中回溯,使用断言和重新编译,以找到程序出错的地方。这些断言应该留在程序中,以备将来可能添加的错误。这再次意味着避免了传统的逐步执行过程,因为这不会向程序添加有价值的断言。
数据可视化的调试 (Data Visualization for Debugging)
通常,很难理解你的程序在做什么,因为它在最终出错之前计算了大量的中间结果。这种情况类似于测量大量数据的科学实验,解决方案是相同的:为自己制作良好的图表和插图,以理解数据的含义。例如,在光线跟踪器中,您可能会编写代码来可视化光线树,这样您就可以看到一个像素的路径,或者在图像重新采样例程中,您可能会制作图表来显示从输入中提取样本的所有点。花在编写代码在可视化程序内部状态上的时间也会在优化程序时更好地理解。
我喜欢格式化调试打印语句,这样输出恰好是一个MATLAB®或Gnu-plot脚本,可以生成有用的图形。-S.M.