OpenGL ES如何渲染Camera1的NV21格式画面

网上关于如何使用OpenGL ES直接渲染NV21格式画面的文章寥寥无几,大多数都是直接用SurfaceTexture提供的纹理来渲染,本文处理的情况是Camera1的回调给了NV21的数组,编写一个滤镜来渲染出来。

理论部分

因为OpenGL要求的是rgb格式,所以需要将yuv转换成rgb格式,转换公式如下:

需要注意的是OpenGL ES的内置矩阵实际是一列一列构建的,比如YUV和RGB的转换矩阵构建是:

c 复制代码
mat3 convertMat = mat3(1.0, 1.0, 1.0,      //第一列
                       0.0,-0.338,1.732, //第二列
                       1.371,-0.698, 0.0);//第三列

在滤镜中需要创建两个纹理,textureY和textureUV,激活纹理时Y的format使用GL_LUMINANCE,UV使用GL_LUMINANCE_ALPHA。

滤镜编写

顶点着色器,常规写法即可。

c 复制代码
#version 300 es
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 aTexture;
out vec2 vTexture;
void main()
{
    gl_Position = a_Position;
    vTexture = aTexture;
}

片元着色器,主要做yuv到rgb的转换。

c 复制代码
#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 vTexture;
uniform sampler2D textureY;
uniform sampler2D textureUV;
void main() {
    //yuv转化得到的rgb向量数据
    vec3 rgb;
    vec3 yuv;
    //分别取yuv各个分量的采样纹理
    yuv.x = texture(textureY, vTexture).r;
    yuv.y = texture(textureUV, vTexture).a - 0.5;
    yuv.z = texture(textureUV, vTexture).r - 0.5;
    //yuv转化为rgb
    rgb = mat3(1.0, 1.0, 1.0,
                0.0, -0.338, 1.732,
                1.4075, -0.7169, 0.0)*yuv;
    FragColor = vec4(rgb, 1.0);
}

滤镜类编写:

kotlin 复制代码
class YuvOESFilter(context: Context): GLImageFilter(context) {
    // 预览的宽高, camera一般宽高反过来,如 720*1280
    private var mVideoWidth: Int = -1
    private var mVideoHeight: Int = -1

    // 纹理接收者
    private var mTextureYHandler: Int = -1
    private var mTextureUVHandler: Int = -1
    // 两个纹理
    private val textures = IntArray(2)
    // 两个buffer
    private var bufferY: ByteBuffer? = null
    private var bufferUV: ByteBuffer? = null
}

在渲染之前必须要确定画面的宽高,为了确定buffer的大小。

kotlin 复制代码
fun setVideoSize(videoW: Int, videoH: Int) {
    mVideoWidth = videoW
    mVideoHeight = videoH
}

每一帧回调时,设置buffer的数据。

kotlin 复制代码
fun setNV21Data(data: ByteArray) {

    try {
        // 初始化
        if (bufferY == null) {
            bufferY = ByteBuffer.allocate(mVideoWidth * mVideoHeight)
            bufferY!!.order(ByteOrder.nativeOrder())
        }

        if (bufferUV == null) {
            bufferUV = ByteBuffer.allocate(mVideoWidth * mVideoHeight / 2)
            bufferUV!!.order(ByteOrder.nativeOrder())
        }
        // Y的数据是宽*高
        bufferY!!.put(data, 0, mVideoWidth * mVideoHeight)
        bufferY!!.position(0)
        // UV的数据是Y的一半
        bufferUV!!.put(data, mVideoWidth * mVideoHeight, mVideoWidth * mVideoHeight / 2)
        bufferUV!!.position(0)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

接下来是渲染,设置顶点坐标、纹理坐标部分省略了,重点在于两个纹理的激活与绑定。

kotlin 复制代码
override fun onDrawTexture(
    textureId: Int,
    vertexBuffer: FloatBuffer?,
    textureBuffer: FloatBuffer?
) {
    ...
    if (textures[0] == 0) {
        // 普通的创建纹理逻辑
        setTexture()
    }
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
    //激活指定纹理单元
    GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
    //绑定纹理ID到纹理单元
    //y
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0])
    GLES30.glUniform1i(mTextureYHandler, 0)
    GLES30.glTexImage2D(
        GLES30.GL_TEXTURE_2D,
        0,
        GLES30.GL_LUMINANCE,
        mVideoHeight,
        mVideoWidth,
        0,
        GLES30.GL_LUMINANCE,
        GLES30.GL_UNSIGNED_BYTE,
        bufferY
    )
    GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[1])
    GLES30.glUniform1i(mTextureUVHandler, 1)
    // 注意使用GLES30.GL_LUMINANCE_ALPHA
    GLES30.glTexImage2D(
        GLES30.GL_TEXTURE_2D,
        0,
        GLES30.GL_LUMINANCE_ALPHA,
        mVideoHeight / 2,
        mVideoWidth / 2,
        0,
        GLES30.GL_LUMINANCE_ALPHA,
        GLES30.GL_UNSIGNED_BYTE,
        bufferUV
    )

    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount)
    // 解绑
    GLES30.glDisableVertexAttribArray(mPositionHandle)
    GLES30.glDisableVertexAttribArray(mTextureCoordinateHandle)
    GLES30.glBindTexture(textureType, 0)
}

override fun release() {
    super.release()
    // GLES20.glDeleteProgram(mProgramHandle);
    bufferY?.clear()
    bufferUV?.clear()
}

这样就完成了渲染NV21的核心逻辑,每帧回调时触发一次滤镜的draw即可实现预览。

参考

在面试中,被反复提及的 OpenGL NV21 图像渲染

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui