Linux 迈向 3D:Mesa/OpenGL 简介
最近,我第一次在我的家用电脑上安装了 Linux。在家拥有一台 Unix 工作站的兴奋感消退后,我开始寻找一种方法将我的分子图形程序 Viewmol 移植到 Linux。我过去常使用 IBM 和 Silicon Graphics 工作站,Viewmol 是使用 Silicon Graphics 的图形库 (Iris GL) 编写的。Linux 上有很多 3D 图形库(柏林工业大学网站上列出了超过 180 个,www.cs.tu-berlin.de/~ki/engines.html),包括 Iris GL 的一些基本实现(YGL,网址为 WWW.thp.Uni-Duisburg.DE/Ygl/ReadMe.html,仅限 2D,以及 VOGL,网址为 http://www.cs.kuleuven.ac.be/~philippe/vogl/),但没有一个库具有我需要的全部功能——然后我发现了 Mesa。Mesa 是一个 3D 图形库,它在源代码上与 OpenGL 兼容,OpenGL 是 Silicon Graphics 的 Iris GL 的继任者。Mesa 的目标是使为 OpenGL 编写的程序可以在包括 Linux 在内的每个 X windows 系统上运行。所以我更好地了解了 Mesa,并决定为 OpenGL 重写我的程序。
Mesa 主要由 Brian Paul 在过去 3 年中编写,目前(截至本文撰写时)版本为 1.2.8。几乎所有的 OpenGL 功能都可用;唯一缺少的功能是抗锯齿、mipmap 纹理、多边形点画和一些纹理查询功能。Mesa 的主页 www.ssec.wisc.edu/~brianp/Mesa.html 列出了许多使用它的应用程序(基本上是科学可视化工具,但也包括 VRML 浏览器)。目前,可以从 C 和 Fortran 例程中调用 Mesa。
虽然 OpenGL 被设计为高性能(和高价)图形硬件的软件接口,但 Mesa 是一个纯软件解决方案,它使用 X windows 与硬件接口。(最近,SVGA 驱动程序和对 3D PC 硬件的一些支持已添加到 Mesa 中。)因此,基于 Mesa 的程序通常比基于 OpenGL 的程序执行速度慢。这两个库都与硬件无关,也与窗口系统无关;因此,窗口系统的处理留给应用程序程序员。让程序员处理窗口化与 Iris GL 不同,但为了实现硬件独立性,Mesa 认为这是必要的。OpenGL 是 3D 计算机图形的标准,由架构审查委员会管理。实现可用于多种操作系统:不同风格的 Unix、Windows 和 MacOS。Mesa 也支持所有这些平台。OpenGL 需要 X 服务器中的扩展 GLX 才能运行。Mesa 不需要此扩展,因为它模拟对 GLX 的调用。有用于 Linux 的商业 OpenGL 实现,其中也包括带有 GLX 的 X 服务器。
OpenGL/Mesa(在以下文本中我将仅使用术语 Mesa,但应注意,所有内容也适用于 OpenGL)不提供用于在 3D 中描述模型的高级命令。它们确实提供了必要的图形基元(例如,点、线、多边形)来构建和操作模型。Mesa 为程序员提供了完全在三维空间中执行模型构建和操作的能力。将 3D 模型转换为平面屏幕上的绘图的所有细节都由库处理,包括 3D 编程中最繁琐的任务之一——隐藏线和曲面的移除。Mesa 还提供“特效”,例如纹理映射、雾化或混合。
Mesa 的主要 ftp 站点是 iris.ssec.wisc.edu,但也可以在 Linux 的常用位置找到它。安装很容易——首先使用以下命令卸载存档文件
gzcat Mesa-1.2.8.tar.gz | tar xf -
然后对于 a.out,给出命令
make linux
或者对于 ELF,给出
make linux-elf
执行 make 将编译 Mesa 库、GL 实用程序库 (GLU)、tk 和辅助库以及一大堆示例程序。(Mesa 的 makefile 配置了 46 种不同的操作系统,包括 MS Windows。)我发现至少在 Linux、AIX、Irix 和 OSF1 上编译是无障碍的。编译后的库可以在 Mesa-1.2.8/lib 中找到,应安装在 /usr/lib 或 /usr/local/lib 中。头文件 (Mesa-1.2.8/include) 也应复制到 /usr/include 或 /usr/local/include 中。我们的 make 过程不包括此步骤——以下示例均假定 Mesa 安装在 /usr/local/lib 和 /usr/local/include 中。
编译总共生成四个库。
1) libMesaGL.* 包含所有基本的图形代码。
2) libMesaGLU.* 提供了一些更高级别的函数,例如绘制几何对象、样条曲线等的子例程。
3) libMesaaux.* 是一个辅助库,它实际上不是 Mesa 的一部分。由于 Mesa 与窗口系统无关,因此需要一些简单的窗口操作函数。创建此库是为了演示 OpenGL 编程指南(“红皮书”)中 OpenGL 的功能。它包含在 Mesa 中,以便可以编译 Mesa 发行版中包含的所有来自“红皮书”的示例程序。
4) libMesatk.* 是另一个窗口系统支持库。libMesaaux.* 依赖于 libMesatk.*,因此为了成功链接使用 libMesaaux.* 的程序,必须将 -lMesatak 添加到命令行。
我不希望用通常的“Hello, world”程序来让您感到厌烦。由于 Mesa 是一个图形库,我们将从一些更适合其功能的程序开始。让我们绘制一些几何形状
#include<stdlib.h> #include<GL/gl.h> #include<glaux.h> void display(void { glClearColor(1.0, 1.0, 1.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0); glBegin(GL_LINES); glVertex2f(-1.0, -1.0); glVertex2f(1.0, 1.0); glEnd(); glBegin(GL_POLYGON); glVertex2f(0.25, -0.75); glVertex2f(0.75, -0.75); glVertex2f(0.75, -0.25); glVertex2f(0.25, -0.25); glEnd(); glFlush(); } void main(int argc, char **argv) { auxInitDisplayMode(AUX_SINGLE | AUX_RGB); auxInitPosition(0, 0, 500, 500); auxInitWindow(argv[0]); auxMainLoop(display); }
如您所见,所有函数都有命名约定。所有 Mesa 函数都以字母 gl 开头。辅助库中的函数以字母 aux 开头。main() 中对辅助库的前两个调用指定了所需的帧缓冲区配置,即单缓冲 rgb 模式。(还有用于动画的双缓冲配置和颜色表模式,但 rgb 模式是首选且更易于处理。)第三个调用打开一个窗口,第四个调用进入一个无限循环,其中每当从 X windows 收到重绘请求时,都会调用函数 display()。正如我之前提到的,Mesa 不直接处理与窗口系统的接口。辅助库仅提供非常基本的功能,不适合大型程序——稍后会详细介绍替代方案。
display() 函数首先调用两次以将窗口的背景清除为白色——首先我们使用 glClearColor() 指定所需的颜色,然后我们使用 glClear() 清除颜色缓冲区。之后,我们将绘制颜色设置为黑色,并使用 glMatrixMode()、glLoadIdentity() 和 glOrtho() 设置投影矩阵。由于 Mesa 可以处理所有必要的数学运算,以从我们的 3D 世界创建 2D 绘图,因此我们只需要给出制作投影的指令。首先,我们使用 glMatrixMode() 来指定我们将要操作投影矩阵。(有一个建模矩阵可以平移或旋转对象,我们稍后会讨论。)然后我们加载一个单位矩阵来初始化矩阵堆栈,最后,我们使用 glOrtho() 来指定正交投影。现在我们从窗口的左下角到右上角绘制一条线,并在右下象限绘制一个正方形。
Mesa 中的所有绘图基元都是通过用 glBegin() 和 glEnd() 调用来包含其顶点规范而创建的。在调用 glBegin() 时,我们指定要绘制的基元。可用的基元有 GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_POLYGON、GL_QUADS、GL_QUAD_STRIP、GL_TRIANGLES、GL_TRIANGLE_STRIP 和 GL_TRIANGLE_FAN。
最后,我们调用 glFlush() 来告诉 Mesa 刷新其图形管道并显示我们指定的对象。要编译我们的演示程序(假设它已存储在名称 demo1.c 下),我们执行以下命令(请注意,需要标准 Xlib、X 扩展库和数学库来解析 Mesa 中的所有引用)
cc -o demo1 demo1.c -I/usr/local/include -L/usr/local/lib \ -lMesaaux -lMesatk -lMesaGL -lXext -lX11 -lm
图 1 显示了我们程序的执行结果,该程序通过按 <ESC> 键退出。与直接使用 X window 相比,使用 Mesa 库进行此练习肯定更容易编程。
现在,由于 Mesa 是一个用于 3D 图形的库,让我们创建一个三维对象。将我们第一个示例中的 display() 函数替换为以下调用
void display(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0); glRotatef(45.0, 0.0, 1.0, 0.0); glRotatef(30.0, 0.0, 0.0, 1.0); auxWireCube(1.0); glFlush(); }
重新编译。我们现在不使用 glBegin()/glEnd() 对来指定对象的顶点,而是使用辅助库函数之一来绘制线框立方体。绘制之前的两个 glRotatef() 调用更改了模型视图矩阵。由于旋转只能更改模型视图矩阵,因此我们不需要显式切换到模型视图矩阵模式;Mesa 会自动进行切换。第一个调用使对象绕 Y 轴旋转 45 度,第二个调用使对象绕 Z 轴旋转 30 度。glRotatef() 的最后一个字母 f 表示其参数是浮点数。(以字母 d、i 或 s 结尾的函数分别接受 double、integer 或 short 类型的参数。)在内部,Mesa 使用函数的 float 版本;因此,直接调用此版本可以节省 Mesa 内的额外函数调用。图 2 显示了运行此版本的程序生成的输出。
接下来,我们为我们的程序添加交互性。为了允许交互式旋转我们的立方体,我们只需要添加一些处理输入的行
#include<stdlib.h> #include<GL/gl.h> #include<glaux.h> float xangle=0.0, yangle=0.0; void display(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 0.0, 0.0); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0); glRotatef(xangle, 1.0, 0.0, 0.0); glRotatef(yangle, 0.0, 1.0, 0.0); auxWireCube(1.0); glFlush(); } void rotX1(void) { xangle+=5.; } void rotX2(void) { xangle-=5.; } void rotY1(void) { yangle+=5.; } void rotY2(void) { yangle-=5.; } void main(int argc, char **argv) { auxInitDisplayMode(AUX_SINGLE | AUX_RGB); auxInitPosition(0, 0, 500, 500); auxInitWindow(argv[0]); auxKeyFunc(AUX_LEFT, rotX1); auxKeyFunc(AUX_RIGHT, rotX2); auxKeyFunc(AUX_UP, rotY1); auxKeyFunc(AUX_DOWN, rotY2); auxMainLoop(display); }
display() 函数与上一个示例中的函数几乎相同,我们已将硬编码的角度替换为变量。我们的 main() 函数现在包含对 auxKeyFunc() 的四个调用,允许我们指定在按下某个键时调用的回调函数(此处使用的常量是指光标键)。最后,我们需要一些函数,这些函数将根据按下的键来增加或减小立方体的旋转角度。该程序再次以相同的方式编译。当此版本的程序运行时,可以通过按任何光标键来旋转立方体。
我们可能更希望我们的应用程序使用鼠标来旋转立方体,但我们目前仅限于辅助库提供的功能。要编写程序以使用鼠标点击而不是光标键,我们需要使用更复杂的 X windows 界面之一(但这又是另一篇文章了)。
最后,我们将为我们的立方体演示添加一些光照效果,并展示如何移除隐藏表面。这些计算也可以通过调用 Mesa 轻松处理,程序员不必担心底层的非平凡数学。我们再次修改 display() 函数如下
void display(void) { GLfloat light0[4] = {0.5, 0.8, 1.0, 0.0}; GLfloat color[4] = {1.0, 0.0, 0.0, 0.0}; glClearColor(1.0, 1.0, 1.0, 0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, color); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glLightfv(GL_LIGHT0, GL_POSITION, light0); glRotatef(xangle, 1.0, 0.0, 0.0); glRotatef(yangle, 0.0, 1.0, 0.0); auxSolidCube(1.0); glFlush(); }
由于启用光照绘制线框立方体没有多大意义,我们将使用实体立方体。为了正确渲染实体立方体,我们需要移除隐藏表面。在 Mesa 中,这可以通过使用 z 缓冲区来实现,z 缓冲区存储有关 3D 空间中点的深度值的信息。然后,Mesa 将自动仅绘制可见的像素。为了在绘制之前初始化 z 缓冲区,我们只需将常量 GL_DEPTH_BUFFER_BIT 添加到对 glClear() 的调用中即可。
对于光照计算,我们不能简单地使用绘图颜色——我们必须将颜色链接到对象。Mesa 使用“材质”来建立此链接,并允许我们指定材质的属性。对 glMaterialfv() 的调用将红色分配为所有多边形正面和背面的漫反射颜色。我们使用对 glLightfv() 的调用来指定光源的位置。Mesa 可以使用许多不同的光源(至少保证 8 个),常量 GL_LIGHT0 ... GL_LIGHT7 可用于引用它们。GL_POSITION 通知 Mesa 我们正在指定位置(其他可能性包括光和颜色),向量 light0[] 将光源放置在指定轴上的无限远处。此特定轴用于在立方体的不同面上实现不同的光强度。请注意,这两个函数显示了另一种类型的命名约定——两个名称都以字母 fv 结尾,即参数是浮点值的向量。
我们还需要修改 main() 函数以包含 z 缓冲和光照计算
void main(int argc, char **argv) { auxInitDisplayMode(AUX_SINGLE | AUX_RGB | AUX_DEPTH); auxInitPosition(0, 0, 500, 500); auxInitWindow(argv[0]); auxKeyFunc(AUX_LEFT, rotX1); auxKeyFunc(AUX_RIGHT, rotX2); auxKeyFunc(AUX_UP, rotY1); auxKeyFunc(AUX_DOWN, rotY2); glShadeModel(GL_SMOOTH); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); auxMainLoop(display); }
首先,对 auxInitDisplayMode() 的调用中的常量 AUX_DEPTH 指示 X windows 提供一个带有 z 缓冲区的窗口。然后我们使用平滑着色 (glShadeModel()) 来绘制在多边形表面上具有不同颜色的多边形。如果我们使用平面着色(默认值),则不同的多边形将清晰可见。当然,这在立方体的情况下不会有任何区别,但在其他对象(例如,使用 auxSolidCone(1.0, 1.0) 将立方体替换为圆锥体并查看结果)中会有所不同。最后,我们使用对 glEnable() 的调用启用光照计算、light0 和深度测试。对于深度测试,我们指定一个函数来比较深度值,以便仅考虑较小的值,即更靠近查看器的值。重新编译。图 3 中显示的已点亮的可旋转立方体是我们程序在完成一些旋转后的输出。
我们现在已经介绍了使用 Mesa 生成逼真的 3D 场景的基本绘图操作。在这些示例中使用的辅助库作为大型程序的窗口系统接口是不够的。一种替代方法是使用 GL 实用程序工具包 (GLUT),该工具包透明地提供与 Iris GL 相同的功能(例如,窗口和事件处理、菜单)。GLUT 由 Silicon Graphics 的 Mark Kilgard 编写,并且是免费提供的。另一种选择是使用 Mesa 包的 widgets 子目录中提供的 OpenGL 小部件。(此子目录必须单独编译。)然后,程序可以以正常的 X 方式完成所有窗口和事件处理,并创建一个或多个 OpenGL 小部件来显示 3D 图形。可以使用对 Mesa 的调用来完成在这些小部件中绘图。作为 Mesa 可以做什么的最后一个示例,图 4 显示了使用我的分子图形程序 Viewmol 对苯分子轨道的渲染。(OpenGL/Mesa 版本的 Viewmol 尚未发布,但将出现在今天可以找到 Iris GL 版本的相同位置:ftp://ccl.osc.edu/pub/chemistry/software/SOURCES/C/viewmol 或 ftp://ftp.ask.uni-karlsruhe.de/pub/education/chemistry/viewmol_ask.html)
图 4.苯分子轨道
Jörg-Rüdiger Hill (jxh@msi.com) 出生于德国柏林,拥有理论化学博士学位。他为一家分子建模软件公司工作,目前正在将他的分子图形程序 Viewmol 移植到 Linux。自 1.0.9 版本以来,他一直在运行 Linux。他非常喜欢南加州的天气,而不是柏林的天气。