OpenGL學(xué)習(xí)(一) 繪制一個(gè)三角形

背景

如果遇到什么錯(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è)基本著色階段:


基本著色器.png

還有可以有更加復(fù)雜的著色器階段:


各個(gè)著色器.png

稍微介紹一下每個(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)準(zhǔn)化設(shè)備坐標(biāo).png

與通常的屏幕坐標(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í)候要記住下面一幅圖:


image.png

這就是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);

可能上面的注釋還不夠清晰,記住下面這幅圖


頂點(diǎn)屬性對(duì)象解析.png

主要這個(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è)火本。

這樣就完成了一次繪制危队。


image.png

總結(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)生了更加深刻的理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舟陆,隨后出現(xiàn)的幾起案子误澳,更是在濱河造成了極大的恐慌,老刑警劉巖秦躯,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忆谓,死亡現(xiàn)場離奇詭異,居然都是意外死亡踱承,警方通過查閱死者的電腦和手機(jī)倡缠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茎活,“玉大人昙沦,你說我怎么就攤上這事∶钌” “怎么了桅滋?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長身辨。 經(jīng)常有香客問我丐谋,道長,這世上最難降的妖魔是什么煌珊? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任号俐,我火速辦了婚禮,結(jié)果婚禮上定庵,老公的妹妹穿的比我還像新娘吏饿。我一直安慰自己,他們只是感情好蔬浙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布猪落。 她就那樣靜靜地躺著,像睡著了一般畴博。 火紅的嫁衣襯著肌膚如雪笨忌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天俱病,我揣著相機(jī)與錄音官疲,去河邊找鬼。 笑死亮隙,一個(gè)胖子當(dāng)著我的面吹牛途凫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溢吻,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼维费,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起犀盟,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤噪漾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后且蓬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡题翰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年恶阴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豹障。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冯事,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出血公,到底是詐尸還是另有隱情昵仅,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布累魔,位于F島的核電站摔笤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垦写。R本人自食惡果不足惜吕世,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梯投。 院中可真熱鬧命辖,春花似錦、人聲如沸分蓖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽么鹤。三九已至终娃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間午磁,已是汗流浹背尝抖。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迅皇,地道東北人昧辽。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像登颓,于是被迫代替她去往敵國和親搅荞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344