愚か者は経験に学び、賢者は歴史に学ぶ
私は愚か者なのでしょうか、よく経験に学びます。
今回得た教訓は、
「OpenGLアプリをうまく設計するのは難しい」
抽象化と処理効率のバランスについてです。
以前、数字をテクスチャで描画するためのクラスを作りました。
簡単に説明しておくと、以下のような、テクスチャをラップして、
メソッド呼び出しに応じて数字の画像をレンダリングするクラスでした。
@interface NumberTexture2D : NSObject { GLuint _texture; // テクスチャID int _figures; // 最大表示桁数 BOOL _fillZero; // ゼロ埋めフラグ(表示する数字の桁数が_figuresに満たない場合にゼロ埋め) float _dispSize; // 1文字の表示サイズ int _align; // テキストアラインメント int _r; // 赤成分 int _g; // 緑成分 int _b; // 青成分 int _a; // アルファ成分 } // 最大表示桁数 @property(assign, nonatomic, readwrite) int figures; // ゼロ埋めフラグ @property(assign, nonatomic, readwrite) BOOL fillZero; // 1文字の表示サイズ @property(assign, nonatomic, readwrite) float size; // テキストアラインメント @property(assign, nonatomic, readwrite) int align; // 初期化メソッド -(id)initWithImageName:(NSString*)textureName; // 描画メソッド -(void)draw:(int)number:(float)x:(float)y; // 色指定メソッド(デフォルトは255, 255, 255, 255) -(void)setColor:(int)r:(int)g:(int)b:(int)a; @end
しかし、この設計は失敗でした。
誰だ!こんな非効率的な設計をしたのは!
どこが問題かというと、それは、テクスチャIDをカプセル化していることです。
このようにしてしまうと、「同じテクスチャを使って」「別の数字」を描画しようとしたときに、別のオブジェクトでも同じテクスチャをロードすることになってしまいます。例えば、左側にゲームの得点を表示して、右側にゲームの残り時間を表示する場合です。
複数のオブジェクトが、同じテクスチャを、別々のメモリに読み込むのは、ハードウェアリソースの利用効率としてよろしくありません。クラスの利用者がこの問題を察知して、同じオブジェクトに違うプロパティ値を設定し直して同じオブジェクトのdrawメソッドを複数回呼び出してくれればまだ良いかもしれません。しかし、それでもまだ効率面での不安は残ります。
同じテクスチャに対して、drawメソッドを何回も呼ぶよりも、同じテクスチャを使って描画するすべての数字の頂点座標を最初に求めておいて、1度にすべての数字を描画する方が高速なのです。即ち、glDrawArraysの呼び出しを最低限にする方が高速ということです。
結局のところ、私の浅はかなオブジェクト指向は、GPUの足を引っ張っていたのです。
より良い設計としては、
・drawメソッドは内部メモリに
頂点座標、
テクスチャのマッピング座標、
頂点の色情報
をバッファリングしておく
・最後にflushメソッドを呼んでもらって、そこでglDrawArraysする
という手段が考えられます。
しかしこの場合も、drawメソッドが何回呼ばれるか分からないから、どれだけの頂点座標メモリを確保しておけば良いのか、あらかじめ分かりません。描画が指示される度にmalloc, memcpyするのは、これまた効率が良くありません。
更に言うならば、数字の表示位置、サイズ等は画面設計時に決まっていて、頂点座標が変わることはないのだから、VBOを使った方良いでしょうし、マルチテクスチャを使って、複数のオブジェクトをまとめていっぺんに描画した方が良くなるかもしれませんし、より複雑なジオメトリを同時に扱うならば、頂点インデックスとglDrawElementsを使う方が良いかもしれません。究極的には、Objective-Cのメッセージ送信のオーバーヘッドを気にしたり、STLを使用するかどうかについてまで慎重にならなければならないかもしれません。
こうやって突き詰めて行くと、最終的には、CでOpenGLのAPIを直接使って、すべての描画を、まとめてバッチ処理するのが一番処理効率が良いという結論になります。当然ながら、こうなってくると、GPUにとってはとても分かりやすいプログラムになるでしょうが、我々人間にとってはとても難解なプログラムになってしまいます。
もしも、「歴史に学ぶ」のであれば、処理効率を落としてでも、
オブジェクト指向によって、人間にとって分かりやすいプログラムを書くべきだ
という教訓から学ぶべきだということになり、この話は堂々巡りになってしまいます。
さてはて、、、
最終的には、可読性、保守性、開発効率等と処理効率をどこまでトレードオフできるかの問題になるでしょうね。
そしてこれを事前に判断するのはとても難しいことです。
よって、最初に書いた教訓、
「OpenGLアプリをうまく設計するのは難しい」
というところに至りました。
個人で開発する場合は、アプリが問題なく動作する分には、あとはできるだけ自分にとって都合良く、で構わないのですが、これがチーム開発となると更に難しくなります。チームリーダーがしっかりと方針付けしなければ、担当者によって抽象化の粒度がばらばらになってしまって、ヒュドラのようなモンスターを誕生させてしまうかもしれない。あるいは、初詣の長蛇の列のように、のろのろとしか動けないアプリになってしまうかもしれない。
自分(あるいはチーム)が作るアプリがどこまで負荷のかかる処理をするのか、
対象とするハードウェアの性能がどれくらいなのか、
によって、どこまでのトレードオフが許容されるかが変わります。
その点を良く考えずに、よく使う関数をまとめただけのような、OpenGLの関数を薄くラップするだけのクラスを作るのは愚かなことだな、と思いました。(反省)
今の私がその辺のバランスを考えようとするには、あまりにもOpenGLに対する理解が足りない。
もっと深く学んで行けば、もう少しGPUの気持ちになってプログラムを組めるようになるかもしれません。
今はまだ学習し始めなので、とりあえず動くことに満足しつつ、自分の中で理解しやすいコードを書いていきたいと思います。そして、徐々に、処理効率が良くなる方向へ設計をずらして行って、その都度、納得できる妥協点で開発していけたらな、という想いを持って、
長くなりました!
今日はこの辺で!