スプライトによるスクリーンへの画像描画

Page updated :

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

よくゲームなどで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 Sprite _sprite = null;

        /// <summary>
        /// スプライト用のテクスチャー
        /// </summary>
        private Texture _texture = null;

        /// <summary>
        /// ティーポットメッシュ
        /// </summary>
        private Mesh _mesh = 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;
            }

            // XYZライン作成
            this.CreateXYZLine();


            // ティーポットメッシュの作成
            this._mesh = Mesh.Teapot(this._device);

            // テクスチャー読み込み
            this._texture = TextureLoader.FromFile(this._device, "Texture.jpg");

            // スプライトを作成
            this._sprite = new Sprite(this._device);


            // ライトを設定
            this.SettingLight();

            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();


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

            // 原点に配置
            this._device.SetTransform(TransformType.World, Matrix.Identity);

            // XYZラインを描画
            this.RenderXYZLine();


            // メッシュの描画
            this._device.RenderState.Lighting = true;
            Material mtrl = new Material();
            mtrl.Diffuse = Color.White;
            mtrl.Ambient = Color.FromArgb(255, 128, 128, 128);
            this._device.Material = mtrl;
            this._device.SetTransform(TransformType.World, Matrix.Identity);
            this._mesh.DrawSubset(0);


            // スプライトの描画
            // スプライトは「Sprite.Begin」と「Sprite.End」メソッドの間で
            // 描画しなければならない
            this._sprite.Begin(SpriteFlags.None);

            // 同じものを2枚描画してみる
            // 1枚目は最前面
            this._sprite.Draw(this._texture, Rectangle.Empty, Vector3.Empty,
                new Vector3(100.0f, 100.0f, 0.0f), Color.White);

            // 2枚目は最背面
            this._sprite.Draw(this._texture, Rectangle.Empty, Vector3.Empty,
                new Vector3(300.0f, 300.0f, 1.0f), Color.White);

            // スプライトの描画はここまで
            this._sprite.End();


            // 文字列の描画
            this._font.DrawText(null, "[Escape]終了", 0, 0, Color.White);
            this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 12, Color.White);
            this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 24, Color.White);

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

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

            // アプリケーションの終了操作
            if (this._keys[(int)Keys.Escape])
            {
                this._form.Close();
            }
        }

        /// <summary>
        /// リソースの破棄をするために呼ばれる
        /// </summary>
        public void Dispose()
        {
            // スプライトの解放
            if (this._sprite != null)
            {
                this._sprite.Dispose();
            }

            // テクスチャーの解放
            if (this._texture != null)
            {
                this._texture.Dispose();
            }

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

            // XYZラインの破棄
            this.DisposeXYZLine();

            // フォントのリソースを解放
            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 = 5.0f;

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

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

        /// <summary>
        /// XYZライン用頂点バッファ
        /// </summary>
        private VertexBuffer _xyzLineVertexBuffer = null;


        /// <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>
        /// XYZライン作成
        /// </summary>
        private void CreateXYZLine()
        {
            // 6つ分の頂点を作成
            this._xyzLineVertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
                6, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);

            // 頂点バッファをロックして、位置、色情報を書き込む
            using (GraphicsStream data = this._xyzLineVertexBuffer.Lock(0, 0, LockFlags.None))
            {
                // 今回は各XYZのラインを原点(0.0f, 0.0f, 0.0f)からプラス方向に 10.0f 伸びた線を作成
                data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
                data.Write(new CustomVertex.PositionColored(10.0f, 0.0f, 0.0f, Color.Red.ToArgb()));
                data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Green.ToArgb()));
                data.Write(new CustomVertex.PositionColored(0.0f, 10.0f, 0.0f, Color.Green.ToArgb()));
                data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 0.0f, Color.Blue.ToArgb()));
                data.Write(new CustomVertex.PositionColored(0.0f, 0.0f, 10.0f, Color.Blue.ToArgb()));

                this._xyzLineVertexBuffer.Unlock();
            }
        }

        /// <summary>
        /// ライトの設定
        /// </summary>
        private void SettingLight()
        {
            // 平行光線を使用
            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();
        }

        /// <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>
        /// XYZライン描画
        /// </summary>
        private void RenderXYZLine()
        {
            this._device.SetStreamSource(0, this._xyzLineVertexBuffer, 0);
            this._device.VertexFormat = CustomVertex.PositionColored.Format;
            this._device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
        }

        /// <summary>
        /// XYZラインの破棄
        /// </summary>
        private void DisposeXYZLine()
        {
            if (this._xyzLineVertexBuffer != null)
            {
                this._xyzLineVertexBuffer.Dispose();
            }
        }
    }
}

/// <summary>
/// スプライト
/// </summary>
private Sprite _sprite = null;

/// <summary>
/// スプライト用のテクスチャー
/// </summary>
private Texture _texture = null;

今回は「スプライト」を使用するので、そのまんま「Sprite」というものを宣言します。スプライトとは、ヘルプによれば「2D サーフェイスにマップされたテクスチャ」と書かれていますが、分かりにくいと思うので「2Dのポリゴン」だと思ってもかまいません。

以前、座標変換済みポリゴンを使ったと思いますが、スプライトはこれを矩形化し、移動や回転などの処理を内部的に計算したり、色の補正などを行ってくれるようにクラス化したものです。2Dのゲームなんかではスプライトの概念はよく使われることが多いです。

スプライトは表示する画像の数に関係なく「1つ」だけでかまいません。描画順など厳密に操作したい場合は複数必要になる場合もありますが、そうでなければ基本的に1つでいいです。

スプライトを描画するには、もとの画像となる「テクスチャー」が必要です。今回は画像を1つだけ使用するので「Texture」は1つですが、こちらは画像の種類にあわせて増やす必要があります


// テクスチャー読み込み
this._texture = TextureLoader.FromFile(this._device, "Texture.jpg");

テクスチャーは今までと同じように読み込みます。


// スプライトを作成
this._sprite = new Sprite(this._device);

スプライトの作成を行っていますが、デバイスを指定するだけで終わりです。


// スプライトの描画
// スプライトは「Sprite.Begin」と「Sprite.End」メソッドの間で
// 描画しなければならない
this._sprite.Begin(SpriteFlags.None);

// 同じものを2枚描画してみる
// 1枚目は最前面
this._sprite.Draw(this._texture, Rectangle.Empty, Vector3.Empty,
    new Vector3(100.0f, 100.0f, 0.0f), Color.White);

// 2枚目は最背面
this._sprite.Draw(this._texture, Rectangle.Empty, Vector3.Empty,
    new Vector3(300.0f, 300.0f, 1.0f), Color.White);

// スプライトの描画はここまで
this._sprite.End();

メインループの描画ですが、スプライトを描画する際決まりがあり、「Sprite.Begin」メソッドと「Sprite.End(または Sprite.Flush)」メソッドの間に記述しなければなりません

理由としてはスプライトを描画するときに「レンダーステート」や「座標変換」などが内部的に変更されるため、スプライトの描画が終わったあとに再度再設定する手間を省くためです。

Sprite.Begin メソッドには複数の「SpriteFlags」を指定するのですが、今回は特に何も指定していません。詳しく知りたい方はヘルプを見てください。半透明やソートなどの設定が出来ます。

描画に関して今回は同じ画像を2回描画しています。しかし、この2つで決定的に違うのは「最前面に描画」しているのと「最背面に描画」しているという違いがあります。その説明は後ほど行います。

描画には「Sprite.Draw」メソッドを使用します。

Sprite.Draw メソッド

バッチ スプライトのリストにスプライトを追加します
srcTexture 描画する際に使用するテクスチャーを指定
srcRectangle 画像の転送矩形です。「Rectangle.Empty」を指定すると画像全体が描画されます。もし、画像に複数のパターンをまとめている場合は、この引数で描画したい範囲を決めます。
center

スプライトの中心座標です。回転や拡大に使用しますが、今回は何もしないのでスプライトの左上に指定しています。ただ、この値を変更すると実際の描画位置も変更されるので注意してください。(下の図参照

画像の中心座標関係

position

スプライトの描画位置です。X と Y に関してはスクリーン座標で指定できます。左上を原点として左に X, 下に Y として計算してください。そして Z ですが、これは前後関係に影響します。0.0 が最前面で 1.0 が最背面です。3D空間上で配置されているポリゴンも、座標変換で計算された Z 値で前後関係を調べています。なので 0.0 を指定すればすべてのポリゴンの前面に描画でき、1.0 を指定すればすべてのポリゴンの背面に描画できるというわけです。
 ちなみに 0.0 と 1.0 の範囲の外を指定すると、カメラ視界の前後範囲外を指定しているのと同じで描画されなくなります。
 あと、前後関係がわかりやすくするためにティーポットを配置しています。

Z値

color テクスチャーの色と掛け合わせる色です。大抵はテクスチャーの色をそのまま使用することが多いので白を指定します。

他に、描画するのに別メソッドとして「Sprite.Draw2D」があります。こちらは回転やサイズ変更などを一度に行えるので便利ですが、Z 値は直接指定出来ません。

Sprite.Draw2D メソッド

バッチ スプライトのリストにスプライトを追加します
srcTexture 描画する際に使用するテクスチャーを指定
srcRectangle 画像の転送矩形です。「Rectangle.Empty」を指定すると画像全体が描画されます。もし、画像に複数のパターンをまとめている場合は、この引数で描画したい範囲を決めます。
destinationRectangle 描画先の範囲です。転送元の画像サイズと違う場合は、拡大縮小されます。
rotationCenter

スプライトの回転の中心を示す位置です。Sprite.Draw と同様、スプライトの位置も変化します。

rotationAngle スプライトの回転の中心に対する回転角度です。単位は Radian で右回転です。
position

スプライトの描画位置です。

color テクスチャーの色と掛け合わせる色です。

補足しておきますが、スプライトが描画されるのは「Sprite.Draw」「Sprite.Draw2D」を行ったときではなく、「Sprite.End」「Sprite.Flush」のときです。このことを認識しておけば、描画順などの対処がしやすくなると思います。


// スプライトの解放
if (this._sprite != null)
{
    this._sprite.Dispose();
}

// テクスチャーの解放
if (this._texture != null)
{
    this._texture.Dispose();
}

最後にリソースを破棄しています。