Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android OpenGLES2.0(十三)——流暢的播放逐幀動畫

Android OpenGLES2.0(十三)——流暢的播放逐幀動畫

編輯:關於Android編程

在當前很多直播應用中,擁有給主播送禮物的功能,當用戶點擊贈送禮物後,視頻界面上會出現比較炫酷的禮物特效。這些特效,有的是用粒子效果做成的,但是更多的時用播放逐幀動畫實現的,本篇博客將會講解在Android下如何利用OpenGLES流暢的播放逐幀動畫。在本篇博客中的動畫素材,是從花椒直播中“借”出來的(只做學習交流用,應該不構成侵權吧:-D)。

逐幀動畫的實現方案分析

有些朋友看到逐幀動畫可能會想,逐幀動畫還不容易嗎?Android中的動畫本來就支持逐幀動畫啊,不是分分鐘就能實現麼?沒錯,用Android的Animation的確很容易就實現了逐幀動畫。但是用Android的Animation實現動畫,當圖片要求較高時,播放會比較卡。為什麼呢?
Png圖片並不能在被直接用來播放動畫,它需要先被解碼成Bitmap,才能被繪制到屏幕上。而這個解碼是一個比較耗時的工作。而且解碼時間與手機、CPU工作狀態、Png圖片內容都有很大的關系。當圖片較小時,播放出來的逐幀動畫效果還不錯,但是當圖片較大時,比如720*720,解碼時間就往往需要100多ms,甚至會達到200ms以上。這個時間讓我們很難以接受。
那麼怎麼辦呢?限制動畫的是PNG解碼時間,而不是渲染時間,用OpenGL做渲染又有什麼用呢?是的,用OpenGL來播放PNG逐幀動畫,雖然比用Animation會有一些改善,但是並不能解決動畫播放卡頓的問題。(當初天真的以為Animation播放動畫是因為Animation用CPU繪制導致卡頓,然後改成用GPU來做,發現然並卵,這才把視線放到PNG解碼上了。)
既然是PNG解碼占用時間,那麼能不能直接用BMP格式存儲圖片,來做動畫呢?這樣解碼的時間就基本可以忽略了。那麼問題又來了,BMP是不進過壓縮的,一張720*720的PNG圖片大小轉成BMP就為720*720*4/1024=2025kb,那麼一秒25幀動畫,就要二十四五兆了。顯然是難以讓人接受的。那麼怎麼辦呢?以下為Android下OpenGLES實現逐幀動畫的方案比較:

待選方案

直接2D紋理逐幀加載PNG 使用ETC壓縮紋理替代PNG 使用ETC2壓縮紋理替代PNG 使用PVRTC壓縮紋理替代PNG 使用S3TC壓縮紋理替代PNG

文件大小對比

PNG圖片大小與其內容有關,透明區域越多,大小越小。 ETC1圖片每個像素占0.5byte,720*720png變為ETC後大小為720*720*2*0.5+16(alpha通道導致文件高度增加一倍,16個字節為文件頭部信息),約507KBytes。 ETC2大小與設置相關,不包含A通道,大小與ETC1不保留A通道相同,包含A通道的,與ETC1保留A通道相同。 S3TC 相對於24位原圖,DXT1壓縮比例為6:1,DXT2-DXT5壓縮比例為4:1。 PVRTC4 壓縮比為6:1,PVRTC2壓縮比為12:1(PVRTC圖片寬高為2的冪數)

文件支持對比

PNG通用 ETC1是OpenGL2.0支持標准,基本上所有支持OpenGLES2.0,版本不低於2.2的Android設備都能使用。 ETC2是OpenGL3.0支持標准,基本上所有支持OpenGLES3.0,版本不低於4.3的Android設備都能使用。 S3TC廣泛用於Windows平台上,DirectX中使用較多。在Android上支持率很低,主要是NVIDIA Tegra2芯片的手機。 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);

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

源碼

所有的代碼全部在一個項目中,托管在Github上——Android OpenGLES 2.0系列博客的Demo

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved