はじめに
基本的な図形の描画方法のためVAOとVBOを紹介する
解説用ソースコード
以下のソースコードを元に、解説を進めていきます。
01-render_with_vbo_vao
VBOとVAO、シェーダーを利用して図形を描画する。
モダンなOpenGLにおいては、この3つを用いて描画を行う。
テクニックによっては、いろいろなアプローチがあるが、基本としてこの描画の仕組みが分かればOKだと思う。
VBO(Vertex Buffer Object)
実際に頂点情報を格納するGPU側のバッファ。ここに頂点の位置や色、法線、テクスチャ座標等を格納する。GL_ARRAY_BUFFERというバインドポイントで生成する。
VAO(Vertex Array Object)
VAOには、2つの機能がある。
1. バッファオブジェクトの属性をまとめる機能
複数のVBOの頂点属性(位置/色/法線など)を一つのVAOへまとめる役割をもつ。頂点属性だけでなくインデックスバッファや、インダイレクトバッファ等のバッファオブジェクトもまとめることができる。
2. シェーダー側への橋渡しであるインターフェイス的な機能
シェーダー側でC++側で生成した頂点データの処理を行うとき、どれが頂点の位置で色で法線なのかわかる必要がある。VAOはこれを解決する橋渡し的な機能を持っている。
シェーダー
Fragment ShaderとVertex Shaderが必須のシェーダーステージなので、それらを使う。
大まかな流れ
ざっくりではあるが、以下の手順で描画までもっていく。
- 頂点データを準備
- VBOに頂点データを格納
- VAOにVBOをまとめる
- シェーダーを適用し、ドローコールで描画
それぞれコードベースで解説する....
頂点データを準備 ~ VBOに頂点データを格納
以下は頂点情報を用意し、VBOに格納しているコード。配列を作成し、glBufferData()
でGPU側のバッファへデータを渡す。色情報も同じ要領で作成する。
GLfloat vertices[] = {
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
glGenBuffers(1, &vertex_vbo); //バッファを作成
glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo); //GLコンテキストにvertex_vboをGL_ARRAY_BUFFERでバインド。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //実データを格納
VAOにVBOをまとめる
ここでは、位置属性を「0」、色属性「1」へバインドしている。後ほど、シェーダー側で位置と色を参照する。
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// Position attribute
glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo); //以下よりvertex_vboでバインドされているバッファが処理される
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
// Color attribute
glBindBuffer(GL_ARRAY_BUFFER, color_vbo); //以下よりcolor_vboでバインドされているバッファが処理される
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
シェーダーを適用し、ドローコールで描画
GLコンテキストにvaoをバインド。glDrawArrays()で描画。
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
oFでも同じことをやってみる。
oFから、VAOとVBOを利用するには、ofVboを利用する。(ややこしいが、VAOの機能もofVboは持っている)
頂点データを準備
頂点データは、上記のverticesを再利用してもいいのだが、ofVboの仕様に合わせるためofVec3fとofFloatColorで再定義を行う。配列は、std::arrayで作成してみた。
ofVbo of_vao;
std::array<ofVec3f, 3> of_vertex;
of_vertex[0] = ofVec3f(0.5f, 0.5f, 0.0f);
of_vertex[1] = ofVec3f(-0.5f, 0.5f, 0.0f);
of_vertex[2] = ofVec3f(0.0f, -0.5f, 0.0f);
std::array<ofFloatColor, 3> of_color;
of_color[0] = ofFloatColor(1.0f, 0.0f, 1.0f, 1.0f);
of_color[1] = ofFloatColor(1.0f, 1.0f, 0.0f, 1.0f);
of_color[2] = ofFloatColor(0.0f, 1.0f, 1.0f, 1.0f);
VAOにVBOをまとめる
バインドにあたり、ofVboではラッパー関数が用意されている。(位置/色/法線/テクスチャ座標/インデックス/任意の属性)いちいち、'glEnableVertexAttribArray()'などやらなくてよい。
それぞれ、ofVboのラッパー関数でVBOにまとめると、以下の属性にバインドされる。
0:位置
1:色
2:法線
3:テクスチャ座標
4~:任意の属性
of_vao.bind();
of_vao.setVertexData(&of_vertex[0], 3 * sizeof(ofVec3f), GL_STATIC_DRAW);
of_vao.setColorData(&of_color[0], 3 * sizeof(ofFloatColor), GL_STATIC_DRAW);
of_vao.unbind();
ドローコールで描画
draw()
メソッドで、glDrawArrays()
を呼び出し描画する。
GLコンテキストのバインドも自動でやってくれる。
of_vao.draw(GL_TRIANGLES, 0, 3);
キャプチャ
ソースをビルドすると、以下の様になる。
まとめ
- VBO/VAOによる基本の描画方法を紹介した。
- oFにおいてはVBO/VAOは、ofVboを利用する、
余談1:glBufferDataの第4引数、迷ったらGL_DYNAMIC_DRAWでOK
glBufferDataは、バッファにデータを格納する関数だ。実データだけでなく、その容量をByteで指定する。また、第4引数だが、バッファの扱いに応じて以下をセットする。
- GL_DYNAMIC_DRAW
- GL_DYNAMIC_READ
- GL_STREAM_COPY
- GL_STREAM_DRAW
- GL_STREAM_READ
- GL_STATIC_COPY
- GL_STATIC_DRAW
- GL_STATIC_READ
更新頻度が高い順に、DYNAMIC→STREAM→STATIC。
用途に応じて、DRAW、READ、COPYをそれぞれ選択するが、
「迷ったらGL_DYNAMIC_DRAWを使おう。一番一般的なバッファの使い方だし」
とOpenGL Super Bible 5th Editionにあるので、何も思いつかないのであればGL_DYNAMIC_DRAWでいいと思う。
余談2:Interleavedで格納すると、パフォーマンスが上がるらしい
ここでは[PPPPPPPPPPPP.....P]、[CCCCCCCCCCC.....C]というようにVBOを2つ用意して1つのVAOにまとめている。(P:位置、C:色)これはSeparateと呼ばれる格納方式。
一方で、Interleavedという格納方式も存在する。これは、一つのVBOに[PC/PC/PC/PC/PC/PC/PC/...../PC]という並びで頂点情報を格納するという方式だ。キャッシュヒット的観点からこっちのほうが速いらしい。