一.视图组件 GLSurfaceView
Android上用于显示OpenGL视图,一般是使用GLSurfaceView,一个继承自SurfaceView的组件。
它的渲染绘制在一个单独的线程中,而非主线程。
GLSurfaceView一般是结合一个GLSurfaceView的内部接口类Renderer来使用。Renderer类负责渲染图形图像,而GLSurfaceView负责触摸事件等逻辑的处理。
Renderer接口
- onSurfaceCreated(GL10 gl, EGLConfig config):GLSurfaceView内的Surface被创建时会被调用到
- onSurfaceChanged(GL10 gl, int width, int height):Surface尺寸改变时调用到
- onDrawFrame(GL10 gl):渲染绘制每一帧时调用到
所以一般情况下,首次创建GLSurfaceView时,会按顺序调用onSurfaceCreated、onSurfaceChanged、onDrawFrame这3个方法,然后每绘制一帧,都会不停地回调onDrawFrame方法。
GLSurfaceView常用方法
- setEGLContextClientVersion:设置OpenGL ES版本,2.0则设置2
- onPause:暂停渲染,最好是在Activity、Fragment的onPause方法内调用,减少不必要的性能开销,避免不必要的崩溃
- onResume:恢复渲染,用法类比onPause
- setRenderer:设置渲染器
- setRenderMode:设置渲染模式
- requestRender: 请求渲染,由于是请求异步线程进行渲染,所以不是同步方法,调用后不会立刻就进行渲染。渲染会回调到Renderer接口的onDrawFrame方法。
- queueEvent:插入一个Runnable任务到后台渲染线程上执行。相应的,渲染线程中可以通过Activity的runOnUIThread的方法来传递事件给主线程去执行
GLSurfaceView渲染模式
- RENDERMODE_CONTINUOUSLY:不停地渲染
- RENDERMODE_WHEN_DIRTY:只有调用了requestRender之后才会触发渲染回调onDrawFrame方法
二.编程流程
- 编写GLSL:重点学习
- 编译GLSL,获取OpenGL程序对象:基本固定,不需要死记,理解即可。后期会进行封装,便于使用。
- 获取GLSL中变量的引用:理解调用方式
- 通过内存Buffer,将数据传递给变量引用,从而控制绘制图形、颜色:重点学习
1. 简单的GLSL
/**
* 顶点着色器
*/
private static final String VERTEX_SHADER = "" +
// vec4:4个分量的向量:x、y、z、w
"attribute vec4 a_Position;\n" +
"void main()\n" +
"{\n" +
// gl_Position:GL中默认定义的输出变量,决定了当前顶点的最终位置
" gl_Position = a_Position;\n" +
// gl_PointSize:GL中默认定义的输出变量,决定了当前顶点的大小
" gl_PointSize = 40.0;\n" +
"}";
/**
* 片段着色器
*/
private static final String FRAGMENT_SHADER = "" +
// 定义所有浮点数据类型的默认精度;有lowp、mediump、highp 三种,但只有部分硬件支持片段着色器使用highp。(顶点着色器默认highp)
"precision mediump float;\n" +
"uniform mediump vec4 u_Color;\n" +
"void main()\n" +
"{\n" +
// gl_FragColor:GL中默认定义的输出变量,决定了当前片段的最终颜色
" gl_FragColor = u_Color;\n" +
"}";
注意
在声明vec向量的时候,一定要标识其精度类型,否则会导致部分机型花屏,如红米note2
2.1 编译着色器
使用compileVertexShader、compileFragmentShader两个方法分别调用上面定义的顶点着色器、片段着色器。
/**
* 编译顶点着色器
*
* @param shaderCode 编译代码
* @return 着色器对象ID
*/
public static int compileVertexShader(String shaderCode) {
return compileShader(GLES20.GL_VERTEX_SHADER, shaderCode);
}
/**
* 编译片段着色器
*
* @param shaderCode 编译代码
* @return 着色器对象ID
*/
public static int compileFragmentShader(String shaderCode) {
return compileShader(GLES20.GL_FRAGMENT_SHADER, shaderCode);
}
/**
* 编译片段着色器
*
* @param type 着色器类型
* @param shaderCode 编译代码
* @return 着色器对象ID
*/
private static int compileShader(int type, String shaderCode) {
// 1.创建一个新的着色器对象
final int shaderObjectId = GLES20.glCreateShader(type);
// 2.获取创建状态
if (shaderObjectId == 0) {
// 在OpenGL中,都是通过整型值去作为OpenGL对象的引用。之后进行操作的时候都是将这个整型值传回给OpenGL进行操作。
// 返回值0代表着创建对象失败。
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new shader.");
}
return 0;
}
// 3.将着色器代码上传到着色器对象中
GLES20.glShaderSource(shaderObjectId, shaderCode);
// 4.编译着色器对象
GLES20.glCompileShader(shaderObjectId);
// 5.获取编译状态:OpenGL将想要获取的值放入长度为1的数组的首位
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (LoggerConfig.ON) {
// 打印编译的着色器信息
Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
+ GLES20.glGetShaderInfoLog(shaderObjectId));
}
// 6.验证编译状态
if (compileStatus[0] == 0) {
// 如果编译失败,则删除创建的着色器对象
GLES20.glDeleteShader(shaderObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Compilation of shader failed.");
}
// 7.返回着色器对象:失败,为0
return 0;
}
// 7.返回着色器对象:成功,非0
return shaderObjectId;
}
2.2 创建OpenGL程序对象,链接顶点着色器、片段着色器
/**
* 创建OpenGL程序对象
*
* @param vertexShader 顶点着色器代码
* @param fragmentShader 片段着色器代码
*/
protected void makeProgram(String vertexShader, String fragmentShader) {
// 步骤1:编译顶点着色器
int vertexShaderId = ShaderHelper.compileVertexShader(vertexShader);
// 步骤2:编译片段着色器
int fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShader);
// 步骤3:将顶点着色器、片段着色器进行链接,组装成一个OpenGL程序
mProgram = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId);
if (LoggerConfig.ON) {
ShaderHelper.validateProgram(mProgram);
}
// 步骤4:通知OpenGL开始使用该程序
GLES20.glUseProgram(mProgram);
}
/**
* 创建OpenGL程序:通过链接顶点着色器、片段着色器
*
* @param vertexShaderId 顶点着色器ID
* @param fragmentShaderId 片段着色器ID
* @return OpenGL程序ID
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
// 1.创建一个OpenGL程序对象
final int programObjectId = GLES20.glCreateProgram();
// 2.获取创建状态
if (programObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new program");
}
return 0;
}
// 3.将顶点着色器依附到OpenGL程序对象
GLES20.glAttachShader(programObjectId, vertexShaderId);
// 3.将片段着色器依附到OpenGL程序对象
GLES20.glAttachShader(programObjectId, fragmentShaderId);
// 4.将两个着色器链接到OpenGL程序对象
GLES20.glLinkProgram(programObjectId);
// 5.获取链接状态:OpenGL将想要获取的值放入长度为1的数组的首位
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (LoggerConfig.ON) {
// 打印链接信息
Log.v(TAG, "Results of linking program:\n"
+ GLES20.glGetProgramInfoLog(programObjectId));
}
// 6.验证链接状态
if (linkStatus[0] == 0) {
// 链接失败则删除程序对象
GLES20.glDeleteProgram(programObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Linking of program failed.");
}
// 7.返回程序对象:失败,为0
return 0;
}
// 7.返回程序对象:成功,非0
return programObjectId;
}
/**
* 验证OpenGL程序对象状态
*
* @param programObjectId OpenGL程序ID
* @return 是否可用
*/
public static boolean validateProgram(int programObjectId) {
GLES20.glValidateProgram(programObjectId);
final int[] validateStatus = new int[1];
GLES20.glGetProgramiv(programObjectId, GLES20.GL_VALIDATE_STATUS, validateStatus, 0);
Log.v(TAG, "Results of validating program: " + validateStatus[0]
+ "\nLog:" + GLES20.glGetProgramInfoLog(programObjectId));
return validateStatus[0] != 0;
}
3. 获取GLSL中的索引
根据索引的类型,调用不同的方法去获取索引,索引的值类型都是int
// 获取顶点坐标属性在OpenGL程序中的索引
aPositionLocation = GLES20.glGetAttribLocation(mProgram, A_POSITION);
// 获取颜色Uniform在OpenGL程序中的索引
uColorLocation = GLES20.glGetUniformLocation(mProgram, U_COLOR);
4.1 将数据传递到Native层内存缓冲中
/**
* Float类型占4Byte
*/
private static final int BYTES_PER_FLOAT = 4;
/**
* 创建一个FloatBuffer
*/
public static FloatBuffer createFloatBuffer(float[] array) {
FloatBuffer buffer = ByteBuffer
// 分配顶点坐标分量个数 * Float占的Byte位数
.allocateDirect(array.length * BYTES_PER_FLOAT)
// 按照本地字节序排序
.order(ByteOrder.nativeOrder())
// Byte类型转Float类型
.asFloatBuffer();
// 将Java Dalvik的内存数据复制到Native内存中
buffer.put(array);
return buffer;
}
4.2 将内存堆中的值传递给GLSL引用
接下来,我们把顶点信息传递给GLSL中的顶点位置引用
// 将缓冲区的指针移动到头部,保证数据是从最开始处读取
mVertexData.position(0);
// 关联顶点坐标属性和缓存数据
// 1. 位置索引;
// 2. 每个顶点属性需要关联的分量个数(必须为1、2、3或者4。初始值为4。);
// 3. 数据类型;
// 4. 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)(只有使用整数数据时)
// 5. 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。
// 6. 数据缓冲区
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, 0, mVertexData);
// 通知GL程序使用指定的顶点属性索引
GLES20.glEnableVertexAttribArray(aPositionLocation);
然后,我们给图形上色
// 更新u_Color的值,即更新画笔颜色
GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
最后,再根据需求绘制不同的图形。当前案例中,我就只绘制一个点。
// 使用数组绘制图形:1.绘制的图形类型;2.从顶点数组读取的起点;3.从顶点数组读取的数据长度
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);
注意:这里一定要先上色,再绘制图形,否则会导致颜色在当前这一帧使用失败,要下一帧才能生效。
刷屏颜色
// 设置刷新屏幕时候使用的颜色值,顺序是RGBA,值的范围从0~1。这里不会立刻刷新,只有在GLES20.glClear调用时使用该颜色值才刷新。
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// 使用glClearColor设置的颜色,刷新Surface
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
注意
Buffer数据在传递给GLSL之前,一定要调用position方法将指针移到正确的位置,当前是0,之后会有课程讲解到非0的情况。
// 将缓冲区的指针移动到头部,保证数据是从最开始处读取
mVertexData.position(0);
将数组数据put进buffer之后,指针并不是在首位,所以一定要position到0,至关重要!否则会有很多奇妙的错误!如:
java.lang.ArrayIndexOutOfBoundsException: remaining() < count < needed
效果
参考
见Android OpenGL ES学习资料所列举的博客、资料。
GitHub代码工程
本系列课程所有相关代码请参考我的GitHub项目GLStudio。
课程目录
本系列课程目录详见 简书 - Android OpenGL ES教程规划