前篇:OpenGL 3.3 架設基本繪圖管線
前一篇還沒有使用uniform buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
整個系列可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個軟體做的
キャラクターなんとか機 http://khmix.sakura.ne.jp/download.shtml
將視窗尺寸設得比較大以配合圖檔,且多了一些header用來讀檔案。
這次要傳給GPU的資料如下
資料跟D3D篇一樣,只是那篇使用C++而本篇使用C,純C用struct定義資料型態必須寫成typedef struct。
說明照搬那邊的:
坐標是這樣,右圖的ABCD是頂點順序。
多數繪圖軟體和函式庫以左上角為原點,但上傳貼圖的函式glTexImage2D()規定給它的資料以左下角為原點,所以圖在OpenGL內部是倒立的狀態。不過OpenGL讀取貼圖也以左下角為原點,兩次顛倒抵消,能畫出正立的圖片。
但如果用framebuffer object把圖畫在記憶體內的點陣圖,再把它當成貼圖畫在另一張點陣圖,就要對坐標特別處理了。
shader這次採用讀外部檔的方式,由於OpenGL 3.3沒有提供事先編譯的方式,主程式要把shader原始碼載入,在執行時編譯。
準備這兩個檔案,因為GLSL程式起點固定叫做main(),不能把兩個shader放在同一個檔案。
「shader的輸入與輸出」提到的輸出入在這裡出現了,看本篇時可以跟那篇對照著看。
shader裡面的處理方式跟D3D篇一樣,以下是從D3D篇照抄:
OpenGL 3.3雖然不能事先編譯,但有一個工具glslangValidator可以在把shader包進主程式前檢查語法。Fedora要裝glslang的套件;Ubuntu要20.04版,Mint要20版以後才有這個套件,名稱是glslang-tools。
Windows的話就自己去官網找下載的地方。
https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/
像這樣打可檢查語法錯誤
它會根據副檔名判斷這是哪一階段的shader、語言是GLSL還是HLSL,把檔案命名為.vert.glsl和.frag.glsl目的在此。
如果要查uniform block裡各變數的位置作為寫C struct的參考,好像必須用「glslangValidator -i 檔名」再從裡面找出offset資訊,不知道有沒有更簡單的方法。
另外如果想在OpenGL比較新的版本用SPIR-V,將程式碼編譯成SPIR-V也是用這個工具。
initGL()、deinitGL和之前一樣。
glDrawArrays()第一參數的幾何形狀改成GL_TRIANGLE_STRIP,第三參數的頂點數量改成4。
本篇要講的東西很多,先不介紹各種幾何形狀。雖然OpenGL 3.0以後把四邊形列為deprecated,OpenGL ES也一開始就沒支援四邊形,只有四個頂點的情況下用triangle strip可以畫四邊形。
要刪除的物件增加3個。
buffer物件有兩個,這裡利用glDeleteBuffers()可以一次刪除多個物件的特性,傳入長度為2的陣列。
接下來的initSettings()是主題,用到的輔助函式會在旁邊解說。
-Vertex & Fragment Shader-
loadWholeFile()用到的Linux API函式請參照這篇:檔案操作—Linux篇,作業系統沒有直接提供「傳入檔名→讀取整個檔案」的函式,但這功能有時候會用到,寫一個函式做這件事。
把shader程式碼讀入記憶體之後,按照「OpenGL 3.3 架設基本繪圖管線」的方法建立shader與program物件、檢查error。
-Vertex buffer & Input assembler-
建buffer和vertex array物件的方法跟「OpenGL 3.3 架設基本繪圖管線」一樣,本篇頂點有三項資料,要呼叫三次glEnableVertexAttribArray()和glVertexAttribPointer()設定格式。glVertexAttribPointer()的參數是什麼意思留待下一篇介紹。
-Uniform buffer-
本篇新增的東西之一。與vertex buffer一樣是buffer所以建立的方法很像,只是bind的第一參數換成GL_UNIFORM_BUFFER。
-Sampler-
本篇新增的東西之二。跟大部分OpenGL物件一樣用Gen函式產生識別碼,不過修改sampler屬性不需要先bind,因為操作sampler的函式用第一參數指定要操作哪個sampler。
FILTER和WRAP是什麼請參照這篇貼圖的部分:shader的輸入與輸出,MAG_FILTER和MIN_FILTER分別設定放大和縮小要做什麼處理,WRAP_S和WRAP_T分別是貼圖的X坐標和Y坐標,兩個方向可以用不同鋪排方式。
OpenGL wiki: Sampler Object
有幾行有標示「//default」,sampler物件建好之後就會填入一些預設值,這幾行剛好跟預設的值相同,把這幾行拿掉執行結果不會變。
-Texture-
本篇新增的東西之三,用到一個輔助函式loadTexture()。用一個函式庫:gdk-pixbuf讀取圖檔和解碼,方法參照這篇:讀取圖檔的方法-Linux篇。
再抄一段D3D篇的過來:PNG、JPG、webP這些編碼過的格式不能給GPU使用,因為GPU讀取貼圖需要迅速找到任意位置的像素(即隨機存取,random access),這些格式必須完全解碼才能得知每個像素的值。用在GPU的壓縮格式必須設計成能隨機存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
gdk-pixbuf解碼出來的byte順序是RGBA,之後把它轉換成BGRA。R,G,B,A四個分量,每個分量8bit剛好形成一個32位元整數,四個byte的順序D3D和OpenGL可以支援RGBA(最低位元組是R)和BGRA(最低位元組是B),筆者選用BGRA的理由是大部分繪圖軟體的十六進位顏色值都是BGR,可以直接把軟體裡的值複製貼上到程式裡。
只用C/C++語法轉換byte順序有點麻煩,這裡用兩個組合語言指令做這工作:bswap和ror,
__bswapd()和_rotr()是所謂的intrinsic function,有些組合語指令沒有直接對應的C/C++語法,C/C++只能用函式的型式提供功能,這些函式編譯後會直接轉換成組合語言指令,不會產生函式呼叫。
之後建立貼圖物件,老樣子的Gen、Bind兩步驟,再用glTexImage2D()設定貼圖格式並上傳資料。參數如下:
1:target。glBindTexture()把貼圖ID設給一個叫GL_TEXTURE_2D的變數,然後glTexImage2D()讀取這個變數得知要操作哪個貼圖物件。
貼圖種類有很多,除了本篇用的2D貼圖以外,還有1D、3D、cube map、貼圖陣列等等。
2:如果有使用mipmap且建立物件時就要上傳mipmap,要用這個參數指定層數,不使用mipmap就填0。
3:在顯示記憶體裡以何種格式儲存。
4、5:寬高。
6:border。是早期版本的功能,但現在的OpenGL取消了,必須填0。
7、8:你傳進去的資料(第9參數)是什麼格式,7是有幾個顏色分量和byte順序,8是各分量是什麼型態。
9:要上傳的資料,byte數會從寬高和格式求出。
3,7,8全部可以填什麼格式要看官方文件
OpenGL wiki: GLAPI/glTexImage2D
內部格式和第7,8參數如果是不同格式,GPU會做轉換,如果不能轉換就產生error code,取得error code的方法以前提過,是用glGetError()。
內部格式可以填這篇文件裡的Base Internal Formats讓GPU自行決定各分量bit數,也可以填Sized Internal Formats指定bit數;不需要指定byte順序,GPU會看情況自行決定。
下面還有Sized Depth and Stencil Internal Formats和Compressed Internal Formats兩個表,要求GPU配置一塊depth或stencil buffer也是用這個函式,文件裡有列出壓縮格式但上傳壓縮過的貼圖要用另一個函式:glCompressedTexImage2D()。
下一行「glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0)」絕對不能少,本篇沒有用到mipmap的功能,但貼圖物件被建出來時是設定成要套用mipmap,如果沒準備好mipmap就不能使用這個物件,所以要用這一行告訴OpenGL這個貼圖不用mipmap。
(筆者有一次因為沒打這行卡了好幾個小時)
-Rasterizer, depth, stencil, and blend-
rasterizer、depth和stencil跟以前一樣,只有blend換了算式,改成正式繪圖軟體使用的。
(Sc, Sa):pixel shader輸出的顏色和alpha,數值範圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
glClearColor()把背景設成白色,且把alpha設成1簡化blend的算式。如果畫面的alpha不是1,那其實D3D和OpenGL的blend設定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時做一點特別處理。
以前有寫過一篇筆記:premultiply alpha的妙用
如果檔名是usetexture.c,用這個指令build
比上次多了`pkg-config --cflags --libs gdk-pixbuf-2.0`是因為用gdk-pixbuf讀取圖檔。
執行的樣子。
uniform buffer、texture、sampler這三種物件如何將主程式和shader的物件對應,可以想成顯卡少女的工作臺有個置物櫃,有很多格子。
跟D3D11不一樣的是,OpenGL所有shader階段共用一個置物櫃,且把貼圖和sampler放在同一個格子。program物件裡有一張表把變數名稱和格子編號對應,由於OpenGL要把全部階段的shader連結成一個program才能使用,這個對應表是全部階段共用。
(註:有兩個擴充:ARB_shading_language_420pack和ARB_separate_shader_objects會改變本節介紹的規則,因為分別是4.2與4.1版才列為標準配備,本篇不介紹)
本篇的shader裡有這兩段,其中的uniform1和sampler1是變數名稱,兩階段同名的變數會視為同一個物件。
link之後program會記住裡面有"uniform1"和"sampler1"兩個變數,留待之後對應到格子。
變數名稱與格子的對應,以及指示顯卡少女把物件放進格子,是在主程式裡做。
這一段是設定uniform buffer
格子的索引是從0開始,本篇故意用2。
不知為何不用一個函式直接填入(programID, "uniform1", 2)就好,還要取得uniformIndex再用另一個函式設定,但這就是OpenGL的規定。
上面說對應表是存在program物件裡,如果有3個program物件,要呼叫3次glGetUniformBlockIndex()和glUniformBlockBinding()分別設定對應,但只要呼叫一次glBindBufferBase()就可以3個program共用這個uniform buffer。
這一段是設定sampler物件
同樣要用一個暫時變數location。
glGetUniformLocation()用第一參數指定要操作哪個program,但glUniform1i()沒有programID的參數,它用前面的glUseProgram()決定要操作的program。OpenGL有個討厭的地方是有些函式只由參數決定行為,但有些函式會讀取內部的全域變數,規則不一致。
texture物件的話,因為GLSL的sampler物件也包含貼圖,sampler部分的前兩行也同時設定texture的格子對應。
把物件放到2號格子要這樣做
格子編號不是填數字,而是用常數GL_TEXTURE0、GL_TEXTURE1……,這些是連續整數,所以也可以用「GL_TEXTURE0+i」的方式。
可以做到這樣,在vertex和fragment shader用不同的變數名稱,但是對應到相同格子。
但這會讓人寫程式混亂,同一個slot最好還是用相同名稱。
具體有幾個格子隨硬體、驅動程式和作業系統而異,要用glGetIntegerv()查詢以下的值:
GL_MAX_VERTEX_UNIFORM_BLOCKS
GL_MAX_FRAGMENT_UNIFORM_BLOCKS
GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS
GL_MAX_TEXTURE_IMAGE_UNITS
…………
在命令列打「glxinfo -B -l」會列出目前環境中OpenGL各功能的上限,其中有這些值。
參考 OpenGL wiki: Resource limitations
OpenGL規格會要求晶片廠商支援一定數量以上,照這篇說明,3.3版至少有12格uniform buffer(編號0~11),16格sampler。
sampler物件是OpenGL 3.3版新增的東西,3.2版以前的做法是每個貼圖物件都內附一個sampler,想設定取樣方式要修改貼圖屬性,如果看OpenGL比較早版本的教學會看到這種用法。
查這兩個函式的說明會看到很多相同的設定值
glTexParameter
glSamplerParameter
前一篇還沒有使用uniform buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
整個系列可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個軟體做的
キャラクターなんとか機 http://khmix.sakura.ne.jp/download.shtml
#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() #include<fcntl.h> //使用open() #include<sys/stat.h> //使用fstat() #include<gdk-pixbuf/gdk-pixbuf.h> //讀取圖檔 #include<x86intrin.h> //使用_rotr()和__bswapd() const int WINDOW_W=400,WINDOW_H=400; //OpenGL global狀態 Display* dsp; Window window; GLXContext context; //OpenGL物件 uint32_t programID; uint32_t vertexData; uint32_t vertexArrayObj; //本篇新增的物件 uint32_t uniformBuffer; uint32_t texture; uint32_t sampler; //要傳給GPU的資料在下面說明 //這些函式在下面說明 static int initGL(); static int initSettings(); static void nextFrame(); static void deinitSettings(); static void deinitGL(); static char* loadWholeFile(const char* fileName, uint32_t* outFileSize); static int loadShader(uint32_t shaderID, const char* fileName); static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao); static uint32_t loadTexture(const char* fileName); //這個函式留待之後介紹layout時解說 static void initVertexData2(uint32_t* outVertexData, uint32_t* outVao); 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; } |
這次要傳給GPU的資料如下
//uniform buffer const float WINDOW_SIZE[]={WINDOW_W, WINDOW_H}; //頂點資料 typedef struct{ float pos[2]; short texCoord[2]; uint32_t color; } VertexData1; VertexData1 VERTEX_DATA1[4]={ {100,0, 0, 0, 0xffffffff}, {100,400,0, 400,0xffffffff}, {400,0, 300,0, 0xffffffff}, {400,400,300,400,0xffffffff}, }; typedef struct{ float pos[8]; short texCoord[8]; uint32_t color[4]; } VertexData2; VertexData2 VERTEX_DATA2={ {100,0,100,400,400,0,400,400}, {0,0,0,400,300,0,300,400}, {0xffffffff,0xffffffff,0xffffffff,0xffffffff}, }; |
說明照搬那邊的:
VERTEX_DATA1和VERTEX_DATA2是相同頂點資料用不同layout儲存,本篇只用1,2等之後寫layout的教學再使用。
這次頂點資料有三項:位置、貼圖坐標、顏色,之前說過D3D和OpenGL的畫面坐標是-1~1、貼圖坐標是0~1,但2D畫面習慣上以畫面左上角為原點、以像素為單位,這裡頂點資料就這樣給,在shader裡換算成D3D和OpenGL標準。
雖然本篇故意在vertex shader裡算,這裡只有4個頂點,在CPU把4個頂點全部算好才傳給GPU其實效能不會差多少,但如果一次畫很多個三角形(如tilemap和粒子系統),在vertex shader裡算比較方便。
坐標是這樣,右圖的ABCD是頂點順序。
多數繪圖軟體和函式庫以左上角為原點,但上傳貼圖的函式glTexImage2D()規定給它的資料以左下角為原點,所以圖在OpenGL內部是倒立的狀態。不過OpenGL讀取貼圖也以左下角為原點,兩次顛倒抵消,能畫出正立的圖片。
但如果用framebuffer object把圖畫在記憶體內的點陣圖,再把它當成貼圖畫在另一張點陣圖,就要對坐標特別處理了。
shader這次採用讀外部檔的方式,由於OpenGL 3.3沒有提供事先編譯的方式,主程式要把shader原始碼載入,在執行時編譯。
準備這兩個檔案,因為GLSL程式起點固定叫做main(),不能把兩個shader放在同一個檔案。
//usetexture.vert.glsl #version 330 layout(std140) uniform uniform1{ vec2 windowSize; }; uniform sampler2D sampler1; //GLSL的sampler物件包含sampler和貼圖 //頂點資料,對應到struct VertexData1 layout(location=0) in vec2 inPos; layout(location=1) in vec2 inTexCoord; layout(location=2) in vec4 inColor; //vertex傳給fragment shader的資料 varying vec2 varTexCoord; varying vec4 varColor; void main(){ vec2 outPos=inPos*vec2(2,-2)/windowSize+vec2(-1,1); gl_Position=vec4(outPos, 0, 1); ivec2 texSize=textureSize(sampler1, 0); varTexCoord=inTexCoord/texSize; varColor=inColor; } |
//usetexture.frag.glsl #version 330 uniform sampler2D sampler1; varying vec2 varTexCoord; varying vec4 varColor; void main(){ vec4 texColor=texture(sampler1, varTexCoord); gl_FragColor=texColor*varColor; } |
shader裡面的處理方式跟D3D篇一樣,以下是從D3D篇照抄:
vertex shader坐標轉換的算式怎麼求出,回想一下學校學過的二元一次方程式。
畫面坐標要把左邊的換算成右邊
二元一次方程式的標準式「x'=xa+b, y'=yc+d」,把兩組坐標代進去,得到兩組聯立方程式
X坐標
-1 = 0 + b
1 = windowW×a + bY坐標
1 = 0 + d
-1 = windowH×c + d
解出a,b,c,d如下
x' =( 2/windowW)x - 1
y' =(-2/windowH)y + 1
因為shader可以一次計算四維向量,可以寫成這樣
outPos = inPos×(2,-2)/(windowW, windowH) + (-1,1)
貼圖坐標只要除以貼圖寬高即可,內建函式textureSize()可以取得貼圖寬高。
顏色就原封不動傳給rasterizer內插。
fragment shader讀取貼圖裡的像素,跟頂點資料裡的顏色相乘。
OpenGL 3.3雖然不能事先編譯,但有一個工具glslangValidator可以在把shader包進主程式前檢查語法。Fedora要裝glslang的套件;Ubuntu要20.04版,Mint要20版以後才有這個套件,名稱是glslang-tools。
Windows的話就自己去官網找下載的地方。
https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/
像這樣打可檢查語法錯誤
glslangValidator usetexture.vert.glsl glslangValidator usetexture.frag.glsl |
如果要查uniform block裡各變數的位置作為寫C struct的參考,好像必須用「glslangValidator -i 檔名」再從裡面找出offset資訊,不知道有沒有更簡單的方法。
另外如果想在OpenGL比較新的版本用SPIR-V,將程式碼編譯成SPIR-V也是用這個工具。
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; } static void deinitGL(){ glXDestroyContext(dsp, context); } |
static void nextFrame(){ glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glXSwapBuffers(dsp, window); } |
本篇要講的東西很多,先不介紹各種幾何形狀。雖然OpenGL 3.0以後把四邊形列為deprecated,OpenGL ES也一開始就沒支援四邊形,只有四個頂點的情況下用triangle strip可以畫四邊形。
static void deinitSettings(){ glDeleteProgram(programID); uint32_t buffers[]={vertexData, uniformBuffer}; glDeleteBuffers(2, buffers); glDeleteVertexArrays(1, &vertexArrayObj); glDeleteTextures(1, &texture); glDeleteSamplers(1, &sampler); } |
buffer物件有兩個,這裡利用glDeleteBuffers()可以一次刪除多個物件的特性,傳入長度為2的陣列。
接下來的initSettings()是主題,用到的輔助函式會在旁邊解說。
-Vertex & Fragment Shader-
//傳回來的指標要用free()釋放 static char* loadWholeFile(const char* fileName, uint32_t* outFileSize){ int file=open(fileName, O_RDONLY); if(file==-1){ *outFileSize=0; return NULL; } struct stat buf; fstat(file, &buf); long fileSize=buf.st_size; char* data=(char*)malloc(fileSize); read(file, data, fileSize); close(file); *outFileSize=fileSize; return data; } //傳回0代表成功、非0代表失敗 static int loadShader(uint32_t shaderID, const char* fileName){ uint32_t fileSize; const char* shaderCode=loadWholeFile(fileName, &fileSize); if(shaderCode==NULL){ return 1; } glShaderSource(shaderID, 1, &shaderCode, &fileSize); free((void*)shaderCode); 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",fileName, info); } return !ret; } |
//傳回0代表成功,非0代表失敗 static int initSettings(){ //compile vertex shader uint32_t vsID=glCreateShader(GL_VERTEX_SHADER); loadShader(vsID, "usetexture.vert.glsl"); //compile fragment shader uint32_t fsID=glCreateShader(GL_FRAGMENT_SHADER); loadShader(fsID, "usetexture.frag.glsl"); //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); |
把shader程式碼讀入記憶體之後,按照「OpenGL 3.3 架設基本繪圖管線」的方法建立shader與program物件、檢查error。
-Vertex buffer & Input assembler-
static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao){ //vertex buffer glGenBuffers(1, outVertexData); glBindBuffer(GL_ARRAY_BUFFER, *outVertexData); glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA1),VERTEX_DATA1, GL_STATIC_DRAW); //vertex array object glGenVertexArrays(1,outVao); glBindVertexArray(*outVao); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT,0, sizeof(VertexData1), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_SHORT,0, sizeof(VertexData1), (void*)offsetof(VertexData1, texCoord)); glEnableVertexAttribArray(2); glVertexAttribPointer(2, GL_BGRA, GL_UNSIGNED_BYTE,1, sizeof(VertexData1), (void*)offsetof(VertexData1, color)); } |
//initSettings()內容 //vertex data & vertex array object initVertexData1(&vertexData, &vertexArrayObj); |
-Uniform buffer-
//uniform buffer glGenBuffers(1, &uniformBuffer); glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer); glBufferData(GL_UNIFORM_BUFFER, sizeof(WINDOW_SIZE), WINDOW_SIZE, GL_STATIC_DRAW); //將這個物件與shader裡的buffer物件對應 uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1"); glUniformBlockBinding(programID, uniformIndex, 2); glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer); |
-Sampler-
//sampler glGenSamplers(1, &sampler); glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //default glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT); //default glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT); //default //將這個物件與shader裡的sampler物件對應 int location=glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); glBindSampler(2, sampler); |
FILTER和WRAP是什麼請參照這篇貼圖的部分:shader的輸入與輸出,MAG_FILTER和MIN_FILTER分別設定放大和縮小要做什麼處理,WRAP_S和WRAP_T分別是貼圖的X坐標和Y坐標,兩個方向可以用不同鋪排方式。
OpenGL wiki: Sampler Object
有幾行有標示「//default」,sampler物件建好之後就會填入一些預設值,這幾行剛好跟預設的值相同,把這幾行拿掉執行結果不會變。
-Texture-
//讀取圖檔、解碼、產生texture物件 //成功傳回OpenGL物件識別碼,失敗傳回0 static uint32_t loadTexture(const char* fileName){ //用gdk-pixbuf讀取圖檔,把RGBA值存在一個叫pixels的變數 GError* error=NULL; GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(fileName,&error); if(pixbuf==NULL){ return 0; } int w = gdk_pixbuf_get_width(pixbuf); int h = gdk_pixbuf_get_height(pixbuf); uint32_t* pixels = (uint32_t*)malloc(w*h*4); GdkPixbuf* outPixbuf=gdk_pixbuf_new_from_data((uint8_t*)pixels, GDK_COLORSPACE_RGB, 1,8,w,h,w*4,NULL,0); gdk_pixbuf_copy_area(pixbuf,0,0,w,h,outPixbuf,0,0); g_object_unref(pixbuf); g_object_unref(outPixbuf); //RGBA轉BGRA uint32_t* tempP=pixels; for(int i=0; i<w*h; i++,tempP++){ *tempP=_rotr(__bswapd(*tempP), 8); } //建立texture物件 uint32_t textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w,h,0, GL_BGRA,GL_UNSIGNED_BYTE, pixels); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0); //設mipmap層數 free(pixels); return textureID; } |
//initSettings()內容 //texture glActiveTexture(GL_TEXTURE2); //目前要操作2號slot texture = loadTexture("char1.png"); |
再抄一段D3D篇的過來:PNG、JPG、webP這些編碼過的格式不能給GPU使用,因為GPU讀取貼圖需要迅速找到任意位置的像素(即隨機存取,random access),這些格式必須完全解碼才能得知每個像素的值。用在GPU的壓縮格式必須設計成能隨機存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
gdk-pixbuf解碼出來的byte順序是RGBA,之後把它轉換成BGRA。R,G,B,A四個分量,每個分量8bit剛好形成一個32位元整數,四個byte的順序D3D和OpenGL可以支援RGBA(最低位元組是R)和BGRA(最低位元組是B),筆者選用BGRA的理由是大部分繪圖軟體的十六進位顏色值都是BGR,可以直接把軟體裡的值複製貼上到程式裡。
只用C/C++語法轉換byte順序有點麻煩,這裡用兩個組合語言指令做這工作:bswap和ror,
__bswapd()和_rotr()是所謂的intrinsic function,有些組合語指令沒有直接對應的C/C++語法,C/C++只能用函式的型式提供功能,這些函式編譯後會直接轉換成組合語言指令,不會產生函式呼叫。
之後建立貼圖物件,老樣子的Gen、Bind兩步驟,再用glTexImage2D()設定貼圖格式並上傳資料。參數如下:
1:target。glBindTexture()把貼圖ID設給一個叫GL_TEXTURE_2D的變數,然後glTexImage2D()讀取這個變數得知要操作哪個貼圖物件。
貼圖種類有很多,除了本篇用的2D貼圖以外,還有1D、3D、cube map、貼圖陣列等等。
2:如果有使用mipmap且建立物件時就要上傳mipmap,要用這個參數指定層數,不使用mipmap就填0。
3:在顯示記憶體裡以何種格式儲存。
4、5:寬高。
6:border。是早期版本的功能,但現在的OpenGL取消了,必須填0。
7、8:你傳進去的資料(第9參數)是什麼格式,7是有幾個顏色分量和byte順序,8是各分量是什麼型態。
9:要上傳的資料,byte數會從寬高和格式求出。
3,7,8全部可以填什麼格式要看官方文件
OpenGL wiki: GLAPI/glTexImage2D
內部格式和第7,8參數如果是不同格式,GPU會做轉換,如果不能轉換就產生error code,取得error code的方法以前提過,是用glGetError()。
內部格式可以填這篇文件裡的Base Internal Formats讓GPU自行決定各分量bit數,也可以填Sized Internal Formats指定bit數;不需要指定byte順序,GPU會看情況自行決定。
下面還有Sized Depth and Stencil Internal Formats和Compressed Internal Formats兩個表,要求GPU配置一塊depth或stencil buffer也是用這個函式,文件裡有列出壓縮格式但上傳壓縮過的貼圖要用另一個函式:glCompressedTexImage2D()。
下一行「glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0)」絕對不能少,本篇沒有用到mipmap的功能,但貼圖物件被建出來時是設定成要套用mipmap,如果沒準備好mipmap就不能使用這個物件,所以要用這一行告訴OpenGL這個貼圖不用mipmap。
(筆者有一次因為沒打這行卡了好幾個小時)
-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 glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); const float color[]={1,1,1,1}; glClearColor(color[0],color[1],color[2],color[3]); return 0; } //initSettings()結束 |
(Sc, Sa):pixel shader輸出的顏色和alpha,數值範圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
glClearColor()把背景設成白色,且把alpha設成1簡化blend的算式。如果畫面的alpha不是1,那其實D3D和OpenGL的blend設定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時做一點特別處理。
以前有寫過一篇筆記:premultiply alpha的妙用
如果檔名是usetexture.c,用這個指令build
gcc usetexture.c -o usetexture -s -Os -lX11 -lGL `pkg-config --cflags --libs gdk-pixbuf-2.0` |
執行的樣子。
uniform buffer、texture、sampler這三種物件如何將主程式和shader的物件對應,可以想成顯卡少女的工作臺有個置物櫃,有很多格子。
跟D3D11不一樣的是,OpenGL所有shader階段共用一個置物櫃,且把貼圖和sampler放在同一個格子。program物件裡有一張表把變數名稱和格子編號對應,由於OpenGL要把全部階段的shader連結成一個program才能使用,這個對應表是全部階段共用。
(註:有兩個擴充:ARB_shading_language_420pack和ARB_separate_shader_objects會改變本節介紹的規則,因為分別是4.2與4.1版才列為標準配備,本篇不介紹)
本篇的shader裡有這兩段,其中的uniform1和sampler1是變數名稱,兩階段同名的變數會視為同一個物件。
//vertex shader layout(std140) uniform uniform1{ vec2 windowSize; }; uniform sampler2D sampler1; |
//fragment shader uniform sampler2D sampler1; |
變數名稱與格子的對應,以及指示顯卡少女把物件放進格子,是在主程式裡做。
這一段是設定uniform buffer
//填對應表,將名稱"uniform1"對應到2號 uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1"); glUniformBlockBinding(programID, uniformIndex, 2); //將物件"uniformBuffer"放入2號格子 glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer); |
不知為何不用一個函式直接填入(programID, "uniform1", 2)就好,還要取得uniformIndex再用另一個函式設定,但這就是OpenGL的規定。
上面說對應表是存在program物件裡,如果有3個program物件,要呼叫3次glGetUniformBlockIndex()和glUniformBlockBinding()分別設定對應,但只要呼叫一次glBindBufferBase()就可以3個program共用這個uniform buffer。
這一段是設定sampler物件
//填對應表,將名稱"sampler1"對應到2號 int location=glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); //將物件"sampler"放入2號格子 glBindSampler(2, sampler); |
glGetUniformLocation()用第一參數指定要操作哪個program,但glUniform1i()沒有programID的參數,它用前面的glUseProgram()決定要操作的program。OpenGL有個討厭的地方是有些函式只由參數決定行為,但有些函式會讀取內部的全域變數,規則不一致。
texture物件的話,因為GLSL的sampler物件也包含貼圖,sampler部分的前兩行也同時設定texture的格子對應。
把物件放到2號格子要這樣做
//設全域變數,設定之後要操作2號格子 glActiveTexture(GL_TEXTURE2); //將物件"textureID"放入2號格子,本篇是在loadTexture()裡呼叫此 glBindTexture(GL_TEXTURE_2D, textureID); |
可以做到這樣,在vertex和fragment shader用不同的變數名稱,但是對應到相同格子。
但這會讓人寫程式混亂,同一個slot最好還是用相同名稱。
//vertex shader uniform sampler2D sampler1; |
//fragment shader uniform sampler2D sampler2; |
//主程式 int location = glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); location = glGetUniformLocation(programID, "sampler2"); glUniform1i(location, 2); |
具體有幾個格子隨硬體、驅動程式和作業系統而異,要用glGetIntegerv()查詢以下的值:
GL_MAX_VERTEX_UNIFORM_BLOCKS
GL_MAX_FRAGMENT_UNIFORM_BLOCKS
GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS
GL_MAX_TEXTURE_IMAGE_UNITS
…………
在命令列打「glxinfo -B -l」會列出目前環境中OpenGL各功能的上限,其中有這些值。
參考 OpenGL wiki: Resource limitations
OpenGL規格會要求晶片廠商支援一定數量以上,照這篇說明,3.3版至少有12格uniform buffer(編號0~11),16格sampler。
sampler物件是OpenGL 3.3版新增的東西,3.2版以前的做法是每個貼圖物件都內附一個sampler,想設定取樣方式要修改貼圖屬性,如果看OpenGL比較早版本的教學會看到這種用法。
glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); |
glTexParameter
glSamplerParameter