OpenGL ES for Android(几何着色器)
简介
几何着色器(Geometry Shader)是一个可选功能,他介于在顶点和片段着色器之间,接收一组顶点数据,可以对数据进行处理,而且可以根据数据生成不止一个图形,假如你想绘制四个顶点,按照以前的方式,需要for循环四次,每次都顶点数据进行处理,最后传入顶点着色器中。而集合着色器就做了这样一件事。在移动平台上,几何着色器需要OpenGL ES 3.2版本(android 7之上),同样的我们可以先参考文档或其他相关资料学习3.0新特性。
3.0变化
这里简单学习一下3.0和2.0比较重要的变化
-
attribute和varying,取而代之的是 in和out
-
文件需要添加#version 300 es (如果是3.2则是320,不加则默认2.0)
-
还有纹理 texture2D和texture3D统统改为 texture
-
内置函数gl_FragColor和gl_FragData删除,如果片段着色器要输出用out声明字段输出。不过保留了gl_Position
-
还有的是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); // 第三个顶点的法向量
}
最后的效果图如下:
这样就能帮助我们来判断模型的法向量是否正确了,同样可以用来实现其他功能,例如毛发效果。
本章对应的文档地址,可以参考进行理论学习。
本章源码地址
有看过文章的朋友可以选择性点个赞,关注下,相互学习。