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

第三章 管線一覽

本章我們會學到什么

  • OpenGL管線的每個階段做什么的
  • 如果連接著色器和固定功能管線階段
  • 如果創(chuàng)建一個程式同時使用圖形管線的每個階段

在本章我們將從始至終過一遍OpenGL管線,對每個階段進行考察,包括固定功能塊和可編程著色器塊。我們已經(jīng)對頂點著色器和片段著色器有了初步的大致了解。然而,我們創(chuàng)建的應用只能簡單地在固定位置繪制一個三角形。如果我們想要使用OpenGL渲染任何有趣的東西,我們必須繼續(xù)學習管線以及我們用它所能做的所有事。本章介紹管線的每個部分,將它們彼此聯(lián)接并為每個階段提供一個著色器示例。

傳遞數(shù)據(jù)給頂點著色器

頂點著色器是OpenGL管線中第一個可編程(programmable)的階段并且是圖形管線中唯一必須的階段。不過,在頂點著色器運行之前,一個稱為頂點獲取(vertex fetching)頂點拉取(vertex pulling)的固定功能階段會運行。它自動為頂點著色器提供輸入數(shù)據(jù)。

頂點屬性

在GLSL中供著色器獲取輸入或輸出數(shù)據(jù)的機制是使用inout存儲標識符聲明全局變量。在第二章“我們的第一個OpenGL程式”中我們簡要介紹了out標識符,在清單2.4中用它從片段著色器輸出一個顏色。在OpenGL管線的開端,我們使用in關(guān)鍵字為頂點著色器輸入數(shù)據(jù)。在階段之間,使用inout組成導管在著色器之間傳遞數(shù)據(jù)。我們馬上就會知道這個?,F(xiàn)在,考慮頂點著色器的輸入以及如果我們使用in存儲標識符聲明一個變量發(fā)生了什么。這個標識符標明這個變量是頂點著色器的輸入,意味著這個變量是OpenGL圖形管線的重要輸入。這個變量在固定功能的頂點獲取階段被自動填充。這個變量即為頂點屬性(vertex attribute)。

頂點屬性是頂點數(shù)據(jù)引入OpenGL管線的手段。要聲明一個頂點屬性,我們在頂點著色器中使用in存儲標識符聲明一個全局變量即可。如清單3.1所示,我們將offset變量聲明為一個輸入的頂點屬性。

清單3.1 聲明一個頂點屬性:

#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變量作為頂點著色器的輸入。因為它是管線第一個著色器的輸入,故它會在頂點獲取階段被自動填充。我們可以用眾多頂點屬性相關(guān)的函數(shù)(glVertexAttrib*())來指示OpenGL在頂點獲取階段為相關(guān)變量填充何值。我們將會用到的glVertexAttrib4fv()的原型如下:

void glVertexAttrib4fv(GLuint index, const GLfloat* v);

參數(shù)index用來索引指定的屬性,v是要放入屬性的新數(shù)據(jù)的指針。你或許已注意到聲明offset屬性中的代碼layout (location = 0)。這是一個布局指示符(layout qualifier),我們用它來設(shè)置指定的頂點屬性的位置(location)為0。這個位置的值就是我們通過index來傳遞進行這個屬性引用的值。

每次我們調(diào)用任何一個glVertexAttrib*()函數(shù)都會更新傳遞給頂點著色器的頂點屬性的值。我們可以使用這個方法來給我們的三角形加上動畫。清單3.2展示了一個更新版本的渲染函數(shù),它在每一幀都會更新offset的值。

清單3.2 更新一個頂點屬性:

// 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);
}

運行清單3.2中的代碼,我們會看到三角形在窗體中以一個圓滑的橢圓形路徑運動。(譯者注: 譯者的倉庫sb7examples中相應工程為chapter3/update_vertex_attribute)

在著色器階段間傳遞數(shù)據(jù)

目前為止我們已經(jīng)看到了如何通過使用in關(guān)鍵字創(chuàng)建一個頂點屬性來傳遞數(shù)據(jù)給頂點著色器,如何通過讀寫諸如gl_VertexID、gl_Position等內(nèi)置變量和固定功能塊交流,如何使用out關(guān)鍵字從片段著色器輸出數(shù)據(jù)。不過,我們同樣可以使用inout關(guān)鍵字在著色器階段之間傳遞我們的數(shù)據(jù)。一如我們在片段著色器中使用out關(guān)鍵字來創(chuàng)建輸出顏色值的變量一樣,我們也能在頂點著色器中使用out關(guān)鍵字創(chuàng)建一個輸出變量。在一個著色器中寫入到一個輸出變量的任何內(nèi)容都會傳遞給下一個著色器階段中以in聲明的同名變量。比如,如果我們在頂點著色器中使用out關(guān)鍵字聲明一個叫vs_color的變量,接下來在片段著色器階段這個變量就會匹配到一個用in關(guān)鍵字聲明的名為vs_color的變量上(假設(shè)它們間沒有其他的階段)。

如果將我們簡單的頂點著色器修改為清單3.3,包含一個vs_color的輸出變量,并相應將我們簡單的片段著色器修改為清單3.4,包含一個vs_color的輸入變量,我們便能將一個值從頂點著色器傳給片段著色器。然后,相比之前輸出一個固定的顏色值,現(xiàn)在這個片段著色器可以將頂點著色器傳給它的顏色值輸出。

清單3.3 帶一個輸出變量的頂點著色器:

#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)檫@個頂點著色器聲明了第二個輸入變量: color(這次location為1),并將它的值寫入到輸出變量vs_color。然后這個值為清單3.4的片段著色器所用,寫入到幀緩沖區(qū)。這使得我們可以把一個通過glVertexAttrib*設(shè)置的頂點屬性的顏色值一路從頂點著色器傳入片段著色器,然后寫到幀緩沖區(qū)。結(jié)果就是我們可以繪制他色的三角形了。

清單3.4 帶有一個輸入變量的片段著色器:

#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中相應工程為chapter3/different_colored_triangle。

數(shù)據(jù)塊接口(Interface Blocks)

一次聲明一個接口變量用來在著色器階段間傳遞數(shù)據(jù)可能是最簡單的方法。然而,在大多數(shù)工業(yè)用的應用的,我們會想在著色器階段間傳遞成堆的數(shù)據(jù),這些可能包括數(shù)組、結(jié)構(gòu)體以及其他復雜排列的變量。為達此目的,我們可以將好些個變量組成一個數(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 帶一個輸出數(shù)據(jù)塊接口的頂點著色器:

#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ù)塊接口同時有一個塊名稱(大寫的VS_OUT)和一個實例名稱(小寫的vs_out)。數(shù)據(jù)塊接口在著色器階段間通過塊名稱匹配(本例中為VS_OUT)但在著色器中則是用實例名稱引用(本例中為vs_out)。將我們的片段著色器修改為清單3.6來使用這個數(shù)據(jù)塊接口。

清單3.6 帶一個輸入數(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中相應工程為chapter3/interface_block_triangle。

通過塊名稱匹配但允許塊實例在每個著色器階段有不同的名稱,這種設(shè)定出于兩方面的考量。第一,允許不同著色器階段使用不同的名稱進行引用,可以避免一些混亂,比如要在片段著色器中使用vs_out。第二,當我們縱橫于一些著色器階段時,比如頂點著色器、細分著色器或者幾何著色器階段(我們馬上就會看到了),這樣使得接口從單個條目變成數(shù)組。值得注意的是數(shù)據(jù)塊接口只能用于著色器階段到著色器階段的數(shù)據(jù)傳遞--我們不能用它將頂點著色器的輸入或者片段著色器的輸出組建成群。

細分曲面(Tessellation)

細分曲面是將高階圖元(在OpenGL中常稱為碎片(patch))降解為很多更小的、更簡單的圖元,諸如三角形之后進行渲染。OpenGL包含一個固定功能的、可配置的細分曲面引擎,它可以將四邊形、三角形以及線段降解為可能很多的可被常規(guī)光柵硬件使用的更小的點、線段或者三角形。從邏輯上來說,細分曲面階段由三部分組成:細分曲面控制著色器,固定功能細分曲面引擎以及細分曲面運算著色器,在OpenGL管線中細分曲面階段直接跟在頂點著色階段之后。

細分曲面控制著色器(Tessellation Control Shaders)

細分曲面三個階段的第一階段為細分曲面控制著色器(TCS;有時簡稱為控制著色器)。這個著色器從頂點著色器接收輸入并主要負責兩件事:1.確定要發(fā)送給細分曲面引擎的細分曲面等級。2.生成細分曲面運行之后要發(fā)送給細分曲面運算著色器的數(shù)據(jù)。

在OpenGL中,細分曲面通過將被稱為碎片(patches)的高階表面降解為點、線段或者三角形而進行正常工作。每一個碎片都由一定數(shù)目的控制點(control points)組成。每個碎片的控制點數(shù)目都是可配置的,通過調(diào)用glPatchParameteri()即可設(shè)置,同時將pname設(shè)置為GL_PATCH_VERTICES以及value設(shè)置為構(gòu)成每個碎片的控制點的數(shù)目。glPatchParameteri()的原型如:

void glPatchParameteri(GLenum pname, GLint value);

缺省情況下,每個碎片的控制點數(shù)目是3。所以,如果這就是我們想要的(一如我們接下來的示例),我們完全可以不調(diào)用這個函數(shù)。用來構(gòu)成一個碎片的最高控制點數(shù)目是由實現(xiàn)定義的,但保證至少為32。

當細分曲面開始時,頂點著色器會針對每一個控制點運行一次,不過細分曲面控制著色器根據(jù)控制點的群組按批次運行,每個批次的大小和每個碎片的頂點數(shù)一樣。意即,頂點被當做控制點而且頂點著色器的輸出結(jié)果被成批送往細分曲面控制著色器當做輸入。每個碎片的控制點數(shù)目是可以改變的所以細分曲面控制著色器輸出的控制點數(shù)目可以和它消耗的控制點數(shù)目不同??刂浦鳟a(chǎn)生的控制點數(shù)目在控制著色器的源代碼中使用一個輸出布局標識符進行設(shè)置。這樣的布局標識符看起來像這樣:

layout (vertices = N) out;

這里的N即每個碎片的控制點數(shù)目??刂浦饔胸熑斡嬎爿敵隹刂泣c的數(shù)目以及設(shè)置作為最終結(jié)果發(fā)送給固定功能細分曲面引擎的碎片的細分曲面因子。輸出的細分曲面因子寫入內(nèi)置變量gl_TessLevelInnergl_TessLevelOuter中,而其他任何在管線中傳遞的數(shù)據(jù)都正常地寫入用戶定義的輸出變量(使用out關(guān)鍵字聲明的或者特殊的內(nèi)置gl_out數(shù)組)。

清單3.7展示了一個簡單的細分曲面控制著色器。它用布局標識符layout (vertices = 3) out;設(shè)置輸出的控制點數(shù)目為3(與缺省的輸入控制點數(shù)目相同),將它的輸入拷貝到輸出(使用內(nèi)置變量gl_ingl_out),并設(shè)置內(nèi)和外的細分曲面級別為5。更高的細分曲面級別會產(chǎn)生更密集的細分曲面輸出,更低的級別會產(chǎn)生更粗糙的細分曲面輸出。將細分曲面因子設(shè)置為0會導致整個碎片被丟棄。

內(nèi)置變量gl_InvocationID被用作gl_ingl_out數(shù)組的索引,從0開始算起。這個變量表示當前被調(diào)用的細分曲面控制著色器中被處理的碎片的控制點索引值。

清單3.7 我們的第一個細分曲面控制著色器:

#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;
}

細分曲面引擎(The Tessellation Engine)

細分曲面引擎是OpenGL管線中的一個固定功能部分,它接收表示為碎片的高階表面并將它們降解為更簡單的圖元,比如:點、線段或者三角形。在細分曲面引擎接收碎片之前,細分曲面控制著色器處理輸入的控制點并設(shè)置細分曲面因子,然后用它們來降解這個碎片。細分曲面引擎生成輸出圖元之后,用以表示這些圖元的頂點被細分曲面運算著色器所利用。細分曲面引擎有責任生成用以調(diào)用細分曲面運算著色器的參數(shù),然后細分曲面運算著色器用這些參數(shù)轉(zhuǎn)換作為最終結(jié)果的圖元并將它們準備好光柵化。

細分曲面運算著色器(Tessellation Evaluation Shaders)

一旦固定功能細分曲面引擎運行后,它會產(chǎn)生一些輸出頂點用來表示生成的圖元。這些頂點會傳遞給細分曲面運算著色器。細分曲面運算著色器(TES;或者簡稱為運算著色器)會對細分曲面器生成的每個頂點運行一次。當細分曲面級別高時,細分曲面運算著色器將會運行很多次。為此,對于復雜的運算著色器和高細分曲面級別我們需要小心應付。

清單3.8展示了一個細分曲面運算著色器,它接受由清單3.7所示的控制著色器運行輸出的頂點作為輸入。在這個著色器開頭是一個布局標識符,它設(shè)置了細分曲面模式。在本例中,我們選擇模式為三角形。其他標識符equal_spacingcw表明新的頂點生成時要是沿著細分的?多邊形邊緣等距的并且生成的三角形的頂點環(huán)繞次序是順時針的。我們會在第八章的“細分曲面”中對其他可能的選項進行更全面的介紹。

這個著色器的剩余部分如頂點著色器一樣對gl_Position進行了賦值。它使用多個內(nèi)置變量來計算gl_Position的值。第一個是gl_TessCoord,它是細分曲面器生成的頂點的質(zhì)心坐標。第二個是gl_in[]結(jié)構(gòu)體數(shù)組的成員gl_Position。gl_in匹配清單3.7中的細分曲面控制著色器寫入的gl_out結(jié)構(gòu)體。這個著色器主要實做了直通細分(pass-through tessellation)。意即,細分后輸出的碎片與原始輸入的三角形碎片形狀一致。

清單3.8 我們的第一個細分曲面運算著色器:

#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);
}

為了能看到細分曲面器的結(jié)果,我們需要指示OpenGL只繪制最終結(jié)果的三角形的輪廓。為達此目的,我們調(diào)用glPolygonMode(),它的原型為:

void glPolygonMode(GLenum face, GLenum mode);

face參數(shù)指明我們想影響哪個類型的多邊形。因為我們想要影響所有東西,故我們設(shè)置它為GL_FRONT_AND_BACK.mode表明我們想要如何渲染多邊形。我們想要渲染為線框模式(即直線),我們將這個參數(shù)設(shè)置為GL_LINE。其他模式我們很快就會解釋的。我們的三角形示例在使用細分曲面以及清單3.7、清單3.8的著色器之后,渲染結(jié)果如圖示3.1:

figure3.1

譯者注: 譯者的sb7examples中相應項目為chapter3/triangle_with_tessellation

幾何著色器(Geometry Shaders)

從邏輯上來講,幾何著色器是是前端著色器的最后階段了,它在頂點和細分曲線階段之后、光柵器之前。幾何著色器針對每個圖元運行一次并且對正在處理的圖元的所有組成頂點有訪問權(quán)限。幾何著色器也是著色器階段中唯一可以編程控制數(shù)據(jù)流總量增減的著色器。雖然說細分曲面著色器也可以增減管線的工作量,但它只能通過設(shè)置碎片的細分曲面級別來隱式地影響工作量。而相對的,幾何著色器包含兩個函數(shù)--EmitVertex()EndPrimitive(),它們能顯示地產(chǎn)生頂點發(fā)送到圖元組裝(primitive assembly)和光柵化(rasterization)。

幾何著色器的另一個獨一的功能是它可以在管線中途改變圖元模式。比如,它們可以接收三角形作為輸入并輸出一些列的點或者線,再或者從一系列不相干的點創(chuàng)建三角形。清單3.9示一例。

清單3.9 我們的第一個幾何著色器:

#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的幾何著色器再次扮演了一個簡單地直通著色器,它將輸入的三角形轉(zhuǎn)換為點,這樣我們便能看見它們的頂點。第一個布局標識符表明這個幾何著色器期望輸入數(shù)據(jù)為三角形。第二個布局標識符指示OpenGL這個幾何著色器會產(chǎn)生點并且每個著色器最多產(chǎn)生三個點。在main函數(shù)中,迭代了gl_in數(shù)組的所有成員,gl_in數(shù)組的長度通過它的.length()函數(shù)獲知。

實際上我們知道gl_in數(shù)組的長度就是三,因為我們正在處理的是三角形,每個三角形有三個頂點。這個幾何著色器的輸出再一次與頂點著色器神似(前面有細分曲面運算著色器)。特別是我們寫入值到gl_Position來設(shè)置輸出頂點的位置。然后,我們調(diào)用EmitVertex(),它在幾何著色器的輸出中產(chǎn)生一個頂點。幾何著色器在著色器結(jié)束的時候自動調(diào)用EndPrimitive(),故本例中我們無須顯示調(diào)用它。運行這個著色器之后,將會產(chǎn)生三個頂點,然后渲染為三個點。

將這個幾何著色器插入到我們之前的細分曲面三角形示例中,我們會得到如圖示3.2的輸出。為了得到這個圖像,我們通過調(diào)用glPointSize()將點的大小設(shè)置為5.0,這樣會使得點大一些容易辨識。

圖示3.2 帶幾何著色器的細分曲面三角形:

figure3.2

譯者注: 譯者的sb7examples中相應的項目為chapter3/triangle_with_tess_geometry

圖元組裝、修剪和光柵化(Primitive Assembly,Clipping, and Rasterization)

管線的前端(這包括頂點著色、細分曲面以及幾何著色)運行完成之后,管線中一個固定功能部分會開始執(zhí)行一系列任務(wù),它接收頂點表示的場景并將其轉(zhuǎn)換為一系列像素,這些像素輪流被上色并寫入到顯示屏幕上。這個過程的第一步是圖元組裝,它將頂點群組為線或者三角形。圖元組裝仍然發(fā)生在“點”上,不過這種情況它無關(guān)緊要了。

一旦各個分散獨立的頂點被組成為圖元,圖元就會針對可顯示區(qū)域進行修剪(clipped),這個可顯示區(qū)域通常是窗體或者顯示屏幕,但也可以是一個更小的稱為視口(viewport)的區(qū)域。最終,圖元中被判斷為可能可見的部分被發(fā)送到一個稱為光柵器(rasterizer)的固定功能子系統(tǒng)。這個子系統(tǒng)會判斷出哪些像素被圖元(點、線或者三角形)覆蓋到并將這些像素發(fā)送到下一階段--換言之,就是片段著色階段。

修剪

隨著頂點離開管線的前端,它們的位置便處于修剪空間(clip space)。修剪空間是眾多用來表示位置的坐標系統(tǒng)之一。你可能已注意到,我們在頂點、細分曲面以及幾何著色器中寫入的gl_Position變量是一個vec4類型的,我們寫入到它的產(chǎn)生的位置值也是四個分量的向量。這個被?稱為齊次坐標(homogeneous coordinate)。齊次坐標系統(tǒng)被用在投影幾何(projective geometry)中,因為很多數(shù)學問題最終在齊次坐標中都比常規(guī)笛卡爾坐標空間(Cartesian space)要簡單。齊次坐標值比對應的笛卡爾坐標值要多一個分量,這也是為什么我們的三維位置向量被表示為四分量的變量。

盡管管線前端的輸出是四分量齊次坐標,但修剪卻是依靠笛卡爾坐標的。因此,為了將齊次坐標轉(zhuǎn)換為笛卡爾坐標,OpenGL執(zhí)行了透視分割(perspective division),將位置的四個分量都用w分量進行分割。這樣可以將頂點從齊次坐標空間投影到笛卡爾坐標空間,保持w為1.0.到目前為止的所有示例,我們都將gl_Positionw分量設(shè)置為1.0,所以這種情況下分割不會有任何效果。我們之后研究投影幾何時會對將w設(shè)置為其他值的效果做討論的。

位置在投影分割之后的結(jié)果就會處于標準設(shè)備空間(normalized device space)。在OpenGL中,標準設(shè)備空間的可視區(qū)域是在x和y軸上-1.0到1.0以及z軸上0.0到1.0的體積。任何在這個區(qū)域內(nèi)的幾何圖形都可能對用戶是可見的而這個區(qū)域外的任何東西都將被丟棄。這個體積的六個面由三維空間的平面組成。因為一個平面將一個坐標空間一分為二,所以這個平面的每一邊的體積被稱為半空間(half-spaces)。

在將圖元傳遞到下一個階段之前,OpenGL通過判斷每個圖元的頂點在六個平面的哪一邊來進行修剪。每個平面實際上有一個外側(cè)(outside)和里側(cè)(inside)。如果一個圖元的所有頂點都處在某一個平面的外側(cè),那這個圖元就被丟棄。如果一個圖元的所有頂點都處在所有平面的里側(cè)(換言之處在可視體積內(nèi)),然后這個圖元就會原封不動傳遞下去。部分可視(意即這個圖元與某個平面交叉)的圖元需要特殊處理。這個主題更多的詳情我們將在第七章“修剪”給出。

視口變換(Viewport Transformation)

在修剪后,幾何圖形的所有頂點都會處在標準設(shè)備坐標范圍內(nèi),也就是在x和y軸上-1.0到1.0意即z軸上0.0到1.0的體積內(nèi)。然而,我們繪制的目標是窗體,它的坐標通常是從左下的(0,0)到右上的(w-1,h-1),其中w和h分別為窗體的寬和高,單位是像素(窗體的坐標原點可以進行更改,比如改為右上)。為了將我們的幾何圖形放入窗體,OpenGL應用視口變換(viewport transform),它將縮放和偏移應用到頂點的標準設(shè)備坐標上,從而將它們放置到窗體坐標(window coordinates)中。要應用的縮放和偏移通過視口界限來決定,界限可以通過調(diào)用glViewport()glDepthRange()來設(shè)置。它們的原型為:

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glDepthRange(GLdouble nearVal, GLdouble farVal);

變換按照如下形式進行:

formula3.1

其中xw、ywzw是頂點最終在窗體空間的坐標,xd、ydzd是頂點在標準設(shè)備空間的坐標。pxpy是視口的寬和高,以像素為單位,然后nf分別是標準設(shè)備坐標中修剪空間z軸的近和遠平面坐標值。最后,oxoy是視口的原點。

剔除(Culling)

在一個三角形繼續(xù)處理之前,它可能需要被傳遞到一個叫“剔除”的階段,這個階段會判斷三角形的面朝向或者背向觀察者,然后會根據(jù)計算結(jié)果決定是否真地進行下一步以及繪制它。如果三角形的面朝向觀察者,那它就被認為是正面,否則,它就被認為是背面。丟棄背面?的三角形是很普遍的,因為當一個對象是封閉的,任何背面的三角形都會被其他正面的三角形所隱藏。

為了判斷一個三角形是正面還是背面,OpenGL會判斷三角形在窗體空間的面積正負。一種判斷三角形面積正負的方法是取兩邊的叉積。方程式為:

formula3.2

其中,xiwyiw是三角形第i個頂點在窗體空間的坐標,i⊕1是(i+1)模3。如果這個面積是正的,那么這個三角形就被認為是正面;如果它是負的,那么它就被認為是背面。這個計算結(jié)果的含義可以通過調(diào)用glFrontFace()進行顛倒,它的原型為:

void glFrontFace(GLenum mode);

mode可被設(shè)置為GL_CW或者GL_CCW(其中GL_CW表示順時針,GL_CCW表示逆時針)。這被稱為三角形的環(huán)繞方向(winding order),順時針和逆時針表示三角形頂點出現(xiàn)在窗體空間的次序。缺省情況下,環(huán)繞方向為逆時針,這意味著頂點次序為逆時針的三角形被認為是正面,頂點次序為順時針的三角形被認為是背面。如果環(huán)繞方面被設(shè)置為GL_CW,那前面方程式中的a的含義在剔除過程中也就和前述相反。圖示3.3以可觀的方式展示方才所述。

圖示3.3 順時針(左)和逆時針(右)環(huán)繞方向

figure3.3

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

因為點和線沒有幾何面積(很顯然,一旦點和線渲染到顯示屏幕上是有面積的,否則我們不會看到它們。然而這個面積是人為造成的,從它們的頂點上并不能直接計算出來),所以這個面向計算并不應用于它們,它們也無法在這個階段被剔除。

光柵化

光柵化是判斷哪些片段被一個圖元(比如一條線或者一個三角形)所覆蓋了的過程。有很多算法可以完成此事,不過對于三角形,大多數(shù)OpenGL系統(tǒng)會選定基于半空間的算法,因為它可以很好的并行實做。簡明扼要地講就是,OpenGL會在窗體坐標內(nèi)為三角形設(shè)定一個界限框(bounding box),并對界限框內(nèi)的每個片段測試它在三角形內(nèi)還是外。要完成這個測試,將三角形的每個邊當成窗體的半空間分割線即可。

在三角形三邊內(nèi)側(cè)的片段被認為是在三角形內(nèi),在任何一邊外側(cè)的片段則被認為是在三角形外。因為判斷一條線、一個點在哪一側(cè)是相對簡單的而且和除了線的端點或點本身的位置之外都不想干,很多測試可以并發(fā)執(zhí)行,這樣為大規(guī)模并行提供了機會。

片段著色器(Fragment Shaders)

片段著色器是OpenGL圖形管線中的最后可編程階段了。每個片段在發(fā)送到幀緩沖(framebuffer)混合到窗體之前,這個階段有責任決定每個片段的顏色。在光柵器處理一個圖元之后,它會產(chǎn)生一系列片段,這些片段需要傳遞給片段著色器進行調(diào)色。到了這一步,管線的工作量會有一個爆炸式的增長,因為每一個三角形都可能產(chǎn)生數(shù)百、數(shù)千甚至上百萬個片段。

片段用來描述一種元素,它可能對像素的最終顏色造成至關(guān)重要的影響。不過像素的最終顏色可能并不會是某個指定的片段著色器執(zhí)行的結(jié)果,因為像素的最終顏色還與很多其他因素有關(guān),比如深度(depth)、模板測試(stencil tests)、混合(blending)以及多重采樣(multi-sampling),當然這些在本書后面都會涉及到。

第二章的清單2.4展示了我們的第一個片段著色器的源代碼。它是一個簡單到不行的著色器,僅僅只是聲明了一個輸出變量,然后為它賦了一個固定的值。在真實世界的應用中,片段著色器通常會復雜很多并且需要進行與光照、材質(zhì)甚至片段深度的諸多計算。片段著色器有多個可用的內(nèi)置輸入變量,比如gl_FragCoord,gl_FragCoord包含了片段在窗體中的位置。我們可以用它來為每個片段都產(chǎn)生獨一無二的顏色。

清單3.10提供了一個片段著色器,它從gl_FragCoord得出輸出的顏色。圖示3.4展示了我們原先的“我們的一個三角形”搭配清單3.10的著色器的輸出。

清單3.10 從一個片段的位置得出顏色:

#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中相應工程為chapter3/triangle_with_fragshader

我們可以看到,圖示3.4中三角形的每個像素的顏色現(xiàn)在都是關(guān)于它的位置的函數(shù),而且產(chǎn)生了與顯示屏幕匹配(screen-aligned)的模式。清單3.10的著色器創(chuàng)建了一種格子樣式(checkered patterns)。

gl_FragCoord是片段著色器可用的內(nèi)置變量中的一個。不過,和其他著色器階段一樣,我們可以為片段著色器定義自定義的輸入,這些輸入會在光柵化前的任何一個階段進行填充。比如,如果我們有一個簡單的程式只有一個頂點著色器和片段著色器,我們可以從頂點著色器傳遞數(shù)據(jù)給片段著色器。

片段著色器的輸入不像其他著色器階段的,OpenGL會針對要渲染的圖元對輸入進行插值計算。為了演示這種情況,我們?nèi)砬鍐?.3的頂點著色器,將它做一些修改,為每個頂點賦予不同的固定顏色,最后如3.11所示。

清單3.11 頂點不同顏色的頂點著色器:

#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中所示,我們增加了第二個常量數(shù)組,它包含一組顏色并且使用gl_VertexID進行索引,它的值最終寫入到輸出變量vs_color中。在清單3.12中,我們將之前簡單的片段著色器進行修改如下。

清單3.12 頂點不同顏色的片段著色器:

#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中相應工程為chapter3/triangle_color_smooth

幀緩沖運算

幀緩沖是OpenGL圖形管線的最后一個階段。它可以表示顯示屏幕的可視內(nèi)容以及用來存儲每個像素除了顏色之外額外值的內(nèi)存區(qū)域。在大多數(shù)平臺上,這意味著我們在桌面上看到的窗體(或者說整個顯示屏幕,如果我們的應用覆蓋了整個顯示屏幕)是屬于操作系統(tǒng)(或者更精確來說是窗體系統(tǒng))的。窗體系統(tǒng)提供的幀緩沖是缺省幀緩沖,但如果我們想渲染到顯示屏幕外的區(qū)域的話,我們是可以提供自己的幀緩沖的。幀緩沖中存儲的狀態(tài)包含有諸多信息,比如片段著色器產(chǎn)生的數(shù)據(jù)寫到哪,這些數(shù)據(jù)是什么格式,等等。這個狀態(tài)存儲在一個幀緩沖對象(framebuffer object)中。像素操作狀態(tài)也可以被認為是幀緩沖的一部分,但不會每個像素都存儲一個幀緩沖對象。

像素運算

片段著色器產(chǎn)生一個輸出片段后,在這個片段寫入窗體前還會做一些事情,比如判斷它是否仍在所屬的窗體中。這些事情我們都可以在應用中打開或者關(guān)閉。首先可能發(fā)生的事是裁剪測試(scissor test),它將片段與我們定義的一個矩形進行測試。如果片段在矩形內(nèi),就繼續(xù)處理;如果片段在矩形外,就被丟棄。

然后會進行模板測試(stencil test)。這一步會將我們的應用提供的值與模板緩沖作比較,模板緩沖為每個像素都對應存儲了一個值。模板緩沖的內(nèi)容并沒有特別的含義,可以隨便存啥。當我們要使用一種叫做多重采樣(multi-sampling)的技術(shù)時,一個幀緩沖中是可以存儲多個深度緩沖、模板緩沖或者每個像素的顏色值的。本書后面我們會對此做更深入的探索。

模板測試完成之后,就會進行深度測試(depth test)。所謂的深度測試就是將片段的z坐標值與深度緩沖(depth buffer)的內(nèi)容做比較。深度緩沖是和模板緩沖相似的一段內(nèi)存區(qū)域,它做為幀緩沖的一部分為每個像素存儲了一個值,這個值就是每個像素的深度(與到觀察者的距離相關(guān))。

通常深度緩沖中的值是從0到1,其中0是深度緩沖中最近的點,1是深度緩沖中最遠的點。為了判斷出同一位置上已被渲染的片段與新的片段哪個更近,OpenGL可以比較新片段與已在深度緩沖中的片段在窗體空間坐標的z分量。如果新片段的z分量小于深度緩沖中片段的z分量,則新片段應可見。當然這個測試結(jié)果的含義是可以變更的。比如,我們讓z值大于、相等或者不等于深度緩沖內(nèi)容的片段通過測試。深度測試的結(jié)果還會影響到OpenGL對模板緩沖的行為。

然后片段的顏色被發(fā)送到混合(blending)階段或者邏輯運算(logical operation)階段,這依賴于幀緩沖是否被認為存儲浮點數(shù)值或者標準整型值。如果幀緩沖的內(nèi)容是浮點值或者標準整型值,那就會應用混合?;旌鲜荗penGL中高度可配置的一個階段,我們將在單獨的章節(jié)詳細討論。

簡而言之,OpenGL可以用很多函數(shù)來綜合片段著色器的輸出與幀緩沖的內(nèi)容計算出新的值并寫回幀緩沖區(qū)。如果幀緩沖區(qū)包含非標準整型值,那邏輯運算與(AND)、或(OR)和異或(XOR)就可以應用到片段著色器的輸出和當前幀緩沖區(qū)的內(nèi)容上,產(chǎn)生一個新的值并會寫回幀緩沖區(qū)。

計算著色器(Compute Shaders)

本章的第一節(jié)描述了OpenGL中的圖形管線(graphics pipeline)。然而,OpenGL還包含計算著色器階段,它可以被想象成獨立于其他圖形相關(guān)階段的單獨管線。

計算著色器是一種挖掘系統(tǒng)中圖形處理器可計算能力的手段。不像以圖形為中心的頂點、細分曲面、幾何以及片段著色器,計算著色器可被當成是一個特殊的,單一階段的管線。每一個計算著色器運算一個單一的被稱為工作項目(work item)的工作。這些工作項目被輪流收集到一個群組當做一個局部工作組(local workgroups)。這些工作組的集合可以被發(fā)送到OpenGL的計算管線進行處理。計算著色器除了幾個少數(shù)的內(nèi)置變量用來指示著色器的工作項目之外,沒有任何固定的輸入或輸出。所有被計算著色器處理的過程都由著色器顯示地寫入內(nèi)存,而不是被下一個管線的階段使用。一個很基礎(chǔ)的計算著色器如清單3.13。

清單3.13 一個啥也不干的計算著色器:

#version 450 core

layout (local_size_x = 32, local_size_y = 32) in;

void main(void)
{
    // Do nothing
}

在其他方面計算著色器和其他著色器一樣。要編譯一個計算著色器,我們用glCreateShader()傳入GL_COMPUTE_SHADER來創(chuàng)建一個計算著色器對象,用glShaderSource()將GLSL源代碼附加上去,用glCompileShader()編譯它,然后用glAttachShader()glLinkProgram()將它鏈接到一個程式中。最后得出一個有編譯的計算著色器的程式對象,它可以啟動為我們工作了。

清單3.13中的著色器指示OpenGL局部工作組會有32*32個工作項目,不過之后它們啥也沒做。要創(chuàng)建一個做些有用事情的計算著色器,我們需要對OpenGL有更多的了解--所以我們之后還會再重溫這個話題。

使用OpenGL擴展(Extensions)

目前為止本書所展示的示例都依賴于OpenGL核心功能。然而OpenGL的一個強大之處在于它可以被硬件制造商、操作系統(tǒng)零售商、甚至工具和調(diào)試器發(fā)行商進行擴展或者加強。OpenGL的擴展有很多不同的效果。

一個擴展是一個OpenGL核心版本的任何附加。所有擴展的列表在OpenGL站點的OpenGL擴展注冊處(http://www.opengl.org/registry/)。這些擴展針對某個特定版本的OpenGL規(guī)范的差別被寫在一個列表中,并標記出那個OpenGL版本是什么。那意味著擴展相關(guān)的文本描述了OpenGL核心規(guī)范必須如何改變以期支持特定的擴展。不過流行的以及廣泛使用的擴展通常會被提升到OpenGL的核心版本中。所以,如果你正在運行OpenGL最新的、最牛逼的版本,那很多有趣的擴展都是核心檔案的一部分了。每個OpenGL版本中提升進來的擴展以及它們的簡要概述的完成列表在附錄C--OpenGL特性和版本。

OpenGL擴展分為三大類:零售商,EXT以及ARB。零售商擴展被編寫以及實做在零售商的硬件中。特定銷售商的首字母縮寫通常是零售商擴展名字的一部分--“AMD”用于Advanced Micro Devices,“NV”用于NVIDIA,等等。多個零售商支持同一個零售商擴展是可能的,特別當這個擴展被廣泛接受之后。EXT擴展便是用于多個零售商支持的擴展。EXT擴展通常作為某一零售商擴展發(fā)展,但如果另一個零售商也對實做這個擴展很感興趣,雖然可能會做一些小的修改,然后這些零售商就會合作產(chǎn)生一個EXT版本的擴展。ARB擴展是OpenGL官方的一部分,因為它們被OpenGL管理團隊(Architecture Review Board-ARB)所批準。ARB擴展通常由大多數(shù)或者全部硬件零售商所支持,并且可能已經(jīng)由零售商擴展或者EXT擴展開始發(fā)展。

這種擴展處理方式第一次聽起來感覺亂亂的?,F(xiàn)在已有數(shù)以百計的擴展可用!不過新版本的OpenGL通常從程序員發(fā)現(xiàn)的有用的擴展構(gòu)造出來。通過這種方式,每個擴展都有被臨幸的機會。優(yōu)秀的擴展就可以被提升到核心中;而不太好的則不被考慮。這種“自然選擇”的處理確保只有大多數(shù)有用而且重要的新特性加入到OpenGL核心版本中。

有一個很有用的工具可以用來判斷我們的機器中OpenGL實現(xiàn)所支持的擴展,它就是Realtech VR的OpenGL Extensions Viewer。它可以從Realtech VR站點免費獲得,如圖示3.6。

圖示3.6 Realtech VR的OpenGL Extensions Viewer:

figure3.6

使用擴展增強OpenGL

在使用任何擴展之前,我們必須確定我們的應用運行的OpenGL實現(xiàn)支持特定的擴展。要獲取OpenGL支持哪些擴展,我們可以用兩個函數(shù)。首先,為了判斷支持的擴展的總數(shù),我們可以調(diào)用glGetIntergerv()傳入?yún)?shù)GL_NUM_EXTENSIONS。下一步,我們調(diào)用glGetStringi()來獲取每個支持的擴展的名字。

const GLubyte* glGetStringi(GLenum name, GLuint index);

我們將name參數(shù)傳遞值GL_EXTENSIONS,index傳遞0到支持的擴展數(shù)減1的值。這個函數(shù)返回一個字符串表示擴展的名字。要查詢一個特定的擴展是否支持,我們可以查詢擴展的總數(shù),然后迭代每個擴展,將它的名字與我們要查找的擴展的名字進行比較。本書的應用框架為我們提供了一個函數(shù)來做這件事:sb7IsExtensionSupported()。它的原型為:

int sb7IsExtensionSupported(const char* extname);

這個函數(shù)在<sb7ext.h>頭文件中進行聲明,它接收擴展的名稱作為參數(shù),如果當前的OpenGL上下文中支持這個擴展就返回非零值,否則就返回零。我們在應用中總是應該確保要使用的擴展是被支持的。

通常擴展通過組合以下四種不同的方式加入到OpenGL:

  • 將之前不合法的東西修正,可以根據(jù)OpenGL規(guī)范簡單地移除一些限制。
  • 添加可以傳遞給現(xiàn)有函數(shù)的標記或者擴展參數(shù)的值的范圍。
  • 擴展GLSL,添加新的功能、內(nèi)置函數(shù)、變量或者數(shù)據(jù)類型。
  • 添加全新的函數(shù)到OpenGL

在第一種情況中,之前被認為錯的東西現(xiàn)在不是錯的了,我們的應用除了使用新添加的合法行為不需要做任何事就好,當然我們需要確保指定的擴展是支持的。同樣,第二種情況中,我們在相關(guān)的函數(shù)上使用新的標記就好,前提是我們已經(jīng)有了這些標記的值。這些標記的值都在擴展的規(guī)范中,所以如果我們的系統(tǒng)頭文件中沒有這些值我們可以去規(guī)范中查找。

為了啟用GLSL中的擴展,我們必須首先在著色器的開頭包含一行代碼,這行代碼指示編譯器我們需要這些特性。舉個栗子,要在GLSL中啟用特定的GL_ABC_foobar_feature擴展,在著色器的開頭包含以下代碼:

#extension GL_ABC_foobar_feature : enable

這句代碼指示編譯器我們意欲使用這個擴展。如果編譯器知曉這個擴展,它會編譯這個著色器,即使底層的硬件并不支持這個特性。如果是這種情況,編譯器如果發(fā)現(xiàn)這個擴展實際上正在被使用應該發(fā)出警告。通常情況下,GLSL的擴展會添加一個預處理器標記來表明它的存在。比如,GL_ABC_foobar_feature會隱式包含

#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實現(xiàn)是否支持特定擴展的功能來有條件地編譯或者執(zhí)行這一功能。如果我們的著色器確實需要某一擴展的支持并且沒有這一擴展就完全無法工作,我們可以將剛才的代碼替換為如下更具斷言性質(zhì)的代碼:

#extension GL_ABC_foobar_feature : require

如果OpenGL實現(xiàn)不支持GL_ABC_foobar_feature擴展,它會編譯失敗并報告包含#extension命令的這行代碼的錯誤。實際上,GLSL擴展是可選的特性,我們在應用中必須預先指示編譯器我們想使用哪個擴展。

在實踐中,很多OpenGL實現(xiàn)缺省啟用很多擴展中的功能,并且不需要在著色器中包含這些命令。不過,如果我們依賴于這一行為,我們的應用很可能無法在其他OpenGL驅(qū)動上工作??紤]到這一風險,我們總是應該顯示啟用計劃使用的擴展。

下面讓我們看看為OpenGL引入新函數(shù)的擴展。在大多數(shù)平臺上,我們無法直接訪問OpenGL驅(qū)動,擴展的函數(shù)也不會變魔術(shù)一般地在我們的應用可以調(diào)用。相反,我們必須向OpenGL驅(qū)動請求一個函數(shù)指針(function pointer),它代表了我們想要調(diào)用的函數(shù)。函數(shù)指針通常聲明為兩部分:第一部分為函數(shù)指針類型的定義,第二部分為函數(shù)指針變量本身。例如以下示例代碼:

typedef void
(APIENTRYP PFNGLDRAWTRANSFORMFEEDBACKPROC) (GLenum mode, GLuint id);

PFNGLDRAWTRANSFORMFEEDBACKPROC glDrawTransformFeedback = NULL;  

這段代碼聲明了一個類型PFNGLDRAWTRANSFORMFEEDBACKPROC,它是一個函數(shù)指針,接收GLenumGLuint參數(shù)。然后這段代碼聲明了類型PFNDRAWTRANSFORMFEEDBAKPROC的一個實例glDrawTransformFeedback。實際上在很多平臺上,glDrawTransformFeedback()的函數(shù)聲明就是像這樣。這看起來有點復雜,幸虧下面的三個頭文件包含所有注冊的OpenGL擴展的函數(shù)的原型、函數(shù)指針類型以及標志值:

#include <glext.h>
#include <glxext.h>
#include <wglext.h>

這些文件可以在OpenGL擴展注冊處站點找到。glext.h頭文件包含標準OpenGL擴展和很多零售商擴展,wglext.h頭文件包含一系列Windows特定的擴展,glxext.h頭文件包含X窗體系統(tǒng)的擴展(X是Linux和很多其他UNIX變種使用的窗體系統(tǒng))。

查詢擴展函數(shù)的地址的方法實際上是平臺相關(guān)的。本書的應用框架將這些雜事包裝到一個便利函數(shù)中,它聲明在<sb7ext.h>頭文件中。這個函數(shù)sb7GetProcAddress()的原型為:

void* sb7GetProcAddress(const char* funcname);

其中,funcname使我們想用的擴展函數(shù)的名字。如果這個擴展函數(shù)被支持,那返回就是這個函數(shù)的地址,否則就是NULL。就算OpenGL返回我們要使用的擴展函數(shù)的可用的函數(shù)指針,但那不代表這個擴展是存在的。有時候同樣的函數(shù)存在于多個擴展中,并且有時候零售商搭載的驅(qū)動只是部分實現(xiàn)了特定的擴展。我們總是應該使用官方策略或者sb7IsExtensionSupported()函數(shù)來檢測支持的擴展。

總結(jié)

在這一章,我們對OpenGL的圖形管線做了一個快速的了解。我們對每個主要的階段做了一個很粗淺的介紹,并且創(chuàng)建了一個用到每一個階段的程式,雖然它沒做啥有意義的事。我們掩蓋甚至忽略了一些OpenGL中有用的特性,主要是為了在比較短的篇幅內(nèi)讓我們可以從零到可以渲染些什么東西。我們還看到OpenGL的管線和功能如何使用擴展進行增強,其中不乏本書之后的示例會用到的。在下面的幾個章節(jié)中,我們會學到更多計算機圖形和OpenGL的基礎(chǔ),然后我們會再次回顧管線,對本章中的主題做更深入的了解,并且對本章跳過的一些OpenGL可以做的事情做了解。

Copyright

本書原著為《OpenGL Super Bible》,版權(quán)歸原作者所有,本譯文僅為愛好者學習交流所用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容