OpenGLで描画する際にVBO(頂点の格納場所)をうまく活用することで高速化した話。
高速した後は描画のコマ落ちが解消されています。
(左:高速化前、右:高速化後)
はじめに
musicLineはMIDI楽譜をピアノロールで表示します。
しかし描画する音符が膨大になると、動作がカクカクと重くなる状態でした。
通常は30FPSでの描画のところ、再生位置によって描画オブジェクトが増加し、3FPS程度まで下がります。
そのため、OpenGLのVBO等で描画の前処理を最適化し、描画の高速化を図ってみました。
描画の高速化
OpenGLでは様々な工程がありますが、その処理をGPUで行います。
そのため、アプリ側のCPUからGPUへ描画する頂点情報(位置、色等)を転送することになります。この転送時間がボトルネックとなるので、効率よく転送することが高速化に繋がります。
高速化1: VBOへ頂点情報を一気に送る
VBO(Vertex Buffer Object)はGPU側の領域で頂点情報を格納しておく場所です。
現在は頂点情報をフレーム毎に配列でGPUへ転送しています。
しかし、毎フレームで情報収集とGPU転送の時間が掛かるため非効率です。
これは、予め数フレーム分の頂点情報をVBOでGPUへ転送しておき、VBOを参照することで毎回頂点情報を転送しなくて済みます。
特にピアノロールのような画面がスクロールするだけでオブジェクトの位置が変化しない場合に有効です。VBOに頂点情報を格納して、毎フレームでスクロール位置のみGPUへ転送すれば、描画するフレーム部分をGPU側で計算できます。
つまり、毎フレームCPU側で計算した頂点情報を転送するのではなく、数フレーム分の頂点情報を一気に送ってGPU側で描画部分を計算することで、転送回数を削減することができます。
高速化2: IBOで重複する頂点情報を省略
IBO(Index Buffer Object)はGPU側の領域で頂点インデックス情報を格納しておく場所です。
例えば音符(四角形)1個を描画する場合、三角形ポリゴンを2枚使います。頂点が各々3点なので合計6点となります。
しかし、2点は共有する頂点なので2点分の頂点情報が重複します。
頂点配列を転送した場合、(頂点インデックスを指定しなければ)順番に3点ずつで三角形ポリゴンを描画することになります。
三角形ポリゴンの構成する頂点インデックス(頂点配列の前からの番号)を指定することで、重複する情報を省略して三角形ポリゴンを描画することができます。
このように重複した情報を省くことで転送データ量が減り、転送時間を短縮します。
なお頂点インデックスもVBOと同様に予めIBOに転送しておくことで、転送回数を削減することができます。
実装
AndroidではGLSurfaceView
をViewに貼り付けて、OpenGLES(ver 2.0)を使用します。
実装方法はこちらの記事がわかりやすいです。
GLSurfaceView
で描画ができていることを前提に、VBOとIBOを使用した実装を考えていきます。
バッファ(VBO/IBO)を使用する手順は
- バッファを作成
- 頂点情報をバッファへ転送
- 頂点情報をLocationにバインド
- 頂点インデックスを指定して描画
となります。
1. バッファを作成
val bufferIds: IntArray // VBO, IBO の確保領域のIDリスト
bufferIds = IntArray(6).also {
GLES20.glGenBuffers(6, it, 0)
}
この例では、6個のバッファ(VBO x 5 + IBO)を確保しています。
VBO
- 座標
- UV位置
- 色
- 角丸幅
- 音符のX範囲
IBO
- 頂点インデックス
ちなみに、使用後必要がなくなったバッファは削除することでメモリを解放します。
GLES20.glDeleteBuffers(6, bufferIds, 0)
2. 頂点情報をバッファへ転送
val vertexBuffer: FloatBuffer = ... // 音符の4頂点座標を集めてFloatBufferに変換する
val floatByte = 4 // floatは4byte
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0]) // バッファIDの指定
vertexBuffer.position(0) // バッファのポインターを先頭へ
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBuffer.capacity() * floatByte, vertexBuffer, GLES20.GL_STATIC_DRAW) // 頂点を転送
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) // バッファIDの解除
この例では、頂点座標をVBOへ転送しています。
glBindBuffer
の第一引数にGLES20.GL_ARRAY_BUFFER
と指定することでVBOへ転送します。
glBindBuffer
の第二引数に確保したバッファIDを指定します。
次の例では、頂点インデックスをIBOへ転送しています。
val indexBuffer: IntBuffer = ... // 頂点インデックスを計算してIntBufferに変換する
val intByte = 4
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5]) // バッファIDの指定
indexBuffer.position(0)
GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.capacity() * intByte, indexBuffer, GLES20.GL_STATIC_DRAW) // 頂点インデックスを転送
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) // バッファIDの解除
glBindBufferの第一引数にGLES20.GL_ELEMENT_ARRAY_BUFFER
と指定することでIBOへ転送します。
3. 頂点情報をLocationにバインド
GLES20.glEnableVertexAttribArray(noteVtPosLoc) // Location有効
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferIds[0]) // バッファIDの指定
GLES20.glVertexAttribPointer(noteVtPosLoc, 2, GLES20.GL_FLOAT, false, 0, 0) // 頂点情報をLocationにバインド
GLES20.glDisableVertexAttribArray(noteVtPosLoc) // Location解除
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) // バッファIDの解除
Shaderで使用できるように、VBOに格納してある頂点情報をShader変数にバインドします。
2.
と同様にglBindBuffer
でバッファIDを指定します。glVertexAttribPointer
でShaderで使用する変数のLocationを指定します。
4. 頂点インデックスを指定して描画
var faceIndexesBufferCount = // 頂点インデックスの数
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferIds[5]) // バッファIDの指定
GLES20.glDrawElements(GLES20.GL_TRIANGLES, faceIndexesBufferCount, GLES20.GL_UNSIGNED_INT, 0) // 描画
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) // バッファIDの解除
2.
と同様にglBindBuffer
でバッファIDを指定します。
glDrawElements
の第二引数で参照するインデックス数、第四引数でIBOの参照オフセットを設定します。
ソースコード等の詳細は元記事へ