15.Dx12、Dx11共有衝突判定など

今回は、衝突判定や補完処理などの説明をします。
「TransHelper.h」には「Vector3やMatric4X4」を使った、主にUpdate系で使用される関数や構造体などが含まれます。
これらは「フルバージョン」におけるコンポーネントの基本にあるアルゴリズム群ですが、「シンプルバージョン」からも使えるように「DxLib」に収められています。
ですから「シンプルバージョン」で制作を進める場合でも、これらの関数群は使用できます。「フルバージョン」では結果的にこれらのアルゴリズムを使用する形になりますが、「シンプルバージョン」では「使っても使わなくてもよい」関数群となります。

この記事は、
コミット「Dx12版のウインドウ作成」
から、
コミット「Dx11版ウインドウ作成と共有ファイルの追加」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。

上記コミット間に環境を整備するのが大変な人は、最新のコミットでも今回の記事はほとんど違いはありませんので、最新のzipファイルをダウンロードし「DxLib」プロジェクトの「TransHelper.h」を参照ください。

まず最初に出てくる「Lerp構造体」は「線形補間」を計算する構造体です。「1次補間(直線)」「2次補間(EaseinあるいはEaseOut)」「3次補間(EaseinのあとEaseOut)」「コサイン(3次補間に似てるけどコサインカーブを使う)」を使用できます。
使用方法は以下のようです

    Vector3 StartVec(0,0,0);
    Vector3 EndVec(5.0f,0,5.0f);
    Vector3 NowVec = Lerp::CalculateLerp(StartVec, EndVec, 0, 1.0f, 0.1f, Lerp::Easein);

これで、「StartVec」から「EndVec」までの間の「0.0f」から「1.0f」に区切った場合の「0.1f」の割合で「Easein(2次補間)」したVector3型の変数が返ります。
Lerp::CalculateLerpはテンプレート関数なので

    Vector3 NowVec = Lerp::CalculateLerp<Vector3>(StartVec, EndVec, 0, 1.0f, 0.1f, Lerp::Easein);

と書くのが正確ですが、形はコンパイラが推測できるので、省略できます。

続く「TransHelper.h」内のコードはすべて衝突判定用のコードになります。
まず初めにお断りしたいのは、これらのコードは「ゲームプログラミングのためのリアルタイム衝突判定(以降、RCDと略す)」(ChristerEricson著:中村達也訳:(株)ボーンデジタル発行:敬称略)の内容を、BaseCrossの環境(Vector3とかMatrix4X4とか)に合わせて書き直したものがほとんどです。
この本がなければ、僕のような数学の素人が「衝突判定アルゴリズム」など書けるはずもなく、結果として、BaseCrossのような(また、前バージョンのDxBase2016、DxBase2015)フレームワークを記述できるはずもありませんでした。ここにこの本に関係された諸氏に感謝の気持ちをお伝えしたいと思います。

衝突判定の基本
衝突判定を実装するには「境界ボリューム」という概念を知る必要があります。
「境界ボリューム」とは、プリミティブな形状を基本とした、オブジェクトの形状です。一番簡単な形状に「球」があります。そして、その「球」をスウィープ(引き延ばすとか流すといった感じ)した形状に「カプセル」があります。
また、それとは別に、「直方体」があります。「直方体」にはXYZ軸に平行な形状として「AABB」があります。回転が加わり、軸とは関係ない形状に「OBB」があります。
このほかに「面」や「三角形」も境界ボリュームかもしれません。「面」「三角形」は表側、裏側という考え方が重要となります。 「BaseCross」では、それらの境界ボリュームのすべてに衝突判定が実装されているわけではありません。
「球」「カプセル」「直方体」に関してはある一定の関数群はそろってます。「直方体」はBaseCrossは、AABBは用意はされてますが、「AABBはOBBの一部」という考え方をしています。この考えには異論があるかもしれませんが、プロセッサの速度が十分に速く、またSSE2命令も実装されている中で、「AABB」という限定的な「OBB」に対応することが必ずしも正しいとは考えてません。
その代り、AABBであっても基本的な判定は用意されていますので、それらを使ってそのゲーム特有の衝突判定を実装するのは「フルバージョンであっても」可能です。またAABBは、「衝突判定する者同士」を判別する、「前処理的な」判定には十分に威力を発揮します。
BaseCrossの前のバージョンのサンプルは「衝突判定」の前処理(つまり空間分割など)は実装されておりません。しかし、ゲームフィールドが大きなものであったり、配置されるオブジェクトの数が比較的多い場合は、衝突判定をする前に、分割された空間に合わせ「自分に近い」オブジェクト同士の判定することが有効です。これはAABBは威力を発揮します。フィールドをいくつかの分割されたAABBを置き、各オブジェクトをそのAABBと関連づけて実装します。そして自分が位置する空間と、その空間に隣接する空間内のオブジエクトだけを衝突判定します。
その考え方(空間分割)をライブラリ側で用意しないわけは、例えば、どんなに大きいフィールドであっても、そのほぼ全体を網羅する巨大なOBBやカプセルがあった場合、空間分割はほぼ意味をなしません。
そういうオブジェクトを「作らない」かあるいはそういうオブジェクトは特別として「一般的な衝突判定から外すか」などの判断は各ゲーム側で行われるものであり、フレームワークの「できる限りの抽象性と一般化」を目指す考え方と相反します。(なんて言い訳書いてますが、ようは空間分割は実装してなので各自考えてね、ってことです)
すこし前置きが長くなりましたが、それではボリューム境界の説明から入ります。

CollisionVolume構造体
これは境界ボリューム各構造体の親クラスです。この親クラスが必要かどうか、異論もあるでしょうが、一応、境界ボリュームの配列を作ったりする場合に便利かと思い実装してます。フレームワーク内でこのクラスを使って何かするコードは現時点ではありません。それと並んでデストラクタを「仮想デストラクタ」にしている理由も、明確ではありません。通常のデストラクタで充分かもしれません(派生クラスでポインタを保持するとも思えないので)。ただ、将来的な部分も考え「仮想デストラクタ」となっています。

OBB構造体
この構造はもちろん「RCD」の記述をBaseCross用に修正した構造です。
中心点、各軸の傾き、サイズ(ただし各軸に対しての半分)、がOBBの形成要素ですが、実はこれって4X4行列とすごく似てるデータです。
ただ4X4行列をそのままデータ化した場合、位置やスケーリングが行列内に入り込んでいるために取り出すのにひと手間かかるのです。それを避けるために別データとしているようです。(これはすごく正しいと思います)
ですからOBBの初期化は4X4行列(特にワールド行列)と相性がすごくいいです。そのまま使えないのは、作成時のXYZ方向のサイズです。通常は各軸に1.0の大きさの立方体として作成し、ワールド変換します。
ですので、OBBのコンストラクタのうち

    OBB(const Vector3& Size,const Matrix4X4& Matrix)

というコンストラクタは、OBBを作るときに非常に便利なので覚えておくといいでしょう。
OBB構造体にはいくつかメンバ関数があります。CreateOBB関数やGetRotMatrix関数は有用かと思いますが、GetNearNormalRot関数はちょっと実験的に入ってます。与えられたベクトルと一番近い傾きを返す関数ですが、衝突した時に反射に使える法線とチェックするのにいいかなと思ったのですが、OBBには「角」もあるので、もう少し修正が必要かと思ってます。

AABB構造体
OBBのうちXYZ軸に平行な直方体がAABBです。OBBに比べ形状が単純なので、衝突判定もスピードが速いです。
ただ、BaseCrossフルバージョンには実装されませんので、使用したい場合は自分で衝突判定関数(これは用意してあります)を使って実装してください。
空間分割には便利かと思います

SPHERE構造体
球の境界ボリュームです。衝突判定的には一番単純な形状ですので、SPHEREで事足りる場合、例えば攻撃砲とプレイヤーや敵の衝突などを判定したい場合は、この形状で充分な場合が多いです。
構造的には中心点と半径しか持ってません。動的に作成するのも簡単ですし(OBBに比べると)、いろいろ使い道は多いと思います。

PLANE構造体
平面です。衝突判定関数はそんなに実装されてません(この辺りは今後の課題かと思います)。ただゲーム側で実装するのはそんなに大変ではないので、例えばでこぼこした床などとプレイヤーの判定などに使えるでしょう。

CAPSULE構造体
カプセルです。カプセルを表現するのは、上部の半球の中心と下部の半球の中心と、あと両半球の半径です。たしかにこれで用足りますただ、カプセルの中心はこの中にありません。ここがSPHEREやOBBと違うところです。
ですので、メンバ変数として、GetCenter関数と、SetCenter関数があります。
また、カプセルの縦方向の半径がわかると、このカプセルを包み込む「球(SPHERE)」を考えることができます。これがわかれば、衝突判定の前処理として、カプセルを球に見立てて判定することにより、最初からカプセルとして判定するより効率期です。ですので縦半径を求めるGetHeightRadius関数があります。

以上でフレームワークが用意するボリューム境界は全部です。ただ、「RCD」にはもっといろんな形状の考察が記述されています。例えば「多面体」とか「三角形」とかです。「直線」に関しても、フレームワークにあるもの以外にもいろんな考察が乗ってます。今後これらの形状に対応するかどうかは優先順位の問題ですが、できるだけ実装を増やしていきたいと思います。
学生の皆さんも、ぜひ、機会があったら「RCD」を手に取って、衝突判定の仕組みを勉強するといいと思います。

さて、以上が境界ボリュームの説明でしたが、これらの衝突を判定する関数群は、そのあとの「HitTest構造体」に含まれます。この構造体は形こそ構造体ですが、static関数(ユーティリティ関数)の集合体です。ですのでメンバ変数は持ちません。
考え方として、この構造体は「Vector3EX namespace」のように「namespace」で記述も可能です。
ただそうした場合、すべての関数をインラインにするわけにはいかない(再帰関数がある)ために、構造体内のstatic関数としています。この場合、cppファイルを記述することでnamespace化できますが、まあ、大きな必要も感じない(構造体内にstatic関数を作って、ユーティリティ的に使う手法は結構みんなやってると思います。手軽にスコープを設定できるので便利ですよね)。

さて、「HitTest構造体」です。いろんな関数がありますが、まず知ってほしいのは「SPHERE_SPHERE関数」や「SPHERE_OBB関数」など、「半角大文字アンダーバー半角大文字」の組み合わせの関数です。
これは単純な衝突判定で、境界ボリューム同士の判定を行います。戻り値はboolで関数によっては「最近接点」をout引数に戻します。例をあげます

    //OBBを得る何らかの関数
    OBB obb = GetObb();
    //SPHEREを得る何らかの関数
    SPHERE sp = GetSphere();
    //最近接点が入るベクトル
    Vector3 Ret;
    if(HitTest::SPHERE_OBB(sp,obb,Ret)){
        //衝突している
        //Retには最近接点が入っているから利用できる
    }
    else{
        //衝突してなくてもRetには最近接点が入っているので利用できる
    }

こんな感じで使います。ただ「SPHERE_SPHERE関数」のように最近接点が返らない場合もありますが、球同士なので中心同士を結んでその中点をとる。あるいは、引き算して、どちらかの半径文の長さをを足したりすることで、お互いの最近接点は得られるのでそのようにやりくりが必要な場合もあります。

これらの、単純な衝突判定は「現在衝突しているか」を計算するものです。しかし、ゲームは、約60分の1秒おきにUpdateの機会が与えられるアニメーションと考えることができます。現実の世界では時間はどんなに小さく刻んでもその一瞬一瞬は必ず存在します。しかしゲームやアニメーションは「飛び飛び」なのです。
その結果として、1つ前のターンから現在のターンの間に、本当なら衝突すべきオブジェクト同士が「すり抜ける」という事態が発生します。これを「トンネル現象」といいます。
「トンネル現象」を防ぐ方法は以下のような感じです。
まず、以下の図を見てください。この図は、オブジェクトの移動を図解したものです。

2016080701

これを、移動とは考えずに、移動範囲を全て網羅する境界ボリュームと考えたらどうなるでしょう?
その観念図が以下になります。

2016080702

ブルーの球体が移動範囲を全て網羅する境界ボリュームです。このブルーの球体とボックスの衝突がなければ移動中の衝突はないということになります。
では衝突した場合はどうなるでしょう。以下のような状態になります。

2016080703

移動距離を直径としたボリューム境界を作成することで、前回のターンから今回のターンの間のどのタイミングかで衝突しているのがわかります。では、どのタイミングで(例えば、0.005秒後など)衝突したかを得ることはできないでしょうか?
その計算方法を表したのが下の図です。いらない床などは外してます。

2016080704

この図では、まず、1ターン分の移動距離を直径とした球体を作成し、それとOBB(ここでは四角い物体)の判定を行います。ここで衝突してなければ判定は終了です。衝突していれば、いまの球体を前半分と後半分を直径とした球体に分けそれぞれ判定します。もし後ろ半分で衝突していれば、さらに半分に分けて衝突判定します。
このように、球体を小分けにしていって限界までに球体が小さくなったとき、が衝突した瞬間(正確には衝突する直前)、ということになります。これを再帰的に検証します。
1ターンの移動距離を直径とした球体、をもとに小分けにするので、最終的な小さな球体の位置は、前回のターンから衝突までの時間、を出すことができます。

そんな形で判定するのが「CollisionTestSphereSphere関数」「CollisionTestSphereCapsule関数」「CollisionTestCapsuleCapsule関数」「CollisionTestSphereAabb関数」「CollisionTestSphereObb関数」「CollisionTestCapsuleObb関数」「CollisionTestObbObb関数」です。名前から推測できる通り、「SPHEREとSPHERE」「SPHEREとCAPSULE」「SPHEREとOBB」「CAPSULEとCAPSULE」「CAPSULEとOBB」「OBBとOBB」は関数があります。AABBについては「SPHEREとAABB」のみあります。これは言ってみればライブラリの不備なのですが、AABBについてはこれでいいかなとは思ってます。なお必要であれば「CAPSULEとAABB」くらいかなと思います。
これらの関数の引数は例えば「CollisionTestSphereObb関数」を例にとりますと

static bool CollisionTestSphereObb(
        const SPHERE& SrcSp,            //動くSPHERE
        const Vector3& SrcVelocity,     //SPHEREの速度(秒速)
        const OBB& DestObb,             //動かないOBB
        float StartTime,                //開始時刻(通常0を渡す)
        float EndTime,                  //終了時刻(通常ターン間の時間を渡す)
        float& HitTime                  //ヒット直前の時刻(戻り値)
        )

となります。戻り値は、衝突すればtrueです。衝突した場合、HitTimeに開始時刻から終了時刻の間の、衝突する直前の時刻が入ります。上記のように通常は、開始時刻は0を、終了時刻は前回のターンからの経過時間を渡します。
これを見て「あれ、OBBは動かせないの?」と思う人もいるでしょう。でもよく見てください。この関数の戻り値で意味があるのは「衝突したかしないか」と「衝突した場合の時刻」です。
SPHEREもOBBも両方動いていたとすれば、SPHEREの速度からOBBの速度を引いてあげればその差は「相対速度」になります。つまり、動かないOBBから見た動くSPHEREの速度を作り出すことができるのです。
ここの戻り値が「衝突する直前のSPHEREの位置」などではなく、「時刻」を返すところに意味が出てくるのです。「どの位置で当たるのか」ではなく「何秒後に当たるのか」がわかれば、それぞれが別々の速度を持っていたとしても、それぞれの速度をそれぞれに適用すれば、当たる直前のそれぞれの位置は計算できます。
もちろんこの関数も「RCD」に記述があります。「RCD」には、「間隔等分法」という名前で紹介されています。掲載されているのは「SPHEREとSPHERE」と、完全に一般化された「境界ボリュームと境界ボリューム」です。
本当は一般化されたものを参考にテンプレート、あるいは「CollisionVolume(親クラス)」で原始的な判定を仮想関数化するなどして実装すればもっと広い範囲で使えるのですが、現時点ではそこまで行ってません。ホント自分の未熟さにがっかりします。一般化は将来のバージョンへの宿題ということで、ご理解のほどお願いします。

さて、「HitTest構造体」にはこれまで紹介した「衝突判定そのもの」ではなく「判定に利用された関数」あるいは「あると便利かなと思われる関数」がいくつか実装されています。
「InsidePtPlane関数」は、点が面の裏側にあるかどうか判定します。「ClosestPtSegmentSegment関数」は線分同士の最近接点を求めます。「ClosetPtPointSegment関数」は点と線分の最近接点を返します。「ClosestPtPointOBB関数」は点とOBBの最近接点を返します。「ClosestPtPointAABB関数」は点とAABBの最近接点を返します。それと「AABB_IN_AABB関数」はあるAABBがもう一つのAABBの中に納まっているか調べます。この関数などは、空間分割と分割されたAABBにオブジェクトが収まっているかどうかの判別に利用できそうです。(AABB_IN_AABB関数、はソースを見ると実に単純なので各自、自作したほうがよさそうですが・・・)。

これで、衝突判定の紹介は終了です。
これらの衝突判定の仕組みは、シンプルバージョンでは、ゲームプログラマが用途に合わせて直接利用できます(利用しなくてもいいです)。フルバージョンの場合は「衝突判定コンポーネント」に隠される形で実装されます。(あるいは直接利用も可能です)

さて、最後に、これまで「DxLib」に記述したソースをインクルードする必要があります。
構成上、ライブラリのインクルードは、共有プロジェクト「DxLib」内に記述するのではなく、個別のプロジェクト「Dx11Lib」および「Dx12Lib」内に記述します。
どちらかが先でも構いませんが、それぞれのプロジェクト内に「Common.h」を記述します。そして以下のように記述します。

#pragma once

//ユーティリティ基本クラス(削除テンプレート、例外処理など)
#include "../DxLib/BaseHelper.h"
//XML読み込み
#include "../DxLib/XmlDoc.h"
//ベクトル計算の計算クラス
#include "../DxLib/MathVector.h"
//行列、クオータニオン、カラーなどの計算クラス
#include "../DxLib/MathExt.h"
//ベクトルのスタティック計算
#include "../DxLib/MathVectorEX.h"
//行列、クオータニオン、カラーなどのスタティック計算
#include "../DxLib/MathExtEX.h"
//衝突判定、補間処理用ユーティリティ
#include "../DxLib/TransHelper.h"

 そして、今度はメインプロジェクト内の「stdafx.h」に以下のように追記します。一番下で結構です
まず、Dx12の場合

//中略
#include "../../Libs/BaseLib/Dx12Lib/Common.h"

そしてDx11の場合

//中略
#include "../../Libs/BaseLib/Dx11Lib/Common.h"

 上記二つの違いがわかりますか?「Dx12」の場合は「Dx12Lib/Common.h」をインクルードし「Dx11」の場合は「Dx11Lib/Common.h」をインクルードするわけです。
 細かい作業ですが、間違いないように設定しましょう。

今回の記事で、ようやく、コミット「Dx11版ウインドウ作成と共有ファイルの追加」まで実装されました。共有プロジェクト「DxLib」にはもう少しクラスが実装されますが、そのクラスは実装するときに紹介します。

次回から、次のコミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」に向けての解説です。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP