techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

ETC1を使ってデータ量を少なくする

Androidで画像リソースを利用する場合はPNGが多いと思いますが、PNGは可逆圧縮のフォーマットのためどうしても一定サイズ以下にデータを小さくできません。
そこでよく行われるのPNGを最適化して使ったり、WebP、JPGなどが良くあります。
最近ではVectorDrawableを利用されるケースもあります。
しかし今回はOpenGLなどで使われるETC(Ericsson Texture Compression)を使って画像表示を行ってみましょう。

ETCとは

ETCはEricsson Reserchが開発した圧縮テクスチャというものです。
他にもPVRTC、DXTCなどが取り上げられたりします。
iOSはPVRTCをサポートしているが、AndroidはPVRTCをサポートしている端末が一部のためETCを利用されるケースが多いそうです。
ETCはAndroidのすべての端末がサポートしておりますが、非可逆圧縮のため多少画像が劣化します。
またアルファ値を扱うことができないためアルファ値を利用する画像には不向きです。
メリットとしてはAndroidのGPUでのサポートがされているため変換などをしないでもそのままテクスチャとしてOpenGLで描画することができます 。
さらにAndroidの開発者ツールにPNGをETCのファイルに変換するツールが有ります。

etc1toolの使い方

Androidの開発環境を整えたらAndroidのSDKがダウンロードされると思います。(Macなら/Users/<ユーザー名>/Library/Android/sdkにダウンロードされると思います)
そのSDK内のplatform-tools内にetc1toolというのがあると思います。
これはPNGファイルをETCのファイルへ変換することと逆にETCのファイルをPNGに変換することができます。

今回は下図のファイル(techium.png)を例に実行してみましょう。

[techium.png]
f:id:muchiki0226:20160612142934p:plain

コマンドラインで下記のようにするとPNGをETCにすることができます。

$ etc1tool techium.png --encode

コマンドを実行するとtechium.pkmというのができます。
ls -lしたら下記のような結果が得られます。

$ ls -l
-rw-r--r--     1 ***  ***         8976  6 12 14:32 techium.pkm
-rw-r--r--     1 ***  ***       25719  6 12 00:48 techium.png

容量が1/3になっているのがわかります。
これを逆にPNGに戻すには下記のコマンドを実行します。

$ etc1tool techium.pkm --decode

ここで注意事項ですが、ETCはアルファ値に対応していないためもしアルファ値が入ったものをエンコードしてデコードすると下図のようになってしまい、全く別の画像になってしまうので注意しましょう。

f:id:muchiki0226:20160612143742p:plain

Androidで表示する

Androidで作成したETCのファイルを表示してみましょう。
OpenGLのテクスチャ表示は次のサイトを参考にさせていただきました。

blog.toridge.com

それではpkmを表示してみましょう。
表示させるコードは次の3つのファイルの記述を行いました。

[MainActivity.java]

public class MainActivity extends AppCompatActivity {

    private EtcGlView mEtcGlView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        try {
            InputStream is = getAssets().open("techium.pkm");
            ETC1Util.ETC1Texture texture = ETC1Util.createTexture(is);
            mEtcGlView = new EtcGlView(this, texture);
        } catch (IOException e) {
            e.printStackTrace();
        }
        setContentView(mEtcGlView);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mEtcGlView.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mEtcGlView.onPause();
    }
}

[EtcGlView.java]

public class EtcGlView extends GLSurfaceView {

    public EtcGlView(Context context, ETC1Util.ETC1Texture texture) {
        super(context);
        setRenderer(new EtcRenderer(texture));
    }
}

[EtcRenderer.java]

public class EtcRenderer implements GLSurfaceView.Renderer {

    private ETC1Util.ETC1Texture mEtc1Texture;

    public EtcRenderer(ETC1Util.ETC1Texture etc1Texture) {
        mEtc1Texture = etc1Texture;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig config) {
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        gl10.glViewport(0, 0, width, height);
        gl10.glEnable(GL10.GL_TEXTURE_2D);
        int[] buffers = new int[1];
        gl10.glGenTextures(1, buffers, 0);
        int texture = buffers[0];
        gl10.glBindTexture(GL10.GL_TEXTURE_2D, texture);
        GLES10.glCompressedTexImage2D(GL10.GL_TEXTURE_2D, 0, ETC1.ETC1_RGB8_OES, mEtc1Texture.getWidth(), mEtc1Texture.getHeight(),
                0, mEtc1Texture.getData().remaining(), mEtc1Texture.getData());

        gl10.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
        gl10.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        gl10.glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
        gl10.glClear(GL10.GL_COLOR_BUFFER_BIT);
        float uv[] = {
                0.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 0.0f,
                1.0f, 1.0f,
        };

        ByteBuffer bbuv = ByteBuffer.allocateDirect(uv.length * 4);
        bbuv.order(ByteOrder.nativeOrder());
        FloatBuffer fbuv = bbuv.asFloatBuffer();
        fbuv.put(uv);
        fbuv.position(0);

        gl10.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl10.glTexCoordPointer(2, GL10.GL_FLOAT, 0, fbuv);
        float positions[] = {
                -1.0f, 1.0f, 0.0f,
                -1.0f, -1.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
                1.0f, -1.0f, 0.0f,
        };

        ByteBuffer bb = ByteBuffer.allocateDirect(positions.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(positions);
        fb.position(0);

        gl10.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl10.glVertexPointer(3, GL10.GL_FLOAT, 0, fb);
        gl10.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
    }
}

まずMainActivityでtechium.pkmの読み込みを行います。
今回はpkmファイルをassetsフォルダ内に置き、それをgetAssets().open("techium.pkm")を使ってInputStreamを取得します。
そしてETC1Util.createTexture(is)でInputStreamを渡しpkmファイル内の読み込みを行ってもらいます。
この時にpkmファイル内の画像データの高さ、横幅、実データの3種類に分割されETC1Textureというクラスに入れられ返されます。

それをEtcRendererのonSurfaceChanged時にテクスチャのデータを指定しますがGLES10.glCompressedTexImage2Dを読み込んだデータを渡し、表示してもらいます。
注意しないといけないのは第1引数はGL10.GL_TEXTURE_2D、第2引数は0しか指定できませんので注意しましょう。
あとは画像の高さ、横幅、実データを渡すと下図のように表示されます。

f:id:muchiki0226:20160612145431p:plain:w300

あと、ETC1.decodeImageというAPIがありpkmをPNGに戻すことができそうなコードがあります。
実際に使ってみると実データ部分のデコード結果が-1で埋められて返却されてきて復元できませんでした。
どうやったら使えるか引き続き調査してみます。

これで画像データは1/3にすることができました。
ネットワーク越しで配信するが画像とかも同じようにETCで配信したら通信料を減らすことができてユーザーは喜ぶかもしれません。
試してみてください。

サンプルコード

github.com