[この記事は Shanee Nishry による Android Developers Blog の記事 "Game Performance: Vertex Array Objects" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。]

以前、頂点レイアウト修飾子を使用して、OpenGL アプリケーションのパフォーマンスと決定性を向上させる方法をご案内しました。この投稿では、オブジェクト描画に際し、パフォーマンスを向上し、よりクリーンなコードを生成できる便利なテクニックをもう 1 つご紹介します。

頂点バッファのバインド

画面上に描画する前に、該当する頂点シェーダー属性に頂点データ(例、ポジション、法線、テクスチャ座標)をバインドする必要があります。それには、頂点バッファをバインドし、全体の頂点属性を有効にして、glVertexAttribPointer でバッファのレイアウトを記述します。

描画処理は以下のようになります。
const GLuint ATTRIBUTE_LOCATION_POSITIONS   = 0;
const GLuint ATTRIBUTE_LOCATION_TEXTUREUV = 1;
const GLuint ATTRIBUTE_LOCATION_NORMALS     = 2;
// Bind shader program, uniforms and textures
// ...
// Bind the vertex buffer
glBindBuffer( GL_ARRAY_BUFFER, vertex_buffer_object );
// Set the vertex attributes
glEnableVertexAttribArray( ATTRIBUTE_LOCATION_POSITIONS );
glVertexAttribPointer( ATTRIBUTE_LOCATION_POSITIONS, 3, GL_FLOAT, GL_FALSE, 32, 0 );

glEnableVertexAttribArray( ATTRIBUTE_LOCATION_TEXTUREUV );
glVertexAttribPointer( ATTRIBUTE_LOCATION_TEXTUREUV, 2, GL_FLOAT, GL_FALSE, 32, 12 );

glEnableVertexAttribArray( ATTRIBUTE_LOCATION_NORMALS );
glVertexAttribPointer( ATTRIBUTE_LOCATION_NORMALS, 3, GL_FLOAT, GL_FALSE, 32, 20 );
// Draw elements
glDrawElements( GL_TRIANGLES, count, GL_UNSIGNED_SHORT, 0 );
このコードは、あまり好ましいものではありません。それにはいくつかの理由があります。1 つめの理由は、描画の前に正しい属性を有効または無効にして、頂点バッファのレイアウトをキャッシュに格納する必要があることです。つまり、ほとんど意味のないタスクのために、ハードコーディングかデータ保管のいずれかを実行しなければなりません。

2 つめの理由はパフォーマンスです。有効化する属性を個別にドライバーに伝えなければならず、これは最適とはいえません。この情報をプリコンパイルしておいて、一括して引き渡すのが最善策です。

最後に、これは純粋に見た目の問題ですが、長いボイラプレートコードによって描画処理が雑然としてしまうということが挙げられます。これは、できるならば避けたいところです。

このコードが好ましくないと思われる理由は他にもありますレイアウト修飾子を活用している点は素晴らしいのですが、このコードではすでに OpenGL ES 3 以上を使用しているため、 ジオメトリのインスタンス化 も使用すればさらに良くなるでしょう。単一の描画処理に多くのインスタンスをメッシュのようにバッチさせることで、パフォーマンスを確実に強化できます。

では、上記のコードをどのように改善できるのでしょうか。

頂点配列オブジェクト(Vertex Array Objects: VAO)

OpenGL ES 3 またはそれ以上を使用している場合、頂点属性の状態を保管するために頂点配列オブジェクト(VAO)を使用します。

VAO を使用することにより、繰り返して使用される頂点の説明フォーマットがドライバーにコンパイルされます。加えて、glVertexAttribPointer で必要とされる頂点フォーマットのキャッシュ格納が不要となり、また、描画ごとのボイラプレートコードを減らすことができます。

頂点配列オブジェクトの作成

最初に VAO を作成する必要があります。頂点バッファ オブジェクトに沿ってメッシュごとに作成すると、以下のようになります。
const GLuint ATTRIBUTE_LOCATION_POSITIONS   = 0;
const GLuint ATTRIBUTE_LOCATION_TEXTUREUV = 1;
const GLuint ATTRIBUTE_LOCATION_NORMALS     = 2;
// Bind the vertex buffer object
glBindBuffer( GL_ARRAY_BUFFER, vertex_buffer_object );
// Create a VAO
GLuint vao;
glGenVertexArrays( 1, &vao );
glBindVertexArray( vao );
// Set the vertex attributes as usual
glEnableVertexAttribArray( ATTRIBUTE_LOCATION_POSITIONS );
glVertexAttribPointer( ATTRIBUTE_LOCATION_POSITIONS, 3, GL_FLOAT, GL_FALSE, 32, 0 );

glEnableVertexAttribArray( ATTRIBUTE_LOCATION_TEXTUREUV );
glVertexAttribPointer( ATTRIBUTE_LOCATION_TEXTUREUV, 2, GL_FLOAT, GL_FALSE, 32, 12 );

glEnableVertexAttribArray( ATTRIBUTE_LOCATION_NORMALS );
glVertexAttribPointer( ATTRIBUTE_LOCATION_NORMALS, 3, GL_FLOAT, GL_FALSE, 32, 20 );
// Unbind the VAO to avoid accidentally overwriting the state
// Skip this if you are confident your code will not do so
glBindVertexArray( 0 );
以下の追加部分を除いて、前のコード セクションに類似していることに気づかれるでしょう。
// Create a vertex array object
GLuint vao;
glGenVertexArrays( 1, &vao );
glBindVertexArray( vao );
これらの行で VAO を作成し、バインドしています。この後の glEnableVertexAttribArrayglVertexAttribPointer コールはすべて今バインドされている VAO に記録されます。手順としては新たに生成された VAO を使用すればいいだけなので、描画ごとの手続きは大いに簡素化されます。

頂点配列オブジェクトの使用

このメッシュを使用して次に描画をする際は、glBindVertexArray を使用して VAO をバインドするのみで、
// Bind shader program, uniforms and textures
// ...
// Bind Vertex Array Object
glBindVertexArray( vao );
// Draw elements
glDrawElements( GL_TRIANGLES, count, GL_UNSIGNED_SHORT, 0 );
頂点属性の経由は不要になります。これにより、コードはよりシンプルに、フレームごとのコールは短く、効果的になり、ドライバーによるバインドが最適化されることでパフォーマンスも向上します。

glBindBuffer は呼び出されていないことにお気づきでしょうか。これは、VAO により glBindBuffer コール自体が記録されることはないものの、VAO が記録されている間の glVertexAttribPointer の呼び出しが、現在バインドされているバッファを参照しているからです。

ゲーム パフォーマンスを改善する方法をさらにお知りになりたい場合は、ゲーム パフォーマンスに関する記事のシリーズをご覧ください。Android でビルドしている場合、Android のパフォーマンス パターンもご参考にしてください。


Posted by Ryuichi Hoshi - Developer Relations Team