Android中的OpenGL使用配置详解
作者:躬行之
引言
PS:虽然感觉好久没更新了,但是输入没有停止,短暂的停止是为了以后更好的更新,共勉。
前面有关 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 的版本
<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
,具体如下:
private class SimpleEGLConfigChooser extends ComponentSizeChooser { public SimpleEGLConfigChooser(boolean withDepthBuffer) { super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); } }
可以通过如下方式修改EGLConfig
的默认行为:
// 设置默认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
来进行设置,具体如下:
// 设置渲染器 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 接口来添加一些自定义行为,具体如下:
// 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
以及制定渲染模式,如下:
// 给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
的基本实现:
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
方法进行渲染绘制,如下:
// 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) } }
渲染器实现如下:
// 渲染器实现 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) } }
上面都是基本的绘制操作,没啥好说的,其中着色器的使用流程会在后续文章中进行介绍,这里就不贴其他代码了,感兴趣的可以直接在文末查看源代码。
绘制效果
上面的绘制没有使用投影矩阵和相机视图来进行坐标转换,当横竖屏切换到时候会到导致变形,这个会在下篇文章中进行修正,看下上述代码绘制的效果图,如下图所示:
以上就是Android中的OpenGL使用配置详解的详细内容,更多关于Android OpenGL配置的资料请关注脚本之家其它相关文章!