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的转换格式图:

在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_ALPHA
glTexImage2D(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()); @Override
protected 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);
} @Override
public BaseOpengl createOpengl() {
YUVRenderOpengl yuvRenderOpengl = new YUVRenderOpengl();
return yuvRenderOpengl;
} @Override
protected void onResume() {
super.onResume();
handler.postDelayed(new Runnable() {
@Override
public 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);
} @Override
protected 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!!!

最新文章

  1. bzoj 4330: JSOI2012 爱之项链
  2. getPhysicalNumberOfCells 与 getLastCellNum的区别
  3. MarkFan的程序员客栈
  4. 前端SPA框架一些看法
  5. android 25 跨进程启动activity
  6. Linux 抓包
  7. Newtonsoft.Json 时区差解决方法
  8. html 任意表格
  9. svn 几个好用的命令
  10. 【转载】jQuery动画中的queue()函数
  11. python3 第十章 - 如何进行进制转化
  12. 在Unix系统中,主存索引节点和辅存索引节点从内容上比较有什么不同,为什么要设置主存索引节点?
  13. java常用数据库连接池 (DBCP、c3p0、Druid) 配置说明
  14. Idea集成maven插件
  15. android spf中存list<string>
  16. 微软已发布 Windows 10 Timeline 功能的官方 Chrome 插件
  17. SpringMVC-RESTRUL___CRUD知识点总结
  18. Linux中无权限使用sudo
  19. EGIT
  20. WDA基础八:ROWREPEATER的使用

热门文章

  1. springboot 集成 docsify 实现随身文档
  2. 怎样编写正确、高效的 Dockerfile
  3. kubeadm 使用 Calico CNI 以及外部 etcd 部署 kubernetes v1.23.1 高可用集群
  4. 使用shell脚本定时重启tomcat服务
  5. windows系统下使用bat脚本文件设置 JDK 系统环境变量
  6. Wine 安装迅雷5.8.14.176
  7. [笔记] 二维FFT
  8. linux基础总结
  9. 谣言检测(DUCK)《DUCK: Rumour Detection on Social Media by Modelling User and Comment Propagation Networks》
  10. LeetCode题目答案及理解汇总(持续更新)