OpenGL:メッシュ表示


~ OpenGL: Rendering meshes ~






■はじめに

碁石の表示に glutSolidSphere を使っていたのですが、 さすがにそれは無いだろうということで独自メッシュで表示するようにしてみました。
まぁ、シェーディングしてないので、こんなのにしたところでたいしたことはないですな

今回のプログラムは、次のものです。
タイトル画面でほっぽっておくとデモモードに突入します。

ソースには、いつものように大量にファイルが入っています。 今回関係するファイルは次のものです。

src/Include/TE/te_model.hアプリケーションのとのインターフェイス
src/Engine/model/te_tem_struct.hメッシュデータの構造体
src/Engine/model/te_tms.hメュシュクラスのヘッダ
src/Engine/model/te_tms.cppメュシュクラスの実体

まだ「仕事でもこんな感じでやってるの?」とか聞いてくる人がいますが、 家で扱いやすい手っ取り早い環境として構築しているので、 仕事でやってることとは全然関係ないと考えてください。

■簡単なやつ

最初に一番簡単なポリゴンの描画方法をおさらいしておきましょう。
OpenGLでのポリゴンの描画は、glBegin, glEnd で囲って、その中で glVertex3f 等の関数で頂点位置を指定していきます。
glBegin で描画する種類を指定しますが、GL_TRIANGLES を使うと頂点指定3つおきにポリゴンが描画されます。
各 glVertex3f の前に、glNormal3f や、glTexCoord2f で法線ベクトルやテクスチャ座標を指定すると、それらのデータを使って描画します。

    glBegin( GL_TRIANGLES ); // 描画開始(3角形ポリゴン)
    
    // 1つ目の頂点の設定
    glNormal3f  ( 0.0, 1.0, 0.0); // 法線ベクトルの設定
    glTexCoord2f( 0.0, 0.0 ); // テクスチャ座標の設定
    glVertex3f  ( 1.0, 0.0, 0.0); // 位置座標の設定
    // 2つ目の頂点の設定
    glNormal3f  ( 0.0, 1.0, 0.0);
    glTexCoord2f( 1.0, 0.0 );
    glVertex3f  ( 0.0, 0.0,-1.0);
    // 3つ目の頂点の設定
    glNormal3f  ( 0.0, 1.0, 0.0);
    glTexCoord2f( 0.0, 1.0 );
    glVertex3f  (-1.0, 0.0, 0.0); // このコマンドでポリゴンが描画される)
    
    // 2つ以上のポリゴンを描画するときは、ここにコマンドを追加
    
    glEnd();              // 描画終了

glVertex3f の後に「v」をつけた glVertex3fv 等を使うと、浮動小数点配列をベクトルとみなして1つの引数だけで各データの設定を行うことができます。
こちらの方がシステマチックな処理をするのに向いています。

    const int vertex_count = 3;
    GLfloat p[3][]={{ 1.0, 0.0, 0.0}, { 0.0, 0.0,-1.0}, {-1.0, 0.0, 0.0}}; // 位置座標
    GLfloat n[3][]={{ 0.0, 1.0, 0.0}, { 0.0, 1.0, 0.0}, { 0.0, 1.0, 0.0}}; // 法線ベクトル
    GLfloat t[2][]={{ 0.0, 0.0,    }, { 1.0, 0.0,    }, { 0.0, 1.0,    }}; // テクスチャ座標
    
    glBegin( GL_TRIANGLES ); // 描画開始(3角形ポリゴン)
    
    for( int i=0; i < vertex_count; i++ )
    {
        glNormal3fv  ( n[i] ); // 法線ベクトルの設定
        glTexCoord2fv( t[i] ); // テクスチャ座標の設定
        glVertex3fv  ( p[i] ); // 位置座標の設定(3の倍数回目のこのコマンドでポリゴンが描画される)
    } 
    
    glEnd();              // 描画終了

いや、こんなのデモとか負荷に余裕がある部分で無い限りありえないでしょ。
いちいちこんな関数呼び出して設定してたらCPUの無駄使いしすぎ。
製品レベルでこんな方法使ってる人なんていないでしょ。もっといい方法ないの?
と、思って当たり前ですよね。
もっと効率的な方法がいくつもあるらしいので、ちょっとやってみましょう。

■ディスプレイリスト

OpenGLでは、gl*** といったコマンドを保存して必要なときにそれらのコマンド群を呼び出す機能があります。
静的な情報なら、コマンドを保存しておけば、(ドライバしだいで)最適化された情報が作られ、もしくはグラフィックシステムのほうにあらかじめ送っておくことができるので高速化が期待されます。

ディスプレイリストの使い方は、最初に glNewList と glEndList で囲った中にOpenGLのコマンドを列挙して溜め込んでおき、描画時に glCallList でそのディスプレイリストを呼び出します。
ディスプレイリストは、複数用意することができます。それらを区別するためにユニークなIDを用意しなくてはなりませんが、それは、glGenLists で確保することができます。

初期化時のプログラムは次のようになります。

te_tms.cpp
0085:     const PTMS_HEADER pHeader = (const PTMS_HEADER)p;// データのヘッダ。独自形式なので気にするな
0086:     _nVertex = pHeader->vertex_count;// 頂点数
0087:     
0091:     _pPosition = (const float *)(pHeader+1);// 頂点座標の配列へのポインタ
0092:     _pNormal   = _pPosition + 3 * _nVertex;// 法線ベクトル配列へのポインタ
0093:     _pTexCoord = _pNormal   + 3 * _nVertex;// テクスチャ座標配列へのポインタ
0094: 
0096:     _iDisplayList = glGenLists(1);// ディスプレイ・リストを生成する(ユニークなIDの発行)
0097:     glNewList( _iDisplayList, GL_COMPILE );// ディスプレイ・リストを作成
0098: 
0099:         // ディスプレイリストの作成
0100:         glBegin( GL_TRIANGLES );
0101:         for( unsigned int i = 0; i < _nVertex; i++ )
0102:         {
0103:             glNormal3fv  (   _pNormal ); _pNormal   += 3;
0104:             glTexCoord2fv( _pTexCoord ); _pTexCoord += 2;
0105:             glVertex3fv  ( _pPosition ); _pPosition += 3;
0106:         }
0107:         glEnd();
0108: 
0109:     glEndList();// ディスプレイリストを保存する

描画時は、次のように glCallList を呼び出すだけです。

te_tms.cpp
0219: void CTms::Render( )
0220: {
0222:     glCallList( _iDisplayList );
0253: }

最後は、glDeleteLists で使っているメモリを開放します。

te_tms.cpp
0138: CTms::~CTms( )
0139: {
0141:     glDeleteLists( _iDisplayList, 1 );
0155: }

では、この方法が一番早いらしいので、とりあえず、この方法を身につけておけばよいでしょう。

■頂点配列

ディスプレイリストもいいんですが、実際問題、一個一個頂点や法線ベクトルを設定するなんてやってられないわけですよ。
それに、パフォーマンスを求められる場所は、データ化されている場合が多いのであらかじめ静的なデータを準備して、それらを処理できるようになっている方が便利になってきます。
ということかどうかは分からんですが、浮動小数点数の配列を一気に設定する機能があります。それが頂点配列です。

頂点配列では、glVertex3f などの関数の代わりに glVertexPointer をつかって、データ配列の先等のポインタを指定します。そして、glDrawArraysを使って一気に描画します。なお、使うときには、glEnableClientState を使ってあらかじめ頂点配列を使うことを宣言しておく必要があります。

次の例でも分かるように、頂点配列は、ディスプレイリスト化しなくても使うことができます。

te_tms.cpp
0216: // ---------------------------------------------------------------------------
0217: // 描画
0218: // ---------------------------------------------------------------------------
0219: void CTms::Render( )
0220: {
0224:     // データ配列を有効にする
0225:     glEnableClientState( GL_VERTEX_ARRAY );
0226:     glEnableClientState( GL_NORMAL_ARRAY );
0227:     glEnableClientState( GL_TEXTURE_COORD_ARRAY );
0228: 
0229:     // データをポインタにセットする
0239:     {
0240:         glVertexPointer( 3, GL_FLOAT, 0, _pPosition );
0241:         glNormalPointer( GL_FLOAT, 0, _pNormal );
0242:         glTexCoordPointer( 2, GL_FLOAT, 0, _pTexCoord );
0243:     }
0244:     
0245:     // 描画
0246:     glDrawArrays( GL_TRIANGLES, 0, _nVertex );
0247: 
0248:     // データ配列を無効にする
0249:     glDisableClientState( GL_VERTEX_ARRAY );
0250:     glDisableClientState( GL_NORMAL_ARRAY );
0251:     glDisableClientState( GL_TEXTURE_COORD_ARRAY );
0253: }

■頂点バッファオブジェクト

で、OpenGL 1.5 から使えるようになったのが「Buffer Objects」というやつらしいです。これは、色々な配列をグラフィックスシステムのメモリに置くことによってパフォーマンスをあげようというものです。
なんか、NeHe Productionsさんいいお手本があるらしいので、これをリスペクトしてみましょう。

Extension を使うので、それらの関数ポインタを用意する必要があります。 これらは適当にサンプルから拝借してきましょう。

te_tms.cpp
0015: // glext.h からの VBO Extension の定義
0016: #define GL_ARRAY_BUFFER_ARB 0x8892
0017: #define GL_STATIC_DRAW_ARB 0x88E4
0018: typedef void (APIENTRY * PFNGLBINDBUFFERARBPROC)    (GLenum target, GLuint buffer);
0019: typedef void (APIENTRY * PFNGLDELETEBUFFERSARBPROC) (GLsizei n, const GLuint *buffers);
0020: typedef void (APIENTRY * PFNGLGENBUFFERSARBPROC)    (GLsizei n, GLuint *buffers);
0021: typedef void (APIENTRY * PFNGLBUFFERDATAARBPROC)    (GLenum target, int size, const GLvoid *data, GLenum usage);
0022: 
0023: // VBO Extension 関数のポインタ
0024: PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL;                  // VBO 名前生成
0025: PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL;                  // VBO 結びつけ
0026: PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL;                  // VBO データロード
0027: PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL;            // VBO 削除

使う関数自体は、wglGetProcAddress で所得することができます。

te_tms.cpp
0190: // ---------------------------------------------------------------------------
0191: // 描画能力を検証する
0192: // ---------------------------------------------------------------------------
0193: void CTms::CheckDeviceAbility()
0194: {
0200:     // VBO のサポートを確認する
0202:     _fVBOSupported = IsExtensionSupported( "GL_ARB_vertex_buffer_object" );
0203:     if( _fVBOSupported )
0204:     {
0205:         // GL 関数のポインタを所得する
0206:         glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");
0207:         glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");
0208:         glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");
0209:         glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");
0210:     }
0214: }

その前に、Extension がサポートされているのか調べましょう。 glGetString(GL_EXTENSIONS) で、使える Extension 一覧が所得できるので、その中で必要なものがあるか調べます。

te_tms.cpp
0157: // ---------------------------------------------------------------------------
0158: // エクステンションのサポートを調べる
0159: // ---------------------------------------------------------------------------
0160: static bool IsExtensionSupported( char* szTargetExtension )
0161: {
0162:     const unsigned char *pszExtensions = NULL;
0163:     const unsigned char *pszStart;
0164:     unsigned char *pszWhere, *pszTerminator;
0165: 
0166:     // Extension の名前が正しいか調べる(NULLや空白が入っちゃ駄目)
0167:     pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );
0168:     if( pszWhere || *szTargetExtension == '\0' )
0169:         return false;
0170: 
0171:     // Extension の文字列を所得する
0172:     pszExtensions = glGetString( GL_EXTENSIONS );
0173: 
0174:     // 文字列の中に必要な extension があるか調べる
0175:     pszStart = pszExtensions;
0176:     for(;;)
0177:     {
0178:         pszWhere = (unsigned char *) strstr( (const char *) pszStart, szTargetExtension );
0179:         if( !pszWhere )
0180:             break;
0181:         pszTerminator = pszWhere + strlen( szTargetExtension );
0182:         if( pszWhere == pszStart || *( pszWhere - 1 ) == ' ' )
0183:             if( *pszTerminator == ' ' || *pszTerminator == '\0' )
0184:                 return true;
0185:         pszStart = pszTerminator;
0186:     }
0187:     return false;
0188: }

頂点バッファオブジェクトを使う時は、初期化のときに頂点バッファオブジェクトを作らなくてはなりません。
頂点バッファオブジェクトを作るには、最初に glGenBuffersARB を使ってユニークなIDを所得し、glBindBufferARB で所得したIDのバッファを有効にしてから glBufferDataARB でデータを作ります。

te_tms.cpp
0080: // ---------------------------------------------------------------------------
0081: // コンストラクタ
0082: // ---------------------------------------------------------------------------
0083: CTms::CTms( const PTMS_DATA p )
0084: {
0085:     const PTMS_HEADER pHeader = (const PTMS_HEADER)p;
0086:     _nVertex = pHeader->vertex_count;
0087:     
0088:     // -----------------------------------------------------------------------
0089:     // データ配列の所得
0090:     // -----------------------------------------------------------------------
0091:     _pPosition = (const float *)(pHeader+1);
0092:     _pNormal   = _pPosition + 3 * _nVertex;
0093:     _pTexCoord = _pNormal   + 3 * _nVertex;
0094: 
0112:     // -----------------------------------------------------------------------
0113:     // 頂点データをグラフィックスカードのメモリに格納する。
0114:     // -----------------------------------------------------------------------
0115:     // 位置座標バッファを生成して結びつける
0116:     glGenBuffersARB( 1, &_nVBOPosition );                           // ID を得る
0117:     glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBOPosition );          // バッファを結びつける
0118:     glBufferDataARB( GL_ARRAY_BUFFER_ARB, _nVertex*3*sizeof(float), _pPosition, GL_STATIC_DRAW_ARB );
0119: 
0120:     // 法線ベクトルバッファを生成して結びつける
0121:     glGenBuffersARB( 1, &_nVBONormal );                             // ID を得る
0122:     glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBONormal );            // バッファを結びつける
0123:     glBufferDataARB( GL_ARRAY_BUFFER_ARB, _nVertex*3*sizeof(float), _pNormal, GL_STATIC_DRAW_ARB );
0124: 
0125:     // テクスチャ座標バッファを生成して結びつける
0126:     // Generate And Bind The Texture Coordinate Buffer
0127:     glGenBuffersARB( 1, &_nVBOTexCoord );                           // ID を得る
0128:     glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBOTexCoord );          // バッファを結びつける
0129:     glBufferDataARB( GL_ARRAY_BUFFER_ARB, _nVertex*2*sizeof(float), _pTexCoord, GL_STATIC_DRAW_ARB );
0132: }

描画する時は、glVertexPointer で頂点配列の指定をするのではなく、glBindBufferARB で頂点バッファオブジェクトを有効にしてから、glVertexPointer にヌルポを渡して設定します。

te_tms.cpp
0216: // ---------------------------------------------------------------------------
0217: // 描画
0218: // ---------------------------------------------------------------------------
0219: void CTms::Render( )
0220: {
0224:     // データ配列を有効にする
0225:     glEnableClientState( GL_VERTEX_ARRAY );
0226:     glEnableClientState( GL_NORMAL_ARRAY );
0227:     glEnableClientState( GL_TEXTURE_COORD_ARRAY );
0228: 
0229:     // データをポインタにセットする
0231:     {
0232:         glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBOPosition );
0233:         glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL );
0234:         glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBONormal );
0235:         glNormalPointer( GL_FLOAT, 0, (char *) NULL );
0236:         glBindBufferARB( GL_ARRAY_BUFFER_ARB, _nVBOTexCoord );
0237:         glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL );
0243:     }
0244:     
0245:     // 描画
0246:     glDrawArrays( GL_TRIANGLES, 0, _nVertex );
0247: 
0248:     // データ配列を無効にする
0249:     glDisableClientState( GL_VERTEX_ARRAY );
0250:     glDisableClientState( GL_NORMAL_ARRAY );
0251:     glDisableClientState( GL_TEXTURE_COORD_ARRAY );
0253: }

glVertexPointer の3つめの引数は異なる頂点の時のデータの幅です。
今回はこの引数は0にしてデータごとに別の配列を用意していますが、これはデータキャッシュの効率が最悪です。
本当は、頂点データの構造体を定義して、その構造体の配列としてデータを準備するような構造にしないとパフォーマンス的にダメダメでしょう。

あっ、最後に glDeleteBuffersARB を使って、使用した頂点バッファオブジェクトを開放しておかないといけません。

te_tms.cpp
0135: // ---------------------------------------------------------------------------
0136: // デストラクタ
0137: // ---------------------------------------------------------------------------
0138: CTms::~CTms( )
0139: {
0144:     // VBO の削除
0146:     {
0147:         unsigned int nBuffers[] = { _nVBOPosition, _nVBONormal, _nVBOTexCoord };
0148:         glDeleteBuffersARB( 3, nBuffers );
0149:     }
0155: }

ゆくゆくは、この頂点バッファオブジェクトに最適化されるようになるのではないでしょうか。

■最後に

まだ、FPSの機能とか追加してないので速度計れないのですが、どれがいいのでしょうかねぇ





もどる

imagire@gmail.com