WisdomSoft - for your serial experiences.

モデルの読み込みと描画

複雑な構造を持った 3D モデルを描画するには、外部のモデリングツールなどを用いてモデルファイルを作成し、これをゲームで読み込みます。XNA Game Studio は標準で DirectX で標準的に用いられていた X ファイルと、Autodesk 社の FBX ファイルに対応しています。

モデルデータ

頂点データをゲーム内で生成するようなことは少なく、一般的なゲーム開発ではモデリングツールを用いてデザインしたモデルデータをゲームで読み込んで利用します。

扱う 3D モデルのフォーマットは、モデリングツールによっても異なり、完全に標準化された共通のデータ形式というものは存在せず、同じデータ形式でもツールによってサポート範囲が異なります。比較的広く使われている汎用的なフォーマットにおいても、出力するモデリングツールによって互換性が十分ではないこともあるので注意が必要です。

XNA Framework では、古くから DirectX で用いられている X ファイルという形式と、Autodesk 社の FBX ファイル形式を使えます。いずれかのフォーマットのファイルをコンテンツプロジェクトに追加してください。画像ファイルと同じように、ビルド時にコンテンツパイプラインによってアセットに変換されます。変換されたアセットを、実行時に読み込むことでモデルとして描画できます。

モデルの描画

コンテンツプロジェクトに登録した X ファイルや FBX ファイルといった 3D モデルは Microsoft.Xna.Framework.Graphics.Model クラスのオブジェクトとして取得できます。

Microsoft.Xna.Framework.Graphics.Model クラス
public sealed class Model

コンテンツに追加したモデルを取得するには、テクスチャを取得する場合と同じように ContentManager クラスの Load() メソッドを使います。

Model model = Content.Load<Model>("TestModel");

モデルの描画に必要な頂点配列やインデックス、エフェクトなどは全て Model オブジェクト内に含まれているため、変換行列を設定するだけでモデルを描画できます。モデル全体の描画には Draw() メソッドを使います。

Model クラス Draw() メソッド
public void Draw (
         Matrix world,
         Matrix view,
         Matrix projection
)

world パラメータにはワールド変換行列を、view パラメータにはビュー変換行列を、projection パラメータには射影変換行列を指定します。Model オブジェクトは、自身が持つデータとエフェクトに対して指定された変換行列を用いて描画します。

コード1
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private Model model;
    private Matrix world, view, projection;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        world = Matrix.Identity;
        projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45), 16F / 9F, 1, 1000
        );
        view = Matrix.CreateLookAt(new Vector3(2, 2, 5), Vector3.Zero, Vector3.Up);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        model = Content.Load<Model>("TestModel");
        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        world *= Matrix.CreateRotationY(MathHelper.ToRadians(2));
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);

        model.Draw(world, view, projection);

        base.Draw(gameTime);
    }
}
実行結果
コード1 実行結果

コード1はテクスチャを使っていない単純なモデルを読み込んで描画しています。キャラクターアニメーションなどを含まない、建造物などのモデルを読み込んで配置するだけであれば、このように Model オブジェクトとして読み込んで Draw() メソッドを呼び出すだけです。

モデルメッシュ

Model クラスは、モデルを構成する個別の要素をメッシュという単位で保有します。メッシュは、頂点やエフェクトといったプリミティブの描画に必要なデータを持ちます。ゲームは、モデルが保有するメッシュを個別に取得して描画します。メッシュは Meshes プロパティから取得できます。

Model クラス Meshes プロパティ
public ModelMeshCollection Meshes { get; }

このプロパティは ReadOnlyCollection クラスを継承する、メッシュの読み取り専用コレクションを表す Microsoft.Xna.Framework.Graphics.ModelMeshCollection クラスのオブジェクトを返します。

Microsoft.Xna.Framework.Graphics.ModelMeshCollection クラス
public sealed class ModelMeshCollection : ReadOnlyCollection<ModelMesh>

ModelMeshCollection は、ReadOnlyCollection クラスの型パラメータに Microsoft.Xna.Framework.Graphics.ModelMesh クラスを指定して継承したものなので、このコレクションは任意の数の ModelMesh オブジェクトを管理します。このコレクションの要素である ModelMesh オブジェクトが 1 つのメッシュに対応します。

Microsoft.Xna.Framework.Graphics.ModelMesh クラス
public sealed class ModelMesh

得られた ModelMesh オブジェクトを描画するには Draw() メソッドを使います。メッシュは内部で頂点などのデータを持ち、Draw() メソッドによってモデルに関連付けられている GraphicsDevice に対して描画されます。手動でメッシュのデータを取得して、編集や組み合わせを行うといった高度な描画処理を行うことも可能ですが、多くの場合は、より簡単な Draw() メソッドを使った描画が選択されるでしょう。

ModelMesh クラス Draw() メソッド
public void Draw ()

メッシュは ModelMesh クラスに関連付けられているエフェクトによって描画されます。メッシュを変換するには、メッシュのエフェクトを取得して行列を設定する必要があります。メッシュが持つエフェクトは Effects プロパティから取得できます。

ModelMesh クラス Effects プロパティ
public ModelEffectCollection Effects { get; }

このプロパティが返す Microsoft.Xna.Framework.Graphics.ModelEffectCollection クラスのオブジェクトは、ReadOnlyCollection クラスの型パラメータに Effect クラスを指定して継承する Effect オブジェクトの読み取り専用のコレクションです。

Microsoft.Xna.Framework.Graphics.ModelEffectCollection クラス
public sealed class ModelEffectCollection : ReadOnlyCollection<Effect>

ここから取得したエフェクトに、必要なパラメータを設定して描画することで、メッシュに対してワールド変換やビュー変換を行えます。モデル全体はなく、モデルを構成する部品ごとに各種の変換を行いたい場合に有効です。XNA Framework デフォルトのプロセッサである「Model - XNA Framework」を用いてビルドされている場合、デフォルトのエフェクトに BasicEffect が用いられています。

コード2
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private Model model;
    private Matrix world, view, projection;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        world = Matrix.Identity;
        projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45), 16F / 9F, 1, 1000
        );
        view = Matrix.CreateLookAt(new Vector3(2, 2, 5), Vector3.Zero, Vector3.Up);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        model = Content.Load<Model>("TestModel");
        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        world *= Matrix.CreateRotationY(MathHelper.ToRadians(2));

        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                effect.World = world;
                effect.View = view;
                effect.Projection = projection;
            }
        }

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);

        foreach (ModelMesh mesh in model.Meshes)
            mesh.Draw();

        base.Draw(gameTime);
    }
}
実行結果
コード2 実行結果

コード2の実行結果はコード1と同じですが、モデルに含まれているメッシュごとにエフェクトを設定して描画している点が異なります。Meshes プロパティからメッシのコレクションを取得し、foreach 文を用いて ModelMesh オブジェクトを個別に描画しています。Update() メソッド内では、メッシュが持つすべてのエフェクトにワールド行列、ビュー行列、射影行列を設定しています。これによって、モデルの移動や伸縮といった制御が可能になります。

メッシュに含まれるデータ

モデルメッシュには、GraphicsDevice クラスの Draw~() メソッドを用いて描画するための情報がすべて含まれています。すなわち、自分自身の頂点配列、インデックス配列、プリミティブ数などが情報として含まれています。

ModelMesh の Draw() メソッドは、内部で GraphicsDevice オブジェクトの DrawIndexedPrimitives() メソッドを呼び出して自身を描画しています。よって、大量のメッシュを foreach 文で順次描画すると、メッシュ単位で DrawIndexedPrimitives() メソッドの呼び出しが発生します。微細なパフォーマンス調整が求められる高度な描画処理では、これを避けるためにメッシュのデータをまとめて、1 度の描画でモデル全体を表示するといった手法が求められることがあります。

このような場合、モデルに含まれている頂点やインデックスのデータにアクセスし、モデルから頂点やインデックスを取り出して GraphicsDevice クラスの Draw() メソッドで描画することができます。描画に必要な情報は、メッシュを構成するエフェクトごとに分割されたメッシュパートから得られます。メッシュパートは MeshParts プロパティから得られます。

ModelMesh クラス MeshParts プロパティ
public ModelMeshPartCollection MeshParts { get; }

このプロパティが返す Microsoft.Xna.Framework.Graphics.ModelMeshPartCollection クラスのオブジェクトは、ReadOnlyCollection クラスを継承しているコレクションです。

Microsoft.Xna.Framework.Graphics.ModelMeshPartCollection クラス
public sealed class ModelMeshPartCollection : ReadOnlyCollection<ModelMeshPart>

このクラスは、メッシュの部分情報を提供する Microsoft.Xna.Framework.Graphics.ModelMeshPart クラスのオブジェクトを管理しています。メッシュは、頂点バッファのデータをさらに細かい区分単位に分割して管理しており、これをメッシュパートと呼びます。ModelMeshPart クラスは 1 つのメッシュパートを表し、頂点バッファとインデックスバッファ全体に対するメッシュパートの開始オフセットやプリミティブ数、エフェクトなどを含みます。一般的なモデリングツールなどでマテリアルと呼ばれている概念に近いものだと考えてください。

Microsoft.Xna.Framework.Graphics.ModelMeshPart クラス
public sealed class ModelMeshPart

メッシュパートは部品を構成する頂点バッファとインデックスバッファを含んできます。これらは親であるメッシュ単位で共有されており、ModelMeshPart オブジェクトごとにプロパティで保有していますが、インスタンスは同じです。メッシュの頂点バッファは VertexBuffer プロパティから、インデックスバッファは IndexBuffer プロパティから取得できます。

ModelMeshPart クラス VertexBuffer プロパティ
public VertexBuffer VertexBuffer { get; }
ModelMeshPart クラス IndexBuffer プロパティ
public IndexBuffer IndexBuffer { get; }

同一のメッシュ下にある頂点バッファとインデックスバッファはメッシュパートで共有されるため、メッシュの描画処理の先頭で 1 度だけ設定すれば問題ありません。

インデックスバッファ内の各要素であるインデックスに追加するオフセットは VertexOffset プロパティから得られます。同様に、使用する頂点の数を NumVertices プロパティから、頂点の読み取りを開始する位置は StartIndex プロパティから、描画するプリミティブの数は PrimitiveCount プロパティからそれぞれ取得できます。

ModelMeshPart クラス VertexOffset プロパティ
public int VertexOffset { get; }
ModelMeshPart クラス NumVertices プロパティ
public int NumVertices { get; }
ModelMeshPart クラス StartIndex プロパティ
public int StartIndex { get; }
ModelMeshPart クラス PrimitiveCount プロパティ
public int PrimitiveCount { get; }

これらのプロパティから得られる値は DrawIndexedPrimitives() メソッドのパラメータに対応しています。メッシュパートの描画は、一般的に次のような形がとられるでしょう。

GraphicsDevice.DrawIndexedPrimitives(
	PrimitiveType.TriangleList,
	meshPart.VertexOffset,
	0,
	meshPart.NumVertices,
	meshPart.StartIndex,
	meshPart.PrimitiveCount
);

加えて、描画に必要な変換を行うためにエフェクトを設定します。メッシュパートの描画に使うエフェクトは Effect プロパティから取得できます。

ModelMeshPart クラス Effect プロパティ
public Effect Effect { get; set; }

ModelMesh クラスの Effects プロパティから取得したエフェクトのコレクションは、実際にはメッシュが保有するメッシュパートの Effect プロパティを反復処理で個別に参照しているだけです。よって、モデルのエフェクトを既定の状態から他のカスタムエフェクトに変更したい場合は、上記の Effect プロパティに目的の Effect オブジェクトを設定することで実現できます。

コード3
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestGame : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new TestGame()) game.Run();
    }

    private GraphicsDeviceManager graphicsDeviceManager;
    private Model model;
    private Matrix world, view, projection;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        world = Matrix.Identity;
        projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45), 16F / 9F, 1, 1000
        );
        view = Matrix.CreateLookAt(new Vector3(2, 2, 5), Vector3.Zero, Vector3.Up);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        model = Content.Load<Model>("TestModel");
        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        world *= Matrix.CreateRotationY(MathHelper.ToRadians(2));

        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (ModelMeshPart meshPart in mesh.MeshParts)
            {
                BasicEffect effect = meshPart.Effect as BasicEffect;
                effect.World = world;
                effect.View = view;
                effect.Projection = projection;
            }
        }

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);

        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (ModelMeshPart meshPart in mesh.MeshParts)
            {
                GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer);
                GraphicsDevice.Indices = meshPart.IndexBuffer;

                foreach (EffectTechnique technique in meshPart.Effect.Techniques)
                {
                    foreach (EffectPass pass in technique.Passes)
                    {
                        pass.Apply();
                        GraphicsDevice.DrawIndexedPrimitives(
                            PrimitiveType.TriangleList, meshPart.VertexOffset, 0,
                            meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount
                        );
                    }
                }
            }
        }

        base.Draw(gameTime);
    }
}
実行結果
コード3 実行結果

コード2が ModelMesh クラスの Draw() メソッドを用いてメッシュを描画していたのに対し、コード3はメッシュのデータを取得して DrawIndexedPrimitives() メソッドで描画しています。実行結果はコード2と同じですが、ModelMesh クラスの Draw() メソッドの働きを理解するために役に立ちます。メッシュのデータ構造を理解することで、実行時にメッシュの加工や結合といった処理を実現できます。