11.チュートリアル

1102.物理計算を使った3Dの配置

 このサンプルはFullSample102というディレクトリに含まれます。
 BaseCrossDx11VS2017.sln、BaseCrossDx11VS2019.slnというソリューションを開くとDx11版が起動します。
 BaseCrossDx12VS2017.sln、BaseCrossDx12VS2017.slnというソリューションを開くとDx12版が起動します。
 VS2017、VS2019はVisualStdioのバージョンです。

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

 

図1102a

 

 このサンプルもXBoxコントローラが必須になります。

Rigidbodyコンポーネント

 前項のBaseCrossとの比較表にも説明しましたように、BaseCross64ではRigidbodyコンポーネントはすなわち物理計算をするコンポーネントです。
 この項ではRigidbodyコンポーネントの基本的な使い方、を説明します。

プレイヤー

 まずプレイヤーからです。
 プレイヤーはPlayer.h/cppに実装があります。以下は、構築用関数であるPlayer::OnCreate()関数です。
//初期化
void Player::OnCreate() {
    //初期位置などの設定
    auto PtrTrans = GetComponent<Transform>();
    PtrTrans->SetScale(Vec3(m_Scale));  //直径25センチの球体
    PtrTrans->SetRotation(0.0f, 0.0f, 0.0f);
    auto bkCamera = App::GetApp()->GetScene<Scene>()->GetBackupCamera();
    Vec3 FirstPos;
    if (!bkCamera) {
        FirstPos = Vec3(0, m_Scale * 0.5f, 0);
    }
    else {
        FirstPos = App::GetApp()->GetScene<Scene>()->GetBackupPlayerPos();
    }
    PtrTrans->SetPosition(FirstPos);
    //WorldMatrixをもとにRigidbodySphereのパラメータを作成
    PsSphereParam param(PtrTrans->GetWorldMatrix(),1.0f,false, PsMotionType::MotionTypeActive);
    //RigidbodySphereコンポーネントを追加
    auto PsPtr = AddComponent<RigidbodySphere>(param);
    //自動的にTransformを設定するフラグは無し
    PsPtr->SetAutoTransform(false);

    //文字列をつける
    auto PtrString = AddComponent<StringSprite>();
    PtrString->SetText(L"");
    PtrString->SetTextRect(Rect2D<float>(16.0f, 16.0f, 640.0f, 480.0f));

    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<Shadowmap>();
    //影の形(メッシュ)を設定
    ShadowPtr->SetMeshResource(L"DEFAULT_SPHERE");
    //描画コンポーネントの設定
    auto PtrDraw = AddComponent<BcPNTStaticDraw>();
    //描画するメッシュを設定
    PtrDraw->SetMeshResource(L"DEFAULT_SPHERE");
    //描画するテクスチャを設定
    PtrDraw->SetTextureResource(L"TRACE_TX");

    //透明処理
    SetAlphaActive(true);
    //カメラを得る
    auto PtrCamera = dynamic_pointer_cast<MyCamera>(OnGetDrawCamera());
    if (PtrCamera) {
        //MyCameraである
        //MyCameraに注目するオブジェクト(プレイヤー)の設定
        PtrCamera->SetTargetObject(GetThis<GameObject>());
        PtrCamera->SetTargetToAt(Vec3(0, 0.25f, 0));
    }
}
 ここでRigidbodyコンポーネントの実装に使っているPsSphereParamというのは構造体です。以下の様な内容になります。(見やすいように一部インライン化してます)
struct PsSphereParam : public PsParam {
    float m_Radius;
    PsSphereParam() :
        m_Radius(1.0f)
    {}
    PsSphereParam(const bsm::Mat4x4& mat,
        float mass, bool UseSleep, PsMotionType mtype){
        //basecrossのスケーリングは直径基準なので、半径基準にする
        m_Radius = mat.scaleInMatrix().y * 0.5f;
        m_Mass = mass;
        //慣性テンソルの計算
        m_Inertia = BasePhysics::CalcInertiaSphere(m_Radius, m_Mass);
        m_UseSleep = UseSleep;
        m_MotionType = mtype;
        m_Quat = mat.quatInMatrix();
        m_Pos = mat.transInMatrix();
    }
};
 PsSphereParamのコンストラクタは親クラスのメンバも直接設定しています。親クラスのPsParamは以下のようになります。
struct PsParam {
    PsMotionType m_MotionType;
    bsm::Quat m_Quat;
    bsm::Vec3 m_Pos;
    bsm::Vec3 m_LinearVelocity;
    bsm::Vec3 m_AngularVelocity;
    bool m_UseSleep;
    float m_Mass;
    //慣性テンソル
    bsm::Mat3x3 m_Inertia;
    float m_Restitution;
    float  m_Friction;
    //衝突判定制御
    uint32_t m_ContactFilterSelf;
    uint32_t m_ContactFilterTarget;
    //オフセット値
    bsm::Quat m_OffsetOrientation;
    bsm::Vec3 m_OffsetPosition;
    PsParam() :
        m_Quat(),
        m_Pos(0),
        m_LinearVelocity(0),
        m_AngularVelocity(0),
        m_UseSleep(true),
        m_Mass(0.0f),
        m_Inertia(),
        m_Restitution(0.2f),
        m_Friction(0.6f),
        m_ContactFilterSelf(0xffffffff),
        m_ContactFilterTarget(0xffffffff),
        m_OffsetOrientation(),
        m_OffsetPosition(0.0f)
    {
        m_Quat.identity();
        m_OffsetOrientation.identity();
    }
};
 このように、物理計算するためには多くのパラメータを必要としますが、ほとんどはデフォルト値です。
 サンプルのプレイヤーは
    PsSphereParam param(PtrTrans->GetWorldMatrix(),1.0f,false, PsMotionType::MotionTypeActive);
 のようにワールド行列を使って初期化しています。
 続く
    //自動的にTransformを設定するフラグは無し
    PsPtr->SetAutoTransform(false);
 と属性を変更しています。この属性は、物理計算結果を自動的にTransformを設定するフラグです。デフォルトはtrueになっています。
 プレイヤーの場合は、コントローラに合わせて位置や回転が目まぐるしく変化します。この値を物理計算に加えなければいけないためfalseにします。

 プレイヤーの更新処理はPlayer::OnUpdate()で行います。
void Player::OnUpdate() {
    //コントローラチェックして入力があればコマンド呼び出し
    m_InputHandler.PushHandle(GetThis<Player>());
    auto Vec = GetMoveVector();
    auto PtrPs = GetComponent<RigidbodySphere>();
    auto Velo = PtrPs->GetLinearVelocity();
    //xとzの速度を修正
    Velo.x = Vec.x * 5.0f;
    Velo.z = Vec.z * 5.0f;
    //速度を設定
    PtrPs->SetLinearVelocity(Velo);
}
 ここではGetMoveVector()により、コントローラから移動方向(移動速度)を取得して、現在の速度のX、Z方向のみ修正します。
 修正した値は、そのままRigidbodySphere(球体のRigidbody)に戻します。
 RigidbodyコンポーネントOnUpdate()とOnUpdate2()の間に物理的な計算をします。ですので、その結果をTransformに設定するのは、Player::OnUpdate2()で行います。
void Player::OnUpdate2() {
    //RigidbodySphereからTransformへのパラメータの設定
    //自動的に設定はされない設定になっているので自分で行う
    auto PtrPs = GetComponent<RigidbodySphere>();
    auto Ptr = GetComponent<Transform>();
    //位置情報はそのまま設定
    Ptr->SetPosition(PtrPs->GetPosition());
    //回転の計算
    Vec3 Angle = GetMoveVector();
    if (Angle.length() > 0.0f) {
        //補間処理を行う回転。
        RotToHead(Angle, 0.1f);
    }
    //文字列の表示
    DrawStrings();
}
 このようにRigidbodyコンポーネントが持ってる位置情報をTransformコンポーネントに実装します。
 回転については、進行方向を見るようにしたいのでRotToHead(Angle, 0.1f);呼び出しで行います。
void Player::RotToHead(const Vec3& Velocity, float LerpFact) {
    if (LerpFact <= 0.0f) {
        //補間係数が0以下なら何もしない
        return;
    }
    auto PtrTransform = GetComponent<Transform>();
    //回転の更新
    if (Velocity.length() > 0.0f) {
        bsm::Vec3 Temp = Velocity;
        Temp.normalize();
        float ToAngle = atan2(Temp.x, Temp.z);
        Quat Qt;
        Qt.rotationRollPitchYawFromVector(bsm::Vec3(0, ToAngle, 0));
        Qt.normalize();
        //現在の回転を取得
        Quat NowQt = PtrTransform->GetQuaternion();
        //現在と目標を補間
        if (LerpFact >= 1.0f) {
            NowQt = Qt;
        }
        else {
            //クオータニオンの補間処理
            NowQt = XMQuaternionSlerp(NowQt, Qt, LerpFact);
        }
        PtrTransform->SetQuaternion(NowQt);
    }
}
 このコードを見ればわかるように、Rigidbodyコンポーネントが持ってる回転角度は完全に無視しています。Rigidbodyコンポーネントの計算では、球体が移動する場合進行方向に回転します。しかしそれではプレイヤーには不似合いなので、無視しています。
 このように、初期化時に設定した
    //自動的にTransformを設定するフラグは無し
    PsPtr->SetAutoTransform(false);
 により、物理計算のいいとこどり、の実装が可能になります。

アクティブなオブジェクト

 このサンプルでは、いくつかの動かすことができるオブジェクトがあります。Character.h/cppに記述があります。
 起動時に上から降ってくるオブジェクトです。落ちる場所や、当たる位置に応じて、自動的に回転や跳ね返りをします。
 形状もがあります。ActivePsBoxActivePsSphereです。どちらも処理はあまり変わりませんので、ActivePsBoxについて説明します。
 ActivePsBoxの初期化(構築)処理は ActivePsBox::OnCreate()です。
void ActivePsBox::OnCreate() {
    auto PtrTrans = GetComponent<Transform>();

    PtrTrans->SetScale(m_Scale);
    PtrTrans->SetQuaternion(m_Qt);
    PtrTrans->SetPosition(m_Position);

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");

    auto PtrDraw = AddComponent<BcPNTStaticDraw>();
    PtrDraw->SetFogEnabled(true);
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetOwnShadowActive(true);
    PtrDraw->SetTextureResource(L"SKY_TX");
    //物理計算ボックス
    PsBoxParam param(PtrTrans->GetWorldMatrix(), 1.0f, true, PsMotionType::MotionTypeActive);
    auto PsPtr = AddComponent<RigidbodyBox>(param);

 赤くなっているところがRigidbodyBox(箱型のRigidbody)を実装しているところです。
 このオブジェクトは、自分で動くわけではないのでActivePsBox::OnUpdate()などは記述する必要がありません。配置すれば勝手に他の物理オブジェクトと相互作用します。
 もちろんプレイヤーが体当たりすれば動きます。ActivePsSphereActivePsSphere::OnCreate()のみ記述されます(コンストラクタ、デストラクタは記述が必要)。

動かないオブジェクト

 動かないオブジェクトはFixedPsBoxです。Character.h/cppに記述があります。
 FixedPsBoxはゲーム盤(床)と前方中央にある箱です。ActivePsBoxなどが落ちてくる場所にあります。
 こちらも初期化処理のみ記述すれば実装できます。以下は FixedPsBox::OnCreate()関数です。
void FixedPsBox::OnCreate() {

    auto PtrTrans = GetComponent<Transform>();

    PtrTrans->SetScale(m_Scale);
    PtrTrans->SetQuaternion(m_Qt);
    PtrTrans->SetPosition(m_Position);

    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(L"DEFAULT_CUBE");

    auto PtrDraw = AddComponent<BcPNTStaticDraw>();
    PtrDraw->SetFogEnabled(true);
    PtrDraw->SetMeshResource(L"DEFAULT_CUBE");
    PtrDraw->SetOwnShadowActive(true);
    PtrDraw->SetTextureResource(L"SKY_TX");
    //物理計算ボックス
    PsBoxParam param(PtrTrans->GetWorldMatrix(), 0.0f, true, PsMotionType::MotionTypeFixed);
    auto PsPtr = AddComponent<RigidbodyBox>(param);
}
 赤くなっているところが動かない箱の初期化です。この記述のみでほかの物理オブジェクトと相互作用します。

まとめ

 Rigidbodyコンポーネントを使うことにより、勝手に物理的な動きをするようになります。
 ここで注意したいのは、物理オブジェクトを配置しただけでゲームを作ったような気になるという点です。
 ゲーム物理シミュレーションではありません。物理計算はあくまで、たんなる道具にほかなりません。
 その道具がユーザーが楽しく遊ぶための手段として有効であればどんどん使うべきですが、無理やり使っても、あまりいいことはないことを覚えておきましょう。

 またRigidbodyコンポーネントはここで紹介した以外でもいろんな機能がありますし、もっといろんな形状を作成することができます。それらについては、のちのサンプルで紹介したいと思います。