学习OpenGL ES for Android(二十四)— 几何着色器


在本章之前还有两章: 高级数据高级GLSL,都是比较理论的知识,有兴趣的话可以自己学习。

简介

几何着色器(Geometry Shader)是一个可选功能,他介于在顶点和片段着色器之间,接收一组顶点数据,可以对数据进行处理,而且可以根据数据生成不止一个图形,假如你想绘制四个顶点,按照以前的方式,需要for循环四次,每次都顶点数据进行处理,最后传入顶点着色器中。而集合着色器就做了这样一件事。在移动平台上,几何着色器需要OpenGL ES 3.2版本(android 7之上),同样的我们可以先参考文档或其他相关资料学习3.0新特性。

3.0变化

这里简单学习一下3.0和2.0比较重要的变化

  1. attribute和varying,取而代之的是 in和out

  2. 文件需要添加#version 300 es (如果是3.2则是320,不加则默认2.0)

  3. 还有纹理 texture2D和texture3D统统改为 texture

  4. 内置函数gl_FragColor和gl_FragData删除,如果片段着色器要输出用out声明字段输出。不过保留了gl_Position

  5. 还有的是layout的作用:可以直接指定位置

    // opengl 2.0
    uniform  float intensity;
    // 代码 赋值
    GLES20.glUniform1f(GLES20.glGetAttribLocation(program, "intensity"), 1f)
    
    //opengl 3.0
    layout (location = 1) uniform  float intensity;
    //直接写上对应的layout的值就可以赋值
    GLES30.glUniform1f(1,1f)
    

几何着色器

绘制点

先看一个几何着色器的例子

#version 320 es
layout (points) in; // 输入
layout (points, max_vertices = 4) out; //输出

in VS_OUT {
    
    
    vec3 color;
} gs_in[];

out vec3 fColor;
void build_point(vec4 position);

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

void build_point(vec4 position){
    
    
    fColor = gs_in[0].color;
    gl_Position = position + vec4(-0.5, 0.5, 0.0, 0.0);// 1:左上角
    gl_PointSize = 20.0;
    EmitVertex();
    gl_Position = position + vec4(0.5, 0.5, 0.0, 0.0);// 2:右上角
    EmitVertex();
    gl_Position = position + vec4(-0.5, -0.5, 0.0, 0.0);// 3:左下角
    gl_PointSize = 10.0;
    EmitVertex();
    gl_Position = position + vec4(0.5, -0.5, 0.0, 0.0);// 4:右下角
    fColor = vec3(1.0, 1.0, 1.0);
    EmitVertex();
    EndPrimitive();
}

这个几何着色器的作用是:输入一个顶点的数据,输出四个顶点显示在屏幕上;前两个点大小为20,后两个点大小为10;前三个点颜色为红色,最后一个是白色。

在几何着色器的顶部,我们需要声明从顶点着色器输入的类型。这需要在in关键字前声明一个布局修饰符(Layout Qualifier)。这个输入布局修饰符可以从顶点着色器接收下列任何一个值:

  • points:绘制GL_POINTS(1)
  • lines:绘制GL_LINES或GL_LINE_STRIP时(2)
  • lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)(3.0新增类型)
  • triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
  • triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)(3.0新增类型)

以上是能提供给glDrawArrays渲染函数的几乎所有图形了。如果我们想要将顶点绘制为GL_GL_POINTS,我们就要将输入修饰符设置为points。括号内的数字表示的是一个图形所包含的最小顶点数。

接下来,我们还需要指定几何着色器输出的类型,这需要在out关键字前面加一个布局修饰符。和输入布局修饰符一样,输出布局修饰符也可以接受几个值:

  • points
  • line_strip
  • triangle_strip

有了这3个输出修饰符,我们就可以使用输入数据创建几乎任意的形状了。要生成一个点的话,我们将输出定义为points,并输出max_vertices个顶点,如果你生成的顶点数大于max_vertices则不会显示。

下面是接口块(参考高级GLSL)VS_OUT的定义,用来接收顶点着色器中传来的数据(当然也可以定义变量来传递,但是用接口块更方便)。

接下来是使用传入的顶点数据,从3.0版本开始GLSL提供了一个内建(Built-in)变量,它的结构大概如下(参考官方文档

 in gl_PerVertex {
	highp vec4 gl_Position;
	highp float gl_PointSize;
} gl_in[];

可以通过gl_in数组来取我们传入的顶点数据,例如传入两个顶点,根据索引取出数据即可(gl_in[0]和gl_in[1])。

EmitVertex方法,指当前一个顶点已经设置完成,包括顶点位置,大小,颜色等等。
EndPrimitive方法,指当前所有顶点这种完成,开始进行绘制。
顶点着色器和片段着色器的代码比较简单,如下

// 顶点着色器用来传递顶点和颜色数据
#version 320 es
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out VS_OUT {
    vec3 color;
} vs_out;

void main(){
    vs_out.color = aColor;
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
}
// 片段着色器
#version 320 es
precision mediump float;

out vec4 FragColor;

in vec3 fColor;

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

我们传入一个顶点屏幕中心(0,0),颜色传入红色,即可得到如下效果图
绘制点

绘制线

我们可以通过对点处理生成线。这里我们传入四个顶点,

points = new float[]{
    
    
	-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  // 左下
};

然后修改几何着色器的代码,每个顶点生成两个点,左顶点x方向减0.1,右顶点x方向加0.1,把输出的类型改为line_strip,最大值为0,代码如下

#version 320 es
layout (points) in;
layout (line_strip, max_vertices = 2) out;

in VS_OUT {
    vec3 color;
} gs_in[];

out vec3 fColor;
void build_line(vec4 position);

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

void build_line(vec4 position){
    fColor = gs_in[0].color;
    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();
}

可绘制出四条线,分别在屏幕的四个角,效果如下
绘制线

绘制房子

一个小房子的样子如下图,
房子
因为几何着色器的三角形输出只有triangle_strip(因为它更节省节点),我们根据传入的顶点分别生成从1到5五个顶点即可绘制成小房子的样式,输入的顶点坐标和绘制线的相同,我们只需要修改几何着色器的代码,输出类型改为triangle_strip,数量为5。修改后的代码如下

#version 320 es
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);

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

void build_house(vec4 position){
    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();
}

效果图如下
房子

爆破物体

物体的爆炸效果,在有些游戏中比较常见,比如当炮弹击中物体时,物体会破碎掉。这样一种效果用几何着色器是可以模拟的(当然效果比较粗糙),我们需要计算绘制的三角形的法向量,然后沿着法向量移动一小段距离,然后根据时间不断的移动或恢复。
计算法向量的代码

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

最后是整个几何着色器的代码,

#version 320 es
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();

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

找出我们之前的模型加载中加载纳米装的代码,传入顶点和纹理,运行代码即看到如下的效果,因为图片被压缩过效果不太好,可以下载源码来查看。
爆炸效果

法向量可视化

在学习光照效果时,处理法向量时可能出现错误,但是因为glsl无法调试,无法找到问题所在。这里我们可以利用几何着色器把法向量可视化,进行调试。
法向量可视化需要先绘制物体,然后绘制法向量,还是以纳米装为例,绘制纳米装不再赘述,绘制法向量则只需要传入顶点和法向量即可,然后在几何着色器中进行处理,根据传入的顶点坐标和经过处理的法向量绘制一条线(每个三角形三条线),代码如下

#version 320 es
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;

in VS_OUT {
    vec3 normal;
} gs_in[];

const float MAGNITUDE = 0.2;
void GenerateLine(int index);

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); // 第三个顶点的法向量
}

最后的效果图如下
法向量可视化
这样就能帮助我们来判断模型的法向量是否正确了,同样可以用来实现其他功能,例如毛发效果。

本章对应的文档地址,可以参考进行理论学习。
本章源码地址

猜你喜欢

转载自blog.csdn.net/jklwan/article/details/104654984