Android の OpenGL ES と SurfaceView / GLSurfaceView

今さらながら Android の OpenGL ES に初めて手を付けてみた。これまでは ImageView の onDraw のカスタムか、ゲーム的なものでも、2D で SurfaceView の画面を丸ごと FrameBuffer として更新するような使い方しかしてこなかった。ところが今回、ちょっと凝った 2D のマップアプリのようなものを作ることになり、先述の通り従来は ImageView の onDraw カスタムで対処してきたのだが、できれば、SurfaceView か、さらには OpenGL ES 化して、Google Map のようなヌルヌルしたものにできないかと思い、3D 用だと思って敬遠してきた OpenGL ES の領域にまで足を踏み入れてみることにした。


SurfaceView

以前、使ったことがあると言っても、ちょっとした 2D ゲームのサンプルで使った程度であり、非ゲームのアプリに利用するというような場合も含めて SurfaceView 自体を研究したことはなかったので、まずは SurfaceView をいじってみることにした。

レンダリング用スレッドを使用する

Activity 側で SurfaceHolder.Callback を implement するなりして、レンダリング用スッドの生成や終了処理を行う。レンダリング用スレッド中では、Canvas を通じて FrameBuffer の描画を行なう。


Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawBitmap(mFrameBuffer, 0, 0, mPaintObject);
mSurfaceHolder.unlockCanvasAndPost(canvas);

レンダリング用スレッドを使わないで、そのまま Main UI スレッドで同様のことを行うこともできるが、圧倒的に処理が重い。SurfaceView を使う意味がなくなる。

カスタム Handler を用意する

Google のサンプル(Grafika)では、レンダリング用スレッドにカスタムメッセージを渡せるようにするために、カスタム Handler を用意して、カスタムメッセージを定義している。

Choreographer.FrameCallback を使用する

さらに、SurfaceView をより最適化して使う方法として、Choreographer.FrameCallback を Activity に implement して使うテクニックがある。v-sync のタイミングに合わせてレンダリング用スレッドを発動させる。また、このコールバックは、次回の v-sync のタイミングをナノ秒単位で通知するので、それを利用して、モデル更新に時間がかかり過ぎている場合は、今回のレンダリングをスキップしてフレームを落とすようにも工夫できる。

Grafika

以上のノウハウの Google 公式サンプルが Grafika の HardwareScaleActivity.java。ただし、このサンプルでは、SurfaceView を使っているものの、レンダリングは OpenGL ES を使っていて、Canvas は使っていない。自分で、OpenGL ES 関連の描画コードを全て除去して、上の Canvas による FrameBuffer 描画で動くようにする必要があった。


GLSurfaceView

本来、OpenGL ES 自体の話が主で、GLSurfaceView はその一環となるものだが、OpenGL ES の話題はボリュームが大きくなるので、SurfaceView との比較から結論を先出しして要点を述べておく。

GLSurfaceView は SurfaceView の派生クラスである

GLSurfaceView はあくまでも SurfaceView の一種に過ぎないというのがポイントで、リファレンスを見ても明らかであり、また具体的にはソースコードを見ればさらに詳細な拡張内容を把握することができる。GLSurfaceView を解析するにあたって、先に SurfaceView の使い方を理解しておく必要があるのは、このためである。

GLSurfaceView ≒ SurfaceView + EGL + Handler

Grafika では、SurfaceView をベースにしてカスタム Handler を実装したりして、OpenGL ES でレンダー用スレッドからレンダリングを行なっていた。GLSurfaceView ではそのようなスレッド関連の処理を組み込んであり、また OpenGL ES を利用する際に必要となる、EGL 関連の手続も組み込まれている。自分は、これらは実際に SurfaceView で組まれている Grafika のサンプルコード(HardwareScaleActivity.java)を GLSurfaceView 化して確認したが、公式ドキュメントにも以下のように説明されている。

The GLSurfaceView class provides helper classes for managing EGL contexts, interthread communication, and interaction with the activity lifecycle. You don't need to use a GLSurfaceView to use GLES.

For example, GLSurfaceView creates a thread for rendering and configures an EGL context there. The state is cleaned up automatically when the activity pauses. Most apps don't need to know anything about EGL to use GLES with GLSurfaceView.

SurfaceView and GLSurfaceView: GLSurfaceView

また、Choreographer.FrameCallback は、GLSurfaceView のソースコードでは確認できなかったが、実際上の挙動から、一定以上の負荷で結果的にフレームが落とされていることを確認した。つまり、


mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

とすることで、Choreographer.FrameCallback からの都度呼び出し(mGLSurfaceView.requestRender())を行って試してみたのだが、かなり負荷をかけても 60fps で requestRender される。しかし、requestRender は結局レンダリング用スレッドへ requestQueue を投げるだけであり、requestQueue からタスクが取り出されて onDrawFrame() される段階で、フレームが間引かれて落ちているようなのである。描画の方は実際にコマ落ちしており、Choreographer.FrameCallback から呼び出される doFrame() 側ではなく、GLSurfaceView のレンダリング用スレッドの onDrawFrame() 内で fps を計測すると、60fps を下回っているのが確認できる。

つまり、GLSurfaceView では Choreographer.FrameCallback を内部的に実装しているわけではないものの、結局、Choreographer.FrameCallback を使った機能の実装を我々が心配する必要もないことになる。

GLSurfaceView のまとめ

つまり、OpenGL ES を使うにあたって、素の SurfaceView を使う場合は、スレッドの扱いや EGL についても我々が自前で対処する必要があったが、GLSurfaceView を使えばそれらを組み込み済なので、レンダリング用スレッドにおける onSurfaceCreated() / onSurfaceChanged() / onDrawFrame() を通じた、OpenGL ES の描画命令を記述することに限定して取り組むことができる。


public class MainActivity extends AppCompatActivity implements GLSurfaceView.Renderer {
        
    private GLSurfaceView mGLSurfaceView;
        
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mGLSurfaceView = findViewById(R.id.glSurfaceView);
        mGLSurfaceView.setEGLContextClientVersion(2);
        mGLSurfaceView.setRenderer(this);
    }

    @Override
    protected void onDestroy() {
        mGLSurfaceView = null;
        super.onDestroy();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume(); // 必須
    }

    @Override
    protected void onPause() {
        mGLSurfaceView.onPause(); // 必須
        super.onPause();
    }

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

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
    }

    @Override
    public void onDrawFrame(GL10 gl) {
    }

OpenGL ES

さて、比較のためにまず SurfaceView の話題から入ったが、本題は OpenGL ES、ここからである。

まず、Android プログラミングの OpenGL ES に関する情報が意外に乏しい。Web 上や書籍などを探っても、2014 年とか 2015 年頃の情報が多く、そこで停滞しているようなのだ。つまり、Android で OpenGL ES が採用されたばかりの時期には一時的に脚光を集めてはいたものの、そのまま立ち消えになっているかのようである。実際には OpenGL ES の方は以後も 1.0 → 2.0 → 3.x と Android での対応が進み、さらには Vulkan 対応も導入されている。にもかかわらず、エンドプログラマー用の情報が、ほとんど立ち消えてしまっている感がある。これは結局、OpenGL ES にしても Vulkan にしても、ゲームフレームワークやゲームエンジンの側での対応の対象となる API であって、エンドプログラマーが直接扱う対象ではないという立ち位置なのかもしれない。

またそういった時代の趨勢に加えて、元々の OpenGL の立ち位置的な問題もある。本来はドライバーで隠蔽されている低レベルのグラフィックスハードウェアへのアクセスをエンドプログラマーに解放するものだから、C/C++ で利用する前提で設計されている。決して Android の Java プログラマー用に作ったオブジェクト志向な API ではない。であるから、Android API に OpenGL ES 用の API があるといっても、基本的には OpenGL ES へのラッパー的な命令が揃えられているに過ぎない感じである。Google の態度も、とりあえずそういったラッパー的命令を一通り用意するだけで、リファレンスも特に説明がほとんどない。チュートリアル的なガイドもない。OpenGL のことは、Android とは独立して、OpenGL 自体の情報として勝手に勉強してくれという、投げ出した状態である。

既に PC で OpenGL 自体を扱った経験のあるプログラマーならば、OpenGL 自体の学習は終えているので、問題ないのだろうが、Android で新たに OpenGL を学ぼうとする人間にとっては、中々これは厳しい。Google の唯一の情報源である、サンプルコードの Grafika を参考にするより他はないが、Google のサンプル(Grafika に限らず)の常として、ただ模範解答だけが示してあって、その解答に到るための道筋についての何の解説もない。このサンプルを解読するにあたって、どこまでが Android 固有のもので、どこからが OpenGL ES 固有のものかを、解析するのはかなりの労苦である。

OpenGL の基本コンセプトについて学ぶ

そこで参考にしたのが『独学で 1 ヶ月間 OpenGL を学んで得た基礎知識のまとめ ~ 2D 編 ~』という 2014 年 12 月に書かれた記事。いきなり Android でサンプルコードをいじりながら学習しようとする限界を認識し、最低限必要な OpenGL が背景にしているコンセプトをコンパクトに 5 回分にまとめて紹介し、最後の 6 回目でやっとサンプルコードを掲載している。

自分は当初、ともかくサンプルを動かしながら進めていたが、シェーダーや、何が OpenGL ES として純粋に必要なのか等、疑問点が次々と湧いて頭が混乱してきたので、こちらの記事で一通り OpenGL のコンセプトについて頭に入れることで、やっと Grafika などのサンプルコードが「OpenGL として何をしているか」を整理して解析できるようになった。サンプルコード的には、Grafika の方がより模範的な気もするが、最後の 6 回目に掲載されたサンプルコードも、細かくパーツごとに解説が付されているので、Grafika の解析時に大いに参考になった。

テクスチャーマッピング

残念ながら、『独学で 1 ヶ月間 OpenGL を学んで得た基礎知識のまとめ ~ 2D 編 ~』では、テクスチャーマッピングが割愛されてしまっている。自分の目的的にテクスチャーマッピングは必須だったので、それについては、Grafika のサンプルコードおよび、『Learn OpenGL ES』の 「Android Lesson Four: Introducing Basic Texturing」などを参考にした。

OpenGL ES 所感

以上の結果、まあ、どこまでが OpenGL ES 固有の処理なのかという切り分けができてしまえば、自分の学習範囲が 2D 用途限定ではあるものの、学習前に抱いていたほどの難しいものではなかった気がする。自分でデバイス画面上のピクセルに対応した FrameBuffer を直接操作しようとする SurfaceView のユースケースとは違い、OpenGL ES ではポリゴンおよびそこに貼り付けるテクスチャーはグラフィックスハードウェア上の VRAM に置く。それゆえ、インデックスを通じた遠隔操作でそれらのオブジェクトを操作するような手続となる。基本的にポリゴンの移動は各頂点の移動を GPU に指示することで行う。VRAM 上の実際のデータ(FrameBuffer)を操作するのは、GPU であって、エンドプログラマーではない。それゆえの API 設計となっている。その辺りの遠隔操作的な性質が呑み込めた感じがする。

コメント

このブログの人気の投稿

EP-805A 廃インク吸収パッド交換

m3u8 ファイルをダウンロードして ffmpeg で MP4 に変換・結合

WZR-HP-AG300H with OpenWrt