你好刽辙,三角形
圖形渲染管線(Pipeline)
3D坐標(biāo)轉(zhuǎn)為2D坐標(biāo)的處理過(guò)程是由OpenGL的圖形渲染管線(Pipeline斟叼,大多譯為管線纸肉,實(shí)際上指的是一堆原始圖形數(shù)據(jù)途經(jīng)一個(gè)輸送管道端逼,期間經(jīng)過(guò)各種變化處理最終出現(xiàn)在屏幕的過(guò)程)管理的颜矿。圖形渲染管線可以被劃分為兩個(gè)主要部分:第一個(gè)部分把你的3D坐標(biāo)轉(zhuǎn)換為2D坐標(biāo)洗贰,第二部分是把2D坐標(biāo)轉(zhuǎn)變?yōu)閷?shí)際的有顏色的像素找岖。
圖形渲染管線可以被劃分為幾個(gè)階段,每個(gè)階段需要把前一個(gè)階段的輸出作為輸入敛滋。所有這些階段都是高度專門化的(它們有一個(gè)特定的函數(shù))许布,它們能簡(jiǎn)單地并行執(zhí)行。由于它們的并行執(zhí)行特性绎晃,當(dāng)今大多數(shù)顯卡都有成千上萬(wàn)的小處理核心蜜唾,在GPU上為每一個(gè)(渲染管線)階段運(yùn)行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)庶艾。這些小程序叫做 著色器(Shader)袁余。因?yàn)樗鼈冞\(yùn)行在GPU上,所以它們會(huì)節(jié)約寶貴的CPU時(shí)間咱揍。
下圖是一個(gè)圖形渲染管線的每個(gè)階段的抽象表達(dá),藍(lán)色部分代表的是我們可以自定義的著色器泌霍。
我們以數(shù)組的形式傳遞3個(gè)3D坐標(biāo)作為圖形渲染管線的輸入,它用來(lái)表示一個(gè)三角形述召,這個(gè)數(shù)組叫做頂點(diǎn)數(shù)據(jù)(Vertex Data)朱转;這里頂點(diǎn)數(shù)據(jù)是一些頂點(diǎn)的集合。一個(gè)頂點(diǎn)是一個(gè)3D坐標(biāo)的集合(也就是x积暖、y藤为、z數(shù)據(jù))。而頂點(diǎn)數(shù)據(jù)是用頂點(diǎn)屬性(Vertex Attributes)表示的夺刑,它可以包含任何我們希望用的數(shù)據(jù)缅疟,但是簡(jiǎn)單起見,我們還是假定每個(gè)頂點(diǎn)只由一個(gè)3D位置(譯注1)和幾個(gè)顏色值組成的吧遍愿。
為了讓OpenGL知道我們的坐標(biāo)和顏色值構(gòu)成的到底是什么存淫,OpenGL需要你去提示你希望這些數(shù)據(jù)所表示的是什么類型。我們是希望把這些數(shù)據(jù)渲染成一系列的點(diǎn)沼填?一系列的三角形桅咆?還是僅僅是一個(gè)長(zhǎng)長(zhǎng)的線?做出的這些提示叫做圖元(Primitives)****坞笙,任何一個(gè)繪制命令的調(diào)用都必須把基本圖形類型傳遞給OpenGL岩饼。這是其中的幾個(gè):GL_POINTS荚虚、GL_TRIANGLES、GL_LINE_STRIP籍茧。
- 圖形渲染管線的第一個(gè)部分是頂點(diǎn)著色器(Vertex Shader)版述,它把一個(gè)單獨(dú)的頂點(diǎn)作為輸入。頂點(diǎn)著色器主要的目的是把3D坐標(biāo)轉(zhuǎn)為另一種3D坐標(biāo)(后面會(huì)解釋)寞冯,同時(shí)頂點(diǎn)著色器允許我們對(duì)頂點(diǎn)屬性進(jìn)行一些基本處理渴析。
- 基本圖元裝配(Primitive Assembly)階段把頂點(diǎn)著色器的表示為基本圖形的所有頂點(diǎn)作為輸入(如果選擇的是GL_POINTS,那么就是一個(gè)單獨(dú)頂點(diǎn))吮龄,把所有點(diǎn)組裝為特定的基本圖形的形狀俭茧;本節(jié)例子是一個(gè)三角形。
- 基本圖形裝配階段的輸出會(huì)傳遞給幾何著色器(Geometry Shader)螟蝙。幾何著色器把基本圖形形式的一系列頂點(diǎn)的集合作為輸入恢恼,它可以通過(guò)產(chǎn)生新頂點(diǎn)構(gòu)造出新的(或是其他的)基本圖形來(lái)生成其他形狀民傻。例子中胰默,它生成了另一個(gè)三角形。
- 細(xì)分著色器(Tessellation Shaders)擁有把給定基本圖形細(xì)分為更多小基本圖形的能力漓踢。這樣我們就能在物體更接近玩家的時(shí)候通過(guò)創(chuàng)建更多的三角形的方式創(chuàng)建出更加平滑的視覺(jué)效果牵署。
- 細(xì)分著色器的輸出會(huì)進(jìn)入光柵化(Rasterization也譯為像素化)階段,這里它會(huì)把基本圖形映射為屏幕上相應(yīng)的像素喧半,生成供片段著色器(Fragment Shader)使用的片段(Fragment)奴迅。在片段著色器運(yùn)行之前,會(huì)執(zhí)行裁切(Clipping)挺据。裁切會(huì)丟棄超出你的視圖以外的那些像素取具,來(lái)提升執(zhí)行效率。(OpenGL中的一個(gè)fragment是OpenGL渲染一個(gè)獨(dú)立像素所需的所有數(shù)據(jù)扁耐。)
- 片段著色器(Fragrament Shader)的主要目的是計(jì)算一個(gè)像素的最終顏色暇检,這也是OpenGL高級(jí)效果產(chǎn)生的地方。通常婉称,片段著色器包含用來(lái)計(jì)算像素最終顏色的3D場(chǎng)景的一些數(shù)據(jù)(比如光照块仆、陰影、光的顏色等等)王暗。
- 在所有相應(yīng)顏色值確定以后悔据,最終它會(huì)傳到另一個(gè)階段,我們叫做alpha測(cè)試和混合(Blending)階段俗壹。這個(gè)階段檢測(cè)像素的相應(yīng)的深度(和Stencil)值(后面會(huì)講)科汗,使用這些,來(lái)檢查這個(gè)像素是否在另一個(gè)物體的前面或后面绷雏,如此做到相應(yīng)取舍肛捍。這個(gè)階段也會(huì)檢查alpha值(alpha值是一個(gè)物體的透明度值)和物體之間的混合(Blend)隐绵。所以,即使在片段著色器中計(jì)算出來(lái)了一個(gè)像素所輸出的顏色拙毫,最后的像素顏色在渲染多個(gè)三角形的時(shí)候也可能完全不同依许。
對(duì)于大多數(shù)場(chǎng)合,我們必須做的只是頂點(diǎn)和片段著色器缀蹄。幾何著色器和細(xì)分著色器是可選的峭跳,通常使用默認(rèn)的著色器就行了。
在現(xiàn)代OpenGL中缺前,我們必須定義至少一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器(因?yàn)镚PU中沒(méi)有默認(rèn)的頂點(diǎn)/片段著色器)蛀醉。出于這個(gè)原因,開始學(xué)習(xí)現(xiàn)代OpenGL的時(shí)候非常困難衅码,因?yàn)樵谀隳軌蜾秩咀约旱牡谝粋€(gè)三角形之前需要一大堆知識(shí)拯刁。本節(jié)結(jié)束就是你可以最終渲染出你的三角形的時(shí)候,你也會(huì)了解到很多圖形編程知識(shí)逝段。
頂點(diǎn)輸入(Vertex Input)
我們?cè)贠penGL中指定的所有坐標(biāo)都是在3D坐標(biāo)里(x垛玻、y和z)。OpenGL只是在當(dāng)它們的3個(gè)軸(x奶躯、y和z)在特定的-1.0到1.0的范圍內(nèi)時(shí)才處理3D坐標(biāo)帚桩。所有在這個(gè)范圍內(nèi)的坐標(biāo)叫做標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates,NDC)會(huì)最終顯示在你的屏幕上(所有出了這個(gè)范圍的都不會(huì)顯示)嘹黔。
我們希望渲染一個(gè)三角形账嚎,我們把它們以GLfloat數(shù)組的方式定義為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(也就是在OpenGL的可見區(qū)域)中。
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
由于OpenGL是在3D空間中工作的儡蔓,我們渲染一個(gè)2D三角形郭蕉,它的每個(gè)頂點(diǎn)都要有同一個(gè)z坐標(biāo)0.0。在這樣的方式中喂江,三角形的每一處的深度都一樣召锈,從而使它看上去就像2D的。
你的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)接著會(huì)變換為屏幕空間坐標(biāo)(Screen-space Coordinates)开呐,這是使用你通過(guò)glViewport函數(shù)提供的數(shù)據(jù)烟勋,進(jìn)行視口變換(Viewport Transform)完成的。
有了這樣的頂點(diǎn)數(shù)據(jù)筐付,我們會(huì)把它作為輸入數(shù)據(jù)發(fā)送給圖形渲染管線的第一個(gè)處理階段:頂點(diǎn)著色器卵惦。它會(huì)在GPU上創(chuàng)建儲(chǔ)存空間用于儲(chǔ)存我們的頂點(diǎn)數(shù)據(jù),還要配置OpenGL如何解釋這些內(nèi)存瓦戚,并且指定如何發(fā)送給顯卡沮尿。頂點(diǎn)著色器接著會(huì)處理我們告訴它要處理內(nèi)存中的頂點(diǎn)的數(shù)量。
我們通過(guò)頂點(diǎn)緩沖對(duì)象(Vertex Buffer Objects, VBO)管理這個(gè)內(nèi)存,它會(huì)在GPU內(nèi)存(通常被稱為顯存)中儲(chǔ)存大批頂點(diǎn)畜疾。使用這些緩沖對(duì)象的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上赴邻,而不是每個(gè)頂點(diǎn)發(fā)送一次。
就像OpenGL中的其他對(duì)象一樣啡捶,這個(gè)緩沖有一個(gè)獨(dú)一無(wú)二的ID姥敛,所以我們可以使用glGenBuffers函數(shù)生成一個(gè)緩沖ID:
GLuint VBO;
glGenBuffers(1, &VBO);
OpenGL有很多緩沖對(duì)象類型,GL_ARRAY_BUFFER是其中一個(gè)頂點(diǎn)緩沖對(duì)象的緩沖類型瞎暑。OpenGL允許我們同時(shí)綁定多個(gè)緩沖彤敛,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數(shù)把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
從這一刻起了赌,我們使用的任何緩沖函數(shù)(在GL_ARRAY_BUFFER目標(biāo)上)都會(huì)用來(lái)配置當(dāng)前綁定的緩沖(VBO)墨榄。然后我們可以調(diào)用glBufferData函數(shù),它會(huì)把之前定義的頂點(diǎn)數(shù)據(jù)復(fù)制到緩沖的內(nèi)存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一個(gè)用來(lái)把用戶定的義數(shù)據(jù)復(fù)制到當(dāng)前綁定緩沖的函數(shù)勿她。它的第一個(gè)參數(shù)是我們希望把數(shù)據(jù)復(fù)制到上面的緩沖類型:頂點(diǎn)緩沖對(duì)象當(dāng)前綁定到GL_ARRAY_BUFFER目標(biāo)上袄秩。第二個(gè)參數(shù)指定我們希望傳遞給緩沖的數(shù)據(jù)的大小(以字節(jié)為單位);用一個(gè)簡(jiǎn)單的sizeof計(jì)算出頂點(diǎn)數(shù)據(jù)就行逢并。第三個(gè)參數(shù)是我們希望發(fā)送的真實(shí)數(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ì)改變箱沦。
三角形的位置數(shù)據(jù)不會(huì)改變辩恼,每次渲染調(diào)用時(shí)都保持原樣,所以它使用的類型最好是GL_STATIC_DRAW谓形。如果灶伊,比如,一個(gè)緩沖中的數(shù)據(jù)將頻繁被改變寒跳,那么使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW聘萨。這樣就能確保圖形卡把數(shù)據(jù)放在高速寫入的內(nèi)存部分。
到現(xiàn)在我們已經(jīng)把頂點(diǎn)數(shù)據(jù)儲(chǔ)存在顯卡的內(nèi)存中童太,并且用VBO頂點(diǎn)緩沖對(duì)象來(lái)管理米辐。下面我們會(huì)創(chuàng)建一個(gè)頂點(diǎn)和片段著色器,來(lái)處理這些數(shù)據(jù)书释。
頂點(diǎn)著色器(Vertex Shader)
我們需要做的第一件事是用著色器語(yǔ)言GLSL寫頂點(diǎn)著色器翘贮,然后編譯這個(gè)著色器,
#version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
每個(gè)著色器都起始于一個(gè)版本聲明爆惧。我們同樣顯式地表示我們會(huì)用核心模式(Core-profile)狸页。
下一步,我們?cè)陧旤c(diǎn)著色器中聲明所有的輸入頂點(diǎn)屬性扯再,使用in關(guān)鍵字∩衷牛現(xiàn)在我們只關(guān)心位置(Position)數(shù)據(jù)址遇,所以我們只需要一個(gè)頂點(diǎn)屬性(Attribute)。
GLSL有一個(gè)向量數(shù)據(jù)類型(vecn)斋竞,它包含1到4個(gè)float元素倔约,包含的數(shù)量可以從它的后綴看出來(lái)。由于每個(gè)頂點(diǎn)都有一個(gè)3D坐標(biāo)坝初,我們就創(chuàng)建一個(gè)vec3輸入變量來(lái)表示位置(Position)跺株。
我們同樣也指定輸入變量的位置值(Location),這是用layout (location = 0)來(lái)完成的脖卖。
在GLSL中一個(gè)向量有最多4個(gè)元素乒省,每個(gè)元素值都可以從各自代表一個(gè)空間坐標(biāo)的vec.x、vec.y畦木、vec.z和vec.w來(lái)獲取到袖扛。
為了設(shè)置頂點(diǎn)著色器的輸出,我們必須把位置數(shù)據(jù)賦值給預(yù)定義的gl_Position變量十籍,這個(gè)位置數(shù)據(jù)是一個(gè)vec4類型的蛆封。在main函數(shù)的最后,無(wú)論我們給gl_Position設(shè)置成什么勾栗,它都會(huì)成為著色器的輸出惨篱。
這個(gè)頂點(diǎn)著色器可能是能想到的最簡(jiǎn)單的了,因?yàn)槲覀兪裁炊紱](méi)有處理就把輸入數(shù)據(jù)輸出了围俘。在真實(shí)的應(yīng)用里輸入數(shù)據(jù)通常都沒(méi)有在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中砸讳,所以我們首先就必須把它們放進(jìn)OpenGL的可視區(qū)域內(nèi)。
編譯一個(gè)著色器
我們已經(jīng)寫了一個(gè)頂點(diǎn)著色器源碼界牡,但是為了OpenGL能夠使用它簿寂,我們必須在運(yùn)行時(shí)動(dòng)態(tài)編譯它的源碼。
- 我們要做的第一件事是創(chuàng)建一個(gè)著色器對(duì)象宿亡,再次引用它的ID常遂。所以我們儲(chǔ)存這個(gè)頂點(diǎn)著色器為GLuint,然后用glCreateShader創(chuàng)建著色器:
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我們把著色器的類型提供glCreateShader作為它的參數(shù)挽荠。這里我們傳遞的參數(shù)是GL_VERTEX_SHADER這樣就創(chuàng)建了一個(gè)頂點(diǎn)著色器克胳。
- 下一步我們把這個(gè)著色器源碼附加到著色器對(duì)象钳吟,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource函數(shù)把著色器對(duì)象作為第一個(gè)參數(shù)來(lái)編譯它怪嫌。第二參數(shù)指定了源碼中有多少個(gè)字符串,這里只有一個(gè)署照。第三個(gè)參數(shù)是頂點(diǎn)著色器真正的源碼臭脓,我們可以把第四個(gè)參數(shù)先設(shè)置為NULL酗钞。
你可能會(huì)希望檢測(cè)調(diào)用glCompileShader后是否編譯成功了,是否要去修正錯(cuò)誤。檢測(cè)編譯時(shí)錯(cuò)誤的方法是:
GLint success;
GLchar infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
首先我們定義一個(gè)整型來(lái)表示是否成功編譯砚作,還需要一個(gè)儲(chǔ)存錯(cuò)誤消息的容器(如果有的話)窘奏。然后我們用glGetShaderiv檢查是否編譯成功了。如果編譯失敗葫录,我們應(yīng)該用glGetShaderInfoLog獲取錯(cuò)誤消息着裹,然后打印它。
如果編譯的時(shí)候沒(méi)有任何錯(cuò)誤米同,頂點(diǎn)著色器就被編譯成功了骇扇。
片段著色器(Fragment Shader)
片段著色器的全部,都是用來(lái)計(jì)算你的像素的最后顏色輸出面粮。為了讓事情比較簡(jiǎn)單少孝,我們的片段著色器只輸出橘黃色。
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
片段著色器只需要一個(gè)輸出變量熬苍,這個(gè)變量是一個(gè)4元素表示的最終輸出顏色的向量稍走。用out關(guān)鍵字聲明輸出變量,這里我們命名為color柴底。下面婿脸,我們簡(jiǎn)單的把一個(gè)帶有alpha值為1.0(1.0代表完全不透明)的橘黃的vec4賦值給color作為輸出。
編譯片段著色器的過(guò)程與頂點(diǎn)著色器相似柄驻,盡管這次我們使用GL_FRAGMENT_SHADER作為著色器類型:
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);
每個(gè)著色器現(xiàn)在都編譯了狐树,剩下的事情是把兩個(gè)著色器對(duì)象鏈接到一個(gè)著色器程序中(Shader Program),它是用來(lái)渲染的鸿脓。
著色器程序(Shader program)
著色器程序?qū)ο?Shader Program Object)是多個(gè)著色器最后鏈接的版本抑钟。如果要使用剛才編譯的著色器我們必須把它們鏈接為一個(gè)著色器程序?qū)ο螅缓螽?dāng)渲染物體的時(shí)候激活這個(gè)著色器程序答憔。激活了的著色器程序的著色器味赃,在調(diào)用渲染函數(shù)時(shí)才可用掀抹。
把著色器鏈接為一個(gè)程序就等于把每個(gè)著色器的輸出鏈接到下一個(gè)著色器的輸入虐拓。如果你的輸出和輸入不匹配那么就會(huì)得到一個(gè)鏈接錯(cuò)誤。
創(chuàng)建一個(gè)程序?qū)ο蠛芎?jiǎn)單:
GLuint shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram函數(shù)創(chuàng)建一個(gè)程序傲武,返回新創(chuàng)建的程序?qū)ο蟮腎D引用∪鼐裕現(xiàn)在我們需要把前面編譯的著色器附加到程序?qū)ο笊希缓笥胓lLinkProgram鏈接它們:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
就像著色器的編譯一樣揪利,我們也可以檢驗(yàn)鏈接著色器程序是否失敗态兴,獲得相應(yīng)的日志。與glGetShaderiv和glGetShaderInfoLog不同疟位,現(xiàn)在我們使用:
GLint success;
GLchar infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
我們可以調(diào)用glUseProgram函數(shù)瞻润,用新創(chuàng)建的程序?qū)ο笞鳛樗膮?shù),這樣就能激活這個(gè)程序?qū)ο螅?/p>
glUseProgram(shaderProgram);
現(xiàn)在在glUseProgram函數(shù)調(diào)用之后的每個(gè)著色器和渲染函數(shù)都會(huì)用到這個(gè)程序?qū)ο?當(dāng)然還有這些鏈接的著色器)了。
在我們把著色器對(duì)象鏈接到程序?qū)ο笠院笊茏玻灰泟h除著色器對(duì)象正勒;我們不再需要它們了:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
現(xiàn)在,我們把輸入頂點(diǎn)數(shù)據(jù)發(fā)送給GPU傻铣,指示GPU如何在頂點(diǎn)和片段著色器中處理它章贞。還沒(méi)結(jié)束,OpenGL還不知道如何解釋內(nèi)存中的頂點(diǎn)數(shù)據(jù)非洲,以及怎樣把頂點(diǎn)數(shù)據(jù)鏈接到頂點(diǎn)著色器的屬性上鸭限。我們需要告訴OpenGL怎么做。
鏈接頂點(diǎn)屬性
頂點(diǎn)著色器允許我們以任何我們想要的形式作為頂點(diǎn)屬性(Vertex Attribute)的輸入两踏,同樣它也具有很強(qiáng)的靈活性败京,這意味著我們必須手動(dòng)指定我們的輸入數(shù)據(jù)的哪一個(gè)部分對(duì)應(yīng)頂點(diǎn)著色器的哪一個(gè)頂點(diǎn)屬性。這意味著我們必須在渲染前指定OpenGL如何解釋頂點(diǎn)數(shù)據(jù)梦染。
我們的頂點(diǎn)緩沖數(shù)據(jù)被格式化為下面的形式:
- 位置數(shù)據(jù)被儲(chǔ)存為32-bit(4 byte)浮點(diǎn)值喧枷。
- 每個(gè)位置包含3個(gè)這樣的值。
- 在這3個(gè)值之間沒(méi)有空隙(或其他值)弓坞。這幾個(gè)值緊密排列為一個(gè)數(shù)組隧甚。
- 數(shù)據(jù)中第一個(gè)值是緩沖的開始位置。
有了這些信息我們就可以告訴OpenGL如何解釋頂點(diǎn)數(shù)據(jù)了(每一個(gè)頂點(diǎn)屬性)渡冻,我們使用glVertexAttribPointer這個(gè)函數(shù)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);:
glVertexAttribPointer函數(shù)有很多參數(shù)戚扳,所以我們仔細(xì)來(lái)了解它們:
- 第一個(gè)參數(shù)指定我們要配置哪一個(gè)頂點(diǎn)屬性。記住族吻,我們?cè)陧旤c(diǎn)著色器中使用layout(location = 0)定義了頂點(diǎn)屬性——位置(position)的位置值(Location)帽借。這樣要把頂點(diǎn)屬性的位置值(Location)設(shè)置為0,因?yàn)槲覀兿M褦?shù)據(jù)傳遞到這個(gè)頂點(diǎn)屬性中超歌,所以我們?cè)谶@里填0砍艾。
- 第二個(gè)參數(shù)指定頂點(diǎn)屬性的大小。頂點(diǎn)屬性是vec3類型巍举,它由3個(gè)數(shù)值組成脆荷。
- 第三個(gè)參數(shù)指定數(shù)據(jù)的類型,這里是GL_FLOAT(GLSL中vec*是由浮點(diǎn)數(shù)組成的)懊悯。
- 下個(gè)參數(shù)定義我們是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化蜓谋。如果我們?cè)O(shè)置為GL_TRUE,所有數(shù)據(jù)都會(huì)被映射到0(對(duì)于有符號(hào)型signed數(shù)據(jù)是-1)到1之間炭分。我們把它設(shè)置為GL_FALSE桃焕。
- 第五個(gè)參數(shù)叫做步長(zhǎng)(Stride),它告訴我們?cè)谶B續(xù)的頂點(diǎn)屬性之間間隔有多少捧毛。由于下個(gè)位置數(shù)據(jù)在3個(gè)GLfloat后面的位置观堂,我們把步長(zhǎng)設(shè)置為3 * sizeof(GLfloat)让网。要注意的是由于我們知道這個(gè)數(shù)組是緊密排列的(在兩個(gè)頂點(diǎn)屬性之間沒(méi)有空隙)我們也可以設(shè)置為0來(lái)讓OpenGL決定具體步長(zhǎng)是多少(只有當(dāng)數(shù)值是緊密排列時(shí)才可用)。每當(dāng)我們有更多的頂點(diǎn)屬性师痕,我們就必須小心地定義每個(gè)頂點(diǎn)屬性之間的空間寂祥,我們?cè)诤竺鏁?huì)看到更多的例子(譯注: 這個(gè)參數(shù)的意思簡(jiǎn)單說(shuō)就是從這個(gè)屬性第二次出現(xiàn)的地方到整個(gè)數(shù)組0位置之間有多少字節(jié))。
- 最后一個(gè)參數(shù)有奇怪的GLvoid*的強(qiáng)制類型轉(zhuǎn)換七兜。它表示我們的位置數(shù)據(jù)在緩沖中起始位置的偏移量丸凭。由于位置數(shù)據(jù)是數(shù)組的開始,所以這里是0腕铸。我們會(huì)在后面詳細(xì)解釋這個(gè)參數(shù)惜犀。
每個(gè)頂點(diǎn)屬性從VBO管理的內(nèi)存中獲得它的數(shù)據(jù),它所獲取數(shù)據(jù)的那個(gè)VBO狠裹,就是當(dāng)調(diào)用glVetexAttribPointer的時(shí)候虽界,最近綁定到GL_ARRAY_BUFFER的那個(gè)VBO。由于在調(diào)用glVertexAttribPointer之前綁定了VBO涛菠,頂點(diǎn)屬性0(position屬性)現(xiàn)在鏈接到了它的頂點(diǎn)數(shù)據(jù)莉御。
現(xiàn)在我們定義了OpenGL如何解釋頂點(diǎn)數(shù)據(jù),我們也要開啟頂點(diǎn)屬性俗冻,使用glEnableVertexAttribArray礁叔,把頂點(diǎn)屬性位置值作為它的參數(shù);頂點(diǎn)屬性默認(rèn)是關(guān)閉的迄薄。
glEnableVertexAttribArray (0);
自此琅关,我們把每件事都做好了:我們使用一個(gè)頂點(diǎn)緩沖對(duì)象初始化了一個(gè)緩沖中的頂點(diǎn)數(shù)據(jù),設(shè)置了一個(gè)頂點(diǎn)和片段著色器讥蔽,告訴了OpenGL如何把頂點(diǎn)數(shù)據(jù)鏈接到頂點(diǎn)著色器的頂點(diǎn)屬性上涣易。在OpenGL繪制一個(gè)物體,看起來(lái)會(huì)像是這樣:
// 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(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 2. 當(dāng)我們打算渲染一個(gè)物體時(shí)要使用著色器程序
glUseProgram(shaderProgram);
// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
我們繪制一個(gè)物體的時(shí)候必須重復(fù)這件事冶伞。這看起來(lái)也不多新症,但是如果有超過(guò)5個(gè)頂點(diǎn)屬性,100多個(gè)不同物體呢(這其實(shí)并不罕見)响禽。綁定合適的緩沖對(duì)象徒爹,為每個(gè)物體配置所有頂點(diǎn)屬性很快就變成一件麻煩事。有沒(méi)有一些方法可以使我們把所有的配置儲(chǔ)存在一個(gè)對(duì)象中金抡,并且可以通過(guò)綁定這個(gè)對(duì)象來(lái)恢復(fù)狀態(tài)瀑焦?
頂點(diǎn)數(shù)組對(duì)象(Vertex Array Object, VAO)
頂點(diǎn)數(shù)組對(duì)象(Vertex Array Object, VAO)可以像頂點(diǎn)緩沖對(duì)象一樣綁定,任何隨后的頂點(diǎn)屬性調(diào)用都會(huì)儲(chǔ)存在這個(gè)VAO中梗肝。這有一個(gè)好處,當(dāng)配置頂點(diǎn)屬性指針時(shí)铺董,你只用做一次巫击,每次繪制一個(gè)物體的時(shí)候禀晓,我們綁定相應(yīng)VAO就行了。切換不同頂點(diǎn)數(shù)據(jù)和屬性配置就像綁定一個(gè)不同的VAO一樣簡(jiǎn)單坝锰。所有狀態(tài)我們都放到了VAO里粹懒。
OpenGL核心模式版要求我們使用VAO,這樣它就能知道對(duì)我們的頂點(diǎn)輸入做些什么顷级。如果我們綁定VAO失敗凫乖,OpenGL會(huì)拒絕繪制任何東西。
一個(gè)頂點(diǎn)數(shù)組對(duì)象儲(chǔ)存下面的內(nèi)容:
- 調(diào)用glEnableVertexAttribArray和glDisableVertexAttribArray弓颈。
- 使用glVertexAttribPointer的頂點(diǎn)屬性配置帽芽。
-
使用glVertexAttribPointer進(jìn)行的頂點(diǎn)緩沖對(duì)象與頂點(diǎn)屬性鏈接。
生成一個(gè)VAO和生成VBO類似:
GLuint VAO;
glGenVertexArrays(1, &VAO);
使用VAO要做的全部就是使用glBindVertexArray綁定VAO翔冀。自此我們就應(yīng)該綁定/配置相應(yīng)的VBO和屬性指針导街,然后解綁VAO以備后用。當(dāng)我們打算繪制一個(gè)物體的時(shí)候纤子,我們只要在繪制物體前簡(jiǎn)單地把VAO綁定到希望用到的配置就行了搬瑰。這段代碼應(yīng)該看起來(lái)像這樣:
// ..:: 初始化代碼 (一次完成 (除非你的物體頻繁改變)) :: ..
// 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(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);
//4. 解綁VAO
glBindVertexArray(0);
[...]
// ..:: 繪制代碼 (in Game loop) :: ..
// 5. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
通常情況下當(dāng)我們配置好它們以后要解綁OpenGL對(duì)象,這樣我們才不會(huì)在某處錯(cuò)誤地配置它們控硼。
前面做的一切都是等待這一刻泽论,我們已經(jīng)把我們的頂點(diǎn)屬性配置和打算使用的VBO儲(chǔ)存到一個(gè)VAO中。一般當(dāng)你有多個(gè)物體打算繪制時(shí)卡乾,你首先要生成/配置所有的VAO(它需要VBO和屬性指針)佩厚,然后儲(chǔ)存它們準(zhǔn)備后面使用。當(dāng)我們打算繪制物體的時(shí)候就拿出相應(yīng)的VAO说订,綁定它抄瓦,繪制完物體后,再解綁VAO陶冷。
我們一直期待的三角形
OpenGL的glDrawArrays函數(shù)為我們提供了繪制物體的能力钙姊,它使用當(dāng)前激活的著色器、前面定義的頂點(diǎn)屬性配置和VBO的頂點(diǎn)數(shù)據(jù)(通過(guò)VAO間接綁定)來(lái)繪制基本圖形埂伦。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glDrawArrays函數(shù)第一個(gè)參數(shù)是我們打算繪制的OpenGL基本圖形的類型煞额。由于我們?cè)谝婚_始時(shí)說(shuō)過(guò),我們希望繪制三角形沾谜,我們傳遞GL_TRIANGLES給它膊毁。第二個(gè)參數(shù)定義了我們打算繪制的那個(gè)頂點(diǎn)數(shù)組的起始位置的索引;我們這里填0基跑。最后一個(gè)參數(shù)指定我們打算繪制多少個(gè)頂點(diǎn)婚温,這里是3(我們只從我們的數(shù)據(jù)渲染一個(gè)三角形,它只有3個(gè)頂點(diǎn))媳否。
如果你編譯通過(guò)了栅螟,你應(yīng)該看到下面的結(jié)果:
完整的程序源碼可以在這里找到荆秦。
索引緩沖對(duì)象(Element Buffer Objects,EBO)
假設(shè)我們不再繪制一個(gè)三角形而是矩形力图。我們就可以繪制兩個(gè)三角形來(lái)組成一個(gè)矩形(OpenGL主要就是繪制三角形)步绸。這會(huì)生成下面的頂點(diǎn)的集合:
GLfloat vertices[] = {
// 第一個(gè)三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二個(gè)三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
有幾個(gè)頂點(diǎn)疊加了。我們指定右下角和左上角兩次吃媒!一個(gè)矩形只有4個(gè)而不是6個(gè)頂點(diǎn)瓤介,這樣就產(chǎn)生50%的額外開銷。最好的解決方案就是每個(gè)頂點(diǎn)只儲(chǔ)存一次赘那。一個(gè)EBO是一個(gè)像頂點(diǎn)緩沖對(duì)象(VBO)一樣的緩沖刑桑,它專門儲(chǔ)存索引,OpenGL調(diào)用這些頂點(diǎn)的索引來(lái)繪制漓概。這樣每個(gè)頂點(diǎn)只儲(chǔ)存一次漾月,當(dāng)我們打算繪制這些頂點(diǎn)的時(shí)候只調(diào)用頂點(diǎn)的索引。
我們先要定義(獨(dú)一無(wú)二的)頂點(diǎn)胃珍,和繪制出矩形的索引:
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
GLuint indices[] = { // 起始于0!
0, 1, 3, // 第一個(gè)三角形
1, 2, 3 // 第二個(gè)三角形
};
下一步我們需要?jiǎng)?chuàng)建索引緩沖對(duì)象:
GLuint EBO;
glGenBuffers(1, &EBO);
我們綁定EBO然后用glBufferData把索引復(fù)制到緩沖里梁肿。這次我們把緩沖的類型定義為GL_ELEMENT_ARRAY_BUFFER。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
要注意的是觅彰,我們現(xiàn)在用GL_ELEMENT_ARRAY_BUFFER當(dāng)作緩沖目標(biāo)吩蔑。最后一件要做的事是用glDrawElements來(lái)替換glDrawArrays函數(shù),來(lái)指明我們從索引緩沖渲染填抬。當(dāng)使用glDrawElements的時(shí)候烛芬,我們就會(huì)用當(dāng)前綁定的索引緩沖進(jìn)行繪制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一個(gè)參數(shù)指定了我們繪制的模式,這個(gè)和glDrawArrays的一樣飒责。第二個(gè)參數(shù)是我們打算繪制頂點(diǎn)的次數(shù)赘娄。我們填6,說(shuō)明我們總共想繪制6個(gè)頂點(diǎn)宏蛉。第三個(gè)參數(shù)是索引的類型遣臼,這里是GL_UNSIGNED_INT。最后一個(gè)參數(shù)里我們可以指定EBO中的偏移量(或者傳遞一個(gè)索引數(shù)組拾并,但是這只是當(dāng)你不是在使用索引緩沖對(duì)象的時(shí)候)揍堰,但是我們只打算在這里填寫0。
glDrawElements函數(shù)從當(dāng)前綁定到GL_ELEMENT_ARRAY_BUFFER目標(biāo)的EBO獲取索引嗅义。這意味著我們必須在每次要用索引渲染一個(gè)物體時(shí)綁定相應(yīng)的EBO屏歹,這還是有點(diǎn)麻煩。不過(guò)頂點(diǎn)數(shù)組對(duì)象仍可以保存索引緩沖對(duì)象的綁定狀態(tài)之碗。VAO綁定之后可以索引緩沖對(duì)象蝙眶,EBO就成為了VAO的索引緩沖對(duì)象。再次綁定VAO的同時(shí)也會(huì)自動(dòng)綁定EBO继控。
當(dāng)目標(biāo)是GL_ELEMENT_ARRAY_BUFFER的時(shí)候械馆,VAO儲(chǔ)存了glBindBuffer的函數(shù)調(diào)用胖眷。這也意味著它也會(huì)儲(chǔ)存解綁調(diào)用武通,所以確保你沒(méi)有在解綁VAO之前解綁索引數(shù)組緩沖霹崎,否則就沒(méi)有這個(gè)EBO配置了。(WHY???????????????)
最后的初始化和繪制代碼現(xiàn)在看起來(lái)像這樣:
// ..:: 初始化代碼 :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把我們的頂點(diǎn)數(shù)組復(fù)制到一個(gè)頂點(diǎn)緩沖中冶忱,提供給OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 復(fù)制我們的索引數(shù)組到一個(gè)索引緩沖中尾菇,提供給OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW);
// 3. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);
// 4. 解綁VAO,不解綁EBO(譯注:解綁緩沖相當(dāng)于沒(méi)有綁定緩沖囚枪,可以在解綁VAO之后解綁緩沖)
glBindVertexArray(0);
[...]
// ..:: 繪制代碼(在游戲循環(huán)中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
運(yùn)行程序會(huì)獲得下面這樣的圖片的結(jié)果派诬。上面的圖片看起來(lái)很熟悉,而下面的則是使用線框模式(Wireframe Mode)繪制的链沼。線框矩形可以顯示出矩形的確是由兩個(gè)三角形組成的默赂。
線框模式(Wireframe Mode)
如果用線框模式繪制你的三角,你可以通過(guò)調(diào)用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)來(lái)配置OpenGL繪制用的基本圖形括勺。第一個(gè)參數(shù)說(shuō):我們打算應(yīng)用到所有的三角形的前面和背面缆八,第二個(gè)參數(shù)告訴我們用線來(lái)繪制。在隨后的繪制函數(shù)調(diào)用后會(huì)一直以線框模式繪制三角形疾捍,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)設(shè)置回了默認(rèn)模式奈辰。
本例代碼在這。
附加資源
- antongerdelan.net/vertexbuffers: 頂點(diǎn)緩沖對(duì)象的一些深入探討乱豆。
該附加資源的代碼在這奖恰。值得一看的講解,關(guān)于VBO宛裕,以及Using Multiple Vertex Buffers for One Object瑟啃。
練習(xí)
- Try to draw 2 triangles next to each other using glDrawArrays by adding more vertices to your data:solution.
- Now create the same 2 triangles using two different VAOs and VBOs for their data: solution.
- Create two shader programs where the second program uses a different fragment shader that outputs the color yellow; draw both triangles again where one outputs the color yellow: solution.
- 論壇上別人提的問(wèn)題