這篇是在X Window初始化OpenGL的方法,Windows的之後寫在另一篇。
因為目標是D3D和OpenGL的只看其中一篇就能看懂,一部分內容與D3D初始化那篇重覆。
網路上有些比較舊的教學用的是OpenGL 1或2,有些做法在OpenGL 3以後被列為deprecated,本系列介紹的是OpenGL 3.3,會全面使用新方法。
2021/04/16:修改deinitGL()的部分。
2022/05/23:建立視窗的方法改成先建FBConfig、取得visualID再建視窗。
OpenGL設計目標是跨平臺,畫布設好之後,在不同平臺上函式和常數名稱是相同的(除了swap buffer以外)。但初始化時「產生一個視窗,把這個視窗設成OpenGL的畫布」不可能跟平臺無關,需要一些平臺相關函式做這件事,X Window的稱為GLX,Windows的稱為WGL。
SDK安裝方法:
要安裝X Window和OpenGL的開發用套件,筆者寫這篇時用的Mint 19.3是這些名稱:libx11-dev、libgl1-mesa-dev,其他發行版就搜尋一下libx、mesa或libgl。
有幾個名稱類似的套件,要注意不是libegl、libgle或libgles。
使用OpenGL除了顯示晶片和驅動程式以外,作業系統API也要配合,Linux版的API是mesa這個團隊開發的,因此套件名稱叫做mesa。
在X Window初始化OpenGL簡單來說是:準備好Display*和Window,將Window設成OpenGL的畫布,且建立視窗的方法要改一下。
參考:Tutorial: OpenGL 3.0 Context Creation (GLX)
建立基本視窗的方法以前寫過一篇教學,以那篇為基礎來製作。
【程式】如何建一個視窗—X Window篇
跟上次的視窗程式不同的地方:
● 宣告了一個GLXContext變數,代表OpenGL系統的global狀態。
● 還有一個Colormap變數,建立視窗時會用到。
● 建視窗移到initGLAndCreateWindow()裡面,因為要先用GLX取得一些資訊才能建立視窗。
● XDestroyWindow()本來在ClientMessage裡面,移到最後面。
接收事件也有些不同。XNextEvent()會讓程式暫停,等收到訊息才繼續,「遊戲程式基本架構」有提到,遊戲程式即使玩家沒在操作時也有東西在跑,不能在這裡停下來,所以先用XPending()檢查有沒有事件,如果有才呼叫XNextEvent()取出事件。
由於break不能一次跳出一層switch和兩層while,用一個做記號的變數isEnd。
usleep()是讓程式暫停一段時間,單位為微秒(microsecond,百萬分之一秒)。這裡16000大約是1000的1/60,即60FPS。
真正寫遊戲不會直接填16000,會先檢查遊戲邏輯和繪製畫面花掉多少時間,然後算出該usleep多久。本篇是入門教學,先不做這個處理。
接下來是本篇的重點:initGLAndCreateWindow()、nextFrame()、deinitGL()這三個函式。
有些教學寫的是早期的做法,使用glXChooseVisual()和glXCreateContext(),舊方法最高只能建立OpenGL 3.0的context,本篇介紹的是新方法。
3D繪圖裡作為畫布的點陣圖稱為framebuffer,畫面本身也是一張framebuffer,首先設定我們要的framebuffer格式,除了一般點陣圖有的色彩格式,還有3D繪圖專用的屬性如multisample、Z buffer,以及一些跟X Window有關的屬性。
設定方法不是填struct或函式參數,而是一個整數陣列,內容是鍵和值交替,這裡GLX_X_RENDERABLE和GLX_DOUBLEBUFFER等等的是glx.h裡定義的常數。
C語言把陣列傳入函式的話函式內收到的只有一個指標,不知道陣列的長度,因此最後填一個None(=0)代表結束。
「GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE,8,」是色彩格式,RGBA各8 bit。
GLX_X_RENDERABLE、GLX_DOUBLEBUFFER如同字面意思,framebuffer設定也可以設成不可繪圖、沒有double buffer,要寫清楚我們要的是什麼。GLX_X_VISUAL_TYPE跟X Window有關,除了true color以外還有direct color,筆者也不太清楚是什麼。
其他的有些不用設,有些用預設值。程式裡沒寫到的其中兩個:
GLX_DRAWABLE_TYPE:畫在視窗還是pixmap(一種X Window的物件),本篇用預設值GLX_WINDOW_BIT。常數名稱的_BIT代表這是bit flag。
GLX_RENDER_TYPE:RGBA還是索引色,本篇用預設值RGBA。(索引色現在應該沒人在用了)
所有可以設的值和解說在這裡(除了multisample沒列上去,因為這是OpenGL 2.1的文件,multisample是之後的版本追加的)
Khronos Group glXChooseFBConfig()的說明
把陣列傳給glXChooseFBConfig(),傳回符合要求的FBConfig,通常不止一個,傳回的是陣列。FBConfig是什麼在下面「glxinfo」的部分解說。(函式名稱的FB=framebuffer)
之後印出fbConfig編號非必要,是為了跟glxinfo對照。
再來指定我們要的OpenGL版本,一樣用None結束的陣列。GLX_CONTEXT_PROFILE_MASK_ARB設定成core profile,這個模式禁用deprecated的函式,相對的是compatibility profile,可以使用舊的做法。然後用glXCreateContextAttribsARB()產生GLXContext。
下一步取出fbConfig對應的visual ID並用它來建視窗。visual ID也是用來設定framebuffer格式,這是什麼在最後「glxinfo」的部分解說。
這時要讓三個物件的framebuffer格式配合:OpenGL、視窗、螢幕。建立視窗時要把格式設成跟OpenGL一樣,否則程式可能掛掉。視窗的格式不一定跟螢幕的相同,還需要一種X Window物件:Colormap,用來在不同格式間轉換,此物件必須一直留著,等視窗刪除時再刪除。
建視窗要用另一個函式XCreateWindow(),這可以指定visual、Colormap和depth,之前用的XCreateSimpleWindow()這三項是取得親視窗的設定來用。
這邊用到的函式請自己查X Window的說明。
Xlib programming manual: function index
(經筆者測試,Intel和AMD晶片比較寬容,用XCreateSimpleWindow()建視窗而不設定visual也可以繼續執行;nVidia晶片比較嚴格,不做這一步程式容易在glXMakeCurrent()這一行掛掉)
用printf()印出螢幕與視窗的visual ID,之後用glxinfo可以查它們是什麼。
準備好context和Window後用glXMakeCurrent()將Window設成OpenGL的畫布,用XFree()釋放fbc和vInfo的記憶體空間。
最後取出擴充函式以及設vsync,這裡先關閉vsync,後面再介紹怎麼開啟。
每個frame執行一次這個函式。要是畫面一片黑我們也看不出是否成功初始化OpenGL,所以在這裡做一點事:讓畫面從黑漸變到紅。跟D3D一樣RGBA分量的範圍為0~1,此處每個frame把顏色改變1/60,也就是一秒循環一次。
glClear(GL_COLOR_BUFFER_BIT)是把畫面塗上單色。
「Direct3D與OpenGL的繪圖管線(下)」有提過,OpenGL常常是在內部設定一個全域變數,之後的函式讀取這個全域變數得知要操作的物件。這裡沒有明確指定要塗上哪個framebuffer,預設就是畫面。前一行glClearColor()也是把顏色存在一個全域變數,glClear()讀取這個變數得知顏色。
glXSwapBuffers()是「遊戲程式基本架構」提到的double buffer,繪圖指令都是先畫在back buffer,再呼叫這個函式把back buffer複製到畫面上。
程式結束時把物件刪除。OpenGL on X Window有一個地方要注意:如果視窗被刪除後呼叫glXSwapBuffers(),程式會掛掉且命令列輸出錯誤訊息,而照main()的寫法,處理完ClientMessage訊息之後還會執行一次nextFrame()才跳出迴圈,所以把XDestroyWindow()移到程式最後面。
(Direct3D和OpenGL on Windows沒這個問題,刪除視窗後呼叫Present()或SwapBuffers()頂多傳回一個值代表失敗,不會讓程式掛掉)
現在的作業系統資源管理很完善,即使你偷懶沒刪除物件,程式結束時作業系統也會把程式使用的資源釋放,不會殘留在系統裡。但順便介紹一下刪除的方法。
程式寫好,假設檔名是simplegl.c,用這個指令build
使用IDE的話可能要設定library。
要用命令列執行才看得到printf輸出的資訊。執行的樣子
這個例子有18個FBConfig符合條件,第一個的編號是0xb3(十六進位);visual ID是0x21和0x109,編號代表什麼意思後述。
- 暫停的方法 -
Linux的暫停有很多方法
poll()和select()本來的用途是暫停並偵測file descriptor,等到file descriptor發生變化(例如有新資料輸入)才繼續,如果file descriptor填NULL或0,就變成單純暫停一段時間。
參數是奈秒不一定真的能計時到奈秒,實際要看硬體性能,可能會呼叫一次至少都停100微秒。
在「檔案操作—Linux篇」講到有個linux.die.net的網站,筆者常在那裡查Linux的系統呼叫,這5個函式的說明如下。
https://linux.die.net/man/3/sleep
https://linux.die.net/man/3/usleep
https://linux.die.net/man/2/nanosleep
https://linux.die.net/man/2/poll
https://linux.die.net/man/2/select
- Vsync -
「遊戲程式基本架構」講到的Vsync,現在很初學的階段就可以玩玩看了,不過OpenGL的vsync有一些細節要注意。
X Window設定vsync有三個函式,interval填1是啟用vsync。
哪個函式有效在每個晶片不一樣,跟版本新舊可能也有關。筆者用手邊的電腦測試,Intel和AMD晶片只有MESA的函式可以用來開關,SGI可以開但不能關(填1才有效,填0無效),EXT完全無效;在nVidia晶片試只有SGI和EXT有效。看起來能cover三家晶片的方法是glXSwapIntervalMESA()和glXSwapIntervalSGI()都呼叫,但不知道有沒有哪個顯卡是只有EXT有效,徵求更多測試案例。
另一個要注意的是,OpenGL可以調整系統設定強制開或關vsync,讓函式呼叫無效(D3D11不能),下面是Intel晶片的方法,雖然是ArchLinux的文件但其他發行版也適用。
Intel graphics - Disable Vertical Synchronization (VSYNC)
如果安裝AMD或nVidia的專有driver,它會附一個GUI工具可調整設定。
沒特別設定的話,預設是開還是關就很難說了。
在本篇的程式把usleep()註解掉,且glXSwapIntervalMESA()和glXSwapIntervalSGI()填1啟用vsync試試看,正常的話程式仍然以穩定速率進行,如果跑得異常快那表示vsync是強制關閉。
此外筆者有遇過一臺電腦,swap interval填1程式就跑得特別慢,只能用計時器定時,可能是驅動程式有問題。
由於無法預料玩家的電腦是什麼情況,寫OpenGL程式最好做到以下事項
● 明確呼叫glXSwapIntervalMESA()與glXSwapIntervalSGI()設定vsync。
● 遊戲中的Option讓玩家可以調整vsync。
● 偵測目前是否有開vsync,再看情況處理。
至於如何偵測,OpenGL沒有直接的方法 (雖然有個函式glXGetSwapIntervalMESA(),它傳回的是上次填入glXSwapIntervalMESA()的值,不能得知系統設定),筆者用的是很土的方法:呼叫glXSwapBuffers() 30次,看是不是經過0.5秒。Linux取得經過時間的函式是clock_gettime(),以後再介紹,有興趣的話先自己查。
- OpenGL的版本與擴充函式 -
這是OpenGL比較麻煩的地方,但處理程式、作業系統版本、晶片之間的相容性問題必須知道這些。
除了最早的版本就有的功能以外,各版本追加的功能是這樣加進去的:
(我有找過網路上有什麼地方把哪一版有什麼擴充整理出來,沒找到,只有找到這個軟體)
如果某個晶片標示「支援OpenGL 3.3」,那表示一定有texture_swizzle、sampler_objects,以及3.2以前的功能,4.0以後的功能不一定有。
如果標示支援3.3卻沒有sampler objects的功能……,可以跟廠商反映標示不實。
查閱OpenGL wiki會常看到這種標示。
主要看的是Core since version,表示Separate attribute format的功能是在OpenGL 4.3被採納。
Core in version是有支援的最新版本,基本上一定是4.6,因為4.6是OpenGL最終版本,之後就用Vulkan取代。
擴充的規格會寫成純文字檔,在OpenGL wiki如上圖的標示點選連結可以看到。
ARB_vertex_attrib_binding的說明
由於一部分是給開發API和顯卡的團隊看的,內容很多,寫程式時主要看的是新增了哪些函式和常數。
麻煩的地方是,實際有哪些擴充各晶片和各平臺會不一樣,相對地D3D比較標準化,只要是相同feature level所有晶片的功能都一樣。
第一個要注意的是比標示的版本新的功能,以及「OpenGL團隊決定不採納,停留在1和2的階段」的擴充,由於不保證所有廠商都支援,儘量不要使用以免在別人的電腦不能執行。假如作者設定程式最低支援OpenGL 3.3,那4.0以後的功能即使自己的電腦有支援也儘量別用。
但有些沒加入標準的擴充很實用,且市面上存在的廠商都已經支援,要不要用這類功能要考慮一下。
anisotropic filtering(各向異性過濾) 4.6版才加入標準;以及texture_compression_s3tc因為演算法不是自由軟體版權而沒有加入標準,但是OpenGL 3.0以後的I牌、A牌和N牌普遍都有支援,這兩個功能算是比較安全,OpenGL wiki也有特別介紹。(D3D9有這兩個功能,只要驅動程式沒有偷工減料,同一個晶片執行OpenGL程式照理說也做得到)
其他功能就不一定,例如vertex_attrib_binding和separate_shader_objects筆者認為實用,分別在4.3和4.1加入標準,筆者調查過一些晶片的支援度,OpenGL 3.3以後的I、A、N三牌都有支援,但不知道有沒有少數例外剛好筆者沒遇到,要不要用這兩個功能還在考慮。
第二個是「4.作業系統API的作者決定把哪些函式加入」。
這張圖再貼一次,一個功能即使晶片做得到,驅動程式和作業系統API沒有配合製作的話,也不能使用。
有加入API的函式不需特別處理就能使用;沒加入API但晶片和驅動程式實際有功能的不是不能用,只是要手動取出函式指標,取出的方法是上面程式裡的glXGetProcAddress()。
本篇用glXSwapIntervalMESA()和glXSwapIntervalSGI()示範這個特性。glxext.h裡有這樣的宣告:
使用這兩個函式有兩種方法
即使header有原型宣告library也不一定有定義,library沒有的函式編譯時不會跳語法錯誤,但連結和執行會跳「找不到某某函式」的錯誤,此時只能用glXGetProcAddress()的方法。
library有定義的函式兩種方法都可以用,全部都用glXGetProcAddress()最為保險,代價是程式會變得冗長。
如果glXGetProcAddress()傳回NULL表示晶片完全沒有此功能。
library有哪些函式mesa各個版本不一樣,越新的版本當然越多。例如本篇的程式,筆者測試時如果直接打函式名稱,Mint 19.3連結時會找不到glXSwapIntervalMESA(),但Fedora 33(內建軟體比Mint 19.3新)兩個函式都可以直接用,所以至少glXSwapIntervalMESA()要用取函式指標的做法,才能在這兩個發行版執行。
以前做Cyber Sprite一代的時候mesa只支援到OpenGL 1.4,而我設定最低支援OpenGL 2.0的晶片,那時需要大量使用glXGetProcAddress(),現在mesa增加很多新函式,glXGetProcAddress()可以少呼叫很多次。
綜合以上,要選定一個較舊的Linux發行版做為最低支援版本,在這個發行版開發,新版build的程式在舊版不一定能執行(不只OpenGL,寫所有Linux程式都是如此);並且要蒐集一下OpenGL各版本的資訊,與各家晶片的OpenGL支援度。
- glxinfo -
在X Window查看OpenGL資訊的軟體,Mint是包含在這個套件:mesa-utils。
在命令列直接打glxinfo會列出很大量的資訊,用「glxinfo>a.txt」把資料輸出到文字檔比較好查,「glxinfo -h」可以查命令列參數的用法。這裡示範其中一些資訊。
打「glxinfo>a.txt -s -t」,然後打開a.txt,最上面有像這樣的內容,每臺電腦不會一樣
列出這臺電腦的GLX資訊,可看出GLX是1.4版,中間一堆GLX_ARB_、GLX_EXT_之類的東西是有支援的GLX擴充功能,各個功能是什麼有興趣就自己查吧。
本篇用到的vsync函式:glXSwapIntervalMESA()和glXSwapIntervalSGI(),其實是這兩個擴充功能:GLX_MESA_swap_control和GLX_SGI_swap_control。
再下面是這些
列出三個profile各自支援的OpenGL版本、shader版本、以及擴充功能。
第一個是core profile,有支援到OpenGL 3.3,這是此晶片最高支援的OpenGL版本。第二個是compatibility profile,支援到3.0。本篇程式裡有「GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,」這一行,是在選擇用哪一個profile。
第三個是OpenGL ES版本。OpenGL ES一般印象中是給行動裝置使用,不過這張圖再拿出來一次:
作業系統API的部分mesa有寫好OpenGL和OpenGL ES的函式庫,右邊接到相同的驅動程式和晶片,所以Linux桌機版也可以使用OpenGL ES。
中間一堆GL_AMD_、GL_ARB_、GL_EXT_等等的是擴充功能,就是晶片實際有的功能,本篇「OpenGL的版本與擴充函式」提到vertex_attrib_binding、texture_compression_s3tc這些,可以在這個列表裡找找看。
再來是GLX Visuals。
列出各種framebuffer格式:RGB bit數是565還是888、有沒有depth buffer和stencil buffer、有沒有accumulation buffer等等。(accumulation buffer是很早期的功能,現在已經有更好的方法替代)
最後是GLXFBConfigs的部分,可看到跟GLX Visuals類似的內容:
早期的glXChooseVisual()和glXCreateContext()使用GLX Visuals來設定畫面的framebuffer格式,3.0以後的方法改用GLXFBConfigs。
visual本來是X Windows的功能,GLX早期的版本也是跟據visual來設計。但後來增加一些OpenGL有但X Window沒有的功能,visual不夠用了,於是新增了FBConfig。
可以比對一下官方文件中兩個函式能用的attribList,glXChooseFBConfig()能設定的項目比較多。上面GLX Visuals的表雖然也有列出一些FBConfig專用的屬性,這些要先取得FBConfig再取得visual ID,不能用glXChooseVisual()設定。
glXChooseVisual()
glXChooseFBConfig()
「glxinfo -v」會印出比較詳細的欄位名稱,每個欄位是什麼意思請參照glXChooseFBConfig()的說明。
本篇的程式印出FBConfig編號是0xb3,以及visual ID是0x21和0x109,找到0x b3、0x 21、0x109這三列就能查到FBConfig和visual的格式。
因為目標是D3D和OpenGL的只看其中一篇就能看懂,一部分內容與D3D初始化那篇重覆。
網路上有些比較舊的教學用的是OpenGL 1或2,有些做法在OpenGL 3以後被列為deprecated,本系列介紹的是OpenGL 3.3,會全面使用新方法。
2021/04/16:修改deinitGL()的部分。
2022/05/23:建立視窗的方法改成先建FBConfig、取得visualID再建視窗。
OpenGL設計目標是跨平臺,畫布設好之後,在不同平臺上函式和常數名稱是相同的(除了swap buffer以外)。但初始化時「產生一個視窗,把這個視窗設成OpenGL的畫布」不可能跟平臺無關,需要一些平臺相關函式做這件事,X Window的稱為GLX,Windows的稱為WGL。
SDK安裝方法:
要安裝X Window和OpenGL的開發用套件,筆者寫這篇時用的Mint 19.3是這些名稱:libx11-dev、libgl1-mesa-dev,其他發行版就搜尋一下libx、mesa或libgl。
有幾個名稱類似的套件,要注意不是libegl、libgle或libgles。
使用OpenGL除了顯示晶片和驅動程式以外,作業系統API也要配合,Linux版的API是mesa這個團隊開發的,因此套件名稱叫做mesa。
在X Window初始化OpenGL簡單來說是:準備好Display*和Window,將Window設成OpenGL的畫布,且建立視窗的方法要改一下。
參考:Tutorial: OpenGL 3.0 Context Creation (GLX)
建立基本視窗的方法以前寫過一篇教學,以那篇為基礎來製作。
【程式】如何建一個視窗—X Window篇
#define GL_GLEXT_PROTOTYPES #define GLX_GLXEXT_PROTOTYPES #include<GL/gl.h>//間接引用GL/glext.h #include<GL/glx.h> //間接引用GL/glxext.h和X11/Xlib.h #include<stdio.h> #include<unistd.h> //使用usleep() const int WINDOW_W=200, WINDOW_H=200; //OpenGL必要物件 Display* dsp; Window window; Colormap cmap; GLXContext context; //畫面顏色 float color[]={0,0,0,1}; //這三個函式在下面說明 static Window initGLAndCreateWindow(); static void nextFrame(); static void deinitGL(); int main(){ dsp = XOpenDisplay(NULL); window=initGLAndCreateWindow(); if(!window){ printf("Can not initialize OpenGL\n"); return 0; } //設標題 XStoreName(dsp, window, "title"); //設定事件mask Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", False); XSetWMProtocols(dsp, window, &wmDelete, 1); XMapWindow( dsp, window ); //接收事件 XEvent evt; int isEnd=0; while(!isEnd){ while(XPending(dsp)){ XNextEvent(dsp, &evt); switch(evt.type){ case ClientMessage: if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; } break; } } //正式寫遊戲時,遊戲邏輯放在此處 nextFrame(); usleep(16000); } deinitGL(); XDestroyWindow( dsp, window ); XFreeColormap(dsp, cmap); XFlush(dsp); XCloseDisplay( dsp ); return 0; } |
跟上次的視窗程式不同的地方:
● 宣告了一個GLXContext變數,代表OpenGL系統的global狀態。
● 還有一個Colormap變數,建立視窗時會用到。
● 建視窗移到initGLAndCreateWindow()裡面,因為要先用GLX取得一些資訊才能建立視窗。
● XDestroyWindow()本來在ClientMessage裡面,移到最後面。
接收事件也有些不同。XNextEvent()會讓程式暫停,等收到訊息才繼續,「遊戲程式基本架構」有提到,遊戲程式即使玩家沒在操作時也有東西在跑,不能在這裡停下來,所以先用XPending()檢查有沒有事件,如果有才呼叫XNextEvent()取出事件。
由於break不能一次跳出一層switch和兩層while,用一個做記號的變數isEnd。
usleep()是讓程式暫停一段時間,單位為微秒(microsecond,百萬分之一秒)。這裡16000大約是1000的1/60,即60FPS。
真正寫遊戲不會直接填16000,會先檢查遊戲邏輯和繪製畫面花掉多少時間,然後算出該usleep多久。本篇是入門教學,先不做這個處理。
接下來是本篇的重點:initGLAndCreateWindow()、nextFrame()、deinitGL()這三個函式。
//傳回0代表成功,非0代表失敗 static Window initGLAndCreateWindow(){ const int fbConfigAttr[]={ GLX_X_RENDERABLE, True, GLX_DOUBLEBUFFER, True, GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE,8, None }; int fbCount; GLXFBConfig* fbc=glXChooseFBConfig(dsp, DefaultScreen(dsp),fbConfigAttr,&fbCount); if(fbc==NULL){ return 0; } //印出fbConfig編號 int fbConfigID; glXGetFBConfigAttrib(dsp,fbc[0], GLX_FBCONFIG_ID, &fbConfigID); printf("fbconfig %x count %d\n", fbConfigID, fbCount); //產生OpenGL 3.3 context const int contextAttribs[] = { GLX_CONTEXT_MAJOR_VERSION_ARB, 3, GLX_CONTEXT_MINOR_VERSION_ARB, 3, GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, None, }; context = glXCreateContextAttribsARB(dsp, fbc[0], 0,True, contextAttribs ); if(context==0){ XFree(fbc); return 0; } //取得這個fbConfig對應的visual,用它來建立視窗 XVisualInfo* vInfo=glXGetVisualFromFBConfig(dsp, fbc[0]); XSetWindowAttributes windowAttr; cmap = XCreateColormap(dsp, DefaultRootWindow(dsp), vInfo->visual,AllocNone); windowAttr.colormap = cmap; windowAttr.background_pixel = 0; windowAttr.border_pixel = 0; Window window = XCreateWindow(dsp, DefaultRootWindow(dsp), 0,0,WINDOW_W,WINDOW_H, 0, vInfo->depth, InputOutput, vInfo->visual, CWBackPixel|CWBorderPixel|CWColormap, &windowAttr); //印出visual編號 int defaultVisualID=XVisualIDFromVisual(DefaultVisual(dsp, DefaultScreen(dsp))); int windowVisualID=vInfo->visualid; printf("defaultVisual:%x windowVisual:%x\n", defaultVisualID, windowVisualID); XFree(vInfo); XFree(fbc); glXMakeCurrent(dsp, window, context); //設定Vsync PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA= (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress("glXSwapIntervalMESA"); if(glXSwapIntervalMESA){ glXSwapIntervalMESA(0); } PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI= (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddress("glXSwapIntervalSGI"); if(glXSwapIntervalSGI){ glXSwapIntervalSGI(0); } return window; } |
有些教學寫的是早期的做法,使用glXChooseVisual()和glXCreateContext(),舊方法最高只能建立OpenGL 3.0的context,本篇介紹的是新方法。
3D繪圖裡作為畫布的點陣圖稱為framebuffer,畫面本身也是一張framebuffer,首先設定我們要的framebuffer格式,除了一般點陣圖有的色彩格式,還有3D繪圖專用的屬性如multisample、Z buffer,以及一些跟X Window有關的屬性。
設定方法不是填struct或函式參數,而是一個整數陣列,內容是鍵和值交替,這裡GLX_X_RENDERABLE和GLX_DOUBLEBUFFER等等的是glx.h裡定義的常數。
C語言把陣列傳入函式的話函式內收到的只有一個指標,不知道陣列的長度,因此最後填一個None(=0)代表結束。
「GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE,8,」是色彩格式,RGBA各8 bit。
GLX_X_RENDERABLE、GLX_DOUBLEBUFFER如同字面意思,framebuffer設定也可以設成不可繪圖、沒有double buffer,要寫清楚我們要的是什麼。GLX_X_VISUAL_TYPE跟X Window有關,除了true color以外還有direct color,筆者也不太清楚是什麼。
其他的有些不用設,有些用預設值。程式裡沒寫到的其中兩個:
GLX_DRAWABLE_TYPE:畫在視窗還是pixmap(一種X Window的物件),本篇用預設值GLX_WINDOW_BIT。常數名稱的_BIT代表這是bit flag。
GLX_RENDER_TYPE:RGBA還是索引色,本篇用預設值RGBA。(索引色現在應該沒人在用了)
所有可以設的值和解說在這裡(除了multisample沒列上去,因為這是OpenGL 2.1的文件,multisample是之後的版本追加的)
Khronos Group glXChooseFBConfig()的說明
把陣列傳給glXChooseFBConfig(),傳回符合要求的FBConfig,通常不止一個,傳回的是陣列。FBConfig是什麼在下面「glxinfo」的部分解說。(函式名稱的FB=framebuffer)
之後印出fbConfig編號非必要,是為了跟glxinfo對照。
再來指定我們要的OpenGL版本,一樣用None結束的陣列。GLX_CONTEXT_PROFILE_MASK_ARB設定成core profile,這個模式禁用deprecated的函式,相對的是compatibility profile,可以使用舊的做法。然後用glXCreateContextAttribsARB()產生GLXContext。
下一步取出fbConfig對應的visual ID並用它來建視窗。visual ID也是用來設定framebuffer格式,這是什麼在最後「glxinfo」的部分解說。
這時要讓三個物件的framebuffer格式配合:OpenGL、視窗、螢幕。建立視窗時要把格式設成跟OpenGL一樣,否則程式可能掛掉。視窗的格式不一定跟螢幕的相同,還需要一種X Window物件:Colormap,用來在不同格式間轉換,此物件必須一直留著,等視窗刪除時再刪除。
建視窗要用另一個函式XCreateWindow(),這可以指定visual、Colormap和depth,之前用的XCreateSimpleWindow()這三項是取得親視窗的設定來用。
這邊用到的函式請自己查X Window的說明。
Xlib programming manual: function index
(經筆者測試,Intel和AMD晶片比較寬容,用XCreateSimpleWindow()建視窗而不設定visual也可以繼續執行;nVidia晶片比較嚴格,不做這一步程式容易在glXMakeCurrent()這一行掛掉)
用printf()印出螢幕與視窗的visual ID,之後用glxinfo可以查它們是什麼。
準備好context和Window後用glXMakeCurrent()將Window設成OpenGL的畫布,用XFree()釋放fbc和vInfo的記憶體空間。
最後取出擴充函式以及設vsync,這裡先關閉vsync,後面再介紹怎麼開啟。
static void nextFrame(){ color[0]+=1.0/60; //修改顏色的R分量 if(color[0]>=1){ color[0]=0; } glClearColor(color[0],color[1],color[2],color[3]); glClear(GL_COLOR_BUFFER_BIT); glXSwapBuffers(dsp, window); } |
glClear(GL_COLOR_BUFFER_BIT)是把畫面塗上單色。
「Direct3D與OpenGL的繪圖管線(下)」有提過,OpenGL常常是在內部設定一個全域變數,之後的函式讀取這個全域變數得知要操作的物件。這裡沒有明確指定要塗上哪個framebuffer,預設就是畫面。前一行glClearColor()也是把顏色存在一個全域變數,glClear()讀取這個變數得知顏色。
glXSwapBuffers()是「遊戲程式基本架構」提到的double buffer,繪圖指令都是先畫在back buffer,再呼叫這個函式把back buffer複製到畫面上。
static void deinitGL(){ glXDestroyContext(dsp, context); } |
(Direct3D和OpenGL on Windows沒這個問題,刪除視窗後呼叫Present()或SwapBuffers()頂多傳回一個值代表失敗,不會讓程式掛掉)
現在的作業系統資源管理很完善,即使你偷懶沒刪除物件,程式結束時作業系統也會把程式使用的資源釋放,不會殘留在系統裡。但順便介紹一下刪除的方法。
程式寫好,假設檔名是simplegl.c,用這個指令build
gcc simplegl.c -o simplegl -s -Os -lX11 -lGL |
要用命令列執行才看得到printf輸出的資訊。執行的樣子
這個例子有18個FBConfig符合條件,第一個的編號是0xb3(十六進位);visual ID是0x21和0x109,編號代表什麼意思後述。
- 暫停的方法 -
Linux的暫停有很多方法
#include<unistd.h> unsigned int sleep(unsigned int seconds); //單位為秒 int usleep(useconds_t usec); //單位為微秒(10^-6秒) #include<time.h> int nanosleep(const struct timespec* req, NULL); //單位為奈秒(10^-9秒) //用這個struct指定時間 struct timespec { long tv_sec; long tv_nsec; }; #include<poll.h> int poll(NULL, 0, int timeout); //單位為毫秒(1/1000秒) #include<select.h> int select(0,NULL,NULL,NULL, struct timeval* timeout); //單位為微秒 //用這個struct指定時間 struct timeval { long tv_sec; long tv_usec; }; |
poll()和select()本來的用途是暫停並偵測file descriptor,等到file descriptor發生變化(例如有新資料輸入)才繼續,如果file descriptor填NULL或0,就變成單純暫停一段時間。
參數是奈秒不一定真的能計時到奈秒,實際要看硬體性能,可能會呼叫一次至少都停100微秒。
在「檔案操作—Linux篇」講到有個linux.die.net的網站,筆者常在那裡查Linux的系統呼叫,這5個函式的說明如下。
https://linux.die.net/man/3/sleep
https://linux.die.net/man/3/usleep
https://linux.die.net/man/2/nanosleep
https://linux.die.net/man/2/poll
https://linux.die.net/man/2/select
- Vsync -
「遊戲程式基本架構」講到的Vsync,現在很初學的階段就可以玩玩看了,不過OpenGL的vsync有一些細節要注意。
X Window設定vsync有三個函式,interval填1是啟用vsync。
int glXSwapIntervalMESA(unsigned int interval); int glXSwapIntervalSGI(int interval); void glXSwapIntervalEXT(Display* dpy, GLXDrawable drawable, int interval); //drawable填window,或用glXGetCurrentDrawable()取得 |
另一個要注意的是,OpenGL可以調整系統設定強制開或關vsync,讓函式呼叫無效(D3D11不能),下面是Intel晶片的方法,雖然是ArchLinux的文件但其他發行版也適用。
Intel graphics - Disable Vertical Synchronization (VSYNC)
如果安裝AMD或nVidia的專有driver,它會附一個GUI工具可調整設定。
沒特別設定的話,預設是開還是關就很難說了。
在本篇的程式把usleep()註解掉,且glXSwapIntervalMESA()和glXSwapIntervalSGI()填1啟用vsync試試看,正常的話程式仍然以穩定速率進行,如果跑得異常快那表示vsync是強制關閉。
此外筆者有遇過一臺電腦,swap interval填1程式就跑得特別慢,只能用計時器定時,可能是驅動程式有問題。
由於無法預料玩家的電腦是什麼情況,寫OpenGL程式最好做到以下事項
● 明確呼叫glXSwapIntervalMESA()與glXSwapIntervalSGI()設定vsync。
● 遊戲中的Option讓玩家可以調整vsync。
● 偵測目前是否有開vsync,再看情況處理。
至於如何偵測,OpenGL沒有直接的方法 (雖然有個函式glXGetSwapIntervalMESA(),它傳回的是上次填入glXSwapIntervalMESA()的值,不能得知系統設定),筆者用的是很土的方法:呼叫glXSwapBuffers() 30次,看是不是經過0.5秒。Linux取得經過時間的函式是clock_gettime(),以後再介紹,有興趣的話先自己查。
- OpenGL的版本與擴充函式 -
這是OpenGL比較麻煩的地方,但處理程式、作業系統版本、晶片之間的相容性問題必須知道這些。
除了最早的版本就有的功能以外,各版本追加的功能是這樣加進去的:
- 顯卡廠商在驅動程式裡加入新函式或新參數,並為功能取一個名稱。擴充功能的命名有一定規則,此階段字頭會帶有廠商名稱,如S3、NV或AMD。
也有可能先不公開,先跟其他廠商討論,直接進到2或3的階段再做出產品。 - 廠商們討論OpenGL規格時覺得這個功能好用,其他廠商也可以跟進,於是把它定為通用規格,字頭改成EXT或ARB。
- OpenGL出新版的時候,定OpenGL標準的團隊認為這個擴充適合加進去,就把它採納成為標準,規定有實作這個功能才能在產品標上「支援OpenGL某某版」。
也有可能OpenGL團隊決定不採納,擴充就停留在1和2的階段。 - 作業系統API作者決定要支援到OpenGL哪一版,把函式和參數加進去。
(我有找過網路上有什麼地方把哪一版有什麼擴充整理出來,沒找到,只有找到這個軟體)
如果某個晶片標示「支援OpenGL 3.3」,那表示一定有texture_swizzle、sampler_objects,以及3.2以前的功能,4.0以後的功能不一定有。
如果標示支援3.3卻沒有sampler objects的功能……,可以跟廠商反映標示不實。
查閱OpenGL wiki會常看到這種標示。
主要看的是Core since version,表示Separate attribute format的功能是在OpenGL 4.3被採納。
Core in version是有支援的最新版本,基本上一定是4.6,因為4.6是OpenGL最終版本,之後就用Vulkan取代。
擴充的規格會寫成純文字檔,在OpenGL wiki如上圖的標示點選連結可以看到。
ARB_vertex_attrib_binding的說明
由於一部分是給開發API和顯卡的團隊看的,內容很多,寫程式時主要看的是新增了哪些函式和常數。
麻煩的地方是,實際有哪些擴充各晶片和各平臺會不一樣,相對地D3D比較標準化,只要是相同feature level所有晶片的功能都一樣。
第一個要注意的是比標示的版本新的功能,以及「OpenGL團隊決定不採納,停留在1和2的階段」的擴充,由於不保證所有廠商都支援,儘量不要使用以免在別人的電腦不能執行。假如作者設定程式最低支援OpenGL 3.3,那4.0以後的功能即使自己的電腦有支援也儘量別用。
但有些沒加入標準的擴充很實用,且市面上存在的廠商都已經支援,要不要用這類功能要考慮一下。
anisotropic filtering(各向異性過濾) 4.6版才加入標準;以及texture_compression_s3tc因為演算法不是自由軟體版權而沒有加入標準,但是OpenGL 3.0以後的I牌、A牌和N牌普遍都有支援,這兩個功能算是比較安全,OpenGL wiki也有特別介紹。(D3D9有這兩個功能,只要驅動程式沒有偷工減料,同一個晶片執行OpenGL程式照理說也做得到)
其他功能就不一定,例如vertex_attrib_binding和separate_shader_objects筆者認為實用,分別在4.3和4.1加入標準,筆者調查過一些晶片的支援度,OpenGL 3.3以後的I、A、N三牌都有支援,但不知道有沒有少數例外剛好筆者沒遇到,要不要用這兩個功能還在考慮。
第二個是「4.作業系統API的作者決定把哪些函式加入」。
這張圖再貼一次,一個功能即使晶片做得到,驅動程式和作業系統API沒有配合製作的話,也不能使用。
有加入API的函式不需特別處理就能使用;沒加入API但晶片和驅動程式實際有功能的不是不能用,只是要手動取出函式指標,取出的方法是上面程式裡的glXGetProcAddress()。
本篇用glXSwapIntervalMESA()和glXSwapIntervalSGI()示範這個特性。glxext.h裡有這樣的宣告:
//函式指標定義 typedef int ( *PFNGLXSWAPINTERVALMESAPROC) (unsigned int interval); typedef int ( *PFNGLXSWAPINTERVALSGIPROC) (int interval); //直接宣告函式 #ifdef GLX_GLXEXT_PROTOTYPES int glXSwapIntervalMESA (unsigned int interval); int glXSwapIntervalSGI (int interval); #endif |
使用這兩個函式有兩種方法
//1.直接打函式名稱 //程式裡要打#define GLX_GLXEXT_PROTOTYPES才能使用 //build時會在libGL.so尋找函式定義 glXSwapIntervalMESA(0); glXSwapIntervalSGI(0); //2.先取得函式指標再使用 //寫法如下,填一個字串參數,再把傳回值轉型 //此時libGL.so不需要有定義 PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress("glXSwapIntervalMESA"); if(glXSwapIntervalMESA){ //檢查一下函式指標是不是NULL避免程式掛掉 glXSwapIntervalMESA(0); } PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddress("glXSwapIntervalSGI"); if(glXSwapIntervalSGI){ glXSwapIntervalSGI(0); } |
library有定義的函式兩種方法都可以用,全部都用glXGetProcAddress()最為保險,代價是程式會變得冗長。
如果glXGetProcAddress()傳回NULL表示晶片完全沒有此功能。
library有哪些函式mesa各個版本不一樣,越新的版本當然越多。例如本篇的程式,筆者測試時如果直接打函式名稱,Mint 19.3連結時會找不到glXSwapIntervalMESA(),但Fedora 33(內建軟體比Mint 19.3新)兩個函式都可以直接用,所以至少glXSwapIntervalMESA()要用取函式指標的做法,才能在這兩個發行版執行。
以前做Cyber Sprite一代的時候mesa只支援到OpenGL 1.4,而我設定最低支援OpenGL 2.0的晶片,那時需要大量使用glXGetProcAddress(),現在mesa增加很多新函式,glXGetProcAddress()可以少呼叫很多次。
綜合以上,要選定一個較舊的Linux發行版做為最低支援版本,在這個發行版開發,新版build的程式在舊版不一定能執行(不只OpenGL,寫所有Linux程式都是如此);並且要蒐集一下OpenGL各版本的資訊,與各家晶片的OpenGL支援度。
- glxinfo -
在X Window查看OpenGL資訊的軟體,Mint是包含在這個套件:mesa-utils。
在命令列直接打glxinfo會列出很大量的資訊,用「glxinfo>a.txt」把資料輸出到文字檔比較好查,「glxinfo -h」可以查命令列參數的用法。這裡示範其中一些資訊。
打「glxinfo>a.txt -s -t」,然後打開a.txt,最上面有像這樣的內容,每臺電腦不會一樣
server glx vendor string: SGI server glx version string: 1.4 server glx extensions: GLX_ARB_create_context GLX_ARB_create_context_no_error GLX_ARB_create_context_profile …… client glx vendor string: Mesa Project and SGI client glx version string: 1.4 …… GLX version: 1.4 …… |
本篇用到的vsync函式:glXSwapIntervalMESA()和glXSwapIntervalSGI(),其實是這兩個擴充功能:GLX_MESA_swap_control和GLX_SGI_swap_control。
再下面是這些
OpenGL vendor string: Intel Open Source Technology Center OpenGL renderer string: Mesa DRI Intel(R) Sandybridge Mobile OpenGL core profile version string: 3.3 (Core Profile) Mesa 19.0.8 OpenGL core profile shading language version string: 3.30 OpenGL core profile context flags: (none) OpenGL core profile profile mask: core profile OpenGL core profile extensions: GL_3DFX_texture_compression_FXT1 GL_AMD_draw_buffers_blend GL_AMD_seamless_cubemap_per_texture …… OpenGL version string: 3.0 Mesa 19.0.8 OpenGL shading language version string: 1.30 OpenGL context flags: (none) OpenGL extensions: …… OpenGL ES profile version string: OpenGL ES 3.0 Mesa 19.0.8 OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.00 OpenGL ES profile extensions: …… |
第一個是core profile,有支援到OpenGL 3.3,這是此晶片最高支援的OpenGL版本。第二個是compatibility profile,支援到3.0。本篇程式裡有「GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,」這一行,是在選擇用哪一個profile。
第三個是OpenGL ES版本。OpenGL ES一般印象中是給行動裝置使用,不過這張圖再拿出來一次:
作業系統API的部分mesa有寫好OpenGL和OpenGL ES的函式庫,右邊接到相同的驅動程式和晶片,所以Linux桌機版也可以使用OpenGL ES。
中間一堆GL_AMD_、GL_ARB_、GL_EXT_等等的是擴充功能,就是晶片實際有的功能,本篇「OpenGL的版本與擴充函式」提到vertex_attrib_binding、texture_compression_s3tc這些,可以在這個列表裡找找看。
再來是GLX Visuals。
74 GLX Visuals Vis Vis Visual Trans buff lev render DB ste r g b a s aux dep ste accum buffer MS MS ID Depth Type parent size el type reo sz sz sz sz flt rgb buf th ncl r g b a num bufs caveats -------------------------------------------------------------------------------------------------------------------- 0x 21 24 TrueColor 0 32 0 rgba 1 0 8 8 8 8 . . 0 24 8 0 0 0 0 0 0 None 0x 22 24 DirectColor 0 32 0 rgba 1 0 8 8 8 8 . . 0 24 8 0 0 0 0 0 0 None 0x109 24 TrueColor 0 32 0 rgba 1 0 8 8 8 8 . . 0 0 0 0 0 0 0 0 0 None 0x10a 24 TrueColor 0 32 0 rgba 0 0 8 8 8 8 . . 0 0 0 0 0 0 0 0 0 None 0x10b 24 TrueColor 0 32 0 rgba 0 0 8 8 8 8 . . 0 24 8 0 0 0 0 0 0 None 0x10c 24 TrueColor 0 24 0 rgba 1 0 8 8 8 0 . . 0 0 0 0 0 0 0 0 0 None 0x10d 24 TrueColor 0 24 0 rgba 0 0 8 8 8 0 . . 0 0 0 0 0 0 0 0 0 None …… |
最後是GLXFBConfigs的部分,可看到跟GLX Visuals類似的內容:
90 GLXFBConfigs: Vis Vis Visual Trans buff lev render DB ste r g b a s aux dep ste accum buffer MS MS ID Depth Type parent size el type reo sz sz sz sz flt rgb buf th ncl r g b a num bufs caveats -------------------------------------------------------------------------------------------------------------------- 0x af 0 TrueColor 0 16 0 rgba 1 0 5 6 5 0 . . 0 0 0 0 0 0 0 0 0 None 0x b0 0 TrueColor 0 16 0 rgba 0 0 5 6 5 0 . . 0 0 0 0 0 0 0 0 0 None 0x b1 0 TrueColor 0 16 0 rgba 1 0 5 6 5 0 . . 0 24 8 0 0 0 0 0 0 None 0x b2 0 TrueColor 0 16 0 rgba 0 0 5 6 5 0 . . 0 24 8 0 0 0 0 0 0 None 0x b3 24 TrueColor 0 32 0 rgba 1 0 8 8 8 8 . . 0 0 0 0 0 0 0 0 0 None 0x b4 24 TrueColor 0 32 0 rgba 0 0 8 8 8 8 . . 0 0 0 0 0 0 0 0 0 None 0x b5 24 TrueColor 0 32 0 rgba 1 0 8 8 8 8 . . 0 24 8 0 0 0 0 0 0 None …… |
visual本來是X Windows的功能,GLX早期的版本也是跟據visual來設計。但後來增加一些OpenGL有但X Window沒有的功能,visual不夠用了,於是新增了FBConfig。
可以比對一下官方文件中兩個函式能用的attribList,glXChooseFBConfig()能設定的項目比較多。上面GLX Visuals的表雖然也有列出一些FBConfig專用的屬性,這些要先取得FBConfig再取得visual ID,不能用glXChooseVisual()設定。
glXChooseVisual()
glXChooseFBConfig()
「glxinfo -v」會印出比較詳細的欄位名稱,每個欄位是什麼意思請參照glXChooseFBConfig()的說明。
本篇的程式印出FBConfig編號是0xb3,以及visual ID是0x21和0x109,找到0x b3、0x 21、0x109這三列就能查到FBConfig和visual的格式。