OpenGL 4.0的Tessellation Shader(細分曲面着色器)


細分曲面着色器(Tessellation Shader)處於頂點着色器階段的下一個階段,我們可以看以下鏈接的OpenGL渲染流水線的圖:https://www.opengl.org/wiki/Rendering_Pipeline_Overview(可能需要翻牆)。它是由ATI在2001年率先設計出來的。

 

細分曲面着色器


直到這個階段,對於操作幾何圖元而言,只有頂點着色器對我們可用。盡管使用頂點着色器可以使用不少圖形技術,不過頂點着色器也確實存在一些限制。一個就是它們在執行過程中無法創建額外的幾何圖形。它們僅僅更新與它們當前所處理的頂點相關的數據。而且,它們甚至無法訪問在當前圖元中其它頂點的數據。

為了解決那些問題,OpenGL流水線含有幾個其它着色器階段來打破這些限制。我們這里所要介紹的細分曲面着色器可以使用一個新的幾何圖元類型,稱為patch(斑點、碎片),來生成一個三角形網格。

細分曲面着色器在OpenGL流水線中增添了兩個着色器階段來生成一個幾何圖元的網格。比起在使用頂點着色器時不得不指定所有線與三角形來形成自己的模型,在使用細分曲面時,一開始指定一個patch,它是一列排好序的頂點。當一個patch被渲染時,細分曲面控制着色器先執行,對patch頂點進行操作,並指定從patch中應該生成多少幾何圖形。細分曲面控制着色器是可選的,我們后面會看到,如果不用它的話需要使用哪些條件。在細分曲面控制着色器完成之后,第二個着色器——細分曲面計算着色器使用細分曲面坐標來放置所生成的頂點,並且將它們發送到光柵化器,或發送到幾何着色器做進一步處理。


細分曲面Patch


細分曲面過程並不對OpenGL典型的幾何圖元(點、線和三角形)進行操作,而是使用一個新的圖元(在OpenGL 4.0版本中新增的),稱為patch。patch由流水線中所有活動的着色階段處理。相比起來,其它圖元類型僅僅被頂點、片段和幾何着色器處理,而旁通細分曲面階段。實際上,如果有任一細分曲面着色器是活躍的,那么傳遞任何其它幾何類型會產生一個GL_INVALID_OPERATION錯誤。相反地,如果企圖渲染一個patch而沒有任何細分曲面着色器(明確地說是一個細分曲面計算着色器;我們會看到細分曲面控制着色器是可選的),那么將也會得到一個GL_INVALID_OPERATION錯誤。

patch僅僅是傳入到OpenGL的一列頂點列表,該列表在處理期間保存它們的次序。當用細分曲面與patch進行渲染時,使用像glDrawArrays()這樣的渲染命令,並指定從綁定的頂點緩存對象(VBO)將被讀出的頂點的總數,然后為該繪制調用進行處理。當用其它的OpenGL圖元進行渲染時,OpenGL基於在繪制調用中所指定的圖元類型而隱式地知道要使用多少頂點,比如使用三個頂點來繪制一個三角形。然后,當使用一個patch時,需要告訴OpenGL頂點數組中要使用多少個頂點來組成一個patch,而這可以通過使用glPatchParameteri()進行指定。由同一個繪制調用所處理的patch,它們的尺寸(即每個patch的頂點個數)將是相同的。

void glPatchParameteri(GLenum pname, GLint value);
/**
 * 使用value來指定一個patch中的頂點個數。pname必須設置為GL_PATCH_VERTICES。
 * 如果value小於零或大於GL_MAX_PATCH_VERTICES,將會產一個GL_INVALID_ENUM的錯誤。
 * 一個patch的默認頂點個數是三。如果一個patch的頂點個數小於參數value值,那么該patch將被忽略,從而不會有幾何圖形產生。
*/

要指定一個patch,使用類型GL_PATCHES輸入到任一OpenGL繪制命令。以下代碼描述了發射兩個patch,每個patch含有四個頂點,然后通過glDrawArrays繪制命令進行渲染。

GLfloat vertices[][2] = {
    {-0.75f, -0.25f}, {-0.25f, -0.25f}, {-0.25f, 0.25f}, {-0.75f, 0.25f},
    {0.25f, -0.25f}, {0.75f, -0.25f}, {0.75f, -0.25f}, {0.75f, 0.25f}, {0.25f, 0.25f}
};

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(vPos, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
glPatchParameteri(GL_PATCH_VERTICES, 4);
glDrawArrays(GL_PATCHES, 0, 8);

每個patch的頂點首先由當前綁定的頂點着色器處理,然后用於初始化數組gl_in,這個變量在細分曲面控制着色器中被隱式地聲明。gl_in中的元素個數與由glPatchParameteri()所指定的patch大小相同。在一個細分曲面着色器內部,變量gl_PatchVerticesIn提供了gl_in中的元素個數(就好比使用sizeof(gl_in) / sizeof(gl_in[0])進行查詢)。


細分曲面控制着色器


一旦應用發射了一個patch,細分曲面控制着色器就會被調用(如果有所綁定的話)並且負責完成以下行動:

⚫︎ 生成細分曲面輸出patch頂點,傳遞到細分曲面計算着色器,以及更新任一每個頂點的,或每個patch的屬性值,若有必要的話。

⚫︎ 指定細分曲面程度因子,控制圖元生成器的操作。這些是特殊的細分曲面控制着色器變量,稱為gl_TessLevelInner和gl_TessLevelOuter,並在細分曲面控制着色器中隱式聲明。

我們將依次討論這些行動的每一個。


生成輸出patch頂點


細分曲面控制器使用由應用所指定的頂點——這些頂點我們稱為輸入patch頂點(作為頂點着色器的輸出)——來生成一組新的頂點,這些新的頂點為輸出patch頂點。它們存放在細分曲面控制着色器的gl_out數組中。細分曲面控制着色器在產出輸出patch頂點時,可以修改傳遞自應用的值(比如頂點屬性),也可以創建或移除來自輸入patch頂點中的頂點。

使用一個layout構造在細分曲面控制着色器中指定輸出patch頂點的個數。下面語句描述了設置輸出patch頂點的個數為16。

layout (vertices = 16) out;

layout指示符中的vertices參數所設置的值做了兩件事情:它設置了輸出patch頂點gl_out的大小;並且指定了細分曲面控制着色器將被執行多少次:對每個輸出patch頂點執行一次。

為了確定正在處理哪個輸出頂點,細分曲面控制着色器可以使用gl_InvocationID變量。該變量最經常被用作為gl_out數組的一個索引。當一個細分曲面控制着色器在執行時,它具有對所有patch頂點數據的訪問,包括輸入頂點和輸出頂點。這可能會導致發射一次着色器調用,該調用需要使用來自另一個着色器調用的數據值,但是那個着色器調用尚未發生。細分曲面控制着色器可以使用GLSL的barrier()函數,該函數使得對於一個輸入patch的所有控制着色器執行並等待所有這些着色器的執行到達那個函數的調用點,從而確保了可能要設置的所有數據值將被計算。

細分曲面控制着色器的一個普遍的習慣用法僅僅是將輸入patch頂點輸出到此着色器的外面。下面的例子描述了帶有四個頂點的一個輸出patch。

#version 410 core

layout (vertices = 4) out;

void main(void)
{
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;

    // 下面設置細分曲面程度
}


細分曲面控制着色器變量


gl_in數組實際上是一個結構體的數組,每個元素被定義為:

in gl_PerVertex {
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];

}gl_in[gl_PatchVerticesIn];

並且對於每個需要向下一個階段傳遞的值(比如,向下傳遞到細分曲面計算着色器),需要進行相應地賦值。如上述代碼片段所述,傳遞了gl_Position變量。

gl_out數組具有相同的結構體成員,不過數組大小與gl_in不同,它是由gl_PatchVerticesOut來指定的。而這個值則是在細分曲面控制器中的out這一layout限定符中設置。此外,以下標量值用於確定正在被着色的圖元和輸出頂點:

gl_InvocationID:當前細分曲面着色器的輸出頂點的調用索引

gl_PrimitiveID:當前輸入patch的圖元索引

gl_PatchVerticesIn:輸入patch中的頂點個數,它作為gl_in數組變量中的元素個數

gl_PatchVerticesOut:輸出patch中的頂點個數,它作為gl_out數組變量中的元素個數

如果我們需要額外的基於每個頂點的屬性值,或為輸入或為輸出,那么這需要在我們的細分曲面控制着色器中將它們聲明為in或out數組。一個輸入數組的大小需要與輸入patch大小相同,或者可以被聲明為缺省大小的,這樣OpenGL將會為其所有值適當地分配空間。類似地,每個頂點的輸出屬性需要與輸出patch中的頂點個數相一致,也可以為輸出屬性聲明為缺省大小的。輸出屬性值將會被傳遞到細分曲面計算着色器,作為其輸入屬性值。

比如:

#version 410 core

layout (vertices = 4) out;

in vec4 vertexCoeffs1[4];    // 我們假定指定一個輸入patch含有4個頂點
in vec4 vertexCoeffs2[];     // 這里使用缺省大小的數組變量,OpenGL將會自動為其分配大小

out vec2 vertexTexCoord1[4];    // 這里需要用上面out的layout (vertices)值一致
out vec2 vertexTexCoord2[];     // 這里使用缺省大小的數組變量,OpenGL將會自動為其分配大小

void main(void)
{
    // Do something here
}

上面給出的是基於逐個頂點的屬性值,它們可以用gl_InvocationID作為索引,不過要注意的是,gl_InvocationID標識的是當前細分曲面着色器的輸出頂點的調用索引。我們可以使用patch限定符來聲明每個patch的輸出變量。每個patch的變量不是以數組方式定義而是以普通單實例變量的方式來定義。當然,我們也可以將它們定義為數組。所有細分曲面控制着色器的調用看到的都是同一個patch變量。比如:

#version 410 core

patch out vec4 data;

layout (vertices = 4) out;

void main(void)
{
    // Do something here

data = vec4(1.0, 0.0, 0.0, 1.0); }

這里,我們定義了一個標識符為data的patch輸出變量。對於每次細分曲面控制着色器的調用,data的值都被寫為vec4(1.0, 0.0, 0.0, 1.0)。因此,任一細分曲面控制着色器可以寫基於每個patch的輸出變量;實際上,所有細分曲面控制着色器的調用一般都將寫到一個基於每個patch的變量。只要它們都寫相同的值,那么一切都是良好的。


控制細分曲面


一個細分曲面控制着色器的另一個功能是指定對輸出patch細分多少。然而我們還沒詳細地討論細分曲面計算着色器,它們控制用於渲染的輸出patch的類型,結果也就是細分曲面所發生的域。OpenGL支持三種細分曲面域:四邊形,三角形,和等值線集合。這些通過細分曲面計算着色器中的inlayout進行指定。

細分曲面的數量通過指定兩組值:內部和外部細分曲面程度來控制的。外部細分曲面的值控制域的周邊是如何划分的,然后存放在一個隱式聲明的名為gl_TessLevelOuter的含有四個元素的數組中。而內部細分曲面程度指定了域的內部如何進行划分,然后存放在一個名為gl_TessLevelInner的含有兩個元素的數組中。所有細分曲面程度因子是浮點值,並且我們將會看到浮點值在細分曲面上以一個比特的效果。最后一點是,盡管隱式聲明的細分曲面程度因子數組的維度是固定的,不過從那些數組所使用的值的個數依賴於細分曲面域的類型。下面來看看這兩個OpenGL內建的細分曲面輸出patch變量的聲明:

patch out float gl_TessLevelOuter[4];
patch out float gl_TessLevelInner[2];

理解外部與內部細分曲面程度如何操作是讓細分曲面做我們想要做的事情的關鍵。每個細分曲面程度因子指定了對一個區域划分多少條“線段”,以及生成多少細分曲面坐標與幾何圖元。這種划分如何完成根據不同域類型而有所不同。我們將依次討論域的每種類型。


四邊形細分曲面


使用四邊形域可能是最直觀的,因此我們先介紹這種類型。當輸入patch形狀為矩形時,這很有用,當我們可能使用二維的樣條曲面時,比如Bézier曲面。四邊形域使用所有內部和外部細分曲面程度來划分單位正方形。比如,如果我們用以下代碼來設置細分曲面程度因子,那么OpenGL將會把四邊形域細分為如下圖所示的樣子。

#version 410 core

layout (vertices = 4) out;

void main(void)
{
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;

    // 下面設置細分曲面程度
    gl_TessLevelInner[0] = 3.0;     // 內部划分3條垂直區域,即內部新增2列頂點
    gl_TessLevelInner[1] = 4.0;     // 內部划分4條水平區域,即內部新增3行頂點
    
    gl_TessLevelOuter[0] = 2.0;     // 左邊2條線段
    gl_TessLevelOuter[1] = 3.0;     // 下邊3條線段
    gl_TessLevelOuter[2] = 4.0;     // 右邊4條線段
    gl_TessLevelOuter[3] = 5.0;     // 上邊5條線段
}

下圖為上述細分曲面控制着色器可能會得到的幾何圖形樣子。

          

 

上圖中,三條黃色線條表示三條垂直區域;四條綠色線條表示四條水平區域。

注意,外部細分曲面程度值對應於圍繞周邊的每條邊的線段個數,而內部細分曲面程度指定了域的內部空間中水平與垂直方向上有多少個“區域”。使用虛線則是將整個域用三角形進行划分。域的三角形划分是依賴於實現的。實心圓點表示細分曲面坐標,每個坐標都會提供給細分曲面計算着色器,作為其輸入。在四邊形域的情況下,細分曲面坐標有兩個坐標值(u, v),兩者值的范圍都在[0, 1]范圍內,並且每個細分曲面坐標將傳遞到細分曲面計算着色器中,作為它的一次調用。


等值線細分曲面


類似於四邊形域,等值線域也生成(u, v)對作為給細分曲面計算着色器的細分曲面坐標。然而,等值線僅僅使用外部細分曲面程度的兩個元素值來判定划分量(這里沒有用到內部細分曲面程度)。

下面是設置等值線域的細分曲面程度:

gl_TessLevelOuter[0] = 6;      // 6條等值線
gl_TessLevelOuter[1] = 8;      // 每條等值線被划分為8條線段

下圖為可能的結果圖形:

 

 

旁通細分曲面控制着色器


正如我們所提到過的,細分曲面着色器往往只是一個直通着色器,將數據從輸入直接到輸出。在這種情況下,我們實際上可以使一個細分曲面着色器進行旁通,僅僅使用主機端OpenGL API來設置細分曲面程度因子。使用glPatchParameterfv()函數來設置內部與外部細分曲面程度。

void glPatchParameterfv(GLenum pname, const GLfloat *values);

/**
 * 當沒有細分曲面控制着色器時,設置內部與外部細分曲面程度。
 * 參數pname要么是GL_PATCH_DEFAULT_OUTER_LEVEL,要么是GL_PATCH_DEFAULT_INNER_LEVEL。
 * 當pname是GL_PATCH_DEFAULT_OUTER_LEVEL時,參數values必須是含有四個單精度浮點值的數組,指定四個外部細分曲面程度。
 * 類似地,當pname是GL_PATCH_DEFAULT_INNER_LEVEL時,values必須是含有兩個單精度浮點值的數組,指定兩個內部細分曲面程度。
*/


細分曲面圖元生成


圖元生成是一個固定功能階段,負責從輸入patch來創建一組新的圖元。此階段僅當一個細分曲面計算着色器在當前程序或程序流水線中活動時才會執行。圖元生成受以下因素影響:

 細分曲面程度

 被細分的頂點的空間划分,這個由細分曲面計算着色器階段進行定義。它可以是equal_spacing、fractional_even_spacing或fractional_odd_spacing。

 由后續細分曲面着色器所定義的圖元類型,即triangles、quads或isolines。細分曲面計算着色器也可以迫使細分曲面的生成作為一系列的點,而不是三角形或線。這個可以通過point_mode來指定。

 由后續細分曲面計算着色器所定義的圖元生成次序,比如cw(順時針)或ccw(逆時針)。


抽象patch


注意,圖元生成不受細分曲面控制着色器中用戶定義的輸出影響(或當沒有細分曲面控制着色器時,也不受頂點着色器的影響),不受細分曲面控制着色器的輸出patch大小影響,也不受任一每個patch的細分曲面控制着色器輸出的影響,而只受細分曲面程度的影響。細分曲面階段的圖元生成部分完全對實際的頂點坐標與其它patch數據是不可見的。

圖元生成系統的目的是確定要生成多少個頂點,用哪個次序來生成它們,以及用哪種圖元來構造它們。實際為這些頂點的每個頂點的數據,諸如位置、顏色等等,是通過細分曲面計算着色器來生成的,基於圖元生成器所提供的信息。

因為這種對分,圖元生成器對可以被認為是一個“抽象patch”的東東進行操作。它並不從細分曲面控制器的輸出來看patch;它僅考慮一個抽象四邊形、三角形或等值線塊的細分曲面。

依賴於抽象patch類型,圖元生成器計算不同數量的細分曲面程度並應用不同的細分曲面算法。每個所生成的頂點在一個抽象patch內具有一個規格化的位置(即,坐標范圍在[0, 1]之內)。這個位置具有兩個或三個分量,依賴於patch的類型。這些坐標通過內建的

in vec3 gl_TessCoord;

輸入提供給細分曲面計算着色器。


細分曲面計算着色器


OpenGL細分曲面流水線的最后一個階段就是細分曲面計算着色器執行。綁定的細分曲面計算着色器對圖元生成器發射的每個細分曲面坐標逐個執行,並負責確定從細分曲面坐標所得到的頂點的位置。我們將看到,細分曲面計算着色器看上去與頂點着色器類似,將頂點變換到屏幕位置(除非細分曲面着色器的數據將被進一步由幾何着色器來處理)。

配置一個細分曲面計算着色器的第一步是配置圖元生成器,這通過使用一個layout指示符來完成。其參數指定了細分曲面域與后續所生成的圖元的類型;實體圖元的面部朝向(用於做面剔除);以及在圖元生成期間如何應用細分曲面程度。


指定圖元生成域


我們現在將描述細分曲面計算着色器的in這個layout的參數。首先,我們先談論指定細分曲面域。我們之前已經提及過,一共有三種類型的域來生成細分曲面坐標:

 quads——單位正方形中的一個矩形域;域坐標:帶有范圍在[0, 1]內的u, v值的一個個坐標對(u, v)。

 triangles——使用重心坐標的一個三角形域;域坐標:帶有范圍在[0, 1]內的a、b、c三個值的坐標(a, b, c),這里a+b+c=1。

 isolines——跨單位正方形的一組線;域坐標:u值范圍在[0, 1],v值范圍在[0, 1)范圍的(uv)坐標對。


指定生成圖元的面部朝向


與OpenGL中任何填充的圖元一樣,所發射的頂點的次序決定了圖元的臉部朝向。由於我們在這種情況下不直接發射頂點,而只是讓圖元生成器為我們來做,不過我們需要告訴圖元生成器我們圖元的右手旋轉方向。在layout指示符中,指定cw為順時針旋轉,ccw為逆時針。


指定細分曲面坐標的空間


此外,我們可以控制外部細分曲面程度的浮點值如何用在確定周邊的細分曲面坐標生成上。(內部細分曲面程度受這些選項影響。)

 equal_spacing——細分曲面程度被裁減到[1, max]范圍內,然后取整到下一個最大整數值。

 fractional_even_spacing——值被裁減到[2, max]范圍內,然后取整到下一個最大偶整數值n。邊然后被划分為n-2條等長部分,以及兩個剩余部分,每個在一端,剩余部分長度可能比其它長度要短。

 fractional_odd_spacing——值被裁減到[1, max-1]范圍內,然后取整到下一個最大奇整數值n。邊然后被划分為n-2條等長部分,以及兩個剩余部分,每個在一端,剩余部分長度可能比其它長度要短。


額外的細分曲面計算着色器layout選項


最后,如果我們想輸出點,而不是等值線或填充區域的話,我們可以應用point_mode選項。該選項將為每個由細分曲面計算着色器所處理的頂點渲染一單個點。

在layout指示符內的選項的次序不重要。下面例子描述的是一個生成三角形域的圖元,使用相等空間,逆時針方向的三角形,但只渲染點,而不是互聯的圖元。

layout (triangles, equal_spacing, ccw, point_mode) in;


指定一個頂點的位置


從細分曲面控制着色器輸出的頂點(即,在gl_out數組中的gl_Position的值)在計算着色器中的gl_in變量中可用。當它們與細分曲面坐標相結合時,可以用於生成輸出頂點的位置。

細分曲面坐標在變量gl_TessCoord中提供給計算着色器。在下面例子中我們使用相等空間划分的四邊形來渲染一個簡單的patch。在這個例子中,細分曲面坐標用於對表面進行上色,然后此例子也描述了如何計算頂點坐標。我們這里要注意的是,gl_in中的gl_Position相當於原始的patch每個頂點的坐標,而gl_TessCoord則是經過細分曲面之后的新增細分曲面頂點,這些頂點的坐標值是被規格化在[0, 1]范圍內的。我們通過以原patch的頂點坐標與當前處理的細分曲面頂點坐標做相應的插值計算來獲得此細分曲面頂點最后輸出的坐標值。我們在細分曲面計算着色器中可以訪問所有gl_in數組的元素,即每次調用可以訪問當前patch所有輸入的頂點坐標。

#version 410 core

layout (quads, equal_spacing, ccw) in;

out vec4 color;

void main(void)
{
    float u = gl_TessCoord.x;
    float omu = 1 - u;    // omu為1減去"u"
    float v = gl_TessCoord.y;
    float omv = 1 - v;    // omv為1減去"v"

    color = gl_TessCoord; // color最后給到片段着色器時,值為(gl_TessCoord.x, gl_TessCoord.y, 0.0, 1.0)

    gl_Position = omu * omv * gl_in[0].gl_Position +
                  u * omv * gl_in[1].gl_Position +
                  u * v * gl_in[2].gl_Position +
                  omu * v * gl_in[3].gl_Position;
}


細分曲面計算着色器變量


與細分曲面控制着色器類似,細分曲面計算着色器也有gl_in數組,它是一個結構體數組,每個元素如下定義:

in gl_PerVertex {
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[gl_PatchVerticesIn];

輸出頂點的數據被存放在一個接口塊中,如下定義:

out gl_PerVertex {
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
};


參考資料:

《OpenGL Programming Guide Eighth Edition -- The Official Guide tyo Learning OpenGL, Version 4.3》

https://www.opengl.org/wiki/Tessellation


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM