OpenGL.ES在Android上的简单实践:3-曲棍球(顶点归一化 、增加颜色)
1、顶点归一化
承接上 简单实践系列文章:2。 运行程序后,大家看见了什么,是不是如下图? what the fxxk?!
以上这个问题详细原因很复杂,随着文章深入,答案自然就会迎刃而解;目前,我们来认识一件事,无论是x坐标还是y坐标,OpenGL都会把屏幕映射到 [-1,1]的范围内。这就意味着屏幕的左边对应x轴的-1,而屏幕的右边对应+1,;屏幕的底边会对应y轴的-1,而屏幕的定边就对应+1
不管屏幕是什么形状和大小,这个坐标范围都是一样的,如果我们需要在屏幕上显示任何东西,都需要在这个范围内绘制它们。让我们回归到tableVerticesWithTriangles,重新定义这些坐标点。
float[] tableVerticesWithTriangles = {
// 第一个三角形
-0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
// 第二个三角形
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
// 中间的分界线
-0.5f, 0f,
0.5f, 0f,
// 两个木槌的质点位置
0f, -0.25f,
0f, 0.25f
};
这次又看到什么呢,是不是如下图所示?
这看起来比之前的好多了,但是木槌位置的质点呢?在OpenGL看来,对于点来说,是需要指定在屏幕上所显示的点的大小的。我们修改顶点着色器simple_vertex_shader.glsl 文件,如下:
attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
gl_PointSize = 10.0; //指定点的大小为10.0
}
通过给另外一个特殊的输出变量gl_PointSize赋值,我们告诉OpenGL这些点的大小应该是10。你可能会懊恼,怎么又蹦出了一个特殊变量啊,究竟有多少个这些特殊变量啊。 其实我之前刚开始学的时候也一脸懵逼,随后慢慢的就开始接受了,并整理了一堆shader的内置变量/常量/函数,详细请参考这里面的 “一些基本的glsl概念”。
这次运行程序,效果应该不差了吧。看看是不是下面这个状态:
现在看起来有模有样了,但还是单调了点了,离我们想要的效果还差多着呢~是时候为我们的曲棍球增添色彩了。
2、顶点间完成平滑着色
之前我们了解到uniform里用单一的颜色绘制顶点片段,其实OpenGL还允许我们平滑地混合一条直线或一个三角形的表面上,每个顶点的颜色值。现在计划使用这种平滑着色,使得桌子中心表现得更加明亮,而其他边缘显得比较暗淡,这就好像一盏灯挂在桌子中间的上方一样。然而,在做这些之前,我们需要更新桌子的顶点结构。
现在我们是用两个三角形绘制桌子的,我们怎样才能让中间显得更明亮呢?我们需要在中间位置加入这个点,这样,就可以在桌子的中间和边缘之间混合颜色。所以,我们以下图的形式更新桌子顶点结构吧。
让我们按照上图更新tableVerticesWithTriangles的顶点数据,并修改onDrawFrame的代码
float[] tableVerticesWithTriangles = {
第一个三角形
//-0.5f, -0.5f,
//0.5f, 0.5f,
//-0.5f, 0.5f,
第二个三角形
//-0.5f, -0.5f,
//0.5f, -0.5f,
//0.5f, 0.5f,
// 三角扇
0, 0,
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
-0.5f, -0.5f,
// 中间的分界线
-0.5f, 0f,
0.5f, 0f,
// 两个木槌的质点位置
0f, -0.25f,
0f, 0.25f
};
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//GLES20.glUniform4f(uColorLocation, 1.0f,1.0f,1.0f, 1.0f);
//GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
GLES20.glUniform4f(uColorLocation, 1.0f,1.0f,1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);
GLES20.glUniform4f(uColorLocation, 1.0f,0.0f,0.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);
GLES20.glUniform4f(uColorLocation, 0.0f,0.0f,1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
GLES20.glUniform4f(uColorLocation, 0.0f,1.0f,0.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
}
这里引入三角扇GL_TRIANGLE_FAN标志,从示意图我想大家都能弄懂什么是三角扇了。 一个三角扇形以一个中心顶点作为开始,使用相邻的两个顶点创建第一个三角形,接下来的每个顶点都会创建一个三角形,围绕起始的中心点按扇形展开。为了使这个扇形闭合,我们只需要在最后重复第二个点即可。
之后我们glDrawArrays选择绘制GL_TRIANGLE_FAN,从位标0开始,共6个点。 这时候看看运行状态吧。
通过在桌子中心增加一个额外的点,我们已经更新了桌子的数据结构,现在我们可以给每个点加入一个颜色属性。让我们把整个数据的数组更新如下:
float[] tableVerticesWithTriangles = {
// X, Y, R, G, B
// 三角扇形
0, 0, 1f, 1f, 1f,
-0.5f, -0.5f, 0.7f,0.7f,0.7f,
0.5f, -0.5f, 0.7f,0.7f,0.7f,
0.5f, 0.5f, 0.7f,0.7f,0.7f,
-0.5f, 0.5f, 0.7f,0.7f,0.7f,
-0.5f, -0.5f, 0.7f,0.7f,0.7f,
// 中间的分界线
-0.5f, 0f, 1f, 0f, 0f,
0.5f, 0f, 1f, 0f, 0f,
// 两个木槌的质点位置
0f, -0.25f, 0f, 0f, 1f,
0f, 0.25f, 1f, 0f, 0f,
};
下一步就是从着色器中去掉uniform定义的颜色,并用一个属性替换它。接下来会更新Java代码以体现这段新的着色器代码。
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = a_Position;
gl_PointSize = 20.0;
}
我们加入了一个新的属性a_Color,也加入了一个叫做v_Color的新的varying。 可能会问“varying究竟是什么呀?”还记得我们说过我们需要在一个三角形平面上让颜色产生变化(vary)么? 就是通过这个被称为varying的特殊的变量类型实现的。为了更好地理解一个varying是做什么的,让我回顾一下图元光栅化的过程。
当OpenGL构建一条直线的时候,它会用两个顶点构成这条直线,并为这条直线生成片段;当OpenGL构建一个三角形的时候,它会同样使用三个顶点构建这个三角形。然后,对于每一个被生成的片段,片段着色器都会被执行一次。
varying是一个特殊的变量类型,它把给它的那些值进行混合,并把这些混合后的值发送给片段着色器。如果顶点0的a_Color是红色,且顶点1的a_Color是绿色,然后,通过把a_Color赋值給v_Color,来告诉OpenGL我们需要每个片段都接收一个混合的颜色。接近顶点0的片段,混合后的颜色显得更红,而接近顶点1的片段,颜色就会越绿。
接下来,我们把varying也加入到片段着色器。修改simple_fragment_shader.glsl,修改如下:
precision mediump float;
//uniform vec4 u_Color;
varying vec4 v_Color;
void main()
{
gl_FragColor = v_Color;
}
我们用varying变量v_Color替换了原来的uniform。如果那个片段属于一个三角形,那OpenGL就会用构成那个三角形的三个顶点计算其混合的颜色。
既然已经更新了着色器,我们也需要在Renderer类中更新java代码,以便我们传递新的颜色属性给顶点着色器的a_Color。
private static final int BYTES_PER_FLOAT = 4;
private static final int POSITION_COMPONENT_COUNT = 2;
private static final int COLOR_COMPONENT_COUNT = 3;
private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
//private static final String U_COLOR = "u_Color";
//private int uColorLocation;
private static final String A_POSITION = "a_Position";
private int aPositionLocation;
private static final String A_COLOR = "a_Color";
private int aColorLocation;
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(programId);
// uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);
aColorLocation = GLES20.glGetAttribLocation(programId, A_COLOR);
vertexData.position(0);
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, STRIDE, vertexData);
GLES20.glEnableVertexAttribArray(aPositionLocation);
vertexData.position(2);
GLES20.glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, STRIDE, vertexData);
GLES20.glEnableVertexAttribArray(aColorLocation);
}
这些代码比较重要,因此让我们花点时间仔细地理解每一行代码:
1、首先,我们可以去掉那些旧的常量和与u_Color相关的变量。 然后增加一个名为“STRIDE”的特殊常量,因为我们现在在同一个数据数组里面既有位置又有颜色属性,OpenGL不能再假定下一个位置是紧跟着前一个位置的。一旦OpenGL读入了一个顶点的位置,如果它想读入下一个顶点的位置,它不得不跳过当前顶点的颜色数据。我们要用那个跨距(stride)告诉OpenGL每个位置之间有多少个字节,这样它就知道需要跳过多少了。
2、我们把vertexData的位置设为POSITION_COMPONENT_COUNT,这个值被设为2。为什么执行这一步呢?这是因为,当OpenGL开始读入颜色属性时,我们要它从第一个颜色属性开始,而不是第一个位置属性。当需要跳过第一个位置时,我们就要把位置分量的大小计算在内;把那个位置设置为POSITION_COMPONENT_COUNT,缓冲区的指针就被调整到第一个颜色属性的位置了。
3、接下来,我们调用glVertexAttribPointer把颜色数据和着色器的a_Color关联起来。那个跨距告诉OpenGL每个颜色之间有多少个字节,这样,当需要读入所有顶点的颜色时,它就知道要读取下一个顶点的颜色需要跳过多少个字节了。跨距以字节为单位是非常重要的。 尽管OpenGL中的一种颜色有四个分量(红、绿、蓝和透明度),我们并不必要指定所有分量,OpenGL会用默认值替换属性中未指定值的分量。前三个分量默认为0,第四个分量被设为1。
4、最好,就像前面讲过的位置属性一样,我们要使能颜色属性数组。
我们就剩下最后一件事情了,更新onDrawFrame。既然我们已经把顶点数据和a_Color关联起来了,只需要调用glDrawArrays即可,OpenGL会自动从vertexData数据里读入颜色属性。
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//GLES20.glUniform4f(uColorLocation, 1.0f,1.0f,1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);
//GLES20.glUniform4f(uColorLocation, 1.0f,0.0f,0.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);
//GLES20.glUniform4f(uColorLocation, 0.0f,0.0f,1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
//GLES20.glUniform4f(uColorLocation, 0.0f,1.0f,0.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
}
看看是不是以下效果吧。
小结:
因为我们已经有一个基本的框架,给每一个顶点增加颜色并不太困难。为此,我们给顶点数据和顶点着色器增加了一个新的属性,并且告诉OpenGL如何使用跨距读入数据。接着我们学习了如何使用一个varying在三角形平面上进行插值。
一个需要记住的重要内容:当传递属性数据时,我们要确保给分量计算和跨距(字节位单位)传递正确的值。如果它们错了,我们最终可能会看到一个混乱的屏幕。