第三章 管線一覽
本章我們會(huì)學(xué)到什么
- OpenGL管線的每個(gè)階段做什么的
- 如果連接著色器和固定功能管線階段
- 如果創(chuàng)建一個(gè)程式同時(shí)使用圖形管線的每個(gè)階段
在本章我們將從始至終過一遍OpenGL管線,對(duì)每個(gè)階段進(jìn)行考察,包括固定功能塊和可編程著色器塊。我們已經(jīng)對(duì)頂點(diǎn)著色器和片段著色器有了初步的大致了解。然而,我們創(chuàng)建的應(yīng)用只能簡單地在固定位置繪制一個(gè)三角形。如果我們想要使用OpenGL渲染任何有趣的東西,我們必須繼續(xù)學(xué)習(xí)管線以及我們用它所能做的所有事。本章介紹管線的每個(gè)部分,將它們彼此聯(lián)接并為每個(gè)階段提供一個(gè)著色器示例。
傳遞數(shù)據(jù)給頂點(diǎn)著色器
頂點(diǎn)著色器是OpenGL管線中第一個(gè)可編程(programmable)的階段并且是圖形管線中唯一必須的階段。不過,在頂點(diǎn)著色器運(yùn)行之前,一個(gè)稱為頂點(diǎn)獲取(vertex fetching)或頂點(diǎn)拉取(vertex pulling)的固定功能階段會(huì)運(yùn)行。它自動(dòng)為頂點(diǎn)著色器提供輸入數(shù)據(jù)。
頂點(diǎn)屬性
在GLSL中供著色器獲取輸入或輸出數(shù)據(jù)的機(jī)制是使用in和out存儲(chǔ)標(biāo)識(shí)符聲明全局變量。在第二章“我們的第一個(gè)OpenGL程式”中我們簡要介紹了out標(biāo)識(shí)符,在清單2.4中用它從片段著色器輸出一個(gè)顏色。在OpenGL管線的開端,我們使用in關(guān)鍵字為頂點(diǎn)著色器輸入數(shù)據(jù)。在階段之間,使用in和out組成導(dǎo)管在著色器之間傳遞數(shù)據(jù)。我們馬上就會(huì)知道這個(gè)。現(xiàn)在,考慮頂點(diǎn)著色器的輸入以及如果我們使用in存儲(chǔ)標(biāo)識(shí)符聲明一個(gè)變量發(fā)生了什么。這個(gè)標(biāo)識(shí)符標(biāo)明這個(gè)變量是頂點(diǎn)著色器的輸入,意味著這個(gè)變量是OpenGL圖形管線的重要輸入。這個(gè)變量在固定功能的頂點(diǎn)獲取階段被自動(dòng)填充。這個(gè)變量即為頂點(diǎn)屬性(vertex attribute)。
頂點(diǎn)屬性是頂點(diǎn)數(shù)據(jù)引入OpenGL管線的手段。要聲明一個(gè)頂點(diǎn)屬性,我們?cè)陧旤c(diǎn)著色器中使用in存儲(chǔ)標(biāo)識(shí)符聲明一個(gè)全局變量即可。如清單3.1所示,我們將offset變量聲明為一個(gè)輸入的頂點(diǎn)屬性。
清單3.1 聲明一個(gè)頂點(diǎn)屬性:
#version 450 core
// 'offset' is an input vertex attribute
layout (location = 0) in vec4 offset;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
}
在清單3.1中,我添加offset變量作為頂點(diǎn)著色器的輸入。因?yàn)樗枪芫€第一個(gè)著色器的輸入,故它會(huì)在頂點(diǎn)獲取階段被自動(dòng)填充。我們可以用眾多頂點(diǎn)屬性相關(guān)的函數(shù)(glVertexAttrib*()
)來指示OpenGL在頂點(diǎn)獲取階段為相關(guān)變量填充何值。我們將會(huì)用到的glVertexAttrib4fv()的原型如下:
void glVertexAttrib4fv(GLuint index, const GLfloat* v);
參數(shù)index
用來索引指定的屬性,v
是要放入屬性的新數(shù)據(jù)的指針。你或許已注意到聲明offset
屬性中的代碼layout (location = 0)
。這是一個(gè)布局指示符(layout qualifier),我們用它來設(shè)置指定的頂點(diǎn)屬性的位置(location)為0。這個(gè)位置的值就是我們通過index
來傳遞進(jìn)行這個(gè)屬性引用的值。
每次我們調(diào)用任何一個(gè)glVertexAttrib*()
函數(shù)都會(huì)更新傳遞給頂點(diǎn)著色器的頂點(diǎn)屬性的值。我們可以使用這個(gè)方法來給我們的三角形加上動(dòng)畫。清單3.2展示了一個(gè)更新版本的渲染函數(shù),它在每一幀都會(huì)更新offset
的值。
清單3.2 更新一個(gè)頂點(diǎn)屬性:
// Our rendering function
virtual void render(double currentTime)
{
const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
(float)cos(currentTime) * 0.5f + 0.5f,
0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, color);
// Use the program object we created earlier for rendering
glUseProgram(rendering_program);
GLfloat attrib[] = { (float)sin(currentTime) * 0.5,
(float)cos(currentTime) * 0.6f,
0.0f, 0.0f };
// Update the value of input attribute 0
glVertexAttrib4fv(0, attrib);
// Draw one triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
}
運(yùn)行清單3.2中的代碼,我們會(huì)看到三角形在窗體中以一個(gè)圓滑的橢圓形路徑運(yùn)動(dòng)。(譯者注: 譯者的倉庫sb7examples中相應(yīng)工程為chapter3/update_vertex_attribute)
在著色器階段間傳遞數(shù)據(jù)
目前為止我們已經(jīng)看到了如何通過使用in關(guān)鍵字創(chuàng)建一個(gè)頂點(diǎn)屬性來傳遞數(shù)據(jù)給頂點(diǎn)著色器,如何通過讀寫諸如gl_VertexID
、gl_Position
等內(nèi)置變量和固定功能塊交流,如何使用out關(guān)鍵字從片段著色器輸出數(shù)據(jù)。不過,我們同樣可以使用in和out關(guān)鍵字在著色器階段之間傳遞我們的數(shù)據(jù)。一如我們?cè)谄沃髦惺褂?strong>out關(guān)鍵字來創(chuàng)建輸出顏色值的變量一樣,我們也能在頂點(diǎn)著色器中使用out關(guān)鍵字創(chuàng)建一個(gè)輸出變量。在一個(gè)著色器中寫入到一個(gè)輸出變量的任何內(nèi)容都會(huì)傳遞給下一個(gè)著色器階段中以in聲明的同名變量。比如,如果我們?cè)陧旤c(diǎn)著色器中使用out關(guān)鍵字聲明一個(gè)叫vs_color
的變量,接下來在片段著色器階段這個(gè)變量就會(huì)匹配到一個(gè)用in關(guān)鍵字聲明的名為vs_color
的變量上(假設(shè)它們間沒有其他的階段)。
如果將我們簡單的頂點(diǎn)著色器修改為清單3.3,包含一個(gè)vs_color
的輸出變量,并相應(yīng)將我們簡單的片段著色器修改為清單3.4,包含一個(gè)vs_color
的輸入變量,我們便能將一個(gè)值從頂點(diǎn)著色器傳給片段著色器。然后,相比之前輸出一個(gè)固定的顏色值,現(xiàn)在這個(gè)片段著色器可以將頂點(diǎn)著色器傳給它的顏色值輸出。
清單3.3 帶一個(gè)輸出變量的頂點(diǎn)著色器:
#version 450 core
// 'offset' and 'colour' are input vertex attributes
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// 'vs_color' is an output that will be sent to the next shader stage
out vec4 vs_color;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_color = color;
}
從清單3.3可以看到,我們?yōu)檫@個(gè)頂點(diǎn)著色器聲明了第二個(gè)輸入變量: color
(這次location為1),并將它的值寫入到輸出變量vs_color
。然后這個(gè)值為清單3.4的片段著色器所用,寫入到幀緩沖區(qū)。這使得我們可以把一個(gè)通過glVertexAttrib*
設(shè)置的頂點(diǎn)屬性的顏色值一路從頂點(diǎn)著色器傳入片段著色器,然后寫到幀緩沖區(qū)。結(jié)果就是我們可以繪制他色的三角形了。
清單3.4 帶有一個(gè)輸入變量的片段著色器:
#version 450 core
// Input from the vertex shader
in vec4 vs_color;
// Output to the framebuffer
out vec4 color;
void main(void)
{
// Simply assign the colour we were given by the vertex shader to our output
color = vs_color;
}
譯者注: 譯者的sb7examples中相應(yīng)工程為chapter3/different_colored_triangle。
數(shù)據(jù)塊接口(Interface Blocks)
一次聲明一個(gè)接口變量用來在著色器階段間傳遞數(shù)據(jù)可能是最簡單的方法。然而,在大多數(shù)工業(yè)用的應(yīng)用的,我們會(huì)想在著色器階段間傳遞成堆的數(shù)據(jù),這些可能包括數(shù)組、結(jié)構(gòu)體以及其他復(fù)雜排列的變量。為達(dá)此目的,我們可以將好些個(gè)變量組成一個(gè)數(shù)據(jù)塊接口(interface block)。數(shù)據(jù)塊接口的聲明和C中結(jié)構(gòu)體的聲明很像,除了數(shù)據(jù)塊接口依據(jù)它是從著色器輸入數(shù)據(jù)還是輸出數(shù)據(jù)而使用in或者out關(guān)鍵字聲明。示一例如清單3.5。
清單3.5 帶一個(gè)輸出數(shù)據(jù)塊接口的頂點(diǎn)著色器:
#version 450 core
// 'offset' is an input vertex attribute
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// Declare VS_OUT as an output interface block
out VS_OUT
{
vec4 colour; // Send color to the next stage
} vs_out;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_out.color = color;
}
值得注意的是清單3.5中的數(shù)據(jù)塊接口同時(shí)有一個(gè)塊名稱(大寫的VS_OUT
)和一個(gè)實(shí)例名稱(小寫的vs_out
)。數(shù)據(jù)塊接口在著色器階段間通過塊名稱匹配(本例中為VS_OUT
)但在著色器中則是用實(shí)例名稱引用(本例中為vs_out
)。將我們的片段著色器修改為清單3.6來使用這個(gè)數(shù)據(jù)塊接口。
清單3.6 帶一個(gè)輸入數(shù)據(jù)塊接口的片段著色器:
#version 450 core
// Declare VS_OUT as an input interface block
in VS_OUT
{
vec4 colour; // Send color to the stage
} fs_in;
// Output to the framebuffer
out vec4 color;
void main(void)
{
// Simply assign the color we were given by the vertex shader to our output
color = fs_in.color;
}
譯者注: 譯者的sb7examples中相應(yīng)工程為chapter3/interface_block_triangle。
通過塊名稱匹配但允許塊實(shí)例在每個(gè)著色器階段有不同的名稱,這種設(shè)定出于兩方面的考量。第一,允許不同著色器階段使用不同的名稱進(jìn)行引用,可以避免一些混亂,比如要在片段著色器中使用vs_out
。第二,當(dāng)我們縱橫于一些著色器階段時(shí),比如頂點(diǎn)著色器、細(xì)分著色器或者幾何著色器階段(我們馬上就會(huì)看到了),這樣使得接口從單個(gè)條目變成數(shù)組。值得注意的是數(shù)據(jù)塊接口只能用于著色器階段到著色器階段的數(shù)據(jù)傳遞--我們不能用它將頂點(diǎn)著色器的輸入或者片段著色器的輸出組建成群。
細(xì)分曲面(Tessellation)
細(xì)分曲面是將高階圖元(在OpenGL中常稱為碎片(patch))降解為很多更小的、更簡單的圖元,諸如三角形之后進(jìn)行渲染。OpenGL包含一個(gè)固定功能的、可配置的細(xì)分曲面引擎,它可以將四邊形、三角形以及線段降解為可能很多的可被常規(guī)光柵硬件使用的更小的點(diǎn)、線段或者三角形。從邏輯上來說,細(xì)分曲面階段由三部分組成:細(xì)分曲面控制著色器,固定功能細(xì)分曲面引擎以及細(xì)分曲面運(yùn)算著色器,在OpenGL管線中細(xì)分曲面階段直接跟在頂點(diǎn)著色階段之后。
細(xì)分曲面控制著色器(Tessellation Control Shaders)
細(xì)分曲面三個(gè)階段的第一階段為細(xì)分曲面控制著色器(TCS;有時(shí)簡稱為控制著色器)。這個(gè)著色器從頂點(diǎn)著色器接收輸入并主要負(fù)責(zé)兩件事:1.確定要發(fā)送給細(xì)分曲面引擎的細(xì)分曲面等級(jí)。2.生成細(xì)分曲面運(yùn)行之后要發(fā)送給細(xì)分曲面運(yùn)算著色器的數(shù)據(jù)。
在OpenGL中,細(xì)分曲面通過將被稱為碎片(patches)的高階表面降解為點(diǎn)、線段或者三角形而進(jìn)行正常工作。每一個(gè)碎片都由一定數(shù)目的控制點(diǎn)(control points)組成。每個(gè)碎片的控制點(diǎn)數(shù)目都是可配置的,通過調(diào)用glPatchParameteri()即可設(shè)置,同時(shí)將pname
設(shè)置為GL_PATCH_VERTICES
以及value
設(shè)置為構(gòu)成每個(gè)碎片的控制點(diǎn)的數(shù)目。glPatchParameteri()的原型如:
void glPatchParameteri(GLenum pname, GLint value);
缺省情況下,每個(gè)碎片的控制點(diǎn)數(shù)目是3。所以,如果這就是我們想要的(一如我們接下來的示例),我們完全可以不調(diào)用這個(gè)函數(shù)。用來構(gòu)成一個(gè)碎片的最高控制點(diǎn)數(shù)目是由實(shí)現(xiàn)定義的,但保證至少為32。
當(dāng)細(xì)分曲面開始時(shí),頂點(diǎn)著色器會(huì)針對(duì)每一個(gè)控制點(diǎn)運(yùn)行一次,不過細(xì)分曲面控制著色器根據(jù)控制點(diǎn)的群組按批次運(yùn)行,每個(gè)批次的大小和每個(gè)碎片的頂點(diǎn)數(shù)一樣。意即,頂點(diǎn)被當(dāng)做控制點(diǎn)而且頂點(diǎn)著色器的輸出結(jié)果被成批送往細(xì)分曲面控制著色器當(dāng)做輸入。每個(gè)碎片的控制點(diǎn)數(shù)目是可以改變的所以細(xì)分曲面控制著色器輸出的控制點(diǎn)數(shù)目可以和它消耗的控制點(diǎn)數(shù)目不同。控制著色器產(chǎn)生的控制點(diǎn)數(shù)目在控制著色器的源代碼中使用一個(gè)輸出布局標(biāo)識(shí)符進(jìn)行設(shè)置。這樣的布局標(biāo)識(shí)符看起來像這樣:
layout (vertices = N) out;
這里的N
即每個(gè)碎片的控制點(diǎn)數(shù)目。控制著色器有責(zé)任計(jì)算輸出控制點(diǎn)的數(shù)目以及設(shè)置作為最終結(jié)果發(fā)送給固定功能細(xì)分曲面引擎的碎片的細(xì)分曲面因子。輸出的細(xì)分曲面因子寫入內(nèi)置變量gl_TessLevelInner
和gl_TessLevelOuter
中,而其他任何在管線中傳遞的數(shù)據(jù)都正常地寫入用戶定義的輸出變量(使用out關(guān)鍵字聲明的或者特殊的內(nèi)置gl_out
數(shù)組)。
清單3.7展示了一個(gè)簡單的細(xì)分曲面控制著色器。它用布局標(biāo)識(shí)符layout (vertices = 3) out;
設(shè)置輸出的控制點(diǎn)數(shù)目為3(與缺省的輸入控制點(diǎn)數(shù)目相同),將它的輸入拷貝到輸出(使用內(nèi)置變量gl_in和gl_out),并設(shè)置內(nèi)和外的細(xì)分曲面級(jí)別為5。更高的細(xì)分曲面級(jí)別會(huì)產(chǎn)生更密集的細(xì)分曲面輸出,更低的級(jí)別會(huì)產(chǎn)生更粗糙的細(xì)分曲面輸出。將細(xì)分曲面因子設(shè)置為0會(huì)導(dǎo)致整個(gè)碎片被丟棄。
內(nèi)置變量gl_InvocationID
被用作gl_in
和gl_out
數(shù)組的索引,從0開始算起。這個(gè)變量表示當(dāng)前被調(diào)用的細(xì)分曲面控制著色器中被處理的碎片的控制點(diǎn)索引值。
清單3.7 我們的第一個(gè)細(xì)分曲面控制著色器:
#version 450 core
layout (vertices = 3) out;
void main(void)
{
// Only if I am invocation 0 ...
if (gl_InvocationID == 0)
{
gl_TessLevelInner[0] = 5.0;
gl_TessLevelOuter[0] = 5.0;
gl_TessLevelOuter[1] = 5.0;
gl_TessLevelOuter[2] = 5.0;
}
// Everybody copies their input to their output
gl_out[gl_InvocationID].gl_Position =
gl_in[gl_InvocationID].gl_Position;
}
細(xì)分曲面引擎(The Tessellation Engine)
細(xì)分曲面引擎是OpenGL管線中的一個(gè)固定功能部分,它接收表示為碎片的高階表面并將它們降解為更簡單的圖元,比如:點(diǎn)、線段或者三角形。在細(xì)分曲面引擎接收碎片之前,細(xì)分曲面控制著色器處理輸入的控制點(diǎn)并設(shè)置細(xì)分曲面因子,然后用它們來降解這個(gè)碎片。細(xì)分曲面引擎生成輸出圖元之后,用以表示這些圖元的頂點(diǎn)被細(xì)分曲面運(yùn)算著色器所利用。細(xì)分曲面引擎有責(zé)任生成用以調(diào)用細(xì)分曲面運(yùn)算著色器的參數(shù),然后細(xì)分曲面運(yùn)算著色器用這些參數(shù)轉(zhuǎn)換作為最終結(jié)果的圖元并將它們準(zhǔn)備好光柵化。
細(xì)分曲面運(yùn)算著色器(Tessellation Evaluation Shaders)
一旦固定功能細(xì)分曲面引擎運(yùn)行后,它會(huì)產(chǎn)生一些輸出頂點(diǎn)用來表示生成的圖元。這些頂點(diǎn)會(huì)傳遞給細(xì)分曲面運(yùn)算著色器。細(xì)分曲面運(yùn)算著色器(TES;或者簡稱為運(yùn)算著色器)會(huì)對(duì)細(xì)分曲面器生成的每個(gè)頂點(diǎn)運(yùn)行一次。當(dāng)細(xì)分曲面級(jí)別高時(shí),細(xì)分曲面運(yùn)算著色器將會(huì)運(yùn)行很多次。為此,對(duì)于復(fù)雜的運(yùn)算著色器和高細(xì)分曲面級(jí)別我們需要小心應(yīng)付。
清單3.8展示了一個(gè)細(xì)分曲面運(yùn)算著色器,它接受由清單3.7所示的控制著色器運(yùn)行輸出的頂點(diǎn)作為輸入。在這個(gè)著色器開頭是一個(gè)布局標(biāo)識(shí)符,它設(shè)置了細(xì)分曲面模式。在本例中,我們選擇模式為三角形。其他標(biāo)識(shí)符equal_spacing
和cw
表明新的頂點(diǎn)生成時(shí)要是沿著細(xì)分的?多邊形邊緣等距的并且生成的三角形的頂點(diǎn)環(huán)繞次序是順時(shí)針的。我們會(huì)在第八章的“細(xì)分曲面”中對(duì)其他可能的選項(xiàng)進(jìn)行更全面的介紹。
這個(gè)著色器的剩余部分如頂點(diǎn)著色器一樣對(duì)gl_Position
進(jìn)行了賦值。它使用多個(gè)內(nèi)置變量來計(jì)算gl_Position
的值。第一個(gè)是gl_TessCoord
,它是細(xì)分曲面器生成的頂點(diǎn)的質(zhì)心坐標(biāo)。第二個(gè)是gl_in[]
結(jié)構(gòu)體數(shù)組的成員gl_Position
。gl_in
匹配清單3.7中的細(xì)分曲面控制著色器寫入的gl_out
結(jié)構(gòu)體。這個(gè)著色器主要實(shí)做了直通細(xì)分(pass-through tessellation)。意即,細(xì)分后輸出的碎片與原始輸入的三角形碎片形狀一致。
清單3.8 我們的第一個(gè)細(xì)分曲面運(yùn)算著色器:
#version 450 core
layout (triangles, equal_spacing, cw) in;
void main(void)
{
gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position +
gl_TessCoord.y * gl_in[1].gl_Position +
gl_TessCoord.z * gl_in[2].gl_Position);
}
為了能看到細(xì)分曲面器的結(jié)果,我們需要指示OpenGL只繪制最終結(jié)果的三角形的輪廓。為達(dá)此目的,我們調(diào)用glPolygonMode(),它的原型為:
void glPolygonMode(GLenum face, GLenum mode);
face
參數(shù)指明我們想影響哪個(gè)類型的多邊形。因?yàn)槲覀兿胍绊懰袞|西,故我們?cè)O(shè)置它為GL_FRONT_AND_BACK
.mode
表明我們想要如何渲染多邊形。我們想要渲染為線框模式(即直線),我們將這個(gè)參數(shù)設(shè)置為GL_LINE
。其他模式我們很快就會(huì)解釋的。我們的三角形示例在使用細(xì)分曲面以及清單3.7、清單3.8的著色器之后,渲染結(jié)果如圖示3.1:
譯者注: 譯者的sb7examples中相應(yīng)項(xiàng)目為chapter3/triangle_with_tessellation
幾何著色器(Geometry Shaders)
從邏輯上來講,幾何著色器是是前端著色器的最后階段了,它在頂點(diǎn)和細(xì)分曲線階段之后、光柵器之前。幾何著色器針對(duì)每個(gè)圖元運(yùn)行一次并且對(duì)正在處理的圖元的所有組成頂點(diǎn)有訪問權(quán)限。幾何著色器也是著色器階段中唯一可以編程控制數(shù)據(jù)流總量增減的著色器。雖然說細(xì)分曲面著色器也可以增減管線的工作量,但它只能通過設(shè)置碎片的細(xì)分曲面級(jí)別來隱式地影響工作量。而相對(duì)的,幾何著色器包含兩個(gè)函數(shù)--EmitVertex()
和EndPrimitive()
,它們能顯示地產(chǎn)生頂點(diǎn)發(fā)送到圖元組裝(primitive assembly)和光柵化(rasterization)。
幾何著色器的另一個(gè)獨(dú)一的功能是它可以在管線中途改變圖元模式。比如,它們可以接收三角形作為輸入并輸出一些列的點(diǎn)或者線,再或者從一系列不相干的點(diǎn)創(chuàng)建三角形。清單3.9示一例。
清單3.9 我們的第一個(gè)幾何著色器:
#version 450 core
layout (triangles) in;
layout (points, max_vertices = 3) out;
void main(void)
{
int i;
for (i = 0; i < gl_in.length(); i++)
{
gl_Position = gl_in[i].gl_Position;
EmitVertex();
}
}
清單3.9的幾何著色器再次扮演了一個(gè)簡單地直通著色器,它將輸入的三角形轉(zhuǎn)換為點(diǎn),這樣我們便能看見它們的頂點(diǎn)。第一個(gè)布局標(biāo)識(shí)符表明這個(gè)幾何著色器期望輸入數(shù)據(jù)為三角形。第二個(gè)布局標(biāo)識(shí)符指示OpenGL這個(gè)幾何著色器會(huì)產(chǎn)生點(diǎn)并且每個(gè)著色器最多產(chǎn)生三個(gè)點(diǎn)。在main
函數(shù)中,迭代了gl_in
數(shù)組的所有成員,gl_in
數(shù)組的長度通過它的.length()
函數(shù)獲知。
實(shí)際上我們知道gl_in
數(shù)組的長度就是三,因?yàn)槲覀冋谔幚淼氖侨切危總€(gè)三角形有三個(gè)頂點(diǎn)。這個(gè)幾何著色器的輸出再一次與頂點(diǎn)著色器神似(前面有細(xì)分曲面運(yùn)算著色器)。特別是我們寫入值到gl_Position
來設(shè)置輸出頂點(diǎn)的位置。然后,我們調(diào)用EmitVertex()
,它在幾何著色器的輸出中產(chǎn)生一個(gè)頂點(diǎn)。幾何著色器在著色器結(jié)束的時(shí)候自動(dòng)調(diào)用EndPrimitive()
,故本例中我們無須顯示調(diào)用它。運(yùn)行這個(gè)著色器之后,將會(huì)產(chǎn)生三個(gè)頂點(diǎn),然后渲染為三個(gè)點(diǎn)。
將這個(gè)幾何著色器插入到我們之前的細(xì)分曲面三角形示例中,我們會(huì)得到如圖示3.2的輸出。為了得到這個(gè)圖像,我們通過調(diào)用glPointSize()將點(diǎn)的大小設(shè)置為5.0,這樣會(huì)使得點(diǎn)大一些容易辨識(shí)。
圖示3.2 帶幾何著色器的細(xì)分曲面三角形:
譯者注: 譯者的sb7examples中相應(yīng)的項(xiàng)目為chapter3/triangle_with_tess_geometry
圖元組裝、修剪和光柵化(Primitive Assembly,Clipping, and Rasterization)
管線的前端(這包括頂點(diǎn)著色、細(xì)分曲面以及幾何著色)運(yùn)行完成之后,管線中一個(gè)固定功能部分會(huì)開始執(zhí)行一系列任務(wù),它接收頂點(diǎn)表示的場(chǎng)景并將其轉(zhuǎn)換為一系列像素,這些像素輪流被上色并寫入到顯示屏幕上。這個(gè)過程的第一步是圖元組裝,它將頂點(diǎn)群組為線或者三角形。圖元組裝仍然發(fā)生在“點(diǎn)”上,不過這種情況它無關(guān)緊要了。
一旦各個(gè)分散獨(dú)立的頂點(diǎn)被組成為圖元,圖元就會(huì)針對(duì)可顯示區(qū)域進(jìn)行修剪(clipped),這個(gè)可顯示區(qū)域通常是窗體或者顯示屏幕,但也可以是一個(gè)更小的稱為視口(viewport)的區(qū)域。最終,圖元中被判斷為可能可見的部分被發(fā)送到一個(gè)稱為光柵器(rasterizer)的固定功能子系統(tǒng)。這個(gè)子系統(tǒng)會(huì)判斷出哪些像素被圖元(點(diǎn)、線或者三角形)覆蓋到并將這些像素發(fā)送到下一階段--換言之,就是片段著色階段。
修剪
隨著頂點(diǎn)離開管線的前端,它們的位置便處于修剪空間(clip space)。修剪空間是眾多用來表示位置的坐標(biāo)系統(tǒng)之一。你可能已注意到,我們?cè)陧旤c(diǎn)、細(xì)分曲面以及幾何著色器中寫入的gl_Position
變量是一個(gè)vec4類型的,我們寫入到它的產(chǎn)生的位置值也是四個(gè)分量的向量。這個(gè)被?稱為齊次坐標(biāo)(homogeneous coordinate)。齊次坐標(biāo)系統(tǒng)被用在投影幾何(projective geometry)中,因?yàn)楹芏鄶?shù)學(xué)問題最終在齊次坐標(biāo)中都比常規(guī)笛卡爾坐標(biāo)空間(Cartesian space)要簡單。齊次坐標(biāo)值比對(duì)應(yīng)的笛卡爾坐標(biāo)值要多一個(gè)分量,這也是為什么我們的三維位置向量被表示為四分量的變量。
盡管管線前端的輸出是四分量齊次坐標(biāo),但修剪卻是依靠笛卡爾坐標(biāo)的。因此,為了將齊次坐標(biāo)轉(zhuǎn)換為笛卡爾坐標(biāo),OpenGL執(zhí)行了透視分割(perspective division),將位置的四個(gè)分量都用w分量進(jìn)行分割。這樣可以將頂點(diǎn)從齊次坐標(biāo)空間投影到笛卡爾坐標(biāo)空間,保持w為1.0.到目前為止的所有示例,我們都將gl_Position
的w分量設(shè)置為1.0,所以這種情況下分割不會(huì)有任何效果。我們之后研究投影幾何時(shí)會(huì)對(duì)將w
設(shè)置為其他值的效果做討論的。
位置在投影分割之后的結(jié)果就會(huì)處于標(biāo)準(zhǔn)設(shè)備空間(normalized device space)。在OpenGL中,標(biāo)準(zhǔn)設(shè)備空間的可視區(qū)域是在x和y軸上-1.0到1.0以及z軸上0.0到1.0的體積。任何在這個(gè)區(qū)域內(nèi)的幾何圖形都可能對(duì)用戶是可見的而這個(gè)區(qū)域外的任何東西都將被丟棄。這個(gè)體積的六個(gè)面由三維空間的平面組成。因?yàn)橐粋€(gè)平面將一個(gè)坐標(biāo)空間一分為二,所以這個(gè)平面的每一邊的體積被稱為半空間(half-spaces)。
在將圖元傳遞到下一個(gè)階段之前,OpenGL通過判斷每個(gè)圖元的頂點(diǎn)在六個(gè)平面的哪一邊來進(jìn)行修剪。每個(gè)平面實(shí)際上有一個(gè)外側(cè)(outside)和里側(cè)(inside)。如果一個(gè)圖元的所有頂點(diǎn)都處在某一個(gè)平面的外側(cè),那這個(gè)圖元就被丟棄。如果一個(gè)圖元的所有頂點(diǎn)都處在所有平面的里側(cè)(換言之處在可視體積內(nèi)),然后這個(gè)圖元就會(huì)原封不動(dòng)傳遞下去。部分可視(意即這個(gè)圖元與某個(gè)平面交叉)的圖元需要特殊處理。這個(gè)主題更多的詳情我們將在第七章“修剪”給出。
視口變換(Viewport Transformation)
在修剪后,幾何圖形的所有頂點(diǎn)都會(huì)處在標(biāo)準(zhǔn)設(shè)備坐標(biāo)范圍內(nèi),也就是在x和y軸上-1.0到1.0意即z軸上0.0到1.0的體積內(nèi)。然而,我們繪制的目標(biāo)是窗體,它的坐標(biāo)通常是從左下的(0,0)到右上的(w-1,h-1),其中w和h分別為窗體的寬和高,單位是像素(窗體的坐標(biāo)原點(diǎn)可以進(jìn)行更改,比如改為右上)。為了將我們的幾何圖形放入窗體,OpenGL應(yīng)用視口變換(viewport transform),它將縮放和偏移應(yīng)用到頂點(diǎn)的標(biāo)準(zhǔn)設(shè)備坐標(biāo)上,從而將它們放置到窗體坐標(biāo)(window coordinates)中。要應(yīng)用的縮放和偏移通過視口界限來決定,界限可以通過調(diào)用glViewport()和glDepthRange()來設(shè)置。它們的原型為:
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glDepthRange(GLdouble nearVal, GLdouble farVal);
變換按照如下形式進(jìn)行:
其中xw、yw和zw是頂點(diǎn)最終在窗體空間的坐標(biāo),xd、yd和zd是頂點(diǎn)在標(biāo)準(zhǔn)設(shè)備空間的坐標(biāo)。px和py是視口的寬和高,以像素為單位,然后n和f分別是標(biāo)準(zhǔn)設(shè)備坐標(biāo)中修剪空間z軸的近和遠(yuǎn)平面坐標(biāo)值。最后,ox和oy是視口的原點(diǎn)。
剔除(Culling)
在一個(gè)三角形繼續(xù)處理之前,它可能需要被傳遞到一個(gè)叫“剔除”的階段,這個(gè)階段會(huì)判斷三角形的面朝向或者背向觀察者,然后會(huì)根據(jù)計(jì)算結(jié)果決定是否真地進(jìn)行下一步以及繪制它。如果三角形的面朝向觀察者,那它就被認(rèn)為是正面,否則,它就被認(rèn)為是背面。丟棄背面?的三角形是很普遍的,因?yàn)楫?dāng)一個(gè)對(duì)象是封閉的,任何背面的三角形都會(huì)被其他正面的三角形所隱藏。
為了判斷一個(gè)三角形是正面還是背面,OpenGL會(huì)判斷三角形在窗體空間的面積正負(fù)。一種判斷三角形面積正負(fù)的方法是取兩邊的叉積。方程式為:
其中,xiw和yiw是三角形第i個(gè)頂點(diǎn)在窗體空間的坐標(biāo),i⊕1是(i+1)模3。如果這個(gè)面積是正的,那么這個(gè)三角形就被認(rèn)為是正面;如果它是負(fù)的,那么它就被認(rèn)為是背面。這個(gè)計(jì)算結(jié)果的含義可以通過調(diào)用glFrontFace()進(jìn)行顛倒,它的原型為:
void glFrontFace(GLenum mode);
mode
可被設(shè)置為GL_CW
或者GL_CCW
(其中GL_CW
表示順時(shí)針,GL_CCW
表示逆時(shí)針)。這被稱為三角形的環(huán)繞方向(winding order),順時(shí)針和逆時(shí)針表示三角形頂點(diǎn)出現(xiàn)在窗體空間的次序。缺省情況下,環(huán)繞方向?yàn)槟鏁r(shí)針,這意味著頂點(diǎn)次序?yàn)槟鏁r(shí)針的三角形被認(rèn)為是正面,頂點(diǎn)次序?yàn)轫槙r(shí)針的三角形被認(rèn)為是背面。如果環(huán)繞方面被設(shè)置為GL_CW
,那前面方程式中的a的含義在剔除過程中也就和前述相反。圖示3.3以可觀的方式展示方才所述。
圖示3.3 順時(shí)針(左)和逆時(shí)針(右)環(huán)繞方向
一旦三角形的方向被確定,OpenGL便可以丟棄正面、背面或者全部。缺省情況下,OpenGL會(huì)渲染所有的三角形,不管三角形的面的方向如何。要打開剔除,調(diào)用glEnable(),將cap
參數(shù)設(shè)置為GL_CULL_FACE。當(dāng)我們啟用剔除,OpenGL缺省下就會(huì)剔除背面的三角形。要想變更三角形剔除的類型,調(diào)用glCullFace(),將face
參數(shù)設(shè)置為GL_FRONT
、GL_BACK
或者GL_FRONT_AND_BACK
。
因?yàn)辄c(diǎn)和線沒有幾何面積(很顯然,一旦點(diǎn)和線渲染到顯示屏幕上是有面積的,否則我們不會(huì)看到它們。然而這個(gè)面積是人為造成的,從它們的頂點(diǎn)上并不能直接計(jì)算出來),所以這個(gè)面向計(jì)算并不應(yīng)用于它們,它們也無法在這個(gè)階段被剔除。
光柵化
光柵化是判斷哪些片段被一個(gè)圖元(比如一條線或者一個(gè)三角形)所覆蓋了的過程。有很多算法可以完成此事,不過對(duì)于三角形,大多數(shù)OpenGL系統(tǒng)會(huì)選定基于半空間的算法,因?yàn)樗梢院芎玫牟⑿袑?shí)做。簡明扼要地講就是,OpenGL會(huì)在窗體坐標(biāo)內(nèi)為三角形設(shè)定一個(gè)界限框(bounding box),并對(duì)界限框內(nèi)的每個(gè)片段測(cè)試它在三角形內(nèi)還是外。要完成這個(gè)測(cè)試,將三角形的每個(gè)邊當(dāng)成窗體的半空間分割線即可。
在三角形三邊內(nèi)側(cè)的片段被認(rèn)為是在三角形內(nèi),在任何一邊外側(cè)的片段則被認(rèn)為是在三角形外。因?yàn)榕袛嘁粭l線、一個(gè)點(diǎn)在哪一側(cè)是相對(duì)簡單的而且和除了線的端點(diǎn)或點(diǎn)本身的位置之外都不想干,很多測(cè)試可以并發(fā)執(zhí)行,這樣為大規(guī)模并行提供了機(jī)會(huì)。
片段著色器(Fragment Shaders)
片段著色器是OpenGL圖形管線中的最后可編程階段了。每個(gè)片段在發(fā)送到幀緩沖(framebuffer)混合到窗體之前,這個(gè)階段有責(zé)任決定每個(gè)片段的顏色。在光柵器處理一個(gè)圖元之后,它會(huì)產(chǎn)生一系列片段,這些片段需要傳遞給片段著色器進(jìn)行調(diào)色。到了這一步,管線的工作量會(huì)有一個(gè)爆炸式的增長,因?yàn)槊恳粋€(gè)三角形都可能產(chǎn)生數(shù)百、數(shù)千甚至上百萬個(gè)片段。
片段用來描述一種元素,它可能對(duì)像素的最終顏色造成至關(guān)重要的影響。不過像素的最終顏色可能并不會(huì)是某個(gè)指定的片段著色器執(zhí)行的結(jié)果,因?yàn)橄袼氐淖罱K顏色還與很多其他因素有關(guān),比如深度(depth)、模板測(cè)試(stencil tests)、混合(blending)以及多重采樣(multi-sampling),當(dāng)然這些在本書后面都會(huì)涉及到。
第二章的清單2.4展示了我們的第一個(gè)片段著色器的源代碼。它是一個(gè)簡單到不行的著色器,僅僅只是聲明了一個(gè)輸出變量,然后為它賦了一個(gè)固定的值。在真實(shí)世界的應(yīng)用中,片段著色器通常會(huì)復(fù)雜很多并且需要進(jìn)行與光照、材質(zhì)甚至片段深度的諸多計(jì)算。片段著色器有多個(gè)可用的內(nèi)置輸入變量,比如gl_FragCoord
,gl_FragCoord
包含了片段在窗體中的位置。我們可以用它來為每個(gè)片段都產(chǎn)生獨(dú)一無二的顏色。
清單3.10提供了一個(gè)片段著色器,它從gl_FragCoord
得出輸出的顏色。圖示3.4展示了我們?cè)鹊摹拔覀兊囊粋€(gè)三角形”搭配清單3.10的著色器的輸出。
清單3.10 從一個(gè)片段的位置得出顏色:
#version 450 core
out vec4 color;
void main(void)
{
color = vec4(sin(gl_FragCoord.x * 0.25) * 0.5 + 0.5,
cos(gl_FragCoord.y * 0.25) * 0.5 + 0.5,
sin(gl_FragCoord.x * 0.15) * cos(gl_FragCoord.y * 0.15),
1.0);
}
圖示3.4:
譯者注:譯者的sb7examples中相應(yīng)工程為chapter3/triangle_with_fragshader
我們可以看到,圖示3.4中三角形的每個(gè)像素的顏色現(xiàn)在都是關(guān)于它的位置的函數(shù),而且產(chǎn)生了與顯示屏幕匹配(screen-aligned)的模式。清單3.10的著色器創(chuàng)建了一種格子樣式(checkered patterns)。
gl_FragCoord
是片段著色器可用的內(nèi)置變量中的一個(gè)。不過,和其他著色器階段一樣,我們可以為片段著色器定義自定義的輸入,這些輸入會(huì)在光柵化前的任何一個(gè)階段進(jìn)行填充。比如,如果我們有一個(gè)簡單的程式只有一個(gè)頂點(diǎn)著色器和片段著色器,我們可以從頂點(diǎn)著色器傳遞數(shù)據(jù)給片段著色器。
片段著色器的輸入不像其他著色器階段的,OpenGL會(huì)針對(duì)要渲染的圖元對(duì)輸入進(jìn)行插值計(jì)算。為了演示這種情況,我們?nèi)砬鍐?.3的頂點(diǎn)著色器,將它做一些修改,為每個(gè)頂點(diǎn)賦予不同的固定顏色,最后如3.11所示。
清單3.11 頂點(diǎn)不同顏色的頂點(diǎn)著色器:
#version 450 core
// 'vs_color' is an output that will be sent to the next shader stage
out vec4 vs_color;
void main(void)
{
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
const vec4 colors[] = vec4[3](vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.0, 1.0, 0.0, 1.0),
vec4(0.0, 0.0, 1.0, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_color = color[gl_VertexID];
}
如清單3.11中所示,我們?cè)黾恿说诙€(gè)常量數(shù)組,它包含一組顏色并且使用gl_VertexID
進(jìn)行索引,它的值最終寫入到輸出變量vs_color
中。在清單3.12中,我們將之前簡單的片段著色器進(jìn)行修改如下。
清單3.12 頂點(diǎn)不同顏色的片段著色器:
#version 450 core
// 'vs_color' is the color produced by the vertex shader
in vec4 vs_color;
out vec4 color;
void main(void)
{
color = vs_color;
}
最終的輸出結(jié)果如圖示3.5,可以看到,顏色在三角形上平滑地變化。
譯者注:譯者的sb7examples中相應(yīng)工程為chapter3/triangle_color_smooth
幀緩沖運(yùn)算
幀緩沖是OpenGL圖形管線的最后一個(gè)階段。它可以表示顯示屏幕的可視內(nèi)容以及用來存儲(chǔ)每個(gè)像素除了顏色之外額外值的內(nèi)存區(qū)域。在大多數(shù)平臺(tái)上,這意味著我們?cè)谧烂嫔峡吹降拇绑w(或者說整個(gè)顯示屏幕,如果我們的應(yīng)用覆蓋了整個(gè)顯示屏幕)是屬于操作系統(tǒng)(或者更精確來說是窗體系統(tǒng))的。窗體系統(tǒng)提供的幀緩沖是缺省幀緩沖,但如果我們想渲染到顯示屏幕外的區(qū)域的話,我們是可以提供自己的幀緩沖的。幀緩沖中存儲(chǔ)的狀態(tài)包含有諸多信息,比如片段著色器產(chǎn)生的數(shù)據(jù)寫到哪,這些數(shù)據(jù)是什么格式,等等。這個(gè)狀態(tài)存儲(chǔ)在一個(gè)幀緩沖對(duì)象(framebuffer object)中。像素操作狀態(tài)也可以被認(rèn)為是幀緩沖的一部分,但不會(huì)每個(gè)像素都存儲(chǔ)一個(gè)幀緩沖對(duì)象。
像素運(yùn)算
片段著色器產(chǎn)生一個(gè)輸出片段后,在這個(gè)片段寫入窗體前還會(huì)做一些事情,比如判斷它是否仍在所屬的窗體中。這些事情我們都可以在應(yīng)用中打開或者關(guān)閉。首先可能發(fā)生的事是裁剪測(cè)試(scissor test),它將片段與我們定義的一個(gè)矩形進(jìn)行測(cè)試。如果片段在矩形內(nèi),就繼續(xù)處理;如果片段在矩形外,就被丟棄。
然后會(huì)進(jìn)行模板測(cè)試(stencil test)。這一步會(huì)將我們的應(yīng)用提供的值與模板緩沖作比較,模板緩沖為每個(gè)像素都對(duì)應(yīng)存儲(chǔ)了一個(gè)值。模板緩沖的內(nèi)容并沒有特別的含義,可以隨便存啥。當(dāng)我們要使用一種叫做多重采樣(multi-sampling)的技術(shù)時(shí),一個(gè)幀緩沖中是可以存儲(chǔ)多個(gè)深度緩沖、模板緩沖或者每個(gè)像素的顏色值的。本書后面我們會(huì)對(duì)此做更深入的探索。
模板測(cè)試完成之后,就會(huì)進(jìn)行深度測(cè)試(depth test)。所謂的深度測(cè)試就是將片段的z坐標(biāo)值與深度緩沖(depth buffer)的內(nèi)容做比較。深度緩沖是和模板緩沖相似的一段內(nèi)存區(qū)域,它做為幀緩沖的一部分為每個(gè)像素存儲(chǔ)了一個(gè)值,這個(gè)值就是每個(gè)像素的深度(與到觀察者的距離相關(guān))。
通常深度緩沖中的值是從0到1,其中0是深度緩沖中最近的點(diǎn),1是深度緩沖中最遠(yuǎn)的點(diǎn)。為了判斷出同一位置上已被渲染的片段與新的片段哪個(gè)更近,OpenGL可以比較新片段與已在深度緩沖中的片段在窗體空間坐標(biāo)的z分量。如果新片段的z分量小于深度緩沖中片段的z分量,則新片段應(yīng)可見。當(dāng)然這個(gè)測(cè)試結(jié)果的含義是可以變更的。比如,我們讓z值大于、相等或者不等于深度緩沖內(nèi)容的片段通過測(cè)試。深度測(cè)試的結(jié)果還會(huì)影響到OpenGL對(duì)模板緩沖的行為。
然后片段的顏色被發(fā)送到混合(blending)階段或者邏輯運(yùn)算(logical operation)階段,這依賴于幀緩沖是否被認(rèn)為存儲(chǔ)浮點(diǎn)數(shù)值或者標(biāo)準(zhǔn)整型值。如果幀緩沖的內(nèi)容是浮點(diǎn)值或者標(biāo)準(zhǔn)整型值,那就會(huì)應(yīng)用混合。混合是OpenGL中高度可配置的一個(gè)階段,我們將在單獨(dú)的章節(jié)詳細(xì)討論。
簡而言之,OpenGL可以用很多函數(shù)來綜合片段著色器的輸出與幀緩沖的內(nèi)容計(jì)算出新的值并寫回幀緩沖區(qū)。如果幀緩沖區(qū)包含非標(biāo)準(zhǔn)整型值,那邏輯運(yùn)算與(AND)、或(OR)和異或(XOR)就可以應(yīng)用到片段著色器的輸出和當(dāng)前幀緩沖區(qū)的內(nèi)容上,產(chǎn)生一個(gè)新的值并會(huì)寫回幀緩沖區(qū)。
計(jì)算著色器(Compute Shaders)
本章的第一節(jié)描述了OpenGL中的圖形管線(graphics pipeline)。然而,OpenGL還包含計(jì)算著色器階段,它可以被想象成獨(dú)立于其他圖形相關(guān)階段的單獨(dú)管線。
計(jì)算著色器是一種挖掘系統(tǒng)中圖形處理器可計(jì)算能力的手段。不像以圖形為中心的頂點(diǎn)、細(xì)分曲面、幾何以及片段著色器,計(jì)算著色器可被當(dāng)成是一個(gè)特殊的,單一階段的管線。每一個(gè)計(jì)算著色器運(yùn)算一個(gè)單一的被稱為工作項(xiàng)目(work item)的工作。這些工作項(xiàng)目被輪流收集到一個(gè)群組當(dāng)做一個(gè)局部工作組(local workgroups)。這些工作組的集合可以被發(fā)送到OpenGL的計(jì)算管線進(jìn)行處理。計(jì)算著色器除了幾個(gè)少數(shù)的內(nèi)置變量用來指示著色器的工作項(xiàng)目之外,沒有任何固定的輸入或輸出。所有被計(jì)算著色器處理的過程都由著色器顯示地寫入內(nèi)存,而不是被下一個(gè)管線的階段使用。一個(gè)很基礎(chǔ)的計(jì)算著色器如清單3.13。
清單3.13 一個(gè)啥也不干的計(jì)算著色器:
#version 450 core
layout (local_size_x = 32, local_size_y = 32) in;
void main(void)
{
// Do nothing
}
在其他方面計(jì)算著色器和其他著色器一樣。要編譯一個(gè)計(jì)算著色器,我們用glCreateShader()傳入GL_COMPUTE_SHADER
來創(chuàng)建一個(gè)計(jì)算著色器對(duì)象,用glShaderSource()將GLSL源代碼附加上去,用glCompileShader()編譯它,然后用glAttachShader()和glLinkProgram()將它鏈接到一個(gè)程式中。最后得出一個(gè)有編譯的計(jì)算著色器的程式對(duì)象,它可以啟動(dòng)為我們工作了。
清單3.13中的著色器指示OpenGL局部工作組會(huì)有32*32個(gè)工作項(xiàng)目,不過之后它們啥也沒做。要?jiǎng)?chuàng)建一個(gè)做些有用事情的計(jì)算著色器,我們需要對(duì)OpenGL有更多的了解--所以我們之后還會(huì)再重溫這個(gè)話題。
使用OpenGL擴(kuò)展(Extensions)
目前為止本書所展示的示例都依賴于OpenGL核心功能。然而OpenGL的一個(gè)強(qiáng)大之處在于它可以被硬件制造商、操作系統(tǒng)零售商、甚至工具和調(diào)試器發(fā)行商進(jìn)行擴(kuò)展或者加強(qiáng)。OpenGL的擴(kuò)展有很多不同的效果。
一個(gè)擴(kuò)展是一個(gè)OpenGL核心版本的任何附加。所有擴(kuò)展的列表在OpenGL站點(diǎn)的OpenGL擴(kuò)展注冊(cè)處(http://www.opengl.org/registry/)。這些擴(kuò)展針對(duì)某個(gè)特定版本的OpenGL規(guī)范的差別被寫在一個(gè)列表中,并標(biāo)記出那個(gè)OpenGL版本是什么。那意味著擴(kuò)展相關(guān)的文本描述了OpenGL核心規(guī)范必須如何改變以期支持特定的擴(kuò)展。不過流行的以及廣泛使用的擴(kuò)展通常會(huì)被提升到OpenGL的核心版本中。所以,如果你正在運(yùn)行OpenGL最新的、最牛逼的版本,那很多有趣的擴(kuò)展都是核心檔案的一部分了。每個(gè)OpenGL版本中提升進(jìn)來的擴(kuò)展以及它們的簡要概述的完成列表在附錄C--OpenGL特性和版本。
OpenGL擴(kuò)展分為三大類:零售商,EXT以及ARB。零售商擴(kuò)展被編寫以及實(shí)做在零售商的硬件中。特定銷售商的首字母縮寫通常是零售商擴(kuò)展名字的一部分--“AMD”用于Advanced Micro Devices,“NV”用于NVIDIA,等等。多個(gè)零售商支持同一個(gè)零售商擴(kuò)展是可能的,特別當(dāng)這個(gè)擴(kuò)展被廣泛接受之后。EXT擴(kuò)展便是用于多個(gè)零售商支持的擴(kuò)展。EXT擴(kuò)展通常作為某一零售商擴(kuò)展發(fā)展,但如果另一個(gè)零售商也對(duì)實(shí)做這個(gè)擴(kuò)展很感興趣,雖然可能會(huì)做一些小的修改,然后這些零售商就會(huì)合作產(chǎn)生一個(gè)EXT版本的擴(kuò)展。ARB擴(kuò)展是OpenGL官方的一部分,因?yàn)樗鼈儽籓penGL管理團(tuán)隊(duì)(Architecture Review Board-ARB)所批準(zhǔn)。ARB擴(kuò)展通常由大多數(shù)或者全部硬件零售商所支持,并且可能已經(jīng)由零售商擴(kuò)展或者EXT擴(kuò)展開始發(fā)展。
這種擴(kuò)展處理方式第一次聽起來感覺亂亂的。現(xiàn)在已有數(shù)以百計(jì)的擴(kuò)展可用!不過新版本的OpenGL通常從程序員發(fā)現(xiàn)的有用的擴(kuò)展構(gòu)造出來。通過這種方式,每個(gè)擴(kuò)展都有被臨幸的機(jī)會(huì)。優(yōu)秀的擴(kuò)展就可以被提升到核心中;而不太好的則不被考慮。這種“自然選擇”的處理確保只有大多數(shù)有用而且重要的新特性加入到OpenGL核心版本中。
有一個(gè)很有用的工具可以用來判斷我們的機(jī)器中OpenGL實(shí)現(xiàn)所支持的擴(kuò)展,它就是Realtech VR的OpenGL Extensions Viewer。它可以從Realtech VR站點(diǎn)免費(fèi)獲得,如圖示3.6。
圖示3.6 Realtech VR的OpenGL Extensions Viewer:
使用擴(kuò)展增強(qiáng)OpenGL
在使用任何擴(kuò)展之前,我們必須確定我們的應(yīng)用運(yùn)行的OpenGL實(shí)現(xiàn)支持特定的擴(kuò)展。要獲取OpenGL支持哪些擴(kuò)展,我們可以用兩個(gè)函數(shù)。首先,為了判斷支持的擴(kuò)展的總數(shù),我們可以調(diào)用glGetIntergerv()傳入?yún)?shù)GL_NUM_EXTENSIONS
。下一步,我們調(diào)用glGetStringi()來獲取每個(gè)支持的擴(kuò)展的名字。
const GLubyte* glGetStringi(GLenum name, GLuint index);
我們將name
參數(shù)傳遞值GL_EXTENSIONS
,index
傳遞0到支持的擴(kuò)展數(shù)減1的值。這個(gè)函數(shù)返回一個(gè)字符串表示擴(kuò)展的名字。要查詢一個(gè)特定的擴(kuò)展是否支持,我們可以查詢擴(kuò)展的總數(shù),然后迭代每個(gè)擴(kuò)展,將它的名字與我們要查找的擴(kuò)展的名字進(jìn)行比較。本書的應(yīng)用框架為我們提供了一個(gè)函數(shù)來做這件事:sb7IsExtensionSupported()。它的原型為:
int sb7IsExtensionSupported(const char* extname);
這個(gè)函數(shù)在<sb7ext.h>頭文件中進(jìn)行聲明,它接收擴(kuò)展的名稱作為參數(shù),如果當(dāng)前的OpenGL上下文中支持這個(gè)擴(kuò)展就返回非零值,否則就返回零。我們?cè)趹?yīng)用中總是應(yīng)該確保要使用的擴(kuò)展是被支持的。
通常擴(kuò)展通過組合以下四種不同的方式加入到OpenGL:
- 將之前不合法的東西修正,可以根據(jù)OpenGL規(guī)范簡單地移除一些限制。
- 添加可以傳遞給現(xiàn)有函數(shù)的標(biāo)記或者擴(kuò)展參數(shù)的值的范圍。
- 擴(kuò)展GLSL,添加新的功能、內(nèi)置函數(shù)、變量或者數(shù)據(jù)類型。
- 添加全新的函數(shù)到OpenGL
在第一種情況中,之前被認(rèn)為錯(cuò)的東西現(xiàn)在不是錯(cuò)的了,我們的應(yīng)用除了使用新添加的合法行為不需要做任何事就好,當(dāng)然我們需要確保指定的擴(kuò)展是支持的。同樣,第二種情況中,我們?cè)谙嚓P(guān)的函數(shù)上使用新的標(biāo)記就好,前提是我們已經(jīng)有了這些標(biāo)記的值。這些標(biāo)記的值都在擴(kuò)展的規(guī)范中,所以如果我們的系統(tǒng)頭文件中沒有這些值我們可以去規(guī)范中查找。
為了啟用GLSL中的擴(kuò)展,我們必須首先在著色器的開頭包含一行代碼,這行代碼指示編譯器我們需要這些特性。舉個(gè)栗子,要在GLSL中啟用特定的GL_ABC_foobar_feature
擴(kuò)展,在著色器的開頭包含以下代碼:
#extension GL_ABC_foobar_feature : enable
這句代碼指示編譯器我們意欲使用這個(gè)擴(kuò)展。如果編譯器知曉這個(gè)擴(kuò)展,它會(huì)編譯這個(gè)著色器,即使底層的硬件并不支持這個(gè)特性。如果是這種情況,編譯器如果發(fā)現(xiàn)這個(gè)擴(kuò)展實(shí)際上正在被使用應(yīng)該發(fā)出警告。通常情況下,GLSL的擴(kuò)展會(huì)添加一個(gè)預(yù)處理器標(biāo)記來表明它的存在。比如,GL_ABC_foobar_feature
會(huì)隱式包含
#define GL_ABC_foobar_feature 1
這意味著你可以寫如下的代碼
#if GL_ABC_foobar_feature
// Use functions from the foobar extension
#else
// Emulate or otherwise work around the missing functionality
#endif
這讓我們可以根據(jù)底層的OpenGL實(shí)現(xiàn)是否支持特定擴(kuò)展的功能來有條件地編譯或者執(zhí)行這一功能。如果我們的著色器確實(shí)需要某一擴(kuò)展的支持并且沒有這一擴(kuò)展就完全無法工作,我們可以將剛才的代碼替換為如下更具斷言性質(zhì)的代碼:
#extension GL_ABC_foobar_feature : require
如果OpenGL實(shí)現(xiàn)不支持GL_ABC_foobar_feature
擴(kuò)展,它會(huì)編譯失敗并報(bào)告包含#extension命令的這行代碼的錯(cuò)誤。實(shí)際上,GLSL擴(kuò)展是可選的特性,我們?cè)趹?yīng)用中必須預(yù)先指示編譯器我們想使用哪個(gè)擴(kuò)展。
在實(shí)踐中,很多OpenGL實(shí)現(xiàn)缺省啟用很多擴(kuò)展中的功能,并且不需要在著色器中包含這些命令。不過,如果我們依賴于這一行為,我們的應(yīng)用很可能無法在其他OpenGL驅(qū)動(dòng)上工作。考慮到這一風(fēng)險(xiǎn),我們總是應(yīng)該顯示啟用計(jì)劃使用的擴(kuò)展。
下面讓我們看看為OpenGL引入新函數(shù)的擴(kuò)展。在大多數(shù)平臺(tái)上,我們無法直接訪問OpenGL驅(qū)動(dòng),擴(kuò)展的函數(shù)也不會(huì)變魔術(shù)一般地在我們的應(yīng)用可以調(diào)用。相反,我們必須向OpenGL驅(qū)動(dòng)請(qǐng)求一個(gè)函數(shù)指針(function pointer),它代表了我們想要調(diào)用的函數(shù)。函數(shù)指針通常聲明為兩部分:第一部分為函數(shù)指針類型的定義,第二部分為函數(shù)指針變量本身。例如以下示例代碼:
typedef void
(APIENTRYP PFNGLDRAWTRANSFORMFEEDBACKPROC) (GLenum mode, GLuint id);
PFNGLDRAWTRANSFORMFEEDBACKPROC glDrawTransformFeedback = NULL;
這段代碼聲明了一個(gè)類型PFNGLDRAWTRANSFORMFEEDBACKPROC
,它是一個(gè)函數(shù)指針,接收GLenum
和GLuint
參數(shù)。然后這段代碼聲明了類型PFNDRAWTRANSFORMFEEDBAKPROC
的一個(gè)實(shí)例glDrawTransformFeedback
。實(shí)際上在很多平臺(tái)上,glDrawTransformFeedback()的函數(shù)聲明就是像這樣。這看起來有點(diǎn)復(fù)雜,幸虧下面的三個(gè)頭文件包含所有注冊(cè)的OpenGL擴(kuò)展的函數(shù)的原型、函數(shù)指針類型以及標(biāo)志值:
#include <glext.h>
#include <glxext.h>
#include <wglext.h>
這些文件可以在OpenGL擴(kuò)展注冊(cè)處站點(diǎn)找到。glext.h
頭文件包含標(biāo)準(zhǔn)OpenGL擴(kuò)展和很多零售商擴(kuò)展,wglext.h
頭文件包含一系列Windows特定的擴(kuò)展,glxext.h
頭文件包含X窗體系統(tǒng)的擴(kuò)展(X是Linux和很多其他UNIX變種使用的窗體系統(tǒng))。
查詢擴(kuò)展函數(shù)的地址的方法實(shí)際上是平臺(tái)相關(guān)的。本書的應(yīng)用框架將這些雜事包裝到一個(gè)便利函數(shù)中,它聲明在<sb7ext.h>頭文件中。這個(gè)函數(shù)sb7GetProcAddress()的原型為:
void* sb7GetProcAddress(const char* funcname);
其中,funcname
使我們想用的擴(kuò)展函數(shù)的名字。如果這個(gè)擴(kuò)展函數(shù)被支持,那返回就是這個(gè)函數(shù)的地址,否則就是NULL
。就算OpenGL返回我們要使用的擴(kuò)展函數(shù)的可用的函數(shù)指針,但那不代表這個(gè)擴(kuò)展是存在的。有時(shí)候同樣的函數(shù)存在于多個(gè)擴(kuò)展中,并且有時(shí)候零售商搭載的驅(qū)動(dòng)只是部分實(shí)現(xiàn)了特定的擴(kuò)展。我們總是應(yīng)該使用官方策略或者sb7IsExtensionSupported()函數(shù)來檢測(cè)支持的擴(kuò)展。
總結(jié)
在這一章,我們對(duì)OpenGL的圖形管線做了一個(gè)快速的了解。我們對(duì)每個(gè)主要的階段做了一個(gè)很粗淺的介紹,并且創(chuàng)建了一個(gè)用到每一個(gè)階段的程式,雖然它沒做啥有意義的事。我們掩蓋甚至忽略了一些OpenGL中有用的特性,主要是為了在比較短的篇幅內(nèi)讓我們可以從零到可以渲染些什么東西。我們還看到OpenGL的管線和功能如何使用擴(kuò)展進(jìn)行增強(qiáng),其中不乏本書之后的示例會(huì)用到的。在下面的幾個(gè)章節(jié)中,我們會(huì)學(xué)到更多計(jì)算機(jī)圖形和OpenGL的基礎(chǔ),然后我們會(huì)再次回顧管線,對(duì)本章中的主題做更深入的了解,并且對(duì)本章跳過的一些OpenGL可以做的事情做了解。
Copyright
本書原著為《OpenGL Super Bible》,版權(quán)歸原作者所有,本譯文僅為愛好者學(xué)習(xí)交流所用。