本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

经过阅览本文,你将获得以下收获:
1.怎样读取解析YUV视频文件
2.怎样将YUV转化为RGBA
3.怎样将YUV帧画面经过OpenGL烘托到屏幕

上篇回顾

上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹路映射(实践篇)现已用代码实例详细(或许很难找到比我更详细的哈哈)展示了怎样进行纹路映射,以及纹路映射能做一些什么有趣效果。有了上一篇的根底,这一篇就能够乘胜追击,从上一篇的静态图片进阶到视频的烘托(跟了这么多篇博文,终于摸到视频的影子了,本系列现已渐入高潮!)。

今日的任务,便是烘托一个YUV视频,立马送上效果图:

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

取自于宫崎骏大师的《龙猫》,满满的幼年回忆~~

为什么是YUV视频呢?假设看过之前我这篇博文音视频开发根底知识之YUV色彩编码以及解析H264视频编码原理——从孙艺珍的电影说起(一) 的朋友应该知道,yuv一般便是视频用来传输的原始数据,所以这儿作为OpenGL烘托视频的第一篇博文,当然是从烘托原始数据构成的视频讲起。究竟勿在浮沙筑高台嘛~

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

温馨提示:假设没有读过一看就懂的OpenGL ES教程——临摹画手的浪漫之纹路映射(理论篇)和一看就懂的OpenGL ES教程——临摹画手的浪漫之纹路映射(实践篇),本文阅览起来或许会十分费劲,所以十分建议阅览本文之前先读完上面两篇文章。

全体思路

依据前两篇讲解纹路映射的文章,咱们要将图片纹路烘托到屏幕上,就要拿到图片的像素数据数组,然后将像素数据数组经过纹路单元传到片段着色器中,再经过纹路采样函数将纹路中对应坐标的色彩值采样出来,最终赋值一个终究的片段色彩值

现在换成了YUV视频,咱们又要怎样处理呢?咱们能够从终究产出方针的终究的片段色彩值触发”回溯“一下:由于终究的片段色彩值是RGBA格局的,所以咱们要采样到纹路对应坐标方位的RGBA色彩值,可是视频是YUV格局的,这儿就需求一个转化过程,行将YUV转为RGBA。怎样将YUV传给片段着色器呢?有没有现成的YUV格局能够直接用呢?

或许你尝试过在OpenGL的glTexImage2D查找过一切的色彩格局,可是却沮丧地发现并没有YUV格局能够直接用

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

别慌,所谓山穷水复疑无路,柳暗花明又一村。

OpenGL又给咱们供给了GL_LUMINANCE这种格局,它表明只取一个色彩通道,假设传入的对应值为L,则在片段着色器中的纹路单元读取出来的值为(L, L, L, 1)

为什么说能够用过GL_LUMINANCE这种格局来读取YUV图画呢?由于经过GL_LUMINANCE这种格局,咱们能够对YUV图画拆分为3个通道来读取。

拆分为3个通道来读取,那么应该怎样从头合成为一个RGBA色彩值呢?这时候,上一篇文章介绍过的纹路单元就能够派上用场了,咱们能够用3个纹路单元,别离将每个通道数据传入着色器中,最终在着色器中将3个通道数据又合在一起

然后再经过音视频开发根底知识之YUV色彩编码介绍的办法将YUV转为RGBA就全部迎刃而解了。

这儿是全体思路,下面立刻出现详解。

首要要做的一点便是,读取YUV视频,将其分解为3个色彩通道,而且能够一帧帧读取

读取解析YUV视频

温馨提示:读本章节之前假设关于YUV还不熟悉,那请务必先读一读音视频开发根底知识之YUV色彩编码,否则本章节理解起来会很费劲。

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

要读取解析YUV视频,首要就要理解它的内部结构,要理解其内部结构,就要首要理解其详细格局。

当时这段YUV视频,是一段每帧由yuv420p、宽高别离为640272的YUV图画组成的视频,而且帧与帧之间无缝衔接。让咱们来温习一下YUV420P的结构,先存储一切的 Y 重量后, 再存储一切的 U 重量,最终再存储V重量

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

(图来源于: 音视频根底知识—像素格局YUV)

又由于yuv420p中从数量上来看y:u:v=4:1:1,所以咱们的解析方案就浮出水面了:

假设每帧图画的宽度为w,高度为h,由于YUV每个通道重量占一个字节巨细,所以咱们能够每一帧都先读取w * h个y,然后读取w * h/4个u,再读取w * h/4个v,一帧数据读取结束,然后对这些数据进行烘托。接下来再继续读取下一帧数据重复这个步骤,直到文件没有剩余的数据能够被读取。

有了方案,那么就要制定详细的办法了。由于每次读取一帧就要进行烘托,那么由于烘托逻辑是写在Native层的,所以读取解析YUV视频文件的逻辑也要写在Native层。在Native层中读取文件,能够运用C语言最基本的File相关的办法(fread)读取,不过这样需求咱们将文件拷贝到手机某个文件夹中而且需求在代码中写死文件路径,显现不符合”高级程序员“的身份。由于咱们是在Android体系,Android现已供给了asset资源目录了,所以咱们能够经过将视频文件放置在asset资源目录中,然后愈加方便地读取视频数据

首要将YUV视频文件放到asset文件夹中:

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

然后经过AssetManager去读取YUV视频文件。你或许会问,在Java层有AssetManager,Native没有怎样办?其实不必担心,谷歌的服务向来都是很周(yi)到(ban)的,ndk在Native层相同供给了AssetManager。名曰AAssetManager

首要在Java层创立Native办法:

public native void loadYuv(Object surface, AssetManager assetManager);

然后在Java层获取到AssetManager,并传入loadYuv办法中:

AssetManager assetManager = getContext().getAssets();
loadYuv(getHolder().getSurface(),assetManager);

然后在Native层创立对应的loadYuv办法,其中读取YUV视频文件代码如下:

//创立3个buffer数组别离用于寄存YUV三个重量
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];//y
buf[1] = new unsigned char[width * height / 4];//u
buf[2] = new unsigned char[width * height / 4];//v
//经过Java层传入的AssetManager方针得到AAssetManager方针指针
AAssetManager *mManeger = AAssetManager_fromJava(env, assetManager);
//得到AAsset方针指针
AAsset *dataAsset = AAssetManager_open(mManeger, "video1_640_272.yuv",
                                       AASSET_MODE_STREAMING);//get file read AAsset
//文件总长度
off_t dataBufferSize = AAsset_getLength(dataAsset);
//纵帧数
long frameCount = dataBufferSize / (width * height * 3 / 2);
LOGD("frameCount:%d", frameCount);
//读取每帧的YUV数据
for (int i = 0; i < frameCount; ++i) {
    //读取y重量
    int bufYRead = AAsset_read(dataAsset, buf[0],
                               width * height);  //begin to read data once time
    //读取u重量
    int bufURead = AAsset_read(dataAsset, buf[1],
                               width * height / 4);  //begin to read data once time
    //读取v重量
    int bufVRead = AAsset_read(dataAsset, buf[2],
                               width * height / 4);  //begin to read data once time
    LOGD("bufYRead:%d,bufURead:%d,bufVRead:%d", bufYRead, bufURead, bufVRead);
    //读到文件结尾了
    if (bufYRead <= 0 || bufURead <= 0 || bufVRead <= 0) {
        AAsset_close(dataAsset);
        return;
    }

1.首要准备好3个水桶:创立3个buffer数组别离用于寄存Y、U、V三个重量。
2.接好水管经过Java层传入的AssetManager方针得到AAssetManager方针指针然后得到AAsset方针指针,并算好要接多少桶水(计算好总帧数)
3.接水:3个水桶按次序接水,经过AAsset_read办法别离将每一帧的Y、U、V重量数据寄存到三个buffer数组中。

就这样,YUV视频文件的三个通道就被解析寄存到3个buffer数组中

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

依照全体思路说到的,接下来便是要把这3个数组别离装进3个纹路中传送到片段着色器里

纹路方针装备

关于缓冲方针、极点纹路坐标的装备,显然关于现阶段的咱们来说现已不在话下了。

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

不过……还是提一下极点坐标和纹路坐标吧,依然是将整个纹路映射到整个显现区

//加入三维极点数据
static float ver[] = {
        1.0f, -1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, 1.0f, 0.0f,
        -1.0f, 1.0f, 0.0f
};
//加入纹路坐标数据
static float fragment[] = {
        1.0f, 0.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        0.0f, 1.0f
};

让咱们直接绕过这些繁琐的东西,犁庭扫穴,看下纹路自身怎样装备:

1.首要创立3个纹路单元

//纹路ID
GLuint textures[3] = {0};
//创立若干个纹路方针,而且得到纹路ID
glGenTextures(3, textures);

2.装备纹路单元

第一个纹路单元是用来盛放Y通道的,装备如下:

//绑定纹路。后边的的设置和加载全部作用于当时绑定的纹路方针
//GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹路方针
//经过 glBindTexture 函数将纹路方针和纹路绑定后,对纹路方针所进行的操作都反映到对纹路上
glBindTexture(GL_TEXTURE_2D, textures[0]);
//缩小的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//放大的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//设置纹路的格局和巨细
// 加载纹路到 OpenGL,读入 buffer 界说的位图数据,并把它复制到当时绑定的纹路方针
// 当时绑定的纹路方针就会被附加上纹路图画。
glTexImage2D(GL_TEXTURE_2D,
             0,//细节基本 默许0
             GL_LUMINANCE,//gpu内部格局 亮度,灰度图(这儿便是只取一个亮度的色彩通道的意思)
             width,//加载的纹路宽度。最好为2的次幂
             height,//加载的纹路高度。最好为2的次幂
             0,//纹路边框
             GL_LUMINANCE,//数据的像素格局 亮度,灰度图
             GL_UNSIGNED_BYTE,//像素点存储的数据类型
             NULL //纹路的数据(先不传)
);

先绑定纹路单元,然后装备纹路过滤,接着便是最要害的glTexImage2D办法了。

glTexImage2D办法在上一篇博文中现已调过,便是将纹路像素数据传递到片段着色器的办法。

这次色彩格局改为上面说到的GL_LUMINANCE,表明亮度,用于构成灰度图。这样假设某个Y的值为L,则传入片段着色器中的纹路就成了(L,L,L,1)。由于YUV的位深为8,所以这儿像素点存储的数据类型为GL_UNSIGNED_BYTE即可。最终一个参数即实际像素数据传NULL。当然传NULL不表明不需求传像素数据就能够烘托(就像那有不付钱就有手抓饼吃呢),而是这儿仅仅提前装备一下,真实的传数据还在后头(真实的好戏还在后头)。

另外还要留意的是这儿Y传入的宽高为width和height,即为视频自身的像素分辨率,是由于YUV420中Y的数量和图画的像素个数持平

第二、三个纹路单元别离用来盛放U,V通道:

//设置纹路的格局和巨细
glTexImage2D(GL_TEXTURE_2D,
             0,//细节基本 默许0
             GL_LUMINANCE,//gpu内部格局 亮度,灰度图(这儿便是只取一个色彩通道的意思)
             width / 2,//u、v数据数量为屏幕的4分之1
             height / 2,
             0,//边框
             GL_LUMINANCE,//数据的像素格局 亮度,灰度图
             GL_UNSIGNED_BYTE,//像素点存储的数据类型
             NULL //纹路的数据(先不传)
);

其他装备和Y相同,仅有的不同点便是传入的尺度,之前现已说过,关于YUV420P来说,U,V各自都是每4个像素采样一个,所以它们都是各自只占总像素数的4分之1,所以传入的尺度值都为:宽width / 2,高height / 2

经过以上设置,咱们现已做好了全部准备作业:创立三个纹路单元,将YUV三个通道别离传入三个纹路单元中,接下来,便是在片段着色器中别离对这三个纹路单元绑定的纹路进行采样了

片段着色器

#version 300 es
precision mediump float;
//纹路坐标
in vec2 vTextCoord;
//输入的yuv三个纹路
uniform sampler2D yTexture;//采样器
uniform sampler2D uTexture;//采样器
uniform sampler2D vTexture;//采样器
out vec4 FragColor;
void main() {
    //采样到的yuv向量数据
    vec3 yuv;
    //yuv转化得到的rgb向量数据
    vec3 rgb;
    //别离取yuv各个重量的采样纹路
    yuv.x = texture(yTexture, vTextCoord).r;
    yuv.y = texture(uTexture, vTextCoord).g - 0.5;
    yuv.z = texture(vTexture, vTextCoord).b - 0.5;
    //yuv转化为rgb
    rgb = mat3(
            1.0, 1.0, 1.0,
            0.0, -0.183, 1.816,
            1.540, -0.459, 0.0
    ) * yuv;
    //gl_FragColor是OpenGL内置的
    FragColor = vec4(rgb, 1.0);
 };

这一次的片段着色器看起来现已不那么”demo“了。咱们的中心思想便是把Y、U、V三个通道别离作为三个纹路,然后别离对三个纹路进行采样,然后将采样成果合并为一个终究的RGBA的色彩值作为当时片段的色彩值

这儿有不少细节值得好好揣摩。

首要yuv.x,yuv.y,yuv.z是什么呢?

其实在GLSL中,向量的组件能够经过{x, y, z, w}{r, g, b, a}{s, t, r, q}等操作来获取。之所以采用这三种不同的命名办法,是由于向量常常会用来表明数学向量、色彩、纹路坐标等。

重量访问符 符号描绘
(x,y,z,w) 与方位相关的重量
(r,g,b,a) 与色彩相关的重量
(s,t,p,q) 与纹路坐标相关的重量

所以yuv.x,yuv.y,yuv.z别离表明yuv向量的第1,2,3个元素。相同的,texture(yTexture, vTextCoord).r表明的是texture函数采样得到的色彩向量的第一个重量。

所以以下采样函数调用表明的便是对yTexture所对应的纹路在vTextCoord方位坐标上进行采样,得到的向量的第一个元素赋值给yuv向量的第一个元素。

yuv.x = texture(yTexture, vTextCoord).r;

由于咱们纹路装备指定的格局为GL_LUMINANCE,这样假设对应方位的Y的值为L,则texture的返回值就为(L,L,L,1),即此刻yuv的第一个元素值为L。而texture(yTexture, vTextCoord).r就表明获取texture办法返回的向量的第一个元素

以此类推出yuv.y,yuv.z别离是对uTexture、vTexture采样得到的值第二、三个元素值(其实这儿随便取第1,2,3个元素值都能够,由于都持平)减去0.5。

可是有意思的是,为什么要减去0.5呢?这也是经典面试题来的(敲黑板)

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

将YUV数据转化为RGBA格局

首要咱们回顾一下YUV转RGB的公式,之前说过,不同的转换规范,运用不同的公式:

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

咱们以BT709 Limited为例,

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

首要讲下U、V的默许值。之前说过,YUV中的U、V别离表明和R、B和亮度Y的误差U、V的默许值为128,由上图公式可知当U、V别离为128的时候,对应的R和B刚好别离等于Y

从上图公式能够看出,代入的U、V都是减去默许值128的

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

转化公式用的是U、V和默许值的偏移值,所以,咱们能够在代入公式之前,先求出这个偏移值,又由于texture函数得到的是一个归一化的,即范围为0-1的值,而128又是U、V的中心值,所以在采样之后需求减的不是128,而是0-1的中心值0.5

减去0.5得到U、V相关于默许值的偏移值之后,代入公式,此刻公式能够用矩阵(mat3表明三个 vec3构成的矩阵)相乘表明:

rgb = mat3(
            1.0, 1.0, 1.0,// 第一列
            0.0, -0.183, 1.816,// 第二列
            1.540, -0.459, 0.0// 第三列
    ) * yuv;

要留意的是,mat3里面的矩阵是依照列次序排的,如注释所示。

于是便愉快地拿到了对应的RGB值。

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

烘托视频帧

当咱们每次拿到每一帧的YUV数据的时候,接下来便是将YUV每个通道别离传入每个纹路单元中。

将片段着色器中界说的sampler变量和纹路单元进行绑定

//对sampler变量,运用函数glUniform1i和glUniform1iv进行设置
glUniform1i(glGetUniformLocation(program, "yTexture"), 0);
glUniform1i(glGetUniformLocation(program, "uTexture"), 1);
glUniform1i(glGetUniformLocation(program, "vTexture"), 2);

然后针对每个色彩通道进行烘托:

比如针对Y通道的烘托:

//激活第一个纹路单元
glActiveTexture(GL_TEXTURE0);
//绑定y对应的纹路
glBindTexture(GL_TEXTURE_2D, textures[0]);
//替换纹路,比从头运用glTexImage2D功能高多
glTexSubImage2D(GL_TEXTURE_2D, 0,
                0, 0,//指定纹路数组中的纹素偏移
                width, height,//加载的纹路宽度、高度。最好为2的次幂
                GL_LUMINANCE, GL_UNSIGNED_BYTE,
                buf[0]);

glTexSubImage2DglTexImage2D作用很类似,可是运用办法有区别,glTexSubImage2D是用于修改纹路,即在一个纹路上只能第一次烘托glTexImage2D,而后边每帧的修改都用glTexSubImage2D,所以glTexSubImage2D用来烘托视频的每帧再适合不过,由于假设每帧都从头创立纹路,那功率实在太低了。

voidglTexSubImage2D( GLenumtarget,
GLintlevel,
GLintxoffset,
GLintyoffset,
GLsizeiwidth,
GLsizeiheight,
GLenumformat,
GLenumtype,
const void *pixels``);

参数:
target

指定活动纹路单元的方针纹路。 有必要是GL_TEXTURE_2D,GL_TEXTURE_CUBE_MAP_POSITIVE_X,GL_TEXTURE_CUBE_MAP_NEGATIVE_X,GL_TEXTURE_CUBE_MAP_POSITIVE_Y,GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,GL_TEXTURE_CUBE_MAP_POSITIVE_Z或GL_TEXTURE_CUBE_MAP_NEGATIVE_Z。

level

指定详细等级编号。 0级是基本图画等级。 等级n是第n个mipmap缩小图画。

xoffset

指定纹路数组中x方向的纹素偏移量,x方向是指纹路坐标。

yoffset

指定纹路数组中y方向的纹素偏移,y方向是指纹路坐标。

width

指定纹路子图画的像素宽度。

height

指定纹路子图画的像素高度。

format

指定像素数据的格局。 承受以下符号值:GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE和GL_LUMINANCE_ALPHA。

type

指定像素数据的数据类型。 承受以下符号值:GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4和GL_UNSIGNED_SHORT_5_5_5_1。

data

指定指向内存中图画数据的指针

关于U、V通道来说,仅仅是纹路的尺度不同罢了,上面现已讲过就不赘述:

//激活第二层纹路,绑定到创立的纹路
glActiveTexture(GL_TEXTURE1);
//绑定u对应的纹路
glBindTexture(GL_TEXTURE_2D, textures[1]);
//替换纹路,比从头运用glTexImage2D功能高
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,
                GL_UNSIGNED_BYTE,
                buf[1]);

运转一下,幼年的感觉又回来了~

一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

项目代码

opengl-es-study-demo 不断更新中,欢迎各位来star~

参考

GLSL 详解(根底篇)

系列文章目录

体系化学习系列博文,请看音视频体系学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

C/C++根底与进阶之路

音视频理论根底系列专栏

音视频开发实战系列专栏

轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形烘托管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL作业机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲方针优化程序(一)
一看就懂的OpenGL ES教程——缓冲方针优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹路映射(理论篇)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹路映射(实践篇)