在顶点着色器之后,片段着色器之前,还有几何着色器,它是可选的,在《OpenGL基础3:渲染管线》这一章中就有提到了,有了几何着色器后可以做很多骚操作,更容易实现很多有意思的效果
一、最简单的几何着色器(Geometry Shader)
几何着色器的输入:一个或多个表示单独基本图形(primitive)的顶点,这个基本图形可以是点、线或者三角形
输出:处理过后的基本图形顶点,这些顶点无论是大小还是位置,甚至是数量、基本图形单元都可以被改变、拼装
最“简单”的几何着色器如下:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT
{
vec2 texIn;
vec3 normalIn;
vec3 fragPosIn;
}vs_in[];
out vec2 texIn;
out vec3 normalIn;
out vec3 fragPosIn;
void main()
{
for (int i = 0; i <= 2; i++)
{
gl_Position = gl_in[i].gl_Position;
texIn = vs_in[i].texIn;
normalIn = vs_in[i].normalIn;
fragPosIn = vs_in[i].fragPosIn;
EmitVertex();
}
EndPrimitive();
}
其实可以看出来,它也不是最简单的几何着色器,因为它接受和输出的顶点都是三角形,而不是点,并且对每个顶点都进行了传输,甚至包括了顶点着色器要传递的值。那之所以拿这个来举例子,是因为它可以应用于链接之前物体的顶点着色器和片段着色器,并且不会对结果进行任何改变
了解了这个几何着色器,就可以很轻松的在这基础上做一些效果了
几何着色器中的 layout 修饰符:
前面两个 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
具体哪一个要和绘制时的 glDrawElements 方法对应
输出的基本图元类型就是以下3种:
- points:点
- line_strip:线
- triangle_strip:三角形
对于上面的样例,就可以理解为输入为最基础的三角形图元,并原样输出
可以看到,第二个layout中还设置了 max_vertices = 3,它限制了输出的顶点数量的最大值,如果超出了这个数值,OpenGL就会忽略剩下的顶点
上面描述的所有东西,都可以用一张图来举例:
已知绘制时填入的基本图形是GL_POINTS,并且顶点着色器会正确的输出上图中5个顶点的位置,那么如何通过几何着色器将这5个顶点按照先后的顺序依次相连成线并输出至片段着色器呢?
那么显然,此时输入的基本图元类型就要定义为points,输出图元类型要定义为line_strip,并且设置最大顶点数量max_vertices = 5,假设错误的设置了max_vertices为2,那么就只会绘制第一段线段,如果错误的设置了输出图元类型或者输入图元类型,那么就会直接报错或得到非常离谱的渲染结果
二、内建变量gl_in[]
这是一个非常重要的内建变量,几何着色器正是从中获得顶点着色器传来的顶点信息的,它也是一个默认的接口块,内容如下:
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
这3个变量也都是顶点着色器的内建变量,前2个在《OpenGL基础39:GLSL内建变量与接口块》这一章有详细介绍,其中gl_Position就是位置信息,大部分情况下也只需要用到它(第三个参数先不管)
它被声明为一个数组,因为大多数渲染基本图形由一个以上顶点组成,几何着色器接收一个基本图形的所有顶点作为它的输入,如果你接受的基本图形是三角形,那么这个数组大小就是3
着色器之间传数据通过 in 和 out 关键字,只需要在当前着色器中声明一个输出,在下一个着色器中声明一个同类型同名字的输入就可以,当然,现在在中间多了一个几何着色器,那么顶点着色器中的数据就不会直接传入片段着色器,而是会被传入几何着色器,那么:
- 在这里几何着色器是中介,要把顶点着色器传入的信息再传入片段着色器
- 有了顶点着色器的这些信息,当然也可以拿来进行数据的处理
正如上面的第一份代码,里面就将顶点着色器的三个数据(接口块),拆掉并传入片段着色器
下面这份代码,就可以将接受的4个顶点,输出为4条线段,每条线段长度为0.2:
glDrawArrays(GL_POINTS, 0, 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);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
三、绘制规则
到这里,几何着色器还有两个非常重要的函数 EmitVertex() 和 EndPrimitive():
- EmitVertex():每当调用一次,当前设置到gl_Position的向量就会被添加到基本图形上,可以理解为输出一个顶点
- EndPrimitive():结束当次图元输出
对于左图,顶点①⑤之间并没有连线,对于右图,顶点①②④并没有连成三角形,这也说明了一点,生成线段和三角形的顺序必须要保证正确
- 对于线段模式:每次EmitVertex(),都会连接当前顶点和上一个顶点
- 对于三角形模式:每次EmitVertex(),都会生成贴着前一个三角形的新三角形,也就是连接当前顶点和上两个顶点
- 只要EndPrimitive(),就会“另起一段”开启下一次绘制,也就是前面说的“上x个顶点”就不存在了,下一个顶点就会是“第一个”顶点,重新开始
也就是说,对于右图顶点的顺序正是数字顺序
四、一个例子
尝试修改下最上面的片段着色器,看下能有什么好玩的效果:
①例如将所有的三角形片元都沿着它的法向量方向移动一段距离:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT
{
vec2 texIn;
vec3 normalIn;
vec3 fragPosIn;
}vs_in[];
out vec2 texIn;
out vec3 normalIn;
out vec3 fragPosIn;
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()
{
for (int i = 0; i <= 2; i++)
{
gl_Position = gl_in[i].gl_Position + vec4(GetNormal(), 1.0) * 0.1f;
texIn = vs_in[i].texIn;
normalIn = vs_in[i].normalIn;
fragPosIn = vs_in[i].fragPosIn;
EmitVertex();
}
EndPrimitive();
}
②又或者新增一套着色器,来实现物体的法向量可视化:
#version 420 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
out VS_OUT
{
vec3 normalIn;
}vs_out;
uniform mat4 model; //模型矩阵
layout (std140, binding = 0) uniform Matrices
{
mat4 view; //观察矩阵
mat4 projection; //投影矩阵
};
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
vs_out.normalIn = mat3(transpose(inverse(model))) * normal;
}
//
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT
{
vec3 normalIn;
}vs_in[];
void main()
{
for (int i = 0; i <= 2; i++)
{
gl_Position = gl_in[i].gl_Position;
EmitVertex();
gl_Position = gl_in[i].gl_Position + vec4(normalize(vs_in[i].normalIn), 0.0f) * 0.2f;
EmitVertex();
EndPrimitive();
}
}
//
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
最后:要改写之前的Shader.h:
#ifndef SHADER_H
#define SHADER_H
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
#include<opengl/glew.h>
class Shader
{
public:
GLuint Program;
Shader(const GLchar* vertexPath, const GLchar* fragmentPath, const GLchar* geometryPath = "")
{
std::string vertexCode;
std::string geometryCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream gShaderFile;
std::ifstream fShaderFile;
vShaderFile.exceptions(std::ifstream::badbit);
gShaderFile.exceptions(std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::badbit);
try
{
vShaderFile.open("Shader/" + (std::string)vertexPath);
fShaderFile.open("Shader/" + (std::string)fragmentPath);
std::stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
vShaderFile.close();
fShaderFile.close();
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
if ((int)((std::string)geometryPath).length() >= 3)
{
std::stringstream gShaderStream;
gShaderFile.open("Shader/" + (std::string)geometryPath);
gShaderStream << gShaderFile.rdbuf();
gShaderFile.close();
geometryCode = gShaderStream.str();
}
}
catch (std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const GLchar* vShaderCode = vertexCode.c_str();
const GLchar* fShaderCode = fragmentCode.c_str();
GLuint vertex, geometry, fragment;
GLint success;
GLchar infoLog[512];
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
geometry = -1;
if (geometryCode.size() != 0)
{
const GLchar* gShaderCode = geometryCode.c_str();
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;
}
}
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
this->Program = glCreateProgram();
glAttachShader(this->Program, vertex);
glAttachShader(this->Program, fragment);
if (geometry != -1)
glAttachShader(this->Program, geometry);
glLinkProgram(this->Program);
glGetProgramiv(this->Program, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(this->Program, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertex);
glDeleteShader(fragment);
if (geometry != -1)
glDeleteShader(geometry);
}
void Use()
{
glUseProgram(this->Program);
}
};
#endif