《Fundamentals of Computer Grahpics》虎书第三版翻译——第一章 介绍

(80) 2024-07-26 09:01:03

1.1 图形领域

在任何领域进行强分类是危险的,但大多数从事图形学行业的人都会赞同以下这些计算机图形学的主要领域:

建模:使用数学规格进行建模,这些数据是能存储在计算机上的形状和外观属性。例如,一个咖啡杯可以被描述作为一组以一些插值规则连接的有序的三维空间上的点,以及反射模型,该模型描述了光与杯子是如何相互作用的

渲染:出自美术的一个专业术语,用于生成从3D计算机模型渲染后的图像

动画:通过一系列图像创造幻觉运动的一门技术。动画使用模型和渲染但添加了随时间运动的关键问题,在基本的建模和渲染中这类问题并不常见。

还有许多其他领域涉及到计算机图形学,但他们是否是计算机核心领域因人而异。这些内容在本书内容会至少会提及一点。内容包括:

用户交互:它处理用户和输入设备的接口,例如鼠标和键盘,应用程序,将图像反馈给用户,以及其他感官的反馈。从历史上看,这一领域与图形IC有很大的关联,因为图形学研究人员是最早接触这些无处不在的输入/输出设备的。

虚拟现实:尝试让用户沉浸在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 图形管线

如今,每台台式电脑都有一个强大的3d图形管道。他是一个特殊的软/硬件子系统,可以有效的绘制基本的3D物体的透视图。通常这些系统可以优化那些具有共享顶点的三维三角形。管线的基本操作是将3D顶点位置映射到2D屏幕位置并对三角形进行渲染,使他们看上去很逼真,并以适当的前后顺序显示。
尽管以有效地以从后往前的顺序绘制三角形在当时是最重要的研究问题,但是现在总能使用Z缓冲区解决,它使用了一个特殊的内存缓冲区来以蛮力解决这个问题。
事实证明在图形管线所使用的几何操作几乎能在由三个传统的几何坐标和辅助透视用的第四个齐次坐标组成的思维空间坐标完成。这些4D坐标是使用4×4矩阵和4维向量处理的。因此,图形流水线包含了许多机器,以便有效地处理和组成各种各样的矩阵和向量。四维坐标系是计算机科学中使用的最微妙和美丽的结构之一,这无疑是学习计算机图形学时需要翻过的最大的障碍。每本图形学书的第一部分的绝大部分都在与这些坐标打交道。
图像生成的速度很大程度取决于绘制三角形的数量。由于交互性在很多应用程序比视觉质量更重要,因此很有必要让表示一个模型所使用到的三角形数量尽可能的最小。此外,如果模型离视野很远的话,需要三角形的数量应该比离视野近的要少。这表面以多细节层次处理(LOD level of detail )表示三角形是很有用的。

1.5 数值问题

许多图形学程序实际上只是3D数学代码。对这类程序来说数学问题通常是至关重要的。在“旧时代”,很难以健壮和可移植的方式处理这些问题,因为机器在内部数字表示不同,更糟糕的是以不同和不兼容的方式处理异常。幸运的是,几乎所有的现代计算机都符合IEEE浮点标准(IEEE标准协会,1985)。
这方便了程序员对如何处理某些特定数值作出判断。
尽管IEEE 浮点有很多特性在编码数字算法时非常好用,但对于图形中遇到的大多数情况,只有少数几个特性是关键的。首先,也是最重要的,是要理解在IEEE 浮点中有三个实数的“特殊”值:

  1. 无穷(∞)。这是一个比所有其他有效数字都大的有效数字。
  2. 负无穷(-∞):这是一个比所有其他有效数字都小的有效数字
  3. 不是数字(NaN):这是一个无效的数,由于结果未定义的操作产生,例如0除以0

IEEE浮点有两种表示形式的零,正零和负零。 虽然−0和+0之间的区别偶尔很重要,但是当他发生的时候最好牢记它。

IEEE浮点标准的设计师做出的一些设计对于程序员来说非常方便。在处理除0异常的时候,许多处理方式于前面三个特殊值有密切关系。在这些情况下,异常会被计算机记录下来,但在许多情况下,程序员可以忽略该异常。具体来说,对于任何正实数a,涉及无穷大除法的规则是这样的:
+a/(+∞) = +0
−a/(+∞) = −0
+a/(−∞) = −0
−a/(−∞) = +0
涉及无穷的运算法则和人们预想的一样,对于正数a,例如:
∞+∞= +∞
∞ − ∞=NaN
∞ × ∞=∞
∞/∞=NaN
∞/a =∞
∞/0 =∞
0/0 = NaN
包含无穷值的布尔表达式中的规则如下所示:

  1. 所有有限有效数均小于+∞
  2. 所有有效数效数均大于+∞
  3. -无穷小于+无穷

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

  1. 任何包含NaN的算术表达式的计算结果都是NaN
  2. 任何涉及NaN的布尔表达式都是false

IEEE浮点数最有用的方面可能是如何处理零除;对于任意正实数a,除以零的规则是这样的:
+a/+0 = +∞
−a/+0 = −∞

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

这样的表达会出现在电阻和透镜情形中。如果除以零将导致程序崩溃(在IEEE浮点数出现之前,许多系统中都会这样),则将需要两个if语句来检查b和c是否为很小的值或零值。而现在有了IEEE浮点数,如果b或c为零,我们则将根据需要将a赋值为零。避免特殊情况检查的另一种常用技术是利用NaN的布尔属性。考虑以下代码段:
A=f(x)
If(A>0)then
Do something

在这里,函数f可能会返回很不想看到的值,例如∞或NaN,但是if语句的条件是明确的:a = NaN或a =-∞时为false(a < 0),而a = +∞时为true(a > 0)。在确定返回什么值时,if语句通常可以做出正确的选择,无需进行特殊检查。这使程序更小,更可靠和更高效。

1.6 效率

没有使代码高效运行的魔法。效率是通过仔细权衡来实现的,对于不同的架构这些权衡是不同的。但是在不久的将来,一个很好的启发是程序员应该更多注重内存访问模式而不是操作次数。这与20年前最好的启发相反。之所以会发生这样的变化是因为内存的速度已经跟不上处理器的速度了。由于这种趋势一直持续着,有限且一致的内存访问对于性能优化的重要性只增不减。
加快代码速度的合理方法是根据需求按以下步骤顺序进行:

  1. 以尽可能直接的方式编写代码。根据需要实时计算中间结果,不存储它们。
  2. 以优化模式编译
  3. 使用优化工具去寻找关键的瓶颈
  4. 检查数据结构来改善局部性。最好使数据单元的大小与目标系统上的缓存/页面大小一致。
  5. 如果分析揭示了数值计算中的瓶颈,则检查编译器生成的汇编代码是否效率低下。重写源代码以解决问题。

这些步骤最重要的一步是第一步。大多数优化只会让代码可读性更差而且优化了个寂寞。而且,把时间花在优化上还不如去修bug或者加功能。另外,要小心旧教材;一些经典技巧,例如把实数换成整数,可能不再提高速度,因为现代CPU执行浮点运算的速度跟整数运算一样快。在任何情况,我们应该对机器和编译器尽可能的优化。

1.7 设计和编写图形程序

某些通用的方法在图形学程序很有用。在这一节中。我们提供了一些对你学习本书有所帮助的建议。
1.7.1 类设计
任何图形学的关键部分在于为几何实体(矩阵和向量)以及图形实体(RGB三原色和图片)提供良好的类或者示例。这些示例应该尽可能的有效和简洁。一个常见的设计问题是我们是否需要把位置和位移归为不同的类,因为他们的运算规则是不一样的。位置1/2没有任何几何意义,但位移1/2是有的。在这个问题上几乎是没有共识的,这可能会引发图形学从业者进行数小时激烈的争论。但为了举例,我们假设不会对它们做出分类。
这意味着我们要编写一些基类,包括:
Vecrtor2:存储x和y分量的2D向量类。他应该将这些组件存储在长度为2的数组中以便能很好的支持索引操作。你也应该包含向量加法,向量减法,点乘,叉乘,标量乘法,和标量除法等运算
Vecrtor3:类似于Vector2
hvector:具有四个分量的同质向量(齐次)
Rgb:存储三个分量的RGB颜色。你也应该支持RGB加法,RGB减法,RGB乘法,标量乘法和标量除法的运算。
Transform:用于变换4*4的矩阵。你应该包含矩阵乘法和成员函数,应用于位置,方向,表面法向量。这些都是不同的,在第六章会看到。
Image:就具有输出操作的二维rgb像素数组

您可能还想到了用于单位矢量的特殊类,尽管它们很让人痛苦
您可能还想到了用于单位矢量的特殊类,尽管它们很让人痛苦
1.7.2 Float vs Double
现在体系结构表面,降低内存使用和保持内存访问一致是提高效率的关键。着以为使用单精度数据。然而,避免数值问题建议使用双精度算法。权衡屈居于程序,但最好在类中使用有一个默认值。
我建议使用double进行几何计算和颜色计算。对于占用大量内存的数据(例如三角形网格),我建议使用float存储,但是用成员函数访问数据时转换为双精度。
1.7.3 调试图形程序
如果你四处询问,你可能会发现,随着程序员越来越有经验,他们使用传统调试器的次数越来越少了。使用这样的调试器对复杂的程序比简单的程序要复杂的多。另一个原因是最难调试的是概念错误,即执行了错误的内容,并且很容易调试了半天缺还找到bug出现的地方。我们发现一些调试方法在图形方面特别有用。

科学的方法:在图形程序中,有一种很有用的替代传统调试的方法。它的缺点是,它与计算机程序员在职业生涯早期被教导不要做的事非常相似。所以,如果你这样做,你可能会觉得很”调皮”:我们创造一个图像,并观察他有什么问题,然后我们对出错原因进行假设并测试它。例如,在光线追踪程序中,我们可能会有许多看起来暗像素。这是大多数人在写光线追踪时遇到的典型的”阴影痤疮“问题。传统的调试在这里没有帮助。相反,我们必须认识到,阴影射线击中的是正在着色的表面。我们可能注意到,黑点的颜色是周围环境的颜色,所以,缺少直射光。直射光可以在阴影中关闭,所以你可以会假设这些点被错误的标记为阴影,但他们不是。为了测试这个假设,我们可以关闭阴影检查,并重新编译。这表面这些是错误的阴影测试,我们可以继续检查。这种方法有时候是良好的实践,因为我们不必发现错误的值或真正确定概念错误。相反,我们只是通过试验缩小了我们的概念误差。通常只需要进行几次试验就可以找到问题所在,并且这种调试很有意思。
调试代码输出的图像:在许多情况下,从图形程序中获取调试信息的最简单方式是输出图形本身。如果你想知道计算每个像素所用到的某个变量的值,你可以临时修改你的程序的值,并将这些值直接复制到输出图像并跳过其他的计算。例如,如果你认为是法平面导致渲染问题,你可以直接将法向量直接复制到图像(x轴变为红色,y轴变为绿色,z轴变为红色),这些色码图解就是你程序中实际向量。或许,你怀疑某个特定值有时会超出有效值的范围,你可以让你程序在发生这种情况的时候绘制出高红亮色的像素。其他常见的技巧包括用明显的颜色绘制表面的背面(背面是看不到的),根据物体的ID号给图像上色,或者根据计算的工作量给像素上色。
使用调试器:仍然有一些实例,特别是当科学发展遇到了矛盾的时候,当没有什么能代替现在正在发生的事情的时候。问题在于,图形程序经常涉及到对相同代码的多次执行(每个像素执行一次,或者每个三角执行一次),这使得从头开始调试逐步调试变得不切实际。最难解决的bug通常只发生在复杂的输入。
一种有用的方法是为bug“设置陷阱”。首先,确保您的程序是确定性的——在单线程中运行它,并确保所有随机数都是从固定种子中计算出来的。然后,找出哪个像素或三角形出现了bug,并在您觉得写错的代码前面添加一条语句,该语句仅在可疑情况下才会执行。例如,如果您发现像素(126,247)出现错误,就添加:

if x=126 and y=247 then
Pirnt “blarg!”
如果你在打印的句子中设置了断点,你可以在计算您感兴趣的像素之前进入调试器。一些调试器具有“条件断点”功能,可以在不修改代码的情况下达到相同的效果。
在程序崩溃的情况下,传统的调试器对于查明崩溃的位置很有用。然后,您应该使用断点和重新编译在程序中开始回溯,以查找程序出了问题的地方。这些断言应留在程序中,以备将来可能添加的错误。这再次意味着避免了传统的逐步过程,因为那样就不会在程序中添加有价值的断言。

数据可视化以进行调试:一般来说,很难理解你的程序在做什么,因为它会在最终出错之前会计算出许多中间结果。这种情况类似于测量大量数据的科学实验,它们的解决方案是相同的:为自己做好设计方案和图解,以了解数据的含义。例如,在光线跟踪中,你可能会编写代码以可视化光线树,以便可以看到对像素有作用的路径,或者在图像重采样例程中,你可能绘图,来显示从输入中采样的所有点。为可视化程序的内部状态所花费的时间,会得到回报,因为在优化时,能够更好地理解程序的行为。

THE END

发表回复