上一篇我们在Windows环境下面用D3D绘制了一个在三维空间当中的三角形。
本篇我们在Linux环境当中用OpenGL来绘制一个在三维空间当中的矩形。
本文所用的代码存储在GitHub:article_12这个分支当中。
netwarm007/GameEngineFromScratch
与之前一样,为了便于之后比较差异,考虑图形模块的具体设计,我们尽量留用前面的代码。
首先进入Platform/Linux目录,复制helloengine_xcb.c到helloengine_opengl.cpp。然后作如下变更:
#include <stdlib.h>
#include <string.h>
+#include <X11/Xlib.h>
+#include <X11/Xlib-xcb.h>
#include <xcb/xcb.h>
+#include <GL/gl.h>
+#include <GL/glx.h>
+#include <GL/glu.h>
+
+#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
+typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
+
+// Helper to check for extension string presence. Adapted from:
+// http://www.opengl.org/resources/features/OGLextensions/
+static bool isExtensionSupported(const char *extList, const char *extension)
+{
+ const char *start;
+ const char *where, *terminator;
+
+ /* Extension names should not have spaces. */
+ where = strchr(extension, ' ');
+ if (where || *extension == '')
+ return false;
+
+ /* It takes a bit of care to be fool-proof about parsing the
+ OpenGL extensions string. Don't be fooled by sub-strings,
+ etc. */
+ for (start=extList;;) {
+ where = strstr(start, extension);
+
+ if (!where)
+ break;
+
+ terminator = where + strlen(extension);
+
+ if ( where == start || *(where - 1) == ' ' )
+ if ( *terminator == ' ' || *terminator == '' )
+ return true;
+
+ start = terminator;
+ }
+
+ return false;
+}
+
+static bool ctxErrorOccurred = false;
+static int ctxErrorHandler(Display *dpy, XErrorEvent *ev)
+{
+ ctxErrorOccurred = true;
+ return 0;
+}
+
+void DrawAQuad() {
+ glClearColor(1.0, 1.0, 1.0, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrtho(-1., 1., -1., 1., 1., 20.);
+
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+ gluLookAt(0., 0., 10., 0., 0., 0., 0., 1., 0.);
+
+ glBegin(GL_QUADS);
+ glColor3f(1., 0., 0.);
+ glVertex3f(-.75, -.75, 0.);
+ glColor3f(0., 1., 0.);
+ glVertex3f( .75, -.75, 0.);
+ glColor3f(0., 0., 1.);
+ glVertex3f( .75, .75, 0.);
+ glColor3f(1., 1., 0.);
+ glVertex3f(-.75, .75, 0.);
+ glEnd();
+}
+
int main(void) {
xcb_connection_t *pConn;
xcb_screen_t *pScreen;
@@ -11,41 +85,145 @@ int main(void) {
xcb_gcontext_t foreground;
xcb_gcontext_t background;
xcb_generic_event_t *pEvent;
+ xcb_colormap_t colormap;
uint32_t mask = 0;
- uint32_t values[2];
+ uint32_t values[3];
uint8_t isQuit = 0;
- char title[] = "Hello, Engine!";
+ char title[] = "Hello, Engine![OpenGL]";
char title_icon[] = "Hello, Engine! (iconified)";
+ Display *display;
+ int default_screen;
+ GLXContext context;
+ GLXFBConfig *fb_configs;
+ GLXFBConfig fb_config;
+ int num_fb_configs = 0;
+ XVisualInfo *vi;
+ GLXDrawable drawable;
+ GLXWindow glxwindow;
+ glXCreateContextAttribsARBProc glXCreateContextAttribsARB;
+ const char *glxExts;
+
+ // Get a matching FB config
+ static int visual_attribs[] =
+ {
+ GLX_X_RENDERABLE , True,
+ GLX_DRAWABLE_TYPE , GLX_WINDOW_BIT,
+ GLX_RENDER_TYPE , GLX_RGBA_BIT,
+ GLX_X_VISUAL_TYPE , GLX_TRUE_COLOR,
+ GLX_RED_SIZE , 8,
+ GLX_GREEN_SIZE , 8,
+ GLX_BLUE_SIZE , 8,
+ GLX_ALPHA_SIZE , 8,
+ GLX_DEPTH_SIZE , 24,
+ GLX_STENCIL_SIZE , 8,
+ GLX_DOUBLEBUFFER , True,
+ //GLX_SAMPLE_BUFFERS , 1,
+ //GLX_SAMPLES , 4,
+ None
+ };
+
+ int glx_major, glx_minor;
+
+ /* Open Xlib Display */
+ display = XOpenDisplay(NULL);
+ if(!display)
+ {
+ fprintf(stderr, "Can't open displayn");
+ return -1;
+ }
+
+ // FBConfigs were added in GLX version 1.3.
+ if (!glXQueryVersion(display, &glx_major, &glx_minor) ||
+ ((glx_major == 1) && (glx_minor < 3)) || (glx_major < 1))
+ {
+ fprintf(stderr, "Invalid GLX versionn");
+ return -1;
+ }
+
+ default_screen = DefaultScreen(display);
+
+ /* Query framebuffer configurations */
+ fb_configs = glXChooseFBConfig(display, default_screen, visual_attribs, &num_fb_configs);
+ if(!fb_configs || num_fb_configs == 0)
+ {
+ fprintf(stderr, "glXGetFBConfigs failedn");
+ return -1;
+ }
+
+ /* Pick the FB config/visual with the most samples per pixel */
+ {
+ int best_fbc = -1, worst_fbc = -1, best_num_samp = -1, worst_num_samp = 999;
+
+ for (int i=0; i<num_fb_configs; ++i)
+ {
+ XVisualInfo *vi = glXGetVisualFromFBConfig(display, fb_configs[i]);
+ if (vi)
+ {
+ int samp_buf, samples;
+ glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLE_BUFFERS, &samp_buf);
+ glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLES, &samples);
+
+ printf( " Matching fbconfig %d, visual ID 0x%lx: SAMPLE_BUFFERS = %d,"
+ " SAMPLES = %dn",
+ i, vi -> visualid, samp_buf, samples);
+
+ if (best_fbc < 0 || (samp_buf && samples > best_num_samp))
+ best_fbc = i, best_num_samp = samples;
+ if (worst_fbc < 0 || !samp_buf || samples < worst_num_samp)
+ worst_fbc = i, worst_num_samp = samples;
+ }
+ XFree( vi );
+ }
+
+ fb_config = fb_configs[best_fbc];
+ }
+
+ /* Get a visual */
+ vi = glXGetVisualFromFBConfig(display, fb_config);
+ printf("Chosen visual ID = 0x%lxn", vi->visualid);
+
/* establish connection to X server */
- pConn = xcb_connect(0, 0);
+ pConn = XGetXCBConnection(display);
+ if(!pConn)
+ {
+ XCloseDisplay(display);
+ fprintf(stderr, "Can't get xcb connection from displayn");
+ return -1;
+ }
- /* get the first screen */
- pScreen = xcb_setup_roots_iterator(xcb_get_setup(pConn)).data;
+ /* Acquire event queue ownership */
+ XSetEventQueueOwner(display, XCBOwnsEventQueue);
+
+ /* Find XCB screen */
+ xcb_screen_iterator_t screen_iter =
+ xcb_setup_roots_iterator(xcb_get_setup(pConn));
+ for(int screen_num = vi->screen;
+ screen_iter.rem && screen_num > 0;
+ --screen_num, xcb_screen_next(&screen_iter));
+ pScreen = screen_iter.data;
/* get the root window */
window = pScreen->root;
- /* create black (foreground) graphic context */
- foreground = xcb_generate_id(pConn);
- mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
- values[0] = pScreen->black_pixel;
- values[1] = 0;
- xcb_create_gc(pConn, foreground, window, mask, values);
+ /* Create XID's for colormap */
+ colormap = xcb_generate_id(pConn);
- /* create which (background) graphic context */
- background = xcb_generate_id(pConn);
- mask = XCB_GC_BACKGROUND | XCB_GC_GRAPHICS_EXPOSURES;
- values[0] = pScreen->white_pixel;
- values[1] = 0;
- xcb_create_gc(pConn, background, window, mask, values);
+ xcb_create_colormap(
+ pConn,
+ XCB_COLORMAP_ALLOC_NONE,
+ colormap,
+ window,
+ vi->visualid
+ );
/* create window */
window = xcb_generate_id(pConn);
- mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
- values[0] = pScreen->white_pixel;
- values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
+ mask = XCB_CW_EVENT_MASK | XCB_CW_COLORMAP;
+ values[0] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
+ values[1] = colormap;
+ values[2] = 0;
xcb_create_window (pConn, /* connection */
XCB_COPY_FROM_PARENT, /* depth */
window, /* window ID */
@@ -54,9 +232,11 @@ int main(void) {
640, 480, /* width, height */
10, /* boarder width */
XCB_WINDOW_CLASS_INPUT_OUTPUT, /* class */
- pScreen->root_visual, /* visual */
+ vi->visualid, /* visual */
mask, values); /* masks */
+ XFree(vi);
+
/* set the title of the window */
xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
@@ -72,13 +252,120 @@ int main(void) {
xcb_flush(pConn);
+ /* Get the default screen's GLX extension list */
+ glxExts = glXQueryExtensionsString(display, default_screen);
+
+ /* NOTE: It is not necessary to create or make current to a context before
+ calling glXGetProcAddressARB */
+ glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
+ glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );
+
+ /* Create OpenGL context */
+ ctxErrorOccurred = false;
+ int (*oldHandler)(Display*, XErrorEvent*) =
+ XSetErrorHandler(&ctxErrorHandler);
+
+ if (!isExtensionSupported(glxExts, "GLX_ARB_create_context") ||
+ !glXCreateContextAttribsARB )
+ {
+ printf( "glXCreateContextAttribsARB() not found"
+ " ... using old-style GLX contextn" );
+ context = glXCreateNewContext(display, fb_config, GLX_RGBA_TYPE, 0, True);
+ if(!context)
+ {
+ fprintf(stderr, "glXCreateNewContext failedn");
+ return -1;
+ }
+ }
+ else
+ {
+ int context_attribs[] =
+ {
+ GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
+ GLX_CONTEXT_MINOR_VERSION_ARB, 0,
+ None
+ };
+
+ printf( "Creating contextn" );
+ context = glXCreateContextAttribsARB(display, fb_config, 0,
+ True, context_attribs );
+
+ XSync(display, False);
+ if (!ctxErrorOccurred && context)
+ printf( "Created GL 3.0 contextn" );
+ else
+ {
+ /* GLX_CONTEXT_MAJOR_VERSION_ARB = 1 */
+ context_attribs[1] = 1;
+ /* GLX_CONTEXT_MINOR_VERSION_ARB = 0 */
+ context_attribs[3] = 0;
+
+ ctxErrorOccurred = false;
+
+ printf( "Failed to create GL 3.0 context"
+ " ... using old-style GLX contextn" );
+ context = glXCreateContextAttribsARB(display, fb_config, 0,
+ True, context_attribs );
+ }
+ }
+
+ XSync(display, False);
+
+ XSetErrorHandler(oldHandler);
+
+ if (ctxErrorOccurred || !context)
+ {
+ printf( "Failed to create an OpenGL contextn" );
+ return -1;
+ }
+
+ /* Verifying that context is a direct context */
+ if (!glXIsDirect (display, context))
+ {
+ printf( "Indirect GLX rendering context obtainedn" );
+ }
+ else
+ {
+ printf( "Direct GLX rendering context obtainedn" );
+ }
+
+ /* Create GLX Window */
+ glxwindow =
+ glXCreateWindow(
+ display,
+ fb_config,
+ window,
+ 0
+ );
+
+ if(!window)
+ {
+ xcb_destroy_window(pConn, window);
+ glXDestroyContext(display, context);
+
+ fprintf(stderr, "glXDestroyContext failedn");
+ return -1;
+ }
+
+ drawable = glxwindow;
+
+ /* make OpenGL context current */
+ if(!glXMakeContextCurrent(display, drawable, drawable, context))
+ {
+ xcb_destroy_window(pConn, window);
+ glXDestroyContext(display, context);
+
+ fprintf(stderr, "glXMakeContextCurrent failedn");
+ return -1;
+ }
+
+
- while((pEvent = xcb_wait_for_event(pConn)) && !isQuit) {
+ while(!isQuit && (pEvent = xcb_wait_for_event(pConn))) {
switch(pEvent->response_type & ~0x80) {
case XCB_EXPOSE:
{
- xcb_rectangle_t rect = { 20, 20, 60, 80 };
- xcb_poly_fill_rectangle(pConn, window, foreground, 1, &rect);
- xcb_flush(pConn);
+ DrawAQuad();
+ glXSwapBuffers(display, drawable);
}
break;
case XCB_KEY_PRESS:
@@ -88,6 +375,8 @@ int main(void) {
free(pEvent);
}
+
+ /* Cleanup */
xcb_disconnect(pConn);
return 0;
几个要点:
首先,在X(当前版本:11)环境当中,就如我们之前所说的,它是被设计为C-S架构,而显卡是被XServer所隐蔽的,所以如果遵从这个架构,我们是不能直接访问显卡,而需要通过一个被称为GLX的X扩展库,将3D绘图指令以X协议扩展的方式发给X Server,然后X Server再发送给显卡。
但是这样的架构对于软实时系统的游戏来说,其实是过于复杂。所以在2008年开始,X导入了DRI架构,也就是对于本地渲染的情况,可以将OpenGL指令直接发给显卡驱动,而不需要经过X。后来又出现了DRI2等。具体细节请参考下面的链接。
但是,GLX这个库在书写的时候还没有XCB,所以它是牢牢绑定Xlib的。而XCB和Xlib是一种替代关系。
所以,在基于XCB的GLX出来之前,我们不得不同时使用XCB和Xlib。用XCB来创建和管理基本的X窗口,而用Xlib + GLX来创建OpenGL相关的图形资源。这就是我们在代码里加了很多头文件的原因。
在代码当中,我们首先给出了我们想要的FrameBuffer(就是用来保存渲染结果并最终生成显示图像的内存上的一片区域)的格式:
// Get a matching FB config
static int visual_attribs[] =
{
GLX_X_RENDERABLE , True,
GLX_DRAWABLE_TYPE , GLX_WINDOW_BIT,
GLX_RENDER_TYPE , GLX_RGBA_BIT,
GLX_X_VISUAL_TYPE , GLX_TRUE_COLOR,
GLX_RED_SIZE , 8,
GLX_GREEN_SIZE , 8,
GLX_BLUE_SIZE , 8,
GLX_ALPHA_SIZE , 8,
GLX_DEPTH_SIZE , 24,
GLX_STENCIL_SIZE , 8,
GLX_DOUBLEBUFFER , True,
//GLX_SAMPLE_BUFFERS , 1,
//GLX_SAMPLES , 4,
None
};
接下来的代码是罗列出缺省显示器所支持的符合上述条件的所有FrameBuffer格式,然后选择一个最好的(采样数最多的):
/* Query framebuffer configurations */
fb_configs = glXChooseFBConfig(display, default_screen, visual_attribs, &num_fb_configs);
if(!fb_configs || num_fb_configs == 0)
{
fprintf(stderr, "glXGetFBConfigs failedn");
return -1;
}
/* Pick the FB config/visual with the most samples per pixel */
{
int best_fbc = -1, worst_fbc = -1, best_num_samp = -1, worst_num_samp = 999;
for (int i=0; i<num_fb_configs; ++i)
{
XVisualInfo *vi = glXGetVisualFromFBConfig(display, fb_configs[i]);
if (vi)
{
int samp_buf, samples;
glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLE_BUFFERS, &samp_buf);
glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLES, &samples);
printf( " Matching fbconfig %d, visual ID 0x%lx: SAMPLE_BUFFERS = %d,"
" SAMPLES = %dn",
i, vi -> visualid, samp_buf, samples);
if (best_fbc < 0 || (samp_buf && samples > best_num_samp))
best_fbc = i, best_num_samp = samples;
if (worst_fbc < 0 || !samp_buf || samples < worst_num_samp)
worst_fbc = i, worst_num_samp = samples;
}
XFree( vi );
}
fb_config = fb_configs[best_fbc];
}
因为上面都是通过Xlib进行的操作,但是我们要使用XCB来创建窗口并管理窗口,所以接下来做了一个同步,让XCB和Xlib都指向同一块屏幕(FrameBuffer)
/* establish connection to X server */
pConn = XGetXCBConnection(display);
if(!pConn)
{
XCloseDisplay(display);
fprintf(stderr, "Can't get xcb connection from displayn");
return -1;
}
/* Acquire event queue ownership */
XSetEventQueueOwner(display, XCBOwnsEventQueue);
/* Find XCB screen */
xcb_screen_iterator_t screen_iter =
xcb_setup_roots_iterator(xcb_get_setup(pConn));
for(int screen_num = vi->screen;
screen_iter.rem && screen_num > 0;
--screen_num, xcb_screen_next(&screen_iter));
pScreen = screen_iter.data;
然后我们通过XCB创建窗体,这里和(九)是基本完全一样的。
再通过Xlib+GLX来创建这个窗体当中的OpenGL绘图上下文(Context)。这里取代的是(九)当中的foreground和background。这里的代码看起来稍微有些复杂,因为在OpenGL 3.0之前(不含)的版本与之后的版本的创建方法是不一样的。当然我们可以按照低版本创建,但是版本越低,能够使用的OpenGL功能就越少。所以我们的代码进行了一些版本的探查,并根据探查结果选择最好的创建方式:
/* Get the default screen's GLX extension list */
glxExts = glXQueryExtensionsString(display, default_screen);
/* NOTE: It is not necessary to create or make current to a context before
calling glXGetProcAddressARB */
glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );
/* Create OpenGL context */
ctxErrorOccurred = false;
int (*oldHandler)(Display*, XErrorEvent*) =
XSetErrorHandler(&ctxErrorHandler);
if (!isExtensionSupported(glxExts, "GLX_ARB_create_context") ||
!glXCreateContextAttribsARB )
{
printf( "glXCreateContextAttribsARB() not found"
" ... using old-style GLX contextn" );
context = glXCreateNewContext(display, fb_config, GLX_RGBA_TYPE, 0, True);
if(!context)
{
fprintf(stderr, "glXCreateNewContext failedn");
return -1;
}
}
else
{
int context_attribs[] =
{
GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
GLX_CONTEXT_MINOR_VERSION_ARB, 0,
None
};
printf( "Creating contextn" );
context = glXCreateContextAttribsARB(display, fb_config, 0,
True, context_attribs );
XSync(display, False);
if (!ctxErrorOccurred && context)
printf( "Created GL 3.0 contextn" );
else
{
/* GLX_CONTEXT_MAJOR_VERSION_ARB = 1 */
context_attribs[1] = 1;
/* GLX_CONTEXT_MINOR_VERSION_ARB = 0 */
context_attribs[3] = 0;
ctxErrorOccurred = false;
printf( "Failed to create GL 3.0 context"
" ... using old-style GLX contextn" );
context = glXCreateContextAttribsARB(display, fb_config, 0,
True, context_attribs );
}
}
XSync(display, False);
XSetErrorHandler(oldHandler);
if (ctxErrorOccurred || !context)
{
printf( "Failed to create an OpenGL contextn" );
return -1;
}
然后为了让GLX能够使用我们通过XCB创建出来的窗口,我们对窗口进行了一次转换,让它也绑定到GLX的对象当中:
/* Create GLX Window */
glxwindow =
glXCreateWindow(
display,
fb_config,
window,
0
);
if(!window)
{
xcb_destroy_window(pConn, window);
glXDestroyContext(display, context);
fprintf(stderr, "glXDestroyContext failedn");
return -1;
}
通知OpenGL(显卡)画布的位置:
drawable = glxwindow;
/* make OpenGL context current */
if(!glXMakeContextCurrent(display, drawable, drawable, context))
{
xcb_destroy_window(pConn, window);
glXDestroyContext(display, context);
fprintf(stderr, "glXMakeContextCurrent failedn");
return -1;
}
之后用XCB处理窗体消息队列,并在XCB_EXPOSE消息处理流程当中,使用OpenGL函数完成绘图。
while(!isQuit && (pEvent = xcb_wait_for_event(pConn))) {
switch(pEvent->response_type & ~0x80) {
case XCB_EXPOSE:
{
DrawAQuad();
glXSwapBuffers(display, drawable);
}
break;
case XCB_KEY_PRESS:
isQuit = 1;
break;
}
free(pEvent);
}
这个程序的编译命令行如下:
[tim@localhost Linux]$ clang -lxcb -lX11 -lX11-xcb -lGL -lGLU -o helloengine_opengl helloengine_opengl.cpp
需要事先用apt或者yum安装libGL-dev,libGLU-dev, libX11-dev,libX11-xcb-dev,libxcb-dev。注意在不同的发行版本当中包的名字会稍有不同。
如果需要调试,则需要增加一个“-g”选项。然后使用gdb进行调试。
运行结果如下:
对比前一篇的Direct 3D,我们可以看到我们并没有提供任何的Shader程序。实际绘图的指令也仅仅是如下数行,比Direct 3D的一连串API调用要简洁明了许多。这就是我们之前提到过的,OpenGL是一种比较高层的封装,它让我们集中在要绘制的内容本身的同时,也隐藏了很多实际的处理。对于CAD、科学仿真等领域来说十分好用,但是对于更为复杂的应用来说,特别是游戏这种需要做深度优化的图形运用来讲,就显得有些封装过头了(当然,我们这里使用的是最简单的固定管道的OpenGL。OpenGL高版本也是支持GPU编程的,这个在后续介绍):
void DrawAQuad() {
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-1., 1., -1., 1., 1., 20.);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0., 0., 10., 0., 0., 0., 0., 1., 0.);
glBegin(GL_QUADS);
glColor3f(1., 0., 0.);
glVertex3f(-.75, -.75, 0.);
glColor3f(0., 1., 0.);
glVertex3f( .75, -.75, 0.);
glColor3f(0., 0., 1.);
glVertex3f( .75, .75, 0.);
glColor3f(1., 1., 0.);
glVertex3f(-.75, .75, 0.);
glEnd();
}