前言

学习Android音视频肯定绕不开OpenGL ES,Android设备可以运用OpenGL ES来支撑高性能的2D和3D图形,它是一个图形烘托API。

本篇文章记载一下用OpenGL ES展现一张图片的一次实战,看似简略的操作,包含了很多的OpenGL ES的操作,有兴趣可以一起敲一下代码。为便利起见,下面对OpenGL ES简称OpenGL。

搭建UI

平常一般运用ImageView来加载并展现图片,而在这儿咱们运用的是TextureView来展现,为便利演示,运用Compose代码如下所示,也可以运用View形式。

咱们需求运用的是TextureViewSurfaceTexture,而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具体可以看aserbao大佬的/post/711967…

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可以传入刚刚获取到的EGLDisplayEGLConfig,来生成一个EGLContext。而这个EGLContext便是一个OpenGL ES上下文。OpenGL上下文是在运用OpenGL API比较要害的资源,用于存储OpenGL API的履行环境,例如当时的烘托状态、矩阵栈、光照参数等。

五、创立EGLSurface

val eglSurface = EGL14.eglCreateWindowSurface(
    eglDisplay, eglConfig, surface, intArrayOf(EGL14.EGL_NONE), 0
)

将刚刚获取的EGLDisplayEGLConfigSurfaceTexture传进去,获得一个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)

注意:glPositionIdglAttribTextureCoordinate两个特点需求和上文极点着色器的代码中的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)

这个时候就可以加载并展现相片了!!可是为什么是倒着的呢?并且还被拉长了!等会说一下。

【Android音视频】简简单单入门OpenGL ES加载一张小猫咪图片吧!

开释资源

别忘了开释资源,由于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)

项目地址

本来想把代码整理一下放上来的,可是太长了,两百多行,估量没人看得下去。这儿放一下项目地址吧。

github.com/MReP1/OpenG…

里边也包含了我对这些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展现一张图片整理下来也就三步,分别是:

  1. 构建EGL资源
  2. 进入GL上下文
  3. 构建纹路资源
  4. 制作纹路资源
  5. 展现到显现设备中

对了,图片中的猫咪是倒着的,并且还拉长了,为什么呢?鉴于篇幅原因,我会放在下一篇笔记中记载一下如何将猫咪摆正。

参阅:

极点着色器 vertex shader /post/684490…

EGL /post/684490…

EGL /post/711967…

chat.openai.com/