OpenGL學習:幾何着色器(geometry shader)

除了頂點着色器(vertex shader)和片元着色器(fragment shader),實際上OpenGL還提供了一個可選的幾何着色器(geometry shader)。幾何着色器位於頂點和片元着色器之間,如果沒有使用時,則頂點着色器輸出到片元着色器,在使用幾何着色器後,頂點着色器輸出組成一個基礎圖元的頂點信息到幾何着色器,經過幾何着色器處理後,再輸出到片元着色器。幾何着色器能夠產生0個以上的基礎圖元(primitive),它能起到一定的裁剪作用、同時也能產生比頂點着色器輸入更多的基礎圖元。本節將學習幾何着色器的基本用法,示例代碼均可以從我的github下載

本文整理自: 
www.learnopengl.com Geometry Shader

幾何着色器的基本概念

幾何着色器在啓用後,它將獲得頂點着色器以組成一個基礎圖元爲一組的頂點輸入,通過對輸入的頂點進行處理,幾何着色器將決定輸出的圖元類型和個數。當輸出的圖元減少或者不輸出時,實際上起到了裁剪圖形的作用,當輸出的圖元類型改變或者輸出更多圖元時起到了產生和改變圖元的作用。 
要啓用幾何着色器,我們需要在之前的頂點和片元着色器基礎上,將幾何着色器GL_GEOMETRY_SHADER鏈接到着色器程序上,在代碼上沒有太大改動,你可以從我的github查看這個頭文件。在程序中,我們創建一個包含上述3中着色器的程序:

// 準備着色器程序
Shader shader("scene.vertex", "scene.frag", "scene.gs"); 
  • 1
  • 2

一個直通的幾何着色器

首先從一個基本的直通幾何着色器來了解(以下簡稱gs)。這裏我們繪製4個點,在gs中將這4個點的位置、大小信息原樣輸出到片元着色器。 
頂點着色器如下:

#version 330 core
layout(location = 0) in vec2 position;
void main()
{
    gl_Position = vec4(position, 0.5, 1.0);
    gl_PointSize = 2.8; // 指定點大小 需要在主程序中開啓 glEnable(GL_PROGRAM_POINT_SIZE); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

幾何着色器

#version 330 core
layout(points) in ;
layout(points, max_vertices = 1) out;

// 直通的幾何着色器 原樣輸出
void main()
{
    gl_Position = gl_in[0].gl_Position;
    gl_PointSize = gl_in[0].gl_PointSize;
    EmitVertex();
    EndPrimitive();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

片元着色器

#version 330 core
out vec4 color;
void main()
{
    color = vec4(0.0, 1.0, 0.0, 1.0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

觀察發現,在幾何着色器中in和out分別指示了輸入的圖元,和輸出的圖元等參數。這裏填寫的是類型points表示輸出點。從頂點着色器輸入的圖元類型,映射到幾何着色器的輸入模式如下表所示(參考自OpenGL SuperBible: Comprehensive Tutorial and Reference, 6th Edition):

幾何着色器輸入模式 頂點着色器輸入 頂點最少個數
points GL_POINTS 1
lines GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP 2
triangles GL_TRIANGLES, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP 3
lines_adjacency GL_LINES_ADJACENCY,GL_LINE_STRIP_ADJACENCY 4
triangles_adjacency GL_TRIANGLES_ADJACENCY,GL_TRIANGLE_STRIP_ADJACENCY 6

同時從幾何着色器輸出模式,則有3種:

  • points
  • line_strip
  • triangle_strip

這3種模式基本包含了所有繪圖類型,例如triangle_strip就包含了triangle這種特例。max_vertices表示從幾何着色器最多輸出頂點數目,如果超過設定的這個數目,OpenGL不會輸出多餘的頂點。

在上述幾何着色器中EmitVertex表示輸出一個頂點,而EndPrimitive表示結束一個圖元的輸出,這是一對命令。gl_in是內置輸入變量,定義爲:

in gl_PerVertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這是一個interface block,對這一概念不熟悉的可以回過頭去查看uniform block這一節的內容。定義輸入block爲一個數組,因爲輸入的頂點要組成一個圖元,因此通常不止一個。上面的例子中,使用一個頂點,因此我們使用gl_in[0]來獲取這個頂點的信息。幾何着色器中內置了一個輸出變量,定義如下:

out gl_PerVertex
{
  vec4 gl_Position;
  float gl_PointSize;
  float gl_ClipDistance[];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這是一個沒有使用名字的interface block,因此在着色器中可以直接引用變量名字。

上面的輸入:

   layout(points) in ;
  • 1

表示從頂點着色器輸入GL_POINTS圖元。

輸出語句:

   layout(points, max_vertices = 1) out;
  • 1

表示從幾何着色器輸出points,因爲是一個點,因此max_vertices選項填寫1。

在主程序中,我們指定頂點數據如下:

   // 指定頂點屬性數據 頂點位置
  GLfloat points[] = {
    -0.5f, 0.5f,    // 左上
    0.5f, 0.5f,     // 右上
    0.5f, -0.5f,    // 右下
    -0.5f, -0.5f    // 左下
 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用命令:

   glDrawArrays(GL_POINTS, 0, 4);
  • 1

繪圖後得到4個點的輸出,效果如下圖所示:

原始點

從點到直線

下面我們在着色器中通過將輸入的一個點,產生兩個發生了少許偏移的頂點,而繪製直線,着色器改爲:

   #version 330 core

layout(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);
    gl_PointSize = gl_in[0].gl_PointSize;
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0);
    EmitVertex();
    EndPrimitive();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

得到的效果如下圖所示:

點變直線

點變爲房子圖案

上面產生了4條直線,我們繼續產生一個triangle_strip輸出,計算一個簡單的房子圖案的輸出如下:

   #version 330 core
layout(points) in ;
layout(triangle_strip, max_vertices = 5) out; // 注意輸出類型

void makeHouse(vec4 position)
{
    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);  // 頂部
    EmitVertex();
    EndPrimitive();
}
// 輸出房子樣式三角形帶
void main()
{
    gl_PointSize = gl_in[0].gl_PointSize;
    makeHouse(gl_in[0].gl_Position);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

採用線框模式繪製得到如下圖所示效果:

點變房子圖案

在集合着色器中,我們仍然可以輸出其他變量,例如顏色。我們調整下頂點屬性數據,包含顏色屬性,數據如下:

   // 指定頂點屬性數據 頂點位置 顏色
    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  // 左下
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在頂點着色器中向幾何着色器輸入顏色,更改爲:

   #version 330 core
layout(location = 0) in vec2 position;
layout(location = 1) in vec3 color;
// 定義輸出interface block
out VS_OUT
{
   vec3 vertColor;
}vs_out;
void main()
{
    gl_Position = vec4(position, 0.5, 1.0);
    gl_PointSize = 2.8; 
    vs_out.vertColor = color; 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在幾何着色器中接受顏色輸入,並調整後輸出到片元着色器:

   #version 330 core
layout(points) in ;
layout(triangle_strip, max_vertices = 5) out; // 注意輸出類型
// 定義輸入interface block
in VS_OUT
{
   vec3 vertColor;
}gs_in[];
out vec3 fcolor;
void makeHouse(vec4 position)
{
   fcolor = gs_in[0].vertColor;
gl_PointSize = gl_in[0].gl_PointSize;
   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();
}
// 輸出房子樣式三角形帶
void main()
{
    makeHouse(gl_in[0].gl_Position);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

繪製得到的房子圖案如下所示:

房子圖案

這裏我們可以發現,從4個點的輸入,通過幾何着色器我們構造了4個房子圖案,比原始輸入產生了更多的圖元,在某些場景中,這種方式能夠節省CPU發往GPU的數據,從而節省帶寬。

構造爆炸效果

幾何着色器還能夠產生很多有趣的效果,這裏動手實踐一個爆炸的效果。實現的基本思路是: 將模型的每個三角形,沿着這個三角形的法向量,隨着時間變動,偏移一定的量offset,這個offset>=0.0,則產生了爆炸效果。在幾何着色器中,首先我們需要計算法向量如下:

   // 從輸入的3個頂點 計算法向量
vec3 getNormal(vec4 pos0, vec4 pos1, vec4 pos2)
{
  vec3 a = vec3(pos0) - vec3(pos1);
  vec3 b = vec3(pos2) - vec3(pos1);
  return normalize(cross(a, b));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然後需要對輸入的頂點,沿着法向量方向,偏移一定的量:

   // 計算偏移後的三角形頂點
void explode()
{
  vec3 normal = getNormal(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_in[2].gl_Position);
  float magnitude = ((sin(time) + 1) / 2.0f) * 2.0f; // 使位移偏量保持在[0, 2.0f]範圍內
  vec4 offset = vec4(normal * magnitude, 0.0f);
  gl_Position = gl_in[0].gl_Position + offset;
  TextCoord = gs_in[0].TextCoord; // 頂點和紋理座標每個頂點都不相同
  EmitVertex();
  gl_Position = gl_in[1].gl_Position + offset;
  TextCoord = gs_in[1].TextCoord;
  EmitVertex();
  gl_Position = gl_in[2].gl_Position + offset;
  TextCoord = gs_in[2].TextCoord;
  EmitVertex();
  EndPrimitive();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在主程序中,設置time的uniform變量:

  glUniform1f(glGetUniformLocation(shader.programId, "time"), glfwGetTime());
  • 1

這樣隨着時間變動,我們的模型的三角形頂點將發生位移,而且這個位移是向外的,因此模擬出了爆炸效果,如下圖所示:

爆炸效果

繪製法向量

另外一個有用的技巧是,通過幾何着色器將模型的法向量渲染出來,這樣能夠觀察法向量是否正確,從而排查一些由於法向量指定、計算錯誤而導致的難以調試的錯誤,例如在光照計算中的法向量。

繪製法向量基本思路是: 繪製兩遍,第一遍,用正常着色器渲染模型;第二遍,用包含了產生代表法向量方向直線的着色器再次繪製模型,這次只輸出這些表示法向量的直線。在繪製代表法向量的直線時, 首先通過頂點着色器輸入法向量,這個法向量需要同gl_Position一樣在裁剪座標系下。同時在幾何着色器中,利用輸入的法向量,爲每個三角形的頂點,繪製一個直線表示這個法向量。

計算模型的法向量到裁剪座標系,需要一些技巧,在頂點着色器中實現爲:

// 定義輸出interface block
out VS_OUT
{
  vec3 normal;
}vs_out;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
    // 注意這裏需要向幾何着色器 輸出裁剪座標系下(clip space)法向量
    // 不是世界座標系或者相機座標系下的法向量
    mat3 normalMatrix = mat3(transpose(inverse(view * model)));
    vs_out.normal = normalize( vec3( projection * vec4(normalMatrix * normal, 1.0) ) ); // 注意再次使用normalize
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注意上面代碼中,最後一行的normalize需要再次調用的,否則計算出錯誤的法向量。 如果對於計算法向量不熟悉的話,可以回過頭去查看光照計算裏面的法向量的轉換

在幾何着色器中,根據輸入的法向量,繪製代表法向量的直線:

   #version 330 core

layout(triangles) in ;  // 輸入三角形
layout(line_strip, max_vertices = 6) out;  // 輸出3個代表法向量的直線

// 定義輸入interface block
in VS_OUT
{
   vec3 normal;
}gs_in[];

float magnitude = 0.1f;

// 爲指定索引的頂點產生代表法向量的直線
void generateNormalLine(int index)
{
  gl_Position = gl_in[index].gl_Position;
  EmitVertex();
  vec4 offset = vec4(gs_in[index].normal * magnitude, 0.0f);
  gl_Position = gl_in[index].gl_Position + offset;
  EmitVertex();
  EndPrimitive();
}

// 輸出代表法向量的直線
void main()
{
    generateNormalLine(0);
    generateNormalLine(1);
    generateNormalLine(2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

經過兩次渲染,最終我們得到的效果如下圖所示:

繪製法向量

這個效果可以用來實現模型的毛髮等效果,看起來就像是身上長了毛髮的效果。

值得注意的是,在頂點着色器中計算裁剪座標系中的法向量時,最後一定要再次使用normalize函數,否則計算出的法向量不正確,而導致錯誤的效果,如下圖所示:

法向量計算出錯導致的錯誤效果

最後的說明

本節介紹了幾何着色器的使用,以及基於此實現的一些特效。實際上還有其他的特效和應用,感興趣地可以自行參考GLSL Geometry Shaders這個非常經典的文檔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章