ホーム < ゲームつくろー! < IKD備忘録

WebGL
モデル読まないと面白く無いよね


 前章ではWebGLで世界にポリゴンを1枚投下しました。前章のポリゴンは3D空間に配置していて、カメラも射影変換行列もあるので、実はもう3Dな角度でポリゴンを描画する下準備だけは揃っています。ただ、肝心のモデル側が貧弱…。かと言って、例えば立方体を出そうとしても結構な頂点数が必要になってしまいます。やっぱり、モデラー等で作成したモデルから頂点情報を抽出してWebGLに流し込むってのが良い落とし所ですよね。

 それを実際どうやるか?…んー、ここまで書いている段階ではまだ何にも知らないし考えてもいないんですが(^-^;、まぁとりあえず走りだしてみます。



@ サーバにあるテキストをロードする

 動的なHTMLはJavaScriptで記述するのが一般的です。なので、大量のデータをJavaScriptで読み込みたいと考えるわけです。JavaScriptはバイナリが得意ではないので、データはテキストで記述します。多分いくつか方法はあるんだろうと思いますが、Google先生に訪ねて見つけたXMLHttpRequestを使って外部テキストファイルを読み込んでみようと思います。

 まずは空っぽのHTML5を作ります:

最低限なHTML5
<html>
<head>
    <meta charset="UTF-8">
</head>
</html>

HTMLを開いたら、XMLHttpRequestでサーバに置いてあるテキストファイルを指定(GET)して、その結果(テキスト)を受け、画面に表示してみます。XMLHttpRequestは次のように使います:

model_01.txtを読み込んで表示
<html>
<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js"></script>
    <script>
        window.onload = function() {
            var http = new XMLHttpRequest();
            if ( http ) {
                http.open( "GET", "model_01.txt", false );
                http.onreadystatechange = function() {
                    if ( http.readyState == 4 )
                        $("#output").text( http.responseText );
                }
                http.send( null );
            }
        }
    </script>
</head>

<body>
    <textarea id="output" cols=30 rows=20>Text loading...</textarea>
</body>
</html>

 ちょっと楽をしたかったのでjQueryを使う事にしました。使わなくても別に構いません。window.onload(読み込みが終わったら呼ばれる関数)に関数を登録しています。関数の中ではXMLHttpRequestオブジェクトを作って、openメソッドの第2引数でファイル名を直接指定しています。onreadstatechangeメソッドはファイルの読み込みが終わったら呼ばれるので、その中でhttp.respnseText変数に格納されるファイルの中身(そのままごそっとやってきます)をテキストエリアに表示しています。

 試しにmodel_01.txtを適当に作って、HTMLと同じフォルダに置いて実験すると、ちゃんと文字列が読み込まれています:

mode_01.txt (UTF-8で保存する事!)
This is a test !
HogeHoge
日本語はどうかな?

わ〜い、これで少なくともモデルデータを外部ファイルに置いて読めると言う事はわかりました(^-^)。



A テキストパースならJSONだよねぇ

 JavaScriptには「JSON」というテキストなデータを扱う強力な機能が標準で備わっています。JSONは基本的には次のような書式を取ります:

{
    "keyname":value,
   "keyname2":value2
}

keynameは文字列で、続くvalueの名前(キー)になります。keynameとvalueは「:」で区切ります。valueは整数、少数、文字列、配列、そして上の「括弧の塊」を置く事ができます。括弧の塊の中には複数の値を格納できて、「,」で区切っていきます。valueに括弧の塊を置けるというのが魅力で、これによって階層構造を作れるんです。それぞれの例を挙げます:

{
    "intvalue":100,
    "floatvalue":0.123,
    "stringvalue":"テストだぁ",
    "arrayvalue":[30,40,50,60,70,80,90,100],
    "hierarchy": {
        "sub1":100,
        "sub2":200.345
    }
}

この構造、モデルを表すのにぴったりなんですよね!例えば頂点座標だったら、

{
    "coord":[
        -1.0, 0.0, 0.0,
        -1.0, 2.0, 0.0,
         1.0, 0.0, 0.0,

       -1.0, 2.0, 0.0,
         1.0, 2.0, 0.0,
         1.0, 0.0, 0.0,
    ]
}

こんな感じで"coord"というキーに対して座標値の配列を設定してあげればいいわけです。このテキストをJavaScript側に読み込んで、

var data = http.responsetext;

var hash = JSON.pars( data );

とJSON.parseメソッドに渡すと、それをJavaScriptのハッシュに変えてくれるんです!すばらし過ぎる(^-^)/
ハッシュなので、上の値には、

hash.coord;
hash["coord"];

上下どちらででもアクセスできて、結果は18個の数値の配列になるわけです。前章のポリゴンの元データは正に配列でしたよね。ですから、そこをごそっと置きかえられるというわけです。



B モデルデータを外部に置いてみる

 ではテストとして前章の板ポリ一枚の頂点データ(頂点座標、頂点カラー)をmodel_01.txtにJSON文字列として分離し、前章のコードをそれを読み込む形式に変更してみます。元のコードは前章のサンプルをご参照ください。

 まずは、ポリゴン頂点データの分離です。前章ではポリゴンデータは次のようにコードに直接書き込んでいました:

前章の頂点データ
// 頂点座標バッファ作成
var coords = [
    -1.0, 0.0, 0.0,
     0.0, 1.0, 0.0,
     1.0, 0.0, 0.0,
];
var coordBuf = glc.createBuffer();
glc.bindBuffer( glc.ARRAY_BUFFER, coordBuf ); // バッファをセットして
glc.bufferData( glc.ARRAY_BUFFER, new Float32Array(coords), glc.STATIC_DRAW ); // 配列を流し込んでいる

// 頂点カラーバッファ作成
var colors = [
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0
];
var colorBuf = glc.createBuffer();
glc.bindBuffer( glc.ARRAY_BUFFER, colorBuf );
glc.bufferData( glc.ARRAY_BUFFER, new Float32Array( colors ), glc.STATIC_DRAW );

coordsとcolorsがそれぞれ頂点座標と頂点カラーの情報です。これをmodel_01.txtにJSON文字列として分離してしまいます:

頂点データをJSON文字列に (model_01.txt)
{
    "coord":[
        -1.0, 0.0, 0.0,
         0.0, 1.0, 0.0,
         1.0, 0.0, 0.0
    ],
    "color":[
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
    ]
}

そして、先のコードの生データ部分をJSON.parseメソッド戻したデータに置き換えてしまいます:

model_01.txtにあるJSON文字列から頂点データを配列として戻す
// JSON文字列をハッシュに
var vertexHash = JSON.parse( jsonStr );  // jsonStrはmodel_01.txtそのものですよ

// 頂点座標バッファ作成
var coords = vertexHash["coord"];

var coordBuf = glc.createBuffer();
glc.bindBuffer( glc.ARRAY_BUFFER, coordBuf ); // バッファをセットして
glc.bufferData( glc.ARRAY_BUFFER, new Float32Array(coords), glc.STATIC_DRAW ); // 配列を流し込んでいる

// 頂点カラーバッファ作成
var colors = vertexHash["color"];

var colorBuf = glc.createBuffer();
glc.bindBuffer( glc.ARRAY_BUFFER, colorBuf );
glc.bufferData( glc.ARRAY_BUFFER, new Float32Array( colors ), glc.STATIC_DRAW );

コードから生データが消えました!これは大きな進歩です(^-^)。

 さて、こうなると他のデータ依存の部分も外部化したくなります。前章のコードでは最後の描画部分辺りが色々ありそうです:

前章のコードで頂点データに依存している部分
// 各バッファとシェーダ引数を繋ぐ
// 座標
glc.bindBuffer( glc.ARRAY_BUFFER, coordBuf );
glc.vertexAttribPointer( aPos, 3, glc.FLOAT, false, 0, 0 );
// カラー
glc.bindBuffer( glc.ARRAY_BUFFER, colorBuf );
glc.vertexAttribPointer( aColor, 4, glc.FLOAT, false, 0, 0 );
// 行列
glc.uniformMatrix4fv( uWVP, false, wvp.ary() );

// 使用するAttributeを指定
glc.enableVertexAttribArray( aPos );
glc.enableVertexAttribArray( aColor );

// 描画
glc.drawArrays( glc.TRIANGLES, 0, 3 );

glc.vertexAttribPointerメソッドの第2引数は、1頂点要素を表すベクトルの次元数です。頂点座標の場合は3次元なので「3」、頂点カラーはRGBAの4次元なので「4」という値が入っています。これは、まぁ殆ど固定と言えば固定なんですが、各頂点要素のデータ内に入れておけばつぶしがききますね。最下端のdrawArraysメソッドは正に描画を行う所ですが、この第1引数はポリゴン列のタイプを指定する箇所です。上の例ではglc.TRIANGLES、つまりDirectXで言う三角形リストを指定しています。もし三角形ストリップを指定したいならglc.TRIANGLE_STRIP、三角形ファンならglc.TRIANGLE_FANを指定します。実際これには「4」「5」「6」という数値が宛がわれているだけなのですが、さすがにそんな直値をデータに入れてもマジックナンバーすぎるので、例えば"triangle"という文字列があればそれをglc.TRIANGLE_FANに変換する簡単なテーブルを作って対処しておきます。drawArraysメソッドの第3引数は描画する頂点数です。

 この辺りのデータ依存な数値を含めた頂点データのJSON文字列は、多分こんな感じになります:

JSON頂点情報
{
    "vertex": {
        "coord":{
            "dim": 3,
            "data": [
                -1.0, 0.0, 0.0,
                0.0, 1.0, 0.0,
                1.0, 0.0, 0.0
            ]
        },

        "color":{
            "dim": 4,
            "data": [
                1.0, 0.0, 0.0, 1.0,
                0.0, 1.0, 0.0, 1.0,
                0.0, 0.0, 1.0, 1.0
            ]
        }
    },

    "primtype":"triangle",
    "vertexnum": 3
}

頂点座標coordのdimに次元数が入ります。頂点情報はdataという名前に統一しました。ポリゴンの型(プリミティブタイプ)はprimtypeに、描画する頂点数はvertexnumとしています。後は先の依存部分をこのハッシュのキー名で置きかえるだけです。一応これだけでも前のコードの描画部分からデータ依存な数値はほぼ無くなります。という事は、後はこのJSON頂点情報を差し替える事で、他のモデルも描画出来る事になるわけです!



B Xファイル → JSON頂点情報に

 さて、Aで最低限の頂点情報をJSON文字列で表現できたわけですが、さすがにこれを手打ちするのは嫌です。なので、既存モデルフォーマットから上の情報のみを抜き出してみたいわけです。手元にあって簡単なのはやはりXファイル。なので、XファイルからJSON頂点情報を抜き取ってみましょう。…とはいえ、Xファイルのパーサーを書くのももちろん嫌です。そこで、D3DXLoadMeshFromX関数で一度メッシュを作り、そこから頂点座標情報を上のように成形してファイルとして出力するコンバータを作ってしまいます。

 まず描画デバイス(IDirect3DDevice9)を作るのですが、それにはウィンドウハンドルが必要です。これ、面倒なのでデスクトップのウィンドウハンドルをGetDesktopWindow関数で取得して借用しちゃいましょう。D3DXLoadMeshFromX関数からID3DXMeshを得たら、ID3DXMesh::GetVertexBuffer関数で頂点バッファを得ます。次にID3DXMesh::GetDeclaration関数で頂点要素の配列を取得して、頂点座標が頂点バッファの何バイト先に並んでいるかを調べます。大抵は先頭0バイト目にあります。頂点の並びはGetIndexBufferで得られるインデックスバッファに記載されていますので、後はそのインデックス順に頂点座標を並べれば"coord"部分は完了です。

 この手のデータ構造を作る上で、あるととても便利な物があります。それはVariant型です。

 Variant型は整数、小数、文字列など複数の型を保持できるマルチ変数です。さらにそれらの配列保持もサポートすると素敵です。さらにさらに、Variant型自身も保持できるようにすると、そこに階層構造が生まれるのがわかると思います。もう一つ、保持した値に名前アクセスできるようにすると「ハッシュテーブル」になります。ハッシュテーブルがあるとこんな事ができるようになります:

ハッシュテーブル
Hash hash;

hash["val1"] = 100;
hash["val2"] = 234.567f;
hash["name"] = "IKD";

std::vector<int> ary;
ary.push_back(300);
ary.push_back(400);
hash["ary"] = ary;

Hash sub;
std::vector<std::string> strAry;
strAry.push_back( "hoge" );
strAry.push_back( "foo" );
sub["names"] = strAry;

hash["sub"] = sub;

整数も小数も文字列も配列もハッシュテーブル自体も格納できるわけです。もちろんC++にはハッシュはありませんのでこういうクラスを作るわけですが(^-^;、この実装について話すと脱線も甚だしいため今は割愛。こういうのがあると思いねぇ。Json文字列はハッシュテーブルがあれば器械的に変換していけます。ここは特に難しい事は無くて、愚直に変換していくのみです。

 Xファイルは階層的に複雑な情報を保持できますが、今はテストなので頂点座標のみ扱うとしましょう。

 この手のパーサーは、便利なヘルパー関数を作っていくと処理を整理できます。頂点の座標にかぎらず頂点の要素はある一つのブロックに整然と並んでいます。あるメモリブロック内にあるi番目の頂点の特定のオフセット位置にある情報を取得する関数はこんな感じになります:

メモリブロックアクセス
void* getValue( void *p, int index, DWORD stride, int offset ) {
    return (void*)((char*)p + stride * i + offset);
}

strideは1頂点を格納しているブロックサイズです。offsetはそのブロックの先頭からのオフセットバイト値になります。

 メッシュ(ID3DXMesh)がすでにあるとして、strideサイズはID3DXMesh::GetNumBytesPerVertexメソッドで取得できます。要素の位置であるoffsetについては自前で取る必要があります。そこで特定の要素のオフセット位置を返す関数を作ります:

要素のオフセット位置を返す
int getOffsetFromElem( D3DVERTEXELEMENT9 *elems, D3DDECLUSAGE usage ) {
    D3DVERTEXELEMENT9 *e = elems;
    while( e->Stream != 0xff ) {
        if ( e->Usage == usage )
            return e->Offset;
        e = e + 1;
    }
    return -1;
}

あとは頂点バッファのデータポインタと取得するインデックスを得るだけです。指定の要素を取りだす関数はこうなります:

要素を返す
// 要素を配列に格納
template< class T, class INDEXBUFFERTYPE>
void getValues( std::vector<T> &ary, void *pVb, void *pIb, DWORD indexNum, int stride, int offset, int dim ) {
    for ( DWORD i = 0; i < indexNum; i++ ) {
    T* v = (T*)getValue( pVb, ((INDEXBUFFERTYPE*)pIb)[i], stride, offset );
    for ( int j = 0; j < dim; j++ )
        ary.push_back( v[j] );
    }
}

// 要素のハッシュを取得
template< class T, int dim >
Hash getElementHash( ID3DXMesh *mesh , D3DDECLUSAGE usage ) {
    // 頂点宣言から取得したい要素のオフセットとストライドサイズを取得
    D3DVERTEXELEMENT9 e[MAX_FVF_DECL_SIZE];
    mesh->GetDeclaration( e );
    int offset = getOffsetFromElem( e, usage );
    DWORD stride = mesh->GetNumBytesPerVertex();

   IDirect3DVertexBuffer9 *vb;
    IDirect3DIndexBuffer9 *ib;

    mesh->GetVertexBuffer( &vb );
    mesh->GetIndexBuffer( &ib );

    void *pVb = 0;
    void *pIb = 0;
    if ( FAILED( vb->Lock(0, 0, &pVb, 0) ) )
        return Hash();
    if ( FAILED( ib->Lock(0, 0, &pIb, 0) ) ) {
        vb->Unlock();
        return Hash();
    }

    D3DINDEXBUFFER_DESC desc;
    ib->GetDesc( &desc );

    std::vector<T> ary;
    DWORD indexNum = mesh->GetNumFaces() * 3;

    if ( desc.Format == D3DFMT_INDEX16 )
        getValues<T, WORD>( ary, pVb, pIb, indexNum, stride, offset, dim );
    else
        getValues<T, DWORD>( ary, pVb, pIb, indexNum, stride, offset, dim );

    vb->Unlock();
    ib->Unlock();

    Hash outHash;
    outHash["data"] = ary;
    outHash["dim"] = dim;

    return outHash;
}

 インデックスバッファには数値をWORDとDWORDで格納する2タイプがあります。そのためgetValues関数を設ける必要がありました。面倒くさい…(-_-;。またインデックスの数はポリゴンの面の数×3としています。

 Xファイルをロードして、そこから頂点座標と頂点カラーを取りだしてハッシュに格納するのはこんな感じになります:

Xファイルから頂点座標と頂点カラーを取りだす
Hash createMeshHashFromX( IDirect3DDevice9 *dev, const char *fileName ) {
    ID3DXMesh *mesh = 0;
    ID3DXBuffer *materialsBuf = 0;
    DWORD numMaterials = 0;
    if ( FAILED( D3DXLoadMeshFromXA( fileName, D3DXMESH_MANAGED, dev, 0, &materialsBuf, 0, &numMaterials, &mesh ) ) )
        return Hash();

    Hash vertexHash;
    vertexHash["coord"] = getElementHash<float, 3>( mesh, D3DDECLUSAGE_POSITION );
    vertexHash["color"] = getElementHash<float, 4>( mesh, D3DDECLUSAGE_COLOR );

    Hash meshHash;
    meshHash["vertex"] = vertexHash;
    meshHash["primitive"] = "triangle";
    meshHash["vertexnum"] = (int)mesh->GetNumFaces() * 3;

    materialsBuf->Release();
    mesh->Release();

    return meshHash;
}

肝心のJSON化の所ですが、これもハッシュの中身を単純に変換していくだけなのでここでは割愛です。サンプルプログラムにクラスがありますので、ご自由にお使い下さい(^-^)。実際にサンプルプログラムから出力したJSON文字列はこんな感じになります(頂点座標情報は一部省略):

出力例(Cube.x)
{
    "primitive":"triangle",
    "vertex":{
        "color":{
        },
        "coord":{
            "data":[5.800000,5.000000,5.000000,...(略)...,-5.000000,5.800000,5.000000],
            "dim":3
        }
    },
    "vertexnum":144
}

まぁ、一応フォーマット通りに出ていますので、これを読み込んで表示するフェーズに入れるわけです(^-^)。



C JSON化モデルデータを読み込んでみよう〜

 という事で、Xファイルから頂点の情報を取りだすコンバータ(のひな型)ができた所で、さっそくそのJSON化モデルを読み込んでみよう〜〜〜:

は?(-_-; 怒

 えーと、サイズが大きすぎると言われています。…あー、頂点カラーが無いんだ(^-^;。えーと、頂点要素の有る無しを考えるには今はちょっと冗長なので、今回は頂点カラーは使わず頂点座標だけでモデルを描画する事にしましょう。前回のコードから頂点カラーに関連する所を抜くだけです。出力結果はこちら〜:

おーし、大きな一歩です(^-^)/

 モデルデータを読めるようになれば、WebGLでゲームを作るという道が大きく開ける事になります。これ、WebGLに限らずDirectX10でもそうなんですけどね。今はXファイルだけですが、FBXとかOBJとか別のモデル形式にもコンバータを対応させれば、夢はどんどん広がります。楽しくなってきました〜(^-^)