概述
这是一个新的系列,学习OpengGl Es,其实是《OpenGl Es 应用开发实践指南 Android卷》的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为笔记,以防以后忘记
最终是要实现一个曲棍球的简单游戏,类似这样的
增添颜色
在现实世界中,物体有各种各样变色的颜色,我也可以在代码给我们的矩形添加颜色
平滑找色
我们上一篇学习了用uniform用单一的颜色绘制物体,那么我们该如何用许多不同的颜色和找色表达一个复杂的场景呢?
opengl有一个方法,可以在同一个三角形中混合不同的颜色,如果一个三角形每个顶点都有一个不同的颜色,并且在这些三角形的表面混合这些颜色,我们最终得到的是一个平滑找色的三角形
平滑找色是在顶点之间完成的
opengl给了我们一个方法,他可以平滑的混合一个直线和一个三角形的表面上每个顶点的颜色,我们接下来要让我们的矩形表现中间明亮而边缘处比较暗淡,所以我们需要知道矩形的中心点,让我们知道在哪里开始去混合颜色,所以我们要修改这个矩形的结构,拿到中心点
三角形扇
上篇文章定义的顶点
/ //逆时针绘制三角形
float[] tableVertices = {
//第一个三角
-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
};
我们需要把这个顶点改了,改成如下的形式
//逆时针绘制三角形
float[] tableVertices = {
//顶点
0f, 0f,
-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
};
那这个该如何理解呢?
随着这个矩形中心点(0,0)的加入,我们最终看到了四个三角形,而不是俩个,如图:
每个三角形需要3个顶点,但有时候同一个顶点会用于多个三角形,我们看下上面的图片,每条边上的顶点都被俩个三角形使用,而中心顶点被4个三角形使用,我们为了不重复写这些顶点,我们可以采用上面的方式定义顶点,然后告诉opengl可以重用这些顶点,把这些顶点作为三角形扇绘制
一个三角形扇,以中间顶点作为起始,使用相邻俩个顶点创建第一个三角形,接下来每个顶点都会创建一个三角形,围绕起始的中心点按扇形展开,为了使这个扇形闭合,我们需要最后重复绘制第二个点
同时我们也要更新opengl函数,让他知道我们绘制的是三角形扇
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
//更新为
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);
然后运行工程,跟上一篇一样,不过绘制的方式变了
增加新的颜色属性
上面我们已经更新了矩形的结构,现在我们为每一个顶点加入颜色属性,我们把数组更新如下
float[] tableVertices = {
//顶点
0f, 0f,
//顶点颜色值
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,
1f, 0f, 0f,
0f, 0.25f,
0f, 0f, 1f
};
我们为每个顶点增加了三个额外数字,这些数字分别代表红,绿,蓝,他们一起构成对应顶点的颜色
给着色器增加颜色属性
我们先更新顶点着色器vertex_shader.glsl
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
v_Color=a_Color;
gl_Position = a_Position;
gl_PointSize=10.0;
}
这次加入了一个属性a_Color
,和一个varying
的v_Color
当opengl构建一条直线和三角形时,会根据顶点构建对应的图形,然后把图像分解为片段,然后每一个片段都会被片段着色器执行一次
varying
是一个特殊的变量类型,他把给他的那些值进行混合,并把这些值发给片段找色器,假如说一个顶点0为红色,顶点1为绿色,然后通过a_Color赋值给v_Color,来告诉opengl接近顶点0的混合后颜色更红,接近顶点1的颜色变得更绿
下面更新片段找色器fragment_shader1.glsl
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
我们用varying
变量v_Color
替换原来代码中的uniform
,如果这个顶点属于直线那么opengl
会用直线的俩个顶点颜色计算混合后的颜色,如果属于三角形,会利用三角形的三个顶点颜色计算混合后的颜色
varying如何生成每个片段混合后的颜色
现在我们了解到,直线或三角形每个片段的混合后颜色可以用一个varying
生成,我们不仅可以混合颜色,可以给varying传递任何值,opengl都会选择直线顶点的俩个值或三角形三个顶点的三个值,然后平滑的混合这些值,每个片段都会有不同的值,这种混合使用线性差值实现的
一条直线的线性差值
假设我们有一条直线,他有一个红色的顶点,和一个绿色的顶点
直线从左到右:最左边100%红色 0%绿色
,然后向右红色逐渐变淡,直到中间50%红色50%绿色
,继续向右绿色逐渐增强,直到最右端0%红色 100%绿色
一旦我们把俩个颜色叠加在一起,最终就得到一个混合后的直线,这个就是线性差值的基本解释,每种颜色的强度依赖每个片段与包含那个颜色顶点的距离
我们可以用顶点0和顶点1的值,计算出当前片段对应的距离比,距离比是0到100之间的百分比,0%是左边顶点,100%是右边顶点,我们向左向右移动,距离比会0%到100%增加
要用线性差值计算混合值,可以用一下公式
blended_value=(vertex_0_value*(100%-diatance_ratio))+(vertex_1_value*diatance_ratio)
红色(1,0,0),绿色(0,1,0),下图验证这个公式
在三角形表面混合
其实三角形颜色混合,和直线差不多,只不过这次计算方式不太一样
如图所示,对于三角形给的任意点,从这个点向三个顶点画一条直线,就可以生成一个内部三角形,这三个三角形的面积比例,就决定了这个点上每种颜色的权重
这些权重之和也是100%,可以用下面公式计算每个点的颜色分量
blended_value=(vertex_0_value*vertex_0_weight) + (vertex_1_value*vertex_1_weight)+
(vertex_2_value*(100%-vertex_0_weight-vertex_1_weight))
原理是一样的,只不过这次要处理三个点,而不是俩个
使用新的颜色属性渲染
我们上方已经更新了着色器,增加了颜色属性,去掉了uniform,那么我的java代码中也要相应的更新一下
首先我们增加俩个常量
private final int BYTES_PER_FLOAT = 4;
private int POSITION_COMPONENT_COUNT = 2;
//新增
private final int COLOR_COMPONENT_COUNT = 3;
private final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
第一个常量COLOR_COMPONENT_COUNT
表示颜色有三个分量rgb表示
第二常量STRIDE
:我们现在同一个数据数组既有位置属性又有颜色属性,opengl不能假定下一个位置紧跟上一个位置,如果OpenGL读入了顶点数据,那么想要读入下一个顶点数据,他需要跳过颜色数据,所以我们需要跨距(STRIDE)告诉opengl每个位置之间有多少个字节,这样他就知道要跳过多少
然后更新属性顶点
//获取shader属性
a_position = GLES20.glGetAttribLocation(program, "a_Position");
a_color = GLES20.glGetAttribLocation(program, "a_Color");
//绑定a_position和verticeData顶点位置
/**
* 第一个参数,这个就是shader属性
* 第二个参数,每个顶点有多少分量,我们这个只有来个分量
* 第三个参数,数据类型
* 第四个参数,只有整形才有意义,忽略
* 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
* 第六个参数,opengl从哪里读取数据
*/
verticeData.position(0);
GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, STRIDE, verticeData);
//开启顶点
GLES20.glEnableVertexAttribArray(a_position);
//注释1
verticeData.position(POSITION_COMPONENT_COUNT);
GLES20.glVertexAttribPointer(a_color, COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, STRIDE, verticeData);
//开启顶点
GLES20.glEnableVertexAttribArray(a_color);
我们从注释1 开始看
verticeData.position(POSITION_COMPONENT_COUNT);
:opengl开始读入颜色属性时,我们要从第一个颜色属性开始,而不是第一个位置属性,所以我们要跳过第一个位置
接下来调用GLES20.glVertexAttribPointer
把颜色数据和找色器中a_color关联起来,跨距告诉opengl俩个颜色之间的距离
运行一下,可以了自己改下颜色试试
使用Android只Color类转换颜色
当我们使用浮点属性时,我们需要0-1之间每个颜色分量的值,如果我们有一个颜色值,怎么才能拿到对应颜色分量的值呢?
Android中提供了Color类,可以轻易的获取颜色每个分量的值,比如:
float red = Color.red(Color.GREEN) / 255f;
float green = Color.green(Color.GREEN) / 255f;
float blue = Color.blue(Color.GREEN) / 255f;
//网络颜色定义
int parseColor = Color.parseColor("#FFFFFF");
float red = Color.red(parseColor) / 255f;
float green = Color.green(parseColor) / 255f;
float blue = Color.blue(parseColor) / 255f;
Color.red(),Color.green(),Color.blue()
,返回0-255,为了转换为opengl的颜色范围,在除以255就行了
完整代码
public class AirHockKeyRender1 implements GLSurfaceView.Renderer {
private final FloatBuffer verticeData;
private final int BYTES_PER_FLOAT = 4;
private int POSITION_COMPONENT_COUNT = 2;
private final int COLOR_COMPONENT_COUNT = 3;
private final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
private final Context mContext;
//逆时针绘制三角形
//逆时针绘制三角形
float[] tableVertices = {
//顶点
0f, 0f,
//顶点颜色值
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,
1f, 0f, 0f,
0f, 0.25f,
0f, 0f, 1f
};
private int a_position;
private int a_color;
public AirHockKeyRender1(Context context) {
this.mContext=context;
//把float加载到本地内存
verticeData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(tableVertices);
verticeData.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//当surface被创建时,GlsurfaceView会调用这个方法,这个发生在应用程序
// 第一次运行的时候或者从其他Activity回来的时候也会调用
//清空屏幕
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
//读取着色器源码
String fragment_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.fragment_shader1);
String vertex_shader_source = ReadResouceText.readResoucetText(mContext, R.raw.vertex_shader1);
//编译着色器源码
int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
//链接程序
int program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);
//验证opengl对象
ShaderHelper.volidateProgram(program);
//使用程序
GLES20.glUseProgram(program);
//获取shader属性
a_position = GLES20.glGetAttribLocation(program, "a_Position");
a_color = GLES20.glGetAttribLocation(program, "a_Color");
//绑定a_position和verticeData顶点位置
/**
* 第一个参数,这个就是shader属性
* 第二个参数,每个顶点有多少分量,我们这个只有来个分量
* 第三个参数,数据类型
* 第四个参数,只有整形才有意义,忽略
* 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
* 第六个参数,opengl从哪里读取数据
*/
verticeData.position(0);
GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, STRIDE, verticeData);
//开启顶点
GLES20.glEnableVertexAttribArray(a_position);
verticeData.position(POSITION_COMPONENT_COUNT);
GLES20.glVertexAttribPointer(a_color, COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, STRIDE, verticeData);
//开启顶点
GLES20.glEnableVertexAttribArray(a_color);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在Surface创建以后,每次surface尺寸大小发生变化,这个方法会被调用到,比如横竖屏切换
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
//当绘制每一帧数据的时候,会调用这个放方法,这个方法一定要绘制一些东西,即使只是清空屏幕
//因为这个方法返回后,渲染区的数据会被交换并显示在屏幕上,如果什么都没有话,会看到闪烁效果
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//绘制长方形
//指定着色器u_color的颜色为白色
/**
* 第一个参数:绘制绘制三角形
* 第二个参数:从顶点数组0索引开始读
* 第三个参数:读入6个顶点
*
* 最终绘制俩个三角形,组成矩形
*/
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);
//绘制分割线
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);
//绘制点
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
}
}