Xファイルからモデルデータ読み込み

Page creation date :

The page you are currently viewing does not support the selected display language.

いよいよ、Xファイルの読み込みを説明します!「Xファイル」とは、ポリゴンの頂点データや面のデータ、マテリアルなどいろんなデータをまとめたファイルです。これを読み込むことによって、簡単に複雑な形状のモデルを表示させることができるようになります。

実行すると下のように表示されます。結構前に作ったモデルなので、モデルの形状とかテクスチャーとか結構適当です。ちなみに名前は「でるでる」です。(ゲーム「JPEGバトラー」より

Xファイルからモデルデータ読み込み

今回のメインコードファイルを載せます。

MainSample.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MDXSample
{
    /// <summary>
    /// メインサンプルクラス
    /// </summary>
    public partial class MainSample : IDisposable
    {
        /// <summary>
        /// メッシュ
        /// </summary>
        private Mesh _mesh = null;

        /// <summary>
        /// マテリアル情報配列
        /// </summary>
        private ExtendedMaterial[] _materials = null;

        /// <summary>
        /// テクスチャー配列
        /// </summary>
        private Texture[] _textures = null;


        /// <summary>
        /// アプリケーションの初期化
        /// </summary>
        /// <param name="topLevelForm">トップレベルウインドウ</param>
        /// <returns>全ての初期化がOKなら true, ひとつでも失敗したら false を返すようにする</returns>
        /// <remarks>
        /// false を返した場合は、自動的にアプリケーションが終了するようになっている
        /// </remarks>
        public bool InitializeApplication(MainForm topLevelForm)
        {
            // フォームの参照を保持
            this._form = topLevelForm;

            // 入力イベント作成
            this.CreateInputEvent(topLevelForm);

            try
            {
                // Direct3D デバイス作成
                this.CreateDevice(topLevelForm);

                // フォントの作成
                this.CreateFont();
            }
            catch (DirectXException ex)
            {
                // 例外発生
                MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }


            // Xファイルを読み込んでメッシュを作成する
            try
            {
                this._mesh = Mesh.FromFile("Deruderu.x",
                    MeshFlags.Managed, this._device, out this._materials);
            }
            catch (DirectXException ex)
            {
                // メッシュの作成に失敗した場合は例外が飛んでくる
                MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // 法線情報がなければ計算して作成
            if ((this._mesh.VertexFormat & VertexFormats.Normal) == 0)
            {
                // 法線情報を加えたメッシュを複製する
                Mesh tempMesh = this._mesh.Clone(this._mesh.Options.Value,
                    this._mesh.VertexFormat | VertexFormats.Normal, this._device);

                // 法線を計算
                tempMesh.ComputeNormals();

                // 古いメッシュを破棄し、置き換える
                this._mesh.Dispose();
                this._mesh = tempMesh;
            }

            // テクスチャーがあれば読み込み
            if (this._materials.Length >= 1)
            {
                // テクスチャー用の配列を作成
                this._textures = new Texture[this._materials.Length];

                // 配列分テクスチャーの読み込みを試みる
                for (int i = 0; i < this._materials.Length; i++)
                {
                    // 必ず null で初期化する
                    this._textures[i] = null;

                    // テクスチャー名が登録されているか確認
                    if (this._materials[i].TextureFilename != null &&
                        this._materials[i].TextureFilename.Length >= 1)
                    {
                        try
                        {
                            // テクスチャーを読み込む
                            this._textures[i] = TextureLoader.FromFile(this._device,
                                this._materials[i].TextureFilename);
                        }
                        catch (DirectXException ex)
                        {
                            // テクスチャーの作成に失敗した場合は例外が飛んでくる
                            MessageBox.Show(ex.ToString(), "エラー",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
                            return false;
                        }
                    }
                }
            }


            // 平行光線を使用
            this._device.Lights[0].Type = LightType.Directional;

            // ライトの方向
            this._device.Lights[0].Direction = new Vector3(1.0f, -1.5f, 2.0f);

            // 光の色は白
            this._device.Lights[0].Diffuse = Color.White;

            // 環境光
            this._device.Lights[0].Ambient = Color.FromArgb(255, 128, 128, 128);

            // 0 番のライトを有効
            this._device.Lights[0].Enabled = true;

            // 0 番のライトを更新
            this._device.Lights[0].Update();

            return true;
        }

        /// <summary>
        /// メインループ処理
        /// </summary>
        public void MainLoop()
        {
            // カメラの設定
            this.SettingCamera();

            // 描画内容を単色でクリアし、Zバッファもクリア
            this._device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);

            // 「BeginScene」と「EndScene」の間に描画内容を記述する
            this._device.BeginScene();


            // メッシュの描画
            // 属性の数だけループさせて描画
            for (int i = 0; i < this._materials.Length; i++)
            {
                // テクスチャーのセット
                this._device.SetTexture(0, this._textures[i]);

                // マテリアルをセット
                this._device.Material = this._materials[i].Material3D;

                // 描画
                this._mesh.DrawSubset(i);
            }


            // 文字列の描画
            this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 0, Color.White);
            this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 12, Color.White);
            this._font.DrawText(null, "マウス位置:" + this._oldMousePoint, 0, 24, Color.White);

            // 描画はここまで
            this._device.EndScene();

            // 実際のディスプレイに描画
            this._device.Present();
        }

        /// <summary>
        /// リソースの破棄をするために呼ばれる
        /// </summary>
        public void Dispose()
        {
            // テクスチャーの解放
            if (this._textures != null)
            {
                foreach (Texture i in this._textures)
                {
                    if (i != null)
                    {
                        i.Dispose();
                    }
                }
            }

            // メッシュの解放
            if (this._mesh != null)
            {
                this._mesh.Dispose();
            }

            // フォントのリソースを解放
            if (this._font != null)
            {
                this._font.Dispose();
            }

            // Direct3D デバイスのリソース解放
            if (this._device != null)
            {
                this._device.Dispose();
            }
        }
    }
}
MainSamplePartial.cs ファイルのコードはこちらです。

MainSamplePartial.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MDXSample
{
    public partial class MainSample
    {
        /// <summary>
        /// メインフォーム
        /// </summary>
        private MainForm _form = null;

        /// <summary>
        /// Direct3D デバイス
        /// </summary>
        private Device _device = null;

        /// <summary>
        /// Direct3D 用フォント
        /// </summary>
        private Microsoft.DirectX.Direct3D.Font _font = null;

        /// <summary>
        /// キーのプレス判定
        /// </summary>
        private bool[] _keys = new bool[256];

        /// <summary>
        /// 1つ前のマウスの位置
        /// </summary>
        private Point _oldMousePoint = Point.Empty;

        /// <summary>
        /// カメラレンズの位置(R)
        /// </summary>
        private float _lensPosRadius = 10.0f;

        /// <summary>
        /// カメラレンズの位置(θ)
        /// </summary>
        private float _lensPosTheta = 300.0f;

        /// <summary>
        /// カメラレンズの位置(φ)
        /// </summary>
        private float _lensPosPhi = 30.0f;


        /// <summary>
        /// 入力イベント作成
        /// </summary>
        /// <param name="topLevelForm">トップレベルウインドウ</param>
        private void CreateInputEvent(MainForm topLevelForm)
        {
            // キーイベント作成
            topLevelForm.KeyDown += new KeyEventHandler(this.form_KeyDown);
            topLevelForm.KeyUp += new KeyEventHandler(this.form_KeyUp);
            // マウス移動イベント
            topLevelForm.MouseMove += new MouseEventHandler(this.form_MouseMove);
        }

        /// <summary>
        /// キーボードのキーを押した瞬間
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void form_KeyDown(object sender, KeyEventArgs e)
        {
            // 押されたキーコードのフラグを立てる
            if ((int)e.KeyCode < this._keys.Length)
            {
                this._keys[(int)e.KeyCode] = true;
            }
        }
        /// <summary>
        /// キーボードのキーを放した瞬間
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void form_KeyUp(object sender, KeyEventArgs e)
        {
            // 放したキーコードのフラグを下ろす
            if ((int)e.KeyCode < this._keys.Length)
            {
                this._keys[(int)e.KeyCode] = false;
            }
        }

        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void form_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                // 回転
                this._lensPosTheta -= e.Location.X - this._oldMousePoint.X;
                this._lensPosPhi += e.Location.Y - this._oldMousePoint.Y;

                // φに関しては制限をつける
                if (this._lensPosPhi >= 90.0f)
                {
                    this._lensPosPhi = 89.9999f;
                }
                else if (this._lensPosPhi <= -90.0f)
                {
                    this._lensPosPhi = -89.9999f;
                }
            }
            // マウスの位置を記憶
            this._oldMousePoint = e.Location;
        }

        /// <summary>
        /// Direct3D デバイスの作成
        /// </summary>
        /// <param name="topLevelForm">トップレベルウインドウ</param>
        private void CreateDevice(MainForm topLevelForm)
        {
            // PresentParameters。デバイスを作成する際に必須
            // どのような環境でデバイスを使用するかを設定する
            PresentParameters pp = new PresentParameters();

            // ウインドウモードなら true、フルスクリーンモードなら false を指定
            pp.Windowed = true;

            // スワップ効果。とりあえず「Discard」を指定。
            pp.SwapEffect = SwapEffect.Discard;

            // 深度ステンシルバッファ。3Dでは前後関係があるので通常 true
            pp.EnableAutoDepthStencil = true;

            // 自動深度ステンシル サーフェイスのフォーマット。
            // 「D16」に対応しているビデオカードは多いが、前後関係の精度があまりよくない。
            // できれば「D24S8」を指定したいところ。
            pp.AutoDepthStencilFormat = DepthFormat.D16;

            try
            {
                // デバイスの作成
                this.CreateDevice(topLevelForm, pp);
            }
            catch (DirectXException ex)
            {
                // 例外発生
                throw ex;
            }
        }
        /// <summary>
        /// Direct3D デバイスの作成
        /// </summary>
        /// <param name="topLevelForm">トップレベルウインドウ</param>
        /// <param name="presentationParameters">PresentParameters 構造体</param>
        private void CreateDevice(MainForm topLevelForm, PresentParameters presentationParameters)
        {
            // 実際にデバイスを作成します。
            // 常に最高のパフォーマンスで作成を試み、
            // 失敗したら下位パフォーマンスで作成するようにしている。
            try
            {
                // ハードウェアによる頂点処理、ラスタライズを行う
                // 最高のパフォーマンスで処理を行えます。
                // ビデオカードによっては実装できない処理が存在します。
                this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
                    CreateFlags.HardwareVertexProcessing, presentationParameters);
            }
            catch (DirectXException ex1)
            {
                // 作成に失敗
                Debug.WriteLine(ex1.ToString());
                try
                {
                    // ソフトウェアによる頂点処理、ハードウェアによるラスタライズを行う
                    this._device = new Device(0, DeviceType.Hardware, topLevelForm.Handle,
                        CreateFlags.SoftwareVertexProcessing, presentationParameters);
                }
                catch (DirectXException ex2)
                {
                    // 作成に失敗
                    Debug.WriteLine(ex2.ToString());
                    try
                    {
                        // ソフトウェアによる頂点処理、ラスタライズを行う
                        // パフォーマンスはとても低いです。
                        // その代わり、ほとんどの処理を制限なく行えます。
                        this._device = new Device(0, DeviceType.Reference, topLevelForm.Handle,
                            CreateFlags.SoftwareVertexProcessing, presentationParameters);
                    }
                    catch (DirectXException ex3)
                    {
                        // 作成に失敗
                        // 事実上デバイスは作成できません。
                        throw ex3;
                    }
                }
            }
        }

        /// <summary>
        /// フォントの作成
        /// </summary>
        private void CreateFont()
        {
            try
            {
                // フォントデータの構造体を作成
                FontDescription fd = new FontDescription();

                // 構造体に必要なデータをセット
                fd.Height = 12;
                fd.FaceName = "MS ゴシック";

                // フォントを作成
                this._font = new Microsoft.DirectX.Direct3D.Font(this._device, fd);
            }
            catch (DirectXException ex)
            {
                // 例外発生
                throw ex;
            }
        }

        /// <summary>
        /// カメラの設定
        /// </summary>
        private void SettingCamera()
        {
            // レンズの位置を三次元極座標で変換
            float radius = this._lensPosRadius;
            float theta = Geometry.DegreeToRadian(this._lensPosTheta);
            float phi = Geometry.DegreeToRadian(this._lensPosPhi);
            Vector3 lensPosition = new Vector3(
                (float)(radius * Math.Cos(theta) * Math.Cos(phi)),
                (float)(radius * Math.Sin(phi)),
                (float)(radius * Math.Sin(theta) * Math.Cos(phi)));

            // ビュー変換行列を設定
            this._device.Transform.View = Matrix.LookAtLH(
                lensPosition, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f));

            // 射影変換を設定
            this._device.Transform.Projection = Matrix.PerspectiveFovLH(
                Geometry.DegreeToRadian(60.0f),
                (float)this._device.Viewport.Width / (float)this._device.Viewport.Height,
                1.0f, 100.0f);
        }
    }
}

/// <summary>
/// メッシュ
/// </summary>
private Mesh _mesh = null;

/// <summary>
/// マテリアル情報配列
/// </summary>
private ExtendedMaterial[] _materials = null;

/// <summary>
/// テクスチャー配列
/// </summary>
private Texture[] _textures = null;

今回、モデルデータをコードで作るのではなく、Xファイルから読み込む方法をとっているため、今までと処理内容が結構変わります。

早速ですが、今まで見たことのない型が2つ出てきました。

まず最初の「Mesh」ですが、日本語では「メッシュ」と呼びます。これは、「頂点バッファ」「インデックスバッファ」「面情報」「属性テーブル」など複数のデータをまとめたクラスです。今までのボックスのようにバッファなどのフィールドを複数持ったり、配列としてもったりする必要はなく、このクラスひとつでまとめてしまうことができるのです。Xファイルからデータを読み込んだ場合、ほとんどはこのクラスにデータを保存します。

2つ目の「ExtendMaterial」は名前のとおり、「拡張したマテリアル」です。といってもマテリアルに関しては何も違いはありません。ただ、追加要素として「テクスチャーのファイル名」が加わっています。これはXファイルに設定されているテクスチャーファイル名を読み込むだけのものであり、テクスチャーを読み込めばほとんど必要なくなります。なので、メモリの無駄遣いを避けたい場合は、Xファイルを読み込むときだけこの型を使い、「Material」だけを持っていたほうがいいでしょう。

「ExtendMaterial」「Texture」の型に関して配列という形をとっていますが、Xファイルには複数のマテリアルやテクスチャーを持っていることがあります。それを描画時に使い分けるために配列化してあります。


// Xファイルを読み込んでメッシュを作成する
try
{
    this._mesh = Mesh.FromFile("Deruderu.x",
        MeshFlags.Managed, this._device, out this._materials);
}
catch (DirectXException ex)
{
    // メッシュの作成に失敗した場合は例外が飛んでくる
    MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return false;
}

Xファイルからメッシュデータを読み込みます。基本的に1行で済んでしまいますが、念のためにエラーチェックを行っています。エラーが起きればメッセージボックスを表示して false を返すようにします。

Mesh.FromFile メソッド

Xファイルからメッシュ関連データを読み込みます。
filename 読み込むXファイル名を指定しています。ファイル名の指定方法は「絶対パス」か「カレントディレクトリからの相対パス」です。今回使用している「Deruderu.x」ファイルは実行ファイルと同じディレクトリにおいてあります。
options メッシュの作成オプション。特になければ「MeshFlags.Managed」でいいでしょう。
device Direct3D デバイス
materials マテリアル データを含む配列を受け取ります。これを使用してテクスチャーを読み込んだり、マテリアルを使用したりします。

前回ボックスを6つの面に分けて作成しましたが、そんな少ないポリゴン数でもコードが非常に長くなってしまいました。しかし、Xファイルには「位置情報」や「テクスチャーのUV座標」など必要なデータがすでに入っているので、ちょこっと読み込むだけで簡単にモデルを作成できます。通常複雑なポリゴン構成を作成するときは、「Xファイル」のようなモデルデータファイルを読み込んで作成するのが一般的です


// 法線情報がなければ計算して作成
if ((this._mesh.VertexFormat & VertexFormats.Normal) == 0)
{
    // 法線情報を加えたメッシュを複製する
    Mesh tempMesh = this._mesh.Clone(this._mesh.Options.Value,
        this._mesh.VertexFormat | VertexFormats.Normal, this._device);

    // 法線を計算
    tempMesh.ComputeNormals();

    // 古いメッシュを破棄し、置き換える
    this._mesh.Dispose();
    this._mesh = tempMesh;
}

さて、Xファイルには「位置」や「UV座標」などのデータがあっても、「法線」のデータがないこともあります。もしなければ、法線データを加えて計算するようにしています。  なぜなら、法線データがないとライトを当てたときに色がおかしくなるためです。もちろんいらなければこのコードをはずしてもかまいません。

まず、条件分岐でメッシュの頂点フォーマットに、法線のデータが含まれているか確認します。法線があれば特に何もしません。

法線がなかった場合は、読み込んだメッシュに法線データを付け加えたメッシュを「複製」します。なぜ複製するのかというと、基本的にメッシュは直接頂点フォーマットを変えることはできません。例外もありますが、法線データを増やすなど、頂点のサイズが変化してしまう場合は必ず複製しなければなりません

Mesh.Clone メソッド

メッシュデータを複製します。
options メッシュの作成オプション。複製元のメッシュのオプションを渡せばいいでしょう。
vertexFormat ここでは複製元のメッシュの頂点フォーマットに法線データを付け加えるように指定します
device Direct3D デバイス

新しく複製したメッシュには法線データを設定できるスペースはできましたが、各頂点の実際の法線データはまだ設定されていません。そこで使用するのが「Mesh.ComputeNormals」メソッドです。これを使用すると、面の法線の関係などから頂点の法線を自動的に計算してくれます

後は最初に読み込んだメッシュを破棄し、新しいメッシュに置き換えます。


// テクスチャーがあれば読み込み
if (this._materials.Length >= 1)
{
    // テクスチャー用の配列を作成
    this._textures = new Texture[this._materials.Length];

    // 配列分テクスチャーの読み込みを試みる
    for (int i = 0; i < this._materials.Length; i++)
    {
        // 必ず null で初期化する
        this._textures[i] = null;

        // テクスチャー名が登録されているか確認
        if (this._materials[i].TextureFilename != null &&
            this._materials[i].TextureFilename.Length >= 1)
        {
            try
            {
                // テクスチャーを読み込む
                this._textures[i] = TextureLoader.FromFile(this._device,
                    this._materials[i].TextureFilename);
            }
            catch (DirectXException ex)
            {
                // テクスチャーの作成に失敗した場合は例外が飛んでくる
                MessageBox.Show(ex.ToString(), "エラー",
                     MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
        }
    }
}

メッシュで使用するテクスチャーを読み込みます。まず、属性の数を確認して読み込むか決めます。マテリアルがなければテクスチャーもないので読み込みません。

属性の数と言っていますが、「マテリアルとテクスチャーの組み合わせの数」と考えたほうがわかりやすいかもしれません。厳密には「属性テーブル」のエントリ数なのですが、ここではあんまり深いところまでは追求しません。ここでは属性の数と呼ぶことにします。

最初にテクスチャーを属性の数だけ作成します。その後、各配列にテクスチャーファイル名があるか確認して読み込みを行います読み込むテクスチャーがない場合は必ず null を設定してください。これは描画時にテクスチャーをセットする場合に使用します。


// メッシュの描画
// 属性の数だけループさせて描画
for (int i = 0; i < this._materials.Length; i++)
{
    // テクスチャーのセット
    this._device.SetTexture(0, this._textures[i]);

    // マテリアルをセット
    this._device.Material = this._materials[i].Material3D;

    // 描画
    this._mesh.DrawSubset(i);
}

Xファイルの読み込みが終わったら描画に移ります。

メッシュの属性の数だけループさせてポリゴンを表示させています。なぜ一度に描画しないのかというと、属性のインデックスによって、使用するマテリアルやテクスチャーが違うため、インデックスが変わるたびにセットし直す必要があるからです。

言葉どおりループ内で毎回テクスチャーとマテリアルをセットしています。「SetTexture」メソッドで第2引数に null を指定した場合は、テクスチャーを使用しないことと同じなので、テクスチャー作成時に null をセットしたのはこのためです。第1引数に関しては今は 0 を指定しておいてください。基本的には 0 でいいです。

最後に「DrawSubset」で描画しています。頂点バッファとかインデックスバッファの設定などはすべてこの中に含まれているので、コードがすっきりします。


// テクスチャーの解放
if (this._textures != null)
{
    foreach (Texture i in this._textures)
    {
        if (i != null)
        {
            i.Dispose();
        }
    }
}

// メッシュの解放
if (this._mesh != null)
{
    this._mesh.Dispose();
}

テクスチャーは読み込まれているならすべて解放します。読み込まれていないインデックスもあるかもしれないので、必ず null であるかチェックします。

メッシュの解放も忘れないように。