カウンタ

XNA

スクリーン上の位置からモデルを選択

ページ更新日:2010/12/19
概要

マウスカーソルの位置にあるモデルを選択できるようにしています。カーソルをモデルに合わせると、Hit テキストが True に変化します。

スクリーン上の位置からモデルを選択
動作環境
必須環境
対応 XNA バージョン
  • 4.0
対応プラットフォーム
  • Windows (XP SP2 以降, Vista, 7)
  • Xbox 360
  • Windows Phone 7
Windows 必須頂点シェーダ バージョン 2.0
Windows 必須ピクセルシェーダ バージョン 2.0
動作確認環境
プラットフォーム
  • Windows 7
  • Xbox 360
  • Windows Phone 7 エミュレーター
サンプルの操作方法
動作 キーボード Xbox 360 コントローラー マウス タッチ
カーソル移動 ↑↓←→ 左スティック マウス移動 -
内容

スクリーン座標から3次元空間座標へ変換

マウスなどで3次元空間上のモデルなどを選択したい場合があると思います。この場合は、スクリーン上の2次元座標点からモデルの存在する3次元座標に変換してあたり判定などを行う必要があります。

しかし、2次元から3次元に要素を拡張するため、X,Y のみの2次元のスクリーン座標から、3次元座標の点を求めることはできません。たとえば、実際に画面をクリックするイメージを考えてもらうとわかるかと思いますが、クリックしたときの3次元空間の位置が、オブジェクトよりも手前の位置なのかオブジェクト自体なのか、はたまたオブジェクトの奥なのかを判定することはできません。

オブジェクトの選択対象の判断

そのため、クリックした位置は点で表すのではなく、カメラの位置からクリックした方向に伸ばしたとして扱います。その線とオブジェクトとの衝突判定を行うことによってモデルの選択などができるようになります。ちなみに線のパラメータは XNA では Ray という構造体で扱うことができます。

スクリーンの位置から3次元空間の位置を取得

XNA にはスクリーンでクリックした方向へ向かう線を求めるようなメソッドなどはありません。しかし、スクリーン座標と奥行きを指定することで3次元空間の点を求めることができるので、カメラの位置と特定の深度で変換した3次元空間座標点を結ぶことによって線を求めることができます。

スクリーン空間座標からオブジェクト空間座標を求めるには「Viewport.Unproject」メソッドを使用することで簡単に求めることができます。

// ビューポートを取得
Viewport viewport = this.GraphicsDevice.Viewport;

// スクリーンの位置を Vector3 で作成 (Z は 1.0 以下を設定)
Vector3 screenPosition = new Vector3(this.markPosition, 1.0f);

// スクリーン座標を3次元座標に変換
Vector3 worldPoint = viewport.Unproject(screenPosition,
                                        this.projection,
                                        this.view,
                                        Matrix.Identity);

第1引数にはスクリーン座標に深度を含めた Vector3 を渡します。X, Y にはスクリーンの座標を設定し、Z に深度の値を設定します。深度はプロジェクションマトリックスの「nearPlaneDistance」と「farPlaneDistance」のパラメータに依存し、0.0f を指定するとカメラの位置から nearPlaneDistance の距離、1.0f を指定するとカメラの位置から farPlaneDistance の距離を求めることができます。

第2引数にはプロジェクションマトリックス、第3引数にはビューマトリックスを指定します。

戻り値としてオブジェクト空間ベクトルを求めることができます。

Viewport.Unproject メソッド
スクリーン空間からオブジェクト空間にベクトルを射影します。
source Vector3 オブジェクト空間座標に変換するためのスクリーン座標ベクトル
projection Matrix 射影行列
view Matrix ビュー行列
world Matrix 最後に行うワールド行列座標変換を指定します
戻り値 Vector3 オブジェクト空間のベクトルを取得します
MSDN ライブラリの Web ページへリンク

レイの作成

線のパラメータには Ray 構造体が使用できます。コンストラクタの第1引数にレイの開始点を指定し、第2引数にレイの向きを指定します。

開始点にカメラの位置を設定し、向きに変換済みの3次元空間座標からカメラの位置を引いて向きを算出します。向きは Vector3.Normalize メソッドで単位ベクトルにしています。

// マークが指す方向へのレイを作成
Ray ray = new Ray(this.cameraPosition,
                    Vector3.Normalize(worldPoint - this.cameraPosition));
Ray コンストラクタ
線のパラメータを格納する「Ray」構造体のインスタンスを作成します。
position Vector3 レイの開始点
direction Vector3 レイの方向
MSDN ライブラリの Web ページへリンク

球とレイの当たり判定

コンテンツパイプラインから読み込んだ ModelMesh クラスには BoundingSphere プロパティというメッシュを包括した球データが含まれています。このクラスの Intersects メソッドに先ほど作成した Ray を指定することによって球とレイが衝突しているかを調べることができます。

衝突する場合はレイの開始点と衝突ポイントの距離が返ります。衝突しない場合は null が返るので、サンプルでは null 判定で衝突しているか調べています。

ただし、このメソッドを使用する場合はモデルが原点に配置されていることが前提です。もし、モデルを移動させている場合は、レイをモデルの移動に合わせて座標変換する必要があります。

ちなみに今回のサンプルモデルは球なので、正確なあたり判定ができると思います。

// 球とレイとの当たり判定を行う
this.isHit = false;
foreach (ModelMesh mesh in this.model.Meshes)
{
    if (mesh.BoundingSphere.Intersects(ray) != null)
    {
        // 球とレイは交差している
        this.isHit = true;
        break;
    }
}
BoundingSphere.Intersects メソッド
包括球とレイの衝突判定を行います。
ray Ray 球と衝突判定させるレイ
戻り値 Nullable<float> 衝突する場合、レイの開始点と球との衝突ポイントの距離を返します。衝突しない場合は null が返ります。
MSDN ライブラリの Web ページへリンク
プロジェクト ダウンロード
ファイル サイズ 対応XNAバージョン プラットフォーム 作成日
xna_tips_modelselectbyscreenposition_4_0_project.zip 208 KB 4.0 Windows (XP SP2 以降, Vista, 7), Xbox 360, Windows Phone 7 2010/12/19
xna_tips_modelselectbyscreenposition_3_0_project.zip 47 KB 3.0 Windows (XP SP2 以降, Vista), Xbox 360 2009/01/04
xna_tips_modelselectbyscreenposition_2_0_project.zip 46 KB 2.0 Windows (XP SP2, Vista), Xbox 360 2008/08/09

※ Ver 4.0 のパッケージには実行ファイルも含まれています。(Windows 版のみ)

サンプル実行ファイル (Windows のみ)
ファイル サイズ 対応XNAバージョン プラットフォーム 作成日
xna_tips_modelselectbyscreenposition_3_0_exe.zip 21.9 KB 3.0 Windows (XP SP2 以降, Vista) 2009/01/04
xna_tips_modelselectbyscreenposition_2_0_exe.zip 25.9 KB 2.0 Windows (XP SP2, Vista) 2008/08/09
全コード
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
#if WINDOWS_PHONE
using Microsoft.Xna.Framework.Input.Touch;
#endif

namespace ModelSelectByScreenPosition
{
    /// <summary>
    /// ゲームメインクラス
    /// </summary>
    public class GameMain : Microsoft.Xna.Framework.Game
    {
        /// <summary>
        /// グラフィックデバイス管理クラス
        /// </summary>
        private GraphicsDeviceManager graphics = null;

        /// <summary>
        /// スプライトのバッチ化クラス
        /// </summary>
        private SpriteBatch spriteBatch = null;

        /// <summary>
        /// スプライトでテキストを描画するためのフォント
        /// </summary>
        private SpriteFont font = null;

        /// <summary>
        /// モデル
        /// </summary>
        private Model model = null;

        /// <summary>
        /// マーク
        /// </summary>
        private Texture2D mark = null;

        /// <summary>
        /// マーク画像の中心位置
        /// </summary>
        private Vector2 markCenterPosition = Vector2.Zero;

        /// <summary>
        /// マークの位置
        /// </summary>
        private Vector2 markPosition = new Vector2(100.0f, 100.0f);

        /// <summary>
        /// モデルへの当たり判定フラグ
        /// </summary>
        private bool isHit = false;

        /// <summary>
        /// カメラの位置
        /// </summary>
        private Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 10.0f);

        /// <summary>
        /// ビューマトリックス
        /// </summary>
        private Matrix view;

        /// <summary>
        /// プロジェクションマトリックス
        /// </summary>
        private Matrix projection;


        /// <summary>
        /// GameMain コンストラクタ
        /// </summary>
        public GameMain()
        {
            // グラフィックデバイス管理クラスの作成
            this.graphics = new GraphicsDeviceManager(this);

            // ゲームコンテンツのルートディレクトリを設定
            this.Content.RootDirectory = "Content";

#if WINDOWS_PHONE
            // Windows Phone のデフォルトのフレームレートは 30 FPS
            this.TargetElapsedTime = TimeSpan.FromTicks(333333);

            // バックバッファサイズの設定
            this.graphics.PreferredBackBufferWidth = 480;
            this.graphics.PreferredBackBufferHeight = 800;

            // フルスクリーン表示
            this.graphics.IsFullScreen = true;
#endif
        }

        /// <summary>
        /// ゲームが始まる前の初期化処理を行うメソッド
        /// グラフィック以外のデータの読み込み、コンポーネントの初期化を行う
        /// </summary>
        protected override void Initialize()
        {
            // ビューマトリックス
            this.view = Matrix.CreateLookAt(
                        this.cameraPosition,
                        Vector3.Zero,
                        Vector3.Up
                    );

            // プロジェクションマトリックス
            this.projection = Matrix.CreatePerspectiveFieldOfView(
                        MathHelper.ToRadians(45.0f),
                        (float)this.GraphicsDevice.Viewport.Width /
                            (float)this.GraphicsDevice.Viewport.Height,
                        1.0f,
                        100.0f
                    );

            // コンポーネントの初期化などを行います
            base.Initialize();
        }

        /// <summary>
        /// ゲームが始まるときに一回だけ呼ばれ
        /// すべてのゲームコンテンツを読み込みます
        /// </summary>
        protected override void LoadContent()
        {
            // テクスチャーを描画するためのスプライトバッチクラスを作成します
            this.spriteBatch = new SpriteBatch(this.GraphicsDevice);

            // フォントをコンテンツパイプラインから読み込む
            this.font = this.Content.Load<SpriteFont>("Font");

            // モデルを作成
            this.model = this.Content.Load<Model>("Model");

            // ライトとビュー、プロジェクションはあらかじめ設定しておく
            foreach (ModelMesh mesh in this.model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    // デフォルトのライト適用
                    effect.EnableDefaultLighting();

                    // ビューマトリックスをあらかじめ設定
                    effect.View = this.view;

                    // プロジェクションマトリックスをあらかじめ設定
                    effect.Projection = this.projection;
                }
            }

            // マーク作成
            this.mark = this.Content.Load<Texture2D>("Mark");

            // マークの中心位置
            this.markCenterPosition = new Vector2(this.mark.Width / 2, this.mark.Height / 2);
        }

        /// <summary>
        /// ゲームが終了するときに一回だけ呼ばれ
        /// すべてのゲームコンテンツをアンロードします
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: ContentManager で管理されていないコンテンツを
            //       ここでアンロードしてください
        }

        /// <summary>
        /// 描画以外のデータ更新等の処理を行うメソッド
        /// 主に入力処理、衝突判定などの物理計算、オーディオの再生など
        /// </summary>
        /// <param name="gameTime">このメソッドが呼ばれたときのゲーム時間</param>
        protected override void Update(GameTime gameTime)
        {
            // キーボードの情報取得
            KeyboardState keyboardState = Keyboard.GetState();

            // ゲームパッドの情報取得
            GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);

            // Xbox 360 コントローラ、Windows Phone の BACK ボタンを押したときに
            // ゲームを終了させます
            if (gamePadState.Buttons.Back == ButtonState.Pressed)
            {
                this.Exit();
            }

            // 移動スピード
            float speed = 200.0f;

            // キーボードによるマークの移動
            if (keyboardState.IsKeyDown(Keys.Left))
            {
                this.markPosition.X -= speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }
            if (keyboardState.IsKeyDown(Keys.Right))
            {
                this.markPosition.X += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }
            if (keyboardState.IsKeyDown(Keys.Up))
            {
                this.markPosition.Y -= speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }
            if (keyboardState.IsKeyDown(Keys.Down))
            {
                this.markPosition.Y += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }

            // ゲームパッドによるマークの移動
            if (gamePadState.IsConnected)
            {
                this.markPosition.X += gamePadState.ThumbSticks.Left.X * speed *
                                       (float)gameTime.ElapsedGameTime.TotalSeconds;
                this.markPosition.Y -= gamePadState.ThumbSticks.Left.Y * speed *
                                       (float)gameTime.ElapsedGameTime.TotalSeconds;
            }

            // マウス処理
            MouseState mouseState = Mouse.GetState();

            if (mouseState.X >= 0 && mouseState.X < this.Window.ClientBounds.Width &&
                mouseState.Y >= 0 && mouseState.Y < this.Window.ClientBounds.Height &&
                mouseState.LeftButton == ButtonState.Pressed)
            {
                // マウスがウインドウ内にあればマウスの位置を優先する
                this.markPosition = new Vector2(mouseState.X, mouseState.Y);
            }

            // ビューポートを取得
            Viewport viewport = this.GraphicsDevice.Viewport;

            // スクリーンの位置を Vector3 で作成 (Z は 1.0 以下を設定)
            Vector3 screenPosition = new Vector3(this.markPosition, 1.0f);

            // スクリーン座標を3次元座標に変換
            Vector3 worldPoint = viewport.Unproject(screenPosition,
                                                    this.projection,
                                                    this.view,
                                                    Matrix.Identity);

            // マークが指す方向へのレイを作成
            Ray ray = new Ray(this.cameraPosition,
                              Vector3.Normalize(worldPoint - this.cameraPosition));

            // 球とレイとの当たり判定を行う
            this.isHit = false;
            foreach (ModelMesh mesh in this.model.Meshes)
            {
                if (mesh.BoundingSphere.Intersects(ray) != null)
                {
                    // 球とレイは交差している
                    this.isHit = true;
                    break;
                }
            }

            // 登録された GameComponent を更新する
            base.Update(gameTime);
        }

        /// <summary>
        /// 描画処理を行うメソッド
        /// </summary>
        /// <param name="gameTime">このメソッドが呼ばれたときのゲーム時間</param>
        protected override void Draw(GameTime gameTime)
        {
            // 画面を指定した色でクリアします
            this.GraphicsDevice.Clear(Color.CornflowerBlue);

            // Zバッファを有効にする
            this.GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            // モデルを描画
            foreach (ModelMesh mesh in this.model.Meshes)
            {
                mesh.Draw();
            }

            // スプライトの描画準備
            this.spriteBatch.Begin();

            // マーク描画
            this.spriteBatch.Draw(this.mark, this.markPosition,
                null, Color.White, 0.0f,
                this.markCenterPosition, 1.0f, SpriteEffects.None, 0.0f);

            // テキスト描画
            this.spriteBatch.DrawString(this.font,
                "Cursor Key Press or" + Environment.NewLine +
                "   MouseLeftButton Drag" + Environment.NewLine +
                "Hit : " + this.isHit,
                new Vector2(50.0f, 50.0f), Color.White);

            // スプライトの一括描画
            this.spriteBatch.End();

            // 登録された DrawableGameComponent を描画する
            base.Draw(gameTime);
        }
    }
}
更新履歴
更新日時 更新内容
2010/12/19 XNA Game Studio 4.0 用に修正
2009/01/04 XNA Game Studio 3.0 のプロジェクト追加
2008/08/09 ページ作成
広告
Copyright (C) since 2005 Yuichi Onodera (おのでら), All rights reserved.