サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

libGDXで3Dシーンを読み込む

前回のチュートリアルでは、LibGDXを使ってmodelを変換、読み込み、表示する方法について見てきました。 ここでは、完全な3Dシーンを読み込む方法について見ていきます。

このチュートリアルの全 ソース素材 と実行可能なテストはこのgithub リポジトリにあります。

分かりやすくするためにクラス名をSceneTest に変えていますが、前回と同じコードをベースとして使用しています。 前回のクラスでは、ModelInstance インスタンスの配列を用意していました。これを使ってシーンを定義します。 船のモデルを読み込む方法については既に見てきたので、いくつかモデルを追加しましょう。 使用しているモデルは ここからダウンロードできます。 これには4つのモデル (obj ファイル)が含まれています: 前回使用した船のモデル、 gdx-invadersの invader.obj モデルと block.obj モデル、宇宙空間のモデルを簡単にまとめました。 宇宙空間は単にテクスチャの張られた巨大な球体の法線を反転させたものです(そのためテスクチャは内側から見ることができます)。

前回はfbx-convを使ってモデルを変換しました。 上記モデルも同じように変換しますが、今のところはobj ファイルのまま使用しましょう。 なので、上記ファイルをassets フォルダー内のデータフォルダーにコピーして前回と同じ様に読み込んでください。 参考までに、完全なコードを以下に記載します。コードの後で変更内容について説明します。:


public class LoadSceneTest implements ApplicationListener {
    public PerspectiveCamera cam;
    public CameraInputController camController;
    public ModelBatch modelBatch;
    public AssetManager assets;
    public Array<ModelInstance> instances = new Array<ModelInstance>();
    public Environment environment;
    public boolean loading;

    public Array<ModelInstance> blocks = new Array<ModelInstance>();
    public Array<ModelInstance> invaders = new Array<ModelInstance>();
    public ModelInstance ship;
    public ModelInstance space;

    @Override
    public void create () {
        modelBatch = new ModelBatch();
        environment = new Environment();
        environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
        environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));

        cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        cam.position.set(0f, 7f, 10f);
        cam.lookAt(0,0,0);
        cam.near = 1f;
        cam.far = 300f;
        cam.update();

        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);

        assets = new AssetManager();
        assets.load("data/ship.obj", Model.class);
        assets.load("data/block.obj", Model.class);
        assets.load("data/invader.obj", Model.class);
        assets.load("data/spacesphere.obj", Model.class);
        loading = true;
    }

    private void doneLoading() {
        ship = new ModelInstance(assets.get("data/ship.obj", Model.class));
        ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
        instances.add(ship);

        Model blockModel = assets.get("data/block.obj", Model.class);
        for (float x = -5f; x <= 5f; x += 2f) {
            ModelInstance block = new ModelInstance(blockModel);
            block.transform.setToTranslation(x, 0, 3f);
            instances.add(block);
            blocks.add(block);
        }

        Model invaderModel = assets.get("data/invader.obj", Model.class);
        for (float x = -5f; x <= 5f; x += 2f) {
            for (float z = -8f; z <= 0f; z += 2f) {
                ModelInstance invader = new ModelInstance(invaderModel);
                invader.transform.setToTranslation(x, 0, z);
                instances.add(invader);
                invaders.add(invader);
            }
        }

        space = new ModelInstance(assets.get("data/spacesphere.obj", Model.class));

        loading = false;
    }

    @Override
    public void render () {
        if (loading && assets.update())
            doneLoading();
        camController.update();

        Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

        modelBatch.begin(cam);
        modelBatch.render(instances, environment);
        if (space != null)
            modelBatch.render(space);
        modelBatch.end();
    }

    @Override
    public void dispose () {
        modelBatch.dispose();
        instances.clear();
        assets.dispose();
    }

    @Override
    public void resume () {
    }

    @Override
    public void resize (int width, int height) {
    }

    @Override
    public void pause () {
    }
}

githubにある全ソースコードを見てください

それでは、変更内容についての説明を始めましょう:


    public Array<ModelInstance> blocks = new Array<ModelInstance>();
    public Array<ModelInstance> invaders = new Array<ModelInstance>();
    public ModelInstance ship;
    public ModelInstance space;

上記コードでは配列を追加してblocks とinvadersを保持しています。 そしてship とspaceは1個ずつ。 今回もこのインスタンスの配列は描写に使用しますが、これを使うことでシーンの各部分へ簡単にアクセスできるようにもなります。 ですから、船を動かしたい場合はship インスタンスを使用するだけです。


    public void create () {
        modelBatch = new ModelBatch();
        ...
        cam.position.set(0f, 7f, 10f);
        ...
        assets.load("data/ship.obj", Model.class);
        assets.load("data/block.obj", Model.class);
        assets.load("data/invader.obj", Model.class);
        assets.load("data/spacesphere.obj", Model.class);
        loading = true;
    }

読み込むシーンにより適した位置にカメラを設定しています。 そして次に、全てのモデルを読み込むようassetmanager へ指示を出します。


    private void doneLoading() {
        ship = new ModelInstance(assets.get("data/ship.obj", Model.class));
        ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
        instances.add(ship);

        Model blockModel = assets.get("data/block.obj", Model.class);
        for (float x = -5f; x <= 5f; x += 2f) {
            ModelInstance block = new ModelInstance(blockModel);
            block.transform.setToTranslation(x, 0, 3f);
            instances.add(block);
            blocks.add(block);
        }

        Model invaderModel = assets.get("data/invader.obj", Model.class);
        for (float x = -5f; x <= 5f; x += 2f) {
            for (float z = -8f; z <= 0f; z += 2f) {
                ModelInstance invader = new ModelInstance(invaderModel);
                invader.transform.setToTranslation(x, 0, z);
                instances.add(invader);
                invaders.add(invader);
            }
        }

        space = new ModelInstance(assets.get("data/spacesphere.obj", Model.class));

        loading = false;
    }

ここからが面白くなって来ます。 最初の行では船のモデルを取得して、それからModelInstance のship を作成しています。 次の行ではship を180度回転させてカメラと反対の方向に向かせ、Z軸上でカメラへ向かって6ユニット移動させています。 最後の三行目ではship をインスタンスの配列に追加して、これで実際に描写されるようになります。

次に、block とinvaderのモデルでも同じ処理を行います。 しかし、ここでは一つのインスタンスだけを作成するのではなく複数のインスタンスを作成します。 block インスタンスはX軸上に並べて配置し、instances 配列とblocks 配列の両方に追加します。 改めて言いますが、これは簡単にアクセスするためだけのもので、例えばship がblockと衝突しているかを確認できるようになります。 invader インスタンスはXZ平面上に格子状に配置します。

最後にModelInstanceのインスタンスspace を作成します。 これはinstances 配列には追加しないので注意してください。 このモデルには照明を適用したくないからです。:


    public void render () {
        ...
        modelBatch.begin(cam);
        modelBatch.render(instances, environment);
        if (space != null)
            modelBatch.render(space);
        modelBatch.end();
    }

render メソッド内では、前回と同じ様に instances を描写しています。 しかし今回はspace インスタンスは別個に描写して照明を適用させません。 space は非同期で読み込みがされるので、描写を行う前にspace が設定済みかどうかを確認する必要があります。

それでは、上記のコードがどのように描写されるか見てみましょう:

scenetest1

なんと、とても素晴らしい出来栄えです。 いくつかのゲームプレイ要素を実装したので、これで作業完了とすることもできます。 実際、世にはこのようにして作られたゲームがたくさんあります。 しかし、これはより大きなシーンの場合は上手く動作しないので、最適化をしてみましょう。

まず最初に、モデルを四つ読み込みます。より大きなシーンでは、もっと多くのモデルを読み込むことが良くあります。それでは最適化します。

お気に入りのモデリングソフトを起動して、空っぽ状態のシーンから新規に作成を始めます。 私はMayaを使用していますが、この例は他のモデリングソフトの場合でも問題ありません。 それでは、テストで使った四つのモデル全て (shipと blockと invader とspacesphere) をシーンにインポートしてください。 あなたが私と同じく素人のモデラーであれば、モデルは1つずつインポートして各モデルが正しく表示されることを確認してください。 例えば、私の場合は手動でテクスチャを割り当てたりテクスチャ座標を反転させたりする必要がありました。 また、各モデルには簡単な名前を付け、変形処理は一切適用しないようにしてください。 そうすると以下のようになります:

scenetest2

編集を簡単にできるようにするためX線を有効にしており、そのためモデルが透明になっています。 バックグラウンドでは、"space"という変数名で作成した宇宙空間が表示されます。 前面には "ship"と "block" と "invader"のモデルが混在して表示されます。 それらモデルの位置が全て(0,0,0)に設定されているためです。 全てのモデルが正しく表示されて正しい名前を付けられている場合は、そのシーンをFBXへ出力できます。 私の場合FBXの名前は"invaders.fbx"としました。 モデリングソフト独自のファイル形式でシーンを保存したいかもしれません。 後ほど必要になります。

今度は FBX ファイルを G3DBへ変換します。 最新版の fbx-conv を取得して FBX ファイルを変換するようにしてください:


fbx-conv invaders.fbx

FBX ファイル作成の際にテクスチャ座標を反転させる必要があった人の場合は、おそらくここでもテクスチャ座標の反転をする必要があるでしょう。 この場合、コマンドラインオプションを追加する必要があります:


fbx-conv -f invaders.fbx

全てのコマンドラインオプションを表示したい場合、引数を付けずに fbx-conv を実行します。

次は作成されたinvaders.g3dbファイルをassetsフォルダ内のデータフィルダへコピーして、そのファイルを読み込んでみましょう:


public class LoadSceneTest extends GdxTest implements ApplicationListener {
    ...
    @Override
    public void create () {
        ...
        assets = new AssetManager();
        assets.load("data/invaders.g3db", Model.class);
        loading = true;
    }

    private void doneLoading() {
        Model model = assets.get("data/invaders.g3db", Model.class);
        ship = new ModelInstance(model, "ship");
        ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
        instances.add(ship);

        for (float x = -5f; x <= 5f; x += 2f) {
            ModelInstance block = new ModelInstance(model, "block");
            block.transform.setToTranslation(x, 0, 3f);
            instances.add(block);
            blocks.add(block);
        }

        for (float x = -5f; x <= 5f; x += 2f) {
            for (float z = -8f; z <= 0f; z += 2f) {
                ModelInstance invader = new ModelInstance(model, "invader");
                invader.transform.setToTranslation(x, 0, z);
                instances.add(invader);
                invaders.add(invader);
            }
        }

        space = new ModelInstance(model, "space");

        loading = false;
    }
...
}

githubにある全ソースコードを見てください

create() メソッド内では、各モデルの個別読み込み処理を削除して 一つのモデル invaders.g3dbに置き換えています。 doneLoading() メソッド内では、assetmanagerからモデルを取得します。 そしてModelInstances を作成する時は、モデルとFBX作成時に使用した名前を引数として渡します。 そのため、例えばship ModelInstanceの場合は、"ship"という名前のmodelのみを表すようにModelInstance へ指示を出します。 もちろん、ModelInstance へ渡す名前はFBXでモデルに設定した名前と正確に一致しなければなりません。 これについては後で詳しく説明しますが、今のところはこのまま実行して前回と全く同じ様になるか見てみましょう。

これは本当に便利です。 シーンに必要な全てのモデルを一つのファイルで用意することができます。 さらに、持っているModelが1つだけなのでModelInstanceはリソースを共有します。 これにより、ModelBatch はパフォーマンスの最適化を行うことができます(詳細は後ほど説明します)。 もちろん、必要であれば複数のファイルを使用することもできます。 実際に、場合によっては (例えば、スキンやアニメーションモデルを使用する場合など)モデルを複数のファイルとして扱う方がよりやりやすいでしょう。

まだ他にもやることがあります。 モデリングソフトに戻って、前回作成したのを同じシーンを開いてください。 今回はshipモデルを選択してY軸を中心に180度回転させ、Z軸に沿って6ユニット移動させます。Javaコード上での処理と同じようにします。 次にblockモデルを選択してZ軸上で3ユニット、X軸上で-5ユニット移動させ、名前を "block" から "block1"へ変更します。 そして invader モデルを選択してX軸上で -5 ユニット移動させて名前を "invader1"へ変更します。

次に、block1モデルを5回複製(インスタンスを作成)し、各インスタンスを複製した位置からそれぞれX軸上で2ユニット移動させます。 すると6個のブロックが隣り合うようになります。 6個のブロックには "block1" から "block6"という名前を順に付けるようにしてください。 デュプリケートインスタンス(モデリングソフトによっては別の呼び方をするかもしれません)では各インスタンスが同じ頂点データを共有します。 今度はinvader1でも同じことをしますが、こちらはZ軸上でも同じ作業を行います。これにより、前回作成したコードと同じような格子状に配置されます。 以下のような表示になるでしょう。:

scenetest3

モデリングソフトで使用したグリッド間隔は5ユニットなので覚えておいてください。

このシーンを invaderscene.fbx へ出力し、そして g3dbに変換しましょう:


fbx-conv -f invaderscene.fbx

改めて説明すると、コマンドラインオプション-f はテクスチャ座標を反転させます。 モデリングソフトによってはこのオプションが必要な場合があります。 それではLibGDXを使ってシーンを読み込んでみましょう:


    public void create () {
        ...
        assets = new AssetManager();
        assets.load("data/invaderscene.g3db", Model.class);
        loading = true;
    }

    private void doneLoading() {
        Model model = assets.get("data/invaderscene.g3db", Model.class);
        for (int i = 0; i < model.nodes.size; i++) {
            String id = model.nodes.get(i).id;
            ModelInstance instance = new ModelInstance(model, id);
            Node node = instance.getNode(id);

            instance.transform.set(node.globalTransform);
            node.translation.set(0,0,0);
            node.scale.set(1,1,1);
            node.rotation.idt();
            instance.calculateTransforms();

            if (id.equals("space")) {
                space = instance;
                continue;
            }

            instances.add(instance);

            if (id.equals("ship"))
                ship = instance;
            else if (id.startsWith("block"))
                blocks.add(instance);
            else if (id.startsWith("invader"))
                invaders.add(instance);
        }

        loading = false;
    }

githubにある全ソースコードを見てください

変更内容は次の通りです: まず前回と同じ様にinvaders モデルを取得します。 ご存知の通り、これには全てのモデルが含まれています。モデリングソフトで使用した名前情報も入っています。 これらはnodes内に保持されています。そのため、nodes に反復処理を行って各nodeの名前(id)を取得します。 それから前回やった時と同じ様に、取得したmodel とid を引数として使ってModelInstance を作成します。 次の行では、instanceからnode を取得しています。取得したnodeは、基本的にはmodel内にあるnodeのコピーです。

次に、ModelInstance のtransformation 情報をnodeのtransformation 情報に設定します。 実際は、これは以前にモデリングソフトで設定したtransformation 情報(回転や移動など)を読み取ります。 そして、今後はModelInstance のtransform情報を使用するので、nodeのtransformation情報をリセットする必要があります。 位置情報には(0,0,0)、スケール情報には(1,1,1)、回転情報には識別するための4元数を設定します。 この後にcalculateTransforms()を実行して、これらの新しい値でModelInstance を更新するようにします。

モデリングソフトの時と同じ様にModelInstance の用意ができたので、ModelInstance をシーンに追加する必要があります。 IDが"space"の場合は、ModelInstance のspace を割り当ててcontinueでループをスキップします。 space はinstances配列のループ内で描写処理を行っておらず、個別に手動で描写処理を行っているためです。 IDが"space"でない場合はそれをinstances 配列に追加するので、正常に描写されるようになります。 最後にIDが"ship"か "block" か "invader"のどれかで始まっているかどうかを確認し、それに応じてModelInstance のship を割り当てるか、 もしくは適切な配列に追加をします。

それでは、コードを実行して前回と全く同じようになるかを見てみましょう。 大変な作業でしたが、モデリングソフト内で全てのシーンを設計するように手順を変えたので設計が大幅に簡単になりました。

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

また、こちらも読んでみてください: libgdxでの3D視錐台カリング