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又分为NV12
和NV21
两种,因而明天咱们的主题就是如何应用Opengl ES对NV12
和NV21
数据进行渲染显示。
在着色器中应用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!!!