2009/08/19
OpenGLES 2.0 頂点フォーマットの管理
Posted by: oga
at 22:42
OpenGLES2.0 上にほぼ Direct3D 互換の環境を作っています。
頂点形式も同じものです。頂点バッファを作ることも出来ました。
API 的に VertexBuffer / IndexBuffer の区別が無いのは Direct3D10 以降と同じです。
GL_STATIC_DRAW は Direct3D の D3D10_USAGE_/D3D11_USAGE_ や AccessFlag
に相当するものです。
与えられるバッファの区別がないので、描画に使えるバッファは 1つだけ。
頂点データを同じバッファに格納することが前提となっているようです。
シェーダーとのバインドは glVertexAttribPointer() そのままです。
必要なパラメータもやってることも、D3D10_INPUT_ELEMENT_DESC,
D3D11_INPUT_ELEMENT_DESC によく似ています。
違うのは、バインドシンボルが SEMANTIC ではなく変数名そのままになること。
上の例だとシェーダー側はこんな感じです。
SEMANTIC を直接変数名にすることで D3D と同じような記述になります。
Direct3D で書くと下記の通りです。
デモやサンプルプログラムならこのままでいいのですが、汎用的な仕組みとして
シェーダーとのバインドを考えると、D3D の InputLayout 相当の機能が必要に
なることがわかります。(ID3D10InputLayout / ID3D11InputLayout)
なので実際に作っておきます。
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 形式に変換しています。詳細は次回以降で。
頂点バッファ (頂点バッファオブジェクト)を使った描画命令も、頂点の設定同様
これまでと同じものでした。
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 シェーダー管理
頂点形式も同じものです。頂点バッファを作ることも出来ました。
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<unsigned char>( layout.GetStrideSize() ); edesc->Components= static_cast<unsigned char>( component ); edesc->ByteOffset= static_cast<unsigned char>( 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 シェーダー管理