OpenGL從入門到放棄 #03 Create a Triangle


??在這節(jié)中逾冬,我們打算使用在上節(jié)創(chuàng)建的窗口里渲染一個(gè)三角形出來,雖然說只是一個(gè)三角形,但里面已經(jīng)涉及到了很多圖形編程的知識(shí)和OpenGL函數(shù)的調(diào)用痹束,所以可能要花很多筆墨去講述這個(gè)三角形究竟是怎么出來。
??在OpenGL中讶请,大部分的工作都是把3D空間的坐標(biāo)(coordinates)轉(zhuǎn)為適應(yīng)屏幕的2D像素祷嘶,而這個(gè)過程的是由OpenGL圖形渲染管線(Graphics Pipeline)實(shí)現(xiàn)的。圖形渲染管線可以被劃分為兩個(gè)部分:第一個(gè)部分就是將3D坐標(biāo)轉(zhuǎn)為2D坐標(biāo);第二部分就是將2D坐標(biāo)轉(zhuǎn)為實(shí)際有顏色的像素论巍。

??2D坐標(biāo)與像素是有所區(qū)別的烛谊。2D坐標(biāo)準(zhǔn)確表示一個(gè)點(diǎn)的在2D空間的位置,而像素只是2D坐標(biāo)的近似值(approximation)嘉汰,因?yàn)槠聊环直媛实年P(guān)系丹禀,所以像素并不會(huì)準(zhǔn)確反映2D坐標(biāo)值。

??圖形渲染管線能夠分為幾個(gè)階段(several step)鞋怀,且每個(gè)階段的輸出都是下個(gè)階段的輸入双泪。所有的這些階段都是高內(nèi)聚低耦合的(highly specialized),即每個(gè)階段都有特定的功能密似,而且這些階段都能夠并行執(zhí)行(be executed in parallel)攒读。因?yàn)閳D形渲染管線的并行執(zhí)行特性,許多的顯卡都會(huì)包含成千上萬個(gè)小處理單元(small processing cores)辛友,在圖形渲染管線每個(gè)階段GPU都會(huì)利用小處理單元運(yùn)行很多的小項(xiàng)目來處理用戶的數(shù)據(jù),而這些小項(xiàng)目就叫著色器(shader)剪返。對(duì)废累,說了這么多其實(shí)就是為了引出著色器這個(gè)概念。
??OpenGL允許開發(fā)者去配置(configurable)部分shaders以得到屬于他們的shaders脱盲,這些shaders能代替已存在的默認(rèn)的shaders邑滨。此舉能讓我們更細(xì)致(fine-grained)地去控制圖形渲染管線的特定部分。因?yàn)閟haders是運(yùn)行在GPU上的钱反,所以節(jié)省了很多CPU的時(shí)間掖看。而對(duì)于shaders本身,是使用OpenGL著色器語言(OpenGL Shading Language, GLSL)寫成的面哥。
??以下是圖形渲染管線所有階段的一個(gè)抽象表示(an abstract representation)哎壳,但其實(shí)這個(gè)圖只是列出了幾個(gè)比較重要的階段,是并不完整的尚卫。
??要注意是的归榕,藍(lán)色的階段代表這個(gè)階段我們可以寫入自己的shaders。


??可以看到圖形渲染管線包含了很多個(gè)階段吱涉,且每個(gè)階段處理的使用都不一樣刹泄,但最終結(jié)果都是為了把進(jìn)來的頂點(diǎn)數(shù)據(jù)(Vertex Date)變成被完全渲染的像素。接下來將簡述每個(gè)階段作用怎爵。
??我們會(huì)通過使用3個(gè)能形成一個(gè)三角形的3D坐標(biāo)構(gòu)成一個(gè)數(shù)組去作為圖形渲染管線的輸入特石,這個(gè)輸入的數(shù)組成為頂點(diǎn)數(shù)據(jù)。一個(gè)頂點(diǎn)是每一個(gè)3D坐標(biāo)的數(shù)據(jù)的基本集合(basically a collection)鳖链。而頂點(diǎn)數(shù)據(jù)是用頂點(diǎn)屬性(Vertex Attribute)表示的姆蘸,它可以包含任何我們想用的數(shù)據(jù)。
??首先CPU會(huì)往GPU丟頂點(diǎn)數(shù)據(jù)和材質(zhì)球的配置,材質(zhì)球包含shaders的代碼和材質(zhì)球的設(shè)置乞旦。這些都會(huì)在圖形渲染管線的階段里所用到贼穆。
??圖形渲染管線的第一個(gè)部分就是頂點(diǎn)著色器(Vertex Shader),它僅接收一個(gè)簡單的頂點(diǎn)兰粉。它的主要目的就是把3D坐標(biāo)轉(zhuǎn)為不同的3D坐標(biāo)(這里不理解故痊,),且它允許我們?nèi)?duì)頂點(diǎn)坐標(biāo)的屬性(attributes)做一些基本的處理玖姑。
??第二個(gè)階段是圖元裝配(primitive assembly)階段愕秫,這個(gè)階段接收頂點(diǎn)著色器輸出的所有頂點(diǎn)(如果在程式碼中選擇了GL_POINTS那就是一個(gè)頂點(diǎn)),這個(gè)階段會(huì)把這些點(diǎn)組成一個(gè)原始的形狀(primitive shape)焰络,這里叫圖元戴甩。在圖示中例获,它形成了一個(gè)三角形作為圖元妇多。通俗地講就是通過傳遞進(jìn)來的信息,圖元裝配階段去決定這些點(diǎn)誰跟誰會(huì)連成線粥喜,誰跟誰會(huì)連成面亦或是誰會(huì)單獨(dú)成一個(gè)點(diǎn)畏腕。
??第三個(gè)階段是幾何著色器(geometry shader)缴川,這個(gè)幾何著色器有能力通過生成新的頂點(diǎn)(emitting new vertices)去構(gòu)造新的圖元(form new primitive)。在圖示中它生成了另外一個(gè)三角形描馅。通俗地講就是這個(gè)著色器能偷偷補(bǔ)一些點(diǎn)進(jìn)去把夸,當(dāng)然這要代碼去實(shí)現(xiàn),使得原本比較粗糙的圖元變得精細(xì)一點(diǎn)铭污。
??第四個(gè)階段是光柵化(rasterization stage)恋日,它會(huì)把幾何著色器輸出的圖元在最終的屏幕上映射(map)為相應(yīng)的像素(corresponding pixels),而且會(huì)生成供片段著色器使用的片段嘹狞。在片段著色器運(yùn)行之前岂膳,會(huì)先執(zhí)行(perform)片段的剪輯(clip),剪輯會(huì)丟棄所有超出你視角范圍的片段磅网,以此提升性能(performance)闷营。
??第五個(gè)階段就是片段著色器(fragment shader),它的主要目的就是計(jì)算每一個(gè)像素的最終顏色知市,而這經(jīng)常是所有OpenGL高級(jí)(advanced)效果產(chǎn)生的地方傻盟。片段著色器經(jīng)常包含3D場景的數(shù)據(jù)(例如燈光、陰影等等)嫂丙,這主要用來計(jì)算最終像素的顏色娘赴。
??在全部像素對(duì)應(yīng)的顏色值都被確定時(shí),在經(jīng)歷最后一個(gè)階段跟啤,稱為alpha測試和混合(blending)測試階段诽表。這個(gè)階段檢查每個(gè)片段相應(yīng)的深度值(depth value)且使用這個(gè)深度值來判斷這個(gè)片段是在其他物體的前面還是后面(in front or behind other objects)唉锌,以此決定是否丟棄這個(gè)片段。這個(gè)階段也會(huì)檢查alpha值(alpha值就是一個(gè)物體的不透明度(opacity))并對(duì)物體進(jìn)行混合竿奏。所以即使一個(gè)像素的顏色在片段著色器被計(jì)算好了袄简,但最終它的顏色也有可能會(huì)在渲染中發(fā)生很大的變化(entirely different)。

??從上述就可以了解到泛啸,GPU會(huì)接收來CPU的頂點(diǎn)數(shù)據(jù)绿语,然后在接收到繪制指令之后,會(huì)把數(shù)據(jù)喂給shaders后開始一連續(xù)的處理最終呈現(xiàn)出漂亮的像素候址。那么我們需要考慮的只是shaders的定義嗎吕粹?不,遠(yuǎn)不止這些岗仑。
??第一個(gè)要考慮的就是:GPU接收頂點(diǎn)數(shù)據(jù)匹耕,那肯定需要一個(gè)緩沖區(qū)來存放這些數(shù)據(jù),上面也有提到荠雕,圖形渲染管線具有并行處理的特性稳其,那么肯定能同時(shí)處理很多數(shù)據(jù),那自然是需要一個(gè)很大緩沖區(qū)(顯存)炸卑。而且僅僅是存放是不夠的既鞠,因?yàn)橐粋€(gè)模型會(huì)對(duì)應(yīng)一大堆頂點(diǎn)數(shù)據(jù),而CPU把數(shù)據(jù)發(fā)送到顯卡相對(duì)較慢矾兜,所以只要可能我們都要嘗試盡量一次性發(fā)送盡可能多的數(shù)據(jù),所以亟需一個(gè)管理者來管理這一次性進(jìn)來的這么多數(shù)據(jù)患久,這個(gè)管理者就叫頂點(diǎn)緩沖對(duì)象(Vertex Buffer Objects, VBO)椅寺。通常一個(gè)VBO對(duì)應(yīng)一個(gè)模型的頂點(diǎn)數(shù)據(jù),它會(huì)在GPU內(nèi)存(通常被稱為顯存)中儲(chǔ)存大量頂點(diǎn)蒋失。使用這些緩沖對(duì)象的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上返帕,而不是每個(gè)頂點(diǎn)發(fā)送一次。當(dāng)數(shù)據(jù)發(fā)送至顯卡的內(nèi)存中后篙挽,頂點(diǎn)著色器幾乎能立即訪問頂點(diǎn)荆萤,這是個(gè)非常快的過程铣卡。
??概括地講链韭,VBO就是用來管理顯存,可大量接收頂點(diǎn)數(shù)據(jù)且做好被頂點(diǎn)著色器訪問的準(zhǔn)備煮落。
??所以在考慮shaers之前敞峭,我們就要做好被shaders接收的準(zhǔn)備,所以要先輸入頂點(diǎn)數(shù)據(jù)創(chuàng)建VBO對(duì)象

輸入頂點(diǎn)數(shù)據(jù)

??首先OpenGL對(duì)輸入的坐標(biāo)是有要求的蝉仇,并不是說你給它什么它都要旋讹,但是真實(shí)情況是進(jìn)來的頂點(diǎn)數(shù)據(jù)一般都不是OpenGL想要的殖蚕。那么OpenGL想要怎樣的坐標(biāo)?OpenGL僅當(dāng)3D坐標(biāo)在3個(gè)軸(x沉迹、y和z)上都為-1.0到1.0的范圍內(nèi)時(shí)才處理它睦疫。這種坐標(biāo)叫做標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates (NDC))

Normalized Device Coordinates (NDC)

Once your vertex coordinates have been processed in the vertex shader, they should be in normalized device coordinates which is a small space where the x, y and z values vary from -1.0 to 1.0. Any coordinates that fall outside this range will be discarded/clipped and won't be visible on your screen. Below you can see the triangle we specified within normalized device coordinates (ignoring the z axis):

Unlike usual screen coordinates the positive y-axis points in the up-direction and the (0,0) coordinates are at the center of the graph, instead of top-left. Eventually you want all the (transformed) coordinates to end up in this coordinate space, otherwise they won't be visible.

Your NDC coordinates will then be transformed to screen-space coordinates via the viewport transform using the data you provided with glViewport. The resulting screen-space coordinates are then transformed to fragments as inputs to your fragment shader.

??一般頂點(diǎn)數(shù)據(jù)OpenGL是不想要的,那就要通過頂點(diǎn)著色器(vertex shader)處理鞭呕,轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)蛤育。這個(gè)標(biāo)準(zhǔn)化設(shè)備坐標(biāo)軸跟我們書上學(xué)的xy坐標(biāo)軸的方向是一致的,y軸向上琅拌,x軸向右缨伊,原點(diǎn)在中央,但與屏幕的坐標(biāo)軸不同(Unlike usual screen coordinates)进宝。由于標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的xyz軸的大小范圍都是-1到刻坊,所以如果有超出坐標(biāo)系的頂點(diǎn),這些頂點(diǎn)會(huì)被丟棄掉(discarded/clipped)且不能在你的屏幕上所被看到党晋。你的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)會(huì)通過glViewport()提供的數(shù)據(jù)轉(zhuǎn)變?yōu)槠聊豢臻g坐標(biāo)(screen-space coordinates)谭胚。這個(gè)屏幕坐標(biāo)會(huì)轉(zhuǎn)變?yōu)槠蝹鬟f給片段著色器。
??概括地講未玻,頂點(diǎn)數(shù)據(jù)從VBO進(jìn)入到頂點(diǎn)著色器后變?yōu)闃?biāo)準(zhǔn)化設(shè)備坐標(biāo)灾而,緊接著轉(zhuǎn)換為屏幕空間坐標(biāo),最后轉(zhuǎn)換為片段輸出給片段著色器扳剿。其實(shí)從VBO進(jìn)入到頂點(diǎn)著色器是需要自己配置的旁趟,不過這是后話了。
??由于我們是初學(xué)的關(guān)系庇绽,并不能實(shí)現(xiàn)頂點(diǎn)著色器的實(shí)際功能锡搜,所以打算在輸入頂點(diǎn)數(shù)據(jù)時(shí),直接輸入標(biāo)準(zhǔn)化設(shè)備坐標(biāo)瞧掺,然后在頂點(diǎn)著色器里不再處理數(shù)據(jù)耕餐,直接原封不動(dòng)的輸出。
??我們希望渲染一個(gè)三角形辟狈,我們一共要指定三個(gè)頂點(diǎn)肠缔,每個(gè)頂點(diǎn)都有一個(gè)3D位置。我們會(huì)將它們以標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的形式(OpenGL的可見區(qū)域)定義為一個(gè)float數(shù)組:

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

創(chuàng)建VBO對(duì)象

??一個(gè)VBO對(duì)象一般都要經(jīng)歷創(chuàng)建→綁定→綁定頂點(diǎn)數(shù)據(jù)哼转。
??首先就是創(chuàng)建:

    //在上節(jié)代碼調(diào)用glViewport()之后
    unsigned int VBO;
    glGenBuffers(1, &VBO);

glGenBuffers(GLsizei n, GLunit *buffers):這個(gè)函數(shù)會(huì)產(chǎn)生VBO對(duì)象明未,且能根據(jù)你給第一個(gè)參數(shù)的數(shù)字創(chuàng)建相應(yīng)個(gè)數(shù)的VBO對(duì)象,因?yàn)镺penGL要求每個(gè)對(duì)象要有獨(dú)一無二的ID壹蔓,所以需要傳給它一個(gè)地址來記錄對(duì)象獨(dú)一無二的ID亚隅。由于我們這里只需要1個(gè)VBO對(duì)象,所以不用傳數(shù)組庶溶,只傳一個(gè)uint類型變量就好煮纵。
??那么如果我想操作某個(gè)VBO懂鸵,那么就要綁定它,說明接下來我的操作都是只對(duì)應(yīng)這個(gè)被綁定的VBO:

    glBindBuffer(GL_ARRAY_BUFFER, VBO);

glBindBuffer(GLenum target, GLunit buffer):OpenGL有很多緩沖對(duì)象類型行疏,所以我們需要指明我們要綁定的是頂點(diǎn)緩沖對(duì)象匆光,其枚舉值是GL_ARRAY_BUFFER。第二個(gè)參數(shù)就是你要綁定的VBO的ID酿联。
??例如我們之后需要做的是綁定頂點(diǎn)數(shù)據(jù)這個(gè)操作终息,雖然沒指定把頂點(diǎn)數(shù)據(jù)綁到哪個(gè)VBO,但是由于我們已經(jīng)在上面做了綁定的操作贞让,所以O(shè)penGL是知道綁定到哪個(gè)VBO的:

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage):前面有提到OpenGL有很多緩沖對(duì)象類型GL_ARRAY_BUFFER周崭,所以這里指定是頂點(diǎn)緩沖對(duì)象,它就知道接下來的操作是對(duì)那個(gè)被綁定的VBO做處理喳张。第二個(gè)參數(shù)是指定傳輸數(shù)據(jù)的大小续镇,這里用一個(gè)sizeof()就能算出來。第三個(gè)參數(shù)就是要發(fā)送的數(shù)據(jù)的地址销部。第四個(gè)參數(shù)指定了我們希望顯卡如何管理給定的數(shù)據(jù)摸航。它有三種形式:
GL_STATIC_DRAW :數(shù)據(jù)不會(huì)或幾乎不會(huì)改變。
GL_DYNAMIC_DRAW:數(shù)據(jù)會(huì)被改變很多舅桩。
GL_STREAM_DRAW :數(shù)據(jù)每次繪制時(shí)都會(huì)改變酱虎。
??因?yàn)槲覀円秩镜娜切蔚臄?shù)據(jù)不會(huì)改變,所以使用GL_STATIC_DRAW形式就好擂涛。
??至此读串,我們已經(jīng)把頂點(diǎn)數(shù)據(jù)灌進(jìn)顯卡的內(nèi)存里面去了,是時(shí)候考慮shaders部分了撒妈。

??在圖形渲染管線的眾多階段中恢暖,我們至少需要定義一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器(因?yàn)镚PU中沒有默認(rèn)的頂點(diǎn)/片段著色器),所以本節(jié)會(huì)著手創(chuàng)建這兩個(gè)著色器,但本節(jié)只會(huì)配置非常簡單的著色器踩身,詳細(xì)的討論我打算留在下一節(jié)胀茵。
??另外社露,在開始創(chuàng)建我們的著色器之前挟阻,我想先討論一下一個(gè)著色器究竟是如何創(chuàng)建的。在前面有提到著色器是要用著色器語言GLSL(OpenGL Shading Language)編寫的峭弟,它雖然與C語言類似附鸽,但是要想OpenGL認(rèn)出你寫的代碼是為了創(chuàng)建一個(gè)著色器,那就要交給OpenGL它本人來編譯(是不能給你IDE的去編譯的瞒瘸,你的IDE并不會(huì)認(rèn)為你寫的是一個(gè)著色器)坷备。在OpenGL中,是有專門的函數(shù)去編譯你寫的代碼的(運(yùn)行時(shí)編譯)情臭,那么如何把你的代碼灌進(jìn)一個(gè)函數(shù)讓它去編譯呢省撑,OpenGL的做法是接收一個(gè)里面保存了代碼的字符串指針赌蔑。所以在下面你能看到這一做法,我在這里已作出解釋竟秫。

頂點(diǎn)著色器(Vertex Shader)

??我覺得首先就要開門見山娃惯,把源碼擺上來,然后在逐一解釋它們的含義最好:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

#version 330 core:每個(gè)著色器都起始于一個(gè)版本聲明肥败,在3.3以后OpenGL的版本與GLSL的版本是匹配的趾浅,因?yàn)槲覀冊诔跏蓟杏玫腛penGL版本就是3.3,所以這里就是330且明確表示用的是核心(core)模式馒稍。
in vec3 aPos;in關(guān)鍵字是用來聲明輸入的頂點(diǎn)屬性(Input Vertex Attribute)皿哨,由于我們這里只有一種頂點(diǎn)屬性就是position,所以只有一條聲明式纽谒。另外我們需要輸入變量來保存我們的頂點(diǎn)屬性证膨,這里是position,是一個(gè)3D坐標(biāo)佛舱,所以就創(chuàng)建一個(gè)vec3類型的輸入變量aPos(這里的vec3跟Unity的vector3一樣都是向量數(shù)據(jù)類型)椎例。概括地講,就是要聲明所有要用到的頂點(diǎn)屬性请祖,并用相應(yīng)類型的變量接收订歪。
layout (location = 0):這里我引用一篇文章中對(duì)layout關(guān)鍵字的解釋:

??另外,這里使用了layout關(guān)鍵字(通常是layout(layoutAttrib1=XXX, layoutAttrib2=XXX, ...)這樣的形式)肆捕。這個(gè)關(guān)鍵字用于一個(gè)具體變量前刷晋,用于顯式標(biāo)明該變量的一些布局屬性,這里就是顯式設(shè)定了該attribute變量的位置值(location)慎陵,

??我的理解是眼虱,一旦我們的頂點(diǎn)著色器要去訪問VBO,那么該從哪里開始去獲得它所需要的指定屬性的數(shù)據(jù)(這里是position)席纽,layout (location = 0)就是告知頂點(diǎn)著色器:你去數(shù)據(jù)最開始的地方(0)獲取數(shù)據(jù)并存在aPos里面捏悬。
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);:這一行是設(shè)置頂點(diǎn)著色器的輸出,gl_Position可以是個(gè)輸入變量也可以是個(gè)輸出變量润梯,在main函數(shù)的最后过牙,我們將gl_Position設(shè)置的值就會(huì)成為該頂點(diǎn)著色器的輸出,輸出給下一著色器纺铭。而且能看出來它是個(gè)vec4類型的變量寇钉,這4個(gè)分量中每個(gè)分量值都代表空間中的一個(gè)坐標(biāo),它們可以通過vec.x舶赔、vec.y扫倡、vec.zvec.w來獲取。前3個(gè)參數(shù)我們直接把接收到的位置數(shù)據(jù)(aPos)原封不動(dòng)的喂給它們竟纳,至于第4個(gè)參數(shù)我現(xiàn)在還不理解撵溃,可能要日后才能作出解釋疚鲤。
??跟前面說的一樣,我們對(duì)頂點(diǎn)著色器的要求就僅限于原封不動(dòng)的輸出數(shù)據(jù)缘挑。

編譯著色器

??由于前面有提到著色器怎么創(chuàng)建的緣故石咬,所以這里不再作過多原理上的解釋,只討論函數(shù)的使用卖哎。而且我把上述的源碼喂給了一個(gè)const char*類型的指針變量鬼悠,命名為:vertexShaderSource。
??與創(chuàng)建VBO對(duì)象類似亏娜,我們也要用相應(yīng)的函數(shù)創(chuàng)建一個(gè)shader對(duì)象焕窝,并用一個(gè)unit類型變量去記錄這個(gè)對(duì)象的ID,由于我們創(chuàng)建的頂點(diǎn)著色器维贺,傳遞的參數(shù)就是GL_VERTEX_SHADER

    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);

??接下里自然就是把我們寫的源碼附加到這個(gè)shader對(duì)象上它掂,然后再去編譯它:

    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

glCompileShader(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length):第一個(gè)參數(shù)表明要把源碼附加到哪個(gè)shader對(duì)象上;第二個(gè)參數(shù)表明源碼的個(gè)數(shù)溯泣,由于我們只有一個(gè)字符串源碼虐秋,而不是字符串?dāng)?shù)組,所以數(shù)量為1垃沦;第三個(gè)參數(shù)表明字符串的位置客给;第四個(gè)參數(shù)用不上,設(shè)置為NULL肢簿。
glCompileShader(GLuint shader):編譯指定shader對(duì)象靶剑。
??至此一個(gè)頂點(diǎn)著色器就被創(chuàng)建好了,接下來可以考慮片段著色器了池充。

片段著色器(Fragment Shader)

??由于有了先前創(chuàng)建頂點(diǎn)著色器的經(jīng)驗(yàn)桩引,現(xiàn)在創(chuàng)建片段著色器只需解釋GLSL部分相關(guān)知識(shí),剩余的部分因?yàn)榕c創(chuàng)建頂點(diǎn)著色器是一致的收夸,所以只給出代碼坑匠,不再贅述。
??先來看看片段著色器的源碼:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

out vec4 FragColor:片段著色器只需要一個(gè)輸出變量卧惜,表示最終輸出的顏色厘灼。顏色要用4分量向量即vec4表示,因?yàn)椋?/p>

??計(jì)算機(jī)圖形中顏色被表示為有4個(gè)元素的數(shù)組:紅色序苏、綠色手幢、藍(lán)色和alpha(透明度)分量捷凄,通吵老辏縮寫為RGBA。

??為了簡單起見跺涤,這個(gè)片段著色器將不會(huì)做計(jì)算像素的顏色輸出匈睁,而是一直輸出橘色监透,對(duì)應(yīng)的RGB值如源碼所示。我把這段源碼寫進(jìn)了一個(gè)命名為fragmentShaderSource的字符串變量里航唆。但我這里有一個(gè)疑惑胀蛮,片段著色器不用輸入的嗎?我?guī)е@個(gè)疑問去網(wǎng)上搜尋了資料糯钙,還好在這里找到我要的答案:

??gl_Position是vertex shader內(nèi)建的輸出變量粪狼,傳遞給fragment shader,必須設(shè)置任岸。這里將Position直接傳遞給fragment shader(片元著色器)再榄。

??是gl_Position直接傳遞,片段著色器自動(dòng)接收享潜,不用額外再給它定義接收輸入的操作困鸥。
??編譯部分與頂點(diǎn)著色器類似,但唯一不同的就是創(chuàng)建的著色器類型不一樣剑按,這里是GL_FRAGMENT_SHADER

    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

??至此兩個(gè)必須著色器已經(jīng)準(zhǔn)備完畢疾就,但別著急啊,現(xiàn)在還不能用艺蝴,OpenGL規(guī)定猬腰,要把所有自己定義的著色器合并并且鏈接(Link)形成一個(gè)著色器程序?qū)ο?Shader Program Object),這個(gè)對(duì)象還要被激活才算是真正投入使用猜敢。

著色器程序?qū)ο?Shader Program Object)

??把各個(gè)著色器合并鏈接成一個(gè)著色器程序?qū)ο笠幸韵虏襟E:
1.創(chuàng)建著色器程序?qū)ο?br> 2.把寫好的著色器按順序附加(attach)到著色器程序?qū)ο笊?br> 3.然后把它們串(鏈接)起來
4.激活著色器程序?qū)ο?br> 5.刪除著色器

1.創(chuàng)建

??與創(chuàng)建其他OpenGL對(duì)象一樣漆诽,只不過用到函數(shù)不一樣,這里用的是glCreateProgram()

    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
2.附加和鏈接
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

glAttachShader(GLuint program, Gluint shader):把指定shader附加到指定program上锣枝。
glLinkProgram(GLunit program):把指定著色器程序里面的著色器給鏈接起來厢拭。

3.激活
    glUseProgram(shaderProgram);

glUseProgram(GLunit program):激活指定program。激活以后撇叁,每個(gè)著色器調(diào)用和渲染調(diào)用都會(huì)使用這個(gè)程序?qū)ο蟆?/p>

4.刪除

??在把著色器附加到著色器程序?qū)ο笾蠊覀兊膶?duì)象已經(jīng)能實(shí)現(xiàn)著色器的相關(guān)功能了,那么這些單獨(dú)存在的著色器就已經(jīng)失去了作用了陨闹,我們不再需要它們了楞捂,記得刪除:

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

??現(xiàn)在我們已經(jīng)把頂點(diǎn)數(shù)據(jù)發(fā)送到了GPU(VBO),而且還告知GPU怎么處理這么頂點(diǎn)趋厉,但是實(shí)際上還有一步需要處理寨闹。在把頂點(diǎn)送到頂點(diǎn)著色器之前,還要告知OpenGL如何解析這么頂點(diǎn)君账。因?yàn)轫旤c(diǎn)數(shù)據(jù)是一堆充滿各種頂點(diǎn)屬性的數(shù)組繁堡,屬性包含了有頂點(diǎn)坐標(biāo)、法向量、UV值等等椭蹄,我們在做的就是告知OpenGL在頂點(diǎn)數(shù)據(jù)里闻牡,哪些數(shù)據(jù)是對(duì)應(yīng)頂點(diǎn)著色器的某個(gè)頂點(diǎn)屬性。

鏈接頂點(diǎn)屬性

??例如我們在頂點(diǎn)輸入時(shí)給出的頂點(diǎn)數(shù)據(jù)應(yīng)該被解析為下面的樣子:


??為了解釋清楚绳矩,我們可以先來回憶一下罩润,在之前討論頂點(diǎn)著色器時(shí),是不是定義了一個(gè)頂點(diǎn)屬性翼馆?那一行源碼是:layout (location = 0) in vec3 aPos;割以。aPos就是一個(gè)頂點(diǎn)屬性的變量(我稱這個(gè)頂點(diǎn)屬性叫坐標(biāo)),它一次接受3個(gè)值应媚。我們要做的就是告知OpenGL如何在頂點(diǎn)數(shù)據(jù)中了連續(xù)篩選出3個(gè)值并存儲(chǔ)成一個(gè)個(gè)坐標(biāo)(在迭代中)拳球。
??為此我們應(yīng)該告知進(jìn)來的頂點(diǎn)數(shù)據(jù)要像上圖一樣解析:告知從數(shù)據(jù)的開始位置直接讀取數(shù)據(jù);告知每3個(gè)值應(yīng)為一個(gè)頂點(diǎn)屬性的大小珍特,且應(yīng)該每次連續(xù)讀取3個(gè)值(沒有空隙)祝峻;在完成一次坐標(biāo)的讀取后,告知下一次讀取位置在哪扎筒,由于我們是只放了坐標(biāo)莱找,沒有其他頂點(diǎn)屬性,所以下一次的位置就是上一次讀取完之后嗜桌。
??現(xiàn)在來看看如何把文字轉(zhuǎn)為代碼實(shí)現(xiàn)奥溺,我們要用glVertexAttribPointer函數(shù)去告知OpenGL該如何解析頂點(diǎn)數(shù)據(jù),為了讓頂點(diǎn)著色器能夠在準(zhǔn)確的位置讀取準(zhǔn)確地讀取到信息骨宠,這個(gè)函數(shù)需要的參數(shù)可以說是很多了浮定。要花較多的筆墨去解釋它們:

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

??它的Signature我就不列出來了,太長了层亿,直接對(duì)應(yīng)上圖解釋參數(shù)的含義吧:

  • 0 第一個(gè)參數(shù)指定我們配置的頂點(diǎn)屬性是哪一個(gè)桦卒,它與定義頂點(diǎn)屬性時(shí)的location對(duì)應(yīng),因?yàn)槲覀兌x的頂點(diǎn)屬性坐標(biāo)的location = 0匿又,所以這里也是0方灾,把數(shù)據(jù)傳遞到aPos這個(gè)頂點(diǎn)屬性中。
  • 3 第二個(gè)參數(shù)指定頂點(diǎn)屬性的大小碌更,由于我們定義頂點(diǎn)屬性時(shí)是個(gè)vec3裕偿,它由3個(gè)值組成,所以是3.
  • GL_FLOAT 第三個(gè)參數(shù)指定頂點(diǎn)屬性的類型痛单,這里是GL_FLOAT(GLSL中vec*都是由浮點(diǎn)數(shù)值組成的)嘿棘。
  • GL_FALSE 第四個(gè)參數(shù)是決定數(shù)據(jù)是否被標(biāo)準(zhǔn)化(Normalize),標(biāo)準(zhǔn)化坐標(biāo)在上面我們也有提到旭绒。如果我們設(shè)置為GL_TRUE鸟妙,所有數(shù)據(jù)都會(huì)被映射到0(對(duì)于有符號(hào)型signed數(shù)據(jù)是-1)到1之間焦人。我們把它設(shè)置為GL_FALSE,因?yàn)槲覀冚斎氲捻旤c(diǎn)數(shù)據(jù)就是標(biāo)準(zhǔn)化坐標(biāo)圆仔。
  • 3 * sizeof(float) 第五個(gè)參數(shù)叫做步長(Stride),這個(gè)參數(shù)告知連續(xù)的頂點(diǎn)屬性組之間的間隔蔫劣,由于下個(gè)組位置數(shù)據(jù)在3個(gè)float之后坪郭,所以設(shè)置為3 * sizeof(float)。一旦我們有更多的頂點(diǎn)屬性脉幢,我們就必須更小心地定義每個(gè)頂點(diǎn)屬性之間的間隔歪沃,
  • (void*)0 最后一個(gè)參數(shù)是void*類型。它表示位置數(shù)據(jù)在緩沖中起始位置的偏移量(Offset)嫌松。但是為什么要進(jìn)行這么奇怪的強(qiáng)制類型轉(zhuǎn)換沪曙?這個(gè)會(huì)在以后詳細(xì)解釋。

Each vertex attribute takes its data from memory managed by a VBO and which VBO it takes its data from (you can have multiple VBOs) is determined by the VBO currently bound to GL_ARRAY_BUFFER when calling glVertexAttribPointer. Since the previously defined VBO is still bound before calling glVertexAttribPointer vertex attribute 0 is now associated with its vertex data.

??每一個(gè)頂點(diǎn)屬性從VBO中獲取它的數(shù)據(jù)萎羔,但是它獲取的是哪一個(gè)VBO中的數(shù)據(jù)呢液走?這一步在glBindBuffer(GL_ARRAY_BUFFER, VBO);綁定VBO時(shí)就已經(jīng)決定。
??現(xiàn)在從VBO到頂點(diǎn)著色器的橋梁已然架好贾陷,需要的只是一聲令下缘眶,這座橋就會(huì)馬上通車,把頂點(diǎn)數(shù)據(jù)源源不斷地髓废、有序地從橋的一頭運(yùn)送到另一頭巷懈。那么充當(dāng)這個(gè)發(fā)令員的就是glEnableVertexAttribArray()函數(shù),它的作用是以頂點(diǎn)屬性位置值(location = 0)作為參數(shù)慌洪,啟用頂點(diǎn)屬性顶燕,因?yàn)轫旤c(diǎn)屬性默認(rèn)是禁用的。

    glEnableVertexAttribArray(0);

??現(xiàn)在冈爹,我們已經(jīng)離終點(diǎn)很近了涌攻,堅(jiān)持住。為了捋順?biāo)悸菲瞪耍覀儗⒄麄€(gè)過程復(fù)述一遍:我們使用VBO數(shù)據(jù)緩沖對(duì)象將從CPU送過來的頂點(diǎn)數(shù)據(jù)儲(chǔ)存在緩沖中癣漆,然后建立頂點(diǎn)著色器和片段著色器,把兩個(gè)著色器鏈接成一個(gè)著色器程序?qū)ο蠹谅颍⒏嬷狾penGL該如何解析在VBO里的頂點(diǎn)數(shù)據(jù)惠爽。


// 0. 復(fù)制頂點(diǎn)數(shù)組到緩沖中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設(shè)置頂點(diǎn)屬性指針
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
// 2. 當(dāng)我們渲染一個(gè)物體時(shí)要使用著色器程序
    glUseProgram(shaderProgram);
// 3. 繪制物體
    someOpenGLFunctionThatDrawsOurTriangle();

??至此我們就能看到一個(gè)橘色的三角形出現(xiàn)視窗中了。但是先別急著看結(jié)果瞬哼,先看看目前代碼仍存在的一點(diǎn)問題:每當(dāng)我們繪制一個(gè)物體時(shí)都要重復(fù)這幾行代碼:綁定VBO婚肆,鏈接頂點(diǎn)屬性,激活頂點(diǎn)屬性坐慰,使用著色器程序较性,繪制物體用僧。試想如果我要繪制很多個(gè)物體,且每個(gè)物體都有很多個(gè)頂點(diǎn)屬性赞咙,那么上述的過程將會(huì)使代碼變得非常冗雜责循,那么有沒有方法,能把這些狀態(tài)配置存儲(chǔ)在一個(gè)對(duì)象中呢攀操?

頂點(diǎn)數(shù)組對(duì)象(Vertex Array Object, VAO)

??我們先來看看VAO究竟能干嘛院仿,于我愚見,VAO有點(diǎn)類似于C#的委托機(jī)制速和,相似的地方在于歹垫,它能幫你自動(dòng)調(diào)用你想調(diào)用的函數(shù),但我覺得比委托更強(qiáng)大颠放,因?yàn)樗€幫你記住了調(diào)用函數(shù)所要用到的參數(shù)排惨,且各個(gè)函數(shù)參數(shù)類型不一,不過要這么方便的使用VAO的前提是:在創(chuàng)建VAO時(shí)先調(diào)用一次對(duì)應(yīng)的函數(shù)碰凶。
??一般一個(gè)VAO對(duì)應(yīng)一個(gè)模型的狀態(tài)配置暮芭,在VAO看來所謂的狀態(tài)配置是指:

  • glEnableVertexAttribArrayglDisableVertexAttribArray的調(diào)用。
  • 通過glVertexAttribPointer設(shè)置的頂點(diǎn)屬性配置欲低。
  • 通過glVertexAttribPointer調(diào)用與頂點(diǎn)屬性關(guān)聯(lián)的頂點(diǎn)緩沖對(duì)象谴麦。
    VAO2示范了多個(gè)頂點(diǎn)屬性時(shí)是怎么記錄的

??能看到一個(gè)VAO最多能存儲(chǔ)16個(gè)頂點(diǎn)屬性。
??創(chuàng)建一個(gè)VAO的操作與創(chuàng)建VBO類似:

    unsigned int VAO;
    glGenVertexArrays(1, &VAO);

??要使用VAO伸头,就要綁定VAO匾效,在綁定了VAO之后,(緊接著)我們應(yīng)該在VAO面前教它如何綁定和配置對(duì)應(yīng)的VBO和鏈接屬性恤磷,在VAO過目不忘的能力下面哼,也不會(huì)辜負(fù)你的期待,日后要是想繪制一個(gè)物體的時(shí)候扫步,我們只要在繪制物體前簡單地把VAO綁定到希望使用的設(shè)定上就行了魔策。

// ..:: 初始化代碼(只運(yùn)行一次 (除非你的物體頻繁改變)) :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把頂點(diǎn)數(shù)組復(fù)制到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

...

// ..:: 繪制代碼(渲染循環(huán)中) :: ..
// 4. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

??一般當(dāng)你打算繪制多個(gè)物體時(shí),你首先要生成/配置所有的VAO(和必須的VBO及屬性指針)河胎,然后儲(chǔ)存它們供后面使用闯袒。當(dāng)我們打算繪制物體的時(shí)候就拿出相應(yīng)的VAO,綁定它游岳,繪制完物體后政敢,再解綁VAO。

Triangle

??千呼萬喚始出來胚迫!我們的三角形快要躍然于屏幕上了喷户。灰常之激動(dòng)是不是访锻?別急褪尝,還有一點(diǎn)手尾需要解決闹获。要想繪制我們想要的物體,OpenGL給我們提供了glDrawArrays()函數(shù)河哑,它使用當(dāng)前激活的著色器避诽,之前定義的頂點(diǎn)屬性配置,和VBO的頂點(diǎn)數(shù)據(jù)(通過VAO間接綁定)來繪制圖元璃谨。

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays(GLenum mode, GLint first, GLsizei count):這個(gè)函數(shù)會(huì)使用當(dāng)前激活的著色器沙庐、配置好的頂點(diǎn)屬性和VBO頂點(diǎn)數(shù)據(jù)來繪制圖元。第一個(gè)參數(shù)指定繪制圖元的類型睬罗,由于我們是打算繪制一個(gè)三角形轨功,所以是GL_TRIANGLES旭斥;第二個(gè)參數(shù)指定了頂點(diǎn)數(shù)組的起始索引容达;第三個(gè)參數(shù)指定了我們打算繪制多少個(gè)頂點(diǎn),這里是3垂券。
??現(xiàn)在嘗試編譯代碼花盐,如果編譯通過了,就能看到三角形啦菇爪!


??需要注意的是算芯,在把源碼灌進(jìn)字符串的時(shí)候,記得加上換行符凳宙,不然編譯會(huì)失敗熙揍,導(dǎo)致只顯示一個(gè)黑色的三角形。

const char * vertexShaderSource =
"#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";    

const char * fragmentShaderSource =
"   #version 330 core                           \n   "
"   out vec4 FragColor;                         \n   "
"   void main()                                 \n   "
"   {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);} \n   ";
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末氏涩,一起剝皮案震驚了整個(gè)濱河市届囚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌是尖,老刑警劉巖意系,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饺汹,居然都是意外死亡蛔添,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門兜辞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來迎瞧,“玉大人,你說我怎么就攤上這事逸吵〖性埽” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵胁塞,是天一觀的道長咏尝。 經(jīng)常有香客問我压语,道長,這世上最難降的妖魔是什么编检? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任胎食,我火速辦了婚禮,結(jié)果婚禮上允懂,老公的妹妹穿的比我還像新娘厕怜。我一直安慰自己,他們只是感情好蕾总,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布粥航。 她就那樣靜靜地躺著,像睡著了一般生百。 火紅的嫁衣襯著肌膚如雪递雀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天蚀浆,我揣著相機(jī)與錄音缀程,去河邊找鬼。 笑死市俊,一個(gè)胖子當(dāng)著我的面吹牛杨凑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摆昧,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼撩满,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绅你?” 一聲冷哼從身側(cè)響起伺帘,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勇吊,沒想到半個(gè)月后曼追,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汉规,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年礼殊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片针史。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晶伦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啄枕,到底是詐尸還是另有隱情婚陪,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布频祝,位于F島的核電站泌参,受9級(jí)特大地震影響脆淹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沽一,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一盖溺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铣缠,春花似錦烘嘱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捡硅,卻和暖如春哮内,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背病曾。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國打工牍蜂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漾根,地道東北人泰涂。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像辐怕,于是被迫代替她去往敵國和親逼蒙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354