Python 3-D 编程

作者:Jason Petrone

图形编程可能很乏味。链接到大型 3-D 库会增加编译时间。由于通常需要对所有内容进行微调才能使其看起来完美,因此在漫长的构建之间埋藏的细微更改是很常见的。这些漫长的调试周期使 3-D 图形成为在高 level 语言(如 Python)中进行原型设计的理想应用程序。

Python 提供了许多 3-D 图形 API 的扩展。对于 IRIX 系统,Python 发行版附带一个模块,提供对 SGI IRIS GL 库的访问。Python 程序可以从 JPython(在 Java 虚拟机内部运行的 Python 实现)内部使用 Java3D API。本文重点介绍 OpenGL 库,因为它被广泛使用,并且对 Linux 和 Python 具有出色的支持。

下载和安装 PyOpenGL

PyOpenGL 是一套 Python 模块,它提供对 OpenGL 的访问,以及各种辅助实用程序和扩展,以补充 OpenGL 的 low-level 接口。它最初由 James Hugunin、Thomas Schwaller 和 David Ascher 创建。Tarn Burton 最近接任首席开发人员,Rene Liebscher 和 Michael Fletcher 也维护该软件包。

由于 OpenGL 包装器构成了 PyOpenGL 功能的大部分,因此您需要对 OpenGL 有基本的了解才能使用它编写程序。有许多关于 OpenGL 的优秀教程和参考资料,请参阅“资源”部分以获取建议列表。

PyOpenGL 的首要要求是 OpenGL 本身。如果您尚未安装 OpenGL 实现,请检查您的 GNU/Linux 发行版是否包含这些软件包,或者从 www.mesa.org 下载 Mesa 3-D 图形库。为了使 PyOpenGL 充分发挥作用,必须安装 Numerical Python 模块。Numeric 和 PyOpenGL 的源代码可以在 numpy.sourceforge.netpyopengl.sourceforge.net 找到。由于 Greg Ward 的 distutils 模块(Python 1.6 版本中包含),编译和安装非常容易。从解压缩的源代码目录内部运行命令 python setup.py install 应该构建和安装模块。从源代码安装之前,您可能需要检查您的 GNU/Linux 发行版是否已提供这些模块。它们包含在我的 Debian 发行版中。注意:我使用的版本是 PyOpenGL 1.5.7,自本文撰写之时起,版本 2.0 已可用。

一个简单的 Python OpenGL 应用程序

OpenGL 规范没有定义与窗口系统交互的规范。因此,使用 OpenGL 的程序必须使用外部 GUI 工具包。清单 1 中的程序使用了 GLUT,这是一个用于 OpenGL 的跨平台窗口工具包。除非您正在使用商业 OpenGL 实现,否则您可能已经安装了 GLUT。

清单 1. 打开窗口,设置光照并使用 GLUT 绘制茶壶

此代码打开一个窗口,设置光照并绘制一个茶壶。除了 Python 提供的附加语法紧凑性之外,它看起来很像用 C 编写的等效程序。一个小的区别是如何设置显示函数回调。在 C 或 C++ 中设置显示函数回调只需要调用函数 glutDisplayFunc(display)。在 PyOpenGL 中设置回调分两个步骤完成:调用 glutSetDisplayFunc(),然后调用 glutDisplayFunc()。这种特性也适用于设置其他回调,例如 glutMouseFunc() 和 glutReshapeFunc()。

虽然 GLUT 适用于大多数小型 OpenGL 应用程序,但它仍然需要相当多的工作才能实现测试时通常需要的功能,例如用于缩放、平移和旋转的鼠标控制。Togl 是一个 Tkinter 小部件,它自动提供这些功能以及默认照明。清单 2 显示了使用 Togl 的相同程序。

清单 2. 使用 Togl 的相同程序

请注意,它使用的代码明显更少,但提供了更多的功能。这样做的代价是灵活性。如果 Togl 的默认照明和用户界面不符合您的要求,您将需要自己重新实现它们。Togl 非常适合原型设计,因为它消除了编写和调试样板照明和导航代码的需要。

PyOpenGL 也与其他具有 3-D 小部件的 GUI 工具包很好地集成。存在 wxWindows、FLTK、FOX 和 GTK 的绑定。

使用纹理贴图

纹理贴图是将图像数据(如照片)“粘贴”到多边形上的技术。使用纹理需要将外部纹理图像转换为 OpenGL 支持的 low-level 内部格式之一。虽然在 Python 中设置纹理所需的代码与普通的 OpenGL 代码没有太大区别,但 Python 确实简化了加载和操作纹理图像文件的过程。

Python 标准库包括 rgbimg,一个用于读取 SGI imglib(.rgb) 文件的模块。虽然这看起来像是一种晦涩的格式,但它的简单性允许将其包含在 Python 的标准库中,而不会造成太多的膨胀。使用 GIMP 或 ImageMagick,您可以将 PNG、JPEG 或 TIFF 等格式的图像转换为 SGI imglib。命令 rgbimg.longimagedata(file) 将读取和解码指定的 SGI imglib 格式的文件,并将其作为四字节 RGBA 像素的二进制字符串返回。此数据可以传递给 OpenGL 函数,如 glTexImage2D,使用格式参数 GL_RGBA 和数据类型 GL_UNSIGNED_BYTE。

Python 标准库中的另一个有用模块是 imageop。此模块包括用于图像裁剪、缩放和灰度转换的函数。imageop 函数以与 rgbimg.longimagedata() 返回的数据相同的 GL_RGBA/GL_UNSIGNED_BYTE 格式操作数据。

PyOpenGL 实用函数

除了包装核心库和工具包外,PyOpenGL 还包括许多实用函数来执行 high-level 任务。您是否曾经想过抓取 OpenGL 场景的静止图像?您始终可以使用支持屏幕捕获的程序,例如 xv 或 GIMP,但是如果您想在特定时间点抓取静止图像怎么办?如果程序需要在无人控制的情况下运行(如 CGI 脚本),则手动截取屏幕截图将不起作用。您也不能手动每秒抓取十几个静止图像来创建电影。PyOpenGL 有几个辅助函数,用于自动抓取各种格式的场景快照。

清单 3 是先前程序的改编版本,它将场景的 PostScript 图像保存到磁盘,然后退出。您可以查看图 1 中保存的图像。

清单 3. 保存 PostScript 图像,然后退出

3-D Programming with Python

图 1. 清单 3 的结果

场景使用命令保存

openglutil.glSaveEPS('ex3.eps', 240, 240)

第一个参数是要保存到的文件名。第二个参数是要捕获的像素宽度,第三个参数是高度。使用与 glSavePPM() 命令相同的语法将以可移植像素图文件格式保存场景图像。此外,如果您已编译具有 TIFF 支持的 PyOpenGL,则 glSaveTIFF() 命令可用于以 TIFF 格式保存场景快照。

PyOpenGL 还为 OpenGL 的对象选择或“拾取”机制提供了方便的包装器。OpenGL 中的选择模式操作会自动告诉您哪些对象落在了视图屏幕的给定区域内。这意味着您可以将选择机制传递到屏幕上鼠标单击发生的位置,它会告诉您单击了哪个对象。

以下是在 PyOpenGL 中设置选择机制的基本步骤

  1. 编写一个方法或函数,在调用 glPushName() 和 glPopName() 之间包装绘制场景中的每个对象。为场景中的每个独立对象向 glPushName() 传递不同的整数作为参数。OpenGL 将使用此值来指示选择了哪些对象。

  2. 设置鼠标单击的事件处理程序。这方面的工作方式取决于您使用的工具包。在 Tkinter 中,鼠标单击事件处理程序通过将方法或函数引用传递给 bind() 方法来设置。

  3. 一旦发生鼠标单击,将鼠标 x 和 y 坐标以及对第一部分中创建的方法或函数的引用传递给函数 OpenGL.GL.glSelectWithCallback()。此函数将返回一个元组列表,形式为 (near, far, names),其中 near 和 far 表示整数深度值,names 是在 glPushName() 中为选定对象传递的名称元组。如果未选择任何对象,则返回一个空元组。

清单 4 演示了 PyOpenGL 中选择机制的使用。此演示在屏幕上绘制一个立方体和一个球体。尝试按住 Ctrl 键并在指针位于不同位置时按下鼠标左键。

清单 4. PyOpenGL 中的选择机制

3-D Programming with Python

图 2. 清单 4 的结果

3-D Programming with Python

PyOpenGL 附带的 fog.py 演示

3-D Programming with Python

使用 PyOpenGL 渲染的具有光照和纹理的简单场景

3-D Programming with Python

使用 PyOpenGL 渲染的 3DStudio 模型

选择机制是执行此任务的最简单方法,但绝不是唯一方法。另一种技术是将对象渲染到 OpenGL 不发送到输出设备的后备缓冲区。为此,请使用不同的颜色渲染每个对象。完成后,后备缓冲区应包含场景的 2-D 渲染。通过检查特定点的颜色,您可以确定在该屏幕区域中绘制了哪个对象。此后备缓冲区在 OpenGL 中称为反馈缓冲区。

另一种对象拾取方法是手动进行相交测试。这意味着您必须从视图屏幕投射一条光线,并测试场景中的每个对象是否相交。找到相交对象列表后,您必须按深度值排序以查看哪个对象最靠近视图屏幕。虽然这可以提供对过程的更多控制,但这是一项艰巨的任务,并且在 Python 中难以高效地完成。

提高性能

现在您已经了解了如何编写简单的 OpenGL 程序,您可能想知道 Python 是否可以扩展到更高级的 3-D 应用程序的需求。虽然 PyOpenGL 程序的性能通常落后于其 C 或 C++ 对等程序,但优化技术可以大大缩小差距。

提高执行速度的主要策略是通过将昂贵的操作移动到本机代码中来减少在 Python 解释器中花费的时间量。实现此目的的一种方法是用快速的本机编译语言(如 C 或 C++)重写程序中运行缓慢的部分。将程序的这些编译部分实现为 Python 扩展模块,允许剩余的解释型 Python 代码访问其功能。虽然这种方法肯定有可能加快速度,但它缺乏纯 Python 解决方案的简单性。它还要求您了解如何用编译为本机代码的语言编写 Python 扩展模块。此外,如果您想用 C 来完成它,您一开始就不会使用 Python!

OpenGL 显示列表提供了一种将操作移动到本机代码中的方法,而不会产生与编写扩展模块的前一种方法相关的任何麻烦。显示列表允许 OpenGL 程序在渲染管道中更深地缓存一组命令。在某些环境中,OpenGL 甚至可以将显示列表存储在显卡本身上,远离 Python 解释器的瓶颈。

glGenLists() 命令创建一个空显示列表数组。它接受一个整数参数,即要生成的显示列表的数量。它返回成功创建的列表数。使用命令 glNewList() 和 glEndList() 包装一组 OpenGL 操作会填充指定的显示列表。存储后,后续调用该组操作只需要一个命令 glCallList()。在 PyOpenGL 中使用显示列表的语法与 C 中的 OpenGL 几乎相同。

后续步骤

我们才刚刚开始了解 Python 中的 OpenGL 编程技术的表面。有关更多信息,请务必查看 PyOpenGL 随附的文档或在线文档:pyopengl.sourceforge.net/documentation/index.html

资源

3-D Programming with Python
Jason Petrone (jpetrone@acm.org) 是弗吉尼亚州雷斯顿市国家研究倡议公司 (CNRI) 的技术人员。他在以前的工作单位国家超级计算应用中心 (NCSA) 从事 CAVE 虚拟现实项目时,首次发现了 OpenGL 和 Python 的强大组合。
加载 Disqus 评论