球と球の衝突判定
3Dゲームでよく使われる衝突判定のひとつです。計算がとても単純で非常に高速に処理できます。モデルの形状が球に近いほど正確な判定が行えます。
今回はティーポットとボックスで判定していますが、Xファイルから読み込んだメッシュでもそのまま置き換えることが可能です。
操作はティーポットを「←」「→」「↑」「↓」キーで「X」「Z」方向に移動できます。二つの球が衝突すると「当たっています!」の文字が表示されます。

下のリンクから今回のプロジェクトをダウンロードできます。
今回のメインコードファイルを載せます。重要なコードを赤色で表示させています。
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></returns>
/// <remarks>
///
/// </remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
this._form = topLevelForm;
this.CreateInputEvent(topLevelForm);
try
{
this.CreateDevice(topLevelForm);
this.CreateFont();
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;
}
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.DarkBlue, 1.0f, 0);
this._device.BeginScene();
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;
}
|
キーボードのカーソルキー「↑↓←→」でティーポットの位置を移動できるようにしています。
float meshLength = Vector3.Length(this._boxPosition - this._teapotPosition);
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);
}
|
当たり判定状態を文字で表示しています。
|