頂点バッファオブジェクト(VBO)については、t-potさんや床井研究室さん、Project Asuraさんといった、有名どころの方々がサンプルを載せてくれています。
が、t-potさんの例では、頂点座標、法線、テクスチャ座標をそれぞれ別個のVBOとして用意しているため、t-potさんも文中で書かれていますが、
GPUのデータキャッシュのヒット率が非常に悪くなり、パフォーマンスが出ません。また、インデックスバッファを使っていないというところも非効率と言えるでしょう。
また、床井研究室さんのところでは、ページの一番下のところで、頂点座標、法線、テクスチャ座標のデータを一つのVBOに格納したサンプルを示されていますが、
という感じで、頂点座標、法線、テクスチャ座標それぞれのデータが離れているため、これもGPUのキャッシュのヒット率が悪く、パフォーマンスがよくありません。
頂点座標、法線、テクスチャ座標といった、頂点座標およびその頂点の属性は、メモリ的に近い位置に格納されていなければいけません。CPU側のコードで言えば、こんなイメージですね。↓
struct D3DXVECTOR3 { float x; float y; float z; }; struct D3DXVECTOR2 { float x; float y; }; struct MY_VERTEX { D3DXVECTOR3 vPos; // 頂点座標 D3DXVECTOR3 vNorm; // 法線 D3DXVECTOR2 vTex; // テクスチャ座標 }; MY_VERTEX *vertex = new MY_VERTEX[1000]; //MY_VERTEX型の頂点が1000個メモリ上に並んでいる。 |
MY_VERTEXのような構造体を利用することによって、頂点座標、法線、テクスチャ座標といった頂点データが、メモリ上で隣り合って存在するわけです。
こうするとGPUのキャッシュもヒットしやすくなり、効率が良くなります。図にすると以下のような感じです。
では、上記のような、頂点ごとのデータを構造体でまとめたものの配列とする構造を作り、この構造をVBOで利用するためには、具体的にどういうコードを書けば良いのでしょうか?
方法は2つあります。
1つが、glInterleavedArrays関数を用いる方法、
2つ目はglVertexPointer、glNormalPointer、glTexCoordPointer関数でストライドとオフセットを構造体にあわせて設定する方法です。
1つ目のglInterleavedArrays関数を用いる方法については、Project Asuraさんのページで解説されています。ただ、私はこの方法はお勧めしません。理由は2つあります。
一つは、
glInterleavedArraysでは頂点データの構造を、あらかじめ決められた定数で指定するのですが、このパターンがたった14パターンしかないという点です。
よって、glInterleavedArraysを使いたい場合は、サポートされているパターンに、頂点データ構造体の構造をあわせる必要があるのです。
しかし、一般的に使われる頂点データの構造パターンは多岐にわたります。作ろうとしている3Dアプリケーションの設計上の理由から、
自分が決めたこの頂点データ構造でないと絶対にダメだ!という場合もあるでしょう。そういう場合にはglInterleavedArraysでは対応できないのです。
二つ目の理由は、OpenGLの組み込み機器向けのサブセットであるOpenGL|ESではglInterleavedArraysが今のところ使えないという点です。
OpenGL|ESはiPhoneやAndroid携帯など多くのポータブルデバイスで使われており、これらで使えないというのはかなりのデメリットです。
よって、私は先に挙げた2つ目の方法、
「glVertexPointer、glNormalPointer、glTexCoordPointerでストライドとオフセットを構造体にあわせて設定する方法」をお勧めします。
具体的な設定方法は以下のサンプルコードをご覧下さい。
#define BUFFER_OFFSET(bytes) ((GLubyte *)NULL + (bytes)) #define VERTEX_NUM 1000 #define INDEX_NUM 2000 struct MY_VERTEX { D3DXVECTOR3 vPos; // 頂点座標 D3DXVECTOR3 vNorm; // 法線 D3DXVECTOR2 vTex; // テクスチャ座標 }; MY_VERTEX *vertexBuf = new MY_VERTEX[VERTEX_NUM]; //CPU側の頂点バッファ領域を用意 int *indexBuf = new int[INDEX_NUM]; //CPU側のインデックスバッファ領域を用意 /* CPU側の頂点バッファとインデックスバッファのデータを用意。ここは省略する。 */ // GPU上に頂点バッファを作成する。CPU側の頂点バッファvertexBufのデータをコピー。 glGenBuffers(1, &vertexBuf_name); glBindBuffer(GL_ARRAY_BUFFER, vertexBuf_name); glBufferData(GL_ARRAY_BUFFER, sizeof( MY_VERTEX ) * VERTEX_NUM, vertexBuf, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); // GPU上にインデックスバッファを作成する。CPU側のインデックスバッファのデータをコピー。 glGenBuffers(1, &indexBuf_name); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuf_name); BufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof( int ) * INDEX_NUM, indexBuf, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //インデックスバッファーをセット glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuf_name); //頂点バッファを設定、セット glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY);
GLsizei stride = sizeof( MY_VERTEX ); //ストライド(頂点データ長)を頂点構造体MY_VERTEXの大きさに。 glBindBuffer(GL_ARRAY_BUFFER, vertexBuf_name); glVertexPointer(3, GL_FLOAT, stride, NULL); glNormalPointer(GL_FLOAT, stride, BUFFER_OFFSET(sizeof(D3DXVECTOR3))); glTexCoordPointer(2, GL_FLOAT, stride, BUFFER_OFFSET(sizeof(D3DXVECTOR3)*2)); //描画 glDrawElements(GL_TRIANGLES, INDEX_NUM, GL_UNSIGNED_INT, BUFFER_OFFSET(0)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); |
赤字のコードがポイントです。オフセットとストライドを設定しています。
glVertexPointerとglTexCoordPointerの第4引数、glNormalPointerの第3引数にはオフセットを指定します。
このオフセットはどこからのオフセットかというと、頂点バッファvertexBuf_nameの先頭アドレスからのオフセットです。もう一度、我々が想定している頂点データの構造を再確認してみましょう。
struct MY_VERTEX { D3DXVECTOR3 vPos; // 頂点座標 D3DXVECTOR3 vNorm; // 法線 D3DXVECTOR2 vTex; // テクスチャ座標 }; |
でしたね。
頂点座標の場合は、頂点構造体MY_VERTEXを見てみると、一番最初にあるので、glVertexPointerに設定するオフセットは0(NULL)です。
法線の場合は、MY_VERTEXを見てみると、D3DXVECTOR3 vPosの直後ですから、glNormalPointerで設定するオフセットはsizeof(D3DXVECTOR3)です。
テクスチャ座標の場合は、D3DXVECTOR3 vNormの直後なので、glTexCoordPointerで設定するオフセットはsizeof(D3DXVECTOR3)*2です。
次にストライドですが、ストライドとは次のデータ位置までの距離のことです。
glVertexPointerとglTexCoordPointerの第3引数、glNormalPointerの第2引数でストライドを指定します。
例えば、頂点座標で考えてみると、最初の頂点座標から、次の頂点座標までのメモリ上の距離は構造体MY_VERTEXのサイズ、つまりsizeof( MY_VERTEX )です。
法線やテクスチャ座標も、最初のデータから次のデータまでの距離はsizeof( MY_VERTEX )です。
つまり、glVertexPointer、glNormalPointer、glTexCoordPointerいずれにも、設定すべきストライドはsizeof( MY_VERTEX )でよいことになります。簡単ですね。
余談ですが、頂点座標、法線、テクスチャ座標それぞれを以下のように連続したメモリに格納している場合は、設定するストライド値は0になります。
sizeof(D3DXVECTOR3)とかを指定してもいいんですが、0を指定すると頂点データがすぐ隣に隣接していることを示す、という関数仕様になっているので、ここは気楽に0を指定しましょう。
いかがでしたでしょうか。「glVertexPointer、glNormalPointer、glTexCoordPointerでストライドとオフセットを構造体にあわせて設定する方法」を使うことで、どのような頂点構造にも対応することができます。
実際の3Dアプリケーションでは、頂点座標、法線、テクスチャ座標の他にも接線ベクトルなど、他にも必要な頂点属性があることがありますが、そういう場合でもこのやり方は同様に使えます。
このVBOの使い方で、OpenGLのパフォーマンスを引き出しましょう!