写在前面
记录一下 OpenGL ES Android 开发的入门教程。逻辑性可能不那么强,想到哪写到哪。也可能自己的一些理解有误。
参考资料:
一、前言
目前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 的基本流程,以及注意点。
2.1 创建一个 Android 工程,在 AndroidManifest.xml 文件中声明使用 opengles3.0
<!-- 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" />
2.2 MainActivity 使用 GLSurfaceView
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 对象。
2.3 实现 SurfaceView.Renderer 接口中的方法
创建一个类,实现 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的重绘都会调用
2.4 OpenGL ES 的关键绘制流程
创建 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 绘制过程
一张图说明 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
};
坐标系统
可能注意到了,因为我们这里绘制最简单的平面二维图像,Z 轴坐标都为 0 ,屏幕中的 X, Y 坐标点都是在(-1,1)的范围。我们没有对视口做任何变换,设置的默认视口,此时的坐标系统是以屏幕正中心为坐标原点。 屏幕最左为 X 轴 -1 , 屏幕最右为 X 轴 +1。同理,屏幕最下方为 Y 轴 -1, 屏幕最上方为 Y 轴 +1。OpenGL 坐标系统使用的是右手坐标系,Z 轴正方向为垂直屏幕向外。
3.1 复制数据到本地内存
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 虚拟机堆内存中复制到本地内存。
3.2 编译着色器并链接到程序
接下来,通过 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/]
我这里记录一下,着色器的编译过程:
3.2.1 创建着色器对象
int shaderObjectId = glCreateShader(type);`
创建一个着色器,并返回着色器的句柄(类似java中的引用),如果返回了 0 ,说明创建失败。GLES 中定义了常量,GL_VERTEX_SHADER
和 GL_FRAGMENT_SHADER
作为参数,分别创建顶点着色器和片段着色器。
3.2.2 编译着色器
编译着色器,
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));
3.2.3 将着色器连接到程序
编译完着色器之后,需要将着色器连接到程序才能使用。
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;
}
3.2.4 判断 program 对象是否有效
在使用 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 , 则无效。
3.3 关联属性与顶点数据的数组
首先调用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 来表示位置。
3.4 绘制图形
最后调用 glDrawArrays(GL_TRIANGLES, 0, 3)
绘制出一个三角形。
glDrawArrays() 方法第一个参数指定绘制的类型, OpenGLES 中定义了一些常量,通常有 GL_TRIANGLES , GL_POINTS, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN 等等类型,具体每种类型代表的意思可以查阅API 文档。
四、 OpenGL 中的 VAO 和 VBO。
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);