系列文章目录
- 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)
参数解释
-
target
:- 指定目标纹理。常用的值是
GL_TEXTURE_2D
,表示二维纹理。
- 指定目标纹理。常用的值是
-
level
:- 指定纹理的级别(细化级别)。对于基本的纹理图像,值通常为
0
。在使用 Mipmap 时,这个值可以是 0 到log2(max(width, height))
之间的整数。
- 指定纹理的级别(细化级别)。对于基本的纹理图像,值通常为
-
internalformat
:- 指定纹理的内部格式。它定义了纹理存储在显卡内存中的格式。例如,
GL_RGB
或GL_RGBA
。这是 OpenGL 如何存储纹理的格式,而不是图像数据的格式。
- 指定纹理的内部格式。它定义了纹理存储在显卡内存中的格式。例如,
-
width
:- 纹理的宽度(以像素为单位)。
-
height
:- 纹理的高度(以像素为单位)。
-
border
:- 纹理的边框宽度。必须为 0。边框在 OpenGL 中不常用且在现代 OpenGL 规范中已经废弃。
-
format
:- 指定像素数据的格式。例如,
GL_RGB
或GL_RGBA
。这是图像数据的格式,描述了像素数据的排列方式。
- 指定像素数据的格式。例如,
-
type
:- 指定像素数据的数据类型。例如,
GL_UNSIGNED_BYTE
表示每个颜色通道使用一个无符号字节(0 到 255)。其他常见类型包括GL_UNSIGNED_SHORT
和GL_FLOAT
。
- 指定像素数据的数据类型。例如,
-
pixels
:- 包含纹理图像数据的缓冲区(Buffer)。这个参数是一个指向图像数据的指针,可以是 ByteBuffer、ShortBuffer 或 FloatBuffer,取决于前面指定的
type
。
- 包含纹理图像数据的缓冲区(Buffer)。这个参数是一个指向图像数据的指针,可以是 ByteBuffer、ShortBuffer 或 FloatBuffer,取决于前面指定的
重要注意事项
- 确保
width
和height
是合法的纹理大小,通常是 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 图像数据的缓冲区。width
和height
:图像的宽度和高度。uvWidth
和uvHeight
: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 分量数据到纹理,使用
uvWidth
和uvHeight
指定宽度和高度,因为 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 图像数据的缓冲区。width
和height
: 图像的宽度和高度。uvWidth
和uvHeight
: 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
表示单通道灰度数据,内部格式和像素数据格式都为灰度。width
和height
为图像的宽度和高度。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。uvWidth
和uvHeight
为 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颜色空间转换公式