前篇:OpenGL 3.3 架設(shè)基本繪圖管線
前一篇還沒(méi)有使用uniform buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
整個(gè)系列可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學(xué)一覽
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個(gè)軟體做的
キャラクターなんとか機(jī) http://khmix.sakura.ne.jp/download.shtml
將視窗尺寸設(shè)得比較大以配合圖檔,且多了一些header用來(lái)讀檔案。
這次要傳給GPU的資料如下
資料跟D3D篇一樣,只是那篇使用C++而本篇使用C,純C用struct定義資料型態(tài)必須寫成typedef struct。
說(shuō)明照搬那邊的:
坐標(biāo)是這樣,右圖的ABCD是頂點(diǎn)順序。
多數(shù)繪圖軟體和函式庫(kù)以左上角為原點(diǎn),但上傳貼圖的函式glTexImage2D()規(guī)定給它的資料以左下角為原點(diǎn),所以圖在OpenGL內(nèi)部是倒立的狀態(tài)。不過(guò)OpenGL讀取貼圖也以左下角為原點(diǎn),兩次顛倒抵消,能畫出正立的圖片。
但如果用framebuffer object把圖畫在記憶體內(nèi)的點(diǎn)陣圖,再把它當(dāng)成貼圖畫在另一張點(diǎn)陣圖,就要對(duì)坐標(biāo)特別處理了。
shader這次採(cǎi)用讀外部檔的方式,由於OpenGL 3.3沒(méi)有提供事先編譯的方式,主程式要把shader原始碼載入,在執(zhí)行時(shí)編譯。
準(zhǔn)備這兩個(gè)檔案,因?yàn)镚LSL程式起點(diǎn)固定叫做main(),不能把兩個(gè)shader放在同一個(gè)檔案。
「shader的輸入與輸出」提到的輸出入在這裡出現(xiàn)了,看本篇時(shí)可以跟那篇對(duì)照著看。
shader裡面的處理方式跟D3D篇一樣,以下是從D3D篇照抄:
OpenGL 3.3雖然不能事先編譯,但有一個(gè)工具glslangValidator可以在把shader包進(jìn)主程式前檢查語(yǔ)法。Fedora要裝glslang的套件;Ubuntu要20.04版,Mint要20版以後才有這個(gè)套件,名稱是glslang-tools。
Windows的話就自己去官網(wǎng)找下載的地方。
https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/
像這樣打可檢查語(yǔ)法錯(cuò)誤
它會(huì)根據(jù)副檔名判斷這是哪一階段的shader、語(yǔ)言是GLSL還是HLSL,把檔案命名為.vert.glsl和.frag.glsl目的在此。
如果要查uniform block裡各變數(shù)的位置作為寫C struct的參考,好像必須用「glslangValidator -i 檔名」再?gòu)难e面找出offset資訊,不知道有沒(méi)有更簡(jiǎn)單的方法。
另外如果想在OpenGL比較新的版本用SPIR-V,將程式碼編譯成SPIR-V也是用這個(gè)工具。
initGL()、deinitGL和之前一樣。
glDrawArrays()第一參數(shù)的幾何形狀改成GL_TRIANGLE_STRIP,第三參數(shù)的頂點(diǎn)數(shù)量改成4。
本篇要講的東西很多,先不介紹各種幾何形狀。雖然OpenGL 3.0以後把四邊形列為deprecated,OpenGL ES也一開(kāi)始就沒(méi)支援四邊形,只有四個(gè)頂點(diǎn)的情況下用triangle strip可以畫四邊形。
要?jiǎng)h除的物件增加3個(gè)。
buffer物件有兩個(gè),這裡利用glDeleteBuffers()可以一次刪除多個(gè)物件的特性,傳入長(zhǎng)度為2的陣列。
接下來(lái)的initSettings()是主題,用到的輔助函式會(huì)在旁邊解說(shuō)。
-Vertex & Fragment Shader-
loadWholeFile()用到的Linux API函式請(qǐng)參照這篇:檔案操作—Linux篇,作業(yè)系統(tǒng)沒(méi)有直接提供「?jìng)魅霗n名→讀取整個(gè)檔案」的函式,但這功能有時(shí)候會(huì)用到,寫一個(gè)函式做這件事。
把shader程式碼讀入記憶體之後,按照「OpenGL 3.3 架設(shè)基本繪圖管線」的方法建立shader與program物件、檢查error。
-Vertex buffer & Input assembler-
建buffer和vertex array物件的方法跟「OpenGL 3.3 架設(shè)基本繪圖管線」一樣,本篇頂點(diǎn)有三項(xiàng)資料,要呼叫三次glEnableVertexAttribArray()和glVertexAttribPointer()設(shè)定格式。glVertexAttribPointer()的參數(shù)是什麼意思留待下一篇介紹。
-Uniform buffer-
本篇新增的東西之一。與vertex buffer一樣是buffer所以建立的方法很像,只是bind的第一參數(shù)換成GL_UNIFORM_BUFFER。
-Sampler-
本篇新增的東西之二。跟大部分OpenGL物件一樣用Gen函式產(chǎn)生識(shí)別碼,不過(guò)修改sampler屬性不需要先bind,因?yàn)椴僮鱯ampler的函式用第一參數(shù)指定要操作哪個(gè)sampler。
FILTER和WRAP是什麼請(qǐng)參照這篇貼圖的部分:shader的輸入與輸出,MAG_FILTER和MIN_FILTER分別設(shè)定放大和縮小要做什麼處理,WRAP_S和WRAP_T分別是貼圖的X坐標(biāo)和Y坐標(biāo),兩個(gè)方向可以用不同鋪排方式。
OpenGL wiki: Sampler Object
有幾行有標(biāo)示「//default」,sampler物件建好之後就會(huì)填入一些預(yù)設(shè)值,這幾行剛好跟預(yù)設(shè)的值相同,把這幾行拿掉執(zhí)行結(jié)果不會(huì)變。
-Texture-
本篇新增的東西之三,用到一個(gè)輔助函式loadTexture()。用一個(gè)函式庫(kù):gdk-pixbuf讀取圖檔和解碼,方法參照這篇:讀取圖檔的方法-Linux篇。
再抄一段D3D篇的過(guò)來(lái):PNG、JPG、webP這些編碼過(guò)的格式不能給GPU使用,因?yàn)镚PU讀取貼圖需要迅速找到任意位置的像素(即隨機(jī)存取,random access),這些格式必須完全解碼才能得知每個(gè)像素的值。用在GPU的壓縮格式必須設(shè)計(jì)成能隨機(jī)存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
gdk-pixbuf解碼出來(lái)的byte順序是RGBA,之後把它轉(zhuǎn)換成BGRA。R,G,B,A四個(gè)分量,每個(gè)分量8bit剛好形成一個(gè)32位元整數(shù),四個(gè)byte的順序D3D和OpenGL可以支援RGBA(最低位元組是R)和BGRA(最低位元組是B),筆者選用BGRA的理由是大部分繪圖軟體的十六進(jìn)位顏色值都是BGR,可以直接把軟體裡的值複製貼上到程式裡。
只用C/C++語(yǔ)法轉(zhuǎn)換byte順序有點(diǎn)麻煩,這裡用兩個(gè)組合語(yǔ)言指令做這工作:bswap和ror,
__bswapd()和_rotr()是所謂的intrinsic function,有些組合語(yǔ)指令沒(méi)有直接對(duì)應(yīng)的C/C++語(yǔ)法,C/C++只能用函式的型式提供功能,這些函式編譯後會(huì)直接轉(zhuǎn)換成組合語(yǔ)言指令,不會(huì)產(chǎn)生函式呼叫。
之後建立貼圖物件,老樣子的Gen、Bind兩步驟,再用glTexImage2D()設(shè)定貼圖格式並上傳資料。參數(shù)如下:
1:target。glBindTexture()把貼圖ID設(shè)給一個(gè)叫GL_TEXTURE_2D的變數(shù),然後glTexImage2D()讀取這個(gè)變數(shù)得知要操作哪個(gè)貼圖物件。
貼圖種類有很多,除了本篇用的2D貼圖以外,還有1D、3D、cube map、貼圖陣列等等。
2:如果有使用mipmap且建立物件時(shí)就要上傳mipmap,要用這個(gè)參數(shù)指定層數(shù),不使用mipmap就填0。
3:在顯示記憶體裡以何種格式儲(chǔ)存。
4、5:寬高。
6:border。是早期版本的功能,但現(xiàn)在的OpenGL取消了,必須填0。
7、8:你傳進(jìn)去的資料(第9參數(shù))是什麼格式,7是有幾個(gè)顏色分量和byte順序,8是各分量是什麼型態(tài)。
9:要上傳的資料,byte數(shù)會(huì)從寬高和格式求出。
3,7,8全部可以填什麼格式要看官方文件
OpenGL wiki: GLAPI/glTexImage2D
內(nèi)部格式和第7,8參數(shù)如果是不同格式,GPU會(huì)做轉(zhuǎn)換,如果不能轉(zhuǎn)換就產(chǎn)生error code,取得error code的方法以前提過(guò),是用glGetError()。
內(nèi)部格式可以填這篇文件裡的Base Internal Formats讓GPU自行決定各分量bit數(shù),也可以填Sized Internal Formats指定bit數(shù);不需要指定byte順序,GPU會(huì)看情況自行決定。
下面還有Sized Depth and Stencil Internal Formats和Compressed Internal Formats兩個(gè)表,要求GPU配置一塊depth或stencil buffer也是用這個(gè)函式,文件裡有列出壓縮格式但上傳壓縮過(guò)的貼圖要用另一個(gè)函式:glCompressedTexImage2D()。
下一行「glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0)」絕對(duì)不能少,本篇沒(méi)有用到mipmap的功能,但貼圖物件被建出來(lái)時(shí)是設(shè)定成要套用mipmap,如果沒(méi)準(zhǔn)備好mipmap就不能使用這個(gè)物件,所以要用這一行告訴OpenGL這個(gè)貼圖不用mipmap。
(筆者有一次因?yàn)闆](méi)打這行卡了好幾個(gè)小時(shí))
-Rasterizer, depth, stencil, and blend-
rasterizer、depth和stencil跟以前一樣,只有blend換了算式,改成正式繪圖軟體使用的。
(Sc, Sa):pixel shader輸出的顏色和alpha,數(shù)值範(fàn)圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
glClearColor()把背景設(shè)成白色,且把a(bǔ)lpha設(shè)成1簡(jiǎn)化blend的算式。如果畫面的alpha不是1,那其實(shí)D3D和OpenGL的blend設(shè)定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時(shí)做一點(diǎn)特別處理。
以前有寫過(guò)一篇筆記:premultiply alpha的妙用
如果檔名是usetexture.c,用這個(gè)指令build
比上次多了`pkg-config --cflags --libs gdk-pixbuf-2.0`是因?yàn)橛胓dk-pixbuf讀取圖檔。
執(zhí)行的樣子。
uniform buffer、texture、sampler這三種物件如何將主程式和shader的物件對(duì)應(yīng),可以想成顯卡少女的工作臺(tái)有個(gè)置物櫃,有很多格子。
跟D3D11不一樣的是,OpenGL所有shader階段共用一個(gè)置物櫃,且把貼圖和sampler放在同一個(gè)格子。program物件裡有一張表把變數(shù)名稱和格子編號(hào)對(duì)應(yīng),由於OpenGL要把全部階段的shader連結(jié)成一個(gè)program才能使用,這個(gè)對(duì)應(yīng)表是全部階段共用。
(註:有兩個(gè)擴(kuò)充:ARB_shading_language_420pack和ARB_separate_shader_objects會(huì)改變本節(jié)介紹的規(guī)則,因?yàn)榉謩e是4.2與4.1版才列為標(biāo)準(zhǔn)配備,本篇不介紹)
本篇的shader裡有這兩段,其中的uniform1和sampler1是變數(shù)名稱,兩階段同名的變數(shù)會(huì)視為同一個(gè)物件。
link之後program會(huì)記住裡面有"uniform1"和"sampler1"兩個(gè)變數(shù),留待之後對(duì)應(yīng)到格子。
變數(shù)名稱與格子的對(duì)應(yīng),以及指示顯卡少女把物件放進(jìn)格子,是在主程式裡做。
這一段是設(shè)定uniform buffer
格子的索引是從0開(kāi)始,本篇故意用2。
不知為何不用一個(gè)函式直接填入(programID, "uniform1", 2)就好,還要取得uniformIndex再用另一個(gè)函式設(shè)定,但這就是OpenGL的規(guī)定。
上面說(shuō)對(duì)應(yīng)表是存在program物件裡,如果有3個(gè)program物件,要呼叫3次glGetUniformBlockIndex()和glUniformBlockBinding()分別設(shè)定對(duì)應(yīng),但只要呼叫一次glBindBufferBase()就可以3個(gè)program共用這個(gè)uniform buffer。
這一段是設(shè)定sampler物件
同樣要用一個(gè)暫時(shí)變數(shù)location。
glGetUniformLocation()用第一參數(shù)指定要操作哪個(gè)program,但glUniform1i()沒(méi)有programID的參數(shù),它用前面的glUseProgram()決定要操作的program。OpenGL有個(gè)討厭的地方是有些函式只由參數(shù)決定行為,但有些函式會(huì)讀取內(nèi)部的全域變數(shù),規(guī)則不一致。
texture物件的話,因?yàn)镚LSL的sampler物件也包含貼圖,sampler部分的前兩行也同時(shí)設(shè)定texture的格子對(duì)應(yīng)。
把物件放到2號(hào)格子要這樣做
格子編號(hào)不是填數(shù)字,而是用常數(shù)GL_TEXTURE0、GL_TEXTURE1……,這些是連續(xù)整數(shù),所以也可以用「GL_TEXTURE0+i」的方式。
可以做到這樣,在vertex和fragment shader用不同的變數(shù)名稱,但是對(duì)應(yīng)到相同格子。
但這會(huì)讓人寫程式混亂,同一個(gè)slot最好還是用相同名稱。
具體有幾個(gè)格子隨硬體、驅(qū)動(dòng)程式和作業(yè)系統(tǒng)而異,要用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」會(huì)列出目前環(huán)境中OpenGL各功能的上限,其中有這些值。
參考 OpenGL wiki: Resource limitations
OpenGL規(guī)格會(huì)要求晶片廠商支援一定數(shù)量以上,照這篇說(shuō)明,3.3版至少有12格uniform buffer(編號(hào)0~11),16格sampler。
sampler物件是OpenGL 3.3版新增的東西,3.2版以前的做法是每個(gè)貼圖物件都內(nèi)附一個(gè)sampler,想設(shè)定取樣方式要修改貼圖屬性,如果看OpenGL比較早版本的教學(xué)會(huì)看到這種用法。
查這兩個(gè)函式的說(shuō)明會(huì)看到很多相同的設(shè)定值
glTexParameter
glSamplerParameter
前一篇還沒(méi)有使用uniform buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。
整個(gè)系列可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學(xué)一覽
本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。
用這個(gè)軟體做的
キャラクターなんとか機(jī) 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狀態(tài) 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的資料在下面說(shuō)明 //這些函式在下面說(shuō)明 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); //這個(gè)函式留待之後介紹layout時(shí)解說(shuō) 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; } //設(shè)標(biāo)題 XStoreName(dsp, window, "simplepipeline"); //設(shè)定事件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; } } //正式寫遊戲時(shí),遊戲邏輯放在此處 nextFrame(); //更新畫面 clock_gettime(CLOCK_MONOTONIC, &nextTime); //單位為奈秒(10^-9秒) int64_t elapsedTime = (nextTime.tv_sec-prevTime.tv_sec)*1000000000 +(nextTime.tv_nsec-prevTime.tv_nsec); //求出經(jīng)過(guò)的奈秒數(shù) 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}; //頂點(diǎn)資料 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}, }; |
說(shuō)明照搬那邊的:
VERTEX_DATA1和VERTEX_DATA2是相同頂點(diǎn)資料用不同layout儲(chǔ)存,本篇只用1,2等之後寫layout的教學(xué)再使用。
這次頂點(diǎn)資料有三項(xiàng):位置、貼圖坐標(biāo)、顏色,之前說(shuō)過(guò)D3D和OpenGL的畫面坐標(biāo)是-1~1、貼圖坐標(biāo)是0~1,但2D畫面習(xí)慣上以畫面左上角為原點(diǎn)、以像素為單位,這裡頂點(diǎn)資料就這樣給,在shader裡換算成D3D和OpenGL標(biāo)準(zhǔn)。
雖然本篇故意在vertex shader裡算,這裡只有4個(gè)頂點(diǎn),在CPU把4個(gè)頂點(diǎn)全部算好才傳給GPU其實(shí)效能不會(huì)差多少,但如果一次畫很多個(gè)三角形(如tilemap和粒子系統(tǒng)),在vertex shader裡算比較方便。
坐標(biāo)是這樣,右圖的ABCD是頂點(diǎn)順序。
多數(shù)繪圖軟體和函式庫(kù)以左上角為原點(diǎn),但上傳貼圖的函式glTexImage2D()規(guī)定給它的資料以左下角為原點(diǎn),所以圖在OpenGL內(nèi)部是倒立的狀態(tài)。不過(guò)OpenGL讀取貼圖也以左下角為原點(diǎn),兩次顛倒抵消,能畫出正立的圖片。
但如果用framebuffer object把圖畫在記憶體內(nèi)的點(diǎn)陣圖,再把它當(dāng)成貼圖畫在另一張點(diǎn)陣圖,就要對(duì)坐標(biāo)特別處理了。
shader這次採(cǎi)用讀外部檔的方式,由於OpenGL 3.3沒(méi)有提供事先編譯的方式,主程式要把shader原始碼載入,在執(zhí)行時(shí)編譯。
準(zhǔn)備這兩個(gè)檔案,因?yàn)镚LSL程式起點(diǎn)固定叫做main(),不能把兩個(gè)shader放在同一個(gè)檔案。
//usetexture.vert.glsl #version 330 layout(std140) uniform uniform1{ vec2 windowSize; }; uniform sampler2D sampler1; //GLSL的sampler物件包含sampler和貼圖 //頂點(diǎn)資料,對(duì)應(yīng)到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坐標(biāo)轉(zhuǎn)換的算式怎麼求出,回想一下學(xué)校學(xué)過(guò)的二元一次方程式。
畫面坐標(biāo)要把左邊的換算成右邊
二元一次方程式的標(biāo)準(zhǔn)式「x'=xa+b, y'=yc+d」,把兩組坐標(biāo)代進(jìn)去,得到兩組聯(lián)立方程式
X坐標(biāo)
-1 = 0 + b
1 = windowW×a + bY坐標(biāo)
1 = 0 + d
-1 = windowH×c + d
解出a,b,c,d如下
x' =( 2/windowW)x - 1
y' =(-2/windowH)y + 1
因?yàn)閟hader可以一次計(jì)算四維向量,可以寫成這樣
outPos = inPos×(2,-2)/(windowW, windowH) + (-1,1)
貼圖坐標(biāo)只要除以貼圖寬高即可,內(nèi)建函式textureSize()可以取得貼圖寬高。
顏色就原封不動(dòng)傳給rasterizer內(nèi)插。
fragment shader讀取貼圖裡的像素,跟頂點(diǎn)資料裡的顏色相乘。
OpenGL 3.3雖然不能事先編譯,但有一個(gè)工具glslangValidator可以在把shader包進(jìn)主程式前檢查語(yǔ)法。Fedora要裝glslang的套件;Ubuntu要20.04版,Mint要20版以後才有這個(gè)套件,名稱是glslang-tools。
Windows的話就自己去官網(wǎng)找下載的地方。
https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/
像這樣打可檢查語(yǔ)法錯(cuò)誤
glslangValidator usetexture.vert.glsl glslangValidator usetexture.frag.glsl |
如果要查uniform block裡各變數(shù)的位置作為寫C struct的參考,好像必須用「glslangValidator -i 檔名」再?gòu)难e面找出offset資訊,不知道有沒(méi)有更簡(jiǎn)單的方法。
另外如果想在OpenGL比較新的版本用SPIR-V,將程式碼編譯成SPIR-V也是用這個(gè)工具。
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; } //產(chǎn)生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); //設(shè)定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也一開(kāi)始就沒(méi)支援四邊形,只有四個(gè)頂點(diǎn)的情況下用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物件有兩個(gè),這裡利用glDeleteBuffers()可以一次刪除多個(gè)物件的特性,傳入長(zhǎng)度為2的陣列。
接下來(lái)的initSettings()是主題,用到的輔助函式會(huì)在旁邊解說(shuō)。
-Vertex & Fragment Shader-
//傳回來(lái)的指標(biāo)要用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錯(cuò)誤 int ret; glGetShaderiv(shaderID, GL_COMPILE_STATUS, &ret); //ret=0代表有錯(cuò)誤 if(!ret){ int infoLen; glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLen); //取得訊息長(zhǎng)度 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); //之後不會(huì)再用到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 架設(shè)基本繪圖管線」的方法建立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()內(nèi)容 //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); //將這個(gè)物件與shader裡的buffer物件對(duì)應(yīng) 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 //將這個(gè)物件與shader裡的sampler物件對(duì)應(yīng) int location=glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); glBindSampler(2, sampler); |
FILTER和WRAP是什麼請(qǐng)參照這篇貼圖的部分:shader的輸入與輸出,MAG_FILTER和MIN_FILTER分別設(shè)定放大和縮小要做什麼處理,WRAP_S和WRAP_T分別是貼圖的X坐標(biāo)和Y坐標(biāo),兩個(gè)方向可以用不同鋪排方式。
OpenGL wiki: Sampler Object
有幾行有標(biāo)示「//default」,sampler物件建好之後就會(huì)填入一些預(yù)設(shè)值,這幾行剛好跟預(yù)設(shè)的值相同,把這幾行拿掉執(zhí)行結(jié)果不會(huì)變。
-Texture-
//讀取圖檔、解碼、產(chǎn)生texture物件 //成功傳回OpenGL物件識(shí)別碼,失敗傳回0 static uint32_t loadTexture(const char* fileName){ //用gdk-pixbuf讀取圖檔,把RGBA值存在一個(gè)叫pixels的變數(shù) 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轉(zhuǎn)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); //設(shè)mipmap層數(shù) free(pixels); return textureID; } |
//initSettings()內(nèi)容 //texture glActiveTexture(GL_TEXTURE2); //目前要操作2號(hào)slot texture = loadTexture("char1.png"); |
再抄一段D3D篇的過(guò)來(lái):PNG、JPG、webP這些編碼過(guò)的格式不能給GPU使用,因?yàn)镚PU讀取貼圖需要迅速找到任意位置的像素(即隨機(jī)存取,random access),這些格式必須完全解碼才能得知每個(gè)像素的值。用在GPU的壓縮格式必須設(shè)計(jì)成能隨機(jī)存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。
gdk-pixbuf解碼出來(lái)的byte順序是RGBA,之後把它轉(zhuǎn)換成BGRA。R,G,B,A四個(gè)分量,每個(gè)分量8bit剛好形成一個(gè)32位元整數(shù),四個(gè)byte的順序D3D和OpenGL可以支援RGBA(最低位元組是R)和BGRA(最低位元組是B),筆者選用BGRA的理由是大部分繪圖軟體的十六進(jìn)位顏色值都是BGR,可以直接把軟體裡的值複製貼上到程式裡。
只用C/C++語(yǔ)法轉(zhuǎn)換byte順序有點(diǎn)麻煩,這裡用兩個(gè)組合語(yǔ)言指令做這工作:bswap和ror,
__bswapd()和_rotr()是所謂的intrinsic function,有些組合語(yǔ)指令沒(méi)有直接對(duì)應(yīng)的C/C++語(yǔ)法,C/C++只能用函式的型式提供功能,這些函式編譯後會(huì)直接轉(zhuǎn)換成組合語(yǔ)言指令,不會(huì)產(chǎn)生函式呼叫。
之後建立貼圖物件,老樣子的Gen、Bind兩步驟,再用glTexImage2D()設(shè)定貼圖格式並上傳資料。參數(shù)如下:
1:target。glBindTexture()把貼圖ID設(shè)給一個(gè)叫GL_TEXTURE_2D的變數(shù),然後glTexImage2D()讀取這個(gè)變數(shù)得知要操作哪個(gè)貼圖物件。
貼圖種類有很多,除了本篇用的2D貼圖以外,還有1D、3D、cube map、貼圖陣列等等。
2:如果有使用mipmap且建立物件時(shí)就要上傳mipmap,要用這個(gè)參數(shù)指定層數(shù),不使用mipmap就填0。
3:在顯示記憶體裡以何種格式儲(chǔ)存。
4、5:寬高。
6:border。是早期版本的功能,但現(xiàn)在的OpenGL取消了,必須填0。
7、8:你傳進(jìn)去的資料(第9參數(shù))是什麼格式,7是有幾個(gè)顏色分量和byte順序,8是各分量是什麼型態(tài)。
9:要上傳的資料,byte數(shù)會(huì)從寬高和格式求出。
3,7,8全部可以填什麼格式要看官方文件
OpenGL wiki: GLAPI/glTexImage2D
內(nèi)部格式和第7,8參數(shù)如果是不同格式,GPU會(huì)做轉(zhuǎn)換,如果不能轉(zhuǎn)換就產(chǎn)生error code,取得error code的方法以前提過(guò),是用glGetError()。
內(nèi)部格式可以填這篇文件裡的Base Internal Formats讓GPU自行決定各分量bit數(shù),也可以填Sized Internal Formats指定bit數(shù);不需要指定byte順序,GPU會(huì)看情況自行決定。
下面還有Sized Depth and Stencil Internal Formats和Compressed Internal Formats兩個(gè)表,要求GPU配置一塊depth或stencil buffer也是用這個(gè)函式,文件裡有列出壓縮格式但上傳壓縮過(guò)的貼圖要用另一個(gè)函式:glCompressedTexImage2D()。
下一行「glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0)」絕對(duì)不能少,本篇沒(méi)有用到mipmap的功能,但貼圖物件被建出來(lái)時(shí)是設(shè)定成要套用mipmap,如果沒(méi)準(zhǔn)備好mipmap就不能使用這個(gè)物件,所以要用這一行告訴OpenGL這個(gè)貼圖不用mipmap。
(筆者有一次因?yàn)闆](méi)打這行卡了好幾個(gè)小時(shí))
-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()結(jié)束 |
(Sc, Sa):pixel shader輸出的顏色和alpha,數(shù)值範(fàn)圍0~1
(Dc, Da):畫面上的顏色和alpha
color = ScSa + Dc(1-Sa)
alpha = screen(Sa, Da) = Sa + Da(1-Sa)
screen()是圖層模式的濾色模式
glClearColor()把背景設(shè)成白色,且把a(bǔ)lpha設(shè)成1簡(jiǎn)化blend的算式。如果畫面的alpha不是1,那其實(shí)D3D和OpenGL的blend設(shè)定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時(shí)做一點(diǎn)特別處理。
以前有寫過(guò)一篇筆記:premultiply alpha的妙用
如果檔名是usetexture.c,用這個(gè)指令build
gcc usetexture.c -o usetexture -s -Os -lX11 -lGL `pkg-config --cflags --libs gdk-pixbuf-2.0` |
執(zhí)行的樣子。
uniform buffer、texture、sampler這三種物件如何將主程式和shader的物件對(duì)應(yīng),可以想成顯卡少女的工作臺(tái)有個(gè)置物櫃,有很多格子。
跟D3D11不一樣的是,OpenGL所有shader階段共用一個(gè)置物櫃,且把貼圖和sampler放在同一個(gè)格子。program物件裡有一張表把變數(shù)名稱和格子編號(hào)對(duì)應(yīng),由於OpenGL要把全部階段的shader連結(jié)成一個(gè)program才能使用,這個(gè)對(duì)應(yīng)表是全部階段共用。
(註:有兩個(gè)擴(kuò)充:ARB_shading_language_420pack和ARB_separate_shader_objects會(huì)改變本節(jié)介紹的規(guī)則,因?yàn)榉謩e是4.2與4.1版才列為標(biāo)準(zhǔn)配備,本篇不介紹)
本篇的shader裡有這兩段,其中的uniform1和sampler1是變數(shù)名稱,兩階段同名的變數(shù)會(huì)視為同一個(gè)物件。
//vertex shader layout(std140) uniform uniform1{ vec2 windowSize; }; uniform sampler2D sampler1; |
//fragment shader uniform sampler2D sampler1; |
變數(shù)名稱與格子的對(duì)應(yīng),以及指示顯卡少女把物件放進(jìn)格子,是在主程式裡做。
這一段是設(shè)定uniform buffer
//填對(duì)應(yīng)表,將名稱"uniform1"對(duì)應(yīng)到2號(hào) uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1"); glUniformBlockBinding(programID, uniformIndex, 2); //將物件"uniformBuffer"放入2號(hào)格子 glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer); |
不知為何不用一個(gè)函式直接填入(programID, "uniform1", 2)就好,還要取得uniformIndex再用另一個(gè)函式設(shè)定,但這就是OpenGL的規(guī)定。
上面說(shuō)對(duì)應(yīng)表是存在program物件裡,如果有3個(gè)program物件,要呼叫3次glGetUniformBlockIndex()和glUniformBlockBinding()分別設(shè)定對(duì)應(yīng),但只要呼叫一次glBindBufferBase()就可以3個(gè)program共用這個(gè)uniform buffer。
這一段是設(shè)定sampler物件
//填對(duì)應(yīng)表,將名稱"sampler1"對(duì)應(yīng)到2號(hào) int location=glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); //將物件"sampler"放入2號(hào)格子 glBindSampler(2, sampler); |
glGetUniformLocation()用第一參數(shù)指定要操作哪個(gè)program,但glUniform1i()沒(méi)有programID的參數(shù),它用前面的glUseProgram()決定要操作的program。OpenGL有個(gè)討厭的地方是有些函式只由參數(shù)決定行為,但有些函式會(huì)讀取內(nèi)部的全域變數(shù),規(guī)則不一致。
texture物件的話,因?yàn)镚LSL的sampler物件也包含貼圖,sampler部分的前兩行也同時(shí)設(shè)定texture的格子對(duì)應(yīng)。
把物件放到2號(hào)格子要這樣做
//設(shè)全域變數(shù),設(shè)定之後要操作2號(hào)格子 glActiveTexture(GL_TEXTURE2); //將物件"textureID"放入2號(hào)格子,本篇是在loadTexture()裡呼叫此 glBindTexture(GL_TEXTURE_2D, textureID); |
可以做到這樣,在vertex和fragment shader用不同的變數(shù)名稱,但是對(duì)應(yīng)到相同格子。
但這會(huì)讓人寫程式混亂,同一個(gè)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); |
具體有幾個(gè)格子隨硬體、驅(qū)動(dòng)程式和作業(yè)系統(tǒng)而異,要用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」會(huì)列出目前環(huán)境中OpenGL各功能的上限,其中有這些值。
參考 OpenGL wiki: Resource limitations
OpenGL規(guī)格會(huì)要求晶片廠商支援一定數(shù)量以上,照這篇說(shuō)明,3.3版至少有12格uniform buffer(編號(hào)0~11),16格sampler。
sampler物件是OpenGL 3.3版新增的東西,3.2版以前的做法是每個(gè)貼圖物件都內(nèi)附一個(gè)sampler,想設(shè)定取樣方式要修改貼圖屬性,如果看OpenGL比較早版本的教學(xué)會(huì)看到這種用法。
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