|
第1章OpenGL ES
3D图形编程首先OpenGL。OpenGL的英文全称是OpenGL Graphics Library,中午名称是开发式图形库。OpenGL为程序开发人员定义了一个跨平台的图形硬件编程接口,可用于三维图像(二维亦可),功能非常强大,适用于从普通PC到大型图型工作站等计算机。它采用LGPL或者GPL许可证。在行业领域中被广泛接纳,自其诞生之日起已经催生了许多优秀的应用程序。OpenGL独立于操作系统,广泛应用在CAD、能源、娱乐、游戏开发、制造业、制药业、虚拟现实等行业。OpenGL定义的软件接口与硬件无关,具有很好的移植性,应用非常广泛。
OpenGLES是OpenGL的子集,针对手机、PAD、游戏机等嵌入式系统设计。OpenGL ES是OpenGL的裁剪版,去除了四边形、多边形等复杂图元,这些图元不是绝对必要的特性。既然决定使用OpenGL ES,就需要决定使用的OpenGL ES版本,目前主要存在的OpenGL ES有OpenGL ES 1.0/1.1、OpenGL ES 2.0,这两个版本的差异非常大。其中对重要的是OpenGL ES2.0 使用了可编程管线技术,允许开发者通过编程的方式控制顶点着色器和片元着色器,实现比OpenGL ES1.x更酷的界面效果。
1.1 3D图形学理论基础
现在,我们队OpenGL有个初步的认识了。作为图形开发人员需要了解一些3D图形学的理论,虽然这不是OpenGL初学者的必需条件。正如你知道的OpenGL已经替我们完成了几乎所有的图形处理和计算,不过,了解3D图形学相关的知识对我们理解和使用OpenGL进行开发将会大有帮助。
1.1.1 点、线段、距离
图形学中点是最基本的几何对象。为了便于描述,会把点放到坐标系中进行描述,一般利用直角坐标系来确定点的位置。例如,原点的坐标为(0,0),位于两个坐标轴相交的地方。通常,我们确定x轴为水平方向,并且x轴的正方向向右,y轴为水平方向,并且y轴的正方向向上。
直角坐标系
只是用X/Y轴表示的为2D空间,两个点P1和P2确定一条线段。假设P1和P2的坐标分别为(和 )和(和),那么它们之间的距离为:
如果在2D坐标系的基础上增加一个Z轴,即可建立3D空间。3D空间中的点需要使用三个坐标表示,根据Z轴方向,可以分为左手系和右手系。如上图所示坐标系,通过沿X轴正方向到Y轴正方向握拳,大拇指所致的方向即为Z轴方向,Z轴朝向屏幕外面,这就是右手坐标系,同理,我们可以使用左手确定Z轴的方向朝向屏幕里边。
3D空间中的距离计算公式为:
通常情况下,大多数程序开发人员都是用右手坐标系,而且OpenGL也采用这种坐标系,所以后续我们使用的都是右手坐标系。
1.1.2 向量和矩阵
介绍向量之前,有必要先介绍下标量。标量表示数量的大小,仅仅是一个数值而已。例如绳子的长度,物体的重量等。
向量表示为同时具有数值和方向的量,相当于具有方向的标量。例如,物体运动的速度,它不仅具有表示大小的数值(速率),而且还有一个方向用于表示行进的方向。通常情况下,在2D空间向量具有x、y两个分量,在3D空间中向量具有x、y、z三个分量。比如球体表面的法线就是向量,它不仅有大小而且具有方向性。
很多时候,我们只关心向量的方向而不在乎他的大小,在这样的情况下,使用单位向量变得非常方便。所谓的单位向量就是大小为1的向量,通常,单位向量也称为标准化向量。一个非零向量的归一化表示如下:
其中 是向量的模(也称为大小,是向量坐标和原点的距离)
具有相同维数的向量可以进行加减法运算。就是说,参加加、减运算的两个向量都是2维、3维或者4维等,具体运算规则,需要把对应的分量相加。例如向量和向量相加结果为,同理我们可以计算出向量。
此外,向量还支持点乘和叉乘运算。
点乘描述了两个向量的相关性,运算结果是一个标量,表示两个向量的“相似”程度。
如果向量A和B的长度都是1,公式简化为:
然后通过反余弦计算,可以知道向量A和向量B之间的夹角。
叉乘仅可应用于3D场景,与点乘不一样的是,向量叉乘得到的是一个向量。向量A和向量B叉乘结果的大小为,方向垂直于向量A和向量B,具体指向,可以使用右手定则进行判断。例如,在A、B交叉点伸出右手,食指指向A,弯曲其他手指直到指向B,此时,拇指的指向就是的指向。向量的这一性质可以帮助我们找出平面的法向量。
矩阵是3D图形学的重要理论基础。在OpenGL 3D图形开发中,物体位移、缩放、旋转等变换和坐标系的转换都使用矩阵表示,对于学习过线性代数的读者肯定不会陌生,我们通过行数列数的方式定义矩阵尺寸。对于矩阵M可以表示为:
方阵、对角矩阵、单位矩阵是特殊的矩阵。
其中,方阵表示函数和列数相同的矩阵,如上所示矩阵M。如果方阵的对角线元素(方阵中行号和列好相同的元素、、)外,所有非对角元素都是0,那么我们称之为对角矩阵。
而单位矩阵是更加特殊的方阵,它是的矩阵,而且对角线元素都是1,其余元素都是0。它具有特殊的性质。
任何矩阵和一个单位矩阵相乘,结果还是原来的矩阵。
矩阵可以和标量相乘,相当于矩阵中的么个元素乘以该标量。
向量是只有一行或者一列的矩阵,它可以和矩阵相乘,并且存在两种组合。行向量左乘矩阵得到行向量;列向量右乘矩阵得到列向量。其中行向量的列数和矩阵的行数相等,列向量的行数和矩阵的列数相等。例如:
图形处理过程中,经常会遇到平移、缩放、旋转等几何变换操作,在OpenGL开发中都是通过矩阵变换来实现的。可以说几何变换是图形处理的核心内容。举个简单的例子,通过如下矩阵运算可以发现结果,,。
所以,矩阵M代表了一个在X轴移动距离a,在Y轴移动b,在Z轴移动c的平移操作。
细心的读者可以能已经发现了,我们在三维坐标空间的矩阵运算使用的是矩阵,没错。这种表示方法叫做齐次坐标表示,基本思想是在n+1维空间解决n为空间的几何问题,它使用了n+1个分量表示n个分量的向量。一般齐次坐标表示不具备唯一性,通常情况下,计算机图形学中会采用规范化齐次坐标,即使用(x,y,1)的形式表示点(x,y)。
同理,我们也可以利用矩阵运算实现缩放和旋转的效果。
说明:旋转矩阵表示了点P绕轴向量旋转度,运算比较复杂,由于本章只是介绍3D图形相关的基础知识,所以就不进行验证计算了,有兴趣的读者可以参考其他资料进一步研究学习。
是一个缩放矩阵,、、分别描述了缩放转换过程中沿X、Y、Z轴方向的缩放率。3D空间中点的齐次坐标乘以此矩阵后,相当于沿坐标轴X、Y、Z方向缩放、、倍的效果。
所以,我们可以通过不同的矩阵分别表示一个运算效果,并且矩阵相乘还可以表示连续的变换。
矩阵表示对3D空间中的点进行平移转换缩放的效果。矩阵相乘的顺序决定了坐标转换的先后关系。调整缩放矩阵和平移矩阵的运算顺序之后,结果如下矩阵所示
1.1.3 投影、裁剪和光照
在图形绘制的过程中,我们可以尽情的对3D物体进行平移、缩放、旋转操作,实现图形处理,动画渲染等功能,最终还需要把虚拟3D世界中的物体投影到二维平面上进行显示。OpenGL 中常用的投影模式有两种,分别是正交投影和透视投影。
平行投影过程中,物体坐标沿平行线投影到观察平面上,物体比例不会发生变化,三维空间中的平行线依然保持平行关系;透视投影目标是提供视觉的真实感,并不保持相关比例。它基于物体相对于投影平面的距离远近来确定其投影的大小。
两种投影方式侧重点不同,应用的领域也不一样。平行投影保持了投影前后物体的比例关系,可以给观察着提供充足的信息来判断物体大小和形状,在工程制图,CAD中应用非常广泛。而透视投影利用深度信息根据物体距离投影面的远近决定物体的大小,能够模拟现实中“近大远小”的效果,满足观察的真实感,因此,在3D游戏或者仿真模拟领域中普遍使用透视投影。
并不是3D世界中的所有物体都可以被观察到,怎么样确定哪些物体是可见,哪些物体不可见呢?图形显示时,我们需要定义一个观察体来对视场中的景象进行裁剪。只有在观察体内的物体才可以输出到显示设备中,所有其它物体都被裁剪掉。观察体的形状和投影类型密切相关。对于正交投影而言,观察体定义成一个矩形平行六面体,而透视投影钟,观察体为一个棱台,采用“近小远大”的形式展现。
为了使生成的图形更具真实感,处理需要精确的数据模型信息外,还需要配合光照效果的使用,光照模型包括许多因素,例如物体形状,物体颜色,距离光源距离,光源类型等。
基本的光照模型包括环境光、漫射光、反射光。
环境光没有空间和方向特性,对于3D空间中的物体而言,各个方向上环境光的数量是恒定不变的。它表示了特定场景中的基准光亮强度,可以简单地模拟3D空间中不同物体表面所发射的同一照明。
在环境光的作用下,不同物体的各个表面上都得到相同的光照数量,但是由于材质的不同,各个表面对光照的放射强度却不相同,主要取决于材质属性。如果物体表面比较光滑,可以在某个观察方向看到高光或者强光,这种现象称为反射。在镜面反射角作用区域内,入射光全部或者大部分会称为反射光射向特定方向,这就是反射光。
一般来说光线都是由某些固定的光源发出的,这些光沿着一定方向照向物体表面,具有特定的方向。此时,为了模拟这种光照效果,我们需要考虑物体表面的属性,还要考虑光源属性,如位置、类型等。漫反射模型指物体表面受到光线照射后,均等地把光线反射到各个方向。
1.2 OpenGLES基本绘制流程
使用OpenGL ES进行图形绘制首先需要了解OpenGL ES管线渲染机制,管线渲染有时也称为渲染流水线,一般是借助显示芯片GPU内部处理图形信号的单元完成。换个角度来看,OpenGL ES的管线渲染实质上是一系列绘制过程,为绘制过程输入3D物体数据信息,经过渲染管线中各个环节的处理之后,输出一帧目标图像。
OpenGL ES 1.x渲染管线
GPU和CPU一样都是按照指令进行技术的,由于GPU具有维数众多的 计算单元,可以在前面的数据处理完成之前继续接受并处理后续数据,像流水线生产一样,效率会很高,这就是管线机制的原理。所谓固定管线就是你不需要知道指令是什么,也不需要修改处理流程,所有数据按照相同的流程进行处理。从OpenGL ES 1.x的固定管线流程图可以看出,基本绘制过程经过了物体数据基本处理、变换和光照、图元装配、光栅化、纹理环境和颜色求和、雾、Alpha测试和裁剪测试、深度测试和模板测试、颜色缓冲混合等一系列过程。
OpenGL ES 2.0中增加了顶点着色器和片元着色器,允许开发人员对图形绘制过程进行更多的控制,借助着色语言完成1.x难以完成的任务处理,灵活性大大增强。
OpenGL ES 2.0 渲染管线
可以看出,顶点着色器取代了OpenGL ES 1.x固定渲染管线的“变换和光照”环节,运行开发者使用着色器语言实现3D场景中的顶点变换、法线计算、纹理坐标处理、光照和材质的应用等。片元着色器取代了OpenGL ES 1.x中的“纹理环境和颜色求和”、“雾”和“Alpha 测试”等阶段。
着色器语言是一种高级的过程语言,基于C/C++语言的语法及流程控制。着色器语言提供了向量vec2,vec3,vec4等向量数据类型和mat2,mat3,mat4等矩阵类型的数据,并可以方便的完成矩阵和向量的运算,从而实现物体位置改变,形状缩放等变换操作。
1.3 OpenGLES处理视频帧数据
OpenGL ES作为业界标准,其图形处理能力广受业界认可,基于OpenGL API可以多图形、图像做个各种变换操作,完成复杂的效果处理。如果能够把这种渲染能力集成到视频转换过程的帧数据处理,应该是一个很棒的功能。现在我们需要借助Android平台研究如何实现OpenGL ES处理视频帧数据。
首先,我们考虑这样一个问题,视频播放时如何实现的呢?首先,视频播放也需要解码处理,解码之后的数据怎么样显示到手机屏幕上的呢?一般情况下,Android开发人员选择使用VideoView来实现,我们的目的不是研究VideoView如何使用,所以会忽略具体的使用步骤,有兴趣的读者可以参考Android开发的API进行了解。
这里,我们希望能够从VideoView跟踪下去找到一种获取视频帧数据的方法。通过调用setVideoPath可以为VideoView设置视频路径,之后VideoView会把自己的显示缓冲区域设置到MediaPlayer中。MediaPlayer是一个视频播放器,可以对视频文件进行解码,并输出到显示缓冲区,这样视频就可以在屏幕上显示了。可以看出显示缓冲区是帧数据存放的地方,通过正确的使用显示缓冲区,可以完成我们对视频帧数据的处理。
VideoView组件播放视频流程图
如果能够把显示缓冲区换成OpenGL ES提供的缓存就可以实现更多的控制了,通过分析OpenGL ES的API我们发现可以使用SurfaceTexture开辟一块缓存区域,用它接受解码器输出的帧数据。之后,把这块缓存中的数据传入到OpenGL ES的渲染管道,通过OpenGL ES通过的接口,完成对帧数据的转换处理。
借助OpenGL ES的图像渲染功能,我们可以完成修改视频分辨率、增加水印、对帧数据进行局部处理等。
第2章 视频转换技术研究
视频转换关键在于数据帧处理。本章将主要研究视频转换技术中的帧数据处理。
通常情况下,提起视频转码,大家首先想到的就是不同格式的视频文件之间的转换。本文研究的视频转换从范围上来说包括不同格式的视频转换,这种转换方式不但可以改变视频的格式,也可以同时修改视频的分辨率。另外,使用一组图片,并且定义图片之间的过渡动画效果,根据这些形成连续的动画效果,进而生成视频文件,也是我们研究的一部分。所以我们的视频转换技术包括两个方面:一、视频转码。将已经压缩编码的视频码流通过再编码的方式转换成另外一种码流。本质上是一个先解码再编码的过程。二、生成新视频。通过一定的编码方式将一组图片作为视频帧,同时帧间动画数据,然后编码成视频码流,并保存为视频文件。
通过我们描述的两种视频转换方式,你也许已经知道了,我们的研究工作主要集中在视频帧的处理方面,比如,视频分辨率调节、通过图片文件生成视频帧、根据两张或者多张图片产生过渡效果的视频帧等。这正是我们需要借助OpenGL ES 完成的工作,恰恰OpenGL ES擅长的就是图形渲染。
2.1 视频转码
视频文件是现代多媒体内容的重要部分。为了适应存储和访问的需要,人们设计了不同的视频文件格式来保存音频和视频到同一个文件中,以便于同步播放音频和视频信息。
目前流行的视频格式有:mp4、3gp、mov、rm、avi、wmv、asf、asx、mpeg、rmvb、flv等。大体上来看,视频格式可以分为适合本地播放的本地视频和适合在网络中播放的流媒体视频两大类。伴随着智能手机的出现,视频分享成为一种时尚,朋友之间总会通过视频进行互动。对视频的要求也越来越多,本地视频文件一般具有较高的分辨率,占用更多的带宽,不适合分享。首先,从文件大小方面来看,在传递速度一定的情况下,文件越大传递需要的时间越长,长时间等待会使用户放弃分享;其次,从数据流量来看,手机上网都是按照流量来收费的,文件越多需要的流量越多。几十M的流量瞬间就会被一个视频文件消耗掉,所以在流量的限制限制下,视频文件分享不没有得到充分利用,有时,看到朋友分享的视频文件,都不敢轻易打开。
能够用手机直接播放的、存储在手机内存或者存储卡的视频内容格式,我们称之为手机视频格式,它有别于网络流媒体。由于播放器支持的视频文件格式不同或者缺少相应格式解码器,或者受到外置设备的限制只能播放固定格式的视频,都会出现视频无法播放的现象。这是就需要对视频文件进行格式转换,来弥补这一缺陷。
目前来看,手机转视频时mp4格式是质量最好的,所以我们在Android平台视频转换的目标格式选择mp4。
2.2 视频格式转换
视频转码本质上是通过先解码再编码的方式实现将已经压缩编码的视频流转换成另外一种视频码流的过程。转换前后的视频码流可能使用相同的视频编码标准,也可能使用不同的视频编码标准。不同编码格式之间的转码需要改变视频编码格式。通常这种转码会改变现有的码流和分辨率。相同编码格式的转换一般不会改变压缩格式,只需要通过转码手段改变其码流或头文件信息,根据使用目的的不同,可以分为改变码流和不改变码流两种方式。
我们的目标是转换出适合手机使用的视频。所以为了避免由于播放器不支持视频格式解码或者缺少相关外置设备造成的视频无法播放,转成mp4格式的文件是一个很好的选择。而且即使视频转换不改变编码,一般也会通过降低分辨率和码率来解决视频过大不适合分享的问题。
视频转码是一个高负荷的运算过程,需要对视频码流进行全解码、处理视频帧数据(图像处理)、对输出格式进行全编码。
由于解码是一个高负荷的运算过程,特别是高清电影码率很高,需要较强的运算能力,而软解码实现依赖于软件的执行,这个过程由CPU处理。但是CPU的运算能力不足,解析高清电影相当吃力。相比较而言,显卡核心GPU的浮点运算能力远远强于CPU,因此,充分利用GPU的运算能力实现视频的解码成为硬解码的重要研究方向,同时,极大的降低了CPU负担。
一般来说,通过硬件实现的解码称为硬解码。硬解码主要依赖的器件是显卡核心GPU。这种处理方式CPU占用率很低,一般手机都集成了硬件解码功能。
视频转码市场以及不断增加的需求吸引了很多公司的关注。出于市场实际需求的考量,不少设备厂商在产品中集成了转码芯片。如松下的HDD录像机DIGA系列采用了UniPher芯片,能够在录制节目的同时将基于MPEG-2的数字电视信号转换成H.264格式,大幅提高视频录制时间。同样,日本的索尼和日立等公司也推出了采用自有芯片的具有转码功能的产品。
由于手机自身内存大小、CPU计算能力等硬件条件的限制,不适合做大量数据运算。根据PC解码视频的经验,使用GPU的运算能力处理视频帧数据是我们的重点研究方向。借助OpenGL ES 2.0通过的可编程管线渲染机制,充分利用OpenGL 提供的图形处理能力是我们处理视频原始数据的最佳选择。
OpenGL 处理原始视频数据
视频解码环节包括4个过程,通常包括获取文件、分离音视频流、解码、输出。视频来源可以是文件,也可以是UDP数据流,主要是把编码后的视频数据放入到内存缓冲区中。视频文件是一个容器,它包含了音频数据和视频数据并按照一定的标准合在一起,分离音视频流就是负责分离音频和视频数据的。视频和音频由各自的codec负责进行解码,之后得到原始数据流,我们把它输出到OpenGL通过的缓冲中。
原始数据输出到OpenGL的缓存区域后,就可以用OpenGL 对帧数据通过渲染的方式进行缩放、平移、透视处理了。
编码环节也需要4个过程,输入、编码、合成音视频流、写入文件。OpenGL处理完成后,数据格式没有发生改变,还是原始数据。我们把结果输出到编码器提供的缓冲中,就可以进行编码工作了,编码器根据设置好的视频参数对原始数据进行编码,然后交个合成器对音频和视频进行混合完成视频格式的封装。最后写入文件。
2.3视频压缩和分辨率转换
随着广播以及IP网络视频的发展,高清视频时代以及来临,PC时代的视觉盛宴让人们充分感受到互联带来的益处。当今市场,智能手机成为最重要的移动终端,为人们带来无处不在的互联体验。
视频娱乐市场以内容为王,能够实时转换任意格式的视频内容是未来市场发展的一个核心趋势,视频转码技术将会得到广泛应用。
视频转码技术的发展及不断增加的需求与互联网的发展密不可分。移动终端最为互联网接入的端口,重要性显而易见。不但能够解码,处理“看”的功能,还需要有计算处理能力,完成视频格式转换的工作,从而满足人们的互动需要。针对手机设备自身硬件资源的限制,充分高效的利用资源尤其显得重要。这也是我们研究视频帧数据处理的目的所在。
视频数据流转变为数据帧
首先,我们明确几个概念。
帧率:每秒显示的图片数。帧率越大,图片越多,画面越流畅,说明帧率影响画面的流畅度,并且与流畅度成正比。根据人类眼睛的生理结构,只要帧率高于16帧,就会感觉画面是连贯的,也就是所说的视觉暂留。帧率达到一定程度后,在增长人眼就不易感觉到了。
分辨率:分辨率描述了视频帧所对应画面的长宽,也就是图片尺寸。
码率:每秒显示的图片压缩后的大小就是码率。它影响文件体积大小。码率越大体积越大,码率越小体积越小()。
对于同一个视频来说,在编码算法相同的情况下,压缩比越高,画面质量越差。
清晰度:画面细腻,没有马赛克。和分辨率没有必然联系。
在码率一定的情况下,分辨率和清晰度是成反比的:分辨率越高,图像月不清晰,分辨率越低,图像越清晰。在固定分辨率的情况下,码率和清晰度成正比例关系,码率越高,图像越清晰,码率越低,图像越模糊。
考虑到人眼的视觉暂留因素,固定码率下,分辨率在一定范围内都是清晰的,同样,固定分辨率下,码率在一定范围是清晰的。
考虑到手机的屏幕尺寸,我们可以在降低码率的同时降低分辨率,更多的满足清晰度要求。
手机视频转换会在满足清晰度要求情况下,对视频分辨率进行裁剪,从而更大限度地降低码率,减小文件体积。
2.4 图片和音乐到视频的转换
从设备角度来看,视频是可以连续播放的一组图片,每帧数据对应一张图片,由于视频对应的数据帧数量巨大,而且视频数据帧内容通常有一定的相关性,所以可以通过一定的算法进行压缩,使得文件体积变小,这个过程也就是所谓的视频编码过程。视频编码过程根据目标文件的格式不同选择适合的压缩算法进行计算处理,在不影响视觉效果的情况下,尽量减小文件体积。
解码过程和编码过程目的是相反的,它会把视频文件按照一定的算法进行恢复,形成原始数据,每帧数据相当于一张图片,当所有数据帧连续刷新的时候,视觉效果上会感觉视频是连贯的,这就是视频播放的过程。
现在,我们再次把目光转移,从设备的角度来看,播放视频的过程就是使用一组连续的图片不断刷新屏幕的过程,进而形成视觉的连贯效果。它不关心每帧数据是从视频文件解码获得还是从许多图片文件中解析而来。
刷新一组图片的工作,可以使用OpenGL进行处理,而且,图片的切换过程可以通过动画来实现,比如,一张图片渐渐变暗知道消失,下一张图片则渐渐地从模糊变得清晰。如果考虑动画效果,我们就可以通过两张图片形成多帧数据。如下图所示:
图片数据转换为数据帧
如果我们把图片形成的帧数据显示到屏幕上,就可以像播放视频一样展示给用户;如果我们把这些数据帧传递给编码器,可以借助编码环节的把展示效果保存为视频文件。从而完成图片到视频的转换工作。
视频文件可以同时包含音频和视频内容,所以我们可以在编码环节增加背景音乐形成声色具备的展示效果。
第3章OpenGL ES在视频转换中的应用
本章继续介绍OpenGL ES在Android系统中的平台优势。并基于OpenGL ES进一步研究Android系统中如何实现视频播放,如何完成视频分辨率转换,如何使用OpenGL 实现视频帧数据的效果处理。
3.1 OpenGL和OpenGL ES
OpenGL是开放图形库,定义了一个跨平台的编程规格,可用于三维图像(二维亦可),采用LGPL或者GPL许可证。在行业领域中被广泛接纳,自其诞生之日起已经催生了许多优秀的应用程序。OpenGL独立于操作系统,广泛应用在CAD、能源、娱乐、游戏开发、制造业、制药业、虚拟现实等行业。OpenGL定义的软件接口与硬件无关,具有很好的移植性,应用非常广泛。
OpenGLES是OpenGL的子集,针对手机、PAD、游戏机等嵌入式系统设计。OpenGL ES是OpenGL的裁剪版,去除了四边形、多边形等复杂图元,这些图元不是绝对必要的特性。既然决定使用OpenGL ES,就需要决定使用的OpenGL ES版本,目前主要存在的OpenGL ES有OpenGL ES 1.0/1.1、OpenGL ES 2.0,这两个版本的差异非常大。其中最重要的是OpenGL ES2.0 使用了可编程管线技术,允许开发者通过编程的方式控制顶点着色器和片元着色器,实现比OpenGL ES1.x更酷的界面效果。
3.2 OpenGLES 2.0实现数据帧渲染
本章主要研究如何基于OpenGL ES的渲染能力完成纹理数据和视频帧数据的转换,实现视频帧数据的输入。
OpenGL ES是一个图像渲染管线状态机。EGL是为OpenGL ES提供平台独立性设计的,是OpenGL ES和底层Native平台视窗系统之间的接口。OpenGL ES的工作环境和参数配置都需要调用EGL的接口实现,如下图所示,OpenGL ES工作环境的配置主要有六个环节。
建立OpenGL ES工作环境。第一步需要获取EGLDisplay对象,EGLDisplay是一个关联系统物理屏幕的通用数据类型,可以通过调用EGL的接口eglGetDisplay获得。
然后,调用EGL的eglInitialize完成内部环境的初始化工作,该函数会回传一个EGL的版本号。接下来传递FrameBuffer的参数,通过eglChooseConfig获取满足要求的config。把选择结果config和display对象作为参数,传递给EGL的eglCreateWindowSurface方法,完成OpenGL ES工作缓存区域的创建,使用EGLSurface类型的数据结构回传缓存区域。
完成以上的工作后还需要使用EGL的eglCreateContext完成上下文的创建。从程序的角度看,管线渲染机制是一个巨大的状态机,而Context就代表了这个状态机,它保存当前设置的颜色、纹理坐标、矩阵信息、渲染模式信息等一大堆状态,程序的工作就是向Context提供图元、设置状态等工作。一帧绘制完成后,需要调用eglSwapBuffers来完成送显工作。
3.2.1Android系统视频播放原理
Android系统中,一般使用VideoView或者MediaPlayer实现视频播放功能。这是Google为开发者提供的视频播放功能组件。深入分析可以发现VideoView播放组件是基于MediaPlayer进行的二次封装。MediaPlayer则封装了视频解码送显功能,它把解码后的帧数据送入缓冲区Surface用于手机显示,这个Surface可以来自TextureView或者SurfaceView或者GLSurfaceView等,有了这个缓冲区,MediaPlayer只要把视频数据输出到目标缓冲区即可。对应的View组件管理这个缓冲区,收到数据后并提交手机显存,并通知手机在下次刷新屏幕的时候使用新数据,从而实现手机界面的连续刷新,进而形成连续的视觉效果。
从功能区分上来看,MediaPlayer完成了视频的解码工作,并将数据输出到指定的缓冲区(Surface),缓冲区Surface由显示组件View提供,数据到达后,Surface会回调View注册的监听,然后通过手机系统完成显示内容的刷新。
前面提到的GLSurfaceView它继承自SurfaceView,拥有自己的缓冲区,并提供了OpenGL 操作方式,借助于OpenGL ES 2.0 API,我们可以在送显之前完成画面效果的预处理,所以MediaPlayer播放视频的流程可以进一步细化,如下图所示。
这里我们使用OpenGL ES 纹理SurfaceTexture接入GLSurfaceView,实现MediaPlayer和缓冲区Surface的对接。MediaPlayer解码后的数据会自动输出到SurfaceTexture的缓冲区,通过在SurfaceTexture注册回到的方式,接收数据帧到达的通知,然后,借助OpenGL ES API实现对帧数据的处理,完成后再输出到显存Surface中。
3.2.2视频帧数据转换
视频文件作为重要的多媒体存储方式,包含了视频信息和音频信息。两者按照某种格式存储在同一个文件中。比较有名的文件格式如AVI、WMV、MPEG、MKV、MOV、OGG、MOD等。AVI把视频和音频混合在一起存储,只有一个视频轨道和一个音频轨道。而MPEG存储方式更多样,可以包涵多个视频、音轨、字幕等,能够适应不同的应用环境。MOV是苹果公司开发的容器,鉴于苹果公司在专业图形领域的统治地位,MOV成为电影制作行业的通用格式,处理音频、视频外还可以存储文字、图片等信息。
视频转换需要针对视频、音频分别处理。所以,首先我们需要完成视频的抽取和音频的抽取。过程如下图所示,针对抽取出来的音频和视频信息,逐帧解析视频数据和音频数据,并进行处理。下图描述了帧数据处理流程和基本环节。
如何借助OpenGL ES 进行视频帧数据的处理操作呢?
第一步,通过编码器Encoder创建一个Surface用于接收原始帧数据输入,然后根据前面介绍的方法建立OpenGL ES工作环境。使编码器的缓冲区Surface和OpenGL ES输出的EGLSurface建立联系,这样OpenGL ES操作的输出就会被送往编码器Encoder处理。
第二步,建立OpenGL ES纹理SurfaceTexture缓冲,设置渲染工作环境参数后,把SurfaceTexture对象绑定到OpenGL ES的内存区域。然后,使用纹理对象SurfaceTexture提供的缓冲区构建Surface缓冲提供给解码器Decoder,接收解码器输出的原始帧数据,这样解码器和OpenGL ES通过Surface关联SurfaceTexture的方式建立了映射关系。
解码器完成帧数据的解码后,结果自动输出到缓冲区(Surface),由于该缓冲区和SurfaceTexture具有对应关系,所以SurfaceTexture可以自动接收到编码器输出的帧数据。
注意,这里我们使用的是SurfaceTexture, 它在绑定到OpenGL ES 内存时需要指定target为OES(GL_TEXTURE_EXTERNAL_OES),OES类型的纹理和普通的纹理操作方式一样,借助OpenGL ES提供的API,可以通过矩阵运算完成平移、缩放等变换操作,从而实现对视频帧数据的转换。
其中,视频的解码和编码都是借助Android的平台能力完成的,所以我们只需要优雅的使用OpenGL ES进行渲染操作,而不用关心编解码的细节问题。
由于OpenGL ES 2.0支持可编程管线渲染,我们可以充分借助GPU的运算能力,完成对视频帧中像素点信息的计算和处理。
3.2.2帧数据效果处理
基于上一节的研究成果,建立了帧数据处理流程和框架,本节将继续探讨如何借助OpenGL ES API操作纹理对象,为帧数据增加水印和滤镜效果。
从上节内容的介绍可以知道,解码器输出的数据保存在SurfaceTexture中,这样OpenGL ES可以把帧数据当做纹理来处理,对它做旋转、移动或者缩放操作。而水印需要的是在帧数据上增加一些特殊的图案或者文字信息,对于熟悉OpenGL ES的开发人员应该不会陌生。实现处理纹理渲染外,OpenGL 还具有建模功能。试想,如果我们建立一个立方体,在它的六个面通过增加纹理的方式,把视频数据绘制在上面会是什么效果?而且我们甚至可以建立房屋或者体育馆的模型,并在墙壁上渲染视频帧数据,是不是有种在家看电视的感觉呢?说了这些,是不是感觉有些迫不及待要试一试了?其实,除此之外还有很多功能和效果可以通过OpenGL ES渲染视频帧的方式实现。
更多的创意等待您的发挥,以上只是笔者个人的想象,由于内容涉及到纹理渲染和空间建模,后续工作还有很多。到底我们该如何操作呢?
有了前两节的基础,相信您已经可以搭建自己的工作环境了,如果使用OpenGL ES 2.0您还需要学习OpenGL ES 着色语言的知识,学习可编程渲染管道的使用方式。
第一步,编写着色器脚本(顶点着色器和片元着色器)。“顶点着色器”可以帮助我们完成顶点变换、法向量计算、纹理坐标转换、光照和材质的应用等。片元着色器可以完成纹理环境和颜色求和、雾、Alpha测试等。色器语言源自广泛使用的C语言,同时它还具有RenderMan等着色语言的优良特性。有兴趣的读者可以通过网络或者购买相关书籍进行深入学习。
第二步,使用OpenGL ES 2.0 API构造渲染管道Program。
GLES20.glCreateShader(shaderType) |
创建着色器对象类型有两个值供选择GLES20.GL_VERTEX_SHADER(顶点着色器)、GLES20.GL_FRAGMENT_SHADER(片元着色器),返回值为着色器句柄。 |
GLES20.glShaderSource(shader, source); |
关联着色器脚本,建立着色器和脚本关联关系 |
GLES20.glCompileShader(shader); |
编译着色器脚本 |
int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); |
检测着色器编译结果,只有成功后才可以进行后续操作 |
GLES20.glCreateProgram(); |
创建着色器程序,返回值为程序句柄 |
GLES20.glAttachShader(program, vertexShader); |
向程序中加入着色器 |
GLES20.glLinkProgram(program); |
链接 |
int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); |
检查着色器程序链接结果,返回数据保存在linkStatus中。 |
GLES20.glGetAttribLocation |
获取属性变量句柄 |
GLES20.glGetUniformLocation |
获取一致变量句柄 |
着色器操作是基于句柄的操作,着色器程序初始化完成后,需要根据着色器脚本获取相关的属性变量句柄和一致变量句柄,以便为对应的变量赋值。
第三步,绑定视频数据和水印对应的纹理。在OpenGL的世界里每个纹理都有自己的ID编号,同时纹理还需要对应到指定的target上。比如:视频数据输出使用的SurfaceTexture对应的是GL_TEXTURE_EXTERNAL_OES,水印纹理则使用了GL_TEXTURE_2D类型的target。绑定一个纹理之后,OpenGL 会在后续的操作中一直使用这个纹理,直到重新绑定其它纹理。
这三步只是完成了OpenGL ES 2.0工作环境的准备工作,希望实现什么样的效果,需要我们通过建模的方式实现。当然,做为初学者,我们暂时忽略这一部分,完全当成在平面上画画一样。现在要做的就是操作OpenGL ES 完成纹理的渲染,实现帧数据处理效果了。我们可以设置帧数据的透明度、位置、缩放等。也可以控制水印的形状和位置,这些由你决定。
3.3本章小结
本章完成了OpenGL ES 工作环境整体搭建,并提供了帧数据接入OpenGL ES的实现方案。使用SurfaceTexture构造缓冲区Surface接收解码器的输出数据,并以纹理的方式嵌入到OpenGL 纹理中,然后使用OpenGL ES API进行视频帧数据的处理。由于在OpenGL ES工作环境的搭建中已经使用EGLSurface对编码器的输入Surface进行了包装,所以我们所绘制的视频数据可以直接输出到编码器的缓冲队列中。这样我们可以把精力集中在操作OpenGL实现视频帧数据处理的工作上了,甚至可以通过建模的方式实现更酷的功能。
第4章 基于OpenGLES的Android视频转换功能实现
基于前面对OpenGL ES 2.0 API的研究,结合Android系统提供的平台能力。本章分别从以下三个方面,分析OpenGL ES转换视频数据的可行性方案,并提供部分关键代码。
图片转换为视频。这个功能从图片数据出发,研究探索如何借助OpenGL的图形渲染能力生成视频数据。
视频到视频。包括分辨率修改、格式转换等,实现视频压缩功能。
根据图片、音乐、动画效果生成视频,完成MV制作功能。
4.1图片转换为视频
单张图片转换为视频的基本流程如下:
图片解析完成后,形成Bitmap对象,借助Android平台提供的API可以实现上传到OpenGL ES内存的功能,每个Bitmap对应一个图片文件,也对应一个OpenGL 纹理,借助OpenGL ES的图形绘制能力我们可以把图片输出到Surface缓冲区,形成原始帧数据,并提交到编码器Encoder完成编码工作,之后,输出到合成器生成一个视频文件。这个功能比较简单目前已经应用于笔者工作单位的大部分旗舰机型。
4.1.1使用OpenGL输入图片数据
基于前面介绍的概念,后续章节将通过提供关键代码的方式进一步解释如何实现视频转换。
根据缓冲区创建的顺序,我们首先使用android.media.MediaCodec构建video/avc类型编码器Encoder,并请求编码器创建一个输入缓冲区Surface。
这里我们有必要结合转视频的需要,进一步讲解如何建立OpenGL ES工作环境,如何设置OpenGL ES输出和编码器的输入Surface关联关系。相关API如下表所示:
EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) |
获取EGLDisplay对象,并把结果和EGL14.EGL_NO_DISPLAY比较,确认操作是否成功。 |
EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1) |
传递上一步获取到的,EGLDisplay对象,进行内部初始化工作 |
EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0) |
attribList保存了一系列参数,并且以EGL14.EGL_NONE结束,函数返回不多于numConfigs数量的config,并把结果保存在configs数组中。Config描述了FrameBuffer的格式和能力,根据需要选择一个合适的config进行工作。设置帧数据缓冲参数 |
EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0) |
EGLContext使用个巨大的状态机,记录了当前的颜色、纹理坐标、矩阵变换等状态信息,程序的工作就是设置Context的状态信息和获取图元、根据状态进行工作 |
EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, surfaceAttribs, 0) |
Surface是一个缓冲区FrameBuffer,其中参数mSurface使用编码器传递过来的输入缓冲区。这样,可以把OpenGL的输出直接传递到编码器处理。 EGLSurface包括widow surface、pbuffers surface、pixmaps surface三大类: configs[0]描述了EGLSurface需要的颜色深度、类型、辅助缓冲等信息。 |
EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext) |
设置当前上下文环境和输出缓冲区。使mEGLContext成为当前使用的状态。一个线程最多可以关联一个Context,而同一时刻,一个Context只能被一个线程设置为当前。 |
EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface) |
交换缓冲区数据,拷贝颜色缓冲到本地窗口,实现送显功能。 |
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs) |
设置当前帧在视频中的时间 |
完成以上工作后,OpenGL和编码器的衔接关系基本建成,此时已经可以启动编码器(mEncoder.start())。然后可以借助于OpenGL ES的纹理处理方案,我们上传Android平台解析到的图片数据Bitmap到OpenGL ES 内存,使用OpenGL ES 2.0的接口,完成纹理的渲染工作。
首先,使用GLES20.glBindTexture绑定纹理到GL_TEXTURE_2D类型的target上,后续操作都会基于这个Texture对象,GLUtils提供了上传纹理的实现,使用GLUtils.texImage2D(GL11.GL_TEXTURE_2D,0, bitmap, 0)上传纹理到GPU。纹理上传一次即可,但是绑定操作却可以多次调用。
准备好OpenGL 纹理之后,可以根据业务需要操作纹理数据,实现渲染效果,纹理的操作都是基于OpenGLES 2.0的,属于帧数据输出实现,具体实现参考下一章节。
4.1.2输出帧数据
在4.2.2一节,我们介绍了可编程管线渲染的主要初始化工作内容,如果要实现纹理的绘制,还需要了解纹理坐标设置和矩阵运算相关知识。
纹理坐标使用浮点数表示,范围从0.0到1.0.
GLES20.glUseProgram(mProgram) |
选择需要使用特定的着色程序进行绘制。 |
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, muMVPMatrix, 0); |
传递浮点类型矩阵到着色程序,并赋值给统一变量,控制整体矩阵变换。 |
GLES20.glVertexAttribPointer ( maPositionHandle, 3, GLES20.GL_FLOAT, false, 3*4, mVertexBuffer ); |
传递3个浮点数组成的顶点位置信息矢量到着色程序 |
GLES20.glVertexAttribPointer ( maTexCoorHandle, 2, GLES20.GL_FLOAT, false, 2*4, mTexCoorBuffer ); |
传递2个浮点数组成的纹理位置信息矢量到着色程序 |
GLES20.glEnableVertexAttribArray(maPositionHandle); GLES20.glEnableVertexAttribArray(maTexCoorHandle); |
启动前面两个步骤设置的位置信息(读取位置信息到着色程序)。 |
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
|
绑定纹理到目标target上。从而OpenGL ES可以使用当前纹理进行绘制工作。 |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); |
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount) |
绘制纹理矩阵。 |
以上操作步骤,描述了,如果设置和启用纹理的基本步骤。
如果我们使用的GLSurfaceView来实现OpenGL ES绘制,这个结果将会显示到手机屏幕上。
4.2视频压缩
广义上来说,视频压缩可以使用更小的码率配合分辨率来完成有损压缩,也可以通过算法优化,在不改变视频分辨率和播放效果下完成。对于手机用户来说,由于手机屏幕较小,减小分辨率对播放效果影响不大。压缩视频可以使用减小码率和分辨率的方式来完成,毕竟流量才是最重要的。
基于这个方向,我们将借助平台的编解码能力,完成视频格式的转换,并且通过OpenGL ES完成帧数据的缩放,和视频分辨率的调整。
4.2.1视频编解码流程分析
在第4章中我们展示了视频格式转换的基本步骤。本节将进一步分析视频格式转换工作的处理流程。
根据处理流程我们按照视频/音频抽取器、解码器、编码器、合成器模块划分来研究如何使用OpenGL ES完成视频格式压缩。
一、视频/音频抽取器(MediaExtractor)。通过setDataSource方法传递原始文件路径到抽取器后,可以使用getTrackCount获取当前文件包含的track数量。然后,取出track的MediaFormat(getTrackFormat方法)并检查是Video还是Audio。
使用MediaExtractor可以获取文件原始视频帧数据,并传递到解码器进行解码操作。然后调用advance可以切换到下一帧。
循环处理知道advance返回值为false,表示文件读取结束。
二、解码器(MediaCodec)。调用MediaCodec的createDecoderByType创建解码器,通过configure设置输入视频格式和输出数据时缓冲Surface。这个Surface关联到了SurfaceTexture上,输出内容可以通过OpenGL ES进行渲染操作。
三、编码器(MediaCodec)。调用MediaCodec的createEncoderByType创建编码器,通过configure设置输出视频格式和编码器工作模式。在视频输出格式中我们可以通过指定视频宽高来改变视频的分辨率。
调用编码器的createInputSurface创建输入缓存,然后调用makeCurrent(EGL14.eglMakeCurrent的封装)关联当前OpenGL 的上下文环境。后续OpenGL 工作时会把内容输出到当前Surface,形成编码器的输入内容。
四、合成器(MediaMuxer)。MediaMuxer基于手机自带媒体库构建,完成视频的合成工作。构造合成器对象时需要传递文件路径和输出文件格式,目前支持的输出视频只有mp4格式。确定音频和视频的MediaFormat之后,调用addTrack添加到合成器,然后才可以start。一旦调用过start之后,就不可以addTrack到Muxer中了。接下来就是使用writeSampleData把编码器的编码结果写入到文件中去。
4.2.2基于OpenGL实现帧数据转换处理
介于解码器和编码器之间的工作是处理每一帧数据,这个过程使用OpenGLES实现的。
在编码器的输出MediaFormat中,我们设置了目标视频的宽高信息,编码器会根据创建inputSurface的时候会创建对应大小的缓冲区。这个尺寸和原始视频的尺寸很可能是不一样的,怎么办呢?没关系,借助OpenGL ES我们可以实现帧数据的自由缩放。所以,解码器放心的向outputSurface输出数据,编码器放心的从inputSurface读取数据就可以了。OpenGL ES通过矩阵操作实现对SurfaceTexture的变换,按照纹理的方式完成渲染工作就行了。