背景
如果遇到什么錯(cuò)誤請(qǐng)?jiān)诒疚闹赋觯?a href="http://www.reibang.com/p/4710b707e3ae" target="_blank">http://www.reibang.com/p/4710b707e3ae
為什么學(xué)習(xí)OpenGL瓦宜,在啟動(dòng)篇中已經(jīng)說的很清楚。實(shí)際上OpenGL實(shí)際上很多顯卡廠商根據(jù)這一套規(guī)則對(duì)接上OpenGL的api,開放給各大系統(tǒng)調(diào)用api通過顯卡指令繪制到屏幕上渤闷。也是這個(gè)原因惶桐,OpenGL實(shí)際是一個(gè)客戶端-服務(wù)端的經(jīng)典C/S交互模式。
本文將會(huì)在Mac上實(shí)現(xiàn)OpenGL的代碼淀歇。這里就不詳細(xì)講解易核,如何安裝OpenGL在Mac OS上的環(huán)境。
我是根據(jù)這篇文章搭建的環(huán)境:http://www.reibang.com/p/891d630e30af
正文
為了能夠清晰明了OpenGL的基本編程流程浪默。我先從創(chuàng)建一個(gè)窗體開始牡直。
創(chuàng)建一個(gè)窗體
創(chuàng)建窗體大致分為如下幾個(gè)步驟:
- 1.初始化OpenGL中g(shù)lfw的版本
- 2.創(chuàng)建一個(gè)GLFWwindow對(duì)象,并且設(shè)置為上下文中的主窗體纳决,并且設(shè)置窗口變化回調(diào)
- 3.創(chuàng)建一個(gè)事件循環(huán)碰逸,該循環(huán)是用來顯示窗體。
初始化glfw
首先先了解什么GLFW阔加。
GLFW是一個(gè)專門針對(duì)OpenGL的C語言庫饵史,它提供了一些渲染物體所需的最低限度的接口
很多時(shí)候,我們也是借助GLFW進(jìn)行進(jìn)行一些基礎(chǔ)渲染操作胜榔。
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//如果是mac的操作系統(tǒng)需要加上這一段
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
同時(shí)設(shè)置當(dāng)前glfw的主版本號(hào)為3胳喷,副版本號(hào)為3,glfw的模式為核心模式夭织。
創(chuàng)建一個(gè)窗口
GLFWwindow *window = glfwCreateWindow(800, 600, "Learn opengl", NULL, NULL);
if(!window){
cout <<"fail open window"<<endl;
glfwTerminate();
return -1;
}
//把這個(gè)窗口作為當(dāng)前線程主要上下文
glfwMakeContextCurrent(window);
//GLAD是用來管理OpenGL的函數(shù)指針的,需要初始化
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
cout<< "failed to init glad" <<endl;
}
//我們還要告訴opengl渲染的窗口大小
//渲染可以比window小吭露,這樣就只會(huì)在window內(nèi)部一個(gè)小窗口渲染
glViewport(0,0,800,600);
藏著我們能夠看到,全局將會(huì)創(chuàng)建GLFWwindow窗口作為整個(gè)線程上下文尊惰。
這里面有出現(xiàn)了一個(gè)新的對(duì)象GLAD讲竿。這里稍微介紹一下glad。
因?yàn)镺penGL只是一個(gè)標(biāo)準(zhǔn)/規(guī)范弄屡,具體的實(shí)現(xiàn)是由驅(qū)動(dòng)開發(fā)商針對(duì)特定顯卡實(shí)現(xiàn)的题禀。由于OpenGL驅(qū)動(dòng)版本眾多,它大多數(shù)函數(shù)的位置都無法在編譯時(shí)確定下來琢岩,需要在運(yùn)行時(shí)查詢投剥。所以任務(wù)就落在了開發(fā)者身上,開發(fā)者需要在運(yùn)行時(shí)獲取函數(shù)地址并將其保存在一個(gè)函數(shù)指針中供以后使用
GLAD是一個(gè)開源的庫担孔,它能解決我們上面提到的那個(gè)繁瑣的問題
如果想要窗體能夠根據(jù)拉動(dòng)變化
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
void framebuffer_size_callback(GLFWwindow *window,int width,int height){
glViewport(0,0,width,height);
}
創(chuàng)建一個(gè)事件循環(huán)江锨,用來處理具體顯示邏輯
就算我們不知道這個(gè)事件循環(huán)該如何實(shí)現(xiàn)吃警,但是我們閱讀這么源碼,就能知道啄育,像這種事件都會(huì)有一個(gè)核心的Looper處理事件酌心。
//并不希望智慧之一個(gè)圖像之后,進(jìn)程就退出挑豌。因此可以在主動(dòng)關(guān)閉之前接受用戶輸入
//判斷當(dāng)前窗口是否被要求退出安券。
while(!glfwWindowShouldClose(window)){
processInput(window);
//交換顏色緩沖,他是一個(gè)存儲(chǔ)著GLFW窗口每一個(gè)像素顏色值的大緩沖
//會(huì)在這個(gè)迭代中用來繪制氓英,并且顯示在屏幕上
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
//檢查有沒有觸發(fā)事件侯勉,鍵盤輸入,鼠標(biāo)移動(dòng)
glfwPollEvents();
}
//雙緩沖體系
//應(yīng)用使用單緩沖繪圖會(huì)造成圖像閃爍問題铝阐。因?yàn)閳D像不是一下子被繪制出來
//而是按照從左到右址貌,從上到下逐個(gè)像素繪制出來。最終圖像不是在瞬間顯示給用戶
//而是一步步生成徘键,這導(dǎo)致渲染結(jié)果布政使练对。
//為了規(guī)避這些問題,我們使用雙緩沖渲染創(chuàng)酷應(yīng)用吹害。前緩沖保存著最終輸出圖像螟凭,顯示在屏幕
//而所有的渲染指令都會(huì)在后緩沖上繪制,當(dāng)所喲肚餓渲染指令執(zhí)行完畢之后它呀,
//我們交換前后緩沖螺男,這樣圖像就顯示出來。
glfwTerminate();
glfwWindowShouldClose代表著每一次循環(huán)之前都會(huì)判斷一次glfw的窗口是否要求被退出钟些,一旦判斷為true則推出烟号,調(diào)用glfwTerminate,結(jié)束進(jìn)程政恍。
processInput這個(gè)方法如下:
void processInput(GLFWwindow *window){
//glfwGetKey確認(rèn)這個(gè)窗口有沒有處理按鍵
//GLFW_KEY_ESCAPE 代表esc按鍵
//GLFW_PRESS代表按下 GLFW_RELEASE 代表沒按下
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){
//關(guān)閉窗口
glfwSetWindowShouldClose(window, true);
}
}
該方法實(shí)際上是監(jiān)聽窗口有沒有按下esc按鍵汪拥。
glfwPollEvents在檢查有沒有觸發(fā)什么事件(比如鍵盤輸入、鼠標(biāo)移動(dòng)等)篙耗、更新窗口狀態(tài)迫筑,并調(diào)用對(duì)應(yīng)的回調(diào)函數(shù)(可以通過回調(diào)方法手動(dòng)設(shè)置)。
glfwSwapBuffers函數(shù)會(huì)交換顏色緩沖(它是一個(gè)儲(chǔ)存著GLFW窗口每一個(gè)像素顏色值的大緩沖)宗弯,它在這一迭代中被用來繪制脯燃,并且將會(huì)作為輸出顯示在屏幕上。
之所以使用glfwSwapBuffers蒙保,是因?yàn)槭褂眠@里使用了雙緩沖技術(shù)辕棚。因?yàn)镺penGL本質(zhì)上是從屏幕的左到右,從上到下,逐個(gè)像素點(diǎn)繪制的逝嚎,那么就會(huì)造成圖像閃爍問題扁瓢。為了規(guī)避這個(gè)問題,我們應(yīng)該使用雙緩沖繪制圖像补君,前緩沖將會(huì)保存著最終的圖像引几,并且在屏幕上顯示。所有的緩沖的指令都會(huì)在后緩沖中繪制挽铁,當(dāng)后緩沖所有的渲染指令都完成伟桅,將會(huì)做一次前后緩沖交換,這樣就顯示了后緩沖的圖像叽掘。
這種思路也會(huì)一直沿用到各大操作系統(tǒng)的顯示中楣铁。
繪制一個(gè)三角形
著色器
當(dāng)我們繪制一個(gè)圖形在上面的事件循環(huán)。試著思考一下够掠,當(dāng)我們嘗試著繪制一個(gè)圖形需要經(jīng)歷什么步驟民褂?
- 1.準(zhǔn)備好一些頂點(diǎn)
- 2.把這些頂點(diǎn)通過某種方式連接起來
- 3.再上色
大致上分為這么幾步驟。但是實(shí)際上疯潭,僅僅提供幾個(gè)api是無法很好處理這個(gè)豐富多彩的世界,需要更加靈活的方式進(jìn)行繪制面殖。
在OpenGL中竖哩,任何事物都在3D空間中,而屏幕和窗口卻是2D像素?cái)?shù)組脊僚,這導(dǎo)致OpenGL的大部分工作都是關(guān)于把3D坐標(biāo)轉(zhuǎn)變?yōu)檫m應(yīng)你屏幕的2D像素相叁。這個(gè)從3d往2d坐標(biāo)系變化的工作稱為OpenGL的圖形渲染管道。
圖形渲染管道實(shí)際上指一堆原始圖形數(shù)據(jù)途徑一個(gè)輸送管道辽幌,期間經(jīng)過各種變化處理最后輸出到屏幕上增淹。
因此OpenGL在繪制屏幕的時(shí)候。
- 1.會(huì)先綁定頂點(diǎn)
- 2.綁定緩沖區(qū)
- 3.接著把頂點(diǎn)輸入到頂點(diǎn)緩沖去中
- 4.把緩沖區(qū)的數(shù)據(jù)傳送到著色器中
- 5.輸出到屏幕乌企。
這里就衍生出一個(gè)新的概念虑润,著色器。
圖形渲染管線接受一組3D坐標(biāo)加酵,然后把它們轉(zhuǎn)變?yōu)槟闫聊簧系挠猩?D像素輸出拳喻。圖形渲染管線可以被劃分為幾個(gè)階段,每個(gè)階段將會(huì)把前一個(gè)階段的輸出作為輸入猪腕。所有這些階段都是高度專門化的(它們都有一個(gè)特定的函數(shù))冗澈,并且很容易并行執(zhí)行。正是由于它們具有并行執(zhí)行的特性陋葡,當(dāng)今大多數(shù)顯卡都有成千上萬的小處理核心亚亲,它們?cè)贕PU上為每一個(gè)(渲染管線)階段運(yùn)行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)。這些小程序叫做著色器(Shader)
這些著色器本身擁有自己的語言:GLSL捌归。開發(fā)者能夠通過這種語言高度定制OpenGL每個(gè)處理階段(著色器)本身的邏輯肛响。
因此,我們能夠把OpenGL看成一個(gè)著色器的編譯器陨溅。
接下來介紹一下OpenGL本身存在的幾個(gè)基本著色階段:
還有可以有更加復(fù)雜的著色器階段:
稍微介紹一下每個(gè)階段著色器究竟做了什么终惑。
頂點(diǎn)著色器
對(duì)于繪制命令傳輸?shù)拿總€(gè)頂點(diǎn),opengl都會(huì)調(diào)用一個(gè)頂點(diǎn)著色器门扇。根據(jù)光柵化之前著色器是否活躍雹有,著色器可能會(huì)十分簡單。比如將數(shù)據(jù)復(fù)制并傳遞到下一個(gè)著色階段臼寄,叫做傳遞著色器霸奕。他也可能十分復(fù)雜,需要大量計(jì)算來得到頂點(diǎn)在屏幕上的位置吉拳,或者通過光照計(jì)算來判斷頂點(diǎn)的顏色质帅。
通常的,一個(gè)復(fù)雜的程序可能包括許多頂點(diǎn)著色器留攒,但是同一時(shí)刻只有一個(gè)頂點(diǎn)著色器起作用
細(xì)分著色
頂點(diǎn)著色處理每個(gè)頂點(diǎn)的關(guān)聯(lián)數(shù)據(jù)之后煤惩,如果同時(shí)激活了細(xì)分著色器,那么他將進(jìn)一步處理這些數(shù)據(jù)炼邀。比如魄揉,細(xì)分著色器會(huì)使用path來描述物體形狀,并使用相對(duì)簡單的patch幾何體聯(lián)機(jī)來完成細(xì)分工作拭宁,其結(jié)果是幾何圖元的數(shù)量增加洛退。并且模型的外觀變得更加平滑。細(xì)分著色階段會(huì)用到兩個(gè)著色器來分別管理patch數(shù)據(jù)并且最終生成最終形狀杰标。
幾何著色
下一個(gè)著色階段兵怯,--幾何著色,允許在光柵化之前對(duì)每個(gè)幾何圖元做更進(jìn)一步的處理腔剂,如創(chuàng)建新的圖元媒区。這額階段是可選。
圖元裝配
前面介紹的著色階段所處理的是頂點(diǎn)數(shù)據(jù)桶蝎,此外這些頂點(diǎn)之間如何構(gòu)成幾何圖元的所有信息都會(huì)被傳遞到opengl驻仅。圖元裝配階段將這些頂點(diǎn)與相關(guān)的幾何圖元之間組織起來,準(zhǔn)備下一個(gè)的光柵化登渣。
剪切
頂點(diǎn)可能落在視口外噪服,也就是我們進(jìn)行繪制的區(qū)域。此時(shí)與頂點(diǎn)相關(guān)的圖元做出改動(dòng)胜茧,保證相關(guān)的像素不再視口外繪制粘优。由opengl自己完成仇味。
光柵化
裁剪玩之后馬上要執(zhí)行的工作,就是將更新之后的圖元傳遞到光柵單元生成對(duì)應(yīng)的片元雹顺。我們可以將一個(gè)片元視為一個(gè)候選的像素丹墨,也就是說可以放置到幀緩存中的像素,但是他也有可能被剔除嬉愧,不更新對(duì)應(yīng)位置的像素贩挣。
片元著色
最后一個(gè)可以通過編程控制屏幕上顯示顏色的階段,叫做片元著色階段没酣,在這個(gè)階段我們使用著色器來計(jì)算片元的最終顏色和它的深度值王财。
片元著色器十分強(qiáng)大,在這里我們會(huì)使用紋理映射的方式裕便,對(duì)頂點(diǎn)處理階段所計(jì)算的顏色色紙進(jìn)行補(bǔ)充绒净。如果我們覺得不應(yīng)該繼續(xù)執(zhí)行某個(gè)片元,在片元著色器中可以終止這個(gè)片元處理偿衰,這一步叫做片元丟棄挂疆。
頂點(diǎn)著色決定了一個(gè)圖元位于屏幕什么位置,而片元著色使用這些信息決定片元是什么顏色下翎。
逐片元的操作
出了在片元著色器里做的工作之外缤言,片元操作的下一步就是最后的獨(dú)立片元處理過程。這個(gè)階段會(huì)使用深度測試和模版測試的方式來決定一個(gè)片元是否可見视事。
如果一個(gè)片元成功通過了所有的測試墨闲,那么他就可以直接繪制到幀緩存中。它對(duì)應(yīng)的像素顏色值也會(huì)更新郑口。如果開啟了融合模式,片元的顏色與當(dāng)前像素顏色疊加盾鳞,形成新的顏色值寫入幀緩存犬性。
實(shí)際上,從上面幾個(gè)階段的描述腾仅,我們可以察覺到有兩個(gè)著色器是必須存在的乒裆,頂點(diǎn)著色器以及片元著色器。
當(dāng)我們繪制最簡單的三角形推励,就只需要使用到兩個(gè)著色器鹤耍。頂點(diǎn)著色器以及片元著色器。
為什么選擇使用三角形验辞?因?yàn)镺penGL本質(zhì)上就是繪制三角形的圖形第三方庫稿黄,而三角形正好是基本圖元。而不是繪制不了矩形跌造,只是顯卡本身繪制三角形會(huì)輕松很多杆怕,而要把矩形作為OpenGL的基本圖元將會(huì)消耗更多的性能族购。
為什么說OpenGL實(shí)際上是一個(gè)著色器的編譯器×暾洌看看著色器是怎么編寫寝杖。
按照上面的邏輯,先編寫一個(gè)頂點(diǎn)著色器互纯。
使用GLSL編寫一個(gè)頂點(diǎn)著色器
#define VERTEX_SHADER ("#version 330 core\n\
layout (location = 0) in vec3 aPos;\n\
void main(){\n\
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);\n\
}\0")
能夠看到define聲明了下面一個(gè)類似c的方法體瑟幕。
#version 330 core
layout (location = 0) in vec3 aPos;
void main(){
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);
}
這里稍微解釋一下,這個(gè)著色器中GLSL編寫的邏輯留潦。能看到在這個(gè)小型程序中只盹,首先有一個(gè)main的主函數(shù)作為小程序主體。
layout代表著這個(gè)頂點(diǎn)著色器從哪個(gè)位置繪制愤兵。location=0.代表著(0,0,0)的位置鹿霸。
in 后面帶著 vec3 修飾的aPos.
這里我們要倒過來看,就能明白輕易的知道意思秆乳。 聲明一個(gè)aPos屬性懦鼠,其類型是vec3。vec3 是指是一個(gè)三維向量屹堰。in是指這個(gè)屬性是從著色器外傳送進(jìn)來的頂點(diǎn)數(shù)據(jù)肛冶。
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);
這里能看到會(huì)聲明一個(gè)vec4修飾4d向量的gl_Position欺缘。
能看到一個(gè)很熟悉的習(xí)慣累驮,在一個(gè)三維空間中顾稀,x代表向量中x軸绊茧,y代表向量中y軸媚污,z代表向量的z軸伺帘。而4d向量并不是說OpenGL在處理4維空間躲叼,這個(gè)最后一個(gè)分量是w蔬捷,代表透視除法(這里不多贅述)厉亏。
對(duì)于向量來說董习,可以直接通過.x/.y/.z/.w直接獲取向量的分量。這個(gè)習(xí)慣和Octave有點(diǎn)像爱只。我們只要把其抽象看成一個(gè)結(jié)構(gòu)體就很好理解了皿淋。
因此此時(shí)的意思是創(chuàng)建一個(gè)4d向量,把從著色器外面?zhèn)鬟M(jìn)來的vec3向量賦值給gl_Position.
使用GLSL編寫一個(gè)片元著色器
此時(shí)我們?cè)陧旤c(diǎn)著色器已經(jīng)獲得了從外部進(jìn)來的頂點(diǎn),接下來我們編寫一個(gè)片元著色器恬试,讓這個(gè)頂點(diǎn)構(gòu)成的圖形上色窝趣。
#define FRAGMENT_SHADER ("#version 330 core\n\
out vec4 FragColor;\n\
void main(){\n\
FragColor = vec4(1.0f,0.5f,0.2f,1.0f);\n\
}\0")
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
在這里面,會(huì)繪制一個(gè)新的4d向量训柴,F(xiàn)ragColor哑舒。上面說了片元著色器,此時(shí)是為了做出渲染出顏色畦粮。因此這個(gè)4d向量代表一個(gè)rbga一個(gè)顏色向量散址。最后用out修飾乖阵,代表這個(gè)FragColor將作為輸出向量。當(dāng)輸出的時(shí)候预麸,就會(huì)把頂點(diǎn)著色上這個(gè)顏色向量瞪浸。
編寫著色器小程序
為什么說是小程序呢?讓我們回憶一下吏祸,C語言編程的時(shí)候对蒲。編譯的流程分為幾步?
編譯的四個(gè)階段:
- 1.預(yù)處理階段 生成.i文件
- 2.編譯階段 生成.S文件
- 3.匯編階段 生成目標(biāo)文件.o文件
- 4.鏈接階段 生成可執(zhí)行文件贡翘。
同理在編寫著色器小程序的時(shí)候蹈矮,很相似。
著色器編寫幾個(gè)步驟:
- 1.創(chuàng)建一個(gè)著色器類型
- 2.拷貝GLSL代碼到著色器類型中
- 3.編譯生成著色器鏈接庫
- 4.創(chuàng)建一個(gè)著色器執(zhí)行程序
- 5.把著色器鏈接到著色器執(zhí)行程序
- 6.鏈接生成帶著著色器的執(zhí)行程序
- 7.刪除之前創(chuàng)建的著色器
生成一個(gè)頂點(diǎn)著色器
//1.初始化著色器
const char* vertexShaderSource = VERTEX_SHADER;
GLuint vertexShader;
//創(chuàng)建一個(gè)著色器類型
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把代碼復(fù)制進(jìn)著色器中
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//編譯頂點(diǎn)著色器
glCompileShader(vertexShader);
判斷頂點(diǎn)著色器是否編譯成功
//判斷是否編譯成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
//判斷是否編譯成功
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
cout<< "error when vertex compile:"<<infoLog<<endl;
return 0;
}
生成一個(gè)片段著色器
///下一個(gè)階段是片段著色器
const char* fragmentShaderSource = FRAGMENT_SHADER;
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
//判斷是否編譯成功
if(!success){
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
cout<< "error when fragment compile:"<<infoLog<<endl;
return 0;
}
生成一個(gè)著色器可執(zhí)行程序鸣驱,并且鏈接著色器鏈接庫
//鏈接泛鸟,創(chuàng)建一個(gè)程序
GLuint shaderProgram;
shaderProgram = glCreateProgram();
//鏈接上共享庫
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//鏈接
glLinkProgram(shaderProgram);
可執(zhí)行程序是否編譯鏈接成功
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success){
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
cout<< "error when link compile:"<<infoLog<<endl;
}
刪除著色器鏈接庫
//編譯好了之后,刪除著色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
準(zhǔn)備傳入著色器的數(shù)據(jù)
傳輸頂點(diǎn)數(shù)據(jù)到著色器中踊东,實(shí)際上有兩個(gè)十分關(guān)鍵的對(duì)象起作用北滥。
一個(gè)是VAO,一個(gè)是VBO闸翅。
VAO 為頂點(diǎn)數(shù)組對(duì)象再芋。頂點(diǎn)數(shù)組對(duì)象可以像頂點(diǎn)緩沖對(duì)象那樣被綁定,任何隨后的頂點(diǎn)屬性的調(diào)用都會(huì)存儲(chǔ)到這個(gè)VAO坚冀。這樣做的好處是济赎,當(dāng)配置了頂點(diǎn)屬性指針之后,你只需要執(zhí)行這些調(diào)用一次记某,之后再繪制物體只需要綁定這個(gè)頂點(diǎn)數(shù)組對(duì)象即可司训。
VBO 頂點(diǎn)緩沖對(duì)象。頂點(diǎn)緩沖對(duì)象管理GPU內(nèi)存內(nèi)存中的大量頂點(diǎn)液南。使用緩沖對(duì)象做的好處豁遭,就是我們可以一次性發(fā)送一批數(shù)據(jù)到GPU上,而不是每個(gè)頂點(diǎn)發(fā)送一次贺拣。從CPU把數(shù)據(jù)發(fā)送到顯卡相對(duì)較慢,所以只要可能我們都要嘗試盡量一次性發(fā)送盡可能多的數(shù)據(jù)捂蕴。數(shù)據(jù)發(fā)送至顯卡的內(nèi)存中后譬涡,頂點(diǎn)著色器幾乎能立即訪問頂點(diǎn),這是個(gè)非成侗妫快的過程涡匀。
VBO的思路實(shí)際上可以從Linux的fwrite的源碼中看到一樣,會(huì)在進(jìn)入內(nèi)核之前有個(gè)緩沖溉知,等到緩沖滿了就會(huì)輸入到內(nèi)核陨瘩。因?yàn)閺挠脩艨臻g到內(nèi)核傳輸數(shù)據(jù)也是一個(gè)相對(duì)耗時(shí)的工作腕够。
在傳送著色器之前,需要介紹以下在OpenGL中的坐標(biāo)系舌劳。
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates, NDC)
OpenGL是一個(gè)3d圖形渲染庫帚湘,所以我們的傳入的坐標(biāo)系都是3d坐標(biāo)(x,y,z軸)。
但是OpenGL不是簡單的把所有的3d坐標(biāo)系都轉(zhuǎn)化為在屏幕上的2d像素甚淡;大诸、
一旦你的頂點(diǎn)坐標(biāo)已經(jīng)在頂點(diǎn)著色器中處理過,它們就應(yīng)該是標(biāo)準(zhǔn)化設(shè)備坐標(biāo)了贯卦,標(biāo)準(zhǔn)化設(shè)備坐標(biāo)是一個(gè)x资柔、y和z值在-1.0到1.0的一小段空間。
與通常的屏幕坐標(biāo)不同撵割,y軸正方向?yàn)橄蛏希?0, 0)坐標(biāo)是這個(gè)圖像的中心贿堰,而不是左上角。最終你希望所有(變換過的)坐標(biāo)都在這個(gè)坐標(biāo)空間中啡彬,否則它們就不可見了羹与。
你的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)接著會(huì)變換為屏幕空間坐標(biāo)(Screen-space Coordinates),這是使用你通過glViewport函數(shù)提供的數(shù)據(jù)外遇,進(jìn)行視口變換(Viewport Transform)完成的注簿。所得的屏幕空間坐標(biāo)又會(huì)被變換為片段輸入到片段著色器中。
因此跳仿,為了達(dá)到這個(gè)效果诡渴,在傳入數(shù)據(jù)的時(shí)候,還有是否歸一化的選項(xiàng)菲语,而歸一化正好能把數(shù)據(jù)縮減到-1到1的區(qū)間妄辩。這么做的好處,就不用擔(dān)心溢出和效率問題山上。
因此我們定義一個(gè)三角形坐標(biāo)
//我們要繪制三角形
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
綁定VAO以及VBO眼耀,做好傳送的準(zhǔn)備
當(dāng)我們編寫OpenGl的時(shí)候要記住下面一幅圖:
這就是OpenGL中VAO和VBO的關(guān)系。能看到的是佩憾,當(dāng)我們聲明一個(gè)VAO頂點(diǎn)數(shù)組對(duì)象的時(shí)候哮伟,里面保存著大量的頂點(diǎn)屬性指針,而每個(gè)指針又會(huì)關(guān)聯(lián)VBO頂點(diǎn)緩沖對(duì)象妄帘。
而這種指針該怎么移動(dòng)解釋VBO的內(nèi)容楞黄,是由開發(fā)者決定。因?yàn)閂BO中可能擁有各種類型的數(shù)據(jù)抡驼。
同時(shí)在OpenGL中鬼廓,如果不綁定VAO,以及打開VAO頂點(diǎn)數(shù)組對(duì)象的開關(guān)致盟,將會(huì)拒絕繪制任何東西碎税。
生成VAO對(duì)象尤慰,并且綁定
GLuint VAO;
//生成分配VAO
glGenVertexArrays(1,&VAO);
//綁定VAO,注意在core模式雷蹂,沒有綁定VAO伟端,opengl拒絕繪制任何東西
glBindVertexArray(VAO);
生成VBO對(duì)象,并且綁定
GLuint VBO;
//生成一個(gè)VBO緩存對(duì)象
glGenBuffers(1, &VBO);
//綁定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//類型為GL_ARRAY_BUFFER 第二第三參數(shù)說明要放入緩存的多少萎河,GL_STATIC_DRAW當(dāng)畫面不懂的時(shí)候推薦使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
能看到的是此時(shí)生成VAO和VBO的邏輯十分相似荔泳,都經(jīng)歷2個(gè)步驟通過glGen的方法獲得一個(gè)VAO/VBO的句柄,調(diào)用glBind方法綁定對(duì)應(yīng)的VAO/VBO虐杯。
復(fù)制數(shù)據(jù)到頂點(diǎn)緩沖對(duì)象
//類型為GL_ARRAY_BUFFER 第二第三參數(shù)說明要放入緩存的多少玛歌,GL_STATIC_DRAW當(dāng)畫面不動(dòng)的時(shí)候推薦使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
把數(shù)據(jù)從對(duì)象傳輸?shù)骄彺鎸?duì)象中
任務(wù)有二:
- 分配頂點(diǎn)數(shù)據(jù)需要的存儲(chǔ)空間
- 將數(shù)據(jù)從應(yīng)用程序的數(shù)組拷貝到opengl服務(wù)端內(nèi)存。
后面的標(biāo)志有下面幾種:
- GL_STATIC_DRAW :數(shù)據(jù)不會(huì)或幾乎不會(huì)改變擎椰。
- GL_DYNAMIC_DRAW:數(shù)據(jù)會(huì)被改變很多支子。
- GL_STREAM_DRAW :數(shù)據(jù)每次繪制時(shí)都會(huì)改變。
設(shè)置頂點(diǎn)屬性指針如何解析緩存數(shù)據(jù)
//設(shè)定頂點(diǎn)屬性指針
//第一個(gè)參數(shù)指定我們要配置頂點(diǎn)屬性达舒,對(duì)應(yīng)vertex glsl中l(wèi)ocation 確定位置
//第二參數(shù)頂點(diǎn)大小值朋,頂點(diǎn)屬性是一個(gè)vec3,由3個(gè)值組成巩搏,大小是3
//第三參數(shù)指定數(shù)據(jù)類型昨登,都是float(glsl中vec*都是float)
//第四個(gè)參數(shù):是否被歸一化
//第五參數(shù):步長,告訴我們連續(xù)的頂點(diǎn)屬性組之間的間隔贯底,這里是每一段都是3個(gè)float丰辣,所以是3*float
//最后一個(gè)是偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
可能上面的注釋還不夠清晰,記住下面這幅圖
主要這個(gè)方法是為了告訴OpenGL遇到這個(gè)頂點(diǎn)數(shù)組對(duì)象的時(shí)候禽捆,該如何移動(dòng)指針解析數(shù)據(jù)笙什。
最后通過glEnableVertexAttribArray 打開該頂點(diǎn)數(shù)組對(duì)象的繪制開關(guān)。
接棒頂點(diǎn)以及緩存對(duì)象
glBindBuffer(GL_ARRAY_BUFFER,0);
glBindVertexArray(0);
為避免出現(xiàn)多線程等干擾胚想,在進(jìn)行下一次執(zhí)行的時(shí)候琐凭,最好先解綁。能看到此時(shí)還是調(diào)用glBind系列的函數(shù)浊服。
glBindVertexArray
glGenVertexArrays返回的數(shù)據(jù)统屈,則創(chuàng)建一個(gè)新的新的頂點(diǎn)數(shù)組對(duì)象并且和名稱關(guān)聯(lián)起來。
如果綁定到已經(jīng)創(chuàng)建的頂點(diǎn)數(shù)組中牙躺,初始化則激活綁定頂點(diǎn)數(shù)據(jù)
當(dāng)array為0鸿吆,則不分配任何對(duì)象.
glBindBuffer
激活當(dāng)前的緩存對(duì)象。
- 如果是第一次綁定buffer述呐,且是一個(gè)非零的無符號(hào)整型,創(chuàng)建一個(gè)與名稱相對(duì)應(yīng)的新緩存對(duì)象
- 如果綁定到一個(gè)已經(jīng)創(chuàng)建的緩存對(duì)象蕉毯,那么它將會(huì)成為當(dāng)前激活緩存對(duì)象
- 如果綁定buffer為0乓搬,則不會(huì)給任何緩存
把繪制事件添加到渲染循環(huán)中思犁。
while(!glfwWindowShouldClose(window)){
processInput(window);
//交換顏色緩沖,他是一個(gè)存儲(chǔ)著GLFW窗口每一個(gè)像素顏色值的大緩沖
//會(huì)在這個(gè)迭代中用來繪制进肯,并且顯示在屏幕上
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//我們已經(jīng)告訴激蹲,程序要數(shù)據(jù)什么數(shù)據(jù),以及怎么解釋整個(gè)數(shù)據(jù)
//數(shù)據(jù)傳輸之后運(yùn)行程序
glUseProgram(shaderProgram);
//綁定數(shù)據(jù)
glBindVertexArray(VAO);
//繪制一個(gè)三角形
//從0開始江掩,3個(gè)
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glfwSwapBuffers(window);
//檢查有沒有觸發(fā)事件学辱,鍵盤輸入,鼠標(biāo)移動(dòng)
glfwPollEvents();
}
glDeleteVertexArrays(1,&VAO);
glDeleteBuffers(1, &VBO);
能看到此時(shí)先調(diào)用glUseProgram环形,運(yùn)行之前的程序策泣,接著glBindVertexArray重新綁定頂點(diǎn)數(shù)據(jù),就能直接通過glDrawArrays繪制抬吟。
glDrawArrays 是指此時(shí)繪制的是三角形以及萨咕,繪制的啟示頂點(diǎn)是第0個(gè),繪制3個(gè)火本。
這樣就完成了一次繪制危队。
總結(jié)
當(dāng)然,在自己編寫的時(shí)候遇到繪制了兩個(gè)三角形的情況钙畔,先顯示小的茫陆,接著出現(xiàn)大。
怎么看都無法想象究竟出現(xiàn)哪路有問題,接著在初始化窗體的時(shí)候發(fā)現(xiàn)自己先去調(diào)用glViewport變化了窗口大小擎析,接著才進(jìn)行繪制簿盅。
glViewport(0,0,800,600);
這樣導(dǎo)致了一個(gè)結(jié)果,還記得我在開篇就說了叔锐,實(shí)際上OpenGL是一個(gè)C/S架構(gòu)的第三方庫挪鹏,當(dāng)我們每一次渲染的時(shí)候,調(diào)用的是每一條渲染指令愉烙。
也就是說讨盒,在渲染循環(huán)中,當(dāng)我在第一輪循環(huán)中步责,由于窗體比較小返顺,因此先按照該窗體的相對(duì)的標(biāo)準(zhǔn)化坐標(biāo)中繪制比較小的三角形。接著第二條渲染指令來了蔓肯,讓窗體變大遂鹊,這個(gè)時(shí)候整個(gè)窗口坐標(biāo)系產(chǎn)生了變化,這個(gè)時(shí)候渲染循環(huán)與根據(jù)此時(shí)相對(duì)的標(biāo)準(zhǔn)化坐標(biāo)繪制了一個(gè)更大的三角形蔗包。
經(jīng)過這一次的小踩坑秉扑,讓我對(duì)OpenGL產(chǎn)生了更加深刻的理解。