OpenGL 圖元處理

圖元處理(Primitive Processing)

  • 如何在場景中使用曲面細(xì)分來添加幾何細(xì)節(jié)
  • 如何使用幾何著色器處理整個(gè)圖元并且立即創(chuàng)建新的幾何體

對(duì)于每一個(gè)發(fā)送到OpenGL的頂點(diǎn)豁陆,頂點(diǎn)著色器都會(huì)運(yùn)行一次并輸出一個(gè)數(shù)據(jù)集合故爵。接來來的幾個(gè)階段和頂點(diǎn)著色器有一定的相似之處蓬豁,實(shí)際上它們被稱為圖元處理階段遍烦。首先,曲面細(xì)分著色器處理圖形塊(patches)瘩燥,接下來幾何著色器處理整個(gè)圖元(點(diǎn)家制、線正林、面),對(duì)于每個(gè)圖元它都會(huì)運(yùn)行一次颤殴。

1 曲面細(xì)分(Tessellation)

曲面細(xì)分指的是將被稱為圖像塊(patch)的幾何體在其被渲染之前分割成更小的圖元觅廓。曲面細(xì)分的應(yīng)用場景很多,最常見的是用于為低保真度的網(wǎng)格添加幾何細(xì)節(jié)涵但。在OpenGL中杈绸,曲面細(xì)分過程分為3個(gè)階段:曲面細(xì)分控制著色器(TCS),固定曲面細(xì)分引擎矮瘟,曲面細(xì)分評(píng)價(jià)著色器(TES)瞳脓。當(dāng)曲面細(xì)分引擎被激活時(shí),頂點(diǎn)著色器如一般狀態(tài)下一樣一次處理頂點(diǎn)數(shù)據(jù)澈侠,然后將數(shù)據(jù)分組發(fā)送給曲面細(xì)分控制著色器劫侧。

曲面細(xì)分控制著色器操作的單個(gè)數(shù)據(jù)組最多支持至少32個(gè)頂點(diǎn)(具體個(gè)數(shù)使參數(shù)GL_ MAX_ PATCH_ VERTICES查詢),他們合起來被稱為一個(gè)圖形塊(patch)哨啃。其曲面細(xì)分大階段內(nèi)烧栋,輸入頂點(diǎn)通常被稱為控制點(diǎn)(control points)。曲面細(xì)分控制著色器有下述3個(gè)職責(zé)棘催。

  • 確定每個(gè)圖形塊的內(nèi)部和外部曲面細(xì)分因子
  • 確定每個(gè)輸出控制點(diǎn)的位置和其他屬性
  • 確定每個(gè)圖形塊的用戶定義漸變量

曲面細(xì)分因子被發(fā)送至曲面細(xì)分引擎劲弦,用于確定OpenGL細(xì)分圖元的方式。在曲面細(xì)分引擎運(yùn)行后醇坝,曲面細(xì)分控制著色器確定的控制點(diǎn)集合將會(huì)被發(fā)送給曲面細(xì)分求值著色器邑跪。一些變量如果對(duì)于所有的輸出頂點(diǎn)都是相同的(例如圖形塊的顏色),它們將被標(biāo)記為按圖形塊計(jì)算(be marked as per patch)呼猪。當(dāng)曲面細(xì)分大階段運(yùn)行后画畅,它會(huì)生成一個(gè)新的頂點(diǎn)集合,它們按照曲面細(xì)分因子和曲面細(xì)分模式定義的方式分布于圖形塊中宋距,曲面細(xì)分模式可以通過在曲面求值著色器中以布局聲明的方式定義轴踱。曲面細(xì)分求值著色器中的唯一輸入變量幾何為OpenGL生成的坐標(biāo)集合,它們保存了新的頂點(diǎn)在圖形塊中的位置信息谚赎。當(dāng)生成的圖元類型是三角形時(shí)淫僻,這些坐標(biāo)為重力坐標(biāo)诱篷。當(dāng)曲面細(xì)分引擎生成線或者三角形時(shí),這些坐標(biāo)僅僅是標(biāo)準(zhǔn)化的值雳灵,表示頂點(diǎn)的相對(duì)位置棕所,它們被保存在變量gl_TessCoord中。該機(jī)制概括如下圖悯辙。

1.1 曲面細(xì)分圖元模式(Tessellation Primitive Modes)

曲面細(xì)分模式?jīng)Q定了OpenGL如何將圖形塊分解為圖元琳省。該模式通過在曲面細(xì)分計(jì)算著色器中使用聲明關(guān)鍵字quads、triangles或者isolines設(shè)置躲撰。圖元模式不僅決定了曲面細(xì)分階段生成的圖元類型针贬,同時(shí)還影響了再曲面細(xì)分計(jì)算著色器中關(guān)鍵字gl_TessCoord的含義。

1.1.1 Quads模式

在使用Quads模式進(jìn)行曲面細(xì)分時(shí)拢蛋,曲面細(xì)分引擎會(huì)生成一個(gè)四邊形义起,并將其分解為三角形集合裁替。曲面細(xì)分控制著色器需要設(shè)置一個(gè)包含兩個(gè)元素的gl_TessLevelInner[]數(shù)組,它控制了四邊形內(nèi)部的曲面細(xì)分等級(jí)。數(shù)組中第一個(gè)元素?fù)?jù)定了水平方向上的細(xì)分等級(jí)蝠咆,而第二個(gè)元素決定了垂直方向上細(xì)分等級(jí)女淑。同樣的拘荡,曲面細(xì)分控制著色器還需要設(shè)置一個(gè)包含4個(gè)元素的gl_TessLevelOuter[]數(shù)組勿锅,它們分別控制外邊的細(xì)分等級(jí)。細(xì)分規(guī)律示意如下皆警。

當(dāng)四邊形被曲面細(xì)分后拦宣,曲面細(xì)分引擎會(huì)在四邊形標(biāo)準(zhǔn)化后的兩維空間內(nèi)生成一系列的頂點(diǎn)。每個(gè)頂點(diǎn)的標(biāo)注化二維坐標(biāo)以二維向量(也就是說只有其x和y分量是有效的)的方式保存在變量gl_TessCorrd內(nèi)信姓,該變量會(huì)被傳遞到曲面細(xì)分計(jì)算著色器內(nèi)鸵隧,結(jié)合由曲面細(xì)分控制著色器確定的控制點(diǎn)坐標(biāo)就能計(jì)算出頂點(diǎn)坐標(biāo)。一個(gè)該模式曲面細(xì)分實(shí)例如下意推,在圖中可以看見在外部邊緣橫軸和豎軸方向分別被平分為5和3份豆瘫,內(nèi)部同樣的分別被平分為9和7份。源代碼請(qǐng)見Chapter 8/8.9-tessmodes菊值。

在上述實(shí)例中外驱,內(nèi)部的細(xì)分因子分別被設(shè)置為9和7,外部的細(xì)分因子分別被設(shè)置為3和5腻窒。曲面細(xì)分控制著色器核心代碼如下昵宇。

#version 410 core
layout (vertices = 4) out;

void main() {
    if (gl_InvocationID == 0) {
        gl_TessLevelInner[0] = 9.0;
        gl_TessLevelInner[1] = 7.0;
        gl_TessLevelOuter[0] = 3.0;
        gl_TessLevelOuter[1] = 5.0;
        gl_TessLevelOuter[2] = 3.0;
        gl_TessLevelOuter[3] = 5.0;
    }
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

核心的曲面細(xì)分計(jì)算著色器代碼如下,注意其中曲面細(xì)分模式的聲明方式儿子。

#version 410 core
layout (quads) in;

void main() {
    // 此處直接提供在quads細(xì)分模式下頂點(diǎn)坐標(biāo)計(jì)算瓦哎,并不推導(dǎo)公式
    // Interpolate along bottom edge using x component of the tessellation coordinate
    vec4 p1 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
    // Interpolate along top edge using x component of the tessellation coordinate
    vec4 p2 = mix(gl_in[2].gl_Position, gl_in[3].gl_Position, gl_TessCoord.x);
    // Now interpolate those two results using the y component of tessellation coordinate
    gl_Position = mix(p1, p2, gl_TessCoord.y);
}
1.1.2 Triangles模式

當(dāng)曲面細(xì)分魔法被設(shè)置為三角形時(shí),曲面細(xì)分引擎首先生成一個(gè)三角形,再將其分解為更小的三角形蒋譬。曲面細(xì)分控制著色器中需設(shè)置一個(gè)包含1個(gè)元素的數(shù)組gl_TessLevelInner[]割岛,它決定了內(nèi)部三角形的細(xì)分等級(jí)。包含3個(gè)元素的數(shù)組gl_TessLevelOuter[]分別決定了三條邊的細(xì)分等級(jí)羡铲,其細(xì)分規(guī)律可用下圖表示蜂桶。

曲面細(xì)分引擎將為每個(gè)頂點(diǎn)生成3維重心坐標(biāo),結(jié)合三角形的三個(gè)頂點(diǎn)坐標(biāo)可以在三角形內(nèi)部進(jìn)行線性插值計(jì)算出所有點(diǎn)的坐標(biāo)也切。三角形細(xì)分模式的實(shí)例如下,可以看見圖中外邊都被分為8等份腰湾,內(nèi)邊被等分為5等份雷恃。源代碼請(qǐng)見Chapter 8/8.9-tessmodes

其曲面細(xì)分控制著色器代碼如下费坊。

#version 410 core
layout (vertices = 3) out;

void main() {
    if (gl_InvocationID == 0) {
        gl_TessLevelInner[0] = 5.0;
        gl_TessLevelOuter[0] = 8.0;
        gl_TessLevelOuter[1] = 8.0;
        gl_TessLevelOuter[2] = 8.0;
    }
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

其曲面細(xì)分計(jì)算著色器源代碼如下倒槐。

#version 410 core
layout (triangles) in;

void main() {
    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);
}
1.1.3 Isolines模式

曲面細(xì)分模式設(shè)置為Isoline模式時(shí),曲面細(xì)分引擎不再生成三角形附井,而是沿著y軸生成等y值的水平線圖元讨越,同時(shí)每條線沿著水平方向再被分解為多個(gè)等長片段。數(shù)組gl_TessLevelOuter[]中的兩個(gè)元素分別表示了線的條數(shù)和每條線上的片段數(shù)永毅,此時(shí)數(shù)組gl_TessLevelInner[]中的元素沒有任何意義把跨。其分解原理如下所示。

使用Isolines細(xì)分的實(shí)例如下沼死,注意盡管下圖中每條線段看上去是一個(gè)整體着逐,但是實(shí)際上它們都是由5個(gè)小線段組成,源代碼請(qǐng)見Chapter 8/8.9-tessmodes意蛀。

其曲面細(xì)分控制著色器代碼如下耸别。

#version 410 core
layout (vertices = 4) out;

void main() {
    if (gl_InvocationID == 0) {
        gl_TessLevelOuter[0] = 5.0;
        gl_TessLevelOuter[1] = 5.0;
    }
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

其曲面細(xì)分計(jì)算著色器代碼如下。

#version 410 core
layout (isolines) in;

void main() {
    // Interpolate along bottom edge using x component of the tessellation coordinate
    vec4 p1 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
    // Interpolate along top edge using x component of the tessellation coordinate
    vec4 p2 = mix(gl_in[2].gl_Position, gl_in[3].gl_Position, gl_TessCoord.x);
    // Now interplate those two results using the y component of tessellation coordinate
    gl_Position = mix(p1, p2, gl_TessCoord.y);
}

為了更好的區(qū)分其中的每個(gè)線段县钥,采用如下曲面細(xì)分計(jì)算著色器代碼對(duì)線的控制點(diǎn)鏡像旋轉(zhuǎn)處理秀姐,得到以下結(jié)果。

其對(duì)應(yīng)的曲面細(xì)分計(jì)算著色器代碼如下若贮。

void main() {
    float r = (gl_TessCoord.y + gl_TessCoord.x / gl_TessLevelOuter[0]);
    float t = gl_TessCoord.x * 2.0 * 3.14159;
    gl_Position = vec4(sin(t)*r, cos(t)*r, 0.5, 1.0);
} 
1.1.4 點(diǎn)模式

使用關(guān)鍵字layout聲明point_mode可以啟用點(diǎn)細(xì)分模式省有,此時(shí)曲面細(xì)分模塊會(huì)生成點(diǎn)圖元。該模式是對(duì)quads兜看、triangles或者isolines模式的進(jìn)一步定義锥咸,因此在使用點(diǎn)模式時(shí)還需要指定上述3個(gè)模式中的一個(gè),它們控制了圖元內(nèi)部和外部的線性插值细移。具體的細(xì)分因子和細(xì)分點(diǎn)坐標(biāo)和quads搏予、triangles或者isolines模式中描述的一致。

1.2 曲面細(xì)分模式(Tessellation Subdivision Modes)

曲面細(xì)分引擎工作的方式是生成一個(gè)四邊形或者三角形圖元弧轧,然后再根據(jù)控制著色器中定義的內(nèi)部和外部細(xì)分因子將其內(nèi)外邊分為多個(gè)片段雪侥。再將生成的頂點(diǎn)按照點(diǎn)碗殷、線和三角形分組以備進(jìn)一步處理。在該過程中OpenGL提供了一些關(guān)鍵字用于控制細(xì)分圖元邊的方式速缨。

默認(rèn)情況下锌妻,曲面細(xì)分引擎會(huì)將每條邊分為相等的多個(gè)部分。默認(rèn)的關(guān)鍵字為equal_spacing旬牲,盡管它是默認(rèn)模式仿粹,仍能在曲面細(xì)分計(jì)算著色器中通過如下方式聲明。layout (equal_spacing) in原茅。

等分模式是最簡單的吭历,它總會(huì)咦尋找到等于或者大于設(shè)置值的一個(gè)最近的整數(shù),但是該方式也有缺陷擂橘,在兩個(gè)相鄰的細(xì)分等級(jí)直接圖形的表現(xiàn)不會(huì)存在過度晌区,而是突變的。為了處理這個(gè)問題通贞,OpenGL還提供了另外兩個(gè)模式fractional_even_spacing和fractional_odd_spacing朗若。

在這兩種模式下,曲面細(xì)分著色器會(huì)分別尋找當(dāng)前設(shè)置細(xì)分因子SetFactor下最接近的偶數(shù)RealFactorEven和奇數(shù)RealFactorOdd昌罩,在此基礎(chǔ)上將剩余部分FactorRemain分為兩等份FractionFactor哭懈。兩種模式分別以RealFactorEven+2 FractionFactor和FactorRemain +2FractionFactor的模式細(xì)分對(duì)應(yīng)的邊。三種不同的細(xì)分模式示意圖如下峡迷。從左到右依次為equal_spacing 银伟、fractional_even_spacing和fractional_odd_spacing

可以通過使用uniform變量或者直接在著色器內(nèi)部均勻的對(duì)曲面細(xì)分等級(jí)做出變化绘搞,從而使得對(duì)應(yīng)的細(xì)分線段長度也跟著動(dòng)態(tài)的均勻變化彤避。奇數(shù)模式和偶數(shù)模式的選擇取決于它們?cè)诔绦蛑械谋憩F(xiàn),并沒有絕對(duì)的最優(yōu)選擇夯辖。但是除非特殊需求琉预,采用漸變模式的視覺效果都會(huì)比突變模式更好。

1.2.1 控制旋轉(zhuǎn)順序

通常蒿褂,圖元的生成的旋轉(zhuǎn)模式取決于程序提供給OpenGL的頂點(diǎn)組成順序圆米。但是,當(dāng)曲面細(xì)分被激活后啄栓,為了控制生成圖元生成時(shí)的旋轉(zhuǎn)順序娄帖,需要手動(dòng)指定頂點(diǎn)的生成順序?yàn)轫槙r(shí)針或者逆時(shí)針。在曲面細(xì)分計(jì)算引擎中分別通過如下關(guān)鍵字生成指定順時(shí)針和逆時(shí)針模式layout (cw) in;和layout (ccw) in;昙楚。

默認(rèn)模式下近速,OpenGL采用的是逆時(shí)針模式,如果該模式是希望的圖元生成旋轉(zhuǎn)順序,那么可以不再在著色器中聲明削葱。另外奖亚,該定義只對(duì)三角形的圖元模式有效,如果是線或者點(diǎn)模式析砸,該聲明無任何意義昔字,將會(huì)直接被OpenGL忽略。

1.3 曲面細(xì)分著色器中的數(shù)據(jù)傳遞(Passing Data between Tessellation Shaders)

在曲面細(xì)分控制著色器運(yùn)行之前首繁,每個(gè)頂點(diǎn)代表了一個(gè)控制點(diǎn)作郭,都會(huì)獨(dú)立運(yùn)行一次頂點(diǎn)著色器生成新的輸出頂點(diǎn)。生成的頂點(diǎn)將會(huì)被分組傳遞到曲面細(xì)分控制著色器中蛮瞄。曲面細(xì)分控制著色器會(huì)處理這些控制點(diǎn)組所坯,并生成新的控制點(diǎn)組,需要注意的是改著色器可以輸入和輸出的控制點(diǎn)組成員個(gè)數(shù)可以不同挂捅。盡管在該著色器中看上去能夠獲取到所有的控制點(diǎn),但是它的運(yùn)行次數(shù)和輸出控制點(diǎn)個(gè)數(shù)相同堂湖。輸入的控制點(diǎn)數(shù)組成員個(gè)數(shù)有以下函數(shù)在應(yīng)用中設(shè)置闲先。glPatchParameteri(GL_PATCH_VERTICES, n)

輸入頂點(diǎn)數(shù)據(jù)被包裝在內(nèi)嵌變量gl_in[]數(shù)組中无蜂,該數(shù)組中的每一個(gè)元素都是gl_PerVertex類型結(jié)構(gòu)體伺糠,可以從中獲取到位置信息,這些頂點(diǎn)數(shù)據(jù)都由頂點(diǎn)著色器設(shè)置斥季。另外训桶,頂點(diǎn)著色器中的所有輸出都會(huì)被包裝為數(shù)組傳遞到曲面細(xì)分控制著色器中。如在頂點(diǎn)著色器中聲明了結(jié)構(gòu)體让虐,那么在曲面細(xì)分著色器中的聲明必須為結(jié)構(gòu)體數(shù)組廉嚼。

out VS_OUT {
  vec4 foo;
} vs_out;
頂點(diǎn)著色器總的上述聲明轉(zhuǎn)換到曲面細(xì)分控制著色器中聲明方式如下
in VS_OUT {
  vec4 foo;
} tcs_in[];

曲面細(xì)分著色器的輸出同樣也是一個(gè)數(shù)組如筛,其數(shù)組成員個(gè)數(shù)由其著色器代碼中的布局設(shè)置定義。通常的做法是直接將輸入頂點(diǎn)數(shù)據(jù)不做修改直接傳遞到下一個(gè)著色器午绳。但是需要注意的是,該著色器中的最大輸出數(shù)組成員個(gè)數(shù)是有限的映之,可以通過常量GL_MAX_PATCH_VERTICES查詢拦焚。

同樣的,曲面細(xì)分計(jì)算著色器的輸入也同樣是一個(gè)數(shù)組杠输,每計(jì)算一個(gè)頂點(diǎn)數(shù)據(jù)赎败,它都會(huì)運(yùn)行一次,但是每次運(yùn)算它都能獲取到所有的數(shù)據(jù)蠢甲。

除了以數(shù)組的方式傳遞數(shù)據(jù)外僵刮,對(duì)于在整批數(shù)據(jù)中的常量,它們可以直接在曲面細(xì)分控制-計(jì)算著色器之間傳遞,只需要在兩個(gè)著色器中對(duì)應(yīng)的變量聲明部分加上關(guān)鍵字patch妓笙。

1.3.1 不使用曲面細(xì)分控制器渲染

有時(shí)若河,應(yīng)用中不為曲面細(xì)分計(jì)算著色器提供頂點(diǎn)數(shù)據(jù),只需設(shè)置細(xì)分因子寞宫,此時(shí)在程序中可以只使用曲面細(xì)分計(jì)算著色器萧福,而不使用曲面細(xì)分計(jì)算著色器。

當(dāng)程序中沒有曲面細(xì)分控制著色器時(shí)辈赋,默認(rèn)的細(xì)分因子都為1鲫忍,可以通過一下函數(shù)來為其賦值void glPatchParameterfv (GLenum pname, const GLfloat * values);

其中參數(shù)可選GL _PATCH _DEFAULT _INNER _LEVEL和GL _PATCH _DEFAULT _OUTER _LEVEL钥屈,其值為包含曲面細(xì)分因子的數(shù)組悟民。如果曲面細(xì)分控制著色器缺失,那么曲面細(xì)分計(jì)算著色器中的成員個(gè)數(shù)和每批處理的頂點(diǎn)個(gè)數(shù)相同篷就。并且此時(shí)該著色器中的頂點(diǎn)數(shù)據(jù)直接來源于頂點(diǎn)著色器射亏。

1.4 著色器調(diào)用之間的通信(Communication between Shader Invocations)

曲面細(xì)分控制著色器的作用不僅限于為下一個(gè)控制器提供必要的參數(shù)和數(shù)據(jù),它還在多次控制著色器調(diào)用之間傳遞數(shù)據(jù)竭业。因?yàn)楫?dāng)每個(gè)控制點(diǎn)被計(jì)算式它都會(huì)被調(diào)用一次智润,同時(shí)每次調(diào)用它都能獲取到前一次調(diào)用時(shí)的輸出數(shù)據(jù)。但是該類調(diào)用被設(shè)計(jì)為并發(fā)調(diào)用未辆,因此無法確定在某一次調(diào)用時(shí)數(shù)組中的某個(gè)元素是否被計(jì)算完成。

為了處理這個(gè)問題咐柜,GLSL提供barrier()函數(shù)兼蜈,它被稱為流控制屏障,它能強(qiáng)制使得多個(gè)著色器調(diào)用以一定的順序被執(zhí)行拙友。barrier()函數(shù)在后續(xù)文章中講到計(jì)算著色器時(shí)再展開論述为狸。在曲面細(xì)分控制著色器中它的使用會(huì)受到一定的限制,特別是該函數(shù)只能在main函數(shù)中直接調(diào)用献宫,不能放入任何流控制結(jié)構(gòu)中(如if钥平、else、while或者Switch)姊途。

在曲面細(xì)分控制著色器中調(diào)用函數(shù)barrier()后涉瘾,當(dāng)同批處理的頂點(diǎn)所運(yùn)行的著色器都運(yùn)行到該行代碼時(shí)著色器才會(huì)繼續(xù)運(yùn)行,因此捷兰,如果在該行代碼前輸出了一個(gè)數(shù)組立叛,那么之后的調(diào)用都能獲取到該數(shù)組的完整數(shù)據(jù)。

1.5 曲面細(xì)分實(shí)例-地形渲染(Tessellation Example - Terrain Rendering)

為說明曲面細(xì)分的潛力贡茅,引用一個(gè)基于四邊形模式的批頂點(diǎn)處理和位移貼圖的實(shí)例秘蛇。該示例的部分代碼負(fù)責(zé)位移映射邏輯其做。位移貼圖未一張包含有每個(gè)位置頂點(diǎn)和平面之間的位移值的紋理。每一批頂點(diǎn)都表示地形的一部分區(qū)域赁还,它們根據(jù)其屏幕空間內(nèi)來決定其曲面細(xì)分邏輯妖泄。每個(gè)曲面細(xì)分后的頂點(diǎn)都沿著它們到平面的切線移動(dòng)一定的距離,位移值從位移貼圖中獲取艘策。這樣可以不用存儲(chǔ)每一個(gè)曲面細(xì)分后頂點(diǎn)的位置信息就可以為平面添加更多的幾何細(xì)節(jié)蹈胡。需要注意的是,只有平面生成的位移才會(huì)被存儲(chǔ)到位移貼圖中朋蔫,并且只有它們能夠在運(yùn)行時(shí)被用于曲面細(xì)分計(jì)算著色器罚渐。在示例中使用的位移貼圖(也稱為高度貼圖)如下。

首先設(shè)置一個(gè)簡單的頂點(diǎn)著色器驯妄,每一批頂點(diǎn)實(shí)際上都代表在xz平面上的一個(gè)簡單四邊形荷并,可以在著色器中直接使用常量來表示頂點(diǎn)的坐標(biāo),不用通過頂點(diǎn)數(shù)組輸入青扔。完整的著色器如下源织。著色器中使用內(nèi)置變量gl_InstanceID來計(jì)算每個(gè)批次頂點(diǎn)的偏移量,每個(gè)頂點(diǎn)原始的寬高都為1微猖,中心位于原點(diǎn)雀鹃。在下面的實(shí)例中將會(huì)渲染64乘64個(gè)圖形塊形成一個(gè)網(wǎng)格,因此計(jì)算每個(gè)圖形塊的x和y索引可以通過邏輯并和位移操作完成励两。同時(shí)頂點(diǎn)著色器還會(huì)計(jì)算每批頂點(diǎn)的紋理坐標(biāo),它將會(huì)被傳遞給曲面控制著色器囊颅。

#version 410 core
out VS_OUT {
  vec2 tc; 
} vs_out;

void main() {
  const vec4 vertices[] = vec4[](vec4(-0.5, 0.0, -0.5, 1.0), vec4( 0.5, 0.0, -0.5, 1.0), vec4(-0.5, 0.0, 0.5, 1.0), vec4( 0.5, 0.0, 0.5, 1.0));
  int x = gl_InstanceID & 63; 
  int y = gl_InstanceID >> 6; 
  vec2 offs = vec2(x, y);
  vs_out.tc = (vertices[gl_VertexID].xz + offs + vec2(0.5)) / 64.0; 
  gl_Position = vertices[gl_VertexID] + vec4(float(x - 32), 0.0, float(y - 32), 0.0);
}

完整的曲面細(xì)分控制著色器代碼如下当悔。本實(shí)例中,大部分渲染邏輯都實(shí)現(xiàn)在曲面細(xì)分控制著色器中踢代,不清絕大部分只有在首次調(diào)用時(shí)才會(huì)執(zhí)行盲憎。通過內(nèi)部變量gl_InvocationID等于0判斷首次調(diào)用,計(jì)算細(xì)分因子胳挎。通過將輸入坐標(biāo)和模型視圖投影矩陣相乘饼疙,并將其除以自生的w分量可以將每個(gè)圖形塊兒的四個(gè)控制點(diǎn)投影在標(biāo)準(zhǔn)設(shè)備坐標(biāo)系內(nèi)(頂點(diǎn)渲染這部分相關(guān)邏輯OpenGL會(huì)自動(dòng)處理,這里手動(dòng)計(jì)算是為了計(jì)算細(xì)分因子)(另外需要注意的是當(dāng)矩形塊被投影到標(biāo)準(zhǔn)設(shè)備坐標(biāo)系后慕爬,可以將其看做為一個(gè)變形的平面圖形窑眯,通常此時(shí)僅相對(duì)于屏幕的xy坐標(biāo)分量有意義)。

接下來医窿,計(jì)算出在標(biāo)準(zhǔn)設(shè)備坐標(biāo)系中每個(gè)矩形塊投影在xy平面上后其對(duì)應(yīng)的各個(gè)邊長磅甩。著色器對(duì)每個(gè)邊長乘以1個(gè)縮放系數(shù)再加上一個(gè)基礎(chǔ)值得到每個(gè)邊上的曲面細(xì)分因子。并取水平和垂直方向上的最小值設(shè)置為內(nèi)部曲面細(xì)分因子姥卢。

#version 410 core
layout (vertices = 4) out;
in VS_OUT {
  vec2 tc; 
} tcs_in[];

out TCS_OUT {
  vec2 tc; 
} tcs_out[];

uniform mat4 mvp;

void main() {
  if (gl_InvocationID == 0) {
    vec4 p0 = mvp * gl_in[0].gl_Position;    vec4 p1 = mvp * gl_in[1].gl_Position; 
    vec4 p2 = mvp * gl_in[2].gl_Position;    vec4 p3 = mvp * gl_in[3].gl_Position; 
    p0 /= p0.w;  p1 /= p1.w;  p2 /= p2.w;  p3 /= p3.w;
    // 此處模型坐標(biāo)已經(jīng)被轉(zhuǎn)化為標(biāo)準(zhǔn)設(shè)備坐標(biāo)NDC卷要,而標(biāo)準(zhǔn)設(shè)備坐標(biāo)系的z軸范圍為1到-1渣聚,其中z軸正方向指向屏幕內(nèi)部
    // 這里剔除了位于觀察者后面的圖形塊,優(yōu)化著色器性能僧叉。
    // 這種優(yōu)化仍有瑕疵奕枝,當(dāng)觀察這位于十分陡峭的懸崖底部向上看時(shí),單個(gè)patch會(huì)被拉伸瓶堕,導(dǎo)致其部分位于觀察者后隘道,部分位于觀察者視野內(nèi)
    if (p0.z <= 0.0 || p1.z <= 0.0 || p2.z <= 0.0 || p3.z <= 0.0) {
      gl_TessLevelOuter[0] = 0.0;    gl_TessLevelOuter[1] = 0.0;
      gl_TessLevelOuter[2] = 0.0;    gl_TessLevelOuter[3] = 0.0;
    } else {
      float l0 = length(p2.xy - p0.xy) * 16.0 + 1.0;   float l1 = length(p3.xy - p2.xy) * 16.0 + 1.0; 
      float l2 = length(p3.xy - p1.xy) * 16.0 + 1.0;   float l3 = length(p1.xy - p0.xy) * 16.0 + 1.0; 
      gl_TessLevelOuter[0] = l0;    gl_TessLevelOuter[1] = l1;    
      gl_TessLevelOuter[2] = l2;    gl_TessLevelOuter[3] = l3;    
      gl_TessLevelInner[0] = min(l1, l3);      gl_TessLevelInner[1] = min(l0, l2);
    }
  }
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
  tcs_out[gl_InvocationID].tc = tcs_in[gl_InvocationID].tc; 
}

當(dāng)曲面細(xì)分控制著色器計(jì)算出細(xì)分因子后,并不對(duì)頂點(diǎn)做任何處理捞烟,直接將其傳遞給曲面細(xì)分計(jì)算著色器薄声,其代碼如下。首先計(jì)算出經(jīng)過曲面細(xì)分后每個(gè)頂點(diǎn)的位置和紋理坐標(biāo)题画,通過紋理坐標(biāo)在對(duì)應(yīng)的位移貼圖中取出在y軸方向上的偏移值將其應(yīng)用于各個(gè)頂點(diǎn)默辨,再將生成的坐標(biāo)左生MVP矩陣從而得到其在NDC上的最終位置。

#version 410 core
layout (quads, fractional_odd_spacing) in;
uniform sampler2D tex_displacement;
uniform mat4 mvp;
uniform float dmap_depth;

in TCS_OUT {
  vec2 tc; 
} tes_in[];

out TES_OUT {
  vec2 tc; 
} tes_out;

void main() {
  vec2 tc1 = mix(tes_in[0].tc, tes_in[1].tc, gl_TessCoord.x); 
  vec2 tc2 = mix(tes_in[2].tc, tes_in[3].tc, gl_TessCoord.x); 
  vec2 tc = mix(tc2, tc1, gl_TessCoord.y);
  vec4 p1 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
  vec4 p2 = mix(gl_in[2].gl_Position, gl_in[3].gl_Position, gl_TessCoord.x);
  vec4 p = mix(p2, p1, gl_TessCoord.y);
  p.y += texture(tex_displacement, tc).r * dmap_depth;
  gl_Position = mvp * p;
  tes_out.tc = tc;
}

將計(jì)算出的紋理坐標(biāo)傳入片段著色器中苍息,通過顏色紋理獲取對(duì)應(yīng)的顏色值缩幸。對(duì)應(yīng)的片段著色器代碼如下。

#version 410 core
out vec4 color;
layout (binding = 1) uniform sampler2D tex_color;
in TES_OUT {
  vec2 tc; 
} fs_in;

void main() {
  color = texture(tex_color, fs_in.tc);
}

使用上述著色器渲染渲染的最終結(jié)果如下圖所示竞思,從表面上看并看不出底層的幾何體已經(jīng)被曲面細(xì)分表谊。實(shí)例源代碼見Chapter 8/8.11-dispmap

然而當(dāng)將由三角形構(gòu)建的網(wǎng)格繪制出來后很明顯的看出幾何體經(jīng)歷曲面細(xì)分階段盖喷,繪制的圖像如下爆办。該程序的目標(biāo)是所有被渲染到屏幕上的三角形應(yīng)該有基本相似的屏幕空間區(qū)域,并且細(xì)分因子的改變不應(yīng)該引發(fā)劇烈的視覺差異课梳。

1.6 曲面細(xì)分實(shí)例-立方體貝塞爾斑點(diǎn)(Cubic Bezier Patches)

在位移貼圖的實(shí)例中距辆,我們使用了一個(gè)非常大的紋理圖來將平面拉升成地形圖,同時(shí)使用曲面細(xì)分來提升場景中幾何體的數(shù)量暮刃。這種靠著數(shù)據(jù)驅(qū)動(dòng)實(shí)現(xiàn)復(fù)雜幾何體的方式十分粗暴跨算。在立方體貝塞爾實(shí)例中,將會(huì)通過渲染貝塞爾圖形塊兒的方式使用數(shù)學(xué)知識(shí)來實(shí)現(xiàn)復(fù)雜的幾何體椭懊。貝塞爾曲線的相關(guān)知識(shí)在前面的章節(jié)OpenGL中的數(shù)學(xué)原理已經(jīng)說明诸蚕。

在本節(jié)示例中,只展示了單個(gè)貝塞爾圖元的繪制氧猬,上個(gè)地形渲染轉(zhuǎn)換成本實(shí)例的方式可以是將保存有多個(gè)貝塞爾圖元的頂點(diǎn)數(shù)據(jù)的模型文件加載到程序中背犯,通過instance多次繪制這些貝塞爾圖形從而實(shí)現(xiàn)相同的地形渲染效果。

貝塞爾圖形是一類高階曲面狂窑,它由一些插值函數(shù)在一系列控制點(diǎn)中插值得到媳板。貝塞爾圖形有16個(gè)控制點(diǎn),分布在4乘4的網(wǎng)格中泉哈。大多數(shù)情況下(包括本實(shí)例)蛉幸,這些控制點(diǎn)在二維平面上均勻分布破讨,只在相對(duì)于一個(gè)共享平面的距離上發(fā)生改變。然而奕纫,它們并不是都服從這個(gè)規(guī)定提陶。自由模式的貝塞爾圖形是一個(gè)非常強(qiáng)大的模型處理工具,在很多建模和設(shè)計(jì)軟件中都被大量使用匹层。

一種簡單渲染貝塞爾圖形的方式是將其每行的4個(gè)控制點(diǎn)當(dāng)成一個(gè)簡單立方貝塞爾曲線的控制點(diǎn)對(duì)待寡具。對(duì)于4乘4的網(wǎng)格忽刽,通過上述處理我們可以得到4條貝塞爾曲線驾荣。當(dāng)t取某個(gè)值時(shí)鲜结,每個(gè)曲線分別得到一個(gè)固定的點(diǎn),這樣能得到多個(gè)由4個(gè)控制點(diǎn)組您访,由它們可以再得到一條新的貝塞爾曲線铅忿,這樣便能得到貝塞爾曲面內(nèi)部所有點(diǎn)的空間坐標(biāo)。這里兩次插值使用到的t分別對(duì)應(yīng)gl_TessCoord中的x和y值灵汪。

本實(shí)例中檀训,將在視圖空間內(nèi)執(zhí)行曲面細(xì)分。這意味這需要先將圖像塊的控制點(diǎn)左乘model-view矩陣享言,從而將其坐標(biāo)轉(zhuǎn)換到視圖空間內(nèi)峻凫。頂點(diǎn)著色器代碼如下。

#version 410 core
in vec4 position; 
uniform mat4 mv_matrix;
void main(void) {
  gl_Position = mv_matrix * position;
}

接下來將視圖坐標(biāo)系下的頂點(diǎn)傳遞給曲面細(xì)分控制著色器览露。在更高級(jí)的算法中荧琼,我們可以將控制點(diǎn)投影到屏幕空間上,根據(jù)曲線的長度來設(shè)置曲面細(xì)分因子(該算法必須估計(jì)貝塞爾曲線的長度差牛,其中包含復(fù)雜的曲面積分)铭腕。在本示例中,僅僅設(shè)置一個(gè)固定的曲面細(xì)分因子多糠。曲面細(xì)分控制著色器代碼如下。

#version 410 core
layout (vertices = 16) out;
void main() {
  if (gl_InvocationID == 0) {
    gl_TessLevelInner[0] = 16.0;
    gl_TessLevelInner[1] = 16.0;
    gl_TessLevelOuter[0] = 16.0;
    gl_TessLevelOuter[1] = 16.0;
    gl_TessLevelOuter[2] = 16.0;
    gl_TessLevelOuter[3] = 16.0;
  }
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

接下來將處理好的數(shù)據(jù)傳入曲面細(xì)分計(jì)算著色器中析孽。其中二次貝塞爾曲線和立方貝塞爾曲線的計(jì)算邏輯在前文中已經(jīng)介紹過。圖形計(jì)算函數(shù)根據(jù)輸入的圖形塊控制點(diǎn)坐標(biāo)和各個(gè)頂點(diǎn)在圖形塊內(nèi)的位置計(jì)算出頂點(diǎn)的坐標(biāo)仲翎,這是其被稱為曲面細(xì)分計(jì)算著色器的原因昂秃。

#version 410 core
layout (quads, equal_spacing, cw) in;
uniform mat4 mv_matrix; 
uniform mat4 proj_matrix;

out TES_OUT {
  vec3 N; 
} tes_out;

vec4 quadratic_bezier(vec4 A, vec4 B, vec4 C, float t) {
  vec4 D = mix(A, B, t); 
  vec4 E = mix(B, C, t);
  return mix(D, E, t); 
}

vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t) {
  vec4 E = mix(A, B, t); 
  vec4 F = mix(B, C, t); 
  vec4 G = mix(C, D, t);
  return quadratic_bezier(E, F, G, t); 
}

vec4 evaluate_patch(vec2 at) {
  vec4 P[4]; int I;
  for (i = 0; i < 4; i++) {
    P[i] = cubic_bezier(gl_in[i + 0].gl_Position,
                        gl_in[i + 4].gl_Position,
                        gl_in[i + 8].gl_Position,
                        gl_in[i + 12].gl_Position, at.y);
  }
  return cubic_bezier(P[0], P[1], P[2], P[3], at.x); 
}

const float epsilon = 0.001;

void main(void) {
  vec4 p1 = evaluate_patch(gl_TessCoord.xy);
  vec4 p2 = evaluate_patch(gl_TessCoord.xy + vec2(0.0, epsilon)); 
  vec4 p3 = evaluate_patch(gl_TessCoord.xy + vec2(epsilon, 0.0));
  vec3 v1 = normalize(p2.xyz - p1.xyz); 
  vec3 v2 = normalize(p3.xyz - p1.xyz);
  tes_out.N = cross(v1, v2);
  gl_Position = proj_matrix * p1;
}

在曲面細(xì)分計(jì)算著色器內(nèi)部,通過在頂點(diǎn)附近分別在x軸和y軸上進(jìn)行一定偏移得到兩個(gè)相鄰點(diǎn)时迫,從而進(jìn)一步得到兩個(gè)相鄰逊移,最后近似的計(jì)算出平面的法向量。并將法向量傳入片段作色器內(nèi)做計(jì)算片段的顏色值宿礁。最后使得變形前的圖形從y軸上方看去為綠色案铺,從y軸下方看去為紅色。片段著色器代碼如下梆靖。

#version 410 core 
out vec4 color;

in TES_OUT {
  vec3 N; 
} fs_in;

void main(void) {
  vec3 N = normalize(fs_in.N);
  vec4 c = vec4(1.0, -1.0, 0.0, 0.0) * N.z + vec4(0.0, 0.0, 0.0, 1.0);
  color = clamp(c, vec4(0.0), vec4(1.0)); 
}

在片段著色器中控汉,使用了平面法向量的z軸分量進(jìn)行了簡單的光照計(jì)算。渲染結(jié)果如下圖返吻。源代碼見Chapter 8.14-cubicbezier姑子。

使用線繪制幾何形能更清楚的看出圖形塊經(jīng)歷了曲面細(xì)分階段,其繪制結(jié)果如下圖测僵。

將控制點(diǎn)連線疊加在原圖上繪制結(jié)果如下街佑。

2 幾何著色器(Geometry Shaders)

相較于其他類型的著色器,幾何著色器有著獨(dú)特的特性捍靠,它一次處理一個(gè)完整圖元(點(diǎn)沐旨、線、面)榨婆,并且能改變OpenGL渲染管道中的數(shù)據(jù)量磁携。頂點(diǎn)著色器一次只能處理一個(gè)頂點(diǎn),并且處理該頂點(diǎn)時(shí)不能獲取其他頂點(diǎn)的信息良风,同時(shí)它是嚴(yán)格遵守單個(gè)頂點(diǎn)輸入颜武,單個(gè)頂點(diǎn)輸出原則。曲面細(xì)分著色器操作圖塊(Patches)拖吼,也可以設(shè)置曲面細(xì)分因子,但是對(duì)圖塊是怎樣細(xì)分的控制能力并不強(qiáng)这吻,也不能生成不連續(xù)的圖元吊档。相似的,片段著色器每次也僅能處理單個(gè)片段唾糯,不能獲取其他片段的數(shù)據(jù)怠硼,不能新建片段鬼贱,只能通過丟棄的方式銷毀片段。另一方面香璃,幾何著色器可以獲取到一個(gè)圖元內(nèi)(通過指定GL_TRIANGLES_ADJACENCY模式圖元最多支持6分頂點(diǎn))的所有頂點(diǎn)數(shù)據(jù)这难,能夠改變圖元的類型,甚至能創(chuàng)建和銷毀圖元葡秒。

在OpenGL渲染管道中姻乓,幾何著色器是一個(gè)可選部分。當(dāng)幾何著色器不存在時(shí)眯牧,使用從頂點(diǎn)著色器或者曲面細(xì)分著色器輸出的頂點(diǎn)數(shù)據(jù)在圖元內(nèi)部進(jìn)行插值蹋岩,然后依次將各個(gè)片段傳遞給片段著色器,最后實(shí)現(xiàn)圖元的渲染学少。然而當(dāng)幾何著色器存在的時(shí)候剪个,從頂點(diǎn)著色器和曲面細(xì)分計(jì)算著色器的輸出數(shù)據(jù)將被傳遞到幾何著色器,幾何著色器重組的圖元將對(duì)變成我們將要插值的對(duì)象版确,最后在將插值后的各個(gè)片段傳遞給片段著色器扣囊。另外幾何著色器在重組圖元的時(shí)候,它還可以對(duì)每個(gè)創(chuàng)建的圖元都應(yīng)用一個(gè)變形矩陣绒疗。

2.1 直通式幾何著色器(The Pass-Through Geometry Shader)

在前文的例子中講到過直通式的幾何著色器侵歇,即對(duì)輸入的圖元不做任何更改直接輸出,其代碼如下忌堂。

#version 410 core
layout (triangles) in;
layout (triangle_strip) out;
layout (max_vertices = 3) out;

void main () {
  for (int i = 0; i < gl_in.length(); i++) {
    gl_Position = gl_in[i].gl_Position;
    EmitVertex();
  }
  EndPrimitive();
}

該著色器的前幾行中指定了輸入圖元為triangles盒至,輸出圖元為triangle_strip(更多的圖元類型將會(huì)在后面介紹)。幾何著色器不僅需要指定輸出的圖元類型士修,還需要通過修飾符max_vertices指定最大的輸出頂點(diǎn)個(gè)數(shù)枷遂。該實(shí)例中的著色器生成的時(shí)三角形條帶,因此指定最大個(gè)數(shù)為3棋嘲。

幾何著色器的主函數(shù)看上去很像頂點(diǎn)著色器或者說是片段著色器酒唉。頂點(diǎn)著色器的所有輸出都被包裝成一個(gè)結(jié)構(gòu)體,而單個(gè)圖元的多個(gè)頂點(diǎn)的輸出則被包裝稱為結(jié)構(gòu)體數(shù)組傳遞給幾何著色器沸移,可以通過變量gl_in獲得痪伦。數(shù)組的大小由輸入圖元的類型決定,在該實(shí)例中雹锣,輸入圖元是三角形网沾,因此數(shù)組的大小為3。

該實(shí)例中蕊爵,在循環(huán)內(nèi)部僅僅將輸入拷貝到輸出中辉哥。輸出結(jié)構(gòu)和頂點(diǎn)著色器幾曲面細(xì)分計(jì)算著色器類似,直接復(fù)制給內(nèi)建變量gl_Position。當(dāng)賦值完單個(gè)頂點(diǎn)的所有屬性后必須調(diào)用函數(shù)EmitVertex()醋旦。該函數(shù)通知幾何著色器我們已經(jīng)完成了對(duì)該頂點(diǎn)的所有賦值操作恒水,需要存儲(chǔ)該頂點(diǎn),并準(zhǔn)備設(shè)置下一個(gè)頂點(diǎn)饲齐。

最后钉凌,當(dāng)循環(huán)執(zhí)行完畢,調(diào)用了另一個(gè)幾何著色器的專有函數(shù)EndPrimitive()捂人。該函數(shù)通知幾何著色器我們已經(jīng)完成了對(duì)當(dāng)前圖元所有頂點(diǎn)的賦值操作御雕,即將處理下一個(gè)圖元。在本實(shí)例中先慷,我們指定了輸出的圖元類型為三角形條帶饮笛,因此吐過我們連續(xù)調(diào)用超過三次EmitVertex()函數(shù),OpenGL將會(huì)將三角形添加到連續(xù)的三角形條帶中论熙。如果我們像生成分離的三角形或者多個(gè)不相連的三角形條帶福青,我們需要在它們之間調(diào)用函數(shù)EndPrimitive()。另外當(dāng)程序沒有調(diào)用該函數(shù)時(shí)脓诡,OpenGL會(huì)自動(dòng)在幾何著色器的末尾調(diào)用該函數(shù)无午。此外,本實(shí)例中生成的是分離的三角形構(gòu)成的條帶祝谚。

2.2 在應(yīng)用中使用幾何著色器(Using Geometry in an Application)

同其他著色器的創(chuàng)建方法相同宪迟,使用函數(shù)glCreateShader和參數(shù)GL_GEOMETRY_SHADE R創(chuàng)建幾何著色器。同樣的交惯,使用函數(shù)glShaderSource管理著色器的源代碼次泽,使用glCompileShader編譯著色器,使用函數(shù)glAttachShader將著色器和程序關(guān)聯(lián)席爽。然后使用函數(shù)glLinkProgram鏈接著色器意荤。此時(shí),當(dāng)使用繪制函數(shù)如glDrawArrays時(shí)只锻,頂點(diǎn)著色器為每個(gè)頂點(diǎn)運(yùn)行一次玖像,幾何著色器為每個(gè)圖元運(yùn)行一次,片段著色器為每個(gè)片段運(yùn)行一次齐饮。幾何著色器的定義的輸入圖元和真實(shí)的輸入圖元類型必須有相關(guān)性捐寥,需要注意的是當(dāng)曲面細(xì)分著色器未被啟用時(shí),幾何著色器的定義的輸入圖元類型必須和調(diào)用繪制函數(shù)時(shí)的指定圖元相匹配祖驱。完整的對(duì)應(yīng)表如下握恳。

幾何著色器輸入類型          允許的輸出類型
points                   GL_POINTS
lines                    GL_LINES,GL_LINE_LOOP捺僻,GL_LINE_STRIP
triangles                GL_TRIANGLES乡洼,GL_TRIANGLE_FAN,GL_TRIANGLE_STRIP
lines_adjacency          GL_LINES_ADJACENCY
triangles_adjacency      GL_TRIANGLES_ADJACENCY

當(dāng)曲面細(xì)分引擎被激活是,使用繪制命令只能使用參數(shù)GL_PATCHES就珠,OPenGL會(huì)在曲面細(xì)分的過程中將圖塊轉(zhuǎn)化為點(diǎn)、線或者三角形圖元醒颖。在本實(shí)例中妻怎,輸入圖元的類型和曲面細(xì)分著色器生成的圖元類型相匹配。幾何著色器接收到了一個(gè)完整圖元的所有頂點(diǎn)參數(shù)泞歉,其內(nèi)建變量gl_in的定義如下逼侦。

這里定義的gl_perVertex并非結(jié)構(gòu)體,而是著色器直接特殊的傳遞類型變量block
in gl_perVertex {
  vec4 gl_Position;
  float gl_PointSize;
  float gl_ClipDistance[];
} gl_in[];

對(duì)于頂點(diǎn)著色器中普通類型輸出變量腰耙,在幾何著色器中都應(yīng)該被定義為相應(yīng)的數(shù)組類型變量榛丢。舉例如下。

// 在頂點(diǎn)著色器中的聲明
out vec4 color挺庞;
out VertexData {
  vec4 color;
  vec3 normal;
} vertex;

// 在幾何著色器中對(duì)應(yīng)的聲明
in vec4 color[];
out VertexData {
  vec4 color;
  vec3 normal;
} vertex[];

如果編寫幾何著色器時(shí)確定其需要處理的圖元類型晰赞,那么可以明確的指定數(shù)組的大小,這樣能在編譯時(shí)節(jié)省額外的錯(cuò)誤檢查時(shí)間选侨。當(dāng)沒有明確指定數(shù)組大小時(shí)掖鱼,OpenGL將會(huì)根據(jù)輸入的圖元類型推斷數(shù)組的大小,其對(duì)應(yīng)關(guān)心如下援制。

Input Primitive Type        Size of Input Arrays
points                      1
lines                       2
triangles                   3
lines_adjacency             4
triangles_adjacency         6

另外戏挡,幾何著色器必須指定輸出的圖元類型,格式為layout (primitive_type) out;晨仑。還必須指定幾何著色器一次能輸出的最大頂點(diǎn)數(shù)胖喳,格式為layout (max_vertices = n) out;童谒,需要主要的是n的設(shè)置必須使得程序能夠正常運(yùn)行的最小值,例如當(dāng)你獲取點(diǎn)類型圖元,要輸出line類型圖元時(shí)啊送,n值最小為2,因?yàn)樾纬删€圖元的最小值為2收津。當(dāng)曲面細(xì)分過于細(xì)時(shí)裳扯,可能需要將n值設(shè)置非常大,這會(huì)耗費(fèi)更多的性能噪珊。n值的上限至少是256晌缘,可以通過函數(shù)glGetIntegerv和參數(shù)GL_ MAC_ GEOMETRY_ OUTPUT_ VERTIVCES獲得準(zhǔn)確的上限值。另外可以在單個(gè)布局聲明中通過逗號(hào)分隔符定義多個(gè)布局屬性痢站,格式如layout (triangle_strip, max_vertices = n) out;磷箕。

如果幾何著色器的源碼中未調(diào)用函數(shù)EmitVertex或者函數(shù)EndPrimitive,渲染管道的所有圖元傳遞到幾何著色器后都會(huì)被丟棄阵难。需要注意的是如果實(shí)現(xiàn)了main函數(shù)岳枷,并且未調(diào)用EndPrimitive方法,OpenGL會(huì)默認(rèn)在main函數(shù)末尾調(diào)用該方法。正如在頂點(diǎn)著色器內(nèi)部的工作空繁,此處設(shè)置幾何著色器處理后的剪切空間坐標(biāo)(clip-space coordinates)殿衰。如果想從幾何著色器傳遞其他變量到片段著色器,可以聲明為block類型(interface block)或者全局變量類型盛泡。無論什么時(shí)候只要調(diào)用函數(shù)EmitVertex闷祥,幾何著色器都會(huì)立即將數(shù)據(jù)存儲(chǔ)到輸出變量中,并且用它們生成一個(gè)新的頂點(diǎn)傲诵。

函數(shù)EndPrimitive表面向圖元末尾追加頂點(diǎn)的工作已經(jīng)完成凯砍。如果輸出圖元的類型為triangle_strip,并且調(diào)用函數(shù)EmitVertex的次數(shù)超過三次拴竹,幾何著色器將會(huì)在單個(gè)條帶內(nèi)產(chǎn)生多個(gè)三角形悟衩。同樣的,如果輸出圖元類型為line_strip栓拜,并且調(diào)用函數(shù)EmitVertex超過兩次座泳,將會(huì)在單個(gè)條帶內(nèi)得到多條線段。在幾何著色器內(nèi)菱属,函數(shù)EndPrimitive表示單個(gè)條帶的結(jié)束钳榨,這意味著如果需要繪制單獨(dú)的線段或者三角形,需要在每兩個(gè)或者三個(gè)頂點(diǎn)之后調(diào)用函數(shù)EndPrimitive纽门。

最后需要提一點(diǎn)薛耻,如果在你調(diào)用EndPrimitive函數(shù)之前,調(diào)用EmitVertex函數(shù)的次數(shù)不足以組成單個(gè)圖元(如使用的輸出圖元類型是triangle_strip赏陵,但是在調(diào)用函數(shù)EndPrimitive之前只生成了兩個(gè)頂點(diǎn))饼齿,那么幾何著色器講不會(huì)生成新的圖元,并且將已經(jīng)生成的頂點(diǎn)數(shù)據(jù)全部丟棄蝙搔。

2.3 在幾何著色器中扔掉幾何體(Discarding Geometry in the Geometry Shader)

通過調(diào)用函數(shù)EmitVertex追加足夠的頂點(diǎn)缕溉,再調(diào)用函數(shù)EndPrimitive即可以生成新的圖元,當(dāng)然也可以不調(diào)用上述函數(shù)從而實(shí)現(xiàn)對(duì)圖元的丟棄吃型。為了說明這一點(diǎn)证鸥,設(shè)計(jì)了示例程序隱面剔除(backface culling)。當(dāng)然OpenGL在幾何處理器后勤晚,在光柵化之前枉层,會(huì)有圖元剔除步驟,并且可以設(shè)置剔除背面或者正面赐写,這里的實(shí)例只做演示功能鸟蜡,實(shí)際項(xiàng)目中基本不會(huì)采取該方式統(tǒng)一剔除模型的背面圖元。

在main函數(shù)中挺邀,我們需要找到三角形的面法向量揉忘。只需要通過三角形所在平面的兩個(gè)法向量叉乘即可獲得跳座,再該實(shí)例中,可以使用三角形的兩條邊泣矛。代碼如下疲眷。

vec3 ab = gl_in[1].gl_Position.xyz - gl_in[0].gl_Position.xyz;
vec3 ac = gl_in[2].gl_Position.xyz - gl_in[0].gl_Position.xyz;
// 計(jì)算模型坐標(biāo)系中的圖元法向量
vec3 normal = normalize(cross(ab, ac));

當(dāng)?shù)玫椒ㄏ蛄亢螅瓤梢源_定該圖元是面向還是背離觀察者您朽。具體做法是將面的法向量轉(zhuǎn)換到觀察坐標(biāo)空間中咪橙,在本實(shí)例中也就是世界坐標(biāo)空間。當(dāng)包含model-view矩陣通用變量時(shí)虚倒,只需將其和法向量相乘即可。準(zhǔn)確的描述是使用左上3*3的子矩陣和法向量相乘产舞。 當(dāng)然魂奥,吐過你的模型-視圖矩陣只包含平移、三軸等比例縮放(uniform scale易猫,不含剪切)以及旋轉(zhuǎn)耻煤,你可以直接使用它和法向量相乘。但是准颓,在此之前我們需要將法向量轉(zhuǎn)化為4維向量哈蝇。接下來我們通過獲取轉(zhuǎn)換后的法向量和觀察向量的點(diǎn)乘結(jié)果來確定圖元是面向還是背離觀察者。

如果點(diǎn)乘的結(jié)果為負(fù)攘已,意味著圖元背向觀察者炮赦,應(yīng)該被剔除,剔除圖元正如前文描述一樣样勃,只需要簡單的不處理輸入數(shù)據(jù)吠勘,那么該圖元就會(huì)被丟棄。如果結(jié)果為正峡眶,那么圖元面向觀察者剧防,此時(shí)需要新建圖元。代碼如下辫樱。

// 如果圖元法向量和設(shè)置的視線逆向量夾角大于90度峭拘,即認(rèn)為圖元法向量背離觀察者,即剔除該圖元
if (dot(normal, vt) > 0.0) {
  for (int n = 0; n < 3; n++) {
    gl_Position = mvpMatrix * gl_in[n].gl_Position;
    color = vertex[n].color;
    EmitVertex();
  }
  EndPrimitive();
}

在該實(shí)例中狮暑,盡管我們選定的輸出圖元類型為triangle strip鸡挠,但是我們?nèi)匀粚?duì)每個(gè)輸入的三角形圖元只生成一個(gè)三角形輸出圖元,因此得到的條帶都只包含單個(gè)三角形心例。這里函數(shù)EndPrimitive可以省略宵凌,此處為了代碼的完整性特意列出。該項(xiàng)目的輸出結(jié)果如下止后。完整的源代碼請(qǐng)見8.16-glculling瞎惫。

上圖的每個(gè)小圖都表示不同的觀察者位置溜腐,正如圖中所示,模型的不同部分被幾何著色器剔除瓜喇。該實(shí)例的實(shí)用性并不重要挺益,它只是為了說明幾何著色器有基于應(yīng)用定義的準(zhǔn)則實(shí)現(xiàn)幾何體裁剪的能力。

2.4 在幾何著色器中修改幾何體(Modifying Geometry in the Geometry Shader)

除了丟棄圖元外乘寒,幾何著色器還可以輸出和輸入圖元形狀不相同的圖元望众。即使幾何著色器不改變頂點(diǎn)的個(gè)數(shù),相對(duì)于只有頂點(diǎn)著色器伞辛,他仍能使我們能做更多的事情烂翰。例如,如果輸入圖元是三角形條帶或扇蚤氏,則輸出的三角形會(huì)共享頂點(diǎn)和邊甘耿,如果使用頂點(diǎn)著色器移動(dòng)一個(gè)頂點(diǎn),那么共享該頂點(diǎn)的所有三角形都會(huì)發(fā)生改變竿滨。然而佳恬,使用幾何著色器即可對(duì)其中的單個(gè)三角形進(jìn)行修改。

如果幾何著色器接受三角形圖元輸入(輸入不能是條帶或者扇狀圖元)于游,并輸出三角形條帶圖元毁葱。那么無論使用繪制命令時(shí)調(diào)用的函數(shù)時(shí)glDrawArrays()或者是glDrawElements(),繪制的圖元是三角形贰剥、三角形條帶或者三角形條帶倾剿,進(jìn)入幾何著色器的一定是獨(dú)立的三角形。除非幾何著色器輸出時(shí)在調(diào)用結(jié)束圖元前輸出的頂點(diǎn)數(shù)大于3哥蚌成,輸出結(jié)果都是獨(dú)立的未連接的三角形柱告。

在下面的實(shí)例中,我們將“炸開一個(gè)模型”笑陈,使得它的三角形都沿著它們的面法向量向外移動(dòng)际度。原始的模型是使用獨(dú)立的三角形還是三角形條帶或者是三角形扇繪制的并不重要。和上一個(gè)例子一樣涵妥,幾何著色器的輸入是一個(gè)三角形乖菱,輸出時(shí)一個(gè)三角形條帶,幾何著色器單個(gè)圖元最大的頂點(diǎn)數(shù)為3蓬网,因?yàn)槲覀儾⑽捶糯蠡蛘吒淖內(nèi)切沃纤缀沃鞯拇a如下。

#version 410 core

layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;

in VS_OUT {
    vec3 normal;
} gs_in[];

out GS_OUT {
    vec3 normal;
} gs_out;

uniform float explode_factor = 0.1;

void main() {
    vec3 ab = gl_in[1].gl_Position.xyz - gl_in[0].gl_Position.xyz;
    vec3 ac = gl_in[2].gl_Position.xyz - gl_in[0].gl_Position.xyz;
    // 這里三角形圖元的法向量被乘以-1帆锋,是因?yàn)檩斎雸D元可能為順時(shí)針
    vec3 face_normal = -normalize(cross(ab, ac));
    for (int i = 0; i < gl_in.length(); i++) {
        // 在裁剪坐標(biāo)系下吵取,將兔子模型的圖元沿著法向量進(jìn)行平移,模擬爆炸效果
        gl_Position = gl_in[i].gl_Position + vec4(face_normal * explode_factor, 0.0);
        gs_out.normal = gs_in[i].normal;
        EmitVertex();
    }
    EndPrimitive();
}

最后模擬的兔子炸裂效果如下圖锯厢。Demo地址為8.17-gsexploder皮官。

為了驗(yàn)證圖元頂點(diǎn)輸入順時(shí)針和逆時(shí)針順序?qū)τ?jì)算法向量的影響脯倒,特設(shè)計(jì)另外一個(gè)實(shí)驗(yàn),其源碼地址為8.17-0-normaloftriangle捺氢。左側(cè)三角形的頂點(diǎn)輸入順序?yàn)轫槙r(shí)針藻丢,右側(cè)為逆時(shí)針,同時(shí)沿著法向量移動(dòng)一定的距離摄乒,可以看出左側(cè)三角形向用戶移動(dòng)悠反,二右側(cè)三角形遠(yuǎn)離用戶。在右手坐標(biāo)系下馍佑,通常我們將面向用戶的圖元以逆時(shí)針輸入頂點(diǎn)斋否。

生成圖元
正如不調(diào)用函數(shù)EmitVertex和函數(shù)EndPrimitive可以丟棄圖元,多次調(diào)用它們可以生成新的圖元拭荤。這就是說如叼,在調(diào)用調(diào)用函數(shù)EndPrimitive之前,如果調(diào)用函數(shù)EmitVertex次數(shù)達(dá)到在幾何著色器頭部聲明的最大頂點(diǎn)數(shù)后穷劈。在幾何著色器中可以拷貝多個(gè)輸入圖元,或者將輸入圖元分解為更小的碎片踊沸,這也是下面的實(shí)例演示的內(nèi)容歇终。輸入的模型是一個(gè)繞原點(diǎn)的四面體,它每一面由一個(gè)三角形圖元組成逼龟。沿著每條邊的中點(diǎn)細(xì)分輸入的三角形圖元评凝,然后移動(dòng)所有生成的頂點(diǎn),使它們到原點(diǎn)的距離都不相同腺律。這樣將四面體轉(zhuǎn)換為釘狀幾何體奕短。

在這里,由于幾何著色器在物體空間操作(object space)(需要注意匀钧,四面體的中心點(diǎn)是原點(diǎn))翎碑,因此在頂點(diǎn)著色器中,不需要進(jìn)行任何處理之斯,而是在幾何著色器中對(duì)新生成的頂點(diǎn)坐標(biāo)進(jìn)行投影變換日杈。另外,頂點(diǎn)著色器中如果存在紋理坐標(biāo)和法向量等屬性佑刷,也應(yīng)該直接傳入幾何著色器莉擒。

在幾何著色器中,其輸入圖元為三角形瘫絮,其輸出圖元為有單個(gè)三角形組成的條帶涨冀。當(dāng)三角形圖元輸入后,幾何著色器會(huì)將其分解為4個(gè)三角形圖元麦萤。我們需要聲明最大的輸出頂點(diǎn)數(shù)為12鹿鳖。

#version 410 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 12) out;
uniform float stretch = 0.3;
// 這里意味這演示變量不會(huì)被線性插值扁眯,https://stackoverflow.com/questions/27581271/flat-qualifier-in-glsl
flat out vec4 color;
uniform mat4 mvMatrix;
uniform mat4 mvpMatrix;

void make_face(vec3 a, vec3 b, vec3 c) {
    vec3 face_normal = normalize(cross(c - a, c - b));
    vec4 face_color = vec4(1.0, 0.2, 0.4, 1.0) * (mat3(mvMatrix) * face_normal).z;
    gl_Position = mvpMatrix * vec4(a, 1.0);
    color = face_color;
    EmitVertex();
    gl_Position = mvpMatrix * vec4(b, 1.0);
    color = face_color;
    EmitVertex();
    gl_Position = mvpMatrix * vec4(c, 1.0);
    color = face_color;
    EmitVertex();
    EndPrimitive();
}

void main() {
    int n;
    vec3 a = gl_in[0].gl_Position.xyz;
    vec3 b = gl_in[1].gl_Position.xyz;
    vec3 c = gl_in[2].gl_Position.xyz;

    // 此處計(jì)算三角形從一個(gè)頂點(diǎn)連向?qū)呏悬c(diǎn)及其延長線上的向量次兆,a+b為原點(diǎn)到ab邊中點(diǎn)構(gòu)成的向量*2
    vec3 d = (a + b) * stretch;
    vec3 e = (b + c) * stretch;
    vec3 f = (c + a) * stretch;
    // 放大器原始頂點(diǎn)森爽,是釘狀形狀更加突出
    a *= (2.0 - stretch);
    b *= (2.0 - stretch);
    c *= (2.0 - stretch);

    // 此處將單個(gè)三角形圖元分解為四個(gè)小的三角形圖元
    make_face(a, d, f);
    make_face(d, b, e);
    make_face(e, c, f);
    make_face(d, e, f);
    EndPrimitive();
}

使用幾何著色器處理復(fù)雜的曲面細(xì)分可能不能得到最佳的性能。如果想要的曲面細(xì)分效果比本實(shí)例中的更復(fù)雜提针,最好使用曲面細(xì)分引擎來完成該需求藕甩。然而施敢,如果只是為單個(gè)圖元生成2~4個(gè)輸出圖元,可能使用幾何著色器更方便狭莱。本實(shí)例的效果如下僵娃。Demo地址:8.18-gstessellate

2.5 在幾何著色器中改變圖元類型(Changing the Primitive Type in the Geometry Shader)

到目前為止腋妙,所有的幾何著色器例子并沒有改變圖元的類型默怨。但是實(shí)際上,幾何著色器可以改變圖元類型骤素,例如將點(diǎn)圖元轉(zhuǎn)換為三角形匙睹,或者將三角形轉(zhuǎn)換為點(diǎn)圖元。在下面的例子中济竹,我們將三角形圖元轉(zhuǎn)換為線圖元痕檬。我們會(huì)計(jì)算頂點(diǎn)法向量并以線圖元的方式展示,也會(huì)計(jì)算面的法向量并展示成為另外一條線送浊。這可以使我們通過頂點(diǎn)和圖元的法向量去觀察模型梦谜。需要注意的是,如果你想在模型的基礎(chǔ)上觀察法向量袭景,那么你需要繪制兩次唁桩。第一次不使用幾何著色器渲染模型,第二次使用幾何著色器可視化法向量耸棒。單個(gè)幾何著色器不能同時(shí)輸出兩種圖元荒澡。在本實(shí)例中的幾何著色器中,除了gl_in內(nèi)置變量与殃,我們還需要每個(gè)頂點(diǎn)的法向量仰猖,該變量可以從頂點(diǎn)著色器中傳入幾何著色器。幾何著色器的代碼如下奈籽。

#version 410 core

layout (triangles) in;
layout (line_strip, max_vertices = 4) out;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

in VS_OUT {
    vec3 normal;
    vec4 color;
} gs_in[];

out GS_OUT {
    vec3 normal;
    vec4 color;
} gs_out;

uniform float normal_length = 0.2;

void main() {
    mat4 mvp = proj_matrix * mv_matrix;
    vec3 ab = gl_in[1].gl_Position.xyz - gl_in[0].gl_Position.xyz;
    vec3 ac = gl_in[2].gl_Position.xyz - gl_in[0].gl_Position.xyz;
    vec3 face_normal = normalize(cross(ab, ac));
    
    vec4 tri_centroid = (gl_in[0].gl_Position +
                         gl_in[1].gl_Position +
                         gl_in[2].gl_Position) / 3.0;

    gl_Position = mvp * tri_centroid;
    gs_out.normal = gs_in[0].normal;
    gs_out.color = gs_in[0].color;
    EmitVertex();

    gl_Position = mvp * (tri_centroid +
                         vec4(face_normal * normal_length, 0.0));
    gs_out.normal = gs_in[0].normal;
    gs_out.color = gs_in[0].color;
    EmitVertex();
    EndPrimitive();

    gl_Position = mvp * gl_in[0].gl_Position;
    gs_out.normal = gs_in[0].normal;
    gs_out.color = gs_in[0].color;
    EmitVertex();

    gl_Position = mvp * (gl_in[0].gl_Position +
                         vec4(gs_in[0].normal * normal_length, 0.0));
    gs_out.normal = gs_in[0].normal;
    gs_out.color = gs_in[0].color;
    EmitVertex();
    EndPrimitive();
}                                                                      

在幾何著色器中饥侵,輸入類型圖元為三角形,輸出圖元為由單個(gè)線段組成的線段條帶衣屏。因?yàn)槲覀冃枰獮槊總€(gè)可視化的法向量生成一條單獨(dú)的線段躏升,因此每消耗一個(gè)頂點(diǎn),都會(huì)生成兩個(gè)頂點(diǎn)表示點(diǎn)的法向量狼忱,兩個(gè)頂點(diǎn)表示面法向量膨疏。因此每個(gè)三角形圖元輸出的最大頂點(diǎn)數(shù)為8一睁。

每個(gè)輸入的頂點(diǎn)被轉(zhuǎn)換到它在世界坐標(biāo)系中的位置,然后被輸出幾何著色器佃却,然后通過該頂點(diǎn)的法向量向外截取標(biāo)準(zhǔn)長度用于生成第二個(gè)頂點(diǎn)者吁,經(jīng)過同樣的轉(zhuǎn)換后被輸出幾何著色器。這樣我們的法向量長度都為1饲帅,但是最后呈現(xiàn)在屏幕上的模型仍能體現(xiàn)出模型-視圖-投影矩陣的改變复凳。將著色器提供的變量和法向量相乘,表示點(diǎn)法向量的線段長度和模型相匹配灶泵。

我們通過三角形圖元的兩個(gè)邊相乘的到面法向量育八,選則三角形的重心為面法向量的起點(diǎn),沿著面法向量截取單位長度并乘以著色器中定義的法向量長度從而獲得第二個(gè)頂點(diǎn)赦邻,同樣將它們轉(zhuǎn)化到視圖坐標(biāo)系中髓棋。

該實(shí)例的最終渲染效果如下圖所示。Demo地址為Chapter 8.19-normalviewer惶洲。

2.6 多重流存儲(chǔ)(Multiple Streams of Storage)

當(dāng)只有頂點(diǎn)著色器存在時(shí)按声,對(duì)于輸入到著色器中的頂點(diǎn)以及存儲(chǔ)在轉(zhuǎn)換反饋緩存中的頂點(diǎn)之間是簡單的單進(jìn)單出關(guān)系。當(dāng)幾何著色器存在的時(shí)候签则,每個(gè)著色器調(diào)用可能會(huì)在轉(zhuǎn)換反饋緩存(transform feedback buffers)中存儲(chǔ)0個(gè)/1個(gè)或者更多的頂點(diǎn)币呵。不僅如此侨颈,我們甚至能夠配置4個(gè)輸出流余赢,同時(shí)使用幾何著色器將輸出發(fā)送到任意一個(gè)選中的輸出流中。該策略可以用于分類幾何體(sort geometry)或者在將幾何體存入轉(zhuǎn)換反饋緩存中的同時(shí)渲染部分圖元哈垢。

在幾何著色器中使用多重輸出流仍有一些限制妻柒。首先,從幾何著色器中輸出的所有輸出流的圖元模式都必須是點(diǎn)圖元(points)耘分。其次举塔,盡管可以在渲染模型的同時(shí)將頂點(diǎn)數(shù)據(jù)存入轉(zhuǎn)換反饋緩存中,只有第一個(gè)流可以被渲染求泰,剩余的輸出流都只能作為存儲(chǔ)使用央渣。如果上述兩個(gè)限制和產(chǎn)品需求不沖突,那么多重流將是一個(gè)強(qiáng)大的特性渴频。

要在幾何著色器中使用多重流特性芽丹,需要使用布局修飾詞stream來設(shè)置變量應(yīng)該輸出到哪個(gè)流之中。stream關(guān)鍵字的用法如下所示卜朗。

out vec4                       foo;       //"foo" is in stream 0 (the default).
layout (stream=2) out vec4     bar;       //"bar" is part of stream 2.
out vec4                       baz;       //"baz" is back in stream 0.
layout (stream=1) out;                    //Everything from here on is in stream 1.
out int                        apple;     // "apple" and "orange" are part
out int                        orange;    // of stream 1.
layout (stream=3) out MY_BLOCK {          // Everything in "MY_BLOCK" is instream 3.
    vec3 purple;
    vec3 green; 
};

在幾何著色器中拔第,使用過函數(shù)EmitVertex()和函數(shù)EndPrimitive()操作的都是對(duì)于輸出流0的咕村。使用函數(shù)EmitStreamVertex()和函數(shù)EndStreamPrimitive()都可以使用整形參數(shù)指定操作的輸出流。

2.7 幾何著色器中的新圖元類型(New Primitive Types Introduced by the Geometry Shader)

幾何著色器中引用了4個(gè)新的圖元類型蚊俺,GL_LINES_ADJACENCY懈涛,GL_LINE_STRIP_ADJACENCY,GL_TRIANGLES_ADJACENCY泳猬,和GL_TRIANGLE_STRIP_ADJACENCY批钠。只有幾何著色器被激活時(shí),這幾個(gè)圖元才是有效的暂殖。當(dāng)這些新的臨近圖元被使用后价匠,對(duì)于每個(gè)傳入幾何著色器的線或者三角形,我們不僅可以獲得當(dāng)前處理的圖元頂點(diǎn)信息呛每,還能獲取到該圖元的臨近圖元的頂點(diǎn)數(shù)據(jù)踩窖。

當(dāng)使用GL_LINES_ADJACENCY圖元渲染時(shí),屬性數(shù)組中每4個(gè)頂點(diǎn)組成一個(gè)線圖元晨横。中間兩個(gè)頂點(diǎn)組成了當(dāng)前線圖元洋腮,前后的頂點(diǎn)時(shí)其相鄰的線圖元的頂點(diǎn)。因此傳入幾何著色器的輸入數(shù)組包含4個(gè)成員手形。幾何著色器的輸入圖元和輸出圖元類型并無關(guān)系啥供,我們甚至可以使用這四個(gè)頂點(diǎn)生成兩個(gè)三角形圖元】饪罚基于這個(gè)特性伙狐,我們甚至可以使用GL_LINES_ADJACENCY圖元類型去渲染矩形。需要注意的是瞬欧,在幾何著色器未被激活時(shí)使用GL_LINES_ADJACENCY渲染模型贷屎,只有中間兩個(gè)頂點(diǎn)會(huì)被渲染到屏幕上,頂點(diǎn)著色器會(huì)直接忽略前后兩個(gè)頂點(diǎn)艘虎。

使用GL_LINE_STRIP_ADJACENCY類型圖元會(huì)出現(xiàn)相似的結(jié)果唉侄。不同的是整個(gè)三角形條帶被認(rèn)為是一個(gè)圖元,在整個(gè)圖元的前部和尾部各包含一個(gè)臨近圖元的頂點(diǎn)野建。如果使用GL_LINES_ADJACENCY類型圖元属划,在渲染管道中放入8個(gè)頂點(diǎn),那么幾何著色器將會(huì)運(yùn)行兩次候生,如果使用GL_LINE_STRIP_ADJANCENCY類型圖元同眯,幾何著色器將會(huì)運(yùn)行5次,詳見下圖唯鸭。

對(duì)于GL_LINE_STRIP_ADJACENCY類型圖元嗽测,幾何著色器分別處理為ABCD和EFGH兩個(gè)圖元,對(duì)于GL_LINE_STRIP_ADJANCENCY類型圖元時(shí),會(huì)被處理稱為ABCD唠粥,BCDE直至EFGH疏魏。另外在上圖中,如果沒有幾何著色器的加入晤愧,只有實(shí)線才會(huì)被渲染到屏幕上大莫。

GL_TRIANGLES_ADJACENCY類型圖元和GL_LINES_ADJACENCY類型圖元的工作原理類似。下面討論6個(gè)頂點(diǎn)組成的該圖元被發(fā)送到幾何著色器后的處理過程官份。第1只厘、3、5個(gè)頂點(diǎn)被認(rèn)為是組成了真實(shí)的三角形圖元舅巷,第2羔味、4、6個(gè)頂點(diǎn)被認(rèn)為是位于三角形之間的頂點(diǎn)钠右。這意味著幾何著色器的輸入數(shù)組包含6個(gè)元素赋元。其具體處理邏輯如下圖。

這幾個(gè)新增圖元中最后介紹的一個(gè)也是最難的一個(gè)(換句話說是最難以理解的)圖元類型是GL_TRIANLGLE_STRIP_ADJACENCY飒房。下圖形象說明了該類型圖元的處理方式搁凸。在圖中,頂點(diǎn)A到P共16個(gè)頂點(diǎn)被傳遞到了幾何著色器狠毯,組成三角形條帶的頂點(diǎn)之間剛好都間隔1個(gè)頂點(diǎn)护糖。

三角形的分割順序如下圖所示,除了頭尾兩個(gè)三角形圖元嚼松,其他的三角形頂點(diǎn)組合規(guī)律都相同嫡良。GL_TRIANLGLE_STRIP_ADJACENCY類型圖元的頂點(diǎn)組合順序可以在OpenGL官網(wǎng)中找到詳細(xì)說明,如果需要使用到該類型圖元献酗,最后去閱讀相關(guān)文檔寝受。

使用幾何著色器渲染四邊形
現(xiàn)代的圖像API不直接支持渲染四邊形,主要的原因可能是GPU不支持四邊形凌摄。通常一個(gè)模型編輯程序在生成由4邊形組成的模型時(shí)羡蛾,都可以選擇將其轉(zhuǎn)換為多個(gè)三角形漓帅。這些圖元就能夠直接被GPU渲染锨亏。部分GPU支持四邊形,但是內(nèi)部也會(huì)將四邊形轉(zhuǎn)化為多個(gè)三角形忙干。

多數(shù)情況下器予,將一個(gè)四邊形轉(zhuǎn)化為多個(gè)三角形的渲染效果和直接渲染四邊形的效果在視覺上并不會(huì)有太大的區(qū)別。但是仍然有很多時(shí)候它們的效果并不相同捐迫,如下圖所示乾翔。

在上圖中三角形的頂點(diǎn)纏繞方向都相同,另外都包含3個(gè)黑色頂點(diǎn)和1個(gè)白色頂點(diǎn),但是左圖中反浓,將四邊形垂直分割位兩個(gè)三角形萌丈,而在右圖中,四邊形被水平分割為兩個(gè)三角形雷则。這導(dǎo)致了左圖兩個(gè)三角形之間有一條明顯的高亮縫隙辆雾,它直接貫穿了四邊形,而右側(cè)四邊形上面三角部分都為黑色月劈,而下面部分由黑色向白色過渡度迂。

造成這個(gè)現(xiàn)象的原因是,我們分別渲染兩個(gè)三角形的時(shí)候會(huì)在單個(gè)三角形的頂點(diǎn)中做插值處理猜揪,最后將結(jié)果傳遞給片段著色器惭墓。而在渲染單個(gè)三角形的時(shí)候,幾何著色器中1次只能獲取到三個(gè)頂點(diǎn)數(shù)據(jù)而姐,因此我們不能考慮到四邊形中的另外一個(gè)頂點(diǎn)數(shù)據(jù)腊凶。

很明顯它們都不是我們想要的結(jié)果,如果我們依賴于導(dǎo)出工具甚至更糟糕的去依賴于一個(gè)運(yùn)行時(shí)庫毅人,這樣分割的三角形我們不能夠控制我們將得到上面的圖片中的那一張吭狡。那我們能做些什么呢?前面講過幾何著色器可以接受GL_LINES_ADJACENCY類型的圖元丈莺,并且其中的每個(gè)圖元都包含四個(gè)頂點(diǎn)划煮,這樣就足夠去描述一個(gè)三角形。這也意味著缔俄,使用臨近線圖元類型弛秋,我們也能得到足夠的信息。

接下來俐载,我們需要處理光柵化器蟹略。前面講過,幾何著色器的輸出只能是點(diǎn)遏佣、線和三角形圖元挖炬,因此我們最好的方式是將每個(gè)四邊形(使用lines_adjacency類型圖元表示)分組一組三角形。你可能會(huì)想這又回到了前面兩個(gè)三角形的老路状婶,但是這種處理方式我們的優(yōu)勢(shì)是能夠傳遞任意我們想要的信息給片段著色器意敛。

為了正確的渲染一個(gè)四邊形,我們必須將我們將要進(jìn)行顏色插值(也可以是其他屬性的插值)的區(qū)域參數(shù)化膛虫。對(duì)于三角形草姻,我們使用重心坐標(biāo)系,然而對(duì)于四邊形稍刀,我們使用2維坐標(biāo)即可撩独。因此我們建立如下坐標(biāo)系。

四邊形坐標(biāo)化后,其中的每個(gè)頂點(diǎn)都可以用一個(gè)二位向量表示综膀。我們可以通過平滑插值的方式在四邊形中找到任意頂點(diǎn)的向量坐標(biāo)澳迫。對(duì)于四邊形的四個(gè)頂點(diǎn)ABCD,它們用向量表示分別為(0,0)剧劝、(0,1)纲刀、(1,0)和(1,1)。我們可以在幾何著色器中為每個(gè)頂點(diǎn)計(jì)算其向量值担平,然后再將它們傳遞給片段著色器示绊。

為了使用向量去追溯其他片段的插值結(jié)果,我們做如下觀察暂论,使用向量的x值使得插值可以分別在AB和CD邊上平滑進(jìn)行面褐,同樣的在垂直方向上使用y值再進(jìn)行一次插值計(jì)算便可以對(duì)四邊形內(nèi)任意一點(diǎn)進(jìn)行插值計(jì)算。

本實(shí)例中的幾何著色器在生成兩個(gè)新的三角形圖元的時(shí)候取胎,為每個(gè)頂點(diǎn)附加四邊形四個(gè)頂點(diǎn)的顏色數(shù)據(jù)展哭,并將其定義為flat類型,指定該屬性在經(jīng)過光柵化器的時(shí)候不會(huì)進(jìn)行插值運(yùn)算闻蛀。同時(shí)將每個(gè)頂點(diǎn)位于四邊形內(nèi)的坐標(biāo)作為一個(gè)普通變量傳遞給片段著色器匪傍。這樣在片段著色器內(nèi)部就可以直接進(jìn)行插值操作。

幾何著色器的代碼如下觉痛。

#version 410 core

layout (lines_adjacency) in;
layout (triangle_strip, max_vertices = 6) out;

in VS_OUT {
    vec4 color;
} gs_in[4];

out GS_OUT {
    flat vec4 color[4];
    vec2 uv;
} gs_out;

void main() {
    gl_Position = gl_in[0].gl_Position;
    gs_out.uv = vec2(1.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[1].gl_Position;
    gs_out.uv = vec2(0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[2].gl_Position;
    gs_out.uv = vec2(0.0, 1.0);
    // We're only writing the output color for the last
    // vertex here because they're flat attributes,
    // and the last vertex is the provoking vertex by default
    gs_out.color[0] = gs_in[0].color;
    gs_out.color[1] = gs_in[1].color;
    gs_out.color[2] = gs_in[2].color;
    gs_out.color[3] = gs_in[3].color;
    EmitVertex();
    
    gl_Position = gl_in[0].gl_Position;
    gs_out.uv = vec2(1.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[2].gl_Position;
    gs_out.uv = vec2(0.0, 1.0);
    EmitVertex();
    gl_Position = gl_in[3].gl_Position;
    gs_out.uv = vec2(1.0, 1.0);
    gs_out.color[0] = gs_in[0].color;
    gs_out.color[1] = gs_in[1].color;
    gs_out.color[2] = gs_in[2].color;
    gs_out.color[3] = gs_in[3].color;
    EmitVertex();
    EndPrimitive();
}

片段著色器的代碼如下役衡。

#version 410 core

in GS_OUT {
    flat vec4 color[4];
    vec2 uv;
} fs_in;

out vec4 color;

void main() {
    vec4 c1 = mix(fs_in.color[0], fs_in.color[1], fs_in.uv.x);
    vec4 c2 = mix(fs_in.color[2], fs_in.color[3], fs_in.uv.x);
    color = mix(c1, c2, fs_in.uv.y);
}

最后渲染的結(jié)果如下圖。本實(shí)例的Demo地址為 8.26-gsquads薪棒。

2.8 多視口轉(zhuǎn)換(Multiple Viewport Transformations)

前面介紹視口轉(zhuǎn)換的時(shí)候提到過使用函數(shù)glViewport()和函數(shù)glDepthRange()可以指定渲染窗口的大小和深度手蝎。通常我們會(huì)將視圖窗口的尺寸設(shè)置為當(dāng)前程序窗口的大小,這取決于我們的程序是否是在整個(gè)程序窗口上運(yùn)行俐芯。此外棵介,我們還可以將單個(gè)幀緩存會(huì)知道我們虛擬分割的多個(gè)窗口中。換句話說吧史,OpenGL允許我們?cè)谕粫r(shí)間將程序窗口分割為多個(gè)更小尺寸的虛擬窗口邮辽,這個(gè)特性被稱為視口數(shù)組(viewport arrays)。

首先我們需要通知OpenGL我們要分割的窗口大小贸营,可以調(diào)用函數(shù)void glViewportIndexedf (GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);和函數(shù)void glViewportIndexe dfv (GLuint index, const GLfloat * v);吨述。兩個(gè)函數(shù)中的參數(shù)index都表示需要修改的視口索引值。需要注意的是這兩個(gè)函數(shù)中的參數(shù)都是GLFLoat類型莽使,和函數(shù)glViewport()中的整形參數(shù)類型不同锐极。OpenGL最少支持16個(gè)視口笙僚,因此視口的索引值可以從0遞增到15芳肌。具體的最大支持視口數(shù)量可以通過GL_MAX_VIEWPORTS和相關(guān)函數(shù)獲取。

同樣的,每個(gè)視口有自己的深度范圍亿笤,可以使用函數(shù)void glDepthRangeIndexed (GLuint index, GLdouble n, GLdouble f);設(shè)置翎迁。實(shí)際上函數(shù)glViewport和函數(shù)glDepthRange設(shè)置了所有視口的尺寸的深度范圍。如果想一次設(shè)置兩個(gè)以上的視口尺寸和深度范圍净薛,就需要調(diào)用函數(shù)void glViewportArrayv (GLuint first, GLsizei count, const GLfloat * v);和函數(shù)void glDepthRangeArrayv (GLuint first, GLsizei count, const GLdouble * v);汪榔。

這兩個(gè)函數(shù)通過第一個(gè)視口的索引值first和數(shù)量count將多個(gè)視口設(shè)置為相同的尺寸,此處的大小有數(shù)組v指定肃拜。對(duì)于函數(shù)glViewportArrayv痴腌,數(shù)組v包含4個(gè)元素,依次分別為x燃领、y士聪、width和height,對(duì)于函數(shù)glDepthRangeArrayv猛蔽,數(shù)組v包含兩個(gè)元素剥悟,分別為n和f,即近平面和遠(yuǎn)平面的距離值曼库。

在指定了視口后需要將幾何體分別繪制到各個(gè)窗口中区岗,這可以通過幾何著色器的多次調(diào)用和內(nèi)置變量gl_ViewportIndex完成。在幾何著色器頭部文件中聲明layout (triangles, invocations = 4) in可以指定為每個(gè)圖元調(diào)用幾何著色器的次數(shù)毁枯,默認(rèn)是1次慈缔。幾何著色器代碼如下。

#version 410 core

// 在幾何著色器內(nèi)部啟用多次調(diào)用顏色值不能正確傳遞進(jìn)入片段著色器种玛,后續(xù)研究
layout (triangles, invocations = 4) in;
layout (triangle_strip, max_vertices = 3) out;

layout (std140) uniform transform_block {
    mat4 mvp_matrix[4];
};

in VS_OUT {
    vec4 color;
} gs_in[];

out GS_OUT {
    vec4 color;
} gs_out;

void main() {
    for (int i = 0; i < gl_in.length(); i++) {
        gl_Position = mvp_matrix[gl_InvocationID] * gl_in[i].gl_Position;
        gs_out.color = gs_in[i].color;
        // 在幾何著色器內(nèi)部啟用多次調(diào)用顏色值不能正確傳遞進(jìn)入片段著色器胀糜,后續(xù)研究
        gs_out.color = vec4(1.0);
        gl_ViewportIndex = gl_InvocationID;
        EmitVertex();
    }
    EndPrimitive();
}

當(dāng)上述著色器執(zhí)行完畢后,該著色器會(huì)被調(diào)用四次蒂誉。在每次調(diào)用的時(shí)候教藻,將內(nèi)置變量gl_InvocationID賦值為gl_InvocationID,就可以將每個(gè)著色器實(shí)例的結(jié)果引導(dǎo)至不同的視口中右锨。同樣的括堤,對(duì)于每一個(gè)調(diào)用,都應(yīng)用單獨(dú)的模型-視圖-投影矩陣绍移,可以通過統(tǒng)一變量閉包獲取悄窃。當(dāng)然,OpenGL允許構(gòu)建更復(fù)雜的著色器蹂窖,但是本實(shí)例已經(jīng)足夠說明如何將多次調(diào)用單個(gè)幾何著色器并將其結(jié)果分別輸入到多個(gè)視口中轧抗。該實(shí)例的渲染結(jié)果如下圖,Demo地址為Chapter 8/8.27-multiViewport瞬测。 但Demo中的多個(gè)立方體都為白色横媚,是因?yàn)閭鬟f顏色發(fā)生未知錯(cuò)誤纠炮,因此直接指定了白色捕捂,暫未發(fā)現(xiàn)具體原因运褪,后續(xù)繼續(xù)研究商膊。

3 總結(jié)(Summary)

在本章中衰伯,學(xué)習(xí)了曲面細(xì)分著色器的2個(gè)可編程階段架馋,不可編程的曲面細(xì)分引擎以及它們的交互方式仇让。此外還學(xué)習(xí)了幾何著色器八孝,同時(shí)了解了曲面細(xì)分著色器和幾何著色器如何組合在一起用于改變OpenGL渲染管道中的頂點(diǎn)數(shù)量欲逃。此外還介紹了在曲面細(xì)分著色器和幾何著色器中可以使用的一些特性问潭。另外介紹了曲面細(xì)分著色器和幾何著色器如果將頂點(diǎn)分組處理猿诸,在曲面細(xì)分著色器中,這些分組構(gòu)成了圖塊(patches)狡忙,在幾何著色器中這些分組構(gòu)成了如線和三角形等傳統(tǒng)圖元两芳。此外,介紹了一組需要幾何著色器才能生效的圖元類型adjacency去枷。在幾何著色器后怖辆,圖元最終被傳遞給光柵化器,然后再處理每個(gè)片段删顶,這將是下一個(gè)章節(jié)討論的內(nèi)容竖螃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市逗余,隨后出現(xiàn)的幾起案子特咆,更是在濱河造成了極大的恐慌,老刑警劉巖录粱,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腻格,死亡現(xiàn)場離奇詭異,居然都是意外死亡啥繁,警方通過查閱死者的電腦和手機(jī)菜职,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旗闽,“玉大人酬核,你說我怎么就攤上這事∈适遥” “怎么了嫡意?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捣辆。 經(jīng)常有香客問我蔬螟,道長,這世上最難降的妖魔是什么汽畴? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任旧巾,我火速辦了婚禮耸序,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘菠齿。我一直安慰自己,他們只是感情好坐昙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布绳匀。 她就那樣靜靜地躺著,像睡著了一般炸客。 火紅的嫁衣襯著肌膚如雪疾棵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天痹仙,我揣著相機(jī)與錄音是尔,去河邊找鬼。 笑死开仰,一個(gè)胖子當(dāng)著我的面吹牛拟枚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播众弓,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恩溅,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了谓娃?” 一聲冷哼從身側(cè)響起脚乡,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎滨达,沒想到半個(gè)月后奶稠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捡遍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年锌订,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片画株。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庸推,到底是詐尸還是另有隱情肘习,我是刑警寧澤投蝉,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布庸娱,位于F島的核電站臣樱,受9級(jí)特大地震影響棚放,放射性物質(zhì)發(fā)生泄漏局骤。R本人自食惡果不足惜凯傲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧甩挫,春花似錦亦渗、人聲如沸搂蜓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽策吠。三九已至逛裤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間猴抹,已是汗流浹背带族。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蟀给,地道東北人蝙砌。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像跋理,于是被迫代替她去往敵國和親择克。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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