カウンタ

  球と球の衝突判定

Google
▲探し物はこちら

 3Dゲームでよく使われる衝突判定のひとつです。計算がとても単純で非常に高速に処理できます。モデルの形状が球に近いほど正確な判定が行えます。
 今回はティーポットとボックスで判定していますが、Xファイルから読み込んだメッシュでもそのまま置き換えることが可能です。

 操作はティーポットを「←」「→」「↑」「↓」キーで「X」「Z」方向に移動できます。二つの球が衝突すると「当たっています!」の文字が表示されます

球と球の衝突判定

 下のリンクから今回のプロジェクトをダウンロードできます。

ファイル名 言語 サイズ バージョン
sphereandsphere_cs_1_1.zip C# 28KB 1.1
sphereandsphere_vb_1_1.zip VB.NET 34KB 1.1
sphereandsphere_cpp_1_1.zip C++/CLI 35KB 1.1

 今回のメインコードファイルを載せます。重要なコードを赤色で表示させています

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 _teapot = null;

        /// <summary>
        /// ティーポット当たり判定球の半径
        /// </summary>
        private float _teapotRadius = 0.0f;

        /// <summary>
        /// ティーポットの当たり判定球実体化用
        /// </summary>
        private Mesh _teapotSphere = null;

        /// <summary>
        /// ティーポットの位置
        /// </summary>
        private Vector3 _teapotPosition = Vector3.Empty;


        /// <summary>
        /// ボックス
        /// </summary>
        private Mesh _box = null;

        /// <summary>
        /// ボックス当たり判定球の半径
        /// </summary>
        private float _boxRadius = 0.0f;

        /// <summary>
        /// ボックスの当たり判定球実体化用
        /// </summary>
        private Mesh _boxSphere = null;

        /// <summary>
        /// ボックスの位置
        /// </summary>
        private Vector3 _boxPosition = new Vector3(-3.0f, 0.0f, -4.0f);


        /// <summary>
        /// 当たり判定フラグ
        /// </summary>
        private bool _hitFlag = false;


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

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


            // カメラの位置セット
            this.SetCameraPosition(15.0f, 270.0f, 80.0f);


            // ティーポット作成
            this._teapot = Mesh.Teapot(this._device);

            // ティーポットの包括球を計算するために頂点バッファ取得
            using (VertexBuffer vb = this._teapot.VertexBuffer)
            {
                Vector3 center;

                // 頂点バッファをロックしてストリーム取得
                using (GraphicsStream vertexData = vb.Lock(0, 0, LockFlags.None))
                {
                    // ���ィ��ポットの包括球を計算し、半��取得
                    this._teapotRadius = Geometry.ComputeBoundingSphere(vertexData,
                        this._teapot.NumberVertices, this._teapot.VertexFormat, out center);

                    vertexData.Close();
                }

                // 頂点ロック解除
                vb.Unlock();
            }

            // 見やすいように当たり判定球を作成
            this._teapotSphere = Mesh.Sphere(this._device, this._teapotRadius, 16, 16);


            // ボックス作成
            this._box = Mesh.Box(this._device, 2.0f, 2.0f, 2.0f);

            // ボックスの包括球を計算するために頂点バッファ取得
            using (VertexBuffer vb = this._box.VertexBuffer)
            {
                Vector3 center;

                // 頂点バッファをロックしてストリーム取得
                using (GraphicsStream vertexData = vb.Lock(0, 0, LockFlags.None))
                {
                    // ボックスの包括球を計算し、半径取得
                    this._boxRadius = Geometry.ComputeBoundingSphere(vertexData,
                        this._box.NumberVertices, this._box.VertexFormat, out center);

                    vertexData.Close();
                }

                // 頂点ロック解除
                vb.Unlock();
            }

            // 見やすいように当たり判定球を作成
            this._boxSphere = Mesh.Sphere(this._device, this._boxRadius, 16, 16);


            // アルファブレンディング方法を設定
            this._device.RenderState.SourceBlend = Blend.SourceAlpha;
            this._device.RenderState.DestinationBlend = Blend.InvSourceAlpha;


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

            return true;
        }

        /// <summary>
        /// デバイスがロストしたとき
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void device_DeviceLost(object sender, EventArgs e)
        {
        }
        /// <summary>
        /// デバイスがリセットしたとき
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void device_DeviceReset(object sender, EventArgs e)
        {
        }

        /// <summary>
        /// 更新処理
        /// </summary>
        public void Update()
        {
            // アプリケーションの終了操作
            if (this._keys[(int)Keys.Escape])
            {
                this._form.Close();
                return;
            }

            // カメラの設定
            this.SettingCamera();

            // ティーポットの移動
            if (this._keys[(int)Keys.Left])
            {
                this._teapotPosition.X -= 0.1f;
            }
            if (this._keys[(int)Keys.Right])
            {
                this._teapotPosition.X += 0.1f;
            }
            if (this._keys[(int)Keys.Down])
            {
                this._teapotPosition.Z -= 0.1f;
            }
            if (this._keys[(int)Keys.Up])
            {
                this._teapotPosition.Z += 0.1f;
            }


            // 2つのメッシュの距離を計算
            float meshLength = Vector3.Length(this._boxPosition - this._teapotPosition);

            // 2つのメッシュの距離が2つのメッシュの半径の合計より小さければ当たっている
            this._hitFlag = (meshLength < (this._teapotRadius + this._boxRadius));
        }

        /// <summary>
        /// 描画処理
        /// </summary>
        public void Draw()
        {
            // デバイスが使える状態か確認する
            if (!this.EnsureDevice())
            {
                return;
            }

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

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


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


            Material mtrl;


            // 不透明マテリアル使用
            mtrl = new Material();
            mtrl.Diffuse = Color.White;
            mtrl.Ambient = Color.FromArgb(255, 128, 128, 128);
            this._device.Material = mtrl;

            // アルファブレンディングを無効にする
            this._device.SetRenderState(RenderStates.AlphaBlendEnable, false);

            // ティーポット描画
            this._device.SetTransform(TransformType.World, Matrix.Translation(this._teapotPosition));
            this._teapot.DrawSubset(0);

            // ボックス描画
            this._device.SetTransform(TransformType.World, Matrix.Translation(this._boxPosition));
            this._box.DrawSubset(0);


            // 半透明マテリアル使用
            mtrl = new Material();
            mtrl.Diffuse = Color.FromArgb(128, 192, 192, 192);
            mtrl.Ambient = Color.FromArgb(128, 0, 0, 128);
            this._device.Material = mtrl;

            // アルファブレンディングを有効にする
            this._device.SetRenderState(RenderStates.AlphaBlendEnable, true);

            // ティーポット包括球描画
            this._device.SetTransform(TransformType.World, Matrix.Translation(this._teapotPosition));
            this._teapotSphere.DrawSubset(0);

            // ボックス包括球描画
            this._device.SetTransform(TransformType.World, Matrix.Translation(this._boxPosition));
            this._boxSphere.DrawSubset(0);


            // 文字列の描画
            this.RenderText();

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

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

        /// <summary>
        /// テキストの描画
        /// </summary>
        private void RenderText()
        {
            this.DrawText("[Escape]終了", 0, 0, Color.White);
            this.DrawText("θ:" + this._lensPosTheta, 0, 12, Color.White);
            this.DrawText("φ:" + this._lensPosPhi, 0, 24, Color.White);
            this.DrawText("Teapot半径 :" + this._teapotRadius.ToString(), 0, 36, Color.White);
            this.DrawText("Box 半径 :" + this._boxRadius.ToString(), 0, 48, Color.White);
            this.DrawText("2つの距離 :" +
                (Vector3.Length(this._teapotPosition - this._boxPosition)).ToString(),
                0, 60, Color.White);
            if (this._hitFlag)
            {
                this.DrawText("当たっています!", 0, 72, Color.Yellow);
            }
            else
            {
                this.DrawText("当たっていません。", 0, 72, Color.LightBlue);
            }
        }

        /// <summary>
        /// リソースの破棄をするために呼ばれる
        /// </summary>
        public void Dispose()
        {
            // 作成したメッシュの破棄
            this.SafeDispose(this._teapot);
            this.SafeDispose(this._teapotSphere);
            this.SafeDispose(this._box);
            this.SafeDispose(this._boxSphere);
            
            // リソースの破棄
            this.DisposeResource();
        }
    }
}

 では、赤文字の部分を説明していきます。そのほかのファイルのコードはこちらです。


/// <summary>
/// ティーポット
/// </summary>
private Mesh _teapot = null;

/// <summary>
/// ティーポット当たり判定球の半径
/// </summary>
private float _teapotRadius = 0.0f;

/// <summary>
/// ティーポットの当たり判定球実体化用
/// </summary>
private Mesh _teapotSphere = null;

/// <summary>
/// ティーポットの位置
/// </summary>
private Vector3 _teapotPosition = Vector3.Empty;

 操作対象のティーポットのメッシュを用意します。

 後のコードでモデルの「包括球」というものを計算しますが、その球の半径をフィールドとして持つようにします。

 実際には必要ないのですが、サンプルとして球が見えたほうが分かりやすいので球のメッシュも用意します。

 あとはティーポットを移動させるので、その位置情報を持ちます。


/// <summary>
/// ボックス
/// </summary>
private Mesh _box = null;

/// <summary>
/// ボックス当たり判定球の半径
/// </summary>
private float _boxRadius = 0.0f;

/// <summary>
/// ボックスの当たり判定球実体化用
/// </summary>
private Mesh _boxSphere = null;

/// <summary>
/// ボックスの位置
/// </summary>
private Vector3 _boxPosition = new Vector3(-3.0f, 0.0f, -4.0f);

 ティーポットと衝突させるオブジェクトとしてボックスを作成します。必要なデータはティーポットと同じです。
 ボックスは移動させないので、先に初期位置を指定しています。


/// <summary>
/// 当たり判定フラグ
/// </summary>
private bool _hitFlag = false;

 衝突しているかを文字で描画するために、衝突フラグを持っておきます。


// ティーポット作成
this._teapot = Mesh.Teapot(this._device);

 ティーポットメッシュを作成しています。Mesh クラスであれば、Xファイルから読み込んだモデルでもかまいません。


// ティーポットの包括球を計算するために頂点バッファ取得
using (VertexBuffer vb = this._teapot.VertexBuffer)
{
    Vector3 center;

    // 頂点バッファをロックしてストリーム取得
    using (GraphicsStream vertexData = vb.Lock(0, 0, LockFlags.None))
    {
        // ティーポットの包括球を計算し、半径取得
        this._teapotRadius = Geometry.ComputeBoundingSphere(vertexData,
            this._teapot.NumberVertices, this._teapot.VertexFormat, out center);

        vertexData.Close();
    }

    // 頂点ロック解除
    vb.Unlock();
}

 ここにメッシュの包括球というものを計算しますが、包括球とはメッシュの全ての頂点を含むことが出来る球のことです。

 これを計算するには、メッシュの頂点バッファを使用します。メッシュの頂点バッファは「Mesh.VertexBuffer」プロパティで取れるので、これを使用することにします。

 頂点バッファを取得したらロックを掛け、頂点データを受け取ります。「Geometry.ComputeBoundingSphere」メソッドで包括球の中心位置球の半径が得られるのでそれを取得します。
 渡すパラメータは「頂点データ」「頂点数」「頂点フォーマット」「取得用の境界球の座標の中心」です。最後の「中心座標」は今回使用しませんが、渡さないといけないので適当に宣言して使っています。
 使い終わった頂点バッファは必ずロック解除します。


// 見やすいように当たり判定球を作成
this._teapotSphere = Mesh.Sphere(this._device, this._teapotRadius, 16, 16);

 当たり判定球が見やすいように、取得した半径から球を作成しています。面の分割数は適当に指定してください。あんまり少ないとティーポットが球からはみ出ます。(ポリゴン数が少ないと正確な球を表現できないため。もちろん見た目だけなので計算上は問題ありません


// ボックス作成
this._box = Mesh.Box(this._device, 2.0f, 2.0f, 2.0f);

// ボックスの包括球を計算するために頂点バッファ取得
using (VertexBuffer vb = this._box.VertexBuffer)
{
    Vector3 center;

    // 頂点バッファをロックしてストリーム取得
    using (GraphicsStream vertexData = vb.Lock(0, 0, LockFlags.None))
    {
        // ボックスの包括球を計算し、半径取得
        this._boxRadius = Geometry.ComputeBoundingSphere(vertexData,
            this._box.NumberVertices, this._box.VertexFormat, out center);

        vertexData.Close();
    }

    // 頂点ロック解除
    vb.Unlock();
}

// 見やすいように当たり判定球を作成
this._boxSphere = Mesh.Sphere(this._device, this._boxRadius, 16, 16);

 ボックスに関���て���、ティ�����ポットと同じ要��で作成します。


// アルファブレンディング方法を設定
this._device.RenderState.SourceBlend = Blend.SourceAlpha;
this._device.RenderState.DestinationBlend = Blend.InvSourceAlpha;

 衝突判定用の球を半透明で表示するのであらかじめアルファブレンディング方法を決めておきます。


// ティーポットの移動
if (this._keys[(int)Keys.Left])
{
    this._teapotPosition.X -= 0.1f;
}
if (this._keys[(int)Keys.Right])
{
    this._teapotPosition.X += 0.1f;
}
if (this._keys[(int)Keys.Down])
{
    this._teapotPosition.Z -= 0.1f;
}
if (this._keys[(int)Keys.Up])
{
    this._teapotPosition.Z += 0.1f;
}

 キーボードのカーソルキー「↑↓←→」でティーポットの位置を移動できるようにしています。


// 2つのメッシュの距離を計算
float meshLength = Vector3.Length(this._boxPosition - this._teapotPosition);

// 2つのメッシュの距離が2つのメッシュの半径の合計より小さければ当たっている
this._hitFlag = (meshLength < (this._teapotRadius + this._boxRadius));

 さて、今回の Tips の目的でもある「球と球の衝突判定」です。

 やっていることはとても簡単で「2つのメッシュの包括球半径の和」と「2つのメッシュの中心座標の距離」を求めて、「2つのメッシュの中心座標の距離」が「2つのメッシュの包括球半径の和」よりも小さければあったっているということになります。文章だとちょっと分かりづらいかと思いますので、下の図を見ていただければ一目瞭然です。

衝突判定方法

 真ん中のパターンを当たりにするかしないかは各自決めてください。


Material mtrl;


// 不透明マテリアル使用
mtrl = new Material();
mtrl.Diffuse = Color.White;
mtrl.Ambient = Color.FromArgb(255, 128, 128, 128);
this._device.Material = mtrl;

// アルファブレンディングを無効にする
this._device.SetRenderState(RenderStates.AlphaBlendEnable, false);

// ティーポット描画
this._device.SetTransform(TransformType.World, Matrix.Translation(this._teapotPosition));
this._teapot.DrawSubset(0);

// ボックス描画
this._device.SetTransform(TransformType.World, Matrix.Translation(this._boxPosition));
this._box.DrawSubset(0);


// 半透明マテリアル使用
mtrl = new Material();
mtrl.Diffuse = Color.FromArgb(128, 192, 192, 192);
mtrl.Ambient = Color.FromArgb(128, 0, 0, 128);
this._device.Material = mtrl;

// アルファブレンディングを有効にする
this._device.SetRenderState(RenderStates.AlphaBlendEnable, true);

// ティーポット包括球描画
this._device.SetTransform(TransformType.World, Matrix.Translation(this._teapotPosition));
this._teapotSphere.DrawSubset(0);

// ボックス包括球描画
this._device.SetTransform(TransformType.World, Matrix.Translation(this._boxPosition));
this._boxSphere.DrawSubset(0);

 今回は描画中心の話しではないので、特に変わった部分はありません。
 「不透明のオブジェクト」と「半透明の球」をレンダリングするので、先に不透明のオブジェクトをレンダリングしてください。これは3Dプログラミングのお約束みたいなもので、逆の順序で描画すると、Zバッファの影響でオブジェクトが描画されないことがあるためです。


if (this._hitFlag)
{
    this.DrawText("当たっています!", 0, 72, Color.Yellow);
}
else
{
    this.DrawText("当たっていません。", 0, 72, Color.LightBlue);
}

 当たり判定状態を文字で表示しています。

その他の関連情報です▼