美文网首页IT@程序员猿媛
一篇文章入门Ubuntu的OpenGL开发

一篇文章入门Ubuntu的OpenGL开发

作者: 闪电的蓝熊猫 | 来源:发表于2019-04-21 16:03 被阅读50次

    因为有点小野心,想写个可以在Linux下跑的渲染库,于是就费了点功夫研究Ubuntu下OpenGL的开发。但是,由于完全没有Ubuntu下开发的经验,遇到了各种问题,折腾了一阵子,总算是有点收获,写篇文章分享一下。

    由于笔者水平有限,文章中若有什么纰漏,请路过的读者指出!

    基础知识

    与Windows不同,linux的内核没有图形界面的代码,它的界面只是运行在操作系统上的一个软件而已。也就是说,即便把这个界面关掉,系统仍然在运行,你还能再打开这个界面,这对windows来说是不可想象的,因为在windows下,图形界面是系统不可分割的一部分。

    于是,在Linux上,实现了一种名为X窗口系统的东西来模拟窗口界面。X窗口系统是基于X协议实现的。所谓的协议,相当于一种语言,你必须要理解并且遵守语言的规则才能沟通,比如http协议,客户端与服务器必须都采用http协议的规范发送、接收、解析数据,才能有绚丽的网页呈现在我们面前。Linux下的窗口与这个过程及其类似,它也是一种客户端/服务器(C/S)结构。不同的是,这个服务器和客户端是在一台机器上。于是,在Linux下进行窗口编程的时候,你会看到很多XServer,XClient的字眼,说的就是实现X协议的服务器与客户端。

    窗口实现

    开始写代码前,先做一个准备工作:安装xcb库。Ubuntu下的安装命令是:sudo apt-get install libxcb1-dev。

    从编程的角度上看,Linux把窗口的显示抽象成了这些概念:连接(connect),窗口(window),屏幕(screen),上下文(context)和事件(event)。

    • 连接(connext):一个xcb_connection_t对象,表示X客户端与X服务器之间的连接。我们创建的是X客户端,客户端需要把绘制指令发送给服务器,所以必须要有一个与服务器之间的连接。
    • 窗口(window):一个xcb_window_t对象,这个不用多说,字面上的意思。
    • 屏幕(screen):一个xcb_screen_t对象,我的理解就是物理意义上的屏幕,也就是显示器,可以通过枚举来找到所有连接的显示器。
    • 上下文(context):一个xcb_gcontext_t对象,这是与窗口关联的绘制环境,类似于Windows下的DC,所有的绘制都是在上下文上进行绘制。
    • 事件(event):一个xcb_generic_event_t对象,将用户的每一个操作都当成一个事件进行处理,类似于Windows下的消息。

    实现的流程是:
    1、创建连接。
    2、获取屏幕(需要用到连接)。
    3、创建上下文(需要用到屏幕的根窗口)。
    4、创建我们要用的窗口(需要用到屏幕)。
    5、映射窗口和连接(需要用到窗口和连接)。
    6、监听并且处理事件(需要用到连接、窗口和上下文)。

    完整代码如下所示:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #include <xcb/xcb.h>
    
    int main(void) {
        xcb_connection_t    *pConn;
        xcb_screen_t        *pScreen;
        xcb_window_t        window;
        xcb_gcontext_t      foreground;
        xcb_gcontext_t      background;
        xcb_generic_event_t *pEvent;
        uint32_t        mask = 0;
        uint32_t        values[2];
        uint8_t         isQuit = 0;
    
        char title[] = "Hello, Engine!";
        char title_icon[] = "Hello, Engine! (iconified)";
    
        /* 第一步:创建连接 */
        //  建立与X服务器的连接
        pConn = xcb_connect(0, 0);
    
        /* 第二步:获取屏幕 */        
        // xcb_get_setup函数用于从X服务器获取数据,获取的数据包括服务器支持的图像格式,
        // 可显示的屏幕列表,可用的视觉效果列表,服务器的最大请求长度等等
        // xcb_setup_roots_iterator函数只查到原型,没有函数的说明,从函数名和使用方式
        // 上看,应该是查找数据用的。
        pScreen = xcb_setup_roots_iterator(xcb_get_setup(pConn)).data;
    
        /* 第三步:创建上下文 */
        // 先获取根窗口
        window = pScreen->root;
    
        // 创建前景上下文(黑色)
        foreground = xcb_generate_id(pConn);  // 生成上下文的ID
        mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;  // 上下文的用途,前景&需要事件
        values[0] = pScreen->black_pixel; // 填充颜色(黑色)
        values[1] = 0; // 结束标志
        xcb_create_gc(pConn, foreground, window, mask, values); // 创建上下文
    
        // 创建背景上下文(白色)
        background = xcb_generate_id(pConn); // 生成上下文ID
        mask = XCB_GC_BACKGROUND | XCB_GC_GRAPHICS_EXPOSURES; // 上下文用途,前景&需要事件
        values[0] = pScreen->white_pixel; // 填充颜色(白色)
        values[1] = 0; // 结束标志
        xcb_create_gc(pConn, background, window, mask, values); // 创建上下文
    
        /* 第四步:创建窗口 */
        window = xcb_generate_id(pConn); // 创建窗口ID
        mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; // 覆盖BackPixmap,需要指定的事件
        values[0] = pScreen->white_pixel; // 白色填充
        values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS; // 需要EXPOSE事件和按键事件
        xcb_create_window (pConn,                   // 连接
                           XCB_COPY_FROM_PARENT,    // 深度值
                           window,                  // 窗口ID
                           pScreen->root,           // 父窗口,屏幕的根窗口
                           20, 20,                  // x,y坐标
                           640, 480,                // 宽度,高度
                           10,                      // 边缘宽度
                           XCB_WINDOW_CLASS_INPUT_OUTPUT, // 要么是0,要么是一些指定的值
                           pScreen->root_visual,    // 视觉效果,暂时不知道是啥玩意
                           mask, values);           // 需要的功能与值设定
    
        // 设置窗口名
        xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
                    XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
                    strlen(title), title);
    
        // 设置窗口图标
        xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
                    XCB_ATOM_WM_ICON_NAME, XCB_ATOM_STRING, 8,
                    strlen(title_icon), title_icon);
    
        /* 第五步:关联窗口和连接 */
        xcb_map_window(pConn, window);
    
        xcb_flush(pConn); // 刷新
    
        /* 第六步:处理事件 */
        while((pEvent = xcb_wait_for_event(pConn)) && !isQuit) {
            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); // 刷新
                }
                break;
            case XCB_KEY_PRESS: // 按键
                isQuit = 1;
                break;
            }
            free(pEvent);
        }
    
        xcb_disconnect(pConn); // 断开连接
    
        return 0;
    }
    

    完成代码,保存成.c文件(比如helloengine_xcb.c)。用gcc helloengine_xcb.c -lxcb -o helloengine_xcb命令构建,或者如果你用clang编译器的话使用clang -lxcb -o helloengine_xcb helloengine_xcb.c命令构建,就可以看到生成的可执行文件helloengine.xcb.out。运行文件,得到如下的结果:

    OpenGL绘制

    纯种的OpenGL绘制方式使用的是GLX库。GLX(全称OpenGL Extension to the X Window System,X窗口系统的OpenGL扩展)是OpenGL和X窗口系统的一个桥梁,它提供了用OpenGL在X窗口绘制的接口。GLX本身使用Xlib库做窗口创建等工作,它出现的时候还没有xcb这东西,xcb的出现本身就是为了代替Xlib,但是目前还没有基于xcb的GLX,所以我们的OpenGL绘制将会使用Xlib创建窗口。(也就是说上面的代码我们用不到-_-)

    画布——Drawable
    在X系统中,一个可以渲染的表面被称为一个Drawable(因为没有找到什么中文词语能够准确表达它的意思,所以还是用英文称呼最准确)。X系统提供了两种不同的Drawable:Window和Pixmap。GLX把Window封装成了GLXWindow,把Pixmap封装成GLXPixmap。要理解这个Drawable,最好的方法就是将它类比成画布,所谓的渲染就是在画布上作画,清晰、准确而且简单。
    不管是GLXWindow还是GLXPixmap,在创建的时候都需要一个GLXFBConfig来说明这块画布是什么样的,也就是画布的属性。画布的属性包括颜色缓冲区的深度以及辅助缓冲区的类型、质量、大小等等。
    为了兼容GLX1.2以及之前的版本,还有一种Drawable类型——Window,注意这不是GLX封装过后的GLXWindow,而是原始的Window。于是,GLXDrawable包括四种Drawable:GLXWindow、GLXPixmap、GLXPBuffer(对我们不重要,忽略之)以及Window。在X系统中,Window是与Visual结构相关的,可以通过Visual结构创建。

    关于Visual,XVisualInfo,以及GLXFBConfig
    早期X窗口系统使用Visual封装相关的颜色属性值(例如颜色类型,颜色深度),这时候OpenGL还没出世。
    当OpenGL出来之后,就弄出了一个XVisualInfo来扩展Visual,添加了更多的功能,比如辅助缓冲区,双缓冲区等。这个XVisualInfo被用来创建OpenGL上下文。
    1998年,GLX的1.3版出世,为了支持更多的功能(透明度,多重采样,样本缓冲区等等),就需要往里面加更多的东西,但是这些属性已经和视觉效果(Visual)关系不大了,于是就用推出了GLXFBconfig。
    所以,到现在,最稳妥的方式是使用GLXFBConfig来创建OpenGL上下文,但是你还是可以从FBConfig中获取到Visual信息。

    绘画工具——Render Context
    RenderContext的中文翻译是渲染上下文,我认为将它理解成绘画工具比较贴切。渲染上下文就是一系列已经存在的绘画工具,这些工具叫状态。设置好状态之后就可以绘制了。绘画工具和画布的属性必须要匹配,具体来说就是:

    • 支持相同类型的渲染(RGBA或者颜色索引)
    • 颜色缓冲区和辅助缓冲区的深度相同(RGB分量的尺寸大小要一样)
    • 都由一种X屏幕创建

    只要画布与绘画工具兼容,那么多个绘画工具(上下文)可以绘制到一张画布(Drawable)上,同样,一个绘画工具可以绘制到多张画布上。

    在实际的代码中,我们首先要创建的是和XServer的连接,这个连接在代码里的名字是Display。很容易引起误解的一个名字,我们要知道它就是与X服务器的连接。

    使用OpenGL的绘制过程包括:
    1、创建连接
    2、选择合适的显示配置(需要用到连接)
    3、创建窗口(需要用到连接和配置)
    4、映射窗口(需要用到连接和窗口)
    5、创建上下文(需要用到连接,配置)
    6、关联窗口和上下文(需要用到连接,窗口,配置)
    7、绘制

    完整的可运行代码如下所示:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <X11/Xlib.h>
    #include <X11/Xutil.h>
    #include <GL/gl.h>
    #include <GL/glx.h>
    
    #define GLX_CONTEXT_MAJOR_VERSION_ARB       0x2091
    #define GLX_CONTEXT_MINOR_VERSION_ARB       0x2092
    typedef GLXContext (* glXCreateContextAttribsARBProc) (Display*, GLXFBConfig, GLXContext, Bool, const int*);
    
    int main (int argc, char* argv[])
    {
        // The XOpenDisplay() function returns a Display structure that serves as the connection 
        // to the X server and that contains all the information about that X server.
        // Display这个东西,与之前所讲的那些都不同,它更像是一个画布和绘画工具集合。所有的东西都和它有关,需要通过它来创建,状态也会保存在它里面。
        Display* display = XOpenDisplay(NULL);  
        if (!display)
        {
            printf("Failed to open X display\n");
            exit(1);
        }
    
        // FBConfigs were added in GLX version 1.3
        // 确保你的GLX版本在1.3之上
        int glx_major, glx_minor;
        if ( !glXQueryVersion( display, &glx_major, &glx_minor ) || 
            ( ( glx_major == 1 ) && ( glx_minor < 3 ) ) || ( glx_major < 1 ) )
        {
          printf("Invalid GLX version");
          exit(1);
        }
    
        // Get a matching FB config
        // A list of attribute/value pairs.
        // 我们需要的显示属性,一会查查系统中有没有满足要求的
        static int visual_attribs[] = 
        {
            GLX_X_RENDERABLE,   True,   // If drawables can be renderd to by X.
            GLX_DRAWABLE_TYPE,  GLX_WINDOW_BIT, // Indicating what drawable types the frame buffer configuration supports. GLX_WINDOW_BIT, GLX_PIXMAP_BIT, GLX_PBUFFER_BIT
            GLX_RENDER_TYPE,    GLX_RGBA_BIT,   // Indicating what type of GLX contexts can be made current to the frame buffer configuration. GLX_RGBA_BIT, GLX_COLOR_INDEX_BIT
            GLX_X_VISUAL_TYPE,  GLX_TRUE_COLOR, // Visual type of associated visual.
            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,
            None
        };
    
        printf( "Getting matching framebuffer configs\n" );
        int fbcount;
        GLXFBConfig* fbc = glXChooseFBConfig(display, DefaultScreen(display), visual_attribs, &fbcount); // 找找有没有满足要求的配置
        if (!fbc)
        {
          printf( "Failed to retrieve a framebuffer config\n" );
          exit(1);
        }
        printf( "Found %d matching FB configs.\n", fbcount );
    
        GLXFBConfig bestFbc = fbc[0]; // 找一个配置保存
    
        // Be sure to free the FBConfig list allocated by glXChooseFBConfig()
        XFree( fbc );
    
        // Get a visual
        // 获取视觉效果信息
        XVisualInfo *vi = glXGetVisualFromFBConfig( display, bestFbc );
        printf( "Chosen visual ID = 0x%x\n", vi->visualid );
    
        printf("Creating colormap\n");
        // 窗口属性,最重要的是颜色,必须创建与视觉效果匹配的颜色属性,这在创建Window的时候有用
        XSetWindowAttributes swa;
        Colormap cmap;
        swa.colormap = cmap = XCreateColormap(display,
            RootWindow(display, vi->screen),
            vi->visual, AllocNone);
        swa.background_pixmap = None;
        swa.border_pixel = 0;
        swa.event_mask = StructureNotifyMask;
    
        printf("Creating window\n");
        Window win = XCreateWindow(display, RootWindow(display, vi->screen),
            0, 0, 100, 100, 0, vi->depth, InputOutput,
            vi->visual,
            CWBorderPixel | CWColormap | CWEventMask, &swa);
        if (!win)
        {
            printf("Failed to create window.\n");
            exit(1);
        }
    
        // Done with the visual info data
        XFree (vi); // 释放获取的视觉效果信息
    
        XStoreName(display, win, "GL 3.0 Window");
    
        printf("Mapping window\n");
        XMapWindow(display, win);
    
        // NOTE:It is note necessary to create or make current to a context before
        // calling glXGetProcAddressARB
        // 获取创建上下文的函数地址
        glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
        glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
            glXGetProcAddressARB((const GLubyte*)"glXCreateContextAttribsARB");
        GLXContext ctx = 0;
    
        int context_attribs[] = 
        {
            GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
            GLX_CONTEXT_MINOR_VERSION_ARB, 0,
            None
        };
    
        printf("Creating context 3.0\n");
        // 创建上下文
        ctx = glXCreateContextAttribsARB(display, bestFbc, 0, True, context_attribs);
    
        // Sync to ensure any errors generated are processed.
        XSync(display, False);
        if (ctx == 0)
        {
            printf("Can't create GL 3.0 context.\n");
            exit(1);
        }
    
        // Sync to ensure any errors generated are processed.
        XSync(display, False);
    
        printf("Draw with context\n");
        glXMakeCurrent(display, win, ctx);
    
        glClearColor(0, 0.5, 1, 1);
        glClear(GL_COLOR_BUFFER_BIT);
        glXSwapBuffers(display, win);
    
        sleep(1);
    
        glClearColor(1, 0.5, 0, 1);
        glClear(GL_COLOR_BUFFER_BIT);
        glXSwapBuffers(display, win);
    
        sleep(1);
    
        glXMakeCurrent(display, 0, 0);
        glXDestroyContext(display, ctx);
    
        XDestroyWindow(display, win);
        XFreeColormap(display, cmap);
        XCloseDisplay(display);
    
        return 0;
    }
    
    

    在编译前,首先需要安装OpenGL的运行环境,需要安装两个库:libgl1-mesa-dev和Xlib,命令是sudo apt install libgl1-mesa-dev Xlib。

    安装完成后,使用g++ -o helloengine_openglxlib helloengine_openglxlib.cpp -lGL -lX11命令来编译代码,-o后接的是输出的可执行文件名,你可以随便取名字;再后面是代码文件名,之后跟的是链接库的名字(GL和X11)。这里解释一下X11是什么,所谓的X11就是X协议的第11个版本,也就是说11只不过是版本号而已。

    运行helloengine_openglxlib,我们得到了如下的结果:


    总结

    一开始写代码的时候非常不适应,不仅是对开发环境,对这些结构名、类名都不熟悉,导致敲代码频频敲错,然后编译的时候各种报错。但是,多敲几遍,多看几遍之后慢慢就熟悉了,渐渐的就有了“代码感”。下面列出的参考资料里推荐阅读linux图形界面编程基础知识OpenGL® Graphics with the X Window System®加深对Linux和Linux下OpenGL编程的理解,非常有用,强烈推荐!

    参考资料

    XCB: XCB Core API
    XCB-WikiPedia
    从零开始手敲次世代游戏引擎(九)
    GLX-Wikipedia
    有关纯xcb的glx的讨论
    linux图形界面编程基础知识
    Tutorial: OpenGL 3.0 Context Creation (GLX)
    OpenGL® Graphics with the X Window System®
    What's the difference between a GLX visual and a FBconfig?

    相关文章

      网友评论

        本文标题:一篇文章入门Ubuntu的OpenGL开发

        本文链接:https://www.haomeiwen.com/subject/prhbwqtx.html