12.Update系の操作

1206.コリジョンの検証

 このサンプルはFullSample206というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 BaseCrossDx12.slnというソリューションを開くとDx12版が起動します。

 実行結果は以下のような画面が出ます。

 

図1206a

 

 この画面でAボタンを押すといつものようにジャンプします。
 Xボタンを押すと、砲弾に見立てた球体を発射します。この球体(砲弾)は際限なく発射します。デバッグ文字列にあるOBJ_COUNT:というところにある数は、現在実装されているゲームオブジェクト数です。砲弾を発射するたびにその数も増えます。しかし、砲弾がステージの外に出てしまって、一定の高さより下になると、更新や描画はしなくなります。
 そのように更新や描画をしないオブジェクトは、次にXボタンが押されたときに再利用されます。
 ステージの上に散乱した場合、砲弾はそのまま放置されますが、動かなくなって、一定の時間がたつと黒くなります。この状態をスリープといいます。スリープ状態になると衝突判定はしなくなります。しかし、別の砲弾がぶつかったり、プレイヤーに触れたりするとウェイクアップ(起きる)状態になり、また、衝突判定をするようになります。
 このように使いまわしスリープの機能により、ステージ上では、ある程度遅いマシンでも200個くらいはオブジェクトを配置することが可能となります。
 しかし、この200個くらいという制限も描画処理のコストの積み上げでそうなります。
 インスタンス描画を実装すれば、もっとたくさんのオブジェクトを配置できるでしょう。

コリジョンとは何か

 この項のテーマはコリジョン、つまり衝突判定です。
 BaseCross64には、2系統のコリジョンが実装されています。
 一つは物理処理(Rigidbidyコンポーネント)が行う判定であり、もう一つはこの項で紹介する、単純な判定です。
 単純とはいえ、オブジェクト同士がぶつかるということは、なかなか奥深い領域であり、よく考えて実装しないとオブジェクト同士が埋まったりしてうまくいきません。
 この項ではライブラリ内で行っている処理を解説するとともに、例えばコリジョンを完全に自作する場合のヒントになるような説明をしたいと思います。

衝突の仕組み

 3D環境で衝突を表現するにはボリューム境界という考え方が必要です。
 ボリューム境界というのは単純化された3Dの形状と考えてもらっていいと思います。BaseCross64では球体、カプセル、直方体の3つのボリューム境界を持っています。モデルなどの複雑な形状では各頂点で判定を行うとコストがかかりすぎます。そのためこのように単純化さえた形状で判定を行います。
 物理世界の場合もう少し複雑な形状も扱えます。
 まず大前提として、ステージのクリエイト時は各オブジェクトは衝突してないか、それぞれの表面で接している状態で始まります。以下のような状態で始まると思ってください。例としてこのドキュメントでは球体と直方体の衝突を考えます。

 

図1206b

 

 この状態から、以下の状態になります。横移動と、重力が実装されているとします。次のターンというのはBaseCross64の場合、約60分の1秒後です。

 

図1206c

 

 次のターンの状態では、球体は二つのボックスと衝突しています。しかしオブジェクトに埋まってしまってますので、以下のように修正しなければいけません。

 

図1206d

 

 埋まっている状態から、埋まっていない状態に持っていくことを拘束を解くといいます。
 それでは順序立てて説明します。

衝突まで

 衝突していない状態から衝突している状態を判定するには、以下の様な手順で行います。
 まず、移動する物体は、移動する前と移動後の大きな球と見立てます。

 

図1206e

 

 この場合、大きな球とボックスは衝突しています。
 次に、その球を半分にし、前半分と後半分のどちらで当たっているか検証します。そして後ろ半分で当たっていれば、さらに半分して、・・・と続けていって、衝突した瞬間を見つけ出します。
 以下の様な感じです

 

図1206f

 

 そして、今回の例の場合、以下の2つの状態(衝突した瞬間)が存在する形となります。

 

図1206g

 

 衝突した瞬間を導き出す低レベルの関数は、ライブラリ中DxLibにあるTransHelper.hに記述があります。
 HitTest::CollisionTestSphereObb()関数です。この関数で、図1206fのアルゴリズムを実装しています。
 これをコリジョンコンポーネントから呼び出すわけですが、それはSharedLib内の
CollisionSphere::CollisionTest(const shared_ptr<CollisionObb>& DestColl)関数になります。以下がその実体です。
void CollisionSphere::CollisionTest(const shared_ptr<CollisionObb>& DestColl) {
    if (!HitTest::AABB_AABB(GetSphere().GetWrappedAABB(), DestColl->GetObb().GetWrappedAABB())) {
        //現在のAABB同士が衝突してないなら、1つ前に衝突していても衝突無し
        return;
    }
    auto PtrTransform = GetGameObject()->GetComponent<Transform>();
    auto PtrDestTransform = DestColl->GetGameObject()->GetComponent<Transform>();
    bsm::Vec3 SrcVelocity = PtrTransform->GetVelocity();
    bsm::Vec3 DestVelocity = PtrDestTransform->GetVelocity();
    //前回のターンからの時間
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    //移動以外変化なし
    SPHERE SrcSphere = GetSphere();
    SPHERE SrcBeforSphere = GetBeforeSphere();
    //相手
    OBB DestObb = DestColl->GetObb();
    OBB DestBeforeObb = DestColl->GetBeforeObb();
    bsm::Vec3 SpanVelocity = SrcVelocity - DestVelocity;
    float HitTime = 0;
    if (HitTest::CollisionTestSphereObb(SrcBeforSphere, SpanVelocity, DestBeforeObb, 0, ElapsedTime, HitTime)) {
        CollisionPair pair;
        pair.m_Src = GetThis<Collision>();
        pair.m_Dest = DestColl;
        SPHERE SrcChkSphere = SrcBeforSphere;
        SrcChkSphere.m_Center += SrcVelocity * HitTime;
        pair.m_SrcCalcHitCenter = SrcChkSphere.m_Center;
        OBB DestChkObb = DestBeforeObb;
        DestChkObb.m_Center += DestVelocity * HitTime;
        pair.m_DestCalcHitCenter = DestChkObb.m_Center;
        bsm::Vec3 ret;
        HitTest::SPHERE_OBB(SrcChkSphere, DestChkObb, ret);
        //衝突した瞬間で法線を計算
        pair.m_SrcHitNormal = SrcChkSphere.m_Center - ret;
        pair.m_SrcHitNormal.normalize();
        pair.m_CalcHitPoint = ret;
        GetCollisionManager()->InsertNewPair(pair);
    }
}
 この中で呼んでいるHitTest::CollisionTestSphereObb()関数は移動中に衝突があれば、trueを返し、HitTimeに指定した範囲内の時間を返します。その情報により、衝突した瞬間が導き出せます。
 導き出したら、その場所に球体を移動させて、その位置から衝突法線衝突した瞬間の球の中心を導きCollisionPair構造体の変数に初期化し、GetCollisionManager()->InsertNewPair(pair);衝突判定マネージャに渡します。
 このようにして、ステージ上にあるコリジョンコンポーネントがついているもの同士の判定を行い、衝突ペアの配列を作成します。

拘束の解消

 一通りの判定を行い、新規に発生した衝突ペアの拘束をひとつづつ解消していくわけですが、衝突ペアの配列はもう一組ありまして、それは前回のターンで解消したにもかかわらず相変わらず衝突したままのペアが存在します。これをキープペアといいます。
 なぜこのような形になるかというと、コリジョンの操作が終わった後、次のターンではコントローラによる移動があったり、重力による落下があったりします。
 これらはターンをまたいでキープされている形となるため、これも一緒に拘束の解消を行う必要があります。
 具体的にはキープされているペアの配列に新規のペアの配列を追加し、それらの拘束を解消します。
 拘束を解消のアルゴリズムは内積を使います。原理は以下の形です。

 

図1206h

 

 球Xが衝突した瞬間です。球Aは埋まってる状態、球Bが拘束を解消する位置です。下のグラフが単純化したものです。
 球Aの中心から球Xの中心を引き算すると、緑色のベクトルになります。
 それと衝突法線の内積を計算すると、下図のように、内積は正規化されたベクトルへの投影を行いますので、マイナスの数値になります。もしこの値が0か正の数になれば、衝突してないことになります。ですので
1、球Aの中心から球Xの中心を引いてベクトルを作り出す。
2、それと衝突法線の内積をとる
3、それがマイナスだったら、その距離のぶんだけ、衝突法線方向に球Aを移動させる。
 この方法で、球Aは球Bの位置に移動します。これを実装しているのは、SharedLib内のCollisionManager.cppにあります、CollisionManager::EscapeCollisionPair()関数です。以下がそのコードです。
void CollisionManager::EscapeCollisionPair(CollisionPair& Pair) {
    auto ShSrc = Pair.m_Src.lock();
    auto ShDest = Pair.m_Dest.lock();
    if (ShSrc->GetAfterCollision() == AfterCollision::None || ShDest->GetAfterCollision() == AfterCollision::None) {
        return;
    }
    bsm::Vec3 SrcCenter = ShSrc->GetCenterPosition();
    bsm::Vec3 DestCenter = ShDest->GetCenterPosition();
    bsm::Vec3 DestMoveVec = DestCenter - Pair.m_DestCalcHitCenter;

    bsm::Vec3 SrcLocalVec = SrcCenter - Pair.m_SrcCalcHitCenter - DestMoveVec;
    float SrcV = bsm::dot(SrcLocalVec, Pair.m_SrcHitNormal);
    if (SrcV < 0.0f) {
        //まだ衝突していたら
        float EscapeLen = abs(SrcV);
        if (!ShDest->IsFixed()) {
            EscapeLen *= 0.5f;
        }
        //Srcのエスケープ
        SrcCenter += Pair.m_SrcHitNormal * EscapeLen;
        if (!ShDest->IsFixed()) {
            //Destのエスケープ
            DestCenter += -Pair.m_SrcHitNormal * EscapeLen;
        }
        SrcCenter.floor(GetEscapeFloor());
        auto PtrSrcTransform = ShSrc->GetGameObject()->GetComponent<Transform>();
        //Srcのエスケープ
        PtrSrcTransform->SetWorldPosition(SrcCenter);
        if (!ShDest->IsFixed()) {
            DestCenter.floor(GetEscapeFloor());
            ShDest->WakeUp();
            auto PtrDestTransform = ShDest->GetGameObject()->GetComponent<Transform>();
            //Destのエスケープ
            PtrDestTransform->SetWorldPosition(DestCenter);
        }
    }
}
 赤くなっているところがこれまで説明してきた計算の部分です。相手がIsFixed()でなければ、相手を逆方向に移動させます。(半分づつ移動させます)。
 また、相手が衝突した瞬間より移動した場合に備え、DestMoveVecという変数で衝突位置をずらしています。

コリジョンのスリープ

 また、衝突判定では、そもそも判定そのものにコストがかかります。
 ステージ上に配置されるオブジェクトすべてで判定を行うと、50個くらいまではいいのですが、100個200個となっていきますと処理落ちするようになります。
 そのため、ある一定時間動きがないオブジェクト同士は判定しないようにします。これをスリープ(眠る)といいます。
 このサンプルではスリープ状態にある砲弾は色が黒くなります。発射直後はオレンジですが、ステージ上で止まると、いずれ黒くなります。この形だと、速いマシンだと250個くらいは配置できます。

衝突後の処理

 衝突後の処理はCharacter.cpp(つまりコンテンツ側)の FireSphere::OnCollisionEnter()関数などで行ってます。
 コリジョンコンポーネントは、Rigidbidyと違い速度は持ちませんし自動的に物理処理を行うわけではありません。
 ここでは単に反発させています。例えばビリヤードなどのような場合は、衝突した後、力が分散する計算などを入れなければなりませんが、このサンプルでは行ってません。各自そのあたりは研究してみてください。