前言
学习Android音视频绝对绕不开OpenGL ES,Android设备可以利用OpenGL ES来支持高性能的2D和3D图形,它是一个图形渲染API。
本篇文章记录一下用OpenGL ES展示一张图片的一次实战,看似简单的操作,包含了大量的OpenGL ES的操作,有兴趣可以一起敲一下代码。为方便起见,下面对OpenGL ES简称OpenGL。
搭建UI
平时一般使用ImageView
来加载并展示图片,而在这里我们使用的是TextureView
来展示,为方便演示,使用Compose代码如下所示,也可以使用View模式。
我们需要利用的是TextureView
的SurfaceTexture
,而SurfaceTexture
初始化需要时间,可以传入一个TextureView.SurfaceTextureListener
监听器,在SurfaceTexture
准备好的时候加载图片:
@Composable
fun OpenGLImageScreen(
modifier: Modifier = Modifier
) {
val viewModel = viewModel<OpenGLImageScreenViewModel>()
AndroidView(
factory = { TextureView(it) },
modifier = modifier
) { textureView ->
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture, width: Int, height: Int
) {
viewModel.loadLittleCatImage(surface)
}
...
}
}
}
复制代码
ViewModel
中声明一个loadLittleCatImage
函数,这个函数干了四件事情:
- 在这个函数中使用TextureView的
SurfaceTexture
完成EGL资源的构建。 - 构建Texture2D纹理并加载图片。
- 将Texture2D纹理中的内容绘制到
EGLSurface
。 - 将
EGLSurface
中的内容展示到显示设备中。
伪代码如下所示
fun loadLittleCatImage(textureView: TextureView) {
// 构建EGL资源
eglSystem = EglSystem.createSurface(textureView.surfaceTexture!!)
// 进入OpenGL上下文
eglSystem.withCurrent {
// 构建Texture2D纹理并加载图片
imageId = GLUtils.generate2DTextureId(bitmap.width, bitmap.height, byteBuffer)
// 将Texture2D纹理绘制到EGLSurface中
shader.drawFrom(imageId, 0, 0, textureView.width, textureView.height)
// 将EGLSurface的内容冲刷到显示设备中
eglSystem.swapBuffers()
}
}
复制代码
构建EGL资源
首先需要构建的是EGL资源,EGL可以简单理解为OpenGL和设备窗口的中间层。
EGL资源拥有四个重要的构件。
- EGLDisplay:连接到特定显示设备的一个抽象类,是EGL系统中的主要入口点,主要用于控制图形渲染的上下文和资源。
- EGLSurface:图形数据将要渲染的目标表面,可以是屏幕或窗口表面,也可以是帧缓冲区或纹理。
- EGLContext:表示OpenGL的上下文,存储当前图形状态,包括当前着色器、矩阵、纹理和缓冲区等。
一、获取EGLDisplay
可以通过eglGetDisplay
函数获取EGLDisplay
,而这个函数是获取EGLDisplay
的入口点,在创建、配置、管理EGL上下文和资源前需要调用这个函数。
val eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
复制代码
二、初始化EGLDisplay
val versions = IntArray(2)
EGL14.eglInitialize(eglDisplay, versions, 0, versions, 1)
复制代码
传入的四个参数,第一个为刚刚获取的eglDisplay,第二个为存储EGL版本的数组,第三个为指向存储EGL主版本的指针,最后一个为指向存储EGL次版本的指针。
三、选择配置
private val attributesForSurface = intArrayOf(
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 0,
EGL14.EGL_STENCIL_SIZE, 0,
EGL14.EGL_RENDERABLE_TYPE, if (glVersion == 2)
EGL14.EGL_OPENGL_ES2_BIT else EGL14.EGL_OPENGL_ES2_BIT or EGLExt.EGL_OPENGL_ES3_BIT_KHR,
EGL_RECORDABLE_ANDROID, 1,
EGL14.EGL_NONE
)
val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(
eglDisplay,
attributesForSurface,
configs, 0,
configs.size,
numConfigs, 0
)
eglConfig = configs[0]
复制代码
根据给定的属性集合中返回EGL配置,该配置可以用于创建GL上下文。
四、创建GL上下文
val attribList = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, glVersion, EGL14.EGL_NONE)
val eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, attribList, 0)
复制代码
eglCreateContext
可以传入刚刚获取到的EGLDisplay
,EGLConfig
,来生成一个EGLContext
。而这个EGLContext
就是一个OpenGL ES上下文。OpenGL上下文是在使用OpenGL API比较关键的资源,用于存储OpenGL API的执行环境,例如当前的渲染状态、矩阵栈、光照参数等。
五、创建EGLSurface
val eglSurface = EGL14.eglCreateWindowSurface(
eglDisplay, eglConfig, surface, intArrayOf(EGL14.EGL_NONE), 0
)
复制代码
将刚刚获取的EGLDisplay
,EGLConfig
和SurfaceTexture
传进去,获得一个EGLSurface
,而这个EGLSurface
是一个渲染表面,用于把OpenGL图形渲染到窗口系统上。
OpenGL ES
一、进入OpenGL上下文
要使用OpenGL API,首先需要进入OpenGL上下文。
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)
复制代码
eglMakeCurrent
函数的作用是将指定的EGL上下文与渲染表面关联,这样OpenGL API才能作用于该表面,调用此函数之后,任何OpenGL API都会直接作用于该渲染表面。
二、创建GL_TEXTURE_2D纹理
val textures = IntArray(1)
val target = GLES30.GL_TEXTURE_2D
GLES30.glGenTextures(textures.size, textures, 0)
GLES30.glBindTexture(target, textures[0])
GLES30.glTexParameteri(target, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR)
GLES30.glTexParameteri(target, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)
GLES30.glTexParameteri(target, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE)
GLES30.glTexParameteri(target, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE)
val textureId = textures[0]
复制代码
glGenTextures
的作用是生成纹理标识符,用于代表OpenGL纹理对象。这里就只生成一个好了。glBindTexture
的作用是将一个纹理对象与一个纹理目标关联,并将该纹理对象绑定到当前激活的纹理单元。而纹理目标是指用于纹理的类型,此处生成一个GL_TEXTURE_2D
纹理。纹理单元是一个逻辑单元,它可以一次性绑定多个纹理。
三、加载数据
private val bitmap = BitmapFactory.decodeResource(application.resources, R.drawable.cat)
private val byteBuffer = ByteBuffer.allocateDirect(bitmap.byteCount).apply {
bitmap.copyPixelsToBuffer(this)
rewind()
}
GLES30.glTexImage2D(
GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap.width, bitmap.height,
0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer
)
复制代码
glTexImage2D
的函数的作用是为纹理对象加载图形数据。依次传入纹理目标、纹理模型层次、图像宽度、图像高度、图像边界宽度、图像数据格式、图像数据类型和图像数据指针。
将纹理中的图像展示出来
一、加载顶点着色器和片段着色器
关于这两个着色器的作用可以看看Kenny大佬的文章Android OpenGL ES 2.0 手把手教学
val VERTEX_SHADER_STRING = """
#version 300 es
precision mediump float;
layout(location = 0) in vec4 position;
layout(location = 1) in vec4 inputTextureCoordinate;
uniform mat4 textureTransform;
out vec2 textureCoordinate;
void main()
{
gl_Position = position;
textureCoordinate = (textureTransform * inputTextureCoordinate).xy;
}
""".trimIndent()
val glPositionId = 0
val glAttribTextureCoordinate = 1
val RGB_FRAGMENT_SHADER_CODE = """
#version 300 es
precision mediump float;
in highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
out vec4 fragColor;
void main()
{
fragColor = texture(inputImageTexture, textureCoordinate);
}
""".trimIndent()
// Load and compile shader code.
val vertexShaderId = GLES30.glCreateShader(GLES30.GL_VERTEX_SHADER)
val fragmentShaderId = GLES30.glCreateShader(GLES30.GL_FRAGMENT_SHADER)
GLES30.glShaderSource(vertexShaderId, VERTEX_SHADER_STRING)
GLES30.glShaderSource(fragmentShaderId, RGB_FRAGMENT_SHADER_CODE)
GLES30.glCompileShader(vertexShaderId)
GLES30.glCompileShader(fragmentShaderId)
复制代码
注意:glPositionId
,glAttribTextureCoordinate
两个属性需要和上文顶点着色器的代码中的layout
中的值相对应。
layout只能用于输入和输出变量,不能在其他类型中使用。
glCreateShader
的作用是创建一个新的着色器对象,返回该着色器对象的标识符。glShaderSource
函数的作用将源代码和着色器对象关联。glCompileShader
的作用是将着色器源代码编译成可执行的代码,如果没有错误,则编译后的代码可以在OpenGL程序中运行。
二、创建OpenGL程序
val programId = GLES30.glCreateProgram()
复制代码
glCreateProgram
函数的作用是创建一个OpenGL程序对象,并返回该对象的标识符,OpenGL程序对象是一组OpenGL着色器对象的集合,共同组成了一个OpenGL应用对象。
三、将着色器附加到OpenGL程序
// Attach shader to GL program.
GLES30.glAttachShader(programId, vertexShaderId)
GLES30.glAttachShader(programId, fragmentShaderId)
复制代码
四、关联OpenGL程序
GLES30.glLinkProgram(programId)
复制代码
glLinkProgram
它的作用是将多个着色器对象链接为一个可执行的OpenGL的程序对象。
五、设置OpenGL程序为当前渲染程序
GLES30.glUseProgram(programId)
复制代码
glUseProgram
可以指定任何已经链接的OpenGL程序对象为当前渲染程序。
六、清除着色器对象
GLES30.glDeleteShader(vertexShaderId)
GLES30.glDeleteShader(fragmentShaderId)
复制代码
glDeleteShader
函数用于删除已经创建的OpenGL着色器对象,将它从内存中释放。
七、应用顶点着色器
private fun FloatArray.toFloatBuffer(): FloatBuffer {
return ByteBuffer.allocateDirect(this.size * Float.SIZE_BITS)
.order(ByteOrder.nativeOrder()).asFloatBuffer().put(this)
.apply { position(0) }
}
val FULL_RECTANGLE_BUF = floatArrayOf(
-1.0f, -1.0f, // Bottom left.
1.0f, -1.0f, // Bottom right.
-1.0f, 1.0f, // Top left.
1.0f, 1.0f // Top right.
).toFloatBuffer().apply { position(0) }
val FULL_RECTANGLE_TEX_BUF = floatArrayOf(
0.0f, 0.0f, // Bottom left.
1.0f, 0.0f, // Bottom right.
0.0f, 1.0f, // Top left.
1.0f, 1.0f // Top right.
).toFloatBuffer().apply { position(0) }
// Enable vertex
GLES30.glEnableVertexAttribArray(glPositionId)
GLES30.glEnableVertexAttribArray(glAttribTextureCoordinate)
// Assign render pointer. Always render full screen.
GLES30.glVertexAttribPointer(
glPositionId, 2, GLES30.GL_FLOAT, false, 0, FULL_RECTANGLE_BUF
)
GLES30.glVertexAttribPointer(
glAttribTextureCoordinate, 2, GLES30.GL_FLOAT, false, 0, FULL_RECTANGLE_TEX_BUF
)
复制代码
glEnableVertexAttribArray
函数用于启用顶点属性。glVertexAttribPointer
函数的作用是定义顶点属性的数据格式,顶点属性数组是存储如顶点位置、颜色、纹理坐标等的数组。
八、绑定纹理
private val emptyMtx = floatArrayOf(
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
)
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId)
// 将纹理单元传入着色器uniform变量
val glInputTextureId = GLES30.glGetUniformLocation(programId, "inputImageTexture");
GLES30.glUniform1i(glInputTextureId, 0)
// 将矩阵传入着色器uniform变量
val glTexTransformId = GLES30.glGetUniformLocation(programId, "textureTransform")
GLES30.glUniformMatrix4fv(glTexTransformId, 1, false, emptyMtx, 0)
复制代码
glActiveTexture
激活纹理单元,在之后的操作可以对该纹理单元进行操作,进行纹理操作前,需要激活一个纹理单元,然后在该单元上绑定纹理才可以实现对纹理的操作。glUniform1i
将当前纹理单元透传进指定的uniform变量中,该变量对应了片段着色器代码中的inputImageTexture
,激活的纹理单元为GLES30.GL_TEXTURE0
,所以此处传0。glUniformMatrix4fv
向一个uniform变量传递一个4*4的矩阵。
九、绘制纹理
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
复制代码
glDrawArrays
按照顶点数组的顺序绘制图形。
十、解绑纹理
GLES30.glBindTexture(target, 0)
复制代码
十一、禁用顶点着色器
GLES30.glDisableVertexAttribArray(glPositionId)
GLES30.glDisableVertexAttribArray(glAttribTextureCoordinate)
复制代码
当OpenGL不再使用指定顶点的属性数组,可以禁用它,提高渲染性能。
绘制图像上屏
这是显示图片的最后一步了,将刚刚绘制完的图片呈现到屏幕上。
EGL14.eglSwapBuffers(eglDisplay, eglSurface)
复制代码
这个时候就可以加载并展示照片了!!但是为什么是倒着的呢?而且还被拉长了!等会说一下。
释放资源
别忘了释放资源,由于OpenGL是会占用Native内存的,在使用完之后记得释放资源,以免造成内存泄漏。
// 释放图片资源
byteBuffer.clear()
bitmap.recycle()
// 释放OpenGL程序资源
GLES30.glDeleteProgram(programId)
// 清除纹理资源
GLES30.glDeleteTextures(1, intArrayOf(textureId), 0)
// 清除Surface
EGL14.eglDestroySurface(eglDisplay, eglSurface)
eglSurface = EGL14.EGL_NO_SURFACE
// 离开GL上下文
EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)
// 清除GL上下文资源
EGL14.eglDestroyContext(eglDisplay, eglContext)
eglContext = EGL14.EGL_NO_CONTEXT
// 释放与该线程关联的EGL资源
EGL14.eglReleaseThread()
// 释放EGL与显示设备或窗口系统的链接,释放与EGL关联的资源
EGL14.eglTerminate(eglDisplay)
复制代码
项目地址
里面也包含了我对这些GL函数的封装库,个人觉得挺实用的,能够把以上的一堆代码以如下方式实现:
fun loadLittleCatImage(textureView: TextureView) {
// 构建EGL资源
eglSystem = EglSystem.createSurface(textureView.surfaceTexture!!)
// 进入OpenGL上下文
eglSystem.withCurrent {
// 构建Texture2D纹理并加载图片
imageId = GLUtils.generate2DTextureId(bitmap.width, bitmap.height, byteBuffer)
// 将Texture2D纹理绘制到EGLSurface中
shader.drawFrom(imageId, 0, 0, textureView.width, textureView.height)
// 将EGLSurface的内容冲刷到显示设备中
eglSystem.swapBuffers()
}
}
复制代码
总结
不知道这篇的笔记有没有人看完,音视频本来就很难入门,非常枯燥。笔记中只是对个别API进行简单介绍,并没有详细针对某一个点展开来讲,也没有详细解释为啥要这么干,就已经这样又长又臭了。但是相信大家跟着步骤把代码敲一下,最后把图片展示出来,绝对会非常有成就感的。我喜欢用小例子记录知识点,所以之后音视频笔记我也会通过小例子来记录。
其实本次用OpenGL展示一张图片整理下来也就三步,分别是:
- 构建EGL资源
- 进入GL上下文
- 构建纹理资源
- 绘制纹理资源
- 展示到显示设备中
对了,图片中的猫咪是倒着的,而且还拉长了,为什么呢?鉴于篇幅原因,我会放在下一篇笔记中记录一下如何将猫咪摆正。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/118814.html