カメラの操作

Page updated :

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

キー入力が出来るようになったので、キーボードでカメラを操作してみたいと思います。

キーボードの「↑↓」でカメラが上下に回転し、「←→」で左右に回転します。カメラは原点を中心に回転するようになっています。

カメラの操作

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

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 float _lensPosTheta = 270.0f;

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


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

            // 四角形ポリゴン作成
            this.CreateSquarePolygon();

            // 射影変換を設定
            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._device.RenderState.Lighting = false;

            // カリングを無効にしてポリゴンの裏も描画する
            //this._device.RenderState.CullMode = Cull.None;

            return true;
        }

        /// <summary>
        /// メインループ処理
        /// </summary>
        public void MainLoop()
        {
            // キーによる移動
            if (this._keys[(int)Keys.Left])
            {
                // ←キーが押されている場合
                this._lensPosTheta -= 3.0f;
            }
            if (this._keys[(int)Keys.Right])
            {
                // →キーが押されている場合
                this._lensPosTheta += 3.0f;
            }
            if (this._keys[(int)Keys.Up])
            {
                // ↑キーが押されている場合
                this._lensPosPhi += 3.0f;
            }
            if (this._keys[(int)Keys.Down])
            {
                // ↓キーが押されている場合
                this._lensPosPhi -= 3.0f;
            }

            // φに関しては制限をつける
            if (this._lensPosPhi >= 90.0f)
            {
                this._lensPosPhi = 89.9999f;
            }
            else if (this._lensPosPhi <= -90.0f)
            {
                this._lensPosPhi = -89.9999f;
            }

            // レンズの位置を三次元極座標で変換
            float radius = 10.0f;
            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));


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

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


            // 四角形ポリゴン描画
            this.RenderSquarePolygon();

            // 文字列の描画
            this._font.DrawText(null, "[↑↓]カメラの上下回転  [←→]カメラの左右回転",
                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();
        }

        /// <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>
        /// キーのプレス判定
        /// </summary>
        private bool[] _keys = new bool[256];

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


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

        /// <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>
        /// 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 CreateSquarePolygon()
        {
            // 四角形ポリゴンを表示するための頂点バッファを作成
            this._vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionColored),
                4, this._device, Usage.None, CustomVertex.PositionColored.Format, Pool.Managed);

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

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

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

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

        /// <summary>
        /// カメラの設定
        /// </summary>
        private void SettingCamera()
        {
            // ビュー変換行列を設定
            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);
        }

        /// <summary>
        /// 四角形ポリゴン描画
        /// </summary>
        private void RenderSquarePolygon()
        {
            // 頂点バッファをデバイスのデータストリームにバインド
            this._device.SetStreamSource(0, this._vertexBuffer, 0);

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

            // レンダリング(描画)
            this._device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2);
        }
    }
}

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

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

カメラの動きですが、今回は物体(ポリゴン)を中心にしてカメラがその周りを周るようにしています。その計算として「三次元極座標」の公式を使用しています。この Tips では数学の公式を説明することを目的としているわけではないので詳しくは説明しませんが、後ほど実際に使用しているところで簡単に説明します。

その際に、現在カメラがどの位置にいるのかという情報を保持するために「θ」と「φ」というパラメータを持っておきます。とりあえずここでは、「θ」が左右移動「φ」が上下移動だと思っておいてかまいません。半径R に関しては固定にします。

θ �� 半径

カメラの初期位置は、-Z 方向(0.0, 0.0 -x)の位置に配置して、原点(0.0, 0.0, 0.0)を見ているという感じになります。


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

キー入力イベントに関しては別ファイルに移動しています。


// 四角形ポリゴン作成
this.CreateSquarePolygon();

ポリゴン作成も同じなのでまとめておきます。


// 射影変換を設定
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._device.RenderState.CullMode = Cull.None;

カリングについては最後に説明します。


// キーによる移動
if (this._keys[(int)Keys.Left])
{
    // ←キーが押されている場合
    this._lensPosTheta -= 3.0f;
}
if (this._keys[(int)Keys.Right])
{
    // →キーが押されている場合
    this._lensPosTheta += 3.0f;
}
if (this._keys[(int)Keys.Up])
{
    // ↑キーが押されている場合
    this._lensPosPhi += 3.0f;
}
if (this._keys[(int)Keys.Down])
{
    // ↓キーが押されている場合
    this._lensPosPhi -= 3.0f;
}

カメラの状態は毎フレーム変わる可能性があるので、メインループのメソッドで処理するようにします。

ここでキーの判定を行っていますが、カーソルキーのフラグが立っている場合、フィールドとして持っている「θ」と「φ」の値を増減しています。どのキーフラグが立っているかは、キーフラグ配列のインデックスに(this.keys[/ここのこと/])「System.Windows.Forms.Keys」列挙のいずれかを「int」でキャストして指定します。

値は「3.0f」と指定していますが、特に意味はありません。カメラの移動スピードなので、いろいろ変えてみるといいでしょう。


// φに関しては制限をつける
if (this._lensPosPhi >= 90.0f)
{
    this._lensPosPhi = 89.9999f;
}
else if (this._lensPosPhi <= -90.0f)
{
    this._lensPosPhi = -89.9999f;
}

ここでは「φ」の値を制限しています。これはカメラの性質上、φが90度を上回ったり、-90度を下回ったりすると、突然カメラの向きが反転するので、制限しています。90度ぴったりだと表示上でエラーが起こります。(強制終了するわけではありません。ポリゴンが見えなくなります。

回避する方法もありますが、少し面倒なので今回はそういう仕様でいきます。

φの角度制限


// レンズの位置を三次元極座標で変換
float radius = 10.0f;
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)));

さて、カメラの位置は「X」「Y」「Z」の位置に直さないといけないので、「R」「θ」「φ」から三次元極座標の公式で変換します。また、角度はすべて「Radian」で計算しなければならないので、あらかじめ「θ」「φ」を「Geometry.DegreeToRadian」メソッドで「Degree」から「Radian」に変換しています。

下がその公式になります。

X = R × cos(θ) × cos(φ)
Y = R × sin(φ)
Z = R × sin(θ) × cos(φ)

これに当てはめればカメラの位置ベクトルは求まります。


// ビュー変換行列を左手座標系ビュー行列で設定する
this._device.Transform.View = Matrix.LookAtLH(
    lensPosition, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f));

レンズの位置が求まれば後は今までと同じようにビュー変換行列を生成してセットするだけです。


// 四角形ポリゴン描画
this.RenderSquarePolygon(); 

四角形ポリゴンの描画もメソッドにまとめています。


さて、これでカメラが回転できるようになりましたが、まわしてみるとポリゴンの裏側が描画されていないことに気づくはずです。

ポリゴンの裏側は描画されない

これはカリングと言って、上記のようにポリゴンの裏側を描画しない��うになっています。例えば閉じた箱をイメージしてみると分かりますが、箱の内側は見えないのでわざわざ描画する必要がありません。これによって描画のコストをいくらか減らそうというのがカリングです。

どちらがポリゴンの表でどちらが裏かというのは、頂点のならびに関係しています。ディフォルトではポリゴンの頂点が右回りになっている面が表です。

ポリゴンの表と裏

どちらの面を描画するのか、または両面描画というのを設定することが出来ます。コードでコメントアウトしている部分がそうです。

// カリングを無効にしてポリゴンの裏も描画する
//this._device.RenderState.CullMode = Cull.None;

Device.RenderState.CullModeCull.None を指定すると両面描画できるようになります。