您的位置:首页 > 汽车 > 新车 > LearnOpenGL - Android OpenGL ES 3.0 YUV 渲染

LearnOpenGL - Android OpenGL ES 3.0 YUV 渲染

2024/7/2 4:26:58 来源:https://blog.csdn.net/weiwei9363/article/details/139889069  浏览:    关键词:LearnOpenGL - Android OpenGL ES 3.0 YUV 渲染

系列文章目录

  • LearnOpenGL 笔记 - 入门 01 OpenGL
  • LearnOpenGL 笔记 - 入门 02 创建窗口
  • LearnOpenGL 笔记 - 入门 03 你好,窗口
  • LearnOpenGL 笔记 - 入门 04 你好,三角形
  • OpenGL - 如何理解 VAO 与 VBO 之间的关系
  • LearnOpenGL - Android OpenGL ES 3.0 绘制三角形
  • LearnOpenGL - Android OpenGL ES 3.0 绘制纹理

文章目录

  • 系列文章目录
  • 一、前言
  • 二、YUV 格式介绍
  • 三、OpenGL ES 中 YUV 转 RGB
    • 3.1 glTexImage2D
      • 函数签名
      • 参数解释
      • 重要注意事项
    • 3.2 glTexImage2D 上传 RGB 数据
    • 3.3 glTexImage2D 上传 YUV420P 数据
      • 详细解释
        • 初始化
        • 上传 Y 分量
        • 上传 U 分量
        • 上传 V 分量
    • 3.3 glTexImage2D 上传 NV21 数据
      • 详细解释
        • 初始化
        • 上传 Y 分量
        • 上传 UV 分量
  • 四、Show me the code
    • 4.1 顶点着色器
    • 4.2 片元着色器
    • 完整代码
  • 参考


一、前言

在上一章 LearnOpenGL - Android OpenGL ES 3.0 绘制纹理 中,我们详细地解释了顶点着色器到片元着色器之间的工作流程,并向你展示了如何绘制一张纹理。
本章节我们将讨论如何使用 OpenGL ES 来将一张 YUV 格式的图片转换为 RGB,并渲染在屏幕上。本文所有代码可以在 YUV420PToRGBDrawer.kt 和 NV21ToRGBDrawer.kt 找到

二、YUV 格式介绍

这部分推荐看之前写的 YUV 文件读取、显示、缩放、裁剪等操作教程
,这次不再赘述。需要重点理解不同 YUV 格式之间的数据排列方式。

三、OpenGL ES 中 YUV 转 RGB

如果在 CPU 上,如何将一张 YUV 图片转换为 RGB 图片,基本思路是遍历所有像素位置,获取到当前位置的 YUV 分量,然后进行转换,伪代码如下:

RGBImage image;
for(int i = 0; i < width; ++i)
{for(int j = 0; j < height; ++j){auto y = getY(i, j);auto u = getU(i, j);auto v = getV(i, j);auto rgb = yuv2rgb(y, u, v);image[i,j] = rgb;}
}

为了在 GPU 上计算 YUV 转换 RGB,我们通常使用纹理将 YUV 数据上传到 GPU 上。还记得 LearnOpenGL - Android OpenGL ES 3.0 绘制纹理 中如何将 RGB 数据复制到纹理上的吗?我们使用 GLUtils.texImage2D 方法将一张 bitmap 数据复制到纹理上,GLUtils.texImage2D 最终还是调用 glTexImage2D 来实现的。

3.1 glTexImage2D

glTexImage2D 是 OpenGL 用于定义二维纹理图像的函数。它的作用是将图像数据上传到指定的纹理目标上。这个函数的参数解释如下:

函数签名

public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, Buffer pixels)

参数解释

  1. target:

    • 指定目标纹理。常用的值是 GL_TEXTURE_2D,表示二维纹理。
  2. level:

    • 指定纹理的级别(细化级别)。对于基本的纹理图像,值通常为 0。在使用 Mipmap 时,这个值可以是 0 到 log2(max(width, height)) 之间的整数。
  3. internalformat:

    • 指定纹理的内部格式。它定义了纹理存储在显卡内存中的格式。例如,GL_RGBGL_RGBA。这是 OpenGL 如何存储纹理的格式,而不是图像数据的格式。
  4. width:

    • 纹理的宽度(以像素为单位)。
  5. height:

    • 纹理的高度(以像素为单位)。
  6. border:

    • 纹理的边框宽度。必须为 0。边框在 OpenGL 中不常用且在现代 OpenGL 规范中已经废弃。
  7. format:

    • 指定像素数据的格式。例如,GL_RGBGL_RGBA。这是图像数据的格式,描述了像素数据的排列方式。
  8. type:

    • 指定像素数据的数据类型。例如,GL_UNSIGNED_BYTE 表示每个颜色通道使用一个无符号字节(0 到 255)。其他常见类型包括 GL_UNSIGNED_SHORTGL_FLOAT
  9. pixels:

    • 包含纹理图像数据的缓冲区(Buffer)。这个参数是一个指向图像数据的指针,可以是 ByteBuffer、ShortBuffer 或 FloatBuffer,取决于前面指定的 type

重要注意事项

  • 确保 widthheight 是合法的纹理大小,通常是 2 的幂次方(例如 256, 512, 1024 等)。
  • 在调用 glTexImage2D 之前,需要先绑定纹理目标,使用 glBindTexture 函数:
    glBindTexture(GL_TEXTURE_2D, textureID);
    

这样,glTexImage2D 函数就能将 imageBuffer 中的图像数据上传到 OpenGL 的纹理内存中,并且在绘制时可以使用该纹理。

3.2 glTexImage2D 上传 RGB 数据

假设你有一个 512x512 的 RGB 图像,存储在一个 ByteBuffer 中,注意此时首先需要保证 RGB 数据的排列是交织的,例如:

R G B R G B R G B R G B R G B R G B
ByteBuffer imageBuffer = ...; // 图像数据glTexImage2D(GL_TEXTURE_2D,        // 目标0,                    // 级别 0(基础级别)GL_RGB,               // 内部格式512,                  // 宽度512,                  // 高度0,                    // 边框,必须为 0GL_RGB,               // 像素数据格式GL_UNSIGNED_BYTE,     // 像素数据类型imageBuffer);         // 像素数据
  • 内部格式设置为 GL_RGB ,这样一来在片元着色器中可以通过 r,g,b 来获取各个颜色通道
  • 像素数据格式设置为 GL_RGB,意味着 imageBuffer 存放的数据是 RGB 排列的,拷贝到纹理时将逐通道获取
  • 像素数据类型为 GL_UNSIGNED_BYTE,表示每个颜色通道使用一个无符号字节(0 到 255)

3.3 glTexImage2D 上传 YUV420P 数据

YUV420P 是 Planar 模式,因此它先存放 Y 分量,接着存放 U,最后存放 V。假设现在有 4x2 带下的 YUV420P 图片,那么有 8 个 Y,2 个 U 和 2 个 V,该图片文件中存放的顺序是:

Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7 U0 U1 V0 V1

为了上传 YUV420P,我们需要三个纹理分别存放 Y、U、V 三个通道的数据,下面代码展示了如何正确地上传 YUV 数据

val byteBuffer = ...; // 图像数据
val width = ...;
val height = ...;
val uvWidth = width/2;
val uvHeight = height/2;// y
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
glTexImage2D(GL_TEXTURE_2D,        // 目标0,                    // 级别 0(基础级别)GL_LUMINANCE,         // 内部格式512,                  // 宽度512,                  // 高度0,                    // 边框,必须为 0GL_LUMINANCE,         // 像素数据格式GL_UNSIGNED_BYTE,     // 像素数据类型byteBuffer);         // 像素数据// u
val uOffset = width * height
byteBuffer.position(uOffset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[1])
glTexImage2D(GL_TEXTURE_2D,        // 目标0,                    // 级别 0(基础级别)GL_LUMINANCE,         // 内部格式512,                  // 宽度512,                  // 高度0,                    // 边框,必须为 0GL_LUMINANCE,         // 像素数据格式GL_UNSIGNED_BYTE,     // 像素数据类型byteBuffer);         // 像素数据// v
val vOffset = width * height + uvWidth * uvHeight
byteBuffer.position(vOffset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[2])
glTexImage2D(GL_TEXTURE_2D,        // 目标0,                    // 级别 0(基础级别)GL_LUMINANCE,         // 内部格式512,                  // 宽度512,                  // 高度0,                    // 边框,必须为 0GL_LUMINANCE,         // 像素数据格式GL_UNSIGNED_BYTE,     // 像素数据类型byteBuffer);         // 像素数据

这段代码使用 OpenGL ES 3.0 来处理和上传 YUV 图像数据到 GPU。YUV 是一种颜色编码格式,其中 Y 代表亮度(Luminance),U 和 V 代表色度(Chrominance)。该代码将 YUV 图像数据上传到三个单独的纹理中,分别存储 Y、U 和 V 分量。

详细解释

初始化
val byteBuffer = ... // 图像数据
val width = ...;
val height = ...;
val uvWidth = width / 2;
val uvHeight = height / 2;
  • byteBuffer:包含 YUV 图像数据的缓冲区。
  • widthheight:图像的宽度和高度。
  • uvWidthuvHeight:U 和 V 分量的宽度和高度,通常是 Y 分量的一半。
上传 Y 分量
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
glTexImage2D(GLES30.GL_TEXTURE_2D,  // 目标0,                      // 级别 0(基础级别)GLES30.GL_LUMINANCE,    // 内部格式width,                  // 宽度height,                 // 高度0,                      // 边框,必须为 0GLES30.GL_LUMINANCE,    // 像素数据格式GLES30.GL_UNSIGNED_BYTE,// 像素数据类型byteBuffer);            // 像素数据
  • 绑定一个纹理(texIds[0])用于存储 Y 分量。
  • 调用 glTexImage2D 上传 Y 分量数据到纹理。
  • GLES30.GL_LUMINANCE 表示单通道灰度数据,内部格式和像素数据格式都为灰度。
  • byteBuffer 包含图像数据的缓冲区。
上传 U 分量
val uOffset = width * height
byteBuffer.position(uOffset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[1])
glTexImage2D(GLES30.GL_TEXTURE_2D,  // 目标0,                      // 级别 0(基础级别)GLES30.GL_LUMINANCE,    // 内部格式uvWidth,                // 宽度uvHeight,               // 高度0,                      // 边框,必须为 0GLES30.GL_LUMINANCE,    // 像素数据格式GLES30.GL_UNSIGNED_BYTE,// 像素数据类型byteBuffer);            // 像素数据
  • 计算 U 分量的偏移量(uOffset),这是因为 Y 分量占据了前 width * height 个字节。
  • 调整 byteBuffer 的位置到 U 分量的起始位置。
  • 绑定一个纹理(texIds[1])用于存储 U 分量。
  • 上传 U 分量数据到纹理,使用 uvWidthuvHeight 指定宽度和高度,因为 U 分量的尺寸通常是 Y 分量的一半。
上传 V 分量
val vOffset = width * height + uvWidth * uvHeight
byteBuffer.position(vOffset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[2])
glTexImage2D(GLES30.GL_TEXTURE_2D,  // 目标0,                      // 级别 0(基础级别)GLES30.GL_LUMINANCE,    // 内部格式uvWidth,                // 宽度uvHeight,               // 高度0,                      // 边框,必须为 0GLES30.GL_LUMINANCE,    // 像素数据格式GLES30.GL_UNSIGNED_BYTE,// 像素数据类型byteBuffer);            // 像素数据

3.3 glTexImage2D 上传 NV21 数据

NV21 是 Planar 模式,它属于 YUV420SP 类型,它先存放 Y 分量,接着 VU 交替存储。假设现在有 4x2 带下的 NV21 图片,那么有 8 个 Y,2 个 U 和 2 个 V,该图片文件中存放的顺序是:

Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7 V0 U0 V1 U1

为了上传 NV21,我们需要两个个纹理分别存放 Y 和 UV 两个通道的数据,下面代码展示了如何正确地上传 NV21 数据

val byteBuffer = ...; // 图像数据
val width = ...;
val height = ...;
val uvWidth = width/2;
val uvHeight = height/2;// y
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D,0,GLES30.GL_LUMINANCE,width,height,0,GLES30.GL_LUMINANCE,GLES30.GL_UNSIGNED_BYTE,byteBuffer)// uv
val offset = width * height
byteBuffer.position(offset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[1])
GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D,0,GLES30.GL_LUMINANCE_ALPHA,uvWidth,uvHeight,0,GLES30.GL_LUMINANCE_ALPHA,GLES30.GL_UNSIGNED_BYTE,byteBuffer
)

这段代码是用来上传 NV21 格式的 YUV 图像数据到 OpenGL ES 3.0 的纹理中。NV21 是一种常见的 YUV 4:2:0 颜色格式,其中 Y 分量单独存储,而 U 和 V 分量交错存储。代码将 Y 分量和交错的 UV 分量分别上传到两个纹理中。

详细解释

初始化
val byteBuffer = ... // 图像数据
val width = ...      // 图像宽度
val height = ...     // 图像高度
val uvWidth = width / 2   // UV 分量的宽度
val uvHeight = height / 2 // UV 分量的高度
  • byteBuffer: 包含 NV21 图像数据的缓冲区。
  • widthheight: 图像的宽度和高度。
  • uvWidthuvHeight: UV 分量的宽度和高度,通常是 Y 分量的一半。
上传 Y 分量
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[0])
GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D,0,GLES30.GL_LUMINANCE,width,height,0,GLES30.GL_LUMINANCE,GLES30.GL_UNSIGNED_BYTE,byteBuffer
)
  • 绑定一个纹理(texIds[0])用于存储 Y 分量。
  • 调用 glTexImage2D 上传 Y 分量数据到纹理:
    • GLES30.GL_LUMINANCE 表示单通道灰度数据,内部格式和像素数据格式都为灰度。
    • widthheight 为图像的宽度和高度。
    • byteBuffer 包含图像数据的缓冲区。
上传 UV 分量
val offset = width * height
byteBuffer.position(offset)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texIds[1])
GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D,0,GLES30.GL_LUMINANCE_ALPHA,uvWidth,uvHeight,0,GLES30.GL_LUMINANCE_ALPHA,GLES30.GL_UNSIGNED_BYTE,byteBuffer
)
  • 计算 UV 分量的偏移量(offset),这是因为 Y 分量占据了前 width * height 个字节。
  • 调整 byteBuffer 的位置到 UV 分量的起始位置。
  • 绑定一个纹理(texIds[1])用于存储交错的 UV 分量。
  • 调用 glTexImage2D 上传交错的 UV 分量数据到纹理:
    • GLES30.GL_LUMINANCE_ALPHA 表示双通道数据,内部格式和像素数据格式都为灰度加 Alpha。
    • uvWidthuvHeight 为 UV 分量的宽度和高度(通常是 Y 分量的一半)。
    • byteBuffer 包含图像数据的缓冲区。

四、Show me the code

先从着色器出发,思考如何编写着色器,然后根据着色器来编写 OpenGL ES 代码

4.1 顶点着色器

顶点着色器与 LearnOpenGL - Android OpenGL ES 3.0 绘制纹理一致,不再赘述

#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_texcoord;
out vec2 v_texcoord;
void main()
{gl_Position = vec4(a_position, 1.0);v_texcoord = a_texcoord;
}

4.2 片元着色器

如果是 YUV420P 格式,那么会有三张纹理,从 y/u/v 纹理中分别获取数据,转换到 rgb

#version 300 es
precision mediump float;
uniform sampler2D y_texture;
uniform sampler2D u_texture;
uniform sampler2D v_texture;
in vec2 v_texcoord;
out vec4 fragColor;
void main()
{vec3 yuv;yuv.x = texture(y_texture,v_texcoord).r-0.0625;      // yyuv.y = texture(u_texture,v_texcoord).r-0.5;  // uyuv.z = texture(v_texture,v_texcoord).r-0.5;  // vmat3 m = mat3(1.164, 1.164, 1.164,  0.0, -0.213, 2.112, 1.793, -0.533, 0.0);vec3 rgb = m * yuv;fragColor = vec4(rgb, 1.0);
}

如果是 NV21 格式,那么会有两张纹理,从 y/uv 纹理中分别获取数据,转换到 rgb,注意一个细节: u 和 v 数据获取时选择的通道

#version 300 es
precision mediump float;
uniform sampler2D y_texture;
uniform sampler2D uv_texture;
in vec2 v_texcoord;
out vec4 fragColor;
void main()
{vec3 yuv;yuv.x = texture(y_texture,v_texcoord).r-0.0625;  // yyuv.y = texture(uv_texture,v_texcoord).a-0.5;  // uyuv.z = texture(uv_texture,v_texcoord).r-0.5;  // vmat3 m = mat3(1.164, 1.164, 1.164,  0.0, -0.213, 2.112, 1.793, -0.533, 0.0);vec3 rgb = m * yuv;fragColor = vec4(rgb, 1.0);
}

YUV 转 RGB 的公式参考 YUV/RGB颜色空间转换公式

完整代码

完整代码在 YUV420PToRGBDrawer.kt 和 NV21ToRGBDrawer.kt 中,就不具体展开说了,该说的内容已经在前面的章节中解释过了,相信你都能看懂的。

参考

  • YUV 文件读取、显示、缩放、裁剪等操作教程
  • NDK OpenGLES 3.0 开发(三):YUV 渲染
  • YUV420PToRGBDrawer.kt
  • NV21ToRGBDrawer.kt
  • YUV/RGB颜色空间转换公式

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com