Android中的OpenGL

前面有关 Android 音视频的渲染都是使用MediaCodec进行渲染，MediaCodec也有自己的弊端比如无法进行视频的编辑处理，而视频可以 OpenGL ES 来进行渲染，可以很好进行处理，比如添加滤镜等，这里介绍下 Android 中 OpenGL，也就是 OpenGL ES，它是免费、跨平台的、功能完善的 2D/3D 图形库接口 API，他针对多种嵌入式系统进行了专门设计，它是一个精心提取出来的 OpenGL 的子集，主要内容如下：

介绍

GLSurfaceView

渲染器 Renderer

坐标映射

绘制三角形

绘制效果

介绍

Android 可通过开放图形库 OpenGL ES 来支持高性能 2D 和 3D 图形，OpenGL 是一种跨平台的图形 API，用于为 3D 图形处理硬件指定标准的软件接口。OpenGL ES 是 OpenGL 规范的一种形式，适用于嵌入式设备，Android 支持多版 OpenGL ES API，各版本情况如下：

OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。

OpenGL ES 2.0 - 此 API 规范受 Android 2.2（API 级别 8）及更高版本的支持。

OpenGL ES 3.0 - 此 API 规范受 Android 4.3（API 级别 18）及更高版本的支持。

OpenGL ES 3.1 - 此 API 规范受 Android 5.0（API 级别 21）及更高版本的支持。

在 AndroidManifest.xml 中声明 OpenGL ES 的版本

Copy
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

GLSurfaceView

GLSurfaceView是SurfaceView的 OpenGL实现，从 Android 1.5 开始加入，在 SurfaceView的基础上添加了 EGL 的管理以及自带的渲染线程 GLThread，其主要功能如下：

管理一个Surface，这个Surface是一块特殊的内存，可以组合到 Android 的 View 系统中，也就是可以和View一起使用。

管理一个 EGL，这个EGL可以让 OpenGL 渲染到这个Surface上，EGL是 Android 与 OpenGL之间的桥梁。

支持用户自定义渲染器Renderer对象。

使用专用线程上进行渲染。

支持按需渲染 (on-demand) 和连续渲染 (continuous )。

Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls.

EGL 窗口、OpenGL 表面、GL 表面含义都相同。

GLSurfaceView常用设置如下：

EGL 配置

EGLConfigChooser的默认实现是SimpleEGLConfigChooser，默认情况下GLSurfaceView将选择深度缓冲深度至少为 16 位的PixelFormat.RGB_888格式的 surface，默认的EGLConfigChooser实现是SimpleEGLConfigChooser，具体如下：

Copy
private class SimpleEGLConfigChooser extends ComponentSizeChooser {
public SimpleEGLConfigChooser(boolean withDepthBuffer) {
super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0);
}
}

可以通过如下方式修改EGLConfig的默认行为：

Copy
// 设置默认EGLConfig的深度缓冲，true则为16位的深度缓冲
setEGLConfigChooser(boolean needDepth)
// 指定自定义的EGLConfigChooser
setEGLConfigChooser(android.opengl.GLSurfaceView.EGLConfigChooser configChooser)
// 指定各个分量的值
public void setEGLConfigChooser(int redSize, int greenSize, int blueSize,
int alphaSize, int depthSize, int stencilSize)

渲染

通过setRenderer设置渲染器并启动渲染线程GLThread，渲染模式有两种如下：

RENDERMODE_CONTINUOUSLY：适合重复渲染的场景，默认的渲染模式。

RENDERMODE_WHEN_DIRTY：只有Surface被创建后渲染一次，只调用了requestRender才会继续渲染。

渲染模式可以通过setRenderMode来进行设置，具体如下：

Copy
// 设置渲染器
public void setRenderer(Renderer renderer)
// 设置渲染模式，仅在setRenderer之后调用生效
public void setRenderMode(int renderMode)

setDebugFlags 和 setGLWrapper

setDebugFlags用于设置 Debug 标记，方便调试跟踪代码，可选值为DEBUG_CHECK_GL_ERROR和DEBUG_LOG_GL_CALLS，setGLWrapper可以通过自定义GLWrapper来委托 GL 接口来添加一些自定义行为，具体如下：

Copy
// DEBUG_CHECK_GL_ERROR：每次GL调用都会检查，如果出现glError则会抛出异常
// DEBUG_LOG_GL_CALLS：以TAG为GLSurfaceView将日志记录在verbose级别的日志中
setDebugFlags(int debugFlags)
// 用于调试跟踪代码，可自定义GLWrapper包装GL接口并返回GL接口，可在
setGLWrapper(android.opengl.GLSurfaceView.GLWrapper glWrapper)

渲染器 Renderer

这部分在前面提到过，这里单独说一下，要想在 GL 表面上执行渲染操作，需要实现Renderer对象完成实际渲染操作，通过如下方式给GLSurfaceView设置渲染器对象Renderer以及制定渲染模式，如下：

Copy
// 给GLSurfaceView设置渲染器对象Renderer
public void setRenderer(Renderer renderer)
// 设置渲染模式，仅在setRenderer之后调用生效
public void setRenderMode(int renderMode)

设置渲染器Renderer的时候，同时会创建独立线程GLThread并开启该线程，这个线程就是独立于 UI 线程的渲染线程。

这里就涉及到两个线程 UI 线程和渲染线程，自然涉及到线程之间的通信，可以使用 volatile 和 synchronized等实现线程之间的通信。

如果是在 UI 线程中调用渲染线程中的操作，可以使用GLSurfaceView的 queueEvent 方法来将该操作执行到渲染线程中，一般需要自定义GLSurfaceView的时候会用到，同样如果在渲染线程可以通过runOnUiThread来将与 UI 相关的操作执行到 UI 线程。

下面看下渲染器Reander的基本实现：

Copy
public class GLES20Renderer implements Renderer {
private static final String TAG = GLES20Renderer.class.getSimpleName();

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i(TAG, "onSurfaceCreated");
GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1);
}

public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.i(TAG, "onSurfaceChanged");
GLES20.glViewport(0, 0, width, height);
}

public void onDrawFrame(GL10 gl) {
Log.i(TAG, "onDrawFrame");
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
}
}

坐标映射

先来了解下 OpenGL 的世界坐标系和与之对应的 Android 上的纹理坐标系，如下图所示：

在 Android 中使用 OpenGL 就要进行相应坐标的转换，下面看下 OpenGL 坐标系在 Android 屏幕中的映射关系，如下图所示：

如上图所示，左侧是默认的 OpenGL 坐标系，右侧是 OpenGL 坐标系在 Android 屏幕上的映射，可以明显看到图中的三角形是变形了的，为了保证图像比例就需要应用 OpenGL 投影模式和相机视图来转换坐标，这就涉及到投影矩阵和视图矩阵，这部分内容会在后续的文章中介绍。

绘制三角形

通过以上内容，Android OpenGL 算是初步入门了，按照习惯来个小案例，这里使用 OpenGL 绘制一个三角形，如下Triangle是三角形数据封装及着色器的的使用，后续渲染直接调用draw方法进行渲染绘制，如下：

Copy
// Triangle
class Triangle(context: Context) {
companion object {
// 坐标数组中每个顶点的坐标数
private const val COORDINATE_PER_VERTEX = 3
}

private var programHandle: Int = 0
private var positionHandle: Int = 0
private var colorHandler: Int = 0
private var vPMatrixHandle: Int = 0
private var vertexStride = COORDINATE_PER_VERTEX * 4

// 三角形的三条边
private var triangleCoordinate = floatArrayOf( // 逆时针的顺序的三条边
0.0f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
)

// 颜色数组
private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
private var vertexBuffer: FloatBuffer =
// (number of coordinate values * 4 bytes per float)
ByteBuffer.allocateDirect(triangleCoordinate.size * 4).run {
// ByteBuffer使用本机字节序
this.order(ByteOrder.nativeOrder())
// ByteBuffer to FloatBuffer
this.asFloatBuffer().apply {
put(triangleCoordinate)
position(0)
}
}

init {
// read shader sourceCode
val vertexShaderCode = GLUtil.readShaderSourceCodeFromRaw(context, R.raw.vertex_shader_triangle_default)
val fragmentShaderCode =
GLUtil.readShaderSourceCodeFromRaw(context, R.raw.fragment_shader_triangle)
if (vertexShaderCode.isNullOrEmpty() || fragmentShaderCode.isNullOrEmpty()) {
throw RuntimeException("vertexShaderCode or fragmentShaderCode is null or empty")
}
// compile shader
val vertexShaderHandler = GLUtil.compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShaderHandler =
GLUtil.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// create and link program
programHandle = GLUtil.createAndLinkProgram(vertexShaderHandler, fragmentShaderHandler)
}

/**
* 绘制方法
*/
fun draw(mvpMatrix: FloatArray) {
GLES20.glUseProgram(programHandle)
// 获取attribute变量的地址索引
// get handle to vertex shader's vPosition member
positionHandle = GLES20.glGetAttribLocation(programHandle, "vPosition").also {
// enable vertex attribute，默认是disable
GLES20.glEnableVertexAttribArray(it)
GLES20.glVertexAttribPointer(
it, // 着色器中第一个顶点属性的位置
COORDINATE_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride, // 连续的顶点属性组之间的间隔
vertexBuffer
)
}
// get handle to fragment shader's vColor member
colorHandler = GLES20.glGetUniformLocation(programHandle, "vColor").also {
GLES20.glUniform4fv(it, 1, color, 0)
}
// draw triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, triangleCoordinate.size / COORDINATE_PER_VERTEX)
GLES20.glDisableVertexAttribArray(positionHandle)
}
}

渲染器实现如下：

Copy
// 渲染器实现
class MRenderer(private var context: Context) : GLSurfaceView.Renderer {
private val tag = MRenderer::class.java.simpleName
private lateinit var triangle: Triangle
private val vPMatrix = FloatArray(16) // 模型视图投影矩阵
private val projectionMatrix = FloatArray(16)
private val viewMatrix = FloatArray(16)
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 创建Surface时调用，在渲染开始时调用，用来创建渲染开始时需要的资源
Log.d(tag, "onSurfaceCreated")
triangle = Triangle(context)
}

override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// Surface改变大小时调用，设置视口
Log.d(tag, "onSurfaceChanged")
GLES20.glViewport(0, 0, width, height)
}

override fun onDrawFrame(gl: GL10?) {
// 绘制当前frame，用于渲染处理具体的内容
Log.d(tag, "onDrawFrame")
triangle.draw(vPMatrix)
}
}

上面都是基本的绘制操作，没啥好说的，其中着色器的使用流程会在后续文章中进行介绍，这里就不贴其他代码了，感兴趣的可以直接在文末查看源代码。

绘制效果

上面的绘制没有使用投影矩阵和相机视图来进行坐标转换，当横竖屏切换到时候会到导致变形，这个会在下篇文章中进行修正，看下上述代码绘制的效果图，如下图所示：

如需要留言关键字【OpenGL】获取源代码。