Opengl ES之YUV数据渲染
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数据并保存,这个可以参考笔者之前的文章:
同时在上面的文章中笔者也介绍了通过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!!!
Opengl ES之YUV数据渲染的更多相关文章
- 详解 OpenGL ES 2.x 渲染流程
khronos官方对OpenGL ES的描述如下: OpenGL ES is a royalty-free, cross-platform API for rendering advanced 2D ...
- Android OpenGL ES 离屏渲染(offscreen render)
通常在Android上使用OpenGL ES,都是希望把渲染后的结果显示在屏幕上,例如图片处理.模型显示等.这种情况下,只需要使用Android API中提供的GLSurfaceView类和Rende ...
- Opengl ES之纹理贴图
纹理可以理解为一个二维数组,它可以存储大量的数据,这些数据可以发送到着色器上.一般情况下我们所说的纹理是表示一副2D图,此时纹理存储的数据就是这个图的像素数据. 所谓的纹理贴图,就是使用Opengl将 ...
- 一文详解 OpenGL ES 3.x 渲染管线
OpenGL ES 构建的三维空间,其中的三维实体由许多的三角形拼接构成.如下图左侧所示的三维实体圆锥,其由许多三角形按照一定规律拼接构成.而组成圆锥的每一个三角形,其任意一个顶点由三维空间中 x.y ...
- OpenGL ES: (3) EGL、EGL绘图的基本步骤、EGLSurface、ANativeWindow
1. EGL概述 EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现. EGL提供如 ...
- [iTyran原创]iPhone中OpenGL ES显示3DS MAX模型之二:lib3ds加载模型
[iTyran原创]iPhone中OpenGL ES显示3DS MAX模型之二:lib3ds加载模型 作者:u0u0 - iTyran 在上一节中,我们分析了OBJ格式.OBJ格式优点是文本形式,可读 ...
- 一步步实现windows版ijkplayer系列文章之六——SDL2源码分析之OpenGL ES在windows上的渲染过程
一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...
- 深度剖析OpenGL ES中的多线程和多窗口渲染技术
由 创新网小编 于 星期五, 2014-04-11 14:56 发表 移动设备中的CPU和GPU已经变得很强大,到处都是配备一个或多个高分辨率屏幕的设备,需要使用带有图形驱动器的复杂交互也日益增加.在 ...
- 还在使用OpenGL ES做渲染,你Out了,赶紧来拥抱Vulkan吧~
背景介绍 Vulkan是Khronos组织制定的"下一代"开放的图形显示API.是与DirectX12能够匹敌的GPU API标准. Vulkan是基于AMD的Mantle API ...
- OpenGL ES无法获取贴图数据原因
最近在做一个项目,要从贴图中获取图像数据,查了很多资料,也琢磨很久,获取到的数据都是0.终于在一次偶然的机会,发现了端倪,成功了. 不得不说这"一分灵感"真的很重要 以下是在获取贴 ...
随机推荐
- .Net下的高效分页
本文技术方案支持.Net/.Net Core/.Net Framework 数据分页,几乎是任何应用系统的必备功能.但当数据量较大时,分页操作的效率就会变得很低.大数据量分页时,一个操作耗时5秒.10 ...
- Kibana:在Kibana中对数据进行深入分析
- Elastic: 创建一个 Elastic 邮件警报 - 7.7 发行版
文章转载自:https://blog.csdn.net/UbuntuTouch/article/details/106185321 总结: 1.elastic 免费版只有发送警报到一个索引或者到Ser ...
- MySQL数据表更新模板
-- ---------------------------- -- 新增表 -- ---------------------------- CREATE TABLE `biz_circle_lead ...
- 使用k8s部署springcloud解决三大问题
1.正式环境使用的话启动时需要指定使用正式的配置文件,这个要咋处理? 解决办法 文章地址:https://www.cnblogs.com/sanduzxcvbnm/p/13262411.html 分析 ...
- CAS核心思想、底层实现
★ 1.CAS 是什么 CAS 是比较并交换,是实现并发算法时常用到的一种技术.当内存的值和期望的值相等时,进行更新,否则 什么都不做 或 重来 . CAS 的底层实现:是靠硬件实现的,靠硬件的原子性 ...
- EFCore分表实现
实现原理 当我们new一个上下文DbContext 后, 每次执行CURD方式时 ,都会依次调用OnConfiguring(),OnModelCreating()两个方法. OnConfiguring ...
- 如何通过 Java 代码隐藏 Word 文档中的指定段落
在编辑Word文档时,我们有时需要将一些重要信息保密. 因此,可以隐藏它们以确保机密性. 在本文中,将向您介绍如何通过 Java 程序中的代码隐藏 Word 文档中的特定段落.下面是我整理的具体步骤, ...
- 驱动开发:内核R3与R0内存映射拷贝
在上一篇博文<驱动开发:内核通过PEB得到进程参数>中我们通过使用KeStackAttachProcess附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这 ...
- TomCat之负载均衡
TomCat之负载均衡 本文讲述了tomcat当nginx负载均衡服务器配置步骤 以下是Tomcat负载均衡配置信息 1.修改nginx的nginx.conf文件 添加如下属性:localhost是名 ...