OpenGL学习23——几何着色器

几何着色器(Geometry Shader)

  • 在顶点和片元着色器之间存在一个可选的着色器阶段称为几何着色器(geometry shader)。一个几何着色器接收组成一个基元的顶点集合作为输入,并在将其发送到下一阶段的着色器前进行合适的变换
  • 下面展示一个几何着色器的例子。
#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标识符,来声明从顶点着色器接收那种类型的基元作为输入。layout限定符可以接受如下基元的类型值:
    • points:绘制GL_POINTS基元。
    • lines:绘制GL_LINESGL_LINE_STRIP
    • lines_adjacencyGL_LINES_ADJACENCYGL_LINE_STRIP_ADJACENCY
    • trianglesGL_TRIANGLESGL_TRIANGLE_STRIPGL_TRIANGLE_FAN
    • triangles_adjacencyGL_TRIANGLES_ADJACENCYGL_TRIANGLES_STRIP_ADJACENCY
  • 几何着色器通过在out关键字之前声明layout标识符来指定输出的基元类型,layout限定符可以接受如下基元的类型值:
    • points
    • line_strip
    • triangle_strip
  • 要输出有意义的结果我们需要以某种方式获取上一着色器阶段的输出,GLSL提供了一个内置变量gl_in,其结构如下所示:
in gl_Vertex
{
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];
  • 注意:上面的结构定义为数组,因为基元一般包含不止一个顶点,几何着色器需要接收基元的所有顶点作为输入。
  • 在几何着色器中,我们可以使用两个函数EmitVertexEndPrimitive,利用从顶点着色器接收的顶点数据来生成新的数据。几何着色器需要你至少生成/输出一个你指定的基元作为输出。
#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();
}
1. 每次调用EmitVertex,当前设置的gl_Position的矢量会被添加到输出的基元中。
2. 无论何时调用EndPrimitive,所有为基元生成的顶点被组合到指定的输出基元。重复调用EndPrimitive可以持续生成多个基元。
  • 使用上面的几何着色器,输入四方形的四个顶点位置数据进行渲染。
glDrawArrays(GL_POINTS, 0, 4);
  • 渲染效果。


    几何着色器渲染:线带

1. 使用几何着色器

  • 要展示如何使用几何着色器,我们来渲染一个简单场景,在z平面上以标准设备坐标绘制四个点,顶点数据如下:
float points[] = {
    -0.5f,  0.5f,   // 左上角
     0.5f,  0.5f,
     0.5f, -0.5f,
    -0.5f, -0.5f,   // 左下角
}
  • 顶点着色器如下:
#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.0f, 1.0f, 0.0f, 1.0f);
}
  • 出于学习目的,我们创建一个称为直通几何着色器,接收一个点基元并直接传递到下一个着色器。
#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;

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

    EndPrimitive();
}
  • 完善我们的着色器类,添加一个包含几何着色器的构造函数,并在实现中将其附加到着色器程序中进行链接。
Shader(const char* vertexPath, const char* geometryPath, const char* fragmentPath);

// 几何着色器对象创建、附加和连接关键步骤:
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
glGetShaderiv(geometry, GL_COMPILE_STATUS, &success);
if (!success)
{
    glGetShaderInfoLog(geometry, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::GEOMETRY::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// 3. link shader program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glAttachShader(ID, geometry);
glLinkProgram(ID);
  • 渲染代码。
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
  • 渲染效果。


    几何着色器渲染:点

2. 构建房子图形

  • 要输出一个房子图形,我们可以将几何着色器的输出设置为triangle_strip并绘制三个三角形,两个构成四方形的房子,一个构造房顶。
  • 在OpenGL中,三角形带是一种更高效地使用更少顶点绘制三角形的方式。在第一个三角形绘制后,每下一个顶点会生成另外一个与第一个三角形相邻的三角形,每三个相邻的顶点都构成一个三角形。如下图所示:(图片取自书中
    三角形带
  • 使用三角形带作为几何着色器的输出,通过以正确的顺序生成三个相邻的三角形,我们可以很容易的创建一个房子图形。下面的图显示,以蓝色点作为输入,我们要创建房子图形所需要的顺序:(图片取自书中
    三角形带:房子图形
  • 上面的房子造型对应的几何着色器如下:
#version 330 core
layput (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);
    EmitVertex();
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}

void main()
{
    build_house(gl_in[0].gl_Position);
}
  • 下面我们使用几何着色器在场景渲染四个不同颜色的房子图形,顶点数据设置如下:
float vertices[] = {
     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 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;
}
  • 这里使用接口模块来传递数据,是因为实际中几何着色器的输入可能变得很大(记住:几何着色器是对基元的顶点集合进行处理),使用接口模块可以更好进行组织。
  • 根据顶点着色器的接口模块声明,我们对前面的几何着色器进行调整,并设置一个输出颜色,传递到片元着色器。
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

in VS_OUT {
    vec3 color;
} gs_in[];

out vec3 fColor;

void main()
{
    fColor = gs_in[0].color;
    gl_Position = gl_in[0].gl_Position + vec4(-0.2, -0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.2, -0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4(-0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.0,  0.4, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}
  • 渲染效果。


    房子图形
  • 对于几何着色器,我们可以将最后生成顶点的颜色修改为白色,让房子看起来像有点积雪的样子。
void main()
{
    fColor = gs_in[0].color;
    gl_Position = gl_in[0].gl_Position + vec4(-0.2, -0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.2, -0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4(-0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.2,  0.2, 0.0, 0.0);
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4( 0.0,  0.4, 0.0, 0.0);
    fColor = vec3(1.0, 1.0, 1.0);
    EmitVertex();

    EndPrimitive();
}
  • 渲染效果。


    积雪房子图形

3. 物体爆炸

  • 我们这里说的物体爆炸是指在很短时间内将每个三角形面片朝它们的法向量移动,产生整个物体的三角形面片像是爆炸了一样。
  • 因为我们要将三角形的顶点朝三角形法向量进行移动,因此我们需要使用三角形的三个顶点来计算三角形的法向量。从变换章节我们知道,我们可以使用叉积(cross product)来计算两个矢量的垂直矢量。下面的方法就是对法向量进行计算。
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的位置,得到的是反方向的法向量,这里顺序很重要。
  • 计算出法向量后,我们就可以设计一个“爆炸”函数,接受顶点位置矢量和法向量参数,输出位置矢量沿法向量移动的新的位置矢量。
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函数的结果转换到[0, 1]之间,然后缩放法向量并保证将移动方向矢量增加到位置矢量上。其中time是uniform变量,我们可以在渲染循环中进行设置。
objectShader.setFloat("time", glfwGetTime());
  • 完整的几何着色器如下:
#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)
{
    float magnitude = 2.0;
    vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
    return position + vec4(direction, 0.0);
}

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

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();
}
  • 渲染效果。


    物体爆炸效果1

    物体爆炸效果2

4. 法向量可视化

  • 下面我们讨论几何着色器一个实际的使用场景:物体法向量可视化。当我们编写光照着色程序,很容易出现奇怪的渲染效果,而常见原因就可能是错误的法向量造成的。要确定我们提供的法向量是否正确,一种很好的方式就是使用几何着色器可视化法向量。
  • 法向量可视化的想法很简单:我首先我们不使用几何着色器绘制常规场景,然后我们使用几何着色器只绘制物体的法向量。几何着色器接收三角形基元作为输入,为每个顶点生成一个法向量。伪代码如下:
shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();
  • 这次我们不再计算法向量,而是直接使用模型提供的法向量数据。因为几何着色器接收的是视空间的位置矢量,我们需要将法向量转换到相同的空间,因此我们在顶点着色器中先对法向量进行转换并通过接口模块将其传递到几何着色器。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out VS_OUT {
    vec3 normal;
} vs_out;

uniform mat4 view;
uniform mat4 model;

void main()
{
    gl_Position = view * model * vec4(aPos, 1.0);
    mat3 normalMatrix = mat3(transpose(inverse(view * model)));
    vs_out.normal = normalize(vec3(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;

uniform mat4 projection;

void GenerateLine(int index)
{
    gl_Position = projection * gl_in[index].gl_Position;
    EmitVertex();
    gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
    EmitVertex();
    EndPrimitive();
}

void main()
{
    GenerateLine(0);
    GenerateLine(1);
    GenerateLine(2);
}
  • 因为可视化法向量一般用于程序调试,因此我们使用单色线显示法向量。片元着色器如下:
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
  • 最后在渲染循环中,我们使用常规着色器绘制模型,使用上述法向量着色器绘制法向量线。效果如下:(马桶刷即视感!!!)


    法向量可视化
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容