从零开始手敲次世代游戏引擎(十二)

上一篇我们在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 – Wikipedia

但是,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();
}

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s

%d 博主赞过: