由於目標是Direct3D和OpenGL的只看其中一篇就能看懂,有些內容和D3D篇重覆。
把「Direct3D與OpenGL的繪圖管線(上)」的圖拿出來看一下。
這次要做的是把左邊列的物件和設定值一樣一樣弄好,除了constant buffer、貼圖和sampler以外,本篇先不用到貼圖。
本篇重點是將頂點資料和shader傳給GPU。OpenGL 2.0的世代不一定要用到shader,而且有方法可以「上傳頂點資料→繪製」兩個工作同時做,但3.2以後的core profile就一定要寫vertex shader和fragment shader,且上傳頂點資料一定要照「建立buffer物件→上傳頂點資料→繪製」的步驟,所以初學階段就得學習怎麼做這兩件事。
本系列的OpenGL目標版本是3.3,一些早期的方法就不介紹了,也不使用4.0以後新增的功能。
因為本篇新介紹的東西很多,在此把shader和vertex array object的一些細節略過,先寫個能用的程式再說,之後教到貼圖和shader輸入輸出時再介紹。
之前的教學可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
大部分和「OpenGL 3.3初始化(X Window)」相同,不同的地方是宣告了幾個uint32_t代表OpenGL的物件,以及計時器採用正式的做法了。
1. nanosleep()之後立刻呼叫clock_gettime()取得目前時間。
2. 做完處埋輸入、遊戲邏輯、繪圖後再取得一次目前時間,將兩者相減算出經過的時間。
3. 再算出應該暫停多久,如果經過時間<16ms才暫停。
Linux取得目前時間有多種方式,其中最符合本篇需求的是clock_gettime(),第一參數填CLOCK_MONOTONIC則取得的是電腦開機後到現在的時間,修改系統時間不影響這個計時器。
傳回值是這個struct:
由於傳回的單位是奈秒,暫停使用同樣是奈秒的nanosleep()。
其他可以計時到毫秒以下的函式,linux.die.net上面的說明:
clock_gettime(),第一參數填不同的值可取得不同的計時器。
clock(),這個process佔用的CPU時間,sleep經過的時間會忽略。
gettimeofday(),秒的部分傳回unix time stamp,會受修改系統時間影響。
還多了這部分。
寫一組最簡單的shader,vertex shader把坐標原封不動傳給rasterizer,pixel shader把像素塗成白色,shader的輸出入、語法等等的之後另寫一篇介紹。
第一行「#version 330」指定shader版本號,這行一定要有,否則會被視為最舊的1.1版編譯,很多功能都不能用。
GLSL版本與OpenGL版本的對應看這篇:OpenGL wiki: Core Language (GLSL)#Compilation_settings
GLSL的程式進入點必須是main(),不像HLSL可以自己指定函式,把兩個shader寫在同一個字串就會出現兩個main(),所以要分成兩個字串。
三個頂點坐標是(0, 0.8)、(0.8, 0)、(-0.5, -0.5),以前有講過D3D和OpenGL的畫面坐標是-1 ~ 1,可以預想畫出的三角形是這樣。
initGL()和之前大同小異。
之後的initSettings()是重點,不過看程式碼之前先了解一下OpenGL的「物件」是什麼。OpenGL不是用C/C++的struct或物件,而是用無號32位元整數代表它內部的物件,程式裡可以宣告成OpenGL自訂的GLuint,或C語言標準的unsigned int、uint32_t。
OpenGL官方文件把這些整數稱為object或name,我是習慣稱為ID(識別碼),以免跟C/C++的物件或字串混淆。
至於整數怎麼對應到驅動程式或顯示記憶體內的資料,OpenGL規格有要求晶片廠商自己想辦法,外面傳整數進來驅動程式要找得到實際的資料。
一般使用物件的方法以vertex array object為例:
shader和program的函式沒有照這個規則所以官方文件稱它們為unconventional object,但用法差不多。
-Vertex and fragment shader-
從準備shader開始,如果shader有錯就直接結束程式。D3D稱為pixel shader的東西在OpenGL稱為fragment shader。
shader是給GPU執行的程式,跟C/C++一樣要先把程式碼編譯成binary才能給GPU使用,OpenGL的shader程式語言稱為GLSL。
OpenGL 3.3必須把shader原始碼附在程式裡,程式執行時送給驅動程式編譯,不像D3D有事先編譯成中間碼的方法,直到4.6版才新增自己的中間碼格式SPIR-V。
先寫一個輔助函式compileShader(),然後才開始initSettings()。
● 用glCreateShader()產生一個shader識別碼,
● 用glShaderSource()載入shader程式碼,第二、第三參數是陣列長度和陣列,可以一次給它多個字串,它會把字串連接起來視為一段長的code,第四參數是各個字串的長度,填NULL會把字串視為null terminate string,偵測null字元求出長度。
OpenGL wiki的glShaderSource()說明
● 用glCompileShader()編譯,檢查shader有沒有寫錯的方法請參照上面程式碼。
fragment shader也按照這個方法準備。
光有shader物件還不能用,還要準備program物件,它是個容器把vertex、fragment、geometry各階段的shader包起來,以後套用program物件就是套用這一組shader。
● 用glCreateProgram()產生program識別碼。
● 用glAttachShader()將shader載入program物件。
● 用glLinkProgram()連結,會檢查各shader宣告的uniform變數有沒有一致、vertex shader的輸出和fragment shader的輸入有沒有配合等等,本篇的shader比較簡單,還比較看不出效果。
● link之後shader物件就不需要了,將它detach之後可以用glShaderSource()重新載入程式碼,用來產生另一個program,如果不會再用到就可刪除。
檢查link有沒有錯誤的方法請參照上面程式碼。
● 最後還要用glUseProgram()套用物件,之後呼叫draw call就會套用這一組shader。
一旦link之後就不可變更其中的shader,如果你有3個vertex shader和3個fragment shader,9種組合都會用到,那只好建立9個program物件了。
4.1版新增了separate program的功能,可以自由切換其中一個階段的shader,要呼叫完全不同的函式來使用。
OpenGL wiki的Separate programs說明
-Vertex buffer-
再來建立buffer物件並把頂點坐標從主記憶體傳給顯卡少女。先Gen、Bind,到glBufferData()這行才設定buffer大小並傳送資料。
Buffer Object的說明
glBufferData()說明
「glBindBuffer(GL_ARRAY_BUFFER, vertexData)」可以想成是把vertexData存在內部一個叫GL_ARRAY_BUFFER的全域變數,「glBufferData(GL_ARRAY_BUFFER,……)」讀取這個全域變數得知要操作哪個buffer。
GL_ARRAY_BUFFER是例外的物件之一,bind只是設一個全域變數而不是把它用在draw call,頂點資料來源是在下面vertex array object設定。
-Input assembler-
「Direct3D與OpenGL的繪圖管線(上)」講過,把頂點資料傳給顯卡少女時,她看到的只是一堆byte,還要告訴她資料是什麼格式。本篇使用OpenGL 3.0新增的方法:vertex array object (VAO)。
照標準物件的用法,Gen產生識別碼、Bind套用。
之後用glEnableVertexAttribArray()告訴顯卡少女啟用index=0的資料,用glVertexAttribPointer()設定格式,參數如下:
1:index,跟glEnableVertexAttribArray()的參數配合。
2:頂點資料都是視為向量,這個參數設定有幾個分量,本篇的坐標有X、Y兩個分量。
3:分量的資料型態。
4、5、6:現在先略過,等之後教到貼圖再介紹用法。
雖然參數沒有明確寫出來,它還會讀取GL_ARRAY_BUFFER得知0號資料來自哪個buffer。
glVertexAttribPointer()說明
OpenGL wiki關於vertex array object的說明
頂點坐標可以有不止一項資料,有幾頂資料就要呼叫幾次glEnableVertexAttribArray()和glVertexAttribPointer(),本篇只有位置一項資料,index和第5,6參數的用途在資料有兩項以上時才看得出來。
呼叫glVertexAttribPointer()之後,「頂點資料來自vertexData」和「格式為float[2]」就儲存在VAO裡了,以後bind這個VAO就會套用這些資訊。
幾何形狀和頂點數量不是在這裡告訴顯卡少女,而是之後draw call時才設定。
-Rasterizer, depth, stencil, and blend-
接下來只是設定值,不用建立物件就比較簡單。OpenGL調整設定值基本上是一個函式修改一個值,跟D3D11必須建立物件不一樣。
先看glEnable()這個函式。
glEnable()、glDisable()說明
不論pipeline的哪個步驟,只要是on, off的設定值都用這個函式設定,且OpenGL出新版本時會追加功能,因此能填旳flag很多。
想用某個功能通常要先用glEnable()啟用,然後用一些函式設定詳細功能,例如想啟用culling要先呼叫glEnable(GL_CULL_FACE),然後用glCullFace()和glFrontFace()調culling的設定。
rasterizer通常要調的設定是culling和multisample,本篇不需要這兩個功能。
還要設viewport,x,y,width,height的單位是像素而不是-1~1,且要注意OpenGL是以視窗左下角為原點,跟一般繪圖軟體不一樣。
glViewport()說明
忘記culling和viewport是什麼的話可以看這篇複習:Direct3D與OpenGL的繪圖管線(上)
本篇不做depth test和stencil test,也根本沒配置depth buffer,用glDisable()關閉功能。
Blend是指pixel的輸出和現在畫面上的像素要用什麼算式計算,本篇用pixel shader的輸出直接取代掉舊的值。
最後用glClearColor()把顏色指定給一個全域變數,之後用glClear()清除畫面會套用這個顏色。
有幾行後面有寫//default,OpenGL context建立完成時就會填好一些設定值,這幾行剛好跟預設的值相同,把這幾行拿掉執行結果不會變,可以讓程式短一些,但如果你無法記住全部預設值,怕弄錯那寫上去也無妨。
本篇省略錯誤檢查的code,但順便介紹一下OpenGL檢查錯誤的方法。
查函式宣告可以看到大部分函式傳回值都是void,OpenGL並不是用函式傳回值傳回錯誤碼,發生error時它會把錯誤碼記在內部一個變數,呼叫glGetError()可取得錯誤碼。
glGetError()說明
發生error之後錯誤碼會一直保留,直到下一次發生error才會修改,而glGetError()會在傳回錯誤碼同時把錯誤碼設為0,所以要用如下方式:
如果第二次glGetError()傳回非0,表示兩次glGetError()之間發生錯誤,可以修改glGetError()的位置縮小範圍。
這個方法只能檢查填錯參數之類的,不能檢查邏輯錯誤,如設定頂點格式時byte數填錯,這時會看到畫面上顯示的不是我們想要的,但glGetError()傳回0,有時候用一些debug工具才比較方便。
實際畫出畫面,用glClear()清除畫面,然後用glDrawArrays()畫出三角形,第一參數是幾何形狀,第三參數是頂點數量。
本篇只畫一個物體,所以各種設定值和VAO、program物件只在初始化時套用一次,之後就不更換,如果要畫多個物體,每次繪製之前要切換物件和設定值。
另外「Direct3D與OpenGL的繪圖管線(下)」有講到,glDrawArrays()時才真正使用這些設定值,因此initSettings()裡調設定的順序可以互換,不用照繪圖管線的步驟。
刪除物件的方法如上面介紹的glDelete*函式,只有program物件比較不一樣。
deinitGL和之前一樣。
假設檔名是simplepipeline.c,用這個指令build。
在Linux執行的樣子。
之前說過在Linux用printf()顯示訊息,要從命令列執行才看得到,如果點滑鼠執行遇到視窗沒出現等問題,可以從命令列執行看訊息。
之前說過以後OpenGL的範例程式只會寫Linux版,如果要在Windows用OpenGL,請參照「OpenGL 3.3初始化 (Windows)」這篇初始化,然後以下函式要照前兩行的方法用wglGetProcAddress()取得函式指標才能使用。
覺得取得函式指標的工作很煩人的話,有個函式庫GLEW能幫忙做這件事,可以用用看。
http://glew.sourceforge.net/
本篇的color theme是Breeze Dark,是KDE Plasma桌面內附的,也有移植到其他軟體上。本系列的OpenGL部分都會用這個配色。
VS code版下載頁面 https://marketplace.visualstudio.com/items?itemName=kde.breeze
一個KDE的軟體:KWrite的畫面
把「Direct3D與OpenGL的繪圖管線(上)」的圖拿出來看一下。
這次要做的是把左邊列的物件和設定值一樣一樣弄好,除了constant buffer、貼圖和sampler以外,本篇先不用到貼圖。
本篇重點是將頂點資料和shader傳給GPU。OpenGL 2.0的世代不一定要用到shader,而且有方法可以「上傳頂點資料→繪製」兩個工作同時做,但3.2以後的core profile就一定要寫vertex shader和fragment shader,且上傳頂點資料一定要照「建立buffer物件→上傳頂點資料→繪製」的步驟,所以初學階段就得學習怎麼做這兩件事。
本系列的OpenGL目標版本是3.3,一些早期的方法就不介紹了,也不使用4.0以後新增的功能。
因為本篇新介紹的東西很多,在此把shader和vertex array object的一些細節略過,先寫個能用的程式再說,之後教到貼圖和shader輸入輸出時再介紹。
之前的教學可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
#define GL_GLEXT_PROTOTYPES #define GLX_GLXEXT_PROTOTYPES #include<GL/gl.h> //間接引用GL/glext.h #include<GL/glx.h> //間接引用X11/Xlib.h #include<stdio.h> #include<time.h> //使用clock_gettime()和nanosleep() const int WINDOW_W=200,WINDOW_H=200; //OpenGL global狀態 Display* dsp; Window window; GLXContext context; //OpenGL物件 uint32_t programID; uint32_t vertexData; uint32_t vertexArrayObj; //要傳給GPU的資料 const float VERTICES[]={ 0,0.8, 0.8,0, -0.5,-0.5, }; const char VERTEX_SHADER[]="#version 330\n \ layout(location=0) in vec2 pos; \ void main(){ \ gl_Position=vec4(pos, 0, 1); \ }"; const char FRAGMENT_SHADER[]="#version 330\n \ void main(){ \ gl_FragColor=vec4(1,1,1,1); \ }"; //這六個函式在下面說明 static int initGL(){ …… } static int compileShader(uint32_t shaderID, const char* code, const char* name){ …… } static int initSettings(){ …… } static void nextFrame(){ …… } static void deinitSettings(){ …… } static void deinitGL(){ …… } int main(){ dsp = XOpenDisplay( NULL ); window = XCreateSimpleWindow(dsp, DefaultRootWindow(dsp), 0, 0,WINDOW_W, WINDOW_H, //xywh 0, 0, 0); if(initGL()){ printf("Can not initialize OpenGL\n"); return 0; } if(initSettings()){ return 0; } //設標題 XStoreName(dsp, window, "simplepipeline"); //設定事件mask Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", True); XSetWMProtocols(dsp, window, &wmDelete, 1); XMapWindow( dsp, window ); //接收事件 XEvent evt; int isEnd=0; struct timespec prevTime,nextTime; while(!isEnd){ clock_gettime(CLOCK_MONOTONIC, &prevTime); while(XPending(dsp)){ XNextEvent(dsp, &evt); switch(evt.type){ case ClientMessage: if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; } break; } } //正式寫遊戲時,遊戲邏輯放在此處 nextFrame(); //更新畫面 clock_gettime(CLOCK_MONOTONIC, &nextTime); //單位為奈秒(10^-9秒) int64_t elapsedTime = (nextTime.tv_sec-prevTime.tv_sec)*1000000000 +(nextTime.tv_nsec-prevTime.tv_nsec); //求出經過的奈秒數 struct timespec sleepTime={0, 16000000-elapsedTime}; if(sleepTime.tv_nsec>0){ nanosleep(&sleepTime, NULL); } } XDestroyWindow( dsp, window ); XFlush(dsp); deinitSettings(); deinitGL(); XCloseDisplay( dsp ); return 0; } |
1. nanosleep()之後立刻呼叫clock_gettime()取得目前時間。
2. 做完處埋輸入、遊戲邏輯、繪圖後再取得一次目前時間,將兩者相減算出經過的時間。
3. 再算出應該暫停多久,如果經過時間<16ms才暫停。
Linux取得目前時間有多種方式,其中最符合本篇需求的是clock_gettime(),第一參數填CLOCK_MONOTONIC則取得的是電腦開機後到現在的時間,修改系統時間不影響這個計時器。
傳回值是這個struct:
struct timespec{ time_t tv_sec; //秒 long tv_nsec; //秒以下的部分,單位為奈秒,最大值為999,999,999 }; //64位元程式裡兩個欄位都是64 bit |
其他可以計時到毫秒以下的函式,linux.die.net上面的說明:
clock_gettime(),第一參數填不同的值可取得不同的計時器。
clock(),這個process佔用的CPU時間,sleep經過的時間會忽略。
gettimeofday(),秒的部分傳回unix time stamp,會受修改系統時間影響。
還多了這部分。
//要傳給GPU的資料 const float VERTICES[]={ 0,0.8, 0.8,0, -0.5,-0.5, }; const char VERTEX_SHADER[]="#version 330\n \ layout(location=0) in vec2 pos; \ void main(){ \ gl_Position=vec4(pos, 0, 1); \ }"; const char FRAGMENT_SHADER[]="#version 330\n \ void main(){ \ gl_FragColor=vec4(1,1,1,1); \ }"; |
第一行「#version 330」指定shader版本號,這行一定要有,否則會被視為最舊的1.1版編譯,很多功能都不能用。
GLSL版本與OpenGL版本的對應看這篇:OpenGL wiki: Core Language (GLSL)#Compilation_settings
GLSL的程式進入點必須是main(),不像HLSL可以自己指定函式,把兩個shader寫在同一個字串就會出現兩個main(),所以要分成兩個字串。
三個頂點坐標是(0, 0.8)、(0.8, 0)、(-0.5, -0.5),以前有講過D3D和OpenGL的畫面坐標是-1 ~ 1,可以預想畫出的三角形是這樣。
static int initGL(){ 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 1; } //產生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); XFree(fbc); if(context==0){ return 1; } glXMakeCurrent(dsp, window, context); //設定Vsync PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA= (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress("glXSwapIntervalMESA"); if(glXSwapIntervalMESA){ glXSwapIntervalMESA(0); } PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI= (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddress("glXSwapIntervalSGI"); if(glXSwapIntervalSGI){ glXSwapIntervalSGI(0); } return 0; } |
之後的initSettings()是重點,不過看程式碼之前先了解一下OpenGL的「物件」是什麼。OpenGL不是用C/C++的struct或物件,而是用無號32位元整數代表它內部的物件,程式裡可以宣告成OpenGL自訂的GLuint,或C語言標準的unsigned int、uint32_t。
OpenGL官方文件把這些整數稱為object或name,我是習慣稱為ID(識別碼),以免跟C/C++的物件或字串混淆。
至於整數怎麼對應到驅動程式或顯示記憶體內的資料,OpenGL規格有要求晶片廠商自己想辦法,外面傳整數進來驅動程式要找得到實際的資料。
一般使用物件的方法以vertex array object為例:
- 用glGenVertexArrays()產生可用的識別碼。
它的原形宣告是「void glGenVertexArrays(GLsizei n, GLuint* arrays);」,可以一次產生多個,第一參數給數量,第二參數給可儲存足夠int的空間。
產生的值隨晶片和平臺而異,有的平臺會傳回流水號但有的平臺不是,不能預先假設它是什麼,但可以確定的是不會產生0。 - 用glBindVertexArray()套用物件,如「Direct3D與OpenGL的繪圖管線(下)」所說,可以想成是叫顯卡少女把物件放在工作臺上,之後draw call就會使用它。
有些物件例外,bind只是把ID指定給內部一個全域變數,要不要用在draw call是用其他函式設定。
填0有什麼作用依物件種類而定,有的代表不套用任何物件,有的是套用一個系統內建的物件,如0號framebuffer代表畫面。 - 在物件裡填入資料,以及指定buffer和texture的大小是bind之後再用其他函式操作。
- 用完後呼叫glDeleteVertexArrays()刪除物件。原形宣告是「void glDeleteVertexArrays(GLsizei n, const GLuint* arrays);」,跟glGenVertexArrays()一樣可以一次刪除多個。
shader和program的函式沒有照這個規則所以官方文件稱它們為unconventional object,但用法差不多。
-Vertex and fragment shader-
從準備shader開始,如果shader有錯就直接結束程式。D3D稱為pixel shader的東西在OpenGL稱為fragment shader。
shader是給GPU執行的程式,跟C/C++一樣要先把程式碼編譯成binary才能給GPU使用,OpenGL的shader程式語言稱為GLSL。
OpenGL 3.3必須把shader原始碼附在程式裡,程式執行時送給驅動程式編譯,不像D3D有事先編譯成中間碼的方法,直到4.6版才新增自己的中間碼格式SPIR-V。
//傳回0代表成功、非0代表失敗 //name是用在顯示錯誤訊息,OpenGL不會用到 static int compileShader(uint32_t shaderID, const char* code, const char* name){ glShaderSource(shaderID, 1, &code, NULL); glCompileShader(shaderID); //檢查shader錯誤 int ret; glGetShaderiv(shaderID, GL_COMPILE_STATUS, &ret); //ret=0代表有錯誤 if(!ret){ int infoLen; glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLen); //取得訊息長度 char info[infoLen]; //配置空間 glGetShaderInfoLog(shaderID, infoLen, NULL, info); //取得訊息 printf("%s:\n%s",name, info); } return !ret; } static int initSettings(){ //compile vertex shader uint32_t vsID=glCreateShader(GL_VERTEX_SHADER); int vsStatus=compileShader(vsID, VERTEX_SHADER, "vertex shader"); //compile fragment shader uint32_t fsID=glCreateShader(GL_FRAGMENT_SHADER); int fsStatus=compileShader(fsID, FRAGMENT_SHADER, "fragment shader"); if(vsStatus || fsStatus){ glDeleteShader(vsID); glDeleteShader(fsID); return 1; } |
● 用glCreateShader()產生一個shader識別碼,
● 用glShaderSource()載入shader程式碼,第二、第三參數是陣列長度和陣列,可以一次給它多個字串,它會把字串連接起來視為一段長的code,第四參數是各個字串的長度,填NULL會把字串視為null terminate string,偵測null字元求出長度。
OpenGL wiki的glShaderSource()說明
● 用glCompileShader()編譯,檢查shader有沒有寫錯的方法請參照上面程式碼。
fragment shader也按照這個方法準備。
//create program object programID=glCreateProgram(); glAttachShader(programID, vsID); glAttachShader(programID, fsID); glLinkProgram(programID); glDetachShader(programID, vsID); glDetachShader(programID, fsID); glDeleteShader(vsID); //之後不會再用到shader物件,可刪除 glDeleteShader(fsID); //check program error int ret; glGetProgramiv(programID,GL_LINK_STATUS, &ret); if(!ret){ int infoLen; glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLen); char info[infoLen]; glGetProgramInfoLog(programID, infoLen, NULL, info); printf("program linking:\n%s", info); glDeleteProgram(programID); return 1; } glUseProgram(programID); |
● 用glCreateProgram()產生program識別碼。
● 用glAttachShader()將shader載入program物件。
● 用glLinkProgram()連結,會檢查各shader宣告的uniform變數有沒有一致、vertex shader的輸出和fragment shader的輸入有沒有配合等等,本篇的shader比較簡單,還比較看不出效果。
● link之後shader物件就不需要了,將它detach之後可以用glShaderSource()重新載入程式碼,用來產生另一個program,如果不會再用到就可刪除。
檢查link有沒有錯誤的方法請參照上面程式碼。
● 最後還要用glUseProgram()套用物件,之後呼叫draw call就會套用這一組shader。
一旦link之後就不可變更其中的shader,如果你有3個vertex shader和3個fragment shader,9種組合都會用到,那只好建立9個program物件了。
4.1版新增了separate program的功能,可以自由切換其中一個階段的shader,要呼叫完全不同的函式來使用。
OpenGL wiki的Separate programs說明
-Vertex buffer-
//vertex buffer glGenBuffers(1, &vertexData); glBindBuffer(GL_ARRAY_BUFFER, vertexData); glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES),VERTICES, GL_STATIC_DRAW); |
Buffer Object的說明
glBufferData()說明
「glBindBuffer(GL_ARRAY_BUFFER, vertexData)」可以想成是把vertexData存在內部一個叫GL_ARRAY_BUFFER的全域變數,「glBufferData(GL_ARRAY_BUFFER,……)」讀取這個全域變數得知要操作哪個buffer。
GL_ARRAY_BUFFER是例外的物件之一,bind只是設一個全域變數而不是把它用在draw call,頂點資料來源是在下面vertex array object設定。
-Input assembler-
//input assembler glGenVertexArrays(1, &vertexArrayObj); glBindVertexArray(vertexArrayObj); glEnableVertexAttribArray(0); glVertexAttribPointer(0,2,GL_FLOAT,0,0,0); |
照標準物件的用法,Gen產生識別碼、Bind套用。
之後用glEnableVertexAttribArray()告訴顯卡少女啟用index=0的資料,用glVertexAttribPointer()設定格式,參數如下:
1:index,跟glEnableVertexAttribArray()的參數配合。
2:頂點資料都是視為向量,這個參數設定有幾個分量,本篇的坐標有X、Y兩個分量。
3:分量的資料型態。
4、5、6:現在先略過,等之後教到貼圖再介紹用法。
雖然參數沒有明確寫出來,它還會讀取GL_ARRAY_BUFFER得知0號資料來自哪個buffer。
glVertexAttribPointer()說明
OpenGL wiki關於vertex array object的說明
頂點坐標可以有不止一項資料,有幾頂資料就要呼叫幾次glEnableVertexAttribArray()和glVertexAttribPointer(),本篇只有位置一項資料,index和第5,6參數的用途在資料有兩項以上時才看得出來。
呼叫glVertexAttribPointer()之後,「頂點資料來自vertexData」和「格式為float[2]」就儲存在VAO裡了,以後bind這個VAO就會套用這些資訊。
幾何形狀和頂點數量不是在這裡告訴顯卡少女,而是之後draw call時才設定。
-Rasterizer, depth, stencil, and blend-
//rasterizer glDisable(GL_CULL_FACE); //default glDisable(GL_MULTISAMPLE); glViewport(0, 0, WINDOW_W, WINDOW_H); //depth and stencil glDisable(GL_DEPTH_TEST); //default glDisable(GL_STENCIL_TEST); //default //blend glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); //default glBlendFunc(GL_ONE, GL_ZERO); //default //設定清除畫面的顏色 const float color[]={0,0,0,0}; glClearColor(color[0],color[1],color[2],color[3]); } //結束initSettings() |
先看glEnable()這個函式。
glEnable()、glDisable()說明
不論pipeline的哪個步驟,只要是on, off的設定值都用這個函式設定,且OpenGL出新版本時會追加功能,因此能填旳flag很多。
想用某個功能通常要先用glEnable()啟用,然後用一些函式設定詳細功能,例如想啟用culling要先呼叫glEnable(GL_CULL_FACE),然後用glCullFace()和glFrontFace()調culling的設定。
rasterizer通常要調的設定是culling和multisample,本篇不需要這兩個功能。
還要設viewport,x,y,width,height的單位是像素而不是-1~1,且要注意OpenGL是以視窗左下角為原點,跟一般繪圖軟體不一樣。
glViewport()說明
忘記culling和viewport是什麼的話可以看這篇複習:Direct3D與OpenGL的繪圖管線(上)
本篇不做depth test和stencil test,也根本沒配置depth buffer,用glDisable()關閉功能。
Blend是指pixel的輸出和現在畫面上的像素要用什麼算式計算,本篇用pixel shader的輸出直接取代掉舊的值。
最後用glClearColor()把顏色指定給一個全域變數,之後用glClear()清除畫面會套用這個顏色。
有幾行後面有寫//default,OpenGL context建立完成時就會填好一些設定值,這幾行剛好跟預設的值相同,把這幾行拿掉執行結果不會變,可以讓程式短一些,但如果你無法記住全部預設值,怕弄錯那寫上去也無妨。
本篇省略錯誤檢查的code,但順便介紹一下OpenGL檢查錯誤的方法。
查函式宣告可以看到大部分函式傳回值都是void,OpenGL並不是用函式傳回值傳回錯誤碼,發生error時它會把錯誤碼記在內部一個變數,呼叫glGetError()可取得錯誤碼。
glGetError()說明
發生error之後錯誤碼會一直保留,直到下一次發生error才會修改,而glGetError()會在傳回錯誤碼同時把錯誤碼設為0,所以要用如下方式:
//呼叫一次將錯誤碼設為0 glGetError(); //vertex buffer glGenBuffers(1, &vertexData); glBindBuffer(GL_ARRAY_BUFFER, vertexData); glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES),VERTICES, GL_STATIC_DRAW); //input assembler glGenVertexArrays(1, &vertexArrayObj); glBindVertexArray(vertexArrayObj); glEnableVertexAttribArray(0); glVertexAttribPointer(0,0,GL_FLOAT,0,0,0); //第二參數故意填錯的值 //再呼叫一次glGetError() printf("error: %x\n", glGetError()); |
這個方法只能檢查填錯參數之類的,不能檢查邏輯錯誤,如設定頂點格式時byte數填錯,這時會看到畫面上顯示的不是我們想要的,但glGetError()傳回0,有時候用一些debug工具才比較方便。
static void nextFrame(){ glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, 3); glXSwapBuffers(dsp, window); } |
本篇只畫一個物體,所以各種設定值和VAO、program物件只在初始化時套用一次,之後就不更換,如果要畫多個物體,每次繪製之前要切換物件和設定值。
另外「Direct3D與OpenGL的繪圖管線(下)」有講到,glDrawArrays()時才真正使用這些設定值,因此initSettings()裡調設定的順序可以互換,不用照繪圖管線的步驟。
static void deinitSettings(){ glDeleteProgram(programID); glDeleteBuffers(1, &vertexData); glDeleteVertexArrays(1, &vertexArrayObj); } |
static void deinitGL(){ glXDestroyContext(dsp, context); } |
假設檔名是simplepipeline.c,用這個指令build。
gcc simplepipeline.c -o simplepipeline -s -Os -lX11 -lGL |
在Linux執行的樣子。
之前說過在Linux用printf()顯示訊息,要從命令列執行才看得到,如果點滑鼠執行遇到視窗沒出現等問題,可以從命令列執行看訊息。
之前說過以後OpenGL的範例程式只會寫Linux版,如果要在Windows用OpenGL,請參照「OpenGL 3.3初始化 (Windows)」這篇初始化,然後以下函式要照前兩行的方法用wglGetProcAddress()取得函式指標才能使用。
PFNGLCREATESHADERPROC glCreateShader= (PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"); glShaderSource glCompileShader glGetShaderiv glGetShaderInfoLog glCreateProgram glAttachShader glLinkProgram glDetachShader glDeleteShader glGetProgramiv glGetProgramInfoLog glUseProgram glGenBuffers glBindBuffer glBufferData glGenVertexArrays glBindVertexArray glEnableVertexAttribArray glVertexAttribPointer glDeleteProgram glDeleteBuffers glDeleteVertexArrays |
http://glew.sourceforge.net/
本篇的color theme是Breeze Dark,是KDE Plasma桌面內附的,也有移植到其他軟體上。本系列的OpenGL部分都會用這個配色。
VS code版下載頁面 https://marketplace.visualstudio.com/items?itemName=kde.breeze
一個KDE的軟體:KWrite的畫面