OpenGL超級(jí)寶典第七版簡體中文-第三章:管線一覽

第三章 管線一覽

本章我們會(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ī)制是使用inout存儲(chǔ)標(biāo)識(shí)符聲明全局變量。在第二章“我們的第一個(gè)OpenGL程式”中我們簡要介紹了out標(biāo)識(shí)符,在清單2.4中用它從片段著色器輸出一個(gè)顏色。在OpenGL管線的開端,我們使用in關(guān)鍵字為頂點(diǎn)著色器輸入數(shù)據(jù)。在階段之間,使用inout組成導(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_VertexIDgl_Position等內(nèi)置變量和固定功能塊交流,如何使用out關(guān)鍵字從片段著色器輸出數(shù)據(jù)。不過,我們同樣可以使用inout關(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_TessLevelInnergl_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_ingl_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_ingl_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_spacingcw表明新的頂點(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_Positiongl_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:

figure3.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ì)分曲面三角形:

figure3.2

譯者注: 譯者的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_Positionw分量設(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)行:

formula3.1

其中xwywzw是頂點(diǎn)最終在窗體空間的坐標(biāo),xdydzd是頂點(diǎn)在標(biāo)準(zhǔn)設(shè)備空間的坐標(biāo)。pxpy是視口的寬和高,以像素為單位,然后nf分別是標(biāo)準(zhǔn)設(shè)備坐標(biāo)中修剪空間z軸的近和遠(yuǎn)平面坐標(biāo)值。最后,oxoy是視口的原點(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ù)的方法是取兩邊的叉積。方程式為:

formula3.2

其中,xiwyiw是三角形第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)繞方向

figure3.3

一旦三角形的方向被確定,OpenGL便可以丟棄正面、背面或者全部。缺省情況下,OpenGL會(huì)渲染所有的三角形,不管三角形的面的方向如何。要打開剔除,調(diào)用glEnable(),將cap參數(shù)設(shè)置為GL_CULL_FACE。當(dāng)我們啟用剔除,OpenGL缺省下就會(huì)剔除背面的三角形。要想變更三角形剔除的類型,調(diào)用glCullFace(),將face參數(shù)設(shè)置為GL_FRONTGL_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_FragCoordgl_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:

figure3.3

譯者注:譯者的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,可以看到,顏色在三角形上平滑地變化。

figure3.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:

figure3.6

使用擴(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_EXTENSIONSindex傳遞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ù)指針,接收GLenumGLuint參數(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í)交流所用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,595評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容