第一章 简介

计算机图形学在技术上发展的很成功。它的基本理念、表示方式、算法和硬件实现诞生于 20 世纪 60-70 年代,并在随后的 20 年间逐渐发展。在 20 世纪 90 年代中期,计算机图形技术已经相当成熟,但其影响仍旧只是局限于某些"高端"程序,例如超级计算机上的科学可视化以及昂贵的飞行模拟器。现在的我们很难相信,在那个年代,很多计算机科学专业的学生对 3D 计算机图形学一无所知!

近几十年来,计算机图形学在商业方面取得了巨大进展。每一个现代 PC 都能够产生高质量的计算机生成图像(computer generated image),大部分是以视频游戏以及虚拟现实环境的形式。整个动画工业已经从其高端(例如 Pixar 电影)转移到了孩子们的电视机前。对于实拍电影,视觉特效领域也已经发生了翻天覆地的变化。当今的观众们也不会在看到不可思议的计算机特效时感到畏惧——这已经在预期当中了。

在本书中,我们将会介绍计算机图形技术中基础的数学与算法。我们使用编程 API(applications programming interface) OpenGL 来完成其中的内容。OpenGL 是一个跨平台的图形编程环境,可以用于创建实时图形程序,例如视频游戏。

1.1 OpenGL

OpenGL 起初作为一种API,负责实现一系列特定的操作,以便绘制3D 计算机图形。随着底层硬件越来越便宜,越来越多的功能也被添加到了图形硬件中,并通过OpenGL API 暴露给用户。随后,通过编写一种具有特定目的的小程序——shader,用户完全控制图形计算的某个部分也变得可行了。shader 通过API 来传递和编译,在OpenGL 中,这些 shader 以GLSL ——这一类 C 风格的语言来编写。两个可编程的主要部分分别是 vertex shader (顶点着色器)和 fragment shader (片段着色器)。这两部分之所以开放给用户,一方面是因为将这些灵活性提供给用户非常有用,另一方面是因为这部分计算可以通过 single instruction multiple data (SIMD) (单指令多数据结构)来并行完成。例如,处理几何体上不同点的操作,可以并行独立完成,而决定一个像素是否需要被显示,也与其他像素完全无关。

在现代的 OpenGL 程序中,许多(并非所有)实际 3D 图形是由用户编写的 shader,而不再由OpenGL API 自身完成。这样来看,OpenGL 更多是关于管理用户数据以及用户 shader,而较少关于3D 图形的绘制本身。随后,我们会对 OpenGL 完成的主要步骤进行综述,也会从较高的层次描述不同种类的 shader 在实现 3D 图形绘制时,在不同步骤中起到的特定作用。

在OpenGL 中,几何体用一些三角面的集合来表示。一方面,三角面对OpenGL 来说处理起来足够简单和有效率,另一方面,我们可以用三角面来近似非常复杂的几何形体(shape)(图1.1)。如果我们的计算机图形程序使用更抽象的几何体表示方法,在使用 OpenGL 绘制之前,必须将它转换为三角面的集合来表示。

简要来说,OpenGL 的计算决定了每个三角面上每个vertex(顶点)在屏幕上的位置,找出哪些屏幕点(即像素)位于哪个三角面上,随后,再进行一些计算来决定该像素应当是什么颜色。现在,我们来更详细的讨论一遍这个流程。

每个三角面由三个顶点组成。我们将一些数值数据关联在每个顶点上,这样的数据称为attribute(属性)。最终,我们需要决定该顶点的位置(二维几何体用2 个 点而三维几何体用3 个点来表示)。我们可以用其他属性在顶点上关联其他类型的数 据,最终用于处理该顶点的外观。例如,我们也许会关联颜色(用RGB 三个数表 示)属性到每个顶点上。其他属性也许会被用来描述相关的材质属性,例如,一个 表面在该顶点处的光泽度。

将顶点数据从CPU 转到GPU 的开销非常高昂,因此通常要尽量减少该操作。 有一种特定的API 调用来将顶点数据传送给 OpenGL,并将数据存入 vertex buffer(顶点缓存)中。

当顶点数据传送给OpenGL 后,我们可以在随后的任何时间发送 draw call (绘制调用)到OpenGL。它会让OpenGL 读取特定顶点缓存,并将三个一组的顶点 (vertex triplet) 绘制为三角面。

一旦给出 draw call 指令,每个顶点(即顶点的所有属性)都会分别被vertex shader 处理(图1.2)。除了属性数据,shader 还会访问 uniform variables (一致变量)。这些变量由应用程序所设置,但你只能在OpenGL 发送的 draw call 之间设置它们, 而不是对每个顶点设置它们。

vertex shader 是你自己的程序,你可以在其中放任何你想要的内容。vertex shader 最常见的用途是决定一个顶点最终在屏幕上的位置。例如,一个顶点在其属性中存有自身 3D 位置的抽象数据。同时,还可能存在uniform variable,描述了一 个虚拟摄影机,用于将抽象的 3D 位置映射到实际的 2D 屏幕。我们将在随后的 2-6 章以及第 10 章中讲到具体的这一类计算。

vertex shader 也可以输出其他的变量供 fragment shader 随后使用在计算三角面所覆盖到的像素的颜色中。这些输出被称为 varying variables (变化变量)因为,我 们随后也很快会解释,它的值在一个三角面中的不同像素间也会有变化。

完成处理后,这些顶点及其 varying variable 会被三角面装配器(assembler)收集,与对应三角面上的点分为一组。

OpenGL 的下一项工作是将每个三角面绘制在屏幕上(图1.3)。这一步称作光栅化(rasterization)。它用三个顶点位置来放置每一个三角面。并随后计算屏幕上的哪些像素位于该三角面之内。光栅化程序为每个像素对每个变化变量计算插值。这表示每个变化变量的值,由三个相关的三角形顶点值混合而来。混合的比例与该像素到每个顶点的距离相关。我们将会在第13 章讲到混合的具体方法。因为光栅化是一个非常特定并且经过高度优化的操作,这一步是不可编程的。

最终,对于每个像素,这些插值后的数据被传递给 fragment shader (图1.4)。fragment shader 是用户用GLSL 编写的另一个交给 OpenGL 处理的程序。fragment shader 的职能是通过传递给它的 varying 及uniform variables 中的信息来决定像素的 颜色。这些由 fragment shader 最终计算出的颜色被放置在 GPU 内存中叫做帧缓存(framebuffer)的地方。帧缓存中的数据随后送去显示在其屏幕上绘制的位置。

在3D 图形中,我们通常计算一些公式来决定像素的颜色,这些公式模拟光线从 材质表面反射的行为。该计算或许会用到存储在 varying variable 中,表示该像素的材质和几何体属性的数据。它或许也会使用保存在uniform variable 中表示场景中光 源位置和颜色的数据。通过改变 fragment shader 中的程序,我们可以模拟光线在不同类型材质上的反弹;这样可以给固定的几何体创造出表面效果的变化,见图 1.5。 我们在 14 章中会更详细的讲这一过程。

作为颜色计算的一部分,我们也可以指示 fragment shader 从存储的附属图像中 获取颜色数据。这样的图像称为纹理(texture)并由uniform variable 来引用。同时, 称作纹理坐标(texture coordinates)的varying variable 告诉 fragment shader 怎样从纹理中获取适合的像素。这一过程称作纹理映射(texture mapping),可以模拟将不 同部分的图像” 粘合”到每个三角面上。该过程可以用来给一个由很少三角面组成的 简单几何体带来很高的视觉复杂度。图 1.6 为例。随后在第 15 章会详细讲到。

当颜色被存入帧缓存时,一个称作融合(merging)的步骤决定了一个从 fragment shader 中输出的” 新” 颜色怎样和或许已经存在于帧缓存中的” 旧” 颜色混合。当 深度缓冲(z-buffering)被启用时,会进行一个测试,来决定一个刚刚由 fragment shader 处理过的几何体点与另一个已经存在于帧缓存中的点相比,距离观察者更近 还是更远。而帧缓存只在新处理的点距离更近时被更新。深度缓冲在从三维场景中创建图像时非常有用。我们在第 11 章中讨论深度缓冲。此外,OpenGL 也可以根据 指示用不同比例将新旧两种颜色混合在一起。这个可以用在透明物体上。这一过程 称作透明混合(alpha blending)并在第 16 章中讨论。因为这个混合步骤涉及到读写 共享的内存(帧缓存),这一步骤也是不可编程的,但可以由不同 API 调用来控制。

在附录 A 中,我们讨论了一个实际的代码片段,它实现了一个简单的 OpenGL 程序,利用纹理映射提供了一些简单的 2D 绘制。这里的目标并不是学习 3D 图形, 而是去理解 API 本身以及 OpenGL 中使用到的处理步骤。在读到第 6 章之前的某个时间,你需要详细的阅读附录 A 中的内容。

练习

1.1 为了对 1980 年代的计算机图形有直观感受,观看电影电子世界争霸战 (Tron)。

1.2 玩视频游戏战争地带(Battlezone)。

Last updated