计算机图形学基础笔记(1)

508 阅读21分钟

Category:Higher Mathematics & Computer Graphics Application

正文

目录

image.png

image.png

image.png

image.png

image.png

image.png

1 简介

计算机图形学一词描述了使用计算机来创建和处理图像的任何用途。本书介绍了可用于创建各种图像的算法和数学工具——逼真的视觉效果、信息丰富的技术插图或精美的计算机动画。图形可以是二维或三维的;图像可以是完全合成的,也可以通过处理照片产生。这本书是关于基本算法和数学的,尤其是那些用于生成三维对象和场景的合成图像的。
实际上,做计算机图形不可避免地需要了解特定的硬件、文件格式,通常还需要一个或两个图形 API[application program interface](参见第 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 4 1 组成。简介 密度值的矩形阵列。计算机图形学用于创建 sh添加的图像可帮助医生从此类数据中提取最显着的信息。
信息可视化 创建不一定具有“自然”视觉描述的数据图像。例如,十种不同股票价格的时间趋势并没有明显的视觉描绘,但巧妙的绘图技术可以帮助人类看到这些数据中的模式。

1.3 图形API

使用图形库的一个关键部分是处理图形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 浮点中实数的三个“特殊”值:

  1. 无穷大 (∞)。这是一个大于所有其他有效数字的有效数字。
  2. 负无穷大 (−∞)。这是一个小于所有其他有效数字的有效数字。
  3. 不是数字(NaN)。这是一个无效数字,由具有未定义结果的操作产生,例如零除以零。 注: IEEE 浮点数对零有两种表示形式,一种被视为正数,另一种被视为负数。 –0 和 0 之间的区别只是偶尔很重要,但对于那些重要的情况,请牢记这一点。

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

image.png
其他涉及无限值的操作的行为方式与人们期望的一样。
同样对于正 a,行为如下:

image.png
涉及无限值的布尔表达式中的规则符合预期:

  1. 所有有限有效数都小于 ∞。
  2. 所有有限有效数都大于-∞。
  3. -∞ 小于 ∞。 涉及具有 NaN 值的表达式的规则很简单:
  4. 任何包含 NaN 的算术表达式都会导致 NaN。
  5. 任何涉及 NaN 的布尔表达式都是错误的。 也许 IEEE 浮点最有用的方面是如何处理除零。对于任何正实数 a,以下涉及除以零值的规则成立:

image.png

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

image.png

这种表达方式出现在电阻器和透镜中。如果除以零导致程序崩溃(就像 IEEE 浮点之前的许多系统中那样),则需要两个 if 语句来检查 b 或 c 的小值或零值。
相反,对于 IEEE 浮点,如果 b 或 c 为零,我们将根据需要获得 a 的零值。另一种避免特殊检查的常用技术是利用 NaN 的布尔属性。考虑以下代码段:
image.png
这里,函数 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)。在这个问题上几乎没有一致意见,这可能会在图形从业者之间引发数小时的激烈辩论,但为了举例,我们假设我们不会做出区分。

我坚信 KISS(“保持简单,愚蠢”)原则,因此,两个类的论点不足以证明增加的复杂性是合理的。—附言
我喜欢将点和向量分开,因为它使代码更具可读性,并且可以让编译器捕获一些错误。—S.M.

这意味着要编写的一些基本类包括:
vector2。存储 x 和 y 分量的 2D 矢量类。它应该将这些组件存储在一个长度为 2 的数组中,以便可以很好地支持索引运算符。您还应该包括向量加法、向量减法、点积、叉积、标量乘法和标量除法的操作。
矢量3。类似于 vector2 的 3D 矢量类。
向量。具有四个分量的齐次向量(参见第 7 章)。
RGB。存储三个分量的 RGB 颜色。您还应该包括用于 RGB 加法、RGB 减法、RGB 乘法、标量乘法和标量除法的操作。
转换。用于转换的 4 × 4 矩阵。您应该包含矩阵乘法和成员函数以应用于位置、方向和表面法线向量。如第 6 章所示,这些都是不同的。
图片。具有输出操作的 RGB 像素的二维数组。

您也可以考虑为单位长度向量设置一个特殊类,尽管我发现它们比它们的价值更痛苦。 ——附言

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

1.7.2 浮点数与双精度数

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

我建议使用双精度数进行几何计算,使用浮点数进行颜色计算。对于占用大量内存的数据,比如三角形网格,我建议存储float数据,但是当通过成员函数访问数据时转换为double。 ——附言

我主张使用浮点数进行所有计算,直到您发现在代码的特定部分中需要双重精度的证据。—S.M.

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

科学方法

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

图像作为编码调试输出

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

使用调试器

仍然存在一些情况,特别是当科学方法似乎导致矛盾时,当没有什么可以替代观察正在发生的事情时。问题在于,图形程序通常涉及多次执行相同的代码(例如,每个像素一次,或每个三角形一次),因此从一开始就在调试器中逐步执行是完全不切实际的。最困难的错误通常只发生在复杂的输入中。
一个有用的方法是为错误“设置陷阱”。首先,确保你的程序是确定性的——在单线程中运行它,并确保所有随机数都是从固定种子中计算出来的。然后,找出哪个像素或三角形出现错误,并在您怀疑不正确的代码之前添加一条语句,该语句将仅针对可疑情况执行。例如,如果您发现像素 (126, 247) 出现错误,则添加:

image.png

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

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

调试数据可视化

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

我喜欢格式化调试打印语句,以便输出恰好是一个 Mat lab 或 Gnuplot 脚本,它可以制作一个有用的图。—S.M.

笔记

软件工程的讨论受到有效 C 系列 (Meyers, 1995, 1997)、极限编程运动 (Beck & Andres, 2004) 和编程实践 (Kernighan & Pike, 1999) 的影响。实验调试的讨论基于与 Steve Parker 的讨论。
有许多与计算机图形相关的年度会议,包括 ACM SIGGRAPH 和 SIGGRAPH Asia、图形接口、游戏开发者大会 (GDC)、欧洲图形学、太平洋图形学、高性能图形学、欧洲图形学渲染研讨会和 IEEE VisWeek。
这些可以通过网络搜索他们的名字很容易找到。