【Android】OpenGL ES高速化テクニック集

speed

はじめに


2014/03/05:3項目追加しました。

OpenGL ESは、OpenGLから機能を削減してかなり軽くなった描画APIです。

当然、AndroidはPCのCPUやメモリには現時点では歯がたちませんので、クロノスグループの人たちがいろいろがんばってくれたわけですが、それでも動作がモッサリしててギビしいよ!という方のために、今回は個人的に凄まじい効果を生んだ高速化テクニックを紹介していきます。

この中には、3Dだけではなく、普段のプログラミングにも活かせるワザがちょこちょこ入っています。

「OpenGL ES 高速化」みたいなキーワードでググってみると、基本中の基本であるVBOやバッファのランクを下げる(ShortBuffer→ByteBuffer)等が多く見られ、それ以上の効果的な最適化処理を紹介しているサイトがありませんので、記事を書くことにしました。

3D系の入門書やサイトを見たらどこにでも書いてあるものは、他サイト様を参照してください。

<もくじ>

・sin cos演算をキャッシュする

・カリング処理を入れる

・インスタンス生成を避ける

・描画しない、処理しない

・1Drawで全てのオブジェクトを描画する

・テクスチャアトラスを使用する

sin cos演算をキャッシュする


3Dプログラミングであったり、2Dでもアナログ時計の針を描画したい場合など、Javaでは以下のようなプログラムを書きます。

double x = r * Math.cos(angle);
double y = r * Math.sin(angle);

Gitさんに上げている3D物理演算のサンプルでは、「ワールドを自由に歩き回る」「視点を回転する」といったことにサインコサインが使われています。

3D物理演算サンプル

ただ、Mathクラスのstaticメソッドは基本的に処理のスピードが遅いものが多いです。

2乗(累乗)の計算を行うMath.powメソッドは、手動で同じ値を掛けた方が速いくらいです(笑)

数学系の処理は極力避けるのが吉なので、特に使用頻度が多いsin cosをなんとかしましょう。

前提条件として、sin cosはDeg値0~360度で全て表すことができるということ。

lengthが360のdouble配列に、初期化で全てのsin cosの結果を入れてしまえば、あとはインデックスを指定するだけで呼び出せます。

sincosCache2

コードに直すとこうなります。

for(int i = 0; i < 361; i++){
	// Math.toRadians()よりも手動の方が速い
	double angRad = Math.PI / 180 * i;
	sinCache[i] = Math.sin(angRad);
	cosCache[i] = Math.cos(angRad);
}

Deg値からRad値に変換する時に、Math,toRadians()を使いたくなりますが、これも高速化を考えれば、手動で行ったほうが速いです。

使い方としては、単純にフィールドに登録しておいて使いたいときに呼び出す。

private double[] sinCache = new double[361], 
		 cosCache = new double[361];

これだけでかなりヌルヌルになります。あまりのヌルヌルさに、null投げてないのにNullPointerExceptionとか言われそうな勢いです。try-catch文でもキャッチできません。

「Mathクラスは極力使わず」は、sin cos以外でも同様です。

ピタゴラスの定理を使って距離を求めるときも、2度求める必要がないときはキャッシュしてください。

Math.sqrtの中にMath.powを複数回使うことになると、低速化の原因になります。

math_class

カリング処理を入れる


OpenGL ESの高速化で真っ先に出るのがVBOです。

頂点バッファをOpenGL ESに最適なメモリ配置にしようというもので、これもかなり早くはなります。

これと同じくらい効果があるのに、なんとなく見逃してしまうのが「カリング」。

カリング処理をすると、指定した面を描画しない(例えば立方体であれば、裏側は常に見えない)ため、描画コストが格段に減ります。

VBOをメモリ配置をしっかりし、カリングで描画する頂点数を軽減できれば、OpenGL ESだけの速度改善で言えばバッチリかなと思います。

cubeCull

立方体にカリング処理を加えないと、

(表6面+裏6面)×描画する立方体の数

となりますので、合計すると低速化するのも、うなずけます。

これをコードにするのは非常に簡単です。

// カリングの有効化
gl.glEnable(GL10.GL_CULL_FACE);

// 裏面を描画しない
gl.glFrontFace(GL10.GL_CCW);
gl.glCullFace(GL10.GL_BACK);

描画後は無効化するのも忘れずに行いましょう。

インスタンス生成を避ける


これは全てのプログラムに共通していますが、Javaでも最も時間のかかる処理の一つにインスタンスの生成があります。

これをなるべく使いまわしてnewする回数を減らすのが、簡単に大幅な改善をする方法です。

例えば画面をタップしたらワールドにオブジェクトを出現させるとして、これを常にnewすると、動作がもっさりになります。

よくある手法ですが、オブジェクトを10個作ったら1個目を再利用すれば、2周目からはnewしなくて済みます。

イニシャルコストを減らすという意味で、バッファの作成はnewの度にしないことも重要です。

1度作ったら、staticで保持しておきましょう。

あんまり頻繁に使うものは、sin cosキャッシュと同様に、フィールド定義したりして高速化を図りましょう。

描画しない、処理しない


結局、これがシンプルで最も効率的な最適化です。

具体的に例をあげるとすれば、「視界に入らない部分と、隠れているメッシュはDrawメソッドの対象から外す」です。

これは「Early-Z Culling」や「オクルージョンカリング」等と呼ばれますが、OpenGL ESでは、Drawメソッドを実行した後、非同期で描画されます。(なので、単純にミリ秒をメソッド前後で取得して性能を測ることができません)

見えない頂点があってもなくても、裏でガリガリしてから描画するかどうかを判断するので、常に見えないと確定した面を描画対象から外す、さきほどのお手軽なカリングではまだ効果が薄いです。

そこでカメラ位置から現在見えない面を計算し、glBufferSubData()あたりを使って描画対象から外す。

Early-Z CullingはOpenGL ES 1.0では手動でやるしかないので(OpenGL ES 3.0ではできます)、視界に入るか入らないかを自分で判断する必要がありますが、「処理コスト>パフォーマンス」が成り立つ場合、効果的です。

その他にも、毎フレーム呼ばれているglEnable()等での無駄なステートの変更は避けましょう。

1Drawで全てのオブジェクトを描画する


以前、Cubeを1000Drawしました。

最適化されてない頂点数36個をVBOを使って描画しましたが、高性能なはずのAndroidが操作できなくなる状況になりました。

当然、こうなるのは目に見えていたのですが、ここまで重くなるとは思っておらず。

そこで目に見えない三角形でCube同士を繋ぎ、1Drawで1000Cubeを描画してみると、FPS値は一定して最高値の60を保つようになりました。

この方法で使用した目に見えない三角形のことを、「縮退三角形」と言います。

詳しいやり方は以下を参照してください。

ゲームプログラマーを目指すひと “縮退三角形”を使ってみた

頂点配列を少しいじるだけで簡単に高速化できるので、静的オブジェクトの描画が目的なら、オススメな高速化です。

テクスチャアトラスを使用する


テクスチャアトラスとは、一つの画像に複数の画像を埋め込み、UVのみを指定してテクスチャを貼る方法です。

複数枚のテクスチャがある時はglBindTexture()を毎フレーム呼んでバインドするテクスチャを変更する必要がありましたが、これを使うとステートの変更がなくなるので、多少高速化します。

まとめ


以上がOpenGL ESの高速化テクニックです。

実際にやってみると、スペックの低い環境でもスピードが出てくるはずです。

他にも、テクスチャのサイズを512×512→128×128等、小さくすると、近づいた時の画質の粗さと引き換えに(ミップマップ処理で改善可)高速化することができます。

現在、3D物理演算プロジェクトの方で、以上に紹介したテクニックを入れて最適化している最中なので、完成したらサイドバーからGitさんのリンクよりコードを見ていただければ、より理解が深まるはずです。

#2013/12/30更新:高速化テクニックを入れたプロジェクトを公開しました。

#2014/01/30更新:プロジェクトにMathクラスの高速化版、CustomMath(com.webprog.toolパケージ内)を追加しました

#2014/03/05更新:高速化処理の方法を3つ追加しました。