前言
在工作中模模糊糊学习了不少碎片化图形学知识,但只见树木不见森林,总有一种不踏实的感觉。有必要系统地学习一遍。
看了一部分闫神的中文教程 GAMES101 ,这是个很好的学习材料。
为了印象深刻,我决定把《Fundamentals of Computer Graphics》读一遍,记下学习笔记。平时要做的事情太多,希望能坚持下来。
本篇是原书第一章。
图形学主要领域分为:
与图形学联系密切的其它领域有:
图形API是一系列执行基础操作的标准函数集,比如向屏幕绘制图片以及3D表面等。
有两类API,一类以集成方式提供,比如JAVA语言本身所支持的。
另一类代表是Direct3D、OpenGL,它们的绘制命令由软件库提供,这些软件库通常与C++等语言绑定,用户交互软件在不同系统中差别很大且编程困难,有可能会用中间层,以封装这些系统特定的用户接口代码。
无论哪种,都会用到本书的内容。
图形管线是一个特殊的软件或硬件子系统,它能有效地绘制透视中的3D图元。通常,这些系统用于处理有共享节点的3D三角形。管线中的基础操作把3D顶点位置映射到2D屏幕位置上,并为三角形着色以使其看起来更真实,且保证其出现在正确的前后位置上。
以有效的“back-to-front”顺序绘制三角形是个很重要的研究课题,但它通常是用z-buffer暴力求解的。
事实证明,几何变换几乎完全可以在一个4D齐次坐标中完成。因此图形管线可以十分高效地处理这些标量和向量。
这个4D坐标是计算机科学中最精妙、最优雅的构造之一,也无疑是学习计算机图形学时需要跨越的最大智力障碍。几乎每本图形学教材的前半部分都在重点讲解这些坐标。
图片生成的速度极度依赖于要绘制的三角面片数,由于交互性比显示质量更重要,将一个模型的面片数最小化是值得的。
另外,如果一个模型位于视线的远处,它们就比近处的模型需要更少的三角面数。
因此,用 细节层次(Level of detail, LOD) 来表示一个模型是很有用的。
许多图形学程序本质上就是数值计算代码。在"旧时代",由于每台机器都有不同的内部数值表示,处理这些问题非常困难。 幸运地是,大部分现代计算机都遵守了IEEE浮点数标准( IEEE Standards Association, 1985. ),使程序员们能更容易地处理数值精度。
IEEE浮点数有许多有用的特性,但对大部分图形学场景来说,只需要知道其中很小一部分。
首先,最重要的是要了解在IEEE浮点数中有三种特殊的实数:
对于任意正实数\(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$$涉及布尔表达式的规则正如预期那样:
涉及\(NaN\)的表达式规则:
除零规则:
$$+a/+0=+\infty$$$$-a/+0=-\infty$$举个例子说明IEEE浮点数的便利之处:
a=f(x)
if (a > 0) then
do something
假如f返回了\(\infty\)或者\(NaN\),这个条件判断仍然可以运行得到正确的结果。
没有一个能让代码更高效的神奇规则,效率是由很多谨慎的取舍实现的,这些取舍在不同架构上也有所不同。
但是在可遇见的未来,一个好的启示是,程序员应该把更多的注意力放在内存访问模式而非操作计数上,这是因为二十年来,内存的速度并没有跟上处理器的速度。
一个合理的办法(按需采纳):
最重要的就是步骤1。很多“优化”使代码变得难读却没有得到性能提升。
另外,花费时间优化代码通常要比修正BUG或增加feature要好。
还有,要当心老旧资料中的建议——一些经典技巧例如使用整数而非实数,可能不会再产生性能提升,因为现代CPU通常可以以同样快的速度处理浮点数和整数。
在任何情况下,分析在目标机器和编译器上的优化手段是否有效,都是必要的。
本节是一些关于编码的建议。
一个常见的设计问题:是否应该将位置与位移定义为独立的类,因为它们各自有不同的方法。
例如,位置乘以二分之一没有任何几何意义,但位移有(
Goldman, 1985
;
DeRese, 1989
)。
这个问题只有少量的共识,但为了举例,假设我们不区分它们。
一些基础类包括:
另外,可能增加一些类,用于区间、标准正交基以及标架。
你可能还考虑定义一个单位向量,但我发现它们带来更多的是麻烦而非收益。——P.S.
现代架构建议降低内存使用、维护合理的内存访问是提高性能的关键,因此建议使用单精度数据。
但是,避免数值问题又建议使用双精度,这个取舍要取决于程序本身。
我建议对几何计算使用double,而颜色计算使用float。对于占用大量内存的数据,如三角面片,我建议使用float存储,但在数据访问时转换为double。——P.S.
我建议使用float进行所有的计算,除非你发现你在某块代码中必须使用double。——S.M.
如果你问周围的人,你会发现越是有经验的程序员,他们使用传统调试器用得越少。一个原因是,对于复杂程序来说使用调试器比简单程序更难。另一个原因是,最困难的错误是构思上的实现错误,它能轻易地让你在单步调试变量值上消耗掉大量时间,却检测不出这些问题。
我们找到了一些调试策略,它们在图形学上十分有用。
科学方法
一个看起来很“没规矩”但有用的图形调试方法:我们创建一张图片,并且观察它有什么问题。然后,我们提出一个关于当前问题如何产生的假设,并测试它。
该方法在实践中有效的一个关键理由是,我们不需要去察觉某一个错误值,或者真的去确定我们的构思错误。相反,我们只是实验性地在我们的构思中缩小范围。通常,只需要几次尝试就能找到问题,这类的调试很有趣。
在代码调试过程中输出图像
可以临时性地修改程序、跳过后续正常流程的计算,将中间值直接拷贝到输出。
其它常见技巧包括:用一个明显的颜色绘制曲面的背面(如果它们本不应该被看到),用对象的ID给图像上色,或者根据像素计算花费的时间给它们上色。
使用调试器
仍然有一些场景,当没有合适的观察手段时,科学调试法会导致矛盾。
一个有用的方法是,对BUG设置一个“陷阱”。
首先确保你的程序是“确定的”——用单线程跑、将所有随机值指定为固定种子。
然后,找到有BUG的的像素,在你怀疑的地方添加一行仅在错误情形下才产生输出的语句。
比如:
if x == 126 and y == 247 then
print "blarg!"
你可以在这之前设置一个断点。
当程序崩溃的情形下,传统调试器就很有用。
你应该使用断言和重新编译不断回溯,找到程序哪里出了错。
将数据可视化用于调试
很多时候,会很难理解程序到底做了什么,因为它在出错之前计算了很多中间结果。
这种场景就像科学实验中的数据测量一样,解决方法也一样:制做一些好的数据图像,使你能理解这些数据代表什么含义。
当到了优化程序性能的时候,你花费时间写内部数据可视化这件事,也会对你理解程序行为有很大的帮助。