1.2 初识OpenGL程序
正因为可以用OpenGL去做那么多的事情,所以OpenGL程序有可能会写得非常庞大和复杂。不过,所有OpenGL程序的基本结构通常都是类似的,其步骤如下:
初始化物体渲染所对应的状态。
设置需要渲染的物体。
在阅读代码之前,我们有必要了解一些最常用的图形学名词。渲染(render)这个词在前文中已经多次出现,它表示计算机从模型创建最终图像的过程。OpenGL只是其中一种渲染系统,除此之外,还有很多其他的渲染系统。OpenGL是基于光栅化的系统,但是也有别的方法用于生成图像。例如光线跟踪(ray tracing),而这类技术已经超出了本书的介绍范围。不过,就算是用到了光线跟踪技术的系统,同样有可能需要用到OpenGL来显示图像,或者计算图像生成所需的信息。
模型(model),或者场景对象(我们会交替地使用这两个名词)是通过几何图元,例如点、线和三角形来构建的,而图元与模型的顶点(vertex)也存在着各种对应的关系。
OpenGL另一个最本质的概念叫做着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的方法是把它看做专为图形处理单元(通常也叫做GPU)编译的一种小型程序。OpenGL在其内部包含了所有的编译器工具,可以直接从着色器源代码创建GPU所需的编译代码并执行。在OpenGL中,会用到四种不同的着色阶段(shader stage)。其中最常用的包括的顶点着色器(vertex shader)以及片元着色器,前者用于处理顶点数据,后者用于处理光栅化后的片元数据。所有的OpenGL程序都需要用到这两类着色器。
最终生成的图像包含了屏幕上绘制的所有像素点。像素(pixel)是显示器上最小的可见单元。计算机系统将所有的像素保存到帧缓存(framebuffer)当中,后者是由图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上。
图1-1所示为一个简单的OpenGL程序的输出结果,它在一个窗口中渲染了两个蓝色的三角形。这个例子的完整源代码如例1.1所示。
例1.1 第一个OpenGL程序triangles.cpp
也许你会觉得这里的代码有点多,不过它的确就是几乎每一个OpenGL程序所必需的基本内容了。我们用到了不属于OpenGL正式部分的一些第三方软件库,以便实现一些简单的工作,例如创建窗口、接收鼠标和键盘输入等—OpenGL自身并不包含这些功能。我们还创建了一些辅助函数和简单的C++类来简化示例程序的编写。尽管OpenGL是一个C语言形式的库,但是本书中的所有示例都会使用C++来编写,但只是非常简单的C++。事实上,我们用到的绝大部分C++代码是用来实现一些数学向量和构建矩阵的。
简单来说,下面列出的就是例1.1做的所有事情。不过不用担心,后面的章节会更详细地解释这些概念。
在程序的起始部分,我们包含了必要的头文件并且声明了一些全局变量和其他有用的编程结构。
init()函数负责设置程序中需要用到的数据。它可能是渲染图元时用到的顶点信息,或者用于执行纹理映射(texture mapping)的图像数据,第6章会介绍这一技术。
在这个init()函数中,首先指定了两个被渲染的三角形的位置信息。然后指定了程序中使用的着色器。在这个示例中,我们只需要使用顶点和片元着色器。这里的LoadShaders()是我们为着色器进入GPU的操作专门实现的函数。第2章会详细介绍与它相关的内容。
init()函数的最后一部分叫做着色管线装配(shader plumbing),也就是将应用程序的数据与着色器程序的变量关联起来。同样会在第2章详细介绍这一部分的内容。
display()函数真正执行了渲染的工作。也就是说,它负责调用OpenGL函数并渲染需要的内容。几乎所有的display()函数都要完成类似这个简单示例中的三个步骤。
1)调用glClear()来清除窗口内容。
2)调用OpenGL命令来渲染对象。
3)将最终图像输出到屏幕。
最后,main()函数执行了创建窗口、调用init()以及最终进入事件循环体系的一系列繁重工作。这里你也会看到一些以gl开头的函数,但是它们看起来和其他的函数又有一些不同。这些函数就是刚才所说的来自第三方库GLUT和GLEW的函数,我们会随时使用它们来快速完成一些简单的功能,并且保证OpenGL程序可以运行在不同的操作系统和窗口系统上。
在深入了解这些函数之前,我们有必要先解释一下OpenGL的函数、常量的命名方式,以及一些有用的编程结构。