Opengl ES之YUV数据渲染
创始人
2024-02-10 05:59:57

YUV回顾

记得在音视频基础知识介绍中,笔者专门介绍过YUV的相关知识,可以参考:
《音视频基础知识-YUV图像》

YUV数据量相比RGB较小,因此YUV适用于传输,但是YUV图不能直接用于显示,需要转换为RGB格式才能显示,因而YUV数据渲染实际上就是使用Opengl ES将YUV数据转换程RGB数据,然后显示出来的过程。

也就是说Opengl ES之所以能渲染YUV数据其实就是使用了Opengl强大的并行计算能力,快速地将YUV数据转换程了RGB数据。

本文首发于微信公总号号:思想觉悟

更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

YUV的格式比较多,我们今天就以YUV420SP为例,而YUV420SP又分为NV12NV21两种,因此今天我们的主题就是如何使用Opengl ES对NV12NV21数据进行渲染显示。

在着色器中使用texture2D对YUV数据进行归一化处理后Y数据的映射范围是0到1,而U和V的数据映射范围是-0.5到0.5。

因为YUV格式图像 UV 分量的默认值分别是 127 ,Y 分量默认值是 0 ,8 个 bit 位的取值范围是 0 ~ 255,由于在 shader 中纹理采样值需要进行归一化,所以 UV 分量的采样值需要分别减去 0.5 ,确保 YUV 到 RGB 正确转换。

YUV数据准备

首先我们可以使用ffmpeg命令行将一张png图片转换成YUV格式的图片:

ffmpeg -i 图片名称.png -s 图片宽x图片高 -pix_fmt nv12或者nv21 输出名称.yuv)

通过上面这个命令行我们就可以将一张图片转换成yuv格式的图片,此时我们可以使用软件YUVViewer看下你转换的图片对不对,如果本身转换出来的图片就是错的,那么后面的程序就白搭了…

注意:转换图片的宽高最好是2的幂次方,笔者测试了下发现如果宽高不是2的幂次方的话虽然能正常转换,但是查看的时候要么有色差,要么有缺陷,也有可能正常。

又或者你可以极客一点,直接使用ffmpeg代码解码视频的方式获得YUV数据并保存,这个可以参考笔者之前的文章:

《FFmpeg连载3-视频解码》

同时在上面的文章中笔者也介绍了通过ffplay命令行的方式查看YUV数据的方法。

YUV数据渲染

YUV 渲染步骤:

  • 生成 2 个纹理,分别用于承载Y数据和UV数据,编译链接着色器程序;

NV21和NV12格式的YUV数据是只有两个平面的,它们的排列顺序是YYYY UVUV或者YYYY VUVU因此我们的片元着色器需要两个纹理采样。

  • 确定纹理坐标及对应的顶点坐标;
  • 分别加载 NV21 的两个 Plane 数据到 2 个纹理,加载纹理坐标和顶点坐标数据到着色器程序;
  • 绘制。

YUV与RGB的转换格式图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpiIVQPw-1669172740519)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/YUV%E8%BD%ACRGB%E5%85%AC%E5%BC%8F.png)]

在OpenGLES的内置矩阵实际上是一列一列地构建的,比如YUV和RGB的转换矩阵的构建是:

// 标准转换,舍弃了部分小数精度
mat3 convertMat = mat3(1.0, 1.0, 1.0,      //第一列0.0,-0.338,1.732, //第二列1.371,-0.698, 0.0);//第三列

OpenGLES 实现 YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的纹理,其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据。

废话少说,show me the code

YUVRenderOpengl.h

#ifndef NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#define NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#include "BaseOpengl.h"class YUVRenderOpengl: public BaseOpengl{public:YUVRenderOpengl();virtual ~YUVRenderOpengl();virtual void onDraw() override;// 设置yuv数据virtual void setYUVData(void *y_data,void *uv_data, int width, int height, int yuvType);private:GLint positionHandle{-1};GLint textureHandle{-1};GLint y_textureSampler{-1};GLint uv_textureSampler{-1};GLuint y_textureId{0};GLuint uv_textureId{0};
};#endif //NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H

YUVRenderOpengl.cpp


#include "YUVRenderOpengl.h"#include "../utils/Log.h"// 顶点着色器
static const char *ver = "#version 300 es\n""in vec4 aPosition;\n""in vec2 aTexCoord;\n""out vec2 TexCoord;\n""void main() {\n""  TexCoord = aTexCoord;\n""  gl_Position = aPosition;\n""}";// 片元着色器 nv12
//static const char *fragment = "#version 300 es\n"
//                              "precision mediump float;\n"
//                              "out vec4 FragColor;\n"
//                              "in vec2 TexCoord;\n"
//                              "uniform sampler2D y_texture; \n"
//                              "uniform sampler2D uv_texture;\n"
//                              "void main()\n"
//                              "{\n"
//                              "vec3 yuv;\n"
//                              "yuv.x = texture(y_texture, TexCoord).r;\n"
//                              "yuv.y = texture(uv_texture, TexCoord).r-0.5;\n"
//                              "yuv.z = texture(uv_texture, TexCoord).a-0.5;\n"
//                              "vec3 rgb =mat3( 1.0,1.0,1.0,\n"
//                              "0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\n"
//                              "FragColor = vec4(rgb, 1);\n"
//                              "}";/***  仅仅是以下两句不同而已*  "yuv.y = texture(uv_texture, TexCoord).r-0.5;\n"*  "yuv.z = texture(uv_texture, TexCoord).a-0.5;\n"*/
// 片元着色器nv21 仅仅是
static const char *fragment = "#version 300 es\n""precision mediump float;\n""out vec4 FragColor;\n""in vec2 TexCoord;\n""uniform sampler2D y_texture; \n""uniform sampler2D uv_texture;\n""void main()\n""{\n""vec3 yuv;\n""yuv.x = texture(y_texture, TexCoord).r;\n""yuv.y = texture(uv_texture, TexCoord).a-0.5;\n""yuv.z = texture(uv_texture, TexCoord).r-0.5;\n""vec3 rgb =mat3( 1.0,1.0,1.0,\n""0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\n""FragColor = vec4(rgb, 1);\n""}";// 使用绘制两个三角形组成一个矩形的形式(三角形带)
// 第一第二第三个点组成一个三角形,第二第三第四个点组成一个三角形
const static GLfloat VERTICES[] = {0.5f,-0.5f, // 右下0.5f,0.5f, // 右上-0.5f,-0.5f, // 左下-0.5f,0.5f // 左上
};// 贴图纹理坐标(参考手机屏幕坐标系统,原点在左上角)
//由于对一个OpenGL纹理来说,它没有内在的方向性,因此我们可以使用不同的坐标把它定向到任何我们喜欢的方向上,然而大多数计算机图像都有一个默认的方向,它们通常被规定为y轴向下,X轴向右
const static GLfloat TEXTURE_COORD[] = {1.0f,1.0f, // 右下1.0f,0.0f, // 右上0.0f,1.0f, // 左下0.0f,0.0f // 左上
};YUVRenderOpengl::YUVRenderOpengl() {initGlProgram(ver,fragment);positionHandle = glGetAttribLocation(program,"aPosition");textureHandle = glGetAttribLocation(program,"aTexCoord");y_textureSampler = glGetUniformLocation(program,"y_texture");uv_textureSampler = glGetUniformLocation(program,"uv_texture");LOGD("program:%d",program);LOGD("positionHandle:%d",positionHandle);LOGD("textureHandle:%d",textureHandle);LOGD("y_textureSampler:%d",y_textureSampler);LOGD("uv_textureSampler:%d",uv_textureSampler);
}YUVRenderOpengl::~YUVRenderOpengl() {}void YUVRenderOpengl::setYUVData(void *y_data, void *uv_data, int width, int height, int yuvType) {// 准备y数据纹理glGenTextures(1, &y_textureId);glActiveTexture(GL_TEXTURE2);glUniform1i(y_textureSampler, 2);// 绑定纹理glBindTexture(GL_TEXTURE_2D, y_textureId);// 为当前绑定的纹理对象设置环绕、过滤方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, y_data);// 生成mip贴图glGenerateMipmap(GL_TEXTURE_2D);glBindTexture(GL_TEXTURE_2D, y_textureId);// 解绑定glBindTexture(GL_TEXTURE_2D, 0);// 准备uv数据纹理glGenTextures(1, &uv_textureId);glActiveTexture(GL_TEXTURE3);glUniform1i(uv_textureSampler, 3);// 绑定纹理glBindTexture(GL_TEXTURE_2D, uv_textureId);// 为当前绑定的纹理对象设置环绕、过滤方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// 注意宽高// 注意要使用 GL_LUMINANCE_ALPHAglTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, width/2, height/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, uv_data);// 生成mip贴图glGenerateMipmap(GL_TEXTURE_2D);glBindTexture(GL_TEXTURE_2D, uv_textureId);// 解绑定glBindTexture(GL_TEXTURE_2D, 0);
}void YUVRenderOpengl::onDraw() {glClearColor(0.0f, 0.0f, 0.0f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(program);// 激活纹理glActiveTexture(GL_TEXTURE2);// 绑定纹理glBindTexture(GL_TEXTURE_2D, y_textureId);glUniform1i(y_textureSampler, 2);// 激活纹理glActiveTexture(GL_TEXTURE3);// 绑定纹理glBindTexture(GL_TEXTURE_2D, uv_textureId);glUniform1i(uv_textureSampler, 3);/*** size 几个数字表示一个点,显示是两个数字表示一个点* normalized 是否需要归一化,不用,这里已经归一化了* stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0*/// 启用顶点数据glEnableVertexAttribArray(positionHandle);glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);// 纹理坐标glEnableVertexAttribArray(textureHandle);glVertexAttribPointer(textureHandle,2,GL_FLOAT,GL_FALSE,0,TEXTURE_COORD);// 4个顶点绘制两个三角形组成矩形glDrawArrays(GL_TRIANGLE_STRIP,0,4);glUseProgram(0);// 禁用顶点glDisableVertexAttribArray(positionHandle);if(nullptr != eglHelper){eglHelper->swapBuffers();}glBindTexture(GL_TEXTURE_2D, 0);
}

注意看着色器代码的注释,NV12和NV21的渲染仅仅是着色器代码有细小差别而已。

YUVRenderActivity.java

public class YUVRenderActivity extends BaseGlActivity {// 注意改成你自己图片的宽高private int yuvWidth = 640;private int yuvHeight = 428;private String nv21Path;private String nv12Path;private Handler handler = new Handler(Looper.getMainLooper());@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 注意申请磁盘写权限// 拷贝资源nv21Path = getFilesDir().getAbsolutePath() + "/nv21.yuv";FileUtils.copyAssertToDest(this,"nv21.yuv",nv21Path);nv12Path = getFilesDir().getAbsolutePath() + "/nv12.yuv";FileUtils.copyAssertToDest(this,"nv12.yuv",nv12Path);}@Overridepublic BaseOpengl createOpengl() {YUVRenderOpengl yuvRenderOpengl = new YUVRenderOpengl();return yuvRenderOpengl;}@Overrideprotected void onResume() {super.onResume();handler.postDelayed(new Runnable() {@Overridepublic void run() {// 注意nv12和nv21的偏远着色器有点不一样的,需要手动改下调试  YUVRenderOpengl.cpp
//                if(!TextUtils.isEmpty(nv12Path)){
//                    loadYuv(nv12Path,BaseOpengl.YUV_DATA_TYPE_NV12);
//                }if(!TextUtils.isEmpty(nv21Path)){loadYuv(nv21Path,BaseOpengl.YUV_DATA_TYPE_NV21);}}},200);}@Overrideprotected void onStop() {handler.removeCallbacksAndMessages(null);super.onStop();}private void loadYuv(String path,int yuvType){try {InputStream inputStream = new FileInputStream(new File(path));Log.v("fly_learn_opengl","---length:" + inputStream.available());byte[] yData = new byte[yuvWidth * yuvHeight];inputStream.read(yData,0,yData.length);byte[] uvData = new byte[yuvWidth * yuvHeight / 2];inputStream.read(uvData,0,uvData.length);Log.v("fly_learn_opengl","---read:" + (yData.length + uvData.length) + "available:" + inputStream.available());myGLSurfaceView.setYuvData(yData,uvData,yuvWidth,yuvHeight);} catch (Exception e) {e.printStackTrace();}}
}

这个主要看懂loadYuv方法,对于YUV数据的读取即可。

思考

都说YUV的格式较多,本文我们介绍了如何使用Opengl ES渲染YUV420SP数据,那么对于YUV420P数据,使用Opengl ES如何渲染呢?欢迎关注评论解答交流。

专栏系列

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO

关注我,一起进步,人生不止coding!!!
微信扫码关注

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...