在当前很多直播应用中,拥有给主播送礼物的功能,当用户点击赠送礼物后,视频界面上会出现比较炫酷的礼物特效。这些特效,有的是用粒子效果做成的,但是更多的时用播放逐帧动画实现的,本篇博客将会讲解在Android下如何利用OpenGLES流畅的播放逐帧动画。在本篇博客中的动画素材,是从花椒直播中“借”出来的。

逐帧动画的实现方案分析

有些朋友看到逐帧动画可能会想,逐帧动画还不容易吗?Android中的动画本来就支持逐帧动画啊,不是分分钟就能实现么?没错,用Android的Animation的确很容易就实现了逐帧动画。但是用Android的Animation实现动画,当图片要求较高时,播放会比较卡。为什么呢?

Png图片并不能在被直接用来播放动画,它需要先被解码成Bitmap,才能被绘制到屏幕上。而这个解码是一个比较耗时的工作。而且解码时间与手机、CPU工作状态、Png图片内容都有很大的关系。当图片较小时,播放出来的逐帧动画效果还不错,但是当图片较大时,比如720720,解码时间就往往需要100多ms,甚至会达到200ms以上。这个时间让我们很难以接受。

那么怎么办呢?限制动画的是PNG解码时间,而不是渲染时间,用OpenGL做渲染又有什么用呢?是的,用OpenGL来播放PNG逐帧动画,虽然比用Animation会有一些改善,但是并不能解决动画播放卡顿的问题。(当初天真的以为Animation播放动画是因为Animation用CPU绘制导致卡顿,然后改成用GPU来做,发现然并卵,这才把视线放到PNG解码上了。)

既然是PNG解码占用时间,那么能不能直接用BMP格式存储图片,来做动画呢?这样解码的时间就基本可以忽略了。那么问题又来了,BMP是不进过压缩的,一张720720的PNG图片大小转成BMP就为7207204/1024=2025kb,那么一秒25帧动画,就要二十四五兆了。显然是难以让人接受的。那么怎么办呢?以下为Android下OpenGLES实现逐帧动画的方案比较:

待选方案

1. 直接2D纹理逐帧加载PNG

2. 使用ETC压缩纹理替代PNG

3. 使用ETC2压缩纹理替代PNG

4. 使用PVRTC压缩纹理替代PNG

5. 使用S3TC压缩纹理替代PNG

文件大小对比

1. PNG图片大小与其内容有关,透明区域越多,大小越小。

2. ETC1图片每个像素占0.5byte,720*720png变为ETC后大小为720*720*2*0.5+16(alpha通道导致文件高度增加一倍,16个字节为文件头部信息),约507KBytes。

3. ETC2大小与设置相关,不包含A通道,大小与ETC1不保留A通道相同,包含A通道的,与ETC1保留A通道相同。

4. S3TC 相对于24位原图,DXT1压缩比例为6:1,DXT2-DXT5压缩比例为4:1。

5. PVRTC4 压缩比为6:1,PVRTC2压缩比为12:1(PVRTC图片宽高为2的幂数)

文件支持对比

1. PNG通用

2. ETC1是OpenGL2.0支持标准,基本上所有支持OpenGLES2.0,版本不低于2.2的Android设备都能使用。

3. ETC2是OpenGL3.0支持标准,基本上所有支持OpenGLES3.0,版本不低于4.3的Android设备都能使用。

4. S3TC广泛用于Windows平台上,DirectX中使用较多。在Android上支持率很低,主要是NVIDIA Tegra2芯片的手机。

5. PVRTC只有PowerVR的显卡支持。在苹果系中使用广泛。

方案选择

根据上述分析,在Android中使用OpenGLES加载动画:

方案4和方案5由于支持问题,直接排除了。

方案1可以使用

当前Android市场Android2.2以下设备基本不没有了,Android2.2及以上到Android4.3下,占比15%左右。所以方案2与方案3之中,取方案2。

选择方案1与方案2进行对比。

方案1和方案2数据

针对测试用的60张png烟花图片动画进行量化分析(图片大小为720*720,手机360F4):

PNG图片总大小为4.88M,ETC总大小29.6M。

PNG IO+解码耗时为15-40ms之间,与单张图片大小有关。ETC不在CPU中解码,只有IO时间,为4-10ms之间。(IO及解码时间与CPU能力及状态有关)

渲染时间二者基本一致。

针对方案2的补充方案

方案2文件总大小太大,针对这个问题,可采用zip压缩纹理,加载时直接加载zip中的纹理文件。数据如下:

总大小7.05M

IO+解码时间为4-16ms。

渲染时间同不进行压缩的ETC

注:不同手机不同环境时间数据不同,此数据仅为PNG加载和压缩纹理方式加载的对比。

播放ZIP包下的ETC1压缩纹理逐帧动画

这种方式,主要是针对PNG透明区域比较多的图片,这样压缩纹理会比PNG大很多,ZIP压缩一下可以压缩的和PNG大小差不多。先直接说在实现过程中踩到的坑吧。

存在的坑

在Mali 官网工具中提供的三个方法中,方法一纹理拼图最简单,但是有的图片在边界处会出现奇怪的线条。这是因为纹理采样的时候,RGB和Alpha压缩在一个文件中,在边界处采样会采样过界,导致颜色不对。方法三虽然使用上步会出什么问题,但是单独的Alpha通道依旧会占用更多空间和内存带宽。所以选方法二。

ZIP打包所有的ETC压缩纹理时,命名上保证顺序,图片数字前要补0,比如有100张图片,变成了200个pkm文件,最后一个为p100alpha.pkm,倒数第二个为p100.pkm。那么第一个应该为p001.pkm,而不是p1.pkm。其他的类似。这个是遍历文件夹、ZIP包的顺序纹理。

Android提供的ETC1Util工具类的 ETC1Util.createTexture(InputStream in)方法有坑。具体问题,后面贴代码的时候说。

实现

压缩纹理的加载,OpenGLES 提供了GLES10.glCompressedTexImage2D(int target,int level,int internalformat,int width,int height, int border,int imageSize,java.nio.Buffer data) 方法,但是在Android中,可以用工具类ETC1Util提供的loadTexture(int target, int level, int border,int fallbackFormat, int fallbackType, ETC1Texture texture)方法来更简单的使用。

这样,我们就需要先得到一个ETC1Texture,而ETC1Util又提供了创建ETC1Texture的方法,上面说过,这个方法在使用中有点小坑,其源码为:

public static ETC1Texture createTexture(InputStream input) throws IOException {

int width = 0;

int height = 0;

byte[] ioBuffer = new byte[4096];

{

if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {

throw new IOException("Unable to read PKM file header.");

}

ByteBuffer headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)

.order(ByteOrder.nativeOrder());

headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);

if (!ETC1.isValid(headerBuffer)) {

throw new IOException("Not a PKM file.");

}

width = ETC1.getWidth(headerBuffer);

height = ETC1.getHeight(headerBuffer);

}

int encodedSize = ETC1.getEncodedDataSize(width, height);

ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());

for (int i = 0; i < encodedSize; ) {

int chunkSize = Math.min(ioBuffer.length, encodedSize - i);

if (input.read(ioBuffer, 0, chunkSize) != chunkSize) {

throw new IOException("Unable to read PKM file data.");

}

dataBuffer.put(ioBuffer, 0, chunkSize);

i += chunkSize;

}

dataBuffer.position(0);

return new ETC1Texture(width, height, dataBuffer);

}

修改为:

ETC1Util.ETC1Texture createTexture(InputStream input) throws IOException {

int width = 0;

int height = 0;

byte[] ioBuffer = new byte[4096];

{

if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {

throw new IOException("Unable to read PKM file header.");

}

if(headerBuffer==null){

headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)

.order(ByteOrder.nativeOrder());

}

headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);

if (!ETC1.isValid(headerBuffer)) {

throw new IOException("Not a PKM file.");

}

width = ETC1.getWidth(headerBuffer);

height = ETC1.getHeight(headerBuffer);

}

int encodedSize = ETC1.getEncodedDataSize(width, height);

ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());

int len;

while ((len =input.read(ioBuffer))!=-1){

dataBuffer.put(ioBuffer,0,len);

}

dataBuffer.position(0);

return new ETC1Util.ETC1Texture(width, height, dataBuffer);

}

这个方法,是通过InputStream得到一个ETC1Texture,所以我们直接读取Zip下的文件生成ETC1Texture就算完成了一大半工作了。读取Zip下的文件代码网上很容易找到,这里直接贴出Demo中的ZipPkmReader:

public class ZipPkmReader {

private String path;

private ZipInputStream mZipStream;

private AssetManager mManager;

private ZipEntry mZipEntry;

private ByteBuffer headerBuffer;

public ZipPkmReader(Context context){

this(context.getAssets());

}

public ZipPkmReader(AssetManager manager){

this.mManager=manager;

}

public void setZipPath(String path){

Log.e("wuwang",path+" set");

this.path=path;

}

public boolean open(){

Log.e("wuwang",path+" open");

if(path==null)return false;

try {

if(path.startsWith("assets/")){

InputStream s=mManager.open(path.substring(7));

mZipStream=new ZipInputStream(s);

}else{

File f=new File(path);

Log.e("wuwang",path+" is File exists->"+f.exists());

mZipStream=new ZipInputStream(new FileInputStream(path));

}

return true;

} catch (IOException e) {

Log.e("wuwang","eee-->"+e.getMessage());

e.printStackTrace();

return false;

}

}

public void close(){

if(mZipStream!=null){

try {

mZipStream.closeEntry();

mZipStream.close();

} catch (Exception e) {

e.printStackTrace();

}

if(headerBuffer!=null){

headerBuffer.clear();

headerBuffer=null;

}

}

}

private boolean hasElements(){

try {

if(mZipStream!=null){

mZipEntry=mZipStream.getNextEntry();

if(mZipEntry!=null){

return true;

}

Log.e("wuwang","mZip entry null");

}

} catch (IOException e) {

Log.e("wuwang","err dd->"+e.getMessage());

e.printStackTrace();

}

return false;

}

public InputStream getNextStream(){

if(hasElements()){

return mZipStream;

}

return null;

}

public ETC1Util.ETC1Texture getNextTexture(){

if(hasElements()){

try {

ETC1Util.ETC1Texture e= createTexture(mZipStream);

return e;

} catch (IOException e1) {

Log.e("wuwang","err->"+e1.getMessage());

e1.printStackTrace();

}

}

return null;

}

private ETC1Util.ETC1Texture createTexture(InputStream input) throws IOException {

int width = 0;

int height = 0;

byte[] ioBuffer = new byte[4096];

{

if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {

throw new IOException("Unable to read PKM file header.");

}

if(headerBuffer==null){

headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)

.order(ByteOrder.nativeOrder());

}

headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);

if (!ETC1.isValid(headerBuffer)) {

throw new IOException("Not a PKM file.");

}

width = ETC1.getWidth(headerBuffer);

height = ETC1.getHeight(headerBuffer);

}

int encodedSize = ETC1.getEncodedDataSize(width, height);

ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());

int len;

while ((len =input.read(ioBuffer))!=-1){

dataBuffer.put(ioBuffer,0,len);

}

dataBuffer.position(0);

return new ETC1Util.ETC1Texture(width, height, dataBuffer);

}

}

Shader直接使用Mali 官网上方法2提供的Shader即可,然后在开启一个定时器,定时requestRender,加载下一帧压缩纹理。动画播放就基本完成了。为了简便,Demo中直接在在GL线程中Sleep然后requestRender的。

这里也贴上Shader的代码吧。

顶点Shader:

attribute vec4 vPosition;

attribute vec2 vCoord;

varying vec2 aCoord;

uniform mat4 vMatrix;

void main(){

aCoord = vCoord;

gl_Position = vMatrix*vPosition;

}

片元Shader:

precision mediump float;

varying vec2 aCoord;

uniform sampler2D vTexture;

uniform sampler2D vTextureAlpha;

void main() {

vec4 color=texture2D( vTexture, aCoord);

color.a=texture2D(vTextureAlpha,aCoord).r;

gl_FragColor = color;

}

可以看到,在片元着色器中,我们需要两个Texture,一个包含着原来PNG图片的RGB信息,一个包含着原PNG图片的Alpha信息。这些信息并不是完全和原PNG信息相同的,压缩纹理在色彩上会有一些损失。

片元着色器中用到了两个采样器,纹理传入的代码为:

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]);

ETC1Util.loadTexture(GLES20.GL_TEXTURE_2D,0,0,GLES20.GL_RGB,GLES20

.GL_UNSIGNED_SHORT_5_6_5,t);

GLES20.glUniform1i(mHTexture,0);

GLES20.glActiveTexture(GLES20.GL_TEXTURE1);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[1]);

ETC1Util.loadTexture(GLES20.GL_TEXTURE_2D,0,0,GLES20.GL_RGB,GLES20

.GL_UNSIGNED_SHORT_5_6_5,tAlpha);

GLES20.glUniform1i(mGlHAlpha,1);

其他地方就和之前渲染图片差不多了。

android opengl 帧动画,Android OpenGLES2.0(十三)——流畅的播放逐帧动画相关推荐

  1. Android OpenGL Cannot create GL program: 0 GL error: 1282

    Android OpenGL create GL program: 0 & GL error: 1282 快速解决 1. 使用GLSurfaceView的话 请在继承类中合适的地方(一般是构造 ...

  2. android 动画后动画效果,Android5.0之后 VectorDrawable实现超炫酷动画效果

    标签介绍: , , , 项目中还是用到了一些动画的标签,这里就不做展示了 path android:name 定义该 path 的名字,这样在其他地方可以通过名字来引用这个路径 android:pat ...

  3. android opengl流程,【Android OpenGL ES】Android Opengl ES创建流程

    在android 1.0rc2 sdk中,提供了以下包支持Opengl ES 编程: 一.openglES包 android.opengl Class: GLDebugHelper:用于调试OpenG ...

  4. android opengl 简书,Android OpenGL入门

    如今VR这么火,感觉有必要先把OpenGL学好,为以后转VR奠定一些基础.一年前,接触过Android的OpenGL,当时是实现了在Android上显示标准的3D文件(STL格式).现在打算整理一下O ...

  5. android opengl美颜功能,Android短视频中如何实现720P磨皮美颜录制

    在Android上要实现一个录制功能,需要有几个方面的知识储备:自定义相机的开发.视频数据格式的了解.编码相关知识以及视频合成技术,同时如果需要美颜.磨皮等滤镜操作还需要一定的openGL的知识.如果 ...

  6. OpenGLES2.0渲图步骤:绘几何图形、图片处理、离屏渲染(3)

    OpenGLES2.0是一个图形渲染(图形处理)库. OpenGL ES 2.0渲染过程为:读取顶点数据--执行顶点着色器--组装图元--光栅化图元--执行片元着色器--写入帧缓冲区--显示到屏幕上. ...

  7. OpenglES2.0 for Android:第一个OpenglES应用

    OpenglES2.0 for Android:第一个OpenglES应用 首先我们新建一个Android工程:com.opengl.openglestest 打开MainActivity,定义一个G ...

  8. Android OpenGL ES 2.0绘制简单三角形

    实现步骤 l  实现一个工具类ShalderUtil,用于将着色器代码加载进显卡进行编译 l  实现一个三角形Triangle类 在该类中加载着色器.初始化顶点数据.初始化着色器以及绘制三角形方法 l ...

  9. android 人物行走动画,android 3D 游戏实现之人物行走(MD2)

    如果充分理解了HelloWorld(见android 3D 游戏实现之First step),了解了一些JPCT-AE游戏框架的机制, 那么,这个示例可以是进阶篇了,其实用JPCT-AE加载3DS,M ...

  10. 【Android OpenGL ES】阅读hello-gl2代码(二)Java代码

    AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xm ...

最新文章

  1. java struts2模拟百度百科图片中的防盗链设置
  2. iOS开发里的Bundle是个啥玩意?!
  3. 数学之美笔记(二十)
  4. vscode安装python插件_python之VSCode
  5. Oracle数据库中的SOUNDEX函数
  6. 背压加载文件– RxJava常见问题解答
  7. 安装Windows 10 V1909对CPU有什么要求?
  8. SpringBoot2注解配置定时任务和异步执行任务
  9. 以太坊平台评估 私有链和联盟链的机会与挑战
  10. 海军领域搜狗细胞词库
  11. 移动硬盘备份linux系统盘,Ubuntu 系统备份到移动硬盘(tar) 还原到另一台电脑
  12. linux excel自动换行,Excel中巧用样式列表快速实现文本换行
  13. 获取url地址栏后面的参数
  14. 全球首只AIGC动画短片发行,日漫风格超治愈!
  15. 解决:java.lang.IllegalStateExceptio:Underflow in restore - more restores than saves异常,Module闪退
  16. 分析谁是2020欧洲杯的最佳球员
  17. 中国量化在AI全球盛会上的惊艳亮相
  18. 什么是防雷接地,防雷接地工程的作用和重要意义
  19. Cesium中gltf模型的坐标系
  20. CentOS /Linux 开放80、8080端口或者开放某个端口

热门文章

  1. python拟合曲线_用python做曲线拟合
  2. html 中thead标签,HTML thead 标签
  3. IDEA导入已有项目
  4. 计算机毕业设计-SSM商场餐厅管理系统-JavaWeb商场餐厅管理系统
  5. 线性稳压芯片的选取要素
  6. 中台建设利器-SPI插件机制
  7. 国家AAAAA级旅游景区数量统计
  8. Android5.1-s5p6818平台adb push 、adb install/uninstall的疑问
  9. 计算机的输入和输出设备
  10. 辽宁计算机专业大学排名及分数线,辽宁一本大学排名及分数线2021