3Dポリゴンの描画

Puslapio sukūrimo data :

Šiuo metu peržiūrimas puslapis nepalaiko pasirinktos rodymo kalbos.

さて、いよいよ3D上でポリゴンを描画します。見た目は前回と似ていますが、きちんと3D空間上にポリゴンを配置しています。

3Dポリゴンの描画

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

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 VertexBuffer _vertexBuffer = null;


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

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


            // ビュー変換行列を設定
            this._device.Transform.View = Matrix.LookAtLH(new Vector3(0.0f, 0.0f, -10.0f),
                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);


            // 三角形ポリゴンを表示するための頂点バッファを作成
            this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
                3, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);

            // 3点の情報を格納するためのメモリを確保
            CustomVertex.PositionColored[] vertices = new CustomVertex.PositionColored[3];

            // 各頂点を設定
            vertices[0] = new CustomVertex.PositionColored(0.0f, 5.0f, 0.0f, Color.Red.ToArgb());
            vertices[1] = new CustomVertex.PositionColored(4.0f, -3.0f, 0.0f, Color.Blue.ToArgb());
            vertices[2] = new CustomVertex.PositionColored(-4.0f, -3.0f, 0.0f, Color.Green.ToArgb());

            // 頂点バッファをロックする
            using (GraphicsStream data = this._vertexBuffer.Lock(0, 0, LockFlags.None))
            {
                // 頂点データを頂点バッファにコピーします。
                data.Write(vertices);

                // 頂点バッファをロック解除します。
                this._vertexBuffer.Unlock();
            }

            // ライトを無効
            this._device.RenderState.Lighting = false;

            return true;
        }

        /// <summary>
        /// メインループ処理
        /// </summary>
        public void MainLoop()
        {
            // 描画内容を単色でクリアし、Zバッファもクリア
            this._device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);

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


            // 頂点バッファをデバイスのデータストリームにバインド
            this._device.SetStreamSource(0, this._vertexBuffer, 0);

            // 描画する頂点のフォーマットをセット
            this._device.VertexFormat = CustomVertex.PositionColored.Format;

            // レンダリング(描画)
            this._device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);


            // 文字列の描画
            this._font.DrawText(null, "頂点バッファを使用した3Dポリゴン表示", 0, 0, Color.White);

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

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

        /// <summary>
        /// リソースの破棄をするために呼ばれる
        /// </summary>
        public void Dispose()
        {
            // 頂点バッファを解放
            if (this._vertexBuffer != null)
            {
                this._vertexBuffer.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>
        /// 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;

            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;
            }
        }
    }
}

コードの説明に入る前に3Dに関して必要な知識を簡単に説明しておきます。

ゲームやCGでよく3Dを見かけるようになりましたが、あれはディスプレイ自体に3次元の空間があるわけではありません(当たり前ですが)。パソコンを触っている方なら分かると思いますが、現時点ではスクリーンは X, Y のような2次元の情報しか持つことが出来ません

ではなぜ画面上に3Dの空間を映像として移すことが出来るのでしょうか?結論を簡単に説明してしまうと、「3次元の情報を2次元の情報に変換」しているのです。もちろん適当に変換しているのではなく、2次元の画面上であたかも3次元の空間として認識できるように変換しています。この3D空間上の位置などの情報を2次元のスクリーン上に変換することを座標変換と呼びます。3Dプログラミングを行ううえでこの座標変換は必須と言ってもいいでしょう。

DirectX プログラミングでは、この座標変換を3つに分類することが出来ます。「ワールド座標変換」「ビュー座標変換」「射影座標変換」の3つです。ではこの3つについて簡単に説明します。

ワールド座標変換

例えば立方体の箱があったとします。この箱は原点(0, 0, 0)を中心として定義されているとします。

ボックス

このボックスを3D空間上 XYZ の任意の位置に移動したり回転させたい場合、実際にはボックスを構成する頂点を全て計算して移動しなければいけません。ボックスならまだしも、人などの複雑なモデルの場合、大変な計算量になってしまいます。

そこでこれらの計算を一括で行ってくれるのが「ワールド座標変換」です。ワールド座標変換では、元のモデルデータを拡大回転移動などを行って3次元空間に配置させることが出来ます。

ワールド座標変換

ビュー座標変換

次に必要なのが、その3D空間を「どこから見ているのか」と「どの方向を見ているのか」という情報を必要になります。これがビュー座標変換です。一般的にカメラという表現を代用して使用されます。

ここで必要なパラメータは「カメラの位置」「カメラの注視点」「カメラの上方方向」です。カメラの向きはカメラの位置とカメラの注視点があれば自動的に求まります。

ビュー座標変換

射影座標変換

最後に射影座標変換を行います。射影座標変換とは、2Dのスクリーンであたかも3D空間として表現させるのに使われます。別な表現をすると、3D空間のどの範囲を2Dのスクリーンに写すか、とも言い換えられます。

ここで使用されるパラメータは「カメラの視野角」「画面のアスペクト比」「カメラ空間の前後クリッピング範囲」です。

射影座標変換

視野角」は、カメラの見える範囲の角度を考えてみればわかると思います。値を減らすとズームインし、増やすとズームアウトします。ここで言う視野角とはY軸方向の角度になります。

アスペクト比」は「スクリーンの横と縦の比率」です。基本的には表示するスクリーンの「幅÷高さ」を指定すればきちんと表示されます。視野角とアスペクト比を掛けた値がX軸の視野角になります。

カメラ空間の前後クリッピング範囲」は手前はどこまで表示するか、奥はどこまで表示するかを指定します。コンピュータの性能上無限遠まで表示することができないので制限をつけておきます。

ただし、上記で説明したのは「遠近射影」というものであり、奥行きを表現するのに使用します。他に「正射影」という変換も存在し、この場合使用するパラメータが異なりますが今回は説明しません。

座標系

座標系には「左手座標系」と「右手座標系」があります。DirectX では基本的に左手座標系が使われることが多いですが、ソフトなどによっては右手座標系が使われることもあります。違いは下の図を見てもらえれば分かると思います。

左手座標系と右手座標系

この Tips では基本的に左手座標系を使用します。

Zバッファ

通常何かを描画する際は、前に描いたものがあった場合、それを上書きして描くのが普通です。しかし、3Dでは常に前後関係が付きまとい、描画ごとに上書きして描いていっては空間描画としてはおかしなものになってしまいます。

以前は「Zソート」と呼ばれる方法で奥のほうから描画する方法が使われ、それなりにうまく描画することは出来ましたが、ポリゴンの交差する場合など、Zソートだけでは正確に前後関係を表現することは出来ませんでした。

そこで考え出されたのが「Zバッファ」です。Zバッファとは、ピクセルごとに割り当てられた奥行きを示すパラメータで、ポリゴンを描画した際に一緒に書き込まれます。

例えば、一番手前が 0.0、 一番奥が 1.0 だとして、最初のポリゴンを 0.4 の位置に書き込み、次のポリゴンを 0.6 の位置に書き込もうとしても、そのポリゴン(正確にはピクセル)はZ値の判定で落ちるので描画されないことになります。なので、わざわざポリゴンをソートしたりしなくても前後関係をきちんと表現することが可能になったのです。さらにZバッファはピクセル単位で判定を行うので、ポリゴンが交差した場合でも正確に書き込むことが出来ます。


/// <summary>
/// 頂点バッファ
/// </summary>
private VertexBuffer _vertexBuffer = null;

3Dになっても頂点バッファを使うことに変わりありません。もちろん以前やった頂点データを描画時に直接転送する方法も使えます。


// 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;

PresentParameters を引っ張り出してきましたが、3D空間では前述したZバッファを使用するので、それにあわせてパラメータを2つ追加します。

追加したのは「EnableAutoDepthStencil」と「AutoDepthStencilFormat」です。

EnableAutoDepthStencil深度バッファZバッファ)を有効にするので true を指定します。

AutoDepthStencilFormat深度バッファの精度です。DepthFormat.D16 は精度はあまりよくありませんが、多くの環境で使用できるのでこれを設定します。環境がよければ D24 などを使用してもかまいません。

設定した PresentParameters でデバイスを作成するために、CreateDevice メソッドに渡しています。


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

さて、今回からカメラに関わる設定をする必要があります。座標変換のうちの「ビュー座標変換」にあたります。

前に述べたとおり、「カメラの位置」「カメラの注視点」「カメラの上方方向」が必要ですが、この変換を行うのに使用するのが「Matrix.LookAtLH」メソッドです。このメソッドを使用することにより、左手座標系ビュー行列が生成されます。基本的に座標変換と言った場合「4×4 の行列(マトリックス)」が使われますが、なぜマトリックスが使用されるのかという話は今回は省きます。おいおい分かってくるでしょう。

メソッドに渡すパラメータは下のテーブルを参照してください。基本的にそのままです。

生成されたビュー座標変換マトリックスを Device.Transform.View にセットすることにより、描画時にこのビュー座標変換が使用されるようになります。

Matrix.LookAtLH メソッド

左手座標系ビュー行列を作成
cameraPosition カメラの位置
cameraTarget カメラの注視点
cameraUpVector ワールドの上方

ちなみに今回は下のようにしてポリゴンを見ています。

ポリゴンを見るイメージ


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

ビュー座標変換と同様に、ここでは射影座標変換の処理を行っています。

Matrix.PerspectiveFovLH」メソッドを使用することにより、「左手座標系遠近射影行列」を生成できるので、これを「Device.Transform.Projection」に渡しています。これにより描画時にこのマトリックスが射影変換として使用されるようになります。

Matrix.PerspectiveFovLH メソッド

左手座標系遠近射影行列を作成
fieldOfViewY Y軸方向の視野角。角度はラジアン(Rasian)で渡す必要があるが、ラジアンは直感的に分かりにくいので、ディグリー(Degree)の単位を使用して「Geometry.DegreeToRadian」メソッドでラジアンに変換して指定しています。
aspectRatio アスペクト比。「スクリーンの横と縦の比率」で、基本的には表示するスクリーンの「幅÷高さ」を指定すればきちんと表示されます。この値を変更すると、画面内のものが縦に伸びたり横に伸びたりします。今回はビューポートのサイズを使用して計算しています。(ビューポートのサイズはディフォルトで描画先のコントロールと同じになる
znearPlane カメラに近い方のビュー面のクリッピング Z 値。
zfarPlane カメラから遠い方のビュー面のクリッピング Z 値。

// 三角形ポリゴンを表示するための頂点バッファを作成
this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
    3, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);

ここからは前回とほとんど変わっていませんが、頂点データを格納する構造体が違うことに注意してください。今回は「CustomVertex.PositionColored」を使用します。前と同じ「位置」と「」の情報を持ちますが、前回は「座標変換済み頂点」であったのに対し、今回は「座標変換を行っていない頂点」になります。

ここで理解した方もいるかもしれませんが、前回の頂点は座標変換済みであったため、スクリーンの位置に直接指定できたのです。今回はまだ座標変換していない頂点なので3D空間上に配置することになります。


// 3点の情報を格納するためのメモリを確保
CustomVertex.PositionColored[] vertices = new CustomVertex.PositionColored[3];

// 各頂点を設定
vertices[0] = new CustomVertex.PositionColored(0.0f, 5.0f, 0.0f, Color.Red.ToArgb());
vertices[1] = new CustomVertex.PositionColored(4.0f, -3.0f, 0.0f, Color.Blue.ToArgb());
vertices[2] = new CustomVertex.PositionColored(-4.0f, -3.0f, 0.0f, Color.Green.ToArgb());

構造体が違う点に注意してください。また、頂点の位置は3D空間上の位置になります。

CustomVertex.PositionColored 構造体

X 3D空間上での位置X
Y 3D空間上での位置Y
Z 3D空間上での位置Z
Color 頂点の色

// 頂点バッファをロックする
using (GraphicsStream data = this._vertexBuffer.Lock(0, 0, LockFlags.None))
{
    // 頂点データを頂点バッファにコピーします
    data.Write(vertices);

    // 頂点バッファをロック解除します
    this._vertexBuffer.Unlock();
}

前回と変わりありません。


// ライトを無効
this._device.RenderState.Lighting = false;

Direct3D ではライトというものが使用できますが、まだ使用しないので false に設定しておきます。ちなみに「true」を指定すると、ポリゴンが真っ黒になるかもしれません。


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

今まではクリアする内容は、書き込まれた色だけでしたが、今回からZバッファも使用するので、それもクリアするようにします。

もし、これをクリアしないと、Zバッファの性質上手前のポリゴンしか描画できないので、最終的には何も書き込めなくなってしまいます。

ちなみにZ値は 0.0 ~ 1.0 の範囲なので、一番奥である 1.0 でクリアするように第3引数に指定します。


// 頂点バッファをデバイスのデータ ストリームにバインド
this._device.SetStreamSource(0, this._vertexBuffer, 0);

// 描画する頂点のフォーマットをセット
this._device.VertexFormat = CustomVertex.PositionColored.Format;

// レンダリング(描画)
this._device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

描画部分もほとんど変わりありません。頂点フォーマットを指定するときの構造体が違うの点に注意してください。


// 頂点バッファを解放
if (this._vertexBuffer != null)
{
    this._vertexBuffer.Dispose();
}

前回と同様です。


さて終わりましたが、今回「ワールド座標変換」は行っていないので、そのままポリゴンは原点に配置されています。ワールド座標変換については後の Tips で必ず使用されますのでそのときに使い方を説明したいと思います。