写在前面

记录一下 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 ES 入门-LMLPHP

三、OpenGL 绘制过程

OpenGL ES 入门-LMLPHP

一张图说明 OpenGL 渲染过程:

OpenGL ES 入门-LMLPHP

我们看 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 Getting started/05 Shaders/]

我这里记录一下,着色器的编译过程:

3.2.1 创建着色器对象

int shaderObjectId = glCreateShader(type);`

创建一个着色器,并返回着色器的句柄(类似java中的引用),如果返回了 0 ,说明创建失败。GLES 中定义了常量,GL_VERTEX_SHADERGL_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卷》):

OpenGL ES 入门-LMLPHP

最后调用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(2, VAO, 0);
glGenBuffers(2, 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]); 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);
05-06 19:59