因為有點(diǎn)小野心,想寫個可以在Linux下跑的渲染庫,于是就費(fèi)了點(diǎn)功夫研究Ubuntu下OpenGL的開發(fā)泛源。但是,由于完全沒有Ubuntu下開發(fā)的經(jīng)驗忿危,遇到了各種問題达箍,折騰了一陣子,總算是有點(diǎn)收獲铺厨,寫篇文章分享一下缎玫。
由于筆者水平有限,文章中若有什么紕漏解滓,請路過的讀者指出赃磨!
基礎(chǔ)知識
與Windows不同,linux的內(nèi)核沒有圖形界面的代碼洼裤,它的界面只是運(yùn)行在操作系統(tǒng)上的一個軟件而已邻辉。也就是說,即便把這個界面關(guān)掉腮鞍,系統(tǒng)仍然在運(yùn)行值骇,你還能再打開這個界面,這對windows來說是不可想象的移国,因為在windows下吱瘩,圖形界面是系統(tǒng)不可分割的一部分。
于是迹缀,在Linux上使碾,實現(xiàn)了一種名為X窗口系統(tǒng)的東西來模擬窗口界面。X窗口系統(tǒng)是基于X協(xié)議實現(xiàn)的祝懂。所謂的協(xié)議票摇,相當(dāng)于一種語言,你必須要理解并且遵守語言的規(guī)則才能溝通嫂易,比如http協(xié)議兄朋,客戶端與服務(wù)器必須都采用http協(xié)議的規(guī)范發(fā)送、接收、解析數(shù)據(jù)颅和,才能有絢麗的網(wǎng)頁呈現(xiàn)在我們面前傅事。Linux下的窗口與這個過程及其類似,它也是一種客戶端/服務(wù)器(C/S)結(jié)構(gòu)峡扩。不同的是蹭越,這個服務(wù)器和客戶端是在一臺機(jī)器上。于是教届,在Linux下進(jìn)行窗口編程的時候响鹃,你會看到很多XServer,XClient的字眼案训,說的就是實現(xiàn)X協(xié)議的服務(wù)器與客戶端买置。
窗口實現(xiàn)
開始寫代碼前,先做一個準(zhǔn)備工作:安裝xcb庫强霎。Ubuntu下的安裝命令是:sudo apt-get install libxcb1-dev忿项。
從編程的角度上看,Linux把窗口的顯示抽象成了這些概念:連接(connect)城舞,窗口(window)轩触,屏幕(screen),上下文(context)和事件(event)家夺。
- 連接(connext):一個xcb_connection_t對象脱柱,表示X客戶端與X服務(wù)器之間的連接。我們創(chuàng)建的是X客戶端拉馋,客戶端需要把繪制指令發(fā)送給服務(wù)器榨为,所以必須要有一個與服務(wù)器之間的連接。
- 窗口(window):一個xcb_window_t對象椅邓,這個不用多說柠逞,字面上的意思。
- 屏幕(screen):一個xcb_screen_t對象景馁,我的理解就是物理意義上的屏幕,也就是顯示器逗鸣,可以通過枚舉來找到所有連接的顯示器合住。
- 上下文(context):一個xcb_gcontext_t對象,這是與窗口關(guān)聯(lián)的繪制環(huán)境撒璧,類似于Windows下的DC透葛,所有的繪制都是在上下文上進(jìn)行繪制。
- 事件(event):一個xcb_generic_event_t對象卿樱,將用戶的每一個操作都當(dāng)成一個事件進(jìn)行處理僚害,類似于Windows下的消息。
實現(xiàn)的流程是:
1繁调、創(chuàng)建連接萨蚕。
2靶草、獲取屏幕(需要用到連接)。
3岳遥、創(chuàng)建上下文(需要用到屏幕的根窗口)奕翔。
4、創(chuàng)建我們要用的窗口(需要用到屏幕)浩蓉。
5派继、映射窗口和連接(需要用到窗口和連接)。
6捻艳、監(jiān)聽并且處理事件(需要用到連接驾窟、窗口和上下文)。
完整代碼如下所示:
#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)";
/* 第一步:創(chuàng)建連接 */
// 建立與X服務(wù)器的連接
pConn = xcb_connect(0, 0);
/* 第二步:獲取屏幕 */
// xcb_get_setup函數(shù)用于從X服務(wù)器獲取數(shù)據(jù)认轨,獲取的數(shù)據(jù)包括服務(wù)器支持的圖像格式绅络,
// 可顯示的屏幕列表,可用的視覺效果列表好渠,服務(wù)器的最大請求長度等等
// xcb_setup_roots_iterator函數(shù)只查到原型昨稼,沒有函數(shù)的說明,從函數(shù)名和使用方式
// 上看拳锚,應(yīng)該是查找數(shù)據(jù)用的假栓。
pScreen = xcb_setup_roots_iterator(xcb_get_setup(pConn)).data;
/* 第三步:創(chuàng)建上下文 */
// 先獲取根窗口
window = pScreen->root;
// 創(chuàng)建前景上下文(黑色)
foreground = xcb_generate_id(pConn); // 生成上下文的ID
mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES; // 上下文的用途,前景&需要事件
values[0] = pScreen->black_pixel; // 填充顏色(黑色)
values[1] = 0; // 結(jié)束標(biāo)志
xcb_create_gc(pConn, foreground, window, mask, values); // 創(chuàng)建上下文
// 創(chuàng)建背景上下文(白色)
background = xcb_generate_id(pConn); // 生成上下文ID
mask = XCB_GC_BACKGROUND | XCB_GC_GRAPHICS_EXPOSURES; // 上下文用途霍掺,前景&需要事件
values[0] = pScreen->white_pixel; // 填充顏色(白色)
values[1] = 0; // 結(jié)束標(biāo)志
xcb_create_gc(pConn, background, window, mask, values); // 創(chuàng)建上下文
/* 第四步:創(chuàng)建窗口 */
window = xcb_generate_id(pConn); // 創(chuàng)建窗口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坐標(biāo)
640, 480, // 寬度牙丽,高度
10, // 邊緣寬度
XCB_WINDOW_CLASS_INPUT_OUTPUT, // 要么是0,要么是一些指定的值
pScreen->root_visual, // 視覺效果兔魂,暫時不知道是啥玩意
mask, values); // 需要的功能與值設(shè)定
// 設(shè)置窗口名
xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
strlen(title), title);
// 設(shè)置窗口圖標(biāo)
xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
XCB_ATOM_WM_ICON_NAME, XCB_ATOM_STRING, 8,
strlen(title_icon), title_icon);
/* 第五步:關(guān)聯(lián)窗口和連接 */
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); // 繪制一塊矩形區(qū)域
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
命令構(gòu)建析校,或者如果你用clang編譯器的話使用clang -lxcb -o helloengine_xcb helloengine_xcb.c
命令構(gòu)建构罗,就可以看到生成的可執(zhí)行文件helloengine.xcb.out。運(yùn)行文件智玻,得到如下的結(jié)果:
OpenGL繪制
純種的OpenGL繪制方式使用的是GLX庫遂唧。GLX(全稱OpenGL Extension to the X Window System,X窗口系統(tǒng)的OpenGL擴(kuò)展)是OpenGL和X窗口系統(tǒng)的一個橋梁吊奢,它提供了用OpenGL在X窗口繪制的接口盖彭。GLX本身使用Xlib庫做窗口創(chuàng)建等工作,它出現(xiàn)的時候還沒有xcb這東西,xcb的出現(xiàn)本身就是為了代替Xlib召边,但是目前還沒有基于xcb的GLX铺呵,所以我們的OpenGL繪制將會使用Xlib創(chuàng)建窗口。(也就是說上面的代碼我們用不到-_-)
畫布——Drawable
在X系統(tǒng)中掌实,一個可以渲染的表面被稱為一個Drawable(因為沒有找到什么中文詞語能夠準(zhǔn)確表達(dá)它的意思陪蜻,所以還是用英文稱呼最準(zhǔn)確)。X系統(tǒng)提供了兩種不同的Drawable:Window和Pixmap贱鼻。GLX把Window封裝成了GLXWindow宴卖,把Pixmap封裝成GLXPixmap。要理解這個Drawable邻悬,最好的方法就是將它類比成畫布症昏,所謂的渲染就是在畫布上作畫,清晰父丰、準(zhǔn)確而且簡單肝谭。
不管是GLXWindow還是GLXPixmap,在創(chuàng)建的時候都需要一個GLXFBConfig來說明這塊畫布是什么樣的蛾扇,也就是畫布的屬性攘烛。畫布的屬性包括顏色緩沖區(qū)的深度以及輔助緩沖區(qū)的類型、質(zhì)量镀首、大小等等坟漱。
為了兼容GLX1.2以及之前的版本,還有一種Drawable類型——Window更哄,注意這不是GLX封裝過后的GLXWindow芋齿,而是原始的Window。于是成翩,GLXDrawable包括四種Drawable:GLXWindow觅捆、GLXPixmap、GLXPBuffer(對我們不重要麻敌,忽略之)以及Window栅炒。在X系統(tǒng)中,Window是與Visual結(jié)構(gòu)相關(guān)的术羔,可以通過Visual結(jié)構(gòu)創(chuàng)建职辅。
關(guān)于Visual,XVisualInfo聂示,以及GLXFBConfig
早期X窗口系統(tǒng)使用Visual封裝相關(guān)的顏色屬性值(例如顏色類型,顏色深度)簇秒,這時候OpenGL還沒出世鱼喉。
當(dāng)OpenGL出來之后,就弄出了一個XVisualInfo來擴(kuò)展Visual,添加了更多的功能扛禽,比如輔助緩沖區(qū)锋边,雙緩沖區(qū)等。這個XVisualInfo被用來創(chuàng)建OpenGL上下文编曼。
1998年豆巨,GLX的1.3版出世,為了支持更多的功能(透明度掐场,多重采樣往扔,樣本緩沖區(qū)等等),就需要往里面加更多的東西熊户,但是這些屬性已經(jīng)和視覺效果(Visual)關(guān)系不大了萍膛,于是就用推出了GLXFBconfig。
所以嚷堡,到現(xiàn)在蝗罗,最穩(wěn)妥的方式是使用GLXFBConfig來創(chuàng)建OpenGL上下文,但是你還是可以從FBConfig中獲取到Visual信息蝌戒。
繪畫工具——Render Context
RenderContext的中文翻譯是渲染上下文串塑,我認(rèn)為將它理解成繪畫工具比較貼切。渲染上下文就是一系列已經(jīng)存在的繪畫工具北苟,這些工具叫狀態(tài)鼓寺。設(shè)置好狀態(tài)之后就可以繪制了。繪畫工具和畫布的屬性必須要匹配卷哩,具體來說就是:
- 支持相同類型的渲染(RGBA或者顏色索引)
- 顏色緩沖區(qū)和輔助緩沖區(qū)的深度相同(RGB分量的尺寸大小要一樣)
- 都由一種X屏幕創(chuàng)建
只要畫布與繪畫工具兼容匙头,那么多個繪畫工具(上下文)可以繪制到一張畫布(Drawable)上,同樣桃移,一個繪畫工具可以繪制到多張畫布上屋匕。
在實際的代碼中,我們首先要創(chuàng)建的是和XServer的連接借杰,這個連接在代碼里的名字是Display过吻。很容易引起誤解的一個名字,我們要知道它就是與X服務(wù)器的連接蔗衡。
使用OpenGL的繪制過程包括:
1纤虽、創(chuàng)建連接
2、選擇合適的顯示配置(需要用到連接)
3绞惦、創(chuàng)建窗口(需要用到連接和配置)
4逼纸、映射窗口(需要用到連接和窗口)
5、創(chuàng)建上下文(需要用到連接济蝉,配置)
6杰刽、關(guān)聯(lián)窗口和上下文(需要用到連接菠发,窗口,配置)
7贺嫂、繪制
完整的可運(yùn)行代碼如下所示:
#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這個東西滓鸠,與之前所講的那些都不同,它更像是一個畫布和繪畫工具集合第喳。所有的東西都和它有關(guān)糜俗,需要通過它來創(chuàng)建,狀態(tài)也會保存在它里面曲饱。
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.
// 我們需要的顯示屬性悠抹,一會查查系統(tǒng)中有沒有滿足要求的
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");
// 窗口屬性,最重要的是顏色渔工,必須創(chuàng)建與視覺效果匹配的顏色屬性锌钮,這在創(chuàng)建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
// 獲取創(chuàng)建上下文的函數(shù)地址
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");
// 創(chuàng)建上下文
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的運(yùn)行環(huán)境引矩,需要安裝兩個庫:libgl1-mesa-dev和Xlib梁丘,命令是sudo apt install libgl1-mesa-dev Xlib。
安裝完成后旺韭,使用g++ -o helloengine_openglxlib helloengine_openglxlib.cpp -lGL -lX11
命令來編譯代碼氛谜,-o后接的是輸出的可執(zhí)行文件名,你可以隨便取名字区端;再后面是代碼文件名值漫,之后跟的是鏈接庫的名字(GL和X11)。這里解釋一下X11是什么织盼,所謂的X11就是X協(xié)議的第11個版本杨何,也就是說11只不過是版本號而已。
運(yùn)行helloengine_openglxlib沥邻,我們得到了如下的結(jié)果:
總結(jié)
一開始寫代碼的時候非常不適應(yīng)危虱,不僅是對開發(fā)環(huán)境,對這些結(jié)構(gòu)名唐全、類名都不熟悉埃跷,導(dǎo)致敲代碼頻頻敲錯,然后編譯的時候各種報錯邮利。但是弥雹,多敲幾遍,多看幾遍之后慢慢就熟悉了延届,漸漸的就有了“代碼感”剪勿。下面列出的參考資料里推薦閱讀linux圖形界面編程基礎(chǔ)知識和OpenGL? Graphics with the X Window System?加深對Linux和Linux下OpenGL編程的理解,非常有用方庭,強(qiáng)烈推薦窗宦!
參考資料
XCB: XCB Core API
XCB-WikiPedia
從零開始手敲次世代游戲引擎(九)
GLX-Wikipedia
有關(guān)純xcb的glx的討論
linux圖形界面編程基礎(chǔ)知識
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?