在 X11 中实现 GTK+ 3 的 OpenGL 支持
最近,开始思考 GTK+ 3.0 的 OpenGL 支持的问题。由于 GtkGLExt 现在还不支持 GTK+ 3.0,其维护者对此没有任何表示。现在最务实的办法是使用 clutter-gtk 库,通过 Clutter 的底层库 Cogl(OpenGL 的面向对象封装)在 GTK+ 3 的 Widget 中绘制 OpenGL 图形。但是,目前 Cogl 功能尚不完善,例如不支持用户自定义帧缓存格式,缺乏光照支持,以及图形渲染方式过于单调,仅支持顶点缓存(Vertex Buffer)渲染。考虑到 GTK+ 在 X11 是通过封装 xlib 实现的,而 xlib 可通过 GLX 实现 OpenGL 支持[1]。因此,通过 xlib 与 GLX 结合的途径,理论上必然可实现 X11 环境中 GTK+ 3.0 的 OpenGL 支持。
从 GLX 和 X11 谈起
在 X11 中使用 XLib 与 GLX 绘制 OpenGL 图形的大致过程[1]如下:
- 使用 XOpenDisplay 函数建立窗口(一个 X Client)与 X Server 端的连接。
- 根据指定的 GLX 帧缓存格式,调用 glXChooseVisual 函数选择创建匹配的 X 窗口画面(Visual)格式,使得 GLX 能够将 OpenGL 光栅化产生的图像转换为适当的 X 窗口画面格式。
- 使用 glXCreateContext 函数创建一个 OpenGL 渲染环境。
- 基于步骤 2 中所选择的 X 窗口画面格式,使用 XCreateWindow 构建一个 X 窗口。
- 使用 glXMakeCurrent 函数将 OpenGL 渲染环境设为当前的 X 窗口渲染环境,然后可调用 OpenGL 绘图函数。
- 使用 XMapWindow 函数显示 X 窗口。
- 开始 X 窗口的事件循环。
- 退出 X 窗口事件循环后的资源释放。
上述过程中的三个主角分别是:X11、GLX 和 OpenGL。它们之间的关系可以理解为:一个 OpenGL 牌子的相机把现实中的图像影印为底片,一个叫做 GLX 的洗照片的人在暗房里将底片处理为相片,然后一个叫做 X Window 的家伙为相片做了个相框(事实上,X Window 是个相框公司的老板,他本人不负责制作相框,而是交给手下的工人,也就是那些被称为“窗口管理器”的家伙们,例如 metacity、mutter、compiz 等等,由他们来制作相框)。
GTK+ 3 与 X11
GTK+ 3 相较于 GTK+ 2,变化很大,主要表现在跨平台支持方面。从 GTK+ 3.0 开始,它将所有的二维图形绘制的任务(也就是 GDK 的任务)都交给了 Cairo 库,而 Cairo 库则是 X11、Wayland、Mac OS X 及 MS Windows 等主流窗口系统的二维图形渲染功能的抽象层。这样,GTK+ 3 就不需要再关注特定平台的二维图形渲染问题了,因此它的跨平台支持可以得到改善。从理论上讲,Cairo 所支持的平台也就是 GTK+ 3 所支持的平台。
下面来看一个很简单的 GTK+ 3 程序:
#include <gtk/gtk.h> int main (void) { GtkWidget *window; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL); gtk_widget_show (window); gtk_main (); return 0; }
编译这个程序很简单,设源文件为 test.c,编译命令为:
$ gcc test.c -o test $(pkg-config --cflags --libs gtk+-3.0)
在 X11 环境中,这个 GTK+ 3.0 程序基本上等价于下面的 xlib 程序:
#include <stdio.h> #include <X11/Xlib.h> int main (int argc, char **argv) { Display *display; Window window; Window root; XSetWindowAttributes attributes; Pixmap pixmap; Visual *visual; int screen; int depth; Atom wmDeleteMessage; XEvent event; display = XOpenDisplay (NULL); screen = DefaultScreen (display); visual = DefaultVisual (display, screen); root = XRootWindow (display, screen); depth = DefaultDepth (display, screen); attributes.background_pixel = XWhitePixel (display, screen); attributes.override_redirect = 0; window = XCreateWindow (display, root, 0, 0, 400, 200, 0, depth, InputOutput, visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect, &attributes); wmDeleteMessage = XInternAtom (display, "WM_DELETE_WINDOW", False); XSetWMProtocols (display, window, &wmDeleteMessage, 1); XStoreName (display, window, "test"); XMapWindow (display, window); do { XNextEvent (display, &event); if (event.type == Expose) { } else if (event.type == ClientMessage && event.xclient.data.l[0] == wmDeleteMessage) { break; } } while (event.type != KeyPress); printf ("closing display\n"); XCloseDisplay (display); }
没必要纠结于上面 xlib 程序每行代码的含义,那是 GTK+ 和 Qt 的开发者的事情。作为 X11 环境中的 GTK+ 用户,我们只需知道 GTK+ 已经对 xlib 进行了良好的封装,对于上一节中谈到的 XLib 与 GLX 绘制 OpenGL 图形的 8 个步骤,上文中的那个 GTK+ 3.0 程序完成了第 1、4、6、7、8 步骤中的全部任务。此外,GTK+ 也提供了一些函数,可以让我们有机会访问 X 窗口的一些状态[2],并且可与 GLX 函数协同工作,见下面的代码片段:
/* 访问 X 窗口状态的一些函数的用法示例 */ GtkWidget *widget; GdkScreen *screen; GdkVisual *visual; Screen xscreen; XVisualInfo *xvisual; Display *display; int attributes[] ={GLX_RGBA, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_DEPTH_SIZE,24, GLX_DOUBLEBUFFER, None}; GLXContext gl_context; ... ... ... gtk_widget_set_app_paintable (widget, TRUE); gtk_widget_set_double_buffered (widget, FALSE); display = gdk_x11_get_default_xdisplay (); xscreen = gdk_x11_get_default_screen (); xvisual = glXChooseVisual (display, xscreen, attributes); screen = gtk_widget_get_screen (window); visual = gdk_x11_screen_lookup_visual (screen, xvisual->visualid); gtk_widget_set_visual (window, visual); gl_context = glXCreateContext (display, xvisual, NULL, GL_TRUE);
上述代码片段中的一些函数的用途如下:
- gtk_widget_set_app_paintable 函数用于设定 GtkWidget 对象在程序中可绘制(有些 Widget,例如 GtkWindow 的背景默认是交给 GTK+ 主题引擎来绘制的,而在其程序中无法进行图形绘制)。
- 与 gtk_widget_set_double_buffered 函数用于关闭 GtkWidget 对象的双缓存绘制功能,因为它会与 OpenGL 自身的双缓存功能产生冲突。
- gdk_x11_get_default_xdisplay 函数用于获取 GtkWidget 对象默认的 Display 数据结构,这个数据结构实际上是由 xlib 的 XOpenDisplay 函数生成的,用于记录程序所连接的 XServer 的相关信息。GtkWidget 对象默认的 Display 数据结构,应该是 X Window 的根窗口所连接的 XServer 的相关信息。
- gdk_x11_get_default_screen 函数用于获取 GtkWidget 对象的默认屏幕。这个函数对应于前文的 xlib 程序示例中的 DefaultScreen 宏。
- glXChooseVisual 函数是 GLX 中的函数,它可以根据当前的 X Server 及其对应屏幕等信息,并结合用户设定的帧缓存格式,产生符合需求的 X 窗口画面格式。
- gtk_widget_get_screen 可获取 GtkWidget 对象当前所在的屏幕。
- gdk_x11_screen_lookup_visual 函数可根据 GtkWidget 对象当前所在的屏幕信息以及指定的 X 窗口画面格式,产生相符的 GdkVisual 结构。可将 GdkVisual 结构理解为 GTK+ Widget 的画面格式。
- gtk_widget_set_visual 函数,可将新的 GdkVisual 结构设为指定 GtkWidget 对象的画面格式。
- 上述各函数所做的工作,实际上都是为 glXCreateContext 函数铺路。glXCreateContext 函数最终产生一个 X 窗口的 OpenGL 渲染环境,这个环境与当前的 GtkWidget 匹配。
在 X11 环境中,上述代码片段基本上等价于文档 [1] 中的创建 GLXContext 渲染环境的代码,因此并不难理解。
薄层封装
既然已经知道了 GTK 3.0 通过 GLX 支持 OpenGL 的原理,那么可以考虑将上述那些比较繁琐且很少需要修改的代码封装成风格较好的一些函数,便于在实际中使用。
首先要考虑到一个 GTK+ 窗口可能会包含多个需要绘制 OpenGL 图形的 Widget,这意味着其中每个 Widget 需要拥有一个 GLXContext。因为 GTK+ Widget 的图形绘制工作一般都是在信号处理函数中进行的,而它所关联的 GLXContext 则是在 Widget 构建过程中生成,为了能够在 Widget 的信号处理函数中使用 GLXContext,并且不希望使用全局变量的话,那么只有像下面这样将 GLXContext 传递于信号处理函数:
GLXContext context; ... ... ... g_signal_connect (widget, "draw", G_CALLBACK (draw), context);
如果我们不需要向 Widget 的信号处理函数传递其他数据,那么这种办法还算方便,否则就需要另行构建结构体,将 GLXContext 与其他数据类型打包向信号处理函数传递,这非常不便。此外 GLXContext 也没有必要暴露于外,因为它与 OpenGL 图形绘制以及 GTK+ 编程都没有什么直接关系。
比较干净的做法可以构建一个 gtk_glx_context_list 渲染环境表,使之作为全局变量。这个表的每个单元都记录着一个 GtkWidget 对象及其对应的 GLXContext。由于 GtkWidget 的信号处理函数的第一个参数通常为 GtkWidget 对象本身,因此在表中可根据对象本身动态查到它对应的 GLXContext 并提取使用。如果不考虑查询效率的话,可以直接利用 GLib 提供的单向链表实现 gtk_glx_context_list,并将 GtkWidget 对象与其对应的 GLXContext 渲染环境封装为 GtkGLXContext 结构体,如下:
static GSList *gtk_glx_context_list = NULL; typedef struct _GtkGLXContext GtkGLXContext; struct _GtkGLXContext { GtkWidget *widget; GLXContext context; };
下面的 gtk_glx_get_context 函数实现了在 gtk_glx_context_list 中根据指定的 GtkWidget 对象查询相应的 GLXContext 渲染环境。
static gint find_context_according_widget (gconstpointer gtkglcontext, gconstpointer gtkwidget) { const GtkGLContext *gtkgc = gtkglcontext; const GtkWidget *widget = gtkwidget; if (gtkgc->widget == widget) return 0; else return -1; } static GSList * gtk_glx_get_context (GtkWidget *widget) { g_return_val_if_fail (gtk_glx_context_list != NULL, NULL); GSList *node = g_slist_find_custom (gtk_glx_context_list, widget, find_context_according_widget); return node; }
下面的 gtk_glx_enable 函数封装了前文所述的 GTK+ 与 GLX 协同创建 GLXContext 渲染环境的代码,并将生成的 GLXContext 连同它对应的 GtkWidget 对象形成记录添加至 gtk_glx_context_list 表中。
void gtk_glx_enable (GtkWidget *widget, gint *attributes) { GdkScreen *screen; GdkVisual *visual; Display *xdisplay; XVisualInfo *xvi; gint xscreen; GtkGLContext *gtkgc; gtk_widget_set_app_paintable (widget, TRUE); gtk_widget_set_double_buffered (widget, FALSE); gtkgc = g_slice_new0 (GtkGLContext); xdisplay = gdk_x11_get_default_xdisplay (); xscreen = gdk_x11_get_default_screen (); g_return_if_fail (NULL != (xvi = glXChooseVisual (xdisplay, xscreen, attributes))); screen = gtk_widget_get_screen (widget); g_return_if_fail (NULL != (visual = gdk_x11_screen_lookup_visual (screen, xvi->visualid))); gtk_widget_set_visual (widget, visual); gtkgc->widget = widget; g_return_if_fail (NULL != (gtkgc->context = glXCreateContext (xdisplay, xvi, NULL, GL_TRUE))); gtk_glx_context_list = g_slist_prepend (gtk_glx_context_list, gtkgc); }
一旦有了 GLXContext,那么便可封装 gtk_glx_make_current 函数,用于开启与指定 GtkWidget 对象相匹配的 GLXContext 渲染环境:
void gtk_glx_make_current (GtkWidget *widget) { Display *display = gdk_x11_get_default_xdisplay (); Window window = gdk_x11_window_get_xid (gtk_widget_get_window (widget)); GSList *node = gtk_glx_get_context (widget); GtkGLContext *gtkgc = node->data; g_return_if_fail (True == glXMakeCurrent (display, window, gtkgc->context)); }
对于支持双缓存的 GLXContext 渲染环境,需要封装一个缓存交换函数,如下:
void gtk_glx_swap_buffers (GtkWidget *widget) { Display *display = gdk_x11_get_default_xdisplay (); Window window = gdk_x11_window_get_xid (gtk_widget_get_window (widget)); glXSwapBuffers (display, window); }
下面所封装的 gtk_glx_disable 函数与 gtk_glx_enable 函数相呼应,用于释放指定的 GtkWidget 对象所对应的 GLXContext 资源,并从 gtk_glx_context_list 中删除相应的 GtkGLXContext 结点。
void gtk_glx_disable (GtkWidget *widget) { Display *display = gdk_x11_get_default_xdisplay (); g_return_if_fail (True == glXMakeCurrent (display, None, NULL)); GSList *node = gtk_glx_get_context (widget); GtkGLContext *gtkgc = node->data; glXDestroyContext (display, gtkgc->context); gtk_glx_context_list = g_slist_remove (gtk_glx_context_list, gtkgc); g_slice_free (GtkGLContext, gtkgc); }
上述便是所有的 gtk_glx_* 函数的封装,这些函数除了 gtk_glx_enable 函数需要接受一个整型数组(帧缓存格式)之外,其他函数只需要一个 GtkWidget 对象指针参数。这些函数的概览见以下 gtkglx.h 头文件:
#ifndef GTK_GLX_H #define GTK_GLX_H #include <gtk/gtk.h> #include <GL/glu.h> #include <GL/glx.h> void gtk_glx_enable (GtkWidget *widget, gint *attributes); void gtk_glx_disable (GtkWidget *widget); void gtk_glx_make_current (GtkWidget *widget); void gtk_glx_swap_buffers (GtkWidget *widget); #endif /* GTK_GLX_H */
虽然基于单链表的 GLXContext 渲染环境全局维护管理方式效率较低,但是一个 GTK+ 窗口中通常也不需要开启太多的 GLXContext。如果需要管理很多 GLXContext 渲染环境,那么可以考虑使用哈系表,以 GtkWidget 对象指针作为键,GLXContext 渲染环境作为值即可。
gtk_glx_* 函数的用法
在 GTK+ 3.0 中,每个 GtkWidget 对象都拥有 realize、configure、draw 以及 destory 信号,其中 show 信号在 Widget 被 gtk_widget_show 函数显示时发射,configure 信号在 Widget 被移动和改变尺寸时发射,draw 信号则在 Widget 区域被绘制时发射,而 destroy 信号则是在销毁 Widget 时发射。
realize 信号处理函数仅被执行一次,并且执行期间 GtkWidget 对象幕后的 X 窗口已经存在(建议使用 g_signal_connect_after 函数进行信号连接),因此可在 realize 信号处理函数中设置 OpenGL 的全局状态。
configure 信号处理函数则是设置 OpenGL 视口及场景的理想场所,因为该函数会在窗口尺寸改变时被调用,视口和场景都需要适应窗口尺寸的变化,以保持 OpenGL 的图形比例。
至于 draw 与 destory 信号处理函数,毫无疑问,它们分别应当作为 OpenGL 图形的绘制及 GLXContext 相关资源释放的场所。
基于以上分析,逐步构建下面的 GTK+ 3.0 的 OpenGL 图形绘制实例。
首先,加载 gtkglx.h 头文件:
#include "gtkglx.h"
然后,定义了几个 OpenGL 函数,完全基于 OpenGL 函数实现,与 GLX 无关,也与 GTK+ 无关,它们分别用于 OpenGL 场景初始化、配置与显示,实现一个有光照的三维场景并在其中渲染一个球体。
static void opengl_scene_init (void) { GLfloat ambient[] = { 1.0, 1.0, 1.0, 1.0 }; GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 }; GLfloat specular[] = { 1.0, 1.0, 1.0, 1.0 }; GLfloat position[] = { 0.0, 1.0, 1.0, 0.0 }; /* 设置光源 */ glLightfv (GL_LIGHT0, GL_AMBIENT, ambient); glLightfv (GL_LIGHT0, GL_DIFFUSE, diffuse); glLightfv (GL_LIGHT0, GL_SPECULAR, specular); glLightfv (GL_LIGHT0, GL_POSITION, position); glEnable (GL_LIGHTING); glEnable (GL_LIGHT0); glEnable (GL_AUTO_NORMAL); glEnable (GL_NORMALIZE); /* 启用深度测试(隐藏面摘除) */ glEnable (GL_DEPTH_TEST); } static void opengl_scene_configure (void) { /* 设置投影矩阵 */ glMatrixMode (GL_PROJECTION); glLoadIdentity (); glOrtho (-1., 1., -1., 1., -1., 20.); /* 设置模型视图 */ glMatrixMode (GL_MODELVIEW); glLoadIdentity (); glTranslatef (0., 0., -10.); // gluLookAt (0., 0., 10., 0., 0., 0., 0., 1., 0.); } static void draw_a_sphere (unsigned int solid, double radius, int slices, int stacks) { GLUquadricObj *quadObj = NULL; quadObj = gluNewQuadric (); if (solid) gluQuadricDrawStyle (quadObj, GLU_FILL); else gluQuadricDrawStyle (quadObj, GLU_LINE); gluQuadricNormals (quadObj, GLU_SMOOTH); gluSphere (quadObj, radius, slices, stacks); } static void opengl_scene_display (void) { /* 背景 */ glClearColor (0.2, 0.4, 0.6, 1.0); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 绘制几何体 */ draw_a_sphere (1, 0.5f, 100, 100); }
下面主要是 GTK+ 代码,用于创建一个 GtkWindow 对象(所有的 GTK+ 入门教程中都可找到类似示例),并在其中调用 gtk_glx_* 函数以及上述的 OpenGL 函数,将 OpenGL 场景渲染至 GtkWindow 区域。
static void glwidget_realize (GtkWidget *widget, gpointer userdata) { gtk_glx_make_current (widget); opengl_scene_init (); } static gboolean glwidget_configure (GtkWidget *widget, GdkEventConfigure *event, gpointer userdata) { GtkAllocation alc; gtk_widget_get_allocation (widget, &alc); gtk_glx_make_current (widget); glViewport (0, 0, alc.height, alc.height); opengl_scene_configure (); return TRUE; } static int glwidget_draw (GtkWidget *widget, cairo_t *cr, gpointer userdata) { gtk_glx_make_current (widget); opengl_scene_display (); gtk_glx_swap_buffers (widget); return TRUE; } static void glwidget_destory (GtkWidget *widget, gpointer userdata) { gtk_glx_disable (widget); } int main (int argc, char **argv) { GtkWidget *window; gint attributes[] ={GLX_RGBA, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_DEPTH_SIZE,24, GLX_DOUBLEBUFFER, None}; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "The OpenGL support of GTK+ 3.0"); gtk_widget_set_size_request (window, 400, 400); g_signal_connect (window, "show", G_CALLBACK (glwidget_show), NULL); g_signal_connect (window, "configure-event", G_CALLBACK (glwidget_configure), NULL); g_signal_connect (window, "draw", G_CALLBACK (glwidget_draw), NULL); g_signal_connect (window, "destroy", G_CALLBACK (glwidget_destory), NULL); g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL); /* GLXContext 渲染环境需要在 gtk widget 显示之前创建 */ gtk_glx_enable (widget, attributes); gtk_widget_show (window); gtk_main (); return 0; }
假设这个示例的源文件为 test.c,那么编译这个示例的命令为:
gcc gtkglx.c main.c -o test `pkg-config --cflags --libs gtk+-3.0` -lGL -lGLU
上述完整的示例代码的下载地址为:http://garfileo.is-programmer.com/user_files/garfileo/File/gtkglx.tar.gz
总结
由于我不是 XLib 和 GLX 专家,也不是 OpenGL 专家,更不是 GTK+ 专家,所以不敢保证上文中所有代码始终正确,它们也许仅仅是在我的机器上工作。如果你有更好的建议,希望能够在评论中告诉我。
参考文档
转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com
2011年5月14日 15:06
哈哈,说起clutter,现在正在win32下玩这个...
2014年12月31日 18:03
正在学习这方面的东西,这篇文章对我很有帮助,谢谢!
对了这篇文章,不由得就钦佩楼主前辈,如果有机会可否认识一下,喝杯茶呢?
2015年1月08日 19:51
感谢楼主的分享,在这篇文章的指导下,我成功的实现了在windows平台下,利用WGL添加GTK 3的OpenGL支持。
借这里做一下广告,上述相关实现的代码可在以下网址获得:
http://antkillerfarm.github.io/technology/2014/12/26/gtk_study.html
楼主的实现使用了全部窗体。我这个实现,成功的将OpenGL渲染绑定到单个控件上。并用到了目前主流的Glade来设计UI。