12.Update系の操作

1207.頂点データの取得

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

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

 

図1207a

 

 プレイヤーはAボタンでジャンプ、Xボタンで球を発射します。この辺りは前項と同じです。
 このサンプルには2つのボックス型オブジェクトと、奥に大きい球体が配置されています。右と奥ははスタティックなモデルで、左がボーンモデルです。
 Xボタンで発射された球は、これらのオブジェクトにぶつかると、スパークを出し反発します。
 ボーンモデルの場合も、アニメーションされている場所で反発します。

メッシュ内の三角形との衝突判定

 前項で紹介した衝突判定ボリューム境界を使用したものです。モデルなどを単純な3Dの形状と見立てて、判定を行います。
 しかし今回のサンプルでは、メッシュ内の各三角形と衝突判定を行います。
 まず、発射された球体との判定ですが、Charaaacter.h/cppに記述がありますFireSphereクラスです。
 このメンバ関数のFireSphere::OnUpdate()に記述があります。
void FireSphere::OnUpdate() {
    auto ptrTrans = GetComponent<Transform>();
    if (ptrTrans->GetPosition().y > m_ActiveMaxY) {
        float elapsedTime = App::GetApp()->GetElapsedTime();
        auto BeforePos = ptrTrans->GetBeforePosition();
        auto Pos = ptrTrans->GetPosition();
        Pos += m_Velocity * elapsedTime;
        ptrTrans->SetPosition(Pos);
        auto ptrColl = GetComponent<CollisionSphere>();
        auto ptrDraw = GetComponent<BcPNTStaticDraw>();
        if (ptrColl->IsSleep()) {
            ptrDraw->SetDiffuse(Col4(0.0f, 0.0f, 1.0f, 1.0f));
        }
        else {
            ptrDraw->SetDiffuse(Col4(1.0f, 1.0f, 1.0f, 1.0f));
            auto EnemyPtr = GetStage()->GetSharedGameObject<EnemyBox>(L"EnemyBox");
            auto BonePtr = GetStage()->GetSharedGameObject<BoneChara>(L"BoneChara");
            auto SpherePtr = GetStage()->GetSharedGameObject<EnemySphere>(L"EnemySphere");
            Vec3 HitPoint;
            TRIANGLE tri;
            bool isModelHit = false;
            if (EnemyPtr->IsHitSegmentTriangles(BeforePos, Pos, tri, HitPoint)) {
                //スパークの放出
                auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark");
                PtrSpark->InsertSpark(HitPoint);
                isModelHit = true;
            }
            else if (BonePtr->IsHitSegmentTriangles(BeforePos, Pos, tri, HitPoint)) {
                //スパークの放出
                auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark");
                PtrSpark->InsertSpark(HitPoint);
                isModelHit = true;
            }
            else if (SpherePtr->IsHitSegmentTriangles(BeforePos, Pos, tri, HitPoint)) {
                //スパークの放出
                auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark");
                PtrSpark->InsertSpark(HitPoint);
                isModelHit = true;
            }
            if (isModelHit) {
                m_Velocity.reflect(tri.GetNormal());
                if (m_Velocity.length() > 20.0f) {
                    m_Velocity.normalize();
                    m_Velocity *= 20.0f;
                }
            }
        }
    }
    else {
        SetUpdateActive(false);
        SetDrawActive(false);
        return;
    }
}
 ここで赤くなっている部分のうち、
            if (EnemyPtr->IsHitSegmentTriangles(BeforePos, Pos, tri,HitPoint)) {
 という部分に注目してください。ここでは右側のボックスIsHitSegmentTriangles()関数を呼んでいます。
 この関数は以下のようになっています。
bool EnemyBox::IsHitSegmentTriangles(const Vec3& StartPos, const Vec3& EndPos, TRIANGLE& tri, Vec3& HitPoint) {
    auto ptrColl = GetComponent<CollisionObb>();
    OBB obb = ptrColl->GetObb();
    SPHERE StartSp;
    StartSp.m_Center = StartPos;
    StartSp.m_Radius = 0.25f;
    SPHERE EndSp;
    EndSp.m_Center = EndPos;
    EndSp.m_Radius = 0.25f;
    SPHERE sp = HitTest::SphereEnclosingSphere(StartSp, EndSp);
    Vec3 ret;
    if (!HitTest::SPHERE_OBB(sp, obb, ret)) {
        //判定前処理
        //これにより無駄な判定を避けられる
        return false;
    }
    auto PtrDraw = GetComponent<BcPNTStaticDraw>();
    size_t hitIndex;
    return PtrDraw->HitTestStaticMeshSegmentTriangles(StartPos, EndPos, HitPoint, tri, hitIndex);
}
 のようになっています。
 上記の描画コンポーネントにおけるHitTestStaticMeshSegmentTriangles()関数線とスタティックメッシュの衝突判定を行います。
 EnemyBoxは六面体ですので12個の三角形で構成されています。その中のどれかの三角形と線分が衝突していればtrueを返し、tri変数に衝突した三角形を入れます。この内容はそのまま呼び出し側(FireSphere::OnUpdate()関数)に返されますので、FireSphere側でその情報を利用できるわけです。
 FireSphere側では、FireSphere::OnUpdate()関数で、
        if (EnemyPtr->IsHitSegmentTriangles(BeforePos, Pos, tri, HitPoint)) {
            //スパークの放出
            auto PtrSpark = GetStage()->GetSharedGameObject<MultiSpark>(L"MultiSpark");
            PtrSpark->InsertSpark(HitPoint);
            isModelHit = true;
        }
 とします。つまり、ヒットポイントをエミッターとしてエフェクトを送出します。また、isModelHitフラグをtrueにします。
 同様の処理をBoneCharaやEnemySphereに対しても行います。BoneCharaは左側のくねくねする物体です。EnemySphereは奥の球体です。このキャラクターのメンバ関数、例えばBoneChara::IsHitSegmentTriangles()関数では、描画コンポーネントHitTestSkinedMeshSegmentTriangles()関数を呼び出します。
 こちらはボーン処理された三角形と線分の衝突判定を行いますので、例えば、このくねくね物体が首をもたげても空中で衝突することはありません。こちらでも同様、スパークを放出します。
 上記どちらかと衝突した場合、isModelHitフラグはtrueになりますので、
        if (isModelHit) {
            m_Velocity.reflect(tri.GetNormal());
            if (m_Velocity.length() > 20.0f) {
                m_Velocity.normalize();
                m_Velocity *= 20.0f;
            }

        }
 という処理で反発(reflect)させます。こうすることで、FireSphereは跳ね返ります。

 FireSphereコリジョンもついてますので、FireSphere::OnCollisionEnter()関数により、同様の処理を行います。
void FireSphere::OnCollisionEnter(const CollisionPair& Pair) {
    auto ptrTrans = GetComponent<Transform>();
    auto shDest = Pair.m_Dest.lock();
    m_Velocity -= shDest->GetVelocity();
    m_Velocity.reflect(Pair.m_SrcHitNormal);
    if (m_Velocity.length() > 20.0f) {
        m_Velocity.normalize();
        m_Velocity *= 20.0f;
    }
}
 という部分です。さらに前項のようにFireSphereは例えばステージ上で減速しますので、FireSphere::OnCollisionExcute()関数で、減速処理を行います。
void FireSphere::OnCollisionExcute(const CollisionPair& Pair) {
    auto shDest = Pair.m_Dest.lock();
    if (shDest->IsFixed()) {
        //減速
        m_Velocity *= 0.95f;
        if (m_Velocity.length() < 0.05f) {
            m_Velocity = Vec3(0);
        }
    }
}

プレイヤー三角形との衝突判定

 このサンプルではプレイヤーも三角形と衝突します。先ほどのボックスやくねくねする物体の三角形、あるいは奥の球体との衝突があった場合、エフェクトを放出します。
 プレイヤーはではなく球体として判定を行っています。ここは、プレイヤーの1つ前のターン位置と現在の位置をとってきて、として判定する方法もあるでしょう、ただこのサンプルでは球体として行います。
 プレイヤーの判定部分は、Player::ChkModelCollision()関数で行ってます。この中から、各物体の三角形と球の判定の関数を呼び出しています。
 FireSphereとは違い、プレイヤー側では衝突後の処理は記述してません。例えばくねくねする物体の曲がった部分に乗るのような処理があればよりリアルなのでしょうが、その部分は各自検討してください。
 その処理をするためにはプレイヤーに速度という概念をおいて、コリジョンコンポーネントがやってるような処理を実装する必要があるでしょう。
 ただし、奥の球体は、球体側で特殊な処理をしています。それは後述します。

ボーン情報をとってくる

 このサンプルではスキンメッシュのボーン情報を取得することも紹介されています。
 Character.h/cppにはBallCharaというオブジェクトも実装されています。
 このオブジェクトはBoneCharaボーン情報をとってきて、そこに自分自身を配置します。BallChara::OnUpdate()関数を見てください。
void BallChara::OnUpdate() {
    auto bonePtr = GetStage()->GetSharedGameObject<BoneChara>(L"BoneChara");
    auto boneTrans = bonePtr->GetComponent<Transform>();
    auto boneDraw = bonePtr->GetComponent<BcPNTnTBoneModelDraw>();

    auto& bones = boneDraw->GetVecLocalBones();
    Mat4x4 mat = bones[2];
    auto mesh = boneDraw->GetMeshResource();
    vector<Vec3> positions;
    mesh->GetLocalPositions(positions);
    Mat4x4 localMat;
    localMat.translation(positions[1]);
    localMat *= mat;
    localMat *= boneTrans->GetWorldMatrix();
    auto ptrTrans = GetComponent<Transform>();
    ptrTrans->SetPosition(localMat.transInMatrix());
    ptrTrans->SetQuaternion(localMat.quatInMatrix());
}
 この赤い部分は、スキンメッシュのボーン情報をとってきて、その2番目のボーンに対して吸着する行列を作り出します(localMat)。
 行列は別の行列を掛け算することで、その、別の行列の単位系にすることができます(いわゆる親子関係です)。
    localMat.translation(positions[1]);
    localMat *= mat;
    localMat *= boneTrans->GetWorldMatrix();
 の記述で、ボーン行列(mat)、ワールド行列(boneTrans->GetWorldMatrix())を続けて掛け算しますので、処理後は、localMatにはBoneCharaを親に持つ行列が出来上がります。
 そして
    ptrTrans->SetPosition(localMat.transInMatrix());
    ptrTrans->SetQuaternion(localMat.quatInMatrix());
 で位置と回転を取り出して設定すれば、BoneCharaの特定のボーンに吸着する動きを実装することができます。

メッシュ内の三角形のデータを変更する

 奥にある球体にプレイヤーが衝突すると、球体が削り取られるように壊れていきます。
 これは、動的にメッシュの内容を変更していくことで実装しています。
 では動的にメッシュの内容を変更するとはどういうことなのでしょうか?
 以下の画像に注目ください。最初は球体だったのが、プレイヤーが衝突することで、形状が削り取られています

 

図1207b

 

 このサンプルのプレイヤーは、空中でジャンプすることも可能です。球体は大きいですが、球体の上部までジャンプしていって、どこでも削り取ることが可能となっています。
 さて、実装ですが、まず、このような使いまわしをしない(つまりインスタンスは、ステージ内で1つ)のようなメッシュは、ゲームオブジェクトのメンバ変数として実装しておくと扱いが便利になります。
 以下、大きい球体(EnemySphere)の宣言部ですが
class EnemySphere : public GameObject {
    float m_Scale;
    Vec3 m_Position;
    shared_ptr<MeshResource> m_MeshRes;
public:
    //構築と破棄
    EnemySphere(const shared_ptr<Stage>& StagePtr,
        float Scale,
        const Vec3& Position
    );
    virtual ~EnemySphere();
    //初期化
    virtual void OnCreate() override;
    //操作
    bool IsHitSegmentTriangles(const Vec3& StartPos, const Vec3& EndPos, TRIANGLE& tri, Vec3& HitPoint);
    bool IsHitSphereTriangles(const SPHERE& StartSp, const SPHERE& EndSp, TRIANGLE& tri, Vec3& HitPoint);
};
 赤い部分がメッシュのポインタです。ここに専用のメッシュを作成して設定します。
 以下、EnemySphere::OnCreate()関数です。
void EnemySphere::OnCreate() {
    GetStage()->SetSharedGameObject(L"EnemySphere", GetThis<EnemySphere>());
    //初期位置などの設定
    auto ptrTrans = GetComponent<Transform>();
    ptrTrans->SetScale(Vec3(m_Scale));
    ptrTrans->SetRotation(Vec3(0));
    ptrTrans->SetPosition(m_Position);
    //CollisionSphere衝突判定を付ける(Sphere取り出しのため)
    auto ptrColl = AddComponent<CollisionSphere>();
    //無効にしておく
    ptrColl->SetUpdateActive(false);
    //描画コンポーネントの設定
    auto ptrDraw = AddComponent<BcPNTStaticDraw>();
    vector<VertexPositionNormalTexture> vertices;
    vector<uint16_t> indices;
    MeshUtill::CreateSphere(1.0f, 18, vertices, indices);
    m_MeshRes = MeshResource::CreateMeshResource(vertices, indices,true);
    ptrDraw->SetMeshResource(m_MeshRes);
    //描画するテクスチャを設定
    ptrDraw->SetTextureResource(L"WALL_TX");
    //影をつける(シャドウマップを描画する)
    auto shadowPtr = AddComponent<Shadowmap>();
    //影の形(メッシュ)を設定
    shadowPtr->SetMeshResource(m_MeshRes);
}
 赤い部分がメッシュを作成しているところです。
    m_MeshRes = MeshResource::CreateMeshResource(vertices, indices,true);
 上記のように、CreateMeshResource()関数の最後の引数をtrueにすると、このメッシュの頂点を変更できるようになります。
 そして、以下はプレイヤーが衝突したらメッシュの形状を変える処理をしているところです。
bool EnemySphere::IsHitSphereTriangles(const SPHERE& StartSp, const SPHERE& EndSp, TRIANGLE& tri, Vec3& HitPoint) {
    auto ptrColl = GetComponent<CollisionSphere>();
    SPHERE sp1 = ptrColl->GetSphere();
    SPHERE sp2 = HitTest::SphereEnclosingSphere(StartSp, EndSp);
    Vec3 ret;
    if (!HitTest::SPHERE_SPHERE(sp1, sp2)) {
        //判定前処理
        //これにより無駄な判定を避けられる
        return false;
    }
    auto PtrDraw = GetComponent<BcPNTStaticDraw>();
    size_t hitIndex;
    if (PtrDraw->HitTestStaticMeshSphereTriangles(StartSp, EndSp, HitPoint, tri, hitIndex)) {
        //hitindexを頂点のインデックスに換算
        hitIndex *= 3;
        auto& verVec = m_MeshRes->GetBackupVerteces<VertexPositionNormalTexture>();
        vector<uint16_t>& indexVec = m_MeshRes->GetBackupIndices<VertexPositionNormalTexture>();
        verVec[indexVec[hitIndex]].position *= 0.95;
        verVec[indexVec[hitIndex + 1]].position *= 0.95;
        verVec[indexVec[hitIndex + 2]].position *= 0.95;
        m_MeshRes->UpdateVirtexBuffer(verVec);
        return true;
    }
    return false;
}
 球体(プレイヤー)と、このメッシュ内の三角形が衝突した場合hitIndexには、三角形のインデックス(並び順)が返されます。
 これをhitIndex *= 3とすることで、頂点の順番(三角形の最初の頂点の順番)に変更できます。ですので、頂点の順番というのは、メッシュ上はインデックスバッファが指す頂点の順番ですから
        verVec[indexVec[hitIndex]].position *= 0.95;
        verVec[indexVec[hitIndex + 1]].position *= 0.95;
        verVec[indexVec[hitIndex + 2]].position *= 0.95;
 のようにすることで、バックアップされている頂点を変更できます。最後に
        m_MeshRes->UpdateVirtexBuffer(verVec);
 とすることで、このメッシュリソースの頂点を変更します。
 position *= 0.95というのは、positionには、球体のローカルポジションが入っていて、それらは、原点Vec3(0,0,0)からの相対的な位置情報になります。ですので、0.95倍することで、原点に頂点が近づきますので、凹むように表現できます。
 この処理はMeshUtill::CreateSphere()で作成するのメッシュだからできる処理です。実際のモデルなどに適用する場合は、注意が必要です。