サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

3D シーンの舞台裏 - パート1

前回のチュートリアルでは、 LibGDXを使って3D のシーンを読み込む方法について見てきました。今回は、シーンの舞台裏では実際には何が起こっているのかを簡単に見てみましょう。 実際に3D APIを使用するためにこの情報を知っておく必要はありません。 どのように動作するかあまり気にならないのであれば、この内容は無視しても問題ありません。 しかし、どのように動作するのか、どのようにすればそこから恩恵を得られるのかを知ることは良い勉強になると私は思うのです。 したがってこのチュートリアルでは、3D APIを使用する時に知っておくべき非常に基本的な内容について説明しています。

このチュートリアルは二部構成になっています。 第一部では g3db/g3dj ファイル形式、Model クラス、これらがモデリングソフトでのシーン作成にどのような影響を及ぼすのか、について説明します。 第二部では、Modelの読み込みからShaderを使って実際に描写するまでの、レンダリングパイプラインについて説明します。

まずは fbx-convから始めましょう。 最新版のfbx-convをダウンロードして、前回作成したinvaderscene.fbxファイルを変換してみましょう。 ですが、既定のバイナリ形式を使うのではなく、テキストjson 形式を読みやすくするためのコマンドラインオプションを追加します:


fbx-conv -o G3DJ invaderscene.fbx

上記コードではinvaderscene.g3djという名前のファイルが作成されます。 お気に入りのプレーンテキストエディタ(メモ帳でも大丈夫です)を使ってこのファイルを開きます。 これら大量の数値に惑わされないで、全体の構造を見てください。:


{
    "version": [  0,   1], 
    "id": "", 
    "meshes": [
    ...
    ], 
    "materials": [
    ...
    ], 
    "nodes": [
    ...
    ], 
    "animations": []
}

ここではあなたがjson ファイル形式について既に理解しているものとして説明を行います。 このファイルは六つのメンバを持つオブジェクトを保持していることが分かります。 一つ目のメンバは versionで、これを何に使うかはモデルローダーの範疇になります。 二つ目のメンバはid (モデリングソフトで設定できる名前)で、今のところこれは使いません。 そして四つの配列 meshesmaterialsnodesanimationsとなります。 前回のチュートリアルで使用したLibGDX Model クラスを簡単に見てみると、クラスの上部でこれら四つの配列が定義されているのが分かるでしょう。:


public class Model implements Disposable {
    public final Array<Mesh> meshes = new Array<Mesh>();
    public final Array<MeshPart> meshParts = new Array<MeshPart>();
    public final Array<Material> materials = new Array<Material>();
    public final Array<Node> nodes = new Array<Node>();
    public final Array<Animation> animations = new Array<Animation>();
    ...
}

Model クラスも meshes、 materials、 nodes 、 animations の配列を持っています。 またmeshParts という配列もあり、これについては後で説明します。 しかし今のところは、g3dj (および g3db)ファイルはそのモデルが実際に内包しているものを一つ一つ表したものと言うことができます。 実際のところこれがfbx-convの主目的で、fbxファイルを描写可能な実行形式に変換します。 またこれは、g3djの中身を調べることで、そのg3djファイルを使って作成されるModelクラスが何を表しているのかを調べるということでもあります。 このことはデバッグやテストで非常に役立ちます。 g3djファイル内のmeshes配列を見てください。 改めて言いますが、これら大量の数字には惑わされないで、全体の構造のみを見てください:

{
    "version": [  0,   1], 
    "id": "", 
    "meshes": [
        {
            "attributes": ["POSITION", "NORMAL", "TEXCOORD0"], 
            "vertices": [
                 25.000017, -95.105652, -18.163574, -0.269870,  0.942723,  0.196072,  0.050000,  0.900000, 
                 ...
            ], 
            "parts": [
                {
                    "id": "mpart1", 
                    "type": "TRIANGLES", 
                    "indices": [
                          0,   1,   2,   1,   0,   3,   3,   4,   5,   4,   3,   0, 
                         ...
                    ]
                }, 
                {
                    "id": "mpart2", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }, 
                {
                    "id": "mpart4", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }
            ]
        }, 
        {
            "attributes": ["POSITION", "NORMAL"], 
            "vertices": [
                ...
            ], 
            "parts": [
                {
                    "id": "mpart3", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }
            ]
        }
    ], 
    "materials": [
        ...
    ], 
    "nodes": [
        ...
    ], 
    "animations": []
}

上記では、meshes配列には二つの項目(二つのmeshes)が含まれています。 各項目には、三つの配列: attributesverticespartsが含まれています。 attributes配列はそのメッシュに含まれる頂点属性を指定します。 LibGDX を使った基本的な3Dのチュートリアルを読んだのであれば、ModelBuilderを使って箱を作成した時にVertexAttributesの Usage.Position と Usage.Normalを既に見たことがあるでしょう。 "TEXCOORD0" 属性では、メッシュがテクスチャ座標を含めるよう指定します。

vertices 配列は単に、メッシュを表す浮動小数点値の入った巨大な配列です。 一つの行内で各頂点がどのように記述されているかに注目してください。 各行において最初の三つの値は位置を表し、次の三つの値は法線を表し、最後の二つの値はテクスチャ座標(UV)を表します。

一つ目のメッシュのparts配列には、三つのオブジェクトが含まれています。 これはらメッシュ・パーツとして知られているものです。 上記のとおり、Modelクラスは個別のmeshParts配列を持っています。 この配列は、全てのメッシュ内の全てのメッシュ・パーツを保持しています。 それでは、一つ目のメッシュ・パーツを見てください。メッシュ・パーツには三つのメンバが含まれています。 一つ目のメンバはidです。これは重複のない識別子で、内部でこのメッシュ・パーツを識別するのに使用されます。 二つ目のメンバはtypeです。これはメッシュ・パーツをどのように描写するかを定義します(プリミティブ型)。 理論的にはtypeに他の値を設定することができますが、実際は常に "TRIANGLES"を設定することになるでしょう。 "TRIANGLES"では、メッシュ・パーツは三つの頂点で設定された三角形で構成されます。 最後のメンバはindices配列です。 改めて言いますが、これはvertices配列内の頂点を識別するために使用される数字が入った巨大な配列です。 そのため、例えば0の値はvertices配列内の最初の行を意味し、1の値は二行目を意味します。 typeに"TRIANGLES"が設定されているので、 最初の三つの値(0, 1, 2) で最初の三角形を設定し、その次の三つの値(1, 0, 3)で二つ目の三角形を設定する、といった流れになります。 各行が12個の値で構成されていると、一行につき四つの三角形ができることになるので覚えておいてください。

LibGDX のMesh クラスをざっと見てみると、以下の行が含まれていることが分かるでしょう:

public class Mesh implements Disposable {
    ...
    final VertexData vertices;
    final IndexData indices;
    ...

上記コードでは、VertexDataを浮動小数点値の巨大な配列としてみることができます。これはg3djファイル内にあるメッシュのvertices配列と同じようなものです。 IndexDataは巨大な配列として見ることもできますが、今のところは短い値が入っています。 個の配列はg3djファイル内にあるmeshの各メッシュパーツのindicesに該当し、メッシュパーツを平坦化します。 したがってmeshes内にある1つ目のメッシュの場合は、一つ目のメッシュパーツ(mpart1)のindices を含み、その次に二つの目のメッシュパーツ(mpart2) のindices を含み、 その次に三つ目のメッシュパーツ(mpart4)のindices を含んでいます。 メッシュ内のメッシュパーツを識別するには、それがIndexData内のどこに置かれているかを知っておく必要があります。 今度はMeshPart クラスを見てみましょう:


public class MeshPart {
    /** unique id within model **/
    public String id;
    /** the primitive type, OpenGL constant like GL_TRIANGLES **/
    public int primitiveType;
    /** the offset into a Mesh's indices array **/
    public int offset;
    /** the number of vertices that make up this part **/
    public int size;
    /** the Mesh the part references, also stored in {@link Model} **/
    public Mesh mesh;
}

まさにこれが我々の必要としているものです。 indexOffset と numVertices の値によって、このメッシュパーツではIndexData のどの部分が使用されるのかを知ることができます。 そのため、 LibGDX 3d api ではメッシュの描写は行わず、メッシュ・パーツの描写を行います。 複数の異なるModelInstances で同じメッシュを共有することができるので、これは役立ちます。 これにより、メッシュを紐付ける必要のある回数が実際に減り、パフォーマンスが向上します。 In fact, notice how we started with four models (ship, block, invader and spacesphere) and now instead only have two meshes, but with a total of four mesh parts. 私達に代わって、Fbx-convが同じ属性を共有するメッシュを結合しました。 blockのモデルはテクスチャを持たないので、二つ目のメッシュにはTEXCOORD0は含まれません。 block モデルにテクスチャ座標を追加してテクスチャを適用しないだけで、これを最適化できます。 これによりメッシュ紐付けの回数を一回だけに減らします。 この作業についてはモデリングソフト固有の話になるので、ここでは説明しません。 ですが、同じvertex 属性を持つことでメッシュを結合する助けになるということを覚えておいてください。 シーンをG3DJ に変更してこれらの種類の最適化を素早く確認することがいつでもできます。

続けて、materials 配列の追加について見ていきましょう:

    "materials": [
        {
            "id": "sphere2_auv1", 
            "diffuse": [ 1.000000,  1.000000,  1.000000], 
            "textures": [
                {
                    "id": "file3", 
                    "filename": "invader.png", 
                    "type": "DIFFUSE"
                }
            ]
        }, 
        {
            "id": "lambert2", 
            "diffuse": [ 1.000000,  1.000000,  1.000000], 
            "textures": [
                {
                    "id": "file1", 
                    "filename": "space.jpg", 
                    "type": "DIFFUSE"
                }
            ]
        }, 
        {
            "id": "cube1_auv1", 
            "diffuse": [ 1.000000,  1.000000,  1.000000], 
            "textures": [
                {
                    "id": "file2", 
                    "filename": "ship.png", 
                    "type": "DIFFUSE"
                }
            ]
        }, 
        {
            "id": "block_default1", 
            "diffuse": [ 0.000000,  0.000000,  1.000000]
        }
    ],

上記コードでは、ファイルには4個のマテリアルが含まれているのが分かります。 全てのマテリアルは一意のIDを持っており、これはモデリングソフト上でのマテリアル名と同じものになります。 ご覧の通り、ID名にあまり意味がありません。 これはobj ファイルからインポートをしたためです。 しかし、マテリアルには意味のあるID名を付けることをお勧めします。 そうすることで、Model クラスのmaterials 配列内にあるマテリアルを識別することができるようになります。 次に、マテリアルにはdiffuse値が含まれています。これは、0から1までの範囲の値を持つ赤と緑と青の配列を使ってマテリアルのディフューズ色を表したものです。 そのため、[0.5, 0.5, 0.5]の値は灰色になり、[1.0, 0.0, 0.0]の値は赤になります。 最後にあるマテリアル(block モデルのマテリアル)だけはディフューズ色が青になっているので気をつけてください。 最後に、最初の三つのmaterials はtextures配列を持っており、これで適用するテクスチャを定義します。 もう一度説明しますが、テクスチャのIDはモデリングソフト上でのテクスチャ名と同じになります。意味のあるID名を付けることをお勧めします。 filename 値は一目瞭然で、テクスチャのファイル名です。 そしてtype 値はテクスチャをどのように適用するかを設定します。今回は "DIFFUSE" が設定されていますが、例えば "NORMALMAP"のような他の値も設定できます。 今のところマテリアルについて深くは掘り下げませんが、id以外の値を設定するのは任意ということに注意してください。 設定されなかった場合は単に適用されないだけです。

今度は nodes 配列について見てみましょう:

    "nodes": [
        {
            "id": "space", 
            "parts": [
                {
                    "meshpartid": "mpart1", 
                    "materialid": "lambert2", 
                    "uvMapping": [[  0]]
                }
            ]
        }, 
        {
            "id": "ship", 
            "rotation": [ 0.000000,  1.000000,  0.000000,  0.000000], 
            "translation": [ 0.000000,  0.000000,  6.000000], 
            "parts": [
                {
                    "meshpartid": "mpart2", 
                    "materialid": "cube1_auv1", 
                    "uvMapping": [[  0]]
                }
            ]
        }, 
        {
            "id": "block1", 
            "translation": [-5.000000,  0.000000,  3.000000], 
            "parts": [
                {
                    "meshpartid": "mpart3", 
                    "materialid": "block_default1"
                }
            ]
        }, 
        ...
        {
            "id": "block6", 
            "translation": [ 5.000000,  0.000000,  3.000000], 
            "parts": [
                {
                    "meshpartid": "mpart3", 
                    "materialid": "block_default1"
                }
            ]
        }, 
        {
            "id": "invader1", 
            "translation": [-5.000000,  0.000000,  0.000000], 
            "parts": [
                {
                    "meshpartid": "mpart4", 
                    "materialid": "sphere2_auv1", 
                    "uvMapping": [[  0]]
                }
            ]
        }, 
        ...
        {
            "id": "invader30", 
            "translation": [ 5.000000,  0.000000, -8.000000], 
            "parts": [
                {
                    "meshpartid": "mpart4", 
                    "materialid": "sphere2_auv1", 
                    "uvMapping": [[  0]]
                }
            ]
        }
    ], 

これは見覚えがあるでしょう。モデリングソフト上で作成した全てのモデルが含まれています。 各node はIDを持っています。このIDはモデリングソフトで使用した名前と一致し、 前回のチュートリアルでは各ModelInstanceを作成するのに使用しました。いくつかのノードは translation 値と rotation 値も持っています。 また、これらルールを適用する値が設定されていない場合は、単にその処理が適用されないだけです。 そのため例えば"space"ノードでは移動と回転は一切行われず、 "ship"ノードでは回転と移動の両方が行われます。 モデリングの時と同じです。 次の値は parts 配列で、ここで全てのものが1つになります。 これはノードがどのように描写されるかを設定します。 ノードパーツと呼ばれる各項目には、メッシュパーツとマテリアル両方のIDガ含まれています。つまり、これで指定されたメッシュパーツはこれで指定されたマテリアルで描写されます。 uvMapping 値は、どのテクスチャにどのテクスチャ座標を使用するかを指定するのに使われます。 頂点属性"TEXCOORD0"とテクスチャ"DIFFUSE"があったことを思い出してください。 今回は頂点属性の "TEXCOORD0" と"TEXCOORD1"があり、そしてテクスチャの "DIFFUSE" と"NORMALMAP"があるものと仮定してください。 このuvMapping 配列では、TEXCOORD0 にどのテクスチャを使用するか(例えばDIFFUSE)、TEXCOORD1 にどのテクスチャを使用するか(例えば NORMALMAP)を設定します。

上記コードでは、 parts 配列は1つのノードパーツしか含まれていません。 しかし例えば、窓を除いた全体にはテクスチャが適用して窓は黒色で描写する、という車のmodelを考えてみてください。 この場合、窓のメッシュパーツと黒マテリアルを含んだパーツ、車の残りの部分のメッシュパーツとテクスチャの付いたマテリアルを含んだパーツ、の二つのパーツがあります。 このことを常に覚えておいてください。 モデリングソフト上で複数の異なるマテリアルをモデルに適用した場合、それは複数のノードパーツに分割されます。 この車の例の場合、小さな黒い矩形をテクスチャに追加し、その矩形の中央のみをカバーするように窓のテクスチャ座標を設定することで、最適化を行えます。

簡単な追記。古いLibGDXの3D Modelクラスを使用している場合、二つを比較することができます。 古いStillModel クラスは SubMesh クラスの配列を持っています。SubMeshには Material、 Mesh 、 プリミティブ型が含まれています。 NodePartにはほぼ同じ情報が含まれていますが、Meshを直接参照するのではなくMeshPart (プリミティブ型を含んでいる)を参照している点が異なります。

改めて言いますが、g3dj ファイル内のノードはModel クラス内のノードと1対1で一致します。 したがって、Model 内のノードはモデリングソフト上でのノードとも一致します。 ほとんどのモデリングソフトではこれらノードは階層構造になるので、Model クラスも階層構造になります。 全てのノードは、自分の子ノードを保持している子配列を持っています。 今のところはそれ以上深くは説明はしませんが、モデリングソフトで階層構造を使用する場合はそれがModelクラスにも反映されるということを覚えておいてください。

あと、g3dj ファイル内の最後にあるanimationsという名前の配列が残っています。 これは一目瞭然で、アニメーションモデルのために使用されるものです。 アニメーションについては後ほど説明しますが、今のところはそういう配列があるということだけ覚えておいてください。

これで、特に問題がなければ invaderscene.g3djファイルをassets フォルダー内のデータフォルダにコピーして、invadersceneファイルの代わりにこれを読み込むことができます。 そうすれば、いくつかの値を変更してそれがシーンにどのような影響を与えるかを簡単に確認できます。 例えば、船のマテリアルのディフューズ色を変更したり、船に別のマテリアルを適用したりしてみてください。

次: 3D シーンの舞台裏 - パート2