日別アーカイブ: 2009年8月19日

OpenGLES 2.0 頂点フォーマットの管理

OpenGLES2.0 上にほぼ Direct3D 互換の環境を作っています。
頂点形式も同じものです。頂点バッファを作ることも出来ました。

GLuint _flFASTCALL
CreateBuffer(
    unsigned int bytesize,
    GLenum buffer,
    GLenum btype,
    const void* initdata
    )
{
    GLuint   bid;
    glGenBuffers( 1, &bid );
    glBindBuffer( buffer, bid );

    if( initdata ){
        glBufferData( buffer, bytesize, initdata, btype );
        ErrorHandle( "CreateBuffer" );
    }
    return  bid;
}

API 的に VertexBuffer / IndexBuffer の区別が無いのは Direct3D10 以降と同じです。

// VertexBuffer
vbuf= CreateBuffer( bytesize, GL_ARRAY_BUFFER, GL_STATIC_DRAW, vdata );

// IndexBuffer
ibuf= CreateBuffer( bytesize, GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW, idata );

GL_STATIC_DRAW は Direct3D の D3D10_USAGE_/D3D11_USAGE_ や AccessFlag
に相当するものです。

glBIndBuffer( GL_ARRAY_BUFFER, vbuf );   // IASetVertexBuffers()
glBIndBuffer( GL_ELEMENT_ARRAY_BUFFER, ibuf );   // IASetIndexBuffer()

与えられるバッファの区別がないので、描画に使えるバッファは 1つだけ。
頂点データを同じバッファに格納することが前提となっているようです。

シェーダーとのバインドは glVertexAttribPointer() そのままです。

GLint loc_position= glGetAttribLocation( ShaderProgram, "POSITION" );
GLint loc_normal=   glGetAttribLocation( ShaderProgram, "NORMAL" );
GLint loc_texcoord= glGetAttribLocation( ShaderProgram, "TEXCOORD" );
glVertexAttribPointer( loc_position, 3, GL_FLOAT, GL_FALSE, 32, (void*)0 );
glVertexAttribPointer( loc_normal,   3, GL_FLOAT, GL_FALSE, 32, (void*)(sizeof(float)*3) );
glVertexAttribPointer( loc_texcoord, 2, GL_FLOAT, GL_FALSE, 32, (void*)(sizeof(float)*6) );

必要なパラメータもやってることも、D3D10_INPUT_ELEMENT_DESC,
D3D11_INPUT_ELEMENT_DESC によく似ています。
違うのは、バインドシンボルが SEMANTIC ではなく変数名そのままになること。

上の例だとシェーダー側はこんな感じです。
SEMANTIC を直接変数名にすることで D3D と同じような記述になります。

// GLSL
attribute vec3 POSITION;
attribute vec3 NORMAL;
attribute vec2 TEXCOORD;

void main()
{
   gl_Position= vec4( POSITION.xyz, 1.0 ) * Matrix;
}

Direct3D で書くと下記の通りです。

// HLSL
struct VS_INPUT {
   float3  vposition : POSITION;
   float3  vnormal   : NORMAL;
   float2  vtexcoord : TEXCOORD0;
};

float4 main( VS_INPUT vin ) : SV_Position
{
   return  mul( float4( vin.vposition.xyz, 1.0f ), Matrix );
}

デモやサンプルプログラムならこのままでいいのですが、汎用的な仕組みとして
シェーダーとのバインドを考えると、D3D の InputLayout 相当の機能が必要に
なることがわかります。(ID3D10InputLayout / ID3D11InputLayout)

なので実際に作っておきます。

// GLInputLayout.h
enum {
    ELEMENTFLAG_NORMALIZE  =  (1<<0),
    ELEMENTFLAG_ACTIVE     =  (1<<1),
    ELEMENTFLAG_END        =  (1<<2),
};

struct ShaderInputElement {
    unsigned int    Location;
    unsigned int    Type;       // GLenum GL_FLOAT
    unsigned char   Stride;
    unsigned char   ByteOffset;
    unsigned char   Components;
    unsigned char   Flag;
};
// GLInputLayout.cpp
void _flFASTCALL
CreateInputLayout( 
      ShaderInputElement* edesc,
      unsigned int desccount,
      GLuint shader,
      const a5::InputLayout& layout
        )
{
    unsigned int   ecount= layout.GetElementCount();
    assert( ecount <= desccount );
    for( unsigned int ei= 0 ; ei< ecount ; ei++, edesc++ ){
        const a5::InputElement*	ep= layout.GetElement( ei );
        unsigned int	component= 0;
        unsigned int	normal= 0;
        edesc->Flag= 0;
        if( ei == ecount-1 ){
            edesc->Flag|= ELEMENTFLAG_END;
        }
        GLint  loc= glGetAttribLocation( shader, ep->SemanticName );
        ErrorHandle( "Layout" );
        if( loc < 0 ){
            continue;
        }
        edesc->Location= loc;
        edesc->Type= PixelToFormat( ep->pixf, &component, &normal );
        edesc->Stride= static_cast( layout.GetStrideSize() );
        edesc->Components= static_cast( component );
        edesc->ByteOffset= static_cast( ep->ByteOffset );
        if( normal ){
            edesc->Flag|= ELEMENTFLAG_NORMALIZE;
        }
        edesc->Flag|= ELEMENTFLAG_ACTIVE;
    }
}

void _flFASTCALL
IASetInputLayout( const ShaderInputElement* edesc )
{
    const ShaderInputElement*   ep= edesc;
    for(;; ep++ ){
        if( ep->Flag & ELEMENTFLAG_ACTIVE ){ 
            glVertexAttribPointer(
                ep->Location,
                ep->Components,
                ep->Type,
                (ep->Flag & ELEMENTFLAG_NORMALIZE) ? GL_TRUE : GL_FALSE,
                ep->Stride,
                (const void*)ep->ByteOffset
                );
            glEnableVertexAttribArray( ep->Location );
        }
        if( ep->Flag & ELEMENTFLAG_END ){
            break;
        }
    }
}

void _flFASTCALL
IAResetInputLayout( const ShaderInputElement* edesc )
{
    const ShaderInputElement*	ep= edesc;
    for(;; ep++ ){
        if( ep->Flag & ELEMENTFLAG_ACTIVE ){ 
            glDisableVertexAttribArray( ep->Location );
        }
        if( ep->Flag & ELEMENTFLAG_END ){
            break;
        }
    }
}

CreateInputLayout() はオブジェクトを生成しないで固定バッファを使用します。
ShaderInputElement がそのまま描画時に使える InputLayout 情報になります。

複雑な Flag 管理を行っているのは、エンドマークやバッファ数を別に持たなくて
済むようにです。必要 Element 分、固定で渡されたバッファで完結させるため。
メモリアロケートに抵抗がなければオブジェクトにした方がずっとシンプルでしょう。
ShaderInputElement の Type が 32bit なのは GLenum の互換性を考えたためです。
実際のシンボル値は 16bit なので、問題なければ圧縮して total 8byte に収まります。
シェーダーに対応するシンボルが無ければ頂点のセットアップを行いません。

a5::InputLayout は、個人的に Direct3D で使用している頂点情報管理 object です。
本来はこの object を元に D3D11_INPUT_ELEMENT_DESC を生成して、
ID3D11InputLayout を作っていました。

ELEMENTFLAG_NORMALIZE の定義値が 1 なのは念のため。

PixelToFormat() は頂点フォーマットに含まれている Direct3D の
DXGI_FORMAT を OpenGL 形式に変換しています。詳細は次回以降で。

// 初期化
a5::InputLayout layout( ~ );
ShaderInputElement vLayout[3];
CreateInputLayout( vLayout, 3, ShaderProgram, layout );

// 描画時
glBIndBuffer( GL_ARRAY_BUFFER, vbuf );   // IASetVertexBuffers()
IASetInputLayout( vLayout );
glBIndBuffer( GL_ELEMENT_ARRAY_BUFFER, ibuf );   // IASetIndexBuffer()

glDrawElements( GL_TRIANGLES, Indices, GL_UNSIGNED_SHORT, NULL );

頂点バッファ (頂点バッファオブジェクト)を使った描画命令も、頂点の設定同様
これまでと同じものでした。
Index の配列はすでに GL_ELEMENT_ARRAY_BUFFER で渡しているため
glDrawElements() の最後は NULL になります。
Indices は Index の個数なので GL_TRIANGLES だと常に 3 の倍数となります。

描画の仕組みを理解して API を使うだけなら難しいことは特にないです。
でも実際にゲーム開発を行っていると、サンプルだけではわからない事項がいくつも
出てきます。3D の描画エンジンを作っていて常に悩まされるのは、
汎用的に使える仕組みとその設計部分です。

例えば CG ツールからどうやって export してどんなフォーマットで保存するか、
数多く存在する頂点フォーマットやピクセルフォーマットに対応するにはどうするか、
データとシェーダーの対応付けやシェーダーの管理をどうやって行うか、など。

3D 系プログラムの開発はこのような地味で面倒なことの方が多いです。
blog の冒頭説明でも書いてますが、ゲーム開発のノウハウの差というのは目新しい
技術の実装だけでなく、データ制作やデータの管理もかなり含まれているはずです。

とりあえず export したモデルが表示できて、任意のシェーダーを適用して
アニメーション出来るところまでは作成済み。

関連エントリ
OpenGLES2.0 の頂点
OpenGLES2.0 D3D座標系
OpenGLES2.0 シェーダー管理