如何使用OpenGL渲染YUV数据

2020-06-23 16:20:46 浏览数 (1)

本篇文章主要描述如何使用OpenGL ES来渲染i420(YUV420P)和nv21(YUV420SP)

首先准备yuv数据文件,使用ffmpeg对图片进行格式转换

原图大小为800x480:

ffmpeg转化为nv21和i420格式的yuv文件

代码语言:javascript复制
// convert to nv21 
ffmpeg -i test.png -s 800x480 -pix_fmt nv21 test.yuv
// convert to i420
ffmpeg -i test.png -s 800x480 -pix_fmt yuv420p yuv420p.yuv

在OpenGL中,片元着色器最后输出的都是rgba的数据,所以使用OpenGL来渲染YUV数据的关键还是将YUV数据传递给着色器,并在着色器中将YUV转化为RGB

在我们创建一个2D纹理并使用glTexImage2D来填充数据的时候可以指定internalformat

代码语言:javascript复制
public static native void glTexImage2D(
        int target,  // 目标纹理,此处必须为GL_TEXTURE_2D
        int level,   // 执行细节级别,0是最基本的图像级别,n表示第N级贴图细化级别
        int internalformat,  // 指定纹理中的颜色组件
        int width,  // 指定纹理图像的宽度,
        int height, //  指定纹理图像的高度,
        int border, // 指定边框的宽度。必须为0
        int format, // 像素数据的颜色格式
        int type, // 指定像素数据的数据类型
        java.nio.Buffer pixels // 指定内存中指向图像数据的指针
    );

internalformat这个参数指定纹理中的颜色组件,可选的值有GL_RGB,GL_RGBA,GL_LUMINANCE,GL_LUMINANCE_ALPHA 等

通常使用的GL_RGBA这种internalformat,它会单独保存R,G,B,A四个数据,而在渲染YUV数据的时候,我们使用GL_LUMINANCE和GL_LUMINANCE_ALPHA

使用GL_LUMINANCE的时候,可以将Y分量存储到像素的各个通道内,这样在着色器中,我们可以通过R,G,B任意一个分量来获取到Y值。U,V分量同理

使用GL_LUMINANCE_ALPHA的时候,首先存储亮度,然后是alpha值,利用这一点可以将U值存储到像素的A通道,V值存储到R,G,B通道

渲染i420

在使用GL渲染i420格式的YUV数据时,需要使用三个2D纹理,每个纹理的颜色组件采用GL_LUMINANCE

代码语言:javascript复制
private fun textureLuminance(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexImage2D(
            GLES20.GL_TEXTURE_2D, 0,
            GLES20.GL_LUMINANCE, width, height, 0,
            GLES20.GL_LUMINANCE,
            GLES20.GL_UNSIGNED_BYTE, imageData
        )
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
  }

首先把i420数据从文件中读取出来,然后创建3个2D纹理和buffer,并填充数据到buffer中,关键代码如下

代码语言:javascript复制
// read i420 data
imageBytes = Util.read("yuv420p.yuv", context!!)
yBuffer = ByteBuffer.allocateDirect(imageWidth * imageHeight)
                .order(ByteOrder.nativeOrder())
yBuffer.put(imageBytes, 0, imageWidth * imageHeight)
yBuffer.position(0)

 // y texture
textureLuminance(yBuffer, imageWidth, imageHeight, yTextureId)

// u, v的流程是一样的,只是填充数据的时候要注意offset和纹理的宽高
uBuffer.put(imageBytes, imageWidth * imageHeight, imageWidth * imageHeight / 4)
textureLuminance(uBuffer, imageWidth / 2, imageHeight / 2, uTextureId)

vBuffer.put(imageBytes, imageWidth * imageHeight * 5 / 4, imageWidth * imageHeight / 4)
textureLuminance(vBuffer, imageWidth / 2, imageHeight / 2, vTextureId)

在渲染的时候,激活三个纹理单元并将纹理传递给着色器即可

在片元着色器中是如何从纹理中拿到Y,U,V分量的数据并且转化为R,G,B的呢?

从纹理中提取Y,U,V分量

代码语言:javascript复制
// We had put the Y values of each pixel to the R, G, B components by GL_LUMINANCE,
// that's why we're pulling it from the R component, we could also use G or B
y = texture2D(yTexture, vTexCoord).r;

// u, v, same as above
u = texture2D(uTexture, vTexCoord).r;
v = texture2D(vTexture, vTexCoord).r;

YUV与RGB的互转公式

根据公式在片元着色器中进行YUV to RGB的转化

代码语言:javascript复制
y = 1.164 * (y - 16.0 / 255.0);
u = u - 128.0 / 255.0;
v = v - 128.0 / 255.0;

r = y   1.596 * v;
g = y - 0.391 * u - 0.813 * v;
b = y   2.018 * u;

gl_FragColor = vec4(r, g, b, 1.0);

渲染nv21

在使用GL渲染nv21格式的YUV数据时,只需要使用两个2D纹理,Y分量纹理的颜色组件采用GL_LUMINANCE,UV分量纹理的颜色组件采用GL_LUMINANCE_ALPHA

代码语言:javascript复制
private fun textureLuminanceAlpha(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexImage2D(
            GLES20.GL_TEXTURE_2D, 0,
            GLES20.GL_LUMINANCE_ALPHA, width, height, 0,
            GLES20.GL_LUMINANCE_ALPHA,
            GLES20.GL_UNSIGNED_BYTE, imageData
        )
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}

从文件中读取nv21数据,创建纹理和buffer,填充数据到buffer的流程和渲染i420的步骤是类似的,此处就不再赘述了

和渲染i420的片元着色器中唯一不同的就是获取U分量是从a通道获取

代码语言:javascript复制
// We had put the U and V values of each pixel to the A and R, G, B components of the
// texture respectively using GL_LUMINANCE_ALPHA. Since U, V bytes are interspread
// in the texture
u = texture2D(uvTexture, vTexCoord).a;
v = texture2D(uvTexture, vTexCoord).r;

DEMO

代码传送门

https://github.com/sifutang/opengl.git

0 人点赞