Androidアプリ開発 OpenGL VBO(Vertex Buffer Object)で高速化

本日は、VBOの話をしようと思う。
VBOはVertex Buffer Object。頂点データをバッファオブジェクトでGPU側で管理する方法。Android端末には、CPUとGPUが搭載されており、どちらも計算を行うのだが、GPUはグラフィックス周りの計算に特化している。
OpenGLによる3Dグラフィックスは、GPUを使って計算処理されることが多い。
GPUはグラフィックスの計算に特化しているので、グラフィックス処理ならCPUより高速に実行できる。ハードウェア回路で必要な計算ができるようになっている。
CPUとGPUの違いは、グラフィックス処理に特化しているかどうかだけ。基本的なしくみは変わらない。CPU、GPUの内部には計算のためにレジスタが存在しているが、レジスタの数は多くないし、容量も少ない。大量のデータは、メモリに保存されていて、計算する際にレジスタに持ってくる。
CPUとメモリの関係は、「ポインタが理解できない理由」に詳しく書いてあるので、興味があればどうぞ。

C言語 ポインタが理解できない理由 [改訂新版] (プログラミングの教科書)

C言語 ポインタが理解できない理由 [改訂新版] (プログラミングの教科書)

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/04/08
  • メディア: 単行本(ソフトカバー)

多くのデスクトップPCでは、CPUが使うメモリとGPUが使うメモリが別になっている。特にゲームをするために、ビデオカードを拡張している場合は、ビデオカードにGPUとGPUが使うメモリが搭載されている。
ノートPCや廉価版のPCでは、CPUにGPUが統合されたオールインワンのCPUが使われている場合がある。このタイプのCPUでは、メモリは同じ。GPUが使うメモリはメインメモリの一部が割り当てられる感じになる。
GPUが使うメモリは「フレームバッファ」または単に「バッファ」と呼ばれる。ビデオメモリとかVRAMと呼ばれる場合もある。
Android端末にもGPUは搭載されている。OpenGLの3D機能を使えば、おのずとGPUが使われる。しかし、OpenGLを使わない普通のCanvasでの描画では、GPUは使用されていない模様。なんともったいない。
ちょっと調べてみる

http://techbooster.jpn.org/andriod/application/7054/

によると、Android 3.0以降なら、Cnavas描画でもGPUアクセラレーションが可能であるらしい。
AndroidManifest.xmlでandroid:hardwareAcceleratedの設定が必要なのか。
2.3だと無視されるのかなぁ...
AREarthroidで実験してみたが、あまり変化はない。3Dの部分はそのままだからなぁ...
2.3だと、Canvas#isHardwareAcceleratedメソッドが存在しないエラーになる。3.0以降じゃないとだめ。
VBO
やっと、本題のVBOである。
VBOを使わない場合、頂点データや法線データは、メインメモリ上にある。描画する際は、GPUを使うので、頂点データは、GPU側のメモリにコピーされる。コピーは描画の度に行われる。
VBOを使うと、メインメモリからGPU側のバッファに移動されて常駐するようになる。描画の度にコピーする必要がなくなるので、その分高速に描画できる。
バッファに頂点データというオブジェクトを作るので、Vertex Buffer Objectとなる。
VBOを使うと描画を高速化できる」というわけ。
ただし、OpenGL 1.1の機能になるため、Android端末のバージョンによっては使用できない。
さて、実際のコードはどうなるのかというと、AREarthroidから一部を抜いてみよう。

int[] vboIds = new int[4];
GL11 gl11 = (GL11) gl;
gl11.glGenBuffers(4, vboIds, 0);	// バッファを4つ作成
vertexBufferObjectId = vboIds[0];
normalBufferObjectId = vboIds[1];
textureBufferObjectId = vboIds[2];
elementBufferObjectId = vboIds[3];
// 頂点データをGPUにアップロード
gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexBufferObjectId);
vertexByteBuffer.position(0);
gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexByteBuffer.capacity(), vertexByteBuffer, GL11.GL_STATIC_DRAW);
// 法線
gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, normalBufferObjectId);
normalByteBuffer.position(0);
gl11.glBufferData(GL11.GL_ARRAY_BUFFER, normalByteBuffer.capacity(), normalByteBuffer, GL11.GL_STATIC_DRAW);
// テクスチャ
gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, textureBufferObjectId);
textureByteBuffer.position(0);
gl11.glBufferData(GL11.GL_ARRAY_BUFFER, textureByteBuffer.capacity(), textureByteBuffer, GL11.GL_STATIC_DRAW);
// 面データもアップロード
gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, elementBufferObjectId);
indexBuffer.position(0);
gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity() * SHORT_SIZE, indexBuffer, GL11.GL_STATIC_DRAW);
// CPU側メモリのバッファはもういらない
vertexBuffer = null;
vertexByteBuffer = null;
normalBuffer = null;
normalByteBuffer = null;
textureBuffer = null;
textureByteBuffer = null;
indexBuffer = null;

まず、glGenBuffersでバッファオブジェクトを作成する。バッファオブジェクトは番号で識別されるので、これを受けるためのint配列を渡してやる。最初の引数は作成するオブジェクトの個数。第二引数が、番号の配列。第三引数はオフセット。

gl11.glGenBuffers(4, vboIds, 0);	// バッファを4つ作成

これで、4つのオブジェクトが作成される。オブジェクトの番号が配列vboIdsに入って戻ってくる。このまま使っても問題はないが、わかりやすくするために、フィールド変数で記憶する。

vertexBufferObjectId = vboIds[0];
normalBufferObjectId = vboIds[1];
textureBufferObjectId = vboIds[2];
elementBufferObjectId = vboIds[3];

vertexBufferObjectIdが、頂点データの入ったオブジェクトの番号。
normalBufferObjectIdが、法線データの入ったオブジェクトの番号。
以下は省略。
次に、バッファオブジェクトにデータを突っ込んでいく。CPU側のメモリからGPU側のメモリにデータをコピーする。

// 頂点データをGPUにアップロード
gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexBufferObjectId);
vertexByteBuffer.position(0);
gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexByteBuffer.capacity(), vertexByteBuffer, GL11.GL_STATIC_DRAW);

glBindBufferで、どのバッファを使うのかを番号で指定する。
vertexByteBufferが頂点データの入ったCPU側メモリのバッファ。先頭からデータが入るように、position(0)でポインタの位置を先頭にしている。
glBufferDataで、vertexByteBufferのデータがGPU側のVBOに転送される。
GL_STATIC_DRAWは、バッファオブジェクトの用途。
同様な手法で、法線データ、テクスチャマッピングデータ、面データもVBOにしてGPU側にアップロードする。面データは、バッファの種類が異なるので注意。
VBOはこれで完成。CPU側メモリのバッファは不要になるので、nullをセット。
次に、VBOを使って描画する方法。
VBOを使わない場合は、glVertexPointerでCPU側メモリのバッファを指定すればよかったが、VBOの場合は、glBindBufferでVBOの番号を指定してから、glVertexPointerでポインタを設定する。

// 頂点バッファ設定
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
if ( useVBO ){
gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexBufferObjectId);
gl11.glVertexPointer(3, GL10.GL_FLOAT, 12, 0);
}
else {
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
}

useVBOはVBOを使用しているかどうがのフラグ。if文のtrueブロックがVBOを使った場合の処理になる。
法線データ、テクスチャ、面データも同様に処理を行う。
glDrawArraysなどの描画関数呼び出しは変更しなくてOK。
glDrawElementsは、indexBufferの指定が、オブジェクトになるので、GL11にあるglDrawElementsを使う。

if ( useVBO ){
gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, elementBufferObjectId);
gl11.glDrawElements(GL10.GL_TRIANGLES, elementCount * 3, GL10.GL_UNSIGNED_SHORT, 0);
}
else {
gl.glDrawElements(GL10.GL_TRIANGLES, indexBuffer.capacity(), GL10.GL_UNSIGNED_SHORT, indexBuffer);
}

バッファオブジェクトは、使用されなくなったら、メモリから削除できる。削除は、glDeleteBuffersで行うことができる。

gl11.glDeleteBuffers(4, vboIds, 0);

引数は、glGenBuffersと同じ。
GLThreadから呼び出さないと無効なので注意。

AREarthroid

かんたんAndroidアプリ作成入門 (プログラミングの教科書)

かんたんAndroidアプリ作成入門 (プログラミングの教科書)

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2013/04/16
  • メディア: 単行本(ソフトカバー)

投稿者プロフィール

asai
asai
システムエンジニア
喋れる言語:日本語、C言語、SQL、JavaScript