计算机图形学(OPENGL):几何着色器

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

几何着色器

  在渲染管线中,位于顶点着色器和片元着色器之间有一个可选的着色器为几何着色器。几何着色器将一系列可以装配为基本体的顶点作为输入,可以对这些顶点进行一些合适的操作再送往下一阶段。几何着色器最有意思的地方在于可以将原来的基本体转化为完全不同的基本体。
  一个几何着色器的例子:

#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); 
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
    EmitVertex();
    
    EndPrimitive();
} 

  在几何着色器的开头我们声明输入基本体的类型(从顶点着色器接收),我们通过在in关键字前用layout说明符声明类型,有以下几种类型可选:

  • points:绘制GL_POINTS基本体。
  • lines:绘制GL_LINES或GL_LINE_STRIP。
  • lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY。
  • triangles:GL_TRIANGLES,GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN。
  • triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY。
      接着声明要输出的基本体类型,我们通过在out关键字前用layout说明符声明类型,有以下几种类型:
  • points
  • line_strip
  • triangle_strip
      作为输出,我们也需要声明每个输出的最大点数,对于line_strip输出,我们可以设置最大点数为2。
      line_strip表明我们连续链接每个点构成线段:



      为了检索上一着色器阶段的结果,GLSL提供内建变量gl_in:

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

  gl_in是一个接口块的实例,内部包含一些变量。注意gl_in是一个数组,这是因为大多数基本体包含一个以上的顶点,几何着色器将一个基本体的所有顶点作为输入。
  对于来自顶点着色器的数据我们可以使用几何着色器的EmitVertex和EndPrimitive方法来生成新的数据。几何着色器要求至少输出一个我们所定义类型的基本体,下面的例子我们生成了一个连续线段基本体:

#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); 
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
    EmitVertex();
    
    EndPrimitive();
}   

  每当我们使用一次EmitVertex,就会在gl_Position位置处生成一个顶点用来作为输出的基本体。在EndPrimitive使用后,所有生成的顶点会装配为一个基本体作为输出。如果不断调用EndPrimitive,那么也会不断使用生成的两个顶点装配基本体。下面是我们调用四次EndPrimitive并作出一些位移的结果:


使用几何着色器

  为了体现几何着色器的用处,我们构建一个场景,绘制下面4个顶点:

float points[] = {
    -0.5f,  0.5f, // top-left
     0.5f,  0.5f, // top-right
     0.5f, -0.5f, // bottom-right
    -0.5f, -0.5f  // bottom-left
};  

  我们创建一个最简单的顶点着色器,将顶点绘制在xy平面上:

#version 330 core
layout (location = 0) in vec2 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
}

  接着在片元着色器中为顶点设置绿色:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(0.0, 1.0, 0.0, 1.0);   
}  

  接着创建VAO和VBO并绘制顶点:

shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);

  如果就这么运行会发现漆黑的场景中有四个绿色的顶点:



  我们接着使用几何着色器:

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

void main() {    
    gl_Position = gl_in[0].gl_Position; 
    EmitVertex();
    EndPrimitive();
}  

  我们在输入的顶点位置处生成一个顶点输出。
  记住几何着色器也需要编译和链接到着色器程序:

geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);  
...
glAttachShader(program, geometryShader);
glLinkProgram(program);  

  运行添加了几何着色器的程序会发现没有什么变化:



  但这也证明我们的几何着色器起了作用。

构建房子

  接下来我们尝试在每个顶点的位置画一些简单的图形,比如一座房子。我们将几何着色器的输出类型改为triangle_strip并绘制三个三角形。
  triangle_strip是一种很有效地绘制三角形的方式。绘制一个三角形后会在这个三角形的基础上邻接绘制另一个三角形,即每3个邻接的顶点会组成一个三角形。比如我们总共有6个顶点,我们就会得到下面的三角形顺序:(1,2,3),(2,3,4),(3,4,5),(4,5,6),总共构成4个三角形。一个triangle_strip至少需要3个顶点,对于N个顶点的triangle_strip总共可绘制N-2个三角形。对于6个顶点,结果如下:



  那么对于房子的形状,我们可以在输入顶点的基础上生成5个顶点,并绘制3个三角形:



  那么我们需要按顺序生成顶点,几何着色器代码如下:
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

void build_house(vec4 position)
{    
    gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:bottom-left
    EmitVertex();   
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:bottom-right
    EmitVertex();
    gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:top-left
    EmitVertex();
    gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:top-right
    EmitVertex();
    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:top
    EmitVertex();
    EndPrimitive();
}

void main() {    
    build_house(gl_in[0].gl_Position);
}  

  我们对每个生成的顶点添加一些偏移量,最终构成一个房子的形状。(由于我们传入的基本体为点,只有一个顶点,所以使用gl_in[0])。
  最终结果如下(左侧是绘制三角形,右侧是线框显示):



  为了给房子增添一些色彩,我们给每个顶点赋予一个颜色向量属性:

float points[] = {
    // xy坐标      // 颜色
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // top-left
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // top-right
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-right
    -0.5f, -0.5f, 1.0f, 1.0f, 0.0f  // bottom-left
};  

  顶点着色器进行以下修改,可以使用我们之前讲过的接口块:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out VS_OUT {
    vec3 color;
} vs_out;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    vs_out.color = aColor;
}  

  同样在几何着色器中也要定义同样接口块的:

in VS_OUT {
    vec3 color;
} gs_in[];  

  我们也要定义一个颜色向量作为输出传入片元着色器:

out vec3 fColor;  

  和接着在生成顶点基本体前我们为fColor赋值:

fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:bottom-left   
EmitVertex();   
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:bottom-right
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:top-left
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:top-right
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:top
EmitVertex();
EndPrimitive();  

  所有生成的顶点将会使用输入顶点的颜色信息,结果如下:



  当然,我们也可以在任意一个生成的顶点前为fColor赋值,比如为最后一个顶点赋值白色:

fColor = gs_in[0].color; 
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:bottom-left   
EmitVertex();   
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:bottom-right
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:top-left
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:top-right
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:top
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();  

  运行结果如下:


  这里给出原文代码参考:Code

炸开物体

  这里的炸开物体并不是真的要让某个物体爆炸,我们将一个模型的每个三角形在一段时间内都沿法线偏移一段距离来实现这种感觉,大概是这样:



  为了实现这一目的,我们需要计算每个三角形的法线,通过三个顶点的位置可以很简单地计算出来,通过构成两个向量接着叉乘得到结果。下面再几何着色器器中实现这一方法:

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

  接下来实现炸开方法:

vec4 explode(vec4 position, vec3 normal)
{
    float magnitude = 2.0;
    vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; 
    return position + vec4(direction, 0.0);
} 

  我们在计算沿法线方向的偏移时,使用了sin方法,以一个统一变量time作为参数,+1/2将范围修改到0-1。
  下面是几何着色器中的实现:

#version 330 core
layout (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();
}  

  注意我们在生成每个顶点时重新赋予了纹理坐标。
  在设置统一变量time后,我们就可以炸开模型了。
  这里给出原文代码参考:Code

绘制法线

  在进行灯光着色时,常见的错误原因时法线计算错误,我们可以将法线绘制出来,以此判断是否是法线出了问题,我们在几何着色器中设置法线。
  做法是:先不使用几何着色器绘制场景,接着使用几何着色器再绘制一边场景来显示法线。用代码实现大概是这样:

shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();

  我们在顶点着色器中使用模型自带的法线信息。为适应缩放和旋转的影响(view和model),我们先使用常规的手段变换法线再将其转置切割空间。这是因为几何着色器接受来自切割空间的坐标所以我们也要将法线转置切割空间。顶点着色器代码:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out VS_OUT {
    vec3 normal;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0); 
    mat3 normalMatrix = mat3(transpose(inverse(view * model)));
    vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0)));

  我们通过接口块传递法线,在几何着色器中这样实现:

#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;

in VS_OUT {
    vec3 normal;
} gs_in[];

const float MAGNITUDE = 0.4;

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.0) * MAGNITUDE;
    EmitVertex();
    EndPrimitive();
}

void main()
{
![](https://upload-images.jianshu.io/upload_images/18672981-8c4bef7d303fed22.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    GenerateLine(0); // first vertex normal
    GenerateLine(1); // second vertex normal
    GenerateLine(2); // third vertex normal
}  

  每个法线的绘制为2个顶点,第一个顶点为原三角形顶点位置,第二个顶点我们沿原顶点的法线方向偏移一段距离,我们通过MAGNITUDE来限制显示法线的大小。
  我们用黄色显示法线:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}  

  最后结果如下:


  这里给出原文代码参考:Code
  最后,给出原文地址供参考:https://learnopengl.com/Advanced-OpenGL/Geometry-Shader

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352