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

导读:本篇文章讲解 【Android音视频】简简单单入门OpenGL ES加载一张小猫咪图片吧,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

前言

学习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资源拥有四个重要的构件。

  • 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)
复制代码

项目地址

里面也包含了我对这些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. 展示到显示设备中

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

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/118814.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!