OpenGL学习笔记(十)-几何着色器-实例化

本文详细介绍了OpenGL中的几何着色器,包括其基本概念、使用方法,展示了如何通过几何着色器创建房屋模型、实现爆破效果以及法线可视化。此外,还探讨了实例化技术,解释了如何通过实例化提高渲染效率,以绘制大量相同物体,如四边形网格和小行星带,阐述了实例化数组的概念和应用。
摘要由CSDN通过智能技术生成

参考网址:LearnOpenGL 中文版

4.7 几何着色器

4.7.1 基本概念

1、顶点和片段着色器之间有一个可选的几何着色器,几何着色器的输入是一个图元(如点或三角形)的一组顶点,顶点发送到下一着色器之前可对它们随意变换,将顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

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

2、在几何着色器的顶部,在in关键字前声明一个布局修饰符,声明从顶点着色器输入的图元类型,括号内的数字表示的是一个图元所包含的最小顶点数。

  • points:绘制GL_POINTS图元时(1)。
  • lines:绘制GL_LINES或GL_LINE_STRIP时(2)
  • lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
  • triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
  • triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)

3、在out关键字前面加一个布局修饰符,指定几何着色器输出的图元类型:

  • points
  • line_strip
  • triangle_strip

4、同时可以设置一个它最大能够输出的顶点数量(超过了这个值,OpenGL将不会绘制多出的顶点),使用上面定义的着色器输出一条线段,最大顶点数等于2:
在这里插入图片描述
5、几何着色器中有一个内建接口块变量gl_in[],包含了几个变量。其中,gl_Position与顶点着色器输出非常相似,它被声明为一个数组,因为几何着色器的输入是一个图元的所有顶点。

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

6、利用几何着色器函数EmitVertexEndPrimitive来生成新的数据了。调用EmitVertex时,gl_Position中的向量会被添加到图元中来。当EndPrimitive被调用时,所有发射出的顶点都会合成为指定的输出渲染图元。在一个或多个EmitVertex调用之后重复调用EndPrimitive能够生成多个图元。

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

7、在这个例子中,接受一个点图元作为输入,从原始顶点位置平移了0.1,发射了两个顶点。之后调用EndPrimitive,将这两个顶点合成为一个包含两个顶点的线条。即以这个点为中心,创建一条水平的线图元。

glDrawArrays(GL_POINTS, 0, 4);

在这里插入图片描述

4.7.2 使用几何着色器

1、在标准化设备坐标的z平面上绘制四个点,包含顶点信息和颜色信息

float 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  // 左下
};

2、顶点着色器在z平面绘制点,使用一个接口块将颜色属性发送到几何着色器中。

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

3、创建一个传递几何着色器,接收点图元,在每个点的位置上绘制一个房子。每个顶点都是原始点的位置加上一个偏移量,来组成一个大的三角形带。使用三角形带(Triangle Strip)绘制,使用的顶点更少。在第一个三角形绘制完之后,每个后续顶点将会在上一个三角形边上生成另一个三角形,每3个临近的顶点将会形成一个三角形,得到(1, 2, 3)、(2, 3, 4)、(3, 4, 5)3个三角形。一个三角形带有N个顶点,会生成N-2个三角形。
在这里插入图片描述

4、几何着色器的输出

  • 将几何着色器的输出设置为triangle_strip
  • 在几何着色器中声明相同的接口块(使用一个不同的接口名VS_OUT )接受颜色属性,因为几何着色器是作用于输入的一组顶点的,从顶点着色器发来输入数据总是会以数组的形式表示出来。
  • 为片段着色器阶段声明一个输出颜色向量fColor,因为片段着色器只需要一个插值的颜色,就是一个单独的向量。
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

in VS_OUT {
    vec3 color;
} gs_in[];

out vec3 fColor;

void build_house(vec4 position)
{    
	fColor = gs_in[0].color; // gs_in[0] 因为只有一个输入顶点
    gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下
    EmitVertex();   
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下
    EmitVertex();
    gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上
    EmitVertex();
    gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上
    EmitVertex();
    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部
    EmitVertex();
    EndPrimitive();
}

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

5、创建着色器时使用GL_GEOMETRY_SHADER作为着色器类型,编译和链接几何着色器

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

在这里插入图片描述
6、也可以将最后一个顶点的颜色设置为白色,给屋顶落上一些雪。

fColor = gs_in[0].color; 
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:左下 
EmitVertex();   
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:右下
EmitVertex();
gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:左上
EmitVertex();
gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:顶部
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();  

在这里插入图片描述

4.7.3 爆破物体

1、爆破一个物体是将每个三角形沿着法向量的方向移动一小段时间,看起来像是沿着每个三角形的法线向量爆炸一样。

2、在几何着色器中,沿着三角形的法向量位移每个顶点,首先使用3个顶点计算法向量,利用3个顶点计算两个平行于三角形表面的向量a和b,对这两个向量进行叉乘来获取法向量了。

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

3、创建一个explode函数,使用法向量和顶点位置向量作为参数,令位置向量沿着法线向量进行位移。sin函数接收一个time参数,它根据时间返回一个-1.0到1.0之间的值。将sin值变换到了[0, 1]的范围内。最终的结果会乘以normal向量,并且最终的direction向量会被加到位置向量上。

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);
}
shader.setFloat("time", glfwGetTime());

4、完整几何着色器,在发射顶点之前输出了对应的纹理坐标。

#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();
}
4.7.4 法线可视化

1、思路是这样的:首先不使用几何着色器正常绘制场景。然后再次绘制场景,但这次只显示通过几何着色器生成法向量。几何着色器接收一个三角形图元,并沿着法向量生成三条线——每个顶点一个法向量。

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

2、使用模型提供的顶点法线,为了适配观察和模型矩阵的缩放和旋转,在将法线变换到裁剪空间坐标之前,先使用法线矩阵变换一次(几何着色器接受的位置向量是剪裁空间坐标,所以应该将法向量变换到相同的空间中)。

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

3、变换后的裁剪空间法向量会以接口块的形式传递到下个着色器阶段。几何着色器会接收每一个顶点,并在每个位置向量处绘制一个法线向量。

#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()
{
    GenerateLine(0); // 第一个顶点法线
    GenerateLine(1); // 第二个顶点法线
    GenerateLine(2); // 第三个顶点法线
}

4、片段着色器将它们显示为单色的线:

#version 330 core
out vec4 FragColor;

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

在这里插入图片描述

4.8 实例化

4.8.1 基本概念

1、之前我们在渲染多个相同的物体时,都是使用循环的方法,但这样很快会因为绘制函数调用过多而达到性能瓶颈。因为OpenGL在绘制顶点数据之前需要做很多准备工作,比如:告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线上进行的。所以即便渲染顶点非常快,命令GPU去渲染却很慢。

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

2、将数据一次性从CPU发送到GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,这就是实例化(Instancing)。

3、实例化使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信。渲染函数为glDrawArraysInstancedglDrawElementsInstanced,函数需要设置渲染的实例个数。

3、利用上述函数渲染的每个物体都是完全相同的,而且还在同一个位置。所以需要利用顶点着色器中的内建变量gl_InstanceID。在使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1,每个实例都有唯一的ID,可以建立一个数组将ID与位置值对应起来,将每个实例放置在世界的不同位置。

4.8.2 实例化绘制

1、绘制100个2D四边形,索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上,得到一个排列整齐的四边形网格。

2、每个四边形由2个三角形所组成,一共有6个顶点。

float quadVertices[] = {
    // 位置          // 颜色
    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
    -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,   
     0.05f,  0.05f,  0.0f, 1.0f, 1.0f                   
};  

3、片段着色器接受颜色向量,并将其设置为它的颜色输出:

#version 330 core
out vec4 FragColor;

in vec3 fColor;

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

4、顶点着色器中定义偏移向量offsets数组,使用gl_InstanceID来索引offsets数组,获取每个实例的偏移向量。

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

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(aPos + offset, 0.0, 1.0);
    fColor = aColor;
}

5、在进入渲染循环之前设置偏移位置,创建100个位移向量,表示10x10网格上的所有位置

glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{
    for(int x = -10; x < 10; x += 2)
    {
        glm::vec2 translation;
        translation.x = (float)x / 10.0f + offset;
        translation.y = (float)y / 10.0f + offset;
        translations[index++] = translation;
    }
}

6、将数据转移到顶点着色器的uniform数组中,将for循环的计数器i转换为一个string,用它来动态创建位置值的字符串,用于uniform位置值的索引。

shader.use();
for(unsigned int i = 0; i < 100; i++)
{
    stringstream ss;
    string index;
    ss << i; 
    index = ss.str(); 
    shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
}

7、使用glDrawArraysInstanced函数:

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

在这里插入图片描述

4.8.3 实例化数组

1、当需要渲染远超过100个实例的时候,会超过最大能够发送至着色器的uniform数据大小上限。因此使用实例化数组,将其定义为一个顶点属性。

2、顶点着色器的每次运行都会获取新一组顶点属性。将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,更新顶点属性的内容。这允许我们对逐顶点的数据使用普通的顶点属性,而对逐实例的数据使用实例化数组。

3、将偏移量uniform数组设置为一个实例化数组。在顶点着色器中添加一个顶点属性:

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

out vec3 fColor;

void main()
{
    gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
    fColor = aColor;
}

4、实例化数组和positioncolor变量一样,都是顶点属性,需要将它的内容存在顶点缓冲对象中,并且配置它的属性指针。首先将translations数组存到一个新的缓冲对象中:

unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

4、然后需要设置它的顶点属性指针,并启用顶点属性。调用了glVertexAttribDivisor函数定义什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数。属性除数是0表示顶点着色器的每次迭代时更新顶点属性。属性除数1表示在渲染一个新实例的时候更新顶点属性。而设置为2时表示每2个实例更新一次属性。

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);   
glVertexAttribDivisor(2, 1);

5、使用gl_InstanceID,从右上到左下逐渐缩小四边形,绘制的第一个四边形的实例会非常小,随着绘制实例的增加,gl_InstanceID会越来越接近100,四边形也就越来越接近原始大小。

void main()
{
    vec2 pos = aPos * (gl_InstanceID / 100.0);
    gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    fColor = aColor;
}

在这里插入图片描述

4.8.4 小行星带

1、利用实例化渲染绘制一个行星带场景,所有的小行星都使用一个模型来表示,再使用不同的变换矩阵来进行少许的变化。

2、为每个小行星生成一个模型矩阵。首先将小行星的x和z位置变换到了一个半径为radius的圆形上,并且在半径的基础上偏移了-offset到offset。让y偏移的影响更小一点,让小行星带更扁平一点。接下来,应用了一个随机的缩放和旋转变换,并将最终的变换矩阵储存在modelMatrices中,这个数组的大小是amount。这里,我们一共生成1000个模型矩阵,每个小行星一个。

unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // 初始化随机种子    
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{
    glm::mat4 model;
    // 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
    float angle = (float)i / (float)amount * 360.0f;
    float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float x = sin(angle) * radius + displacement;
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float z = cos(angle) * radius + displacement;
    model = glm::translate(model, glm::vec3(x, y, z));

    // 2. 缩放:在 0.05 和 0.25f 之间缩放
    float scale = (rand() % 20) / 100.0f + 0.05;
    model = glm::scale(model, glm::vec3(scale));

    // 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
    float rotAngle = (rand() % 360);
    model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

    // 4. 添加到矩阵的数组中
    modelMatrices[i] = model;
}  

2、在顶点着色器中,增加mat4的顶点属性,存储实例化数组的变换矩阵。顶点属性最大允许的数据大小等于一个vec4。因为一个mat4本质上是4个vec4,因此需要为这个矩阵预留4个顶点属性,矩阵每一列的顶点属性位置值就是3、4、5和6。

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;

out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); 
    TexCoords = aTexCoords;
}

3、为这4个顶点属性设置属性指针,并将它们设置为实例化数组:

// 顶点缓冲对象
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    unsigned int VAO = rock.meshes[i].VAO;
    glBindVertexArray(VAO);
    // 顶点属性
    GLsizei vec4Size = sizeof(glm::vec4);
    glEnableVertexAttribArray(3); 
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
    glEnableVertexAttribArray(4); 
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
    glEnableVertexAttribArray(5); 
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
    glEnableVertexAttribArray(6); 
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glVertexAttribDivisor(6, 1);

    glBindVertexArray(0);
}  

4、绘制小行星

instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    glBindVertexArray(rock.meshes[i].VAO);
    glDrawElementsInstanced(
        GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
    );
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值