■ 2008年08月29日 [OpenGL] 頂点配列
宅地造成
大学の周辺の山で大規模な宅地造成が行われています.みるみるうちにいろんな建物が建っていくのですが,少し前に,大学からこういう建物が見えることに気づきました.
パルテノン神殿 (@_@;)
聞くところによると,これは住宅地の給水施設だという話です.この団地は国道からの入り口にロダンの「考える人」のレプリカを置いてみたり,なかなか楽しいものをいろいろ作っています.先日この建物の下あたりをのぞきに行ったら,「パルテノン公園」という公園がありましたから,やっぱりこれはパルテノン神殿なんでしょう.多分,この建物から撮影されたと思われる QuickTime VR の画像があります.
頂点配列
書き忘れたと思っていたことの四つ目です.これまでは学生さん向けのチュートリアルを前提に書いてきたので,何も考えずに glBegin() / glEnd() を使っていました.しかし実際のアプリケーションでは,もう頂点配列や Vertex Buffer Object (VBO) を使うのが当たり前だと思います.頂点配列はトゥーンシェーディングの時に使っていましたけど,説明していませんでしたので,改めて書きとめておこうと思います.
データ構造
データは全て三角形とします.まず,頂点位置を保存する配列 vert と,その頂点における法線ベクトル norm,その頂点におけるテクスチャ座標 texc,およびどの頂点を結んで一つの三角形を構成するのかを表した頂点のインデックス face という三つの配列があったとします.
/* 頂点データ */ static GLfloat vert[][3] = { ... }; /* 法線データ */ static GLfloat norm[][3] = { ... }; /* テクスチャ座標 */ static GLfloat texc[][2] = { ... }; /* 頂点のインデックス */ static GLuint face[][3] = { ... };
また,このデータに含まれる三角形の数を nf とします.
/* 三角形の数 */ static int nf = sizeof face / sizeof face[0];
glBegin() / glEnd() による描画
この図形は,glBegin() / glEnd() を使うと,次の手順で描くことができます.
/* ** 図形の表示 */ void display(void) { int i; ... glEnable(GL_TEXTURE_2D); glBegin(GL_TRIANGLES); for (i = 0; i < nf; ++i) { int i0 = face[i][0], i1 = face[i][1], i2 = face[i][2]; glTexCoord2fv(texc[i0]); glNormal3fv(norm[i0]); glVertex3fv(vert[i0]); glTexCoord2fv(texc[i1]); glNormal3fv(norm[i1]); glVertex3fv(vert[i1]); glTexCoord2fv(texc[i2]); glNormal3fv(norm[i2]); glVertex3fv(vert[i2]); } glEnd() glDisable(GL_TEXTURE_2D); ... }
頂点配列による描画
頂点配列を使うと,これを次のように書くことができます.
/*
** 図形の表示
*/
void display(void)
{
...
最初にクライアント(アプリケーション)側に置く頂点,法線,およびテクスチャ座標の配列を有効にします.
/* 頂点データ,法線データ,テクスチャ座標の配列を有効にする */ glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY);
次に,これらのデータが格納されている場所を指定します.
/* 頂点データ,法線データ,テクスチャ座標の場所を指定する */ glVertexPointer(3, GL_FLOAT, 0, vert); glNormalPointer(GL_FLOAT, 0, norm); glTexCoordPointer(2, GL_FLOAT, 0, texc);
- void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *ptr)
- 頂点データの格納場所を指定します.size は1つの頂点に与えるデータの数で,この場合3次元データなので 3 を指定しています.type はデータの型です.stride は頂点データの間隔で,データが詰まって配置されていれば 0 を指定します.ptr はデータの格納場所です.
- void glNormalPointer(GLenum type, GLsizei stride, const GLvoid *ptr)
- 法線データの格納場所を指定します.type,stride,ptr は glVertexPointer と同じです.法線データは1つの頂点に対して必ず3つのデータを持っているので,size を指定する必要はありません.
- void glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const GLvoid *ptr)
- テクスチャ座標の格納場所を指定します.size,type,stride,および ptr は glVertexPointer() と同じです.ここではテクスチャ座標を2次元で指定しているので,size に 2 を指定しています.
そして,テクスチャマッピングを有効にして,このデータを描画します.
/* 頂点のインデックスの場所を指定して図形を描画する */ glEnable(GL_TEXTURE_2D); glDrawElements(GL_TRIANGLES, nf * 3, GL_UNSIGNED_INT, face); glDisable(GL_TEXTURE_2D);
- void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices)
- 頂点配列で与えられた図形を描画します.mode は glBegin() に指定するのと同じ図形のタイプです.count は描画する要素(この場合は頂点)の数です.この場合,三角形が nf 個あるので,与える頂点の数(face の要素の数)は nf * 3 個になります.type は引数 indices の型です.indices はインデックスデータの格納場所です.
最後にクライアント側に置いた頂点,法線,およびテクスチャ座標の配列を無効にします.
/* 頂点データ,法線データ,テクスチャ座標の配列を無効にする */
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
...
}
頂点配列のメリット
この方法は glBegin(),glEnd(),およびその間の glTexCoord2fv(),glNormal3fv(),glVertex3fv() の繰り返しを,glDrawElements() 一つで実行できるので,OpenGL の API(gl* で始まる関数)の呼び出し(グラフィックスサブシステムの機能呼び出し)の回数を大幅に減じることができます.これはデータ量が大きくなるほど顕著になります.
頂点配列の問題
しかし,glDrawElements() を使っても,描画のたびにクライアント側のメモリからサーバ(グラフィックスサブシステム)側のメモリ(ビデオカード上のメモリ等)へのデータ転送が起こります.このデータ転送は低速なバスを介して行われるので,グラフィックスサブシステムがデータ転送の完了を待つために,本来の性能が発揮できないような状況が発生する場合があります.そこで,グラフィックスサブシステム側に十分なメモリがあるときは,データをグラフィックスサブシステム上のメモリに置いたままにして描画を行うことにより,データ転送の回数を減じることができます.これは Vertex Buffer Object (VBO) を使うことにより実現できます.
補足
頂点配列の描画に使う API には,インデックスデータを用いずに描画を行う glDrawArray() や,glDrawElements() および glDrawArray() で描く要素の1つだけを描く glArrayElement() などもありますが,ここでは割愛します.glDrawArray() を使って GL_TRIANGLE_STRIP を描けば効率が良さそうに思えるのですが,うまくデータを作るのは面倒くさそう…