记录一下 OpenGL ES Android 开发的入门教程。逻辑性可能不那么强,想到哪写到哪。也可能自己的一些理解有误。
参考资料:
LearnOpenGL CN
Android官方文档
《OpenGL ES应用开发实践指南Android卷》
《OpenGL ES 3.0 编程指南第2版》
目前android 4.3或以上支持opengles 3.0,但目前很多运行android 4.3系统的硬件能支持opengles 3.0的也是非常少的。不过,opengles 3.0是向后兼容的,当程序发现硬件不支持opengles 3.0时则会自动调用opengles 2.0的API。Andorid 中使用 OpenGLES 有两种方式,一种是基于Android框架API, 另一种是基于 Native Development Kit(NDK)使用 OpenGL。本文介绍Android框架接口。
本文写一个最基本的三角形绘制,来说明一下 OpenGL ES 的基本流程,以及注意点。
<!-- Tell the system this app requires OpenGL ES 3.0. --> <uses-feature android:glEsVersion="0x00030000" android:required="true" />
如果程序中使用了纹理压缩的话,还需进行如下声明,以防止不支持这些压缩格式的设备尝试运行程序。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /> <supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
MainActivity.java 代码:
package com.sharpcj.openglesdemo; import android.app.ActivityManager; import android.content.Context; import android.content.pm.ConfigurationInfo; import android.opengl.GLSurfaceView; import android.os.Build; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GLSurfaceView mGlSurfaceView; private boolean mRendererSet; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); if (!checkGlEsSupport(this)) { Log.d(TAG, "Device is not support OpenGL ES 2"); return; } mGlSurfaceView = new GLSurfaceView(this); mGlSurfaceView.setEGLContextClientVersion(2); mGlSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); mGlSurfaceView.setRenderer(new MyRenderer(this)); setContentView(mGlSurfaceView); mRendererSet = true; } @Override protected void onPause() { super.onPause(); if (mRendererSet) { mGlSurfaceView.onPause(); } } @Override protected void onResume() { super.onResume(); if (mRendererSet) { mGlSurfaceView.onResume(); } } /** * 检查设备是否支持 OpenGLEs 2.0 * * @param context 上下文环境 * @return 返回设备是否支持 OpenGLEs 2.0 */ public boolean checkGlEsSupport(Context context) { final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo(); final boolean supportGlEs2 = configurationInfo.reqGlEsVersion >= 0x20000 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && (Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Andorid SDK built for x86"))); return supportGlEs2; } }
关键步骤:
- 创建一个 GLSurfaceView 对象
- 给GLSurfaceView 对象设置 Renderer 对象
- 调用
setContentView()
方法,传入 GLSurfaceView 对象。
创建一个类,实现 GLSurfaceView.Renderer
接口,并实现其中的关键方法
package com.sharpcj.openglesdemo; import android.content.Context; import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import static android.opengl.GLES30.*; public class MyRenderer implements GLSurfaceView.Renderer { private Context mContext; private MyTriangle mTriangle; public MyRenderer(Context mContext) { this.mContext = mContext; } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); mTriangle = new MyTriangle(mContext); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 gl) { glClear(GL_COLOR_BUFFER_BIT); mTriangle.draw(); } }
三个关键方法:
- onSurfaceCreated() - 在View的OpenGL环境被创建的时候调用。
- onSurfaceChanged() - 如果视图的几何形状发生变化(例如,当设备的屏幕方向改变时),则调用此方法。
- onDrawFrame() - 每一次View的重绘都会调用
glViewport(0, 0, width, height); 用于设置视口。
glCrearColor(1.0f, 1.0f, 1.0f, 1.0f) 方法用指定颜色(这里是白色)清空屏幕。
在 onDrawFrame 中调用 glClearColor(GL_COLOR_BUFFER_BIT) ,擦除屏幕现有的绘制,并用之前的颜色清空屏幕。 该方法中一定要绘制一些东西,即便只是清空屏幕,因为该方法调用后会交换缓冲区,并显示在屏幕上,否则可能会出现闪烁。该例子中将具体的绘制封装在了 Triangle 类中的draw
方法中了。
注意:在 windows 版的 OpenGL 中,需要手动调用glfwSwapBuffers(window)
来交换缓冲区。
创建 MyTriangle.java
类:
package com.sharpcj.openglesdemo; import android.content.Context; import com.sharpcj.openglesdemo.util.ShaderHelper; import com.sharpcj.openglesdemo.util.TextResourceReader; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import static android.opengl.GLES30.*; public class MyTriangle { private final FloatBuffer mVertexBuffer; static final int COORDS_PER_VERTEX = 3; // number of coordinates per vertex in this array static final int COLOR_PER_VERTEX = 3; // number of coordinates per vertex in this array static float triangleCoords[] = { // in counterclockwise order: 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right }; private Context mContext; private int mProgram; public MyTriangle(Context context) { mContext = context; // initialize vertex byte buffer for shape coordinates mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertexBuffer.put(triangleCoords); // add the coordinates to the FloatBuffer mVertexBuffer.position(0); // set the buffer to read the first coordinate String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_glsl); String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_glsl); int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode); int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode); mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader); } public void draw() { if (!ShaderHelper.validateProgram(mProgram)) { glDeleteProgram(mProgram); return; } glUseProgram(mProgram); // Add program to OpenGL ES environment // int aPos = glGetAttribLocation(mProgram, "aPos"); // get handle to vertex shader's vPosition member mVertexBuffer.position(0); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(0); // Enable a handle to the triangle vertices // int aColor = glGetAttribLocation(mProgram, "aColor"); mVertexBuffer.position(3); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(1); // Draw the triangle glDrawArrays(GL_TRIANGLES, 0, 3); } }
在该类中,我们使用了,两个工具类:TextResourceReader.java
, 用于读取文件的类容,返回一个字符串,准确说,它与 OpenGL 本身没有关系。
package com.sharpcj.openglesdemo.util; import android.content.Context; import android.content.res.Resources; import android.util.Log; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class TextResourceReader { private static String TAG = "TextResourceReader"; public static String readTextFileFromResource(Context context, int resourceId) { StringBuilder body = new StringBuilder(); InputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader bufferedReader = null; try { inputStream = context.getResources().openRawResource(resourceId); inputStreamReader = new InputStreamReader(inputStream); bufferedReader = new BufferedReader(inputStreamReader); String nextLine; while ((nextLine = bufferedReader.readLine()) != null) { body.append(nextLine); body.append("\n"); } } catch (IOException e) { throw new RuntimeException("Could not open resource: " + resourceId, e); } catch (Resources.NotFoundException nfe) { throw new RuntimeException("Resource not found: " + resourceId, nfe); } finally { closeStream(inputStream); closeStream(inputStreamReader); closeStream(bufferedReader); } return body.toString(); } private static void closeStream(Closeable c) { if (c != null) { try { c.close(); } catch (IOException e) { Log.e(TAG, e.getMessage()); } } } }
ShaderHelper.java
着色器的工具类,这个跟 OpenGL 就有非常大的关系了。
package com.sharpcj.openglesdemo.util; import android.util.Log; import static android.opengl.GLES30.*; public class ShaderHelper { private static final String TAG = "ShaderHelper"; public static int compileVertexShader(String shaderCode) { return compileShader(GL_VERTEX_SHADER, shaderCode); } public static int compileFragmentShader(String shaderCode) { return compileShader(GL_FRAGMENT_SHADER, shaderCode); } private static int compileShader(int type, String shaderCode) { final int shaderObjectId = glCreateShader(type); if (shaderObjectId == 0) { Log.w(TAG, "could not create new shader."); return 0; } glShaderSource(shaderObjectId, shaderCode); glCompileShader(shaderObjectId); final int[] compileStatus = new int[1]; glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0); /*Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId));*/ if (compileStatus[0] == 0) { glDeleteShader(shaderObjectId); Log.w(TAG, "Compilation of shader failed."); return 0; } return shaderObjectId; } public static int linkProgram(int vertexShaderId, int fragmentShaderId) { final int programObjectId = glCreateProgram(); if (programObjectId == 0) { Log.w(TAG, "could not create new program"); return 0; } glAttachShader(programObjectId, vertexShaderId); glAttachShader(programObjectId, fragmentShaderId); glLinkProgram(programObjectId); final int[] linkStatus = new int[1]; glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0); /*Log.d(TAG, "Results of linking program: \n" + glGetProgramInfoLog(programObjectId));*/ if (linkStatus[0] == 0) { glDeleteProgram(programObjectId); Log.w(TAG, "Linking of program failed"); return 0; } return programObjectId; } public static boolean validateProgram(int programId) { glValidateProgram(programId); final int[] validateStatus = new int[1]; glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0); /*Log.d(TAG, "Results of validating program: " + validateStatus[0] + "\n Log: " + glGetProgramInfoLog(programId));*/ return validateStatus[0] != 0; } }
着色器是 OpenGL 里面非常重要的概念,这里我先把代码贴上来,然后来讲流程。
在 res/raw 文件夹下,我们创建了两个着色器文件。
顶点着色器,simple_vertex_shader.glsl
#version 330 layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 out vec3 vColor; // 向片段着色器输出一个颜色 void main() { gl_Position = vec4(aPos.xyz, 1.0); vColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 }
片段着色器, simple_fragment_shader.glsl
#version 330 precision mediump float; in vec3 vColor; out vec4 FragColor; void main() { FragColor = vec4(vColor, 1.0); }
全部的代码就只这样了,具体绘制过程下面来说。运行程序,我们看到效果如下:
一张图说明 OpenGL 渲染过程:
我们看 MyTriangle.java
这个类。
要绘制三角形,我们肯定要定义三角形的顶点坐标和颜色。(废话,不然GPU怎么知道用什么颜色绘制在哪里)。
首先我们定义了一个 float 型数组:
static float triangleCoords[] = { // in counterclockwise order: 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right };
注意:这个数组中,定义了 top, bottom left, bottom right 三个点。每个点包含六个数据,前三个数表示顶点坐标,后三个点表示颜色的 RGB 值。
可能注意到了,因为我们这里绘制最简单的平面二维图像,Z 轴坐标都为 0 ,屏幕中的 X, Y 坐标点都是在(-1,1)的范围。我们没有对视口做任何变换,设置的默认视口,此时的坐标系统是以屏幕正中心为坐标原点。 屏幕最左为 X 轴 -1 , 屏幕最右为 X 轴 +1。同理,屏幕最下方为 Y 轴 -1, 屏幕最上方为 Y 轴 +1。OpenGL 坐标系统使用的是右手坐标系,Z 轴正方向为垂直屏幕向外。
mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertexBuffer.put(triangleCoords);
这一行代码,作用是将数据从 java 堆复制到本地堆。我们知道,在 java 虚拟机内存模型中,数组存在 java 堆中,受 JVM 垃圾回收机制影响,可能会被回收掉。所以我们要将数据复制到本地堆。
首先调用 ByteBuffer.allocateDirect()
分配一块本地内存,一个 float 类型的数字占 4 个字节,所以分配的内存大小为 triangleCoords.length * 4 。
调用 order()
指定字节缓冲区中的排列顺序, 传入 ByteOrder.nativeOrder() 保证作为一个平台,使用相同的排序顺序。
调用 asFloatBuffer()
可以得到一个反映底层字节的 FloatBuffer 类的实例。
最后调用 put(triangleCoords)
把数据从 Android 虚拟机堆内存中复制到本地内存。
接下来,通过 TextResourceReader 工具类,读取顶点着色器和片段着色器文件的的内容。
String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_shader); String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_shader);
然后通过 ShaderHelper 工具类编译着色器。然后链接到程序。
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode); int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode); mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader); ShaderHelper.validateProgram(mProgram);
着色器是一个运行在 GPU 上的小程序。着色器的文件其实定义了变量,并且包含 main 函数。关于着色器的详细教程,请查阅:(LearnOpenGL CN 中的着色器教程)[https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/]
我这里记录一下,着色器的编译过程:
int shaderObjectId = glCreateShader(type);`
创建一个着色器,并返回着色器的句柄(类似java中的引用),如果返回了 0 ,说明创建失败。GLES 中定义了常量,GL_VERTEX_SHADER
和 GL_FRAGMENT_SHADER
作为参数,分别创建顶点着色器和片段着色器。
编译着色器,
glShaderSource(shaderObjectId, shaderCode); glCompileShader(shaderObjectId);
下面的代码,用于获取编译着色器的状态结果。
final int[] compileStatus = new int[1]; glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0); Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId)); if (compileStatus[0] == 0) { glDeleteShader(shaderObjectId); Log.w(TAG, "Compilation of shader failed."); return 0; }
亲测上面的程序在我手上真机可以正常运行,在 genymotion 模拟器中运行报了如下错误:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xfe
网上搜索了一下,这个异常是由于Java虚拟机内部的dalvik/vm/CheckJni.c中的checkUtfString函数抛出的,并且JVM的这个接口明确是不支持四个字节的UTF8字符。因此需要在调用函数之前,对接口传入的字符串进行过滤,过滤函数,可以上网搜到,这不是本文重点,所以我把这个 log 注释掉了
Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId));
编译完着色器之后,需要将着色器连接到程序才能使用。
int programObjectId = glCreateProgram();
创建一个 program 对象,并返回句柄,如果返回了 0 ,说明创建失败。
glAttachShader(programObjectId, vertexShaderId); glAttachShader(programObjectId, fragmentShaderId); glLinkProgram(programObjectId);
将顶点着色器个片段着色器链接到 program 对象。下面的代码用于获取链接的状态结果:
final int[] linkStatus = new int[1]; glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0); /*Log.d(TAG, "Results of linking program: \n" + glGetProgramInfoLog(programObjectId));*/ if (linkStatus[0] == 0) { glDeleteProgram(programObjectId); Log.w(TAG, "Linking of program failed"); return 0; }
在使用 program 对象之前,我们还做了有效性判断:
glValidateProgram(programId); final int[] validateStatus = new int[1]; glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0); /*Log.d(TAG, "Results of validating program: " + validateStatus[0] + "\n Log: " + glGetProgramInfoLog(programId));*/
如果 validateStatus[0] == 0 , 则无效。
首先调用glUseProgram(mProgram)
将 program 对象添加到 OpenGL ES 的绘制环境。
看如下代码:
mVertexData.position(0); // 移动指针到 0,表示从开头开始读取 // 告诉 OpenGL, 可以在缓冲区中找到 a_Position 对应的数据 int aPos = glGetAttribLocation(mProgram, "aPos"); glVertexAttribPointer(aPos, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(aPos); int aColor = glGetUniformLocation(mProgram, "aColor"); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(aColor);
在 OpenGL ES 2.0 中,我们通过如上代码,使用数据。调用 glGetAttribLocation()
方法,找到顶点和颜色对应的数据位置,第一个参数是 program 对象,第二个参数是着色器中的入参参数名。
然后调用 glVertexAttribPointer()
方法
参数如下(图片截取自《OpenGL ES应用开发实践指南Android卷》):
最后调用glEnableVertexAttribArray(aPos);
使 OpenGL 能使用这个数据。
但是你发现,我们上面给的代码中并没有调用 glGetAttribLocation()
方法寻找位置,这是因为,我使用的 OpenGLES 3.0 ,在 OpenGL ES 3.0 中,着色器代码中,新增了 layout(location = 0)
类似的语法支持。
#version 330 layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 out vec3 vColor; // 向片段着色器输出一个颜色 void main() { gl_Position = vec4(aPos.xyz, 1.0); vColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 }
这里已经指明了属性在顶点数组中对应的位置,所以在代码中,可以直接使用 0 和 1 来表示位置。
最后调用 glDrawArrays(GL_TRIANGLES, 0, 3)
绘制出一个三角形。
glDrawArrays() 方法第一个参数指定绘制的类型, OpenGLES 中定义了一些常量,通常有 GL_TRIANGLES , GL_POINTS, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN 等等类型,具体每种类型代表的意思可以查阅API 文档。
VAO : 顶点数组对象
VBO :顶点缓冲对象
通过使用 VAO 和 VBO ,可以建立 VAO 与 VBO 的索引对应关系,一次写入数据之后,每次使用只需要调用 glBindVertexArray
方法即可,避免重复进行数据的复制, 大大提高绘制效率。
int[] VBO = new int[2]; int[] VAO = new int[2]; glGenVertexArrays(0, VAO, 0); glGenBuffers(0, VBO, 0); glBindVertexArray(VAO[0]); glBindBuffer(GL_ARRAY_BUFFER, VBO[0]); glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer, GL_STATIC_DRAW); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4); glEnableVertexAttribArray(1); glBindVertexArray(VAO[0]); glGenVertexArrays(1, VAO, 0); glGenBuffers(1, VBO, 0); glBindVertexArray(VAO[1]); glBindBuffer(GL_ARRAY_BUFFER, VBO[1]); glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer2, GL_STATIC_DRAW); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4); glEnableVertexAttribArray(1); glBindVertexArray(VAO[1]); glBindVertexArray(VAO[0]); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(VAO[1]); glDrawArrays(GL_TRIANGLES, 0, 3);