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!!!