13.Draw系の操作とオーディオ

1305.インスタンス描画

 このサンプルはFullSample305というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルはリリースモードで起動してください(デバッグモードでは重すぎます)。

 

図1305a

 中央で回転する球はの集合です。プレイヤーで当たってみましょう。
 すると、プレイヤーと衝突した点は、上部にはね、周りに散らばります。
 どんどんやっていくと以下の様な状態になります。

 

図1305b

インスタンス描画

 この球を形成する点インスタンン描画という手法で描画しています。
 インスタンス描画とは一度のシェーダ描画で1つのメッシュを複数個描画する手法です。
 この描画方法をとることで、ハイスピードな描画を実装できます。
 インスタンス描画自体は古くからある手法なのです。Dx11の場合、ワールド行列は通常コンスタントバッファに入力しますが、インスタンス描画をする場合は複数のワールド行列スロットに入れてしまいます。そうすることで、頂点シェーダではスロットに入ったワールド行列ごとに処理をします。
 BaseCross64で内部的にインスタンス描画を行っているのはマルチパーティクル(つまりエフェクト)の描画です。エフェクトは、単純でありますが大量のメッシュをできるだけ速く描画する必要があるためにそのような実装になっています。
 一般的なゲームオブジェクトではPCTStaticDrawなど、先頭にBcがつかない描画コンポーネントでインスタンス描画が実装できるようになっています。
 BcPCTStaticDrawなどのBcがつく描画コンポーネントでは、コンスタントバッファにライティングに必要な情報が入っているために、対応できていません。(作りこめばできるのでしょうが、そこまで用意してません)

位置の集合の作成

 このサンプルでは球のメッシュから頂点位置データのみを取り出し、それをワールド行列化して実装しています。
 具体的にはCharacter.h/cppにあるPointsBallクラスです。
 PointsBall::OnCreate()関数を見てみましょう。
void PointsBall::OnCreate() {
    vector<VertexPositionNormalTexture> vertices;
    vector<uint16_t> indices;
    //球体を作成
    MeshUtill::CreateSphere(1.0f, 28,vertices, indices);
    for (auto& v : vertices) {
        LocalData tempLocalData;
        tempLocalData.m_LocalPosition = v.position;
        //各頂点をインスタンスの位置に設定
        m_LocalDataVec.push_back(tempLocalData);
    }
    //描画用メッシュの作成
    float helfSize = 0.04f;
    Col4 col(1.0f, 1.0f, 0.0f, 1.0f);
    //頂点配列
    vector<VertexPositionColorTexture> meshVertices = {
        { VertexPositionColorTexture(Vec3(-helfSize, helfSize, 0),col, Vec2(0.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(helfSize, helfSize, 0),col, Vec2(1.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(-helfSize, -helfSize, 0),col, Vec2(0.0f, 1.0f)) },
        { VertexPositionColorTexture(Vec3(helfSize, -helfSize, 0),col, Vec2(1.0f, 1.0f)) },
    };
    //インデックス配列
    vector<uint16_t> meshIndex = { 0, 1, 2, 1, 3, 2 };
    //2次元平面とする(頂点数が少ないため)
    m_MeshRes = MeshResource::CreateMeshResource(meshVertices, meshIndex, true);
    //全体の位置関連
    auto ptrTransform = GetComponent<Transform>();
    ptrTransform->SetScale(Vec3(m_Scale));
    ptrTransform->SetRotation(Vec3(0));
    ptrTransform->SetPosition(m_Position);
    //描画コンポーネントの追加(インスタンス描画)
    auto PtrDraw = AddComponent<PCTStaticInstanceDraw>();
    PtrDraw->SetMeshResource(m_MeshRes);
    PtrDraw->SetTextureResource(L"SPARK_TX");
    PtrDraw->SetDepthStencilState(DepthStencilState::Read);
    //各頂点ごとに行列を作成
    for (auto& v : m_LocalDataVec) {
        Mat4x4 tempMat;
        tempMat.affineTransformation(
            Vec3(1.0f),
            Vec3(0.0f),
            Quat(),
            v.m_LocalPosition
        );
        //インスタンス描画の行列として設定
        PtrDraw->AddMatrix(tempMat);
    }
    SetAlphaActive(true);
}
 ここではまず
    //球体を作成
    MeshUtill::CreateSphere(1.0f, 28,vertices, indices);
 と球体のメッシュを作成します。28というのは分割数です。こうすると、2000個弱の頂点の球が出来上がります。
 BaseCross64のインスタンス描画で使用する描画数は2000個です。この値を変更することもできますが、2000個以上使用すると目に見えて描画速度が落ちますので注意しましょう。
 続いて
    for (auto& v : vertices) {
        LocalData tempLocalData;
        tempLocalData.m_LocalPosition = v.position;
        //各頂点をインスタンスの位置に設定
        m_LocalDataVec.push_back(tempLocalData);
    }
 という形で、球の頂点を位置情報をLocalDataのm_LocalPositionにセットします。
 LocalDataヘッダ部に宣言されている構造体で
struct LocalData {
    LocalState m_State;
    Vec3 m_LocalPosition;
    Mat4x4 m_FixedMatrix;
    Vec3 m_Velocity;
    LocalData() :
        m_State(LocalState::Roll),
        m_LocalPosition(0.0f),
        m_FixedMatrix(),
        m_Velocity(0.0f)
    {}
};
 というメンバです。LocalStateenum class
enum class LocalState {
    Roll,
    Down,
    Fix
};
 となっています。LocalDataには1つの点が入るのですが、その状態を表します。ステートマシンステートのようなものです。初期値はRollとなります。
 PointsBall::OnCreate()関数では、一つ一つの点を
        //各頂点をインスタンスの位置に設定
        m_LocalDataVec.push_back(tempLocalData);
 とm_LocalDataVec配列に取っておきます。
 ここで注意したいのはm_LocalDataVecローカル座標だということです。ここでのローカル座標球の中心を原点とした座標系です。

メッシュの作成

 点の集合はできましたが、実際に描画するにはメッシュが必要です。球体や四角形などのメッシュを描画することはかのうですが、出来るだけ軽くするために平面のメッシュを使います。これだと3角形を2つ描画するだけですみます。
    //描画用メッシュの作成
    float helfSize = 0.04f;
    Col4 col(1.0f, 1.0f, 0.0f, 1.0f);
    //頂点配列
    vector<VertexPositionColorTexture> meshVertices = {
        { VertexPositionColorTexture(Vec3(-helfSize, helfSize, 0),col, Vec2(0.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(helfSize, helfSize, 0),col, Vec2(1.0f, 0.0f)) },
        { VertexPositionColorTexture(Vec3(-helfSize, -helfSize, 0),col, Vec2(0.0f, 1.0f)) },
        { VertexPositionColorTexture(Vec3(helfSize, -helfSize, 0),col, Vec2(1.0f, 1.0f)) },
    };
    //インデックス配列
    vector<uint16_t> meshIndex = { 0, 1, 2, 1, 3, 2 };
    //2次元平面とする(頂点数が少ないため)
    m_MeshRes = MeshResource::CreateMeshResource(meshVertices, meshIndex, true);
 こんな感じです。
 ここでhelfSize0.04fです。これで、1辺が0.08の四角形メッシュが作成されます。

コンポーネントの設定

 コンポーネントはまずTransformを設定します。これは大きな球体のTransformです。
 スケーリング位置情報がコンストラクタで渡されますのでそれを設定します。
    //全体の位置関連
    auto ptrTransform = GetComponent<Transform>();
    ptrTransform->SetScale(Vec3(m_Scale));
    ptrTransform->SetRotation(Vec3(0));
    ptrTransform->SetPosition(m_Position);
 続いて描画コンポーネントです。
    //描画コンポーネントの追加(インスタンス描画)
    auto PtrDraw = AddComponent<PCTStaticInstanceDraw>();
    PtrDraw->SetMeshResource(m_MeshRes);
    PtrDraw->SetTextureResource(L"SPARK_TX");
    PtrDraw->SetDepthStencilState(DepthStencilState::Read);
    //各頂点ごとに行列を作成
    for (auto& v : m_LocalDataVec) {
        Mat4x4 tempMat;
        tempMat.affineTransformation(
            Vec3(1.0f),
            Vec3(0.0f),
            Quat(),
            v.m_LocalPosition
        );
        //インスタンス描画の行列として設定
        PtrDraw->AddMatrix(tempMat);
    }
    SetAlphaActive(true);
 このPCTStaticInstanceDrawコンポーネントがこのサンプルの主人公です。インスタンス描画を行います。
 メッシュは先ほど作成した四角形のメッシュ、テクスチャは、あらかじめリソース化したL"SPARK_TX"を設定します。
 インスタンス描画コンポーネントではAddMatrix()関数によりあらかじめ、描画する行列を設定しておきます。
 データ内容的にはOnUpdate()関数で変更するので、意味のある物でなくてもかまいません。

更新処理

 PointsBall::OnUpdate()関数は以下のように2つの関数を呼んでいます。
void PointsBall::OnUpdate() {
    //ステートのUpdate
    UpdateState();
    //各インスタンスのUpdate
    UpdateInstances();
}
 以下は、最初に呼ばれる関数です。各インスタンスの状態のチェックを行い、必要なら状態を変更します。
//ステートのUpdate
void PointsBall::UpdateState() {
    if (m_MatVec.size() != m_LocalDataVec.size()) {
        //m_MatVecがまだ初期化されてない可能性がある
        return;
    }
    //各定数
    const float baseY = m_Scale * 0.02f;
    const float velocityPower = 3.0f;
    const Vec3 gravity(0, -9.8f, 0);
    float elapsedTime = App::GetApp()->GetElapsedTime();
    auto playerSh = GetStage()->GetSharedGameObject<Player>(L"Player");
    auto playerPos = playerSh->GetComponent<Transform>()->GetWorldPosition();
    //各頂点でループ
    for (size_t i = 0; i < m_LocalDataVec.size(); i++) {
        switch (m_LocalDataVec[i].m_State) {
        case LocalState::Roll:
            //回転している状態
            {
                auto len = length(playerPos - m_MatVec[i].transInMatrix());
                if (len < 0.4f) {
                    //lenが0.4未満なら衝突してると判断
                    //ステートを変更
                    //衝突していたら球から飛び出すように速度を設定
                    m_LocalDataVec[i].m_State = LocalState::Down;
                    m_LocalDataVec[i].m_Velocity = playerPos - m_MatVec[i].transInMatrix();
                    m_LocalDataVec[i].m_Velocity.normalize();
                    m_LocalDataVec[i].m_Velocity.y = 1.0f;
                    m_LocalDataVec[i].m_Velocity *= velocityPower;
                }
            }
            break;
        case LocalState::Down:
            //落下中の状態
            if (m_MatVec[i].transInMatrix().y <= baseY) {
                //落下終了
                m_LocalDataVec[i].m_State = LocalState::Fix;
                //終了時の行列を保存
                //Y値を0.1にする
                m_MatVec[i]._42 = baseY;
                //m_LocalDataVecとm_MatVecは各インスタンスは同じインデックスである
                m_LocalDataVec[i].m_FixedMatrix = m_MatVec[i];
            }
            else {
                m_LocalDataVec[i].m_Velocity += gravity * elapsedTime;
                m_LocalDataVec[i].m_LocalPosition += m_LocalDataVec[i].m_Velocity * elapsedTime;
            }
            break;
        case LocalState::Fix:
            //落下終了の状態
            break;
        }
    }
}
 ここで注目すべき点は、プレイヤーが各点に当たったときの処理です。
 上方にぴょんとはねるように飛び出すわけですが、そこはcase LocalState::Roll:ステート時に
                    //lenが0.4未満なら衝突してると判断
                    //ステートを変更
                    //衝突していたら球から飛び出すように速度を設定
                    m_LocalDataVec[i].m_State = LocalState::Down;
                    m_LocalDataVec[i].m_Velocity = playerPos - m_MatVec[i].transInMatrix();
                    m_LocalDataVec[i].m_Velocity.normalize();
                    m_LocalDataVec[i].m_Velocity.y = 1.0f;
                    m_LocalDataVec[i].m_Velocity *= velocityPower;
 といった処理で速度を変えm_LocalDataVec[i].m_State = LocalState::Down;とステートを変更します。
 LocalState::Downステート時は、まだ
            if (m_MatVec[i].transInMatrix().y <= baseY) {
 と地面につかないかどうかを確認し、落下中であれば重力を設定します。
 地面についたらLocalState::Fixステートに変更します。その際
                //終了時の行列を保存
                //Y値を0.1にする
                m_MatVec[i]._42 = baseY;
 と行列中の位置データのY座標をbaseYにし
                //m_LocalDataVecとm_MatVecは各インスタンスは同じインデックスである
                m_LocalDataVec[i].m_FixedMatrix = m_MatVec[i];
 とm_LocalDataVec[i].m_FixedMatrixm_MatVec[i]を設定します。
 m_LocalDataVecm_MatVecは、同じ点は同じインデックスを保持するので、このような処理が可能になります。

 更新処理のもう一つの関数はPointsBall::UpdateInstances()関数ですが、以下の処理になります。
void PointsBall::UpdateInstances() {
    float elapsedTime = App::GetApp()->GetElapsedTime();
    auto ptrDraw = GetComponent<PCTStaticInstanceDraw>();
    auto camera = OnGetDrawCamera();
    //カメラのレイを作成しておく
    auto lay = camera->GetAt() - camera->GetEye();
    lay.normalize();
    Quat qtCamera;
    //回転は常にカメラを向くようにする
    qtCamera.facing(lay);
    auto ptrTransform = GetComponent<Transform>();
    //全体を回転させる
    auto worldQt = ptrTransform->GetQuaternion();
    Quat spanQt(Vec3(0, 1, 0), elapsedTime);
    worldQt *= spanQt;
    ptrTransform->SetQuaternion(worldQt);
    //行列の配列をクリア
    m_MatVec.clear();
    Mat4x4 worldMat;
    for (auto& v : m_LocalDataVec) {
        if (v.m_State == LocalState::Fix) {
            //落下終了の状態
            worldMat.affineTransformation(
                v.m_FixedMatrix.scaleInMatrix(),
                Vec3(0.0f),
                qtCamera,
                v.m_FixedMatrix.transInMatrix()
            );
        }
        else {
            Mat4x4 localMat;
            localMat.affineTransformation(
                Vec3(1.0f),
                Vec3(0.0f),
                Quat(),
                v.m_LocalPosition
            );
            worldMat = localMat * ptrTransform->GetWorldMatrix();
            worldMat.affineTransformation(
                worldMat.scaleInMatrix(),
                Vec3(0.0f),
                qtCamera,
                worldMat.transInMatrix()
            );
        }
        m_MatVec.push_back(worldMat);
    }
    //インスタンス行列の更新
    ptrDraw->UpdateMultiMatrix(m_MatVec);
}
 ここでは
        if (v.m_State == LocalState::Fix) {
 かどうかで処理を変えているのですが、落下終了の場合はv.m_FixedMatrixを使用します。

 また、この関数では、各点をカメラのほうを向く処理をします。
 これは、メッシュが平面のため、つねにカメラのほうを向く処理をしないと平面が斜めになってしまうためです。
 この辺はエフェクトと同じ処理です。
            worldMat.affineTransformation(
                v.m_FixedMatrix.scaleInMatrix(),
                Vec3(0.0f),
                qtCamera,
                v.m_FixedMatrix.transInMatrix()
            );
 もしくは
            worldMat = localMat * ptrTransform->GetWorldMatrix();
            worldMat.affineTransformation(
                worldMat.scaleInMatrix(),
                Vec3(0.0f),
                qtCamera,
                worldMat.transInMatrix()
            );
 という方法でworldMatを決定します。
 worldMatが出来上がったら
        m_MatVec.push_back(worldMat);
 と、m_MatVecに設定し
    //インスタンス行列の更新
    ptrDraw->UpdateMultiMatrix(m_MatVec);
 とすることで、すべてのインスタンスの行列が更新されます。

まとめ

 この項ではインスタンス描画について解説しました。
 同じゲームオブジェクトを大量に表示させたい場合などは開発中に出てでてきます。
 ゲームオブジェクトは機能は豊富ですが、その分コストがかかります。
 そんなに厳密な処理を必要としないオブジェクトの場合は、ぜひインスタンス描画を検討してみて下さい。