FCG-Chapter-1


前言

在工作中模模糊糊学习了不少碎片化图形学知识,但只见树木不见森林,总有一种不踏实的感觉。有必要系统地学习一遍。
看了一部分闫神的中文教程 GAMES101 ,这是个很好的学习材料。
为了印象深刻,我决定把《Fundamentals of Computer Graphics》读一遍,记下学习笔记。

平时要做的事情太多,希望能坚持下来。

本篇是原书第一章。

1. 导论

1.1 图形学领域

图形学主要领域分为:

  • 建模
  • 渲染
  • 动画

与图形学联系密切的其它领域有:

  • 用户交互
  • 虚拟现实
  • 可视化
  • 图像处理
  • 三维扫描

1.2 主要应用

  • 视频游戏
  • 卡通片
  • 视觉特效
  • 动画电影
  • CAD/CAM
  • 模拟
  • 医学影像
  • 信息可视化

1.3 图形API

图形API是一系列执行基础操作的标准函数集,比如向屏幕绘制图片以及3D表面等。

有两类API,一类以集成方式提供,比如JAVA语言本身所支持的。
另一类代表是Direct3D、OpenGL,它们的绘制命令由软件库提供,这些软件库通常与C++等语言绑定,用户交互软件在不同系统中差别很大且编程困难,有可能会用中间层,以封装这些系统特定的用户接口代码。

无论哪种,都会用到本书的内容。

1.4 图形管线

图形管线是一个特殊的软件或硬件子系统,它能有效地绘制透视中的3D图元。通常,这些系统用于处理有共享节点的3D三角形。管线中的基础操作把3D顶点位置映射到2D屏幕位置上,并为三角形着色以使其看起来更真实,且保证其出现在正确的前后位置上。

以有效的“back-to-front”顺序绘制三角形是个很重要的研究课题,但它通常是用z-buffer暴力求解的。

事实证明,几何变换几乎完全可以在一个4D齐次坐标中完成。因此图形管线可以十分高效地处理这些标量和向量。

这个4D坐标是计算机科学中最精妙、最优雅的构造之一,也无疑是学习计算机图形学时需要跨越的最大智力障碍。几乎每本图形学教材的前半部分都在重点讲解这些坐标。

图片生成的速度极度依赖于要绘制的三角面片数,由于交互性比显示质量更重要,将一个模型的面片数最小化是值得的。
另外,如果一个模型位于视线的远处,它们就比近处的模型需要更少的三角面数。
因此,用 细节层次(Level of detail, LOD) 来表示一个模型是很有用的。

1.5 数值问题

许多图形学程序本质上就是数值计算代码。在"旧时代",由于每台机器都有不同的内部数值表示,处理这些问题非常困难。 幸运地是,大部分现代计算机都遵守了IEEE浮点数标准( IEEE Standards Association, 1985. ),使程序员们能更容易地处理数值精度。

IEEE浮点数有许多有用的特性,但对大部分图形学场景来说,只需要知道其中很小一部分。
首先,最重要的是要了解在IEEE浮点数中有三种特殊的实数:

  1. 正无穷大(\(+\infty\))
  2. 负无穷大(\(-\infty\))
  3. 无效数字(NaN)

对于任意正实数\(a\),满足以下规则

$$+a/(+\infty)=+0,$$

$$-a/(+\infty)=-0,$$

$$+a/(-\infty)=-0,$$

$$-a/(-\infty)=+0.$$

以及其它规则:

$$\infty+\infty=+\infty$$

$$\infty-\infty=NaN$$

$$\infty\times\infty=\infty$$

$$\infty/\infty=NaN$$

$$\infty/a=\infty$$

$$\infty/0=\infty$$

$$0/0=NaN$$

涉及布尔表达式的规则正如预期那样:

  1. 所有有限有效数字都小于\(+\infty\).
  2. 所有有限有效数字都大于\(-\infty\).
  3. \(-\infty\)小于\(+\infty\).

涉及\(NaN\)的表达式规则:

  1. 任何包含\(NaN\)的算术表达式都产生\(NaN\).
  2. 任何涉及\(NaN\)的布尔表达式都是false.

除零规则:

$$+a/+0=+\infty$$

$$-a/+0=-\infty$$

举个例子说明IEEE浮点数的便利之处:

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

假如f返回了\(\infty\)或者\(NaN\),这个条件判断仍然可以运行得到正确的结果。

1.6 效率

没有一个能让代码更高效的神奇规则,效率是由很多谨慎的取舍实现的,这些取舍在不同架构上也有所不同。
但是在可遇见的未来,一个好的启示是,程序员应该把更多的注意力放在内存访问模式而非操作计数上,这是因为二十年来,内存的速度并没有跟上处理器的速度。

一个合理的办法(按需采纳):

  1. 以最直接的方式写代码。中间结果应实时处理而非保存它们。
  2. 以优化模式进行编译。
  3. 使用现有的任何分析工具去找到关键的性能瓶颈。
  4. 审查数据结构以寻找局部提升的途径。如果可能的话,使数据单元大小与目标架构的缓存/页大小相匹配。
  5. 如果分析工具显示瓶颈在数值计算上,审查编译器产生的汇编代码以找到丢失的性能。然后重写源代码,以解决你找到的任何问题。

最重要的就是步骤1。很多“优化”使代码变得难读却没有得到性能提升。
另外,花费时间优化代码通常要比修正BUG或增加feature要好。
还有,要当心老旧资料中的建议——一些经典技巧例如使用整数而非实数,可能不会再产生性能提升,因为现代CPU通常可以以同样快的速度处理浮点数和整数。
在任何情况下,分析在目标机器和编译器上的优化手段是否有效,都是必要的。

1.7 设计与编写图形学程序

本节是一些关于编码的建议。

1.7.1 类定义

一个常见的设计问题:是否应该将位置与位移定义为独立的类,因为它们各自有不同的方法。
例如,位置乘以二分之一没有任何几何意义,但位移有( Goldman, 1985 ; DeRese, 1989 )。
这个问题只有少量的共识,但为了举例,假设我们不区分它们。

一些基础类包括:

  • vector2:一个2维向量类,存储x和y分量。它应该在一个长为2的数组中保存其分量,你也可以定义向量加、减、点乘、叉乘、标量乘、标量除等操作。
  • vector3:一个3维向量类,与vector2类似。
  • hvector:一个有4分量的齐次向量(见第8章)。
  • rgb:颜色值。你也可以定义RGB加、RGB减、RGB乘、标量乘、标量除操作。
  • transform:一个用于变换的4*4矩阵。你可以包括一个矩阵乘,以及应用到位置、方向、表面法向量的成员函数。见第7章。
  • image:一个2维RGB像素,有一个输出操作。

另外,可能增加一些类,用于区间、标准正交基以及标架。

你可能还考虑定义一个单位向量,但我发现它们带来更多的是麻烦而非收益。——P.S.

1.7.2 单精度与双精度

现代架构建议降低内存使用、维护合理的内存访问是提高性能的关键,因此建议使用单精度数据。
但是,避免数值问题又建议使用双精度,这个取舍要取决于程序本身。

我建议对几何计算使用double,而颜色计算使用float。对于占用大量内存的数据,如三角面片,我建议使用float存储,但在数据访问时转换为double。——P.S.

我建议使用float进行所有的计算,除非你发现你在某块代码中必须使用double。——S.M.

1.7.3 调试图形程序

如果你问周围的人,你会发现越是有经验的程序员,他们使用传统调试器用得越少。一个原因是,对于复杂程序来说使用调试器比简单程序更难。另一个原因是,最困难的错误是构思上的实现错误,它能轻易地让你在单步调试变量值上消耗掉大量时间,却检测不出这些问题。
我们找到了一些调试策略,它们在图形学上十分有用。

科学方法

一个看起来很“没规矩”但有用的图形调试方法:我们创建一张图片,并且观察它有什么问题。然后,我们提出一个关于当前问题如何产生的假设,并测试它。

该方法在实践中有效的一个关键理由是,我们不需要去察觉某一个错误值,或者真的去确定我们的构思错误。相反,我们只是实验性地在我们的构思中缩小范围。通常,只需要几次尝试就能找到问题,这类的调试很有趣。

在代码调试过程中输出图像

可以临时性地修改程序、跳过后续正常流程的计算,将中间值直接拷贝到输出。

其它常见技巧包括:用一个明显的颜色绘制曲面的背面(如果它们本不应该被看到),用对象的ID给图像上色,或者根据像素计算花费的时间给它们上色。

使用调试器

仍然有一些场景,当没有合适的观察手段时,科学调试法会导致矛盾。

一个有用的方法是,对BUG设置一个“陷阱”。
首先确保你的程序是“确定的”——用单线程跑、将所有随机值指定为固定种子。
然后,找到有BUG的的像素,在你怀疑的地方添加一行仅在错误情形下才产生输出的语句。
比如:

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

你可以在这之前设置一个断点。

当程序崩溃的情形下,传统调试器就很有用。
你应该使用断言和重新编译不断回溯,找到程序哪里出了错。

将数据可视化用于调试

很多时候,会很难理解程序到底做了什么,因为它在出错之前计算了很多中间结果。

这种场景就像科学实验中的数据测量一样,解决方法也一样:制做一些好的数据图像,使你能理解这些数据代表什么含义。
当到了优化程序性能的时候,你花费时间写内部数据可视化这件事,也会对你理解程序行为有很大的帮助。