前面介紹了關于GPU編程的技術點,本章介紹一下關于幾何著色器,幾何著色器是在頂點和片段著色器之間有一個可選的著色器,叫做幾何著色器(Geometry Shader)。幾何著色器以一個或多個表示為一個單獨基本圖形(primitive)的頂點作為輸入,比如可以是一個點或者三角形。幾何著色器在將這些頂點發送到下一個著色階段之前,可以將這些頂點轉變為它認為合適的內容。幾何著色器有意思的地方在于它可以把(一個或多個)頂點轉變為完全不同的基本圖形(primitive),從而生成比原來多得多的頂點。我們直接用一個例子深入了解一下:
#version 330 corelayout (points) in;layout (line_strip, max_vertices = 2) out;void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive();}
每個幾何著色器開始位置我們需要聲明輸入的基本圖形(primitive)類型,這個輸入是我們從頂點著色器中接收到的。我們在in關鍵字前面聲明一個layout標識符。這個輸入layout修飾符可以從一個頂點著色器接收以下基本圖形值:
基本圖形描述
points繪制GL_POINTS基本圖形的時候(1)lines當繪制GL_LINES或GL_LINE_STRIP(2)時lines_adjacencyGL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)trianglesGL_TRIANGLES, GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)triangles_adjacencyGL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)
這是我們能夠給渲染函數的幾乎所有的基本圖形。如果我們選擇以GL_TRIANGLES繪制頂點,我們要把輸入修飾符設置為triangles。括號里的數字代表一個基本圖形所能包含的最少的頂點數。
當我們需要指定一個幾何著色器所輸出的基本圖形類型時,我們就在out關鍵字前面加一個layout修飾符。和輸入layout標識符一樣,輸出的layout標識符也可以接受以下基本圖形值:
- points
- line_strip
- triangle_strip
使用這3個輸出修飾符我們可以從輸入的基本圖形創建任何我們想要的形狀。為了生成一個三角形,我們定義一個triangle_strip作為輸出,然后輸出3個頂點。
幾何著色器同時希望我們設置一個它能輸出的頂點數量的最大值(如果你超出了這個數值,OpenGL就會忽略剩下的頂點),我們可以在out關鍵字的layout標識符上做這件事。在這個特殊的情況中,我們將使用最大值為2個頂點,來輸出一個line_strip。
這種情況,你會奇怪什么是線條:一個線條是把多個點鏈接起來表示出一個連續的線,它最少有兩個點來組成。每個后一個點在前一個新渲染的點后面渲染,你可以看看下面的圖,其中包含5個頂點:

上面的著色器,我們只能輸出一個線段,因為頂點的最大值設置為2。
為生成更有意義的結果,我們需要某種方式從前一個著色階段獲得輸出。GLSL為我們提供了一個內建變量,它叫做gl_in,它的內部看起來可能像這樣:
in gl_Vertex{ vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[];} gl_in[];
這里它被聲明為一個接口塊(interface block,前面的教程已經討論過),它包含幾個有意思的變量,其中最有意思的是gl_Position,它包含著和我們設置的頂點著色器的輸出相似的向量。
要注意的是,它被聲明為一個數組,因為大多數渲染基本圖形由一個以上頂點組成,幾何著色器接收一個基本圖形的所有頂點作為它的輸入。
使用從前一個頂點著色階段的頂點數據,我們就可以開始生成新的數據了,這是通過2個幾何著色器函數EmitVertex和EndPrimitive來完成的。幾何著色器需要你去生成/輸出至少一個你定義為輸出的基本圖形。在我們的例子里我們打算至少生成一個線條(line strip)基本圖形。
void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive();}
每次我們調用EmitVertex,當前設置到gl_Position的向量就會被添加到基本圖形上。無論何時調用EndPrimitive,所有為這個基本圖形發射出去的頂點都將結合為一個特定的輸出渲染基本圖形。一個或多個EmitVertex函數調用后,重復調用EndPrimitive就能生成多個基本圖形。這個特殊的例子里,發射了兩個頂點,它們被從頂點原來的位置平移了一段距離,然后調用EndPrimitive將這兩個頂點結合為一個單獨的有兩個頂點的線條。
現在你了解了幾何著色器的工作方式,你就可能猜出這個幾何著色器做了什么。這個幾何著色器接收一個基本圖形——點,作為它的輸入,使用輸入點作為它的中心,創建了一個水平線基本圖形。如果我們渲染它,結果就會像這樣:

并不是非常引人注目,但是考慮到它的輸出是使用下面的渲染命令生成的就很有意思了:
glDrawArrays(GL_POINTS, 0, 4);
這是個相對簡單的例子,它向你展示了我們如何使用幾何著色器來動態地在運行時生成新的形狀。接下來給讀者介紹幾個復雜一點的案例:
繪制點和線沒什么意思,所以我們將在每個點上使用幾何著色器繪制一個房子。我們可以通過把幾何著色器的輸出設置為triangle_strip來達到這個目的,總共要繪制3個三角形:兩個用來組成方形和另表示一個屋頂。
在OpenGL中三角形帶(triangle strip)繪制起來更高效,因為它所使用的頂點更少。第一個三角形繪制完以后,每個后續的頂點會生成一個毗連前一個三角形的新三角形:每3個毗連的頂點都能構成一個三角形。如果我們有6個頂點,它們以三角形帶的方式組合起來,那么我們會得到這些三角形:(1, 2, 3)、(2, 3, 4)、(3, 4, 5)、(4,5,6)因此總共可以表示出4個三角形。一個三角形帶至少要用3個頂點才行,它能生曾N-2個三角形;6個頂點我們就能創建6-2=4個三角形。下面的圖片表達了這點:
使用一個三角形帶作為一個幾何著色器的輸出,我們可以輕松創建房子的形狀,只要以正確的順序來生成3個毗連的三角形。下面的圖像顯示,我們需要以何種順序來繪制點,才能獲得我們需要的三角形,圖上的藍點代表輸入點:
上圖的內容轉變為幾何著色器:
#version 330 corelayout (points) in;layout (triangle_strip, max_vertices = 5) out;void build_house(vec4 position){ gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f);// 1:左下角 EmitVertex(); gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);// 2:右下角 EmitVertex(); gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f);// 3:左上 EmitVertex(); gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f);// 4:右上 EmitVertex(); gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f);// 5:屋頂 EmitVertex(); EndPrimitive();}void main(){ build_house(gl_in[0].gl_Position);}
這個幾何著色器生成5個頂點,每個頂點是點(point)的位置加上一個偏移量,來組成一個大三角形帶。接著最后的基本圖形被像素化,片段著色器處理整三角形帶,結果是為我們繪制的每個點生成一個綠房子:
可以看到,每個房子實則是由3個三角形組成,都是僅僅使用空間中一點來繪制的。綠房子看起來還是不夠漂亮,所以我們再給每個房子加一個不同的顏色。我們將在頂點著色器中為每個頂點增加一個額外的代表顏色信息的頂點屬性。
下面是更新了的頂點數據:
GLfloat points[] = { -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // 左上 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 右下 -0.5f, -0.5f, 1.0f, 1.0f, 0.0f // 左下};
然后我們更新頂點著色器,使用一個接口塊來項幾何著色器發送顏色屬性:
#version 330 corelayout (location = 0) in vec2 position;layout (location = 1) in vec3 color;out VS_OUT { vec3 color;} vs_out;void main(){ gl_Position = vec4(position.x, position.y, 0.0f, 1.0f); vs_out.color = color;}
接著我們還需要在幾何著色器中聲明同樣的接口塊(使用一個不同的接口名):
in VS_OUT { vec3 color;} gs_in[];
因為幾何著色器把多個頂點作為它的輸入,從頂點著色器來的輸入數據總是被以數組的形式表示出來,即使現在我們只有一個頂點。
我們不是必須使用接口塊來把數據發送到幾何著色器中。我們還可以這么寫:
in vec3 vColor[];
如果頂點著色器發送的顏色向量是out vec3 vColor那么接口塊就會在比如幾何著色器這樣的著色器中更輕松地完成工作。事實上,幾何著色器的輸入可以非常大,把它們組成一個大的接口塊數組會更有意義。
然后我們還要為下一個像素著色階段聲明一個輸出顏色向量:
out vec3 fColor;
因為片段著色器只需要一個(已進行了插值的)顏色,傳送多個顏色沒有意義。fColor向量這樣就不是一個數組,而是一個單一的向量。當發射一個頂點時,為了它的片段著色器運行,每個頂點都會儲存最后在fColor中儲存的值。對于這些房子來說,我們可以在第一個頂點被發射,對整個房子上色前,只使用來自頂點著色器的顏色填充fColor一次:
fColor = gs_in[0].color; //只有一個輸出顏色,所以直接設置為gs_in[0]gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); // 1:左下EmitVertex();gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f); // 2:右下EmitVertex();gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); // 3:左上EmitVertex();gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); // 4:右上EmitVertex();gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); // 5:屋頂EmitVertex();EndPrimitive();
所有發射出去的頂點都把最后儲存在fColor中的值嵌入到他們的數據中,和我們在他們的屬性中定義的頂點顏色相同。所有的分房子便都有了自己的顏色:
為了好玩兒,我們還可以假裝這是在冬天,給最后一個頂點一個自己的白色,就像在屋頂上落了一些雪。
fColor = gs_in[0].color;gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); EmitVertex();gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);EmitVertex();gl_Position = position + vec4(-0.2f, 0.2f, 0.0f, 0.0f); EmitVertex();gl_Position = position + vec4( 0.2f, 0.2f, 0.0f, 0.0f); EmitVertex();gl_Position = position + vec4( 0.0f, 0.4f, 0.0f, 0.0f); fColor = vec3(1.0f, 1.0f, 1.0f);EmitVertex();EndPrimitive();
結果就像這樣:
你可以看到,使用幾何著色器,你可以使用最簡單的基本圖形就能獲得漂亮的新玩意。因為這些形狀是在你的GPU超快硬件上動態生成的,這要比使用頂點緩沖自己定義這些形狀更為高效。幾何緩沖在簡單的經常被重復的形狀比如體素(voxel)的世界和室外的草地上,是一種非常強大的優化工具。
繪制房子的確很有趣,但我們不會經常這么用。這就是為什么現在我們將撬起物體缺口,形成爆炸式物體的原因!雖然這個我們也不會經常用到,但是它能向你展示一些幾何著色器的強大之處。
當我們說對一個物體進行爆破(Explode)的時候并不是說我們將要把之前的那堆頂點炸掉,但是我們打算把每個三角形沿著它們的法線向量移動一小段距離。效果是整個物體上的三角形看起來就像沿著它們的法線向量爆炸了一樣。納米服上的三角形的爆炸式效果看起來是這樣的:
這樣一個幾何著色器效果的一大好處是,它可以用到任何物體上,無論它們多復雜。
因為我們打算沿著三角形的法線向量移動三角形的每個頂點,我們需要先計算它的法線向量。我們要做的是計算出一個向量,它垂直于三角形的表面,使用這三個我們已經的到的頂點就能做到。你可能記得變換教程中,我們可以使用叉乘獲取一個垂直于兩個其他向量的向量。如果我們有兩個向量a和b,它們平行于三角形的表面,我們就可以對這兩個向量進行叉乘得到法線向量了。下面的幾何著色器函數做的正是這件事,它使用3個輸入頂點坐標獲取法線向量:
vec3 GetNormal(){ vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position); vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position); return normalize(cross(a, b));}
這里我們使用減法獲取了兩個向量a和b,它們平行于三角形的表面。兩個向量相減得到一個兩個向量的差值,由于所有3個點都在三角形平面上,任何向量相減都會得到一個平行于平面的向量。一定要注意,如果我們調換了a和b的叉乘順序,我們得到的法線向量就會使反的,順序很重要!
知道了如何計算法線向量,我們就能創建一個explode函數,函數返回的是一個新向量,它把位置向量沿著法線向量方向平移:
vec4 explode(vec4 position, vec3 normal){ float magnitude = 2.0f; vec3 direction = normal * ((sin(time) + 1.0f) / 2.0f) * magnitude; return position + vec4(direction, 0.0f);}
函數本身并不復雜,sin(正弦)函數把一個time變量作為它的參數,它根據時間來返回一個-1.0到1.0之間的值。因為我們不想讓物體坍縮,所以我們把sin返回的值做成0到1的范圍。最后的值去乘以法線向量,direction向量被添加到位置向量上。
爆炸效果的完整的幾何著色器是這樣的,它使用我們的模型加載器,繪制出一個模型:
#version 330 corelayout (triangles) in;layout (triangle_strip, max_vertices = 3) out;in VS_OUT { vec2 texCoords;} gs_in[];out vec2 TexCoords;uniform float time;vec4 explode(vec4 position, vec3 normal) { ... }vec3 GetNormal() { ... }void main() { vec3 normal = GetNormal(); gl_Position = explode(gl_in[0].gl_Position, normal); TexCoords = gs_in[0].texCoords; EmitVertex(); gl_Position = explode(gl_in[1].gl_Position, normal); TexCoords = gs_in[1].texCoords; EmitVertex(); gl_Position = explode(gl_in[2].gl_Position, normal); TexCoords = gs_in[2].texCoords; EmitVertex(); EndPrimitive();}
注意我們同樣在發射一個頂點前輸出了合適的紋理坐標。
也不要忘記在OpenGL代碼中設置time變量:
glUniform1f(glGetUniformLocation(shader.Program, "time"), glfwGetTime()); ```最后的結果是一個隨著時間持續不斷地爆炸的3D模型(不斷爆炸不斷回到正常狀態)。盡管沒什么大用處,它卻向你展示出很多幾何著色器的高級用法。你可以用[完整的源碼](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode)和[著色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode_shaders)對比一下你自己的。# 顯示法向量在這部分我們將使用幾何著色器寫一個例子,非常有用:顯示一個法線向量。當編寫光照著色器的時候,你最終會遇到奇怪的視頻輸出問題,你很難決定是什么導致了這個問題。通常導致光照錯誤的是,不正確的加載頂點數據,以及給它們指定了不合理的頂點屬性,又或是在著色器中不合理的管理,導致產生了不正確的法線向量。我們所希望的是有某種方式可以檢測出法線向量是否正確。把法線向量顯示出來正是這樣一種方法,恰好幾何著色器能夠完美地達成這個目的。思路是這樣的:我們先不用幾何著色器,正常繪制場景,然后我們再次繪制一遍場景,但這次只顯示我們通過幾何著色器生成的法線向量。幾何著色器把一個三角形基本圖形作為輸入類型,用它們生成3條和法線向量同向的線段,每個頂點一條。偽代碼應該是這樣的:```c++shader.Use();DrawScene();normalDisplayShader.Use();DrawScene();
這次我們會創建一個使用模型提供的頂點法線,而不是自己去生成。為了適應縮放和旋轉我們會在把它變換到裁切空間坐標前,使用法線矩陣來法線(幾何著色器用他的位置向量做為裁切空間坐標,所以我們還要把法線向量變換到同一個空間)。這些都能在頂點著色器中完成:
#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec3 normal;out VS_OUT { vec3 normal;} vs_out;uniform mat4 projection;uniform mat4 view;uniform mat4 model;void main(){ gl_Position = projection * view * model * vec4(position, 1.0f); mat3 normalMatrix = mat3(transpose(inverse(view * model))); vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * normal, 1.0)));}
經過變換的裁切空間法線向量接著通過一個接口塊被傳遞到下個著色階段。幾何著色器接收每個頂點(帶有位置和法線向量),從每個位置向量繪制出一個法線向量:
#version 330 corelayout (triangles) in;layout (line_strip, max_vertices = 6) out;in VS_OUT { vec3 normal;} gs_in[];const float MAGNITUDE = 0.4f;void GenerateLine(int index){ gl_Position = gl_in[index].gl_Position; EmitVertex(); gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0f) * MAGNITUDE; EmitVertex(); EndPrimitive();}void main(){ GenerateLine(0); // First vertex normal GenerateLine(1); // Second vertex normal GenerateLine(2); // Third vertex normal}
到現在為止,像這樣的幾何著色器的內容就不言自明了。需要注意的是我們我們把法線向量乘以一個MAGNITUDE向量來限制顯示出的法線向量的大小(否則它們就太大了)。
由于把法線顯示出來通常用于調試的目的,我們可以在片段著色器的幫助下把它們顯示為單色的線(如果你愿意也可以更炫一點)。
#version 330 coreout vec4 color;void main(){ color = vec4(1.0f, 1.0f, 0.0f, 1.0f);}
現在先使用普通著色器來渲染你的模型,然后使用特制的法線可視著色器,你會看到這樣的效果:
除了我們的納米服現在看起來有點像一個帶著隔熱手套的全身多毛的家伙外,它給了我們一種非常有效的檢查一個模型的法線向量是否有錯誤的方式。你可以想象下這樣的幾何著色器也經常能被用在給物體添加毛發上。
Shader代碼如下所示:
// ================// Vertex shader:// ================#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec3 normal;out VS_OUT { vec3 normal;} vs_out;uniform mat4 projection;uniform mat4 view;uniform mat4 model;void main(){ gl_Position = projection * view * model * vec4(position, 1.0f); mat3 normalMatrix = mat3(transpose(inverse(view * model))); vs_out.normal = vec3(projection * vec4(normalMatrix * normal, 1.0));}// ================// Fragment shader:// ================#version 330 coreout vec4 color;void main(){ color = vec4(1.0f, 1.0f, 0.0f, 1.0f);}// ================// Geometry shader:// ================#version 330 corelayout (triangles) in;layout (line_strip, max_vertices = 6) out;in VS_OUT { vec3 normal;} gs_in[];const float MAGNITUDE = 0.2f;void GenerateLine(int index){ gl_Position = gl_in[index].gl_Position; EmitVertex(); gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0f) * MAGNITUDE; EmitVertex(); EndPrimitive();}void main(){ GenerateLine(0); // First vertex normal GenerateLine(1); // Second vertex normal GenerateLine(2); // Third vertex normal}
C++ 核心代碼如下所示:
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Add transformation matrices shader.Use(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(camera.GetViewMatrix())); glm::mat4 model; glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); // Draw model nanosuit.Draw(shader); // Now set transformation matrices for drawing normals normalShader.Use(); glUniformMatrix4fv(glGetUniformLocation(normalShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(camera.GetViewMatrix())); model = glm::mat4(); glUniformMatrix4fv(glGetUniformLocation(normalShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); // And draw model again, this time only drawing normal vectors using the geometry shaders (on top of previous model) nanosuit.Draw(normalShader); // Swap the buffers glfwSwapBuffers(window);
看文倉www.kanwencang.com網友整理上傳,為您提供最全的知識大全,期待您的分享,轉載請注明出處。
歡迎轉載:http://www.kanwencang.com/bangong/20170220/104360.html
文章列表