403.シンプルな3Dの描画

 この項では3D表現に中でも、一番シンプルな描画方法について説明します。
 FullSample403を実行すると以下のような画面が現れます。

 

図0403a

 このサンプルはシンプルな3D表現のサンプルです。
 ポイントを絞るために、衝突判定は実装していません。プレイヤー(中央の球)は移動しますが、これは各オブジェクトを見やすくするためです。

プリミティブオブジェクト

 まず、プリミティブオブジェクトについて説明します。
 球が奥に7つ並んでいます。それぞれ頂点フォーマットが違います。
 一番左はPCフォーマット(位置と頂点色)を持ったフォーマットです。2番目はPTフォーマット(位置とテクスチャUV)、3番目はPCTフォーマット(位置と頂点色とテクスチャUV)、そして4番目はPNTフォーマット(位置と法線とテクスチャUV)、5番目は4番目と同じPNTフォーマットですが、テクスチャを設定しない形です。
 さらに共通の設定として、それぞれ色要素としてエミッシブ(放射光)とデフューズ(拡散光)を設定できます。
 そして右の2つの球体はPNTフォーマットです。これらはスペキュラー色(反射光)を加えています。
 これらのの計算はまさしくシェーダでの記述であり、どのように導くかはシェーダをどう記述するかによって変わります。ですから各光に名前はついているものの、決まりがあるかわけではありません。

 BaseCrossでの記述はおおむね以下のようになってます。
1、基本色(PNTフォーマットは法線ライティング)にデフューズ(拡散光)を掛ける。
2、1にエミッシブ色を足す
3、PNTフォーマットはスペキュラーを足す
4、3を基本色とし、テクスチャがある場合はサンプリングした値に3を掛ける。
5、影がある場合は影要素を掛ける
 これらの計算は頂点フォーマットによって微妙に違います。詳しくはシェーダを参照してください。
 シェーダを自作する場合は、これらの計算の計算順番を変えたり、あるいは、ある要素は強調するように計算したりなど、無限の表現方法があります。
 また、これらの計算は頂点シェーダピクセルシェーダでの計算であり、シェーダを自作する場合は、この2つの間にハルシェーダ、ドメインシェーダそしてジオメトリシェーダを加えることができます。ハルシェーダとドメインシェーダテセレーション(ポリゴン分割)を実現するためのシェーダであり、ジオメトリシェーダ頂点を増やすシェーダです。
 BaseCrossではジオメトリシェーダについては比較的簡単に実装できるようクラス化されています。(言い方を変えればテセレーションは作成可能ですが、実装は各自直接記述ということです)。
 以下に、グラフィックパイプラインの実行順番を記します。

 

図0403b

 この中で頂点シェーダピクセルシェーダは必須なシェーダです。
 さてそれではさまざまなオブジェクトの実装を見ていきます。

ビューの作成

 GameStage.h/cppを見てください。
 GameStage::CreateViewLight()関数で、ビューを作成しています。このサンプルで使用するビューはシングルビューで、ライトは1個です。シンプルな3D描画のシェーダは複数のライトには対応してません。また、カメラはLookAtCameraを使用していますが、パラメータで10.0fを指定しています。これは初期のアーム(腕の長さ)の設定です。
    //ビューのカメラの設定
    auto PtrLookAtCamera = ObjectFactory::Create<LookAtCamera>(10.0f);

球体の作成

 7つの球体はそれぞれPcSphere(PC頂点の球)、PtSphere(PT頂点の球)、PctSphere(PCT頂点の球)、PntSphere(PNT頂点の球)、PntSpSphere(PNT頂点のスペキュラー使用の球)です。
 PNT頂点の球以外はMeshUtill::CreateSphere()関数を使ってPNT頂点の球を作成し、必要に応じて法線やテクスチャUVを削除したりしています。
 サンプルを簡略化するために今回はOnUpdate()等による更新処理は実装してません。
 また、プレイヤーは移動できますので、各球体に近づいて表現を細かく見ることができます。

シェーダによる表現の違い

 表現を細かく見ていきますと、頂点定義の違いによりずいぶん違うのがわかります。一番使われるのはPNT頂点ですが、インターフェイスや特殊な表現にはほかの頂点定義も有用なのがわかります。
 例えばチュートリアル006で紹介した背番号のような数字はPCT頂点が有効なのが想像できると思います(ライティングは必要ない)。
 それではタイプごとに内容を見てみましょう。

頂点変更ができるメッシュ

 以下はPC頂点を持った球体の作成方法です。PcSphere::OnCreate()に記述があります。
void PcSphere::OnCreate() {
    auto PtrTrans = GetComponent<Transform>();
    PtrTrans->SetScale(Vec3(1.0f, 1.0f, 1.0f));
    Quat Qt;
    Qt.identity();
    PtrTrans->SetQuaternion(Qt);
    PtrTrans->SetPosition(m_StartPos);

    //描画コンポーネント
    auto PtrDraw = AddComponent<PCStaticDraw>();
    vector<VertexPositionNormalTexture> vertices;
    vector<VertexPositionColor> new_vertices;
    vector<uint16_t> indices;
    MeshUtill::CreateSphere(1.0f,18, vertices, indices);
    for (size_t i = 0; i < vertices.size(); i++) {
        VertexPositionColor new_v;
        new_v.position = vertices[i].position;
        new_v.color = Col4(
            new_v.position.x * 2.0f, 
            new_v.position.y * 2.0f, 
            new_v.position.z * 2.0f, 
            1.0f);
        new_vertices.push_back(new_v);

    }
    PtrDraw->CreateOriginalMesh(new_vertices, indices);
    PtrDraw->SetOriginalMeshUse(true);
    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(PtrDraw->GetOriginalMeshResource());
}
 赤くなっているところは球体を作成しているところです。前述したようにPNT球体が作成され、verticesおよびindices頂点の配列とインデックスの配列が代入されます。
 その後、PC頂点の配列であるnew_verticesに移植するわけですが、PNT頂点にはないカラー要素を持っているのでカラーグラデーションを、位置情報をもとに作成しています。
 その後
    PtrDraw->CreateOriginalMesh(new_vertices, indices);
    PtrDraw->SetOriginalMeshUse(true);
 この2行でオリジナルメッシュを作成し、そのメッシュを使用するという設定にします。描画コンポートに設定するメッシュは、一般的はリソースとして登録してそのポインタを保持します。そうすることで同じメッシュを複数のオブジェクトで使用できるようにします。
 しかし、上記の例のように他では使用しないオリジナルメッシュという形で登録することができます。このようにしておけば動的に頂点を変更してもほかのオブジェクトには影響を与えません。
 また影の描画にこのオリジナルメッシュを使用するので
    //影をつける
    auto ShadowPtr = AddComponent<Shadowmap>();
    ShadowPtr->SetMeshResource(PtrDraw->GetOriginalMeshResource());
 と設定します。

頂点の変更

 このサンプルは、PC頂点、PT頂点、PCT頂点についてはオリジナルメッシュを作成してます。
 それらは動的に頂点の変更がされるのですが、その変更に行動クラスを使ってます。以下がそのクラスです。ProjectBehavior.hに記述されてます。
//--------------------------------------------------------------------------------------
///頂点を変更する行動クラス
//--------------------------------------------------------------------------------------
class VertexBehavior : public Behavior {
    float m_TotalTime;

public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    @param[in]  GameObjectPtr   このコンポーネントを所持するゲームオブジェクト
    */
    //--------------------------------------------------------------------------------------
    VertexBehavior(const shared_ptr<GameObject>& GameObjectPtr) :
        Behavior(GameObjectPtr),
        m_TotalTime(0)
    {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~VertexBehavior() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief 伸び縮みする
    @tparam T   頂点の型
    @return なし
    */
    //--------------------------------------------------------------------------------------
    template<typename T>
    void ExpandAndContract() {
        float ElapsedTime = App::GetApp()->GetElapsedTime();
        m_TotalTime += ElapsedTime;
        if (m_TotalTime >= XM_2PI) {
            m_TotalTime = 0;
        }
        auto PtrDraw = GetGameObject()->GetDynamicComponent<StaticBaseDraw>();
        const vector<T>& BackupVec = PtrDraw->GetOriginalMeshResource()->GetBackupVerteces<T>();
        vector<T> new_vec;
        for (auto& v : BackupVec) {
            T new_v;
            new_v = v;
            auto Len = (sin(m_TotalTime) * 0.5f) + 1.0f;
            new_v.position.x *= Len;
            new_v.position.z *= Len;
            new_vec.push_back(new_v);
        }
        PtrDraw->UpdateVertices(new_vec);
    }

};
 このクラス内のExpandAndContract()テンプレート関数によって頂点の伸び縮みを表現します。
 行動クラスのメンバ関数テンプレート関数にすることで、複数の頂点型に対応した行動を作成することができます。
 注意したいのは描画コンポーネントの取り出しに
auto PtrDraw = GetGameObject()->GetDynamicComponent<StaticBaseDraw>();
 としているところです。StaticBaseDrawコンポーネントを取り出しいぇいるところです。こららはPCStaticDrawPCTStaticDrawなどの親クラスにあたり、共通の処理なので、GetDynamicComponent()関数で取り出します。この関数はダイナミックキャストで、コンポーネントの存在チェックをします。
 この行動クラスの呼び出しは、各球体クラスのOnUpdate()関数を使います。
 以下はPcSphere::OnUpdate()です。
void PcSphere::OnUpdate() {
    auto Beh = GetBehavior<VertexBehavior>();
    Beh->ExpandAndContract<VertexPositionColor>();
}
 このようにテンプレート関数を呼び出します。

PNT頂点

 BaceCrossで一番よく使用されるのはPNT頂点でしょう。これは位置、法線、テクスチャUVを持ちます。法線はライティング処理に必要です。
 このサンプルの右の4つはPNT頂点を持つオブジェクトです。表現方法が少しずつ違います。
 その中の左2つは通常の描画です。テクスチャが設定されてないときは白く描画されます。色を付ける場合はディフューズ色やエミッシブ色を調整してください。スペキュラーを設定する場合は右2つのように設定します。
 以下は、スペキュラー色をテクスチャ付きの球体に設定した例です。右から2番目のオブジェクトです。
//初期化
void PntSpSphere::OnCreate() {
    auto PtrTrans = GetComponent<Transform>();
    PtrTrans->SetScale(Vec3(1.0f, 1.0f, 1.0f));
    Quat Qt;
    Qt.identity();
    PtrTrans->SetQuaternion(Qt);
    PtrTrans->SetPosition(m_StartPos);

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

    //描画コンポーネント
    auto PtrDraw = AddComponent<PNTStaticDraw>();
    PtrDraw->SetSpecular(Col4(1.0f, 1.0f, 1.0f, 1.0f));
    PtrDraw->SetMeshResource(L"DEFAULT_SPHERE");
    if (m_TextureUse) {
        PtrDraw->SetTextureResource(L"SKY_TX");
    }
}
 赤くなっているところがスペキュラーの設定です。

モデルの描画

 モデル描画例として、ボーンアニメーション付きのモデルとついてないモデルのサンプルが実装されています。
 モデルのメッシュは、あらかじめFbx2BinツールによってFbxファイルからbmfファイルに変換しておきます。

ボーンモデル

 まず、ボーンモデルですが、BoneCharaクラスがそうです。モデルのファイルはシーンなどで以下のようにリソース化しておきます。
void Scene::CreateResourses() {
    wstring DataDir;
    //サンプルのためアセットディレクトリを取得
    App::GetApp()->GetAssetsDirectory(DataDir);
    //各ゲームは以下のようにデータディレクトリを取得すべき
    //App::GetApp()->GetDataDirectory(DataDir);

    //中略

    auto ModelMesh = MeshResource::CreateBoneModelMesh(DataDir, L"Chara_R.bmf");
    App::GetApp()->RegisterResource(L"Chara_R_MESH", ModelMesh);
    auto StaticModelMesh = MeshResource::CreateStaticModelMesh(DataDir, L"Character_01.bmf");
    App::GetApp()->RegisterResource(L"MODEL_MESH", StaticModelMesh);

    //スタティックモデル(マルチメッシュ)の通常リソース
    auto StaticMultiModelMesh = MultiMeshResource::CreateStaticModelMultiMesh(DataDir, L"ObjectOnly.bmf");
    App::GetApp()->RegisterResource(L"ObjectOnly_MESH", StaticMultiModelMesh);
    //ボーンモデル(マルチメッシュ)の通常リソース
    auto MultiModelMesh = MultiMeshResource::CreateBoneModelMultiMesh(DataDir, L"Object_WalkAnimation.bmf");
    App::GetApp()->RegisterResource(L"Object_WalkAnimation_MESH", MultiModelMesh);
}
 このようにしてリソース登録したL"Chara_R_MESH"を使用します。
 また、ここでマルチメッシュとして作成されるモデルの実装も行ってます。L"ObjectOnly_MESH"L"Object_WalkAnimation_MESH"です。この二つのモデルは1つのFBXファイルに複数のメッシュがある場合のモデルをデータ変換したものです。BaseCrossではマルチメッシュと称しています。
 以下はBoneChara::OnCreate()関数です。
//初期化
void BoneChara::OnCreate() {
    //初期位置などの設定
    auto Ptr = AddComponent<Transform>();
    Ptr->SetScale(0.5f, 0.5f, 0.5f);
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    Ptr->SetPosition(m_StartPos);

    Mat4x4 SpanMat; // モデルとトランスフォームの間の差分行列
    SpanMat.affineTransformation(
        Vec3(1.0f, 1.0f, 1.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f)
    );

    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<Shadowmap>();

    //影の形(メッシュ)を設定
    ShadowPtr->SetMeshResource(L"Chara_R_MESH");
    ShadowPtr->SetMeshToTransformMatrix(SpanMat);

    //描画コンポーネントの設定
    auto PtrDraw = AddComponent<PNTBoneModelDraw>();
    //描画するメッシュを設定
    PtrDraw->SetMeshResource(L"Chara_R_MESH");
    PtrDraw->SetMeshToTransformMatrix(SpanMat);

    PtrDraw->AddAnimation(L"Default", 0, 50, true, 20.0f);
    PtrDraw->ChangeCurrentAnimation(L"Default");

    //透明処理
    SetAlphaActive(true);

}
 ここではアニメーションを追加して、そのアニメーションをカレントアニメーションにしています。赤くなっているところです。
 アニメーションを動かすのにはBoneChara::OnUpdate()関数などでアニメーションの更新を行います。
//更新
void BoneChara::OnUpdate() {
    //アニメーションを更新する
    auto PtrDraw = GetComponent<PNTBoneModelDraw>();
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    PtrDraw->UpdateAnimation(ElapsedTime);
}
 以下はマルチメッシュであるBoneMultiMeshCharaBoneMultiMeshChara::OnCreate()です。
//初期化
void BoneMultiMeshChara::OnCreate() {
    //初期位置などの設定
    auto Ptr = AddComponent<Transform>();
    Ptr->SetScale(0.5f, 0.5f, 0.5f);
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    Ptr->SetPosition(m_StartPos);

    Mat4x4 SpanMat; // モデルとトランスフォームの間の差分行列
    SpanMat.affineTransformation(
        Vec3(1.0f, 1.0f, 1.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f)
    );

    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<Shadowmap>();

    //影の形(メッシュ)を設定
    ShadowPtr->SetMultiMeshResource(L"Object_WalkAnimation_MESH");
    ShadowPtr->SetMeshToTransformMatrix(SpanMat);

    //描画コンポーネントの設定
    auto PtrDraw = AddComponent<PNTBoneModelDraw>();
    //描画するメッシュを設定
    PtrDraw->SetMultiMeshResource(L"Object_WalkAnimation_MESH");
    PtrDraw->SetSamplerState(SamplerState::LinearWrap);
    PtrDraw->SetMeshToTransformMatrix(SpanMat);

    PtrDraw->AddAnimation(L"Default", 0, 30, true, 10.0f);
    PtrDraw->ChangeCurrentAnimation(L"Default");

}
 赤くなっているところに注意してください。SetMultiMeshResource()関数で、マルチメッシュを各コンポーネントに設定しています。
 もう一つ注意してほしいのは、単体のメッシュマルチメッシュは一つのコンポーネントに同居できないということです。
 以下は、OnUpdate()です。
//更新
void BoneMultiMeshChara::OnUpdate() {
    auto PtrDraw = GetComponent<PNTBoneModelDraw>();
    float ElapsedTime = App::GetApp()->GetElapsedTime();
    PtrDraw->UpdateAnimation(ElapsedTime);
}
 こちらは単体のメッシュと変わりありません。

スタティックモデル

 以下はスタティックモデルの作成です。StaticChara::OnCreate()関数に記述します。
//初期化
void StaticChara::OnCreate() {
    //初期位置などの設定
    auto Ptr = AddComponent<Transform>();
    Ptr->SetScale(0.5f, 0.5f, 0.5f);
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    Ptr->SetPosition(m_StartPos);

    Mat4x4 SpanMat; // モデルとトランスフォームの間の差分行列
    SpanMat.affineTransformation(
        Vec3(1.0f, 1.0f, 1.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f)
    );

    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<Shadowmap>();
    //影の形(メッシュ)を設定
    ShadowPtr->SetMeshResource(L"MODEL_MESH");
    ShadowPtr->SetMeshToTransformMatrix(SpanMat);

    auto PtrDraw = AddComponent<PNTStaticModelDraw>();
    PtrDraw->SetMeshResource(L"MODEL_MESH");
    PtrDraw->SetMeshToTransformMatrix(SpanMat);


}
 こちらもボーンモデル同様マルチメッシュのオブジェクトがあります。StaticMultiMeshCharaです。以下がStaticMultiMeshChara::OnCreate()です。
//初期化
void StaticMultiMeshChara::OnCreate() {
    //初期位置などの設定
    auto Ptr = AddComponent<Transform>();
    Ptr->SetScale(0.5f, 0.5f, 0.5f);
    Ptr->SetRotation(0.0f, 0.0f, 0.0f);
    Ptr->SetPosition(m_StartPos);

    Mat4x4 SpanMat; // モデルとトランスフォームの間の差分行列
    SpanMat.affineTransformation(
        Vec3(1.0f, 1.0f, 1.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f),
        Vec3(0.0f, 0.0f, 0.0f)
    );

    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<Shadowmap>();
    //影の形(メッシュ)を設定
    ShadowPtr->SetMultiMeshResource(L"ObjectOnly_MESH");
    ShadowPtr->SetMeshToTransformMatrix(SpanMat);

    auto PtrDraw = AddComponent<PNTStaticModelDraw>();
    PtrDraw->SetMultiMeshResource(L"ObjectOnly_MESH");
    PtrDraw->SetMeshToTransformMatrix(SpanMat);
}
 こちらも、赤くなっているところはマルチメッシュを登録しているところです。

GameStageへの配置

 以上が各オブジェクトの初期化や更新です。最後にGameStage::OnCreate()で各オブジェクトをAddGameObjectします。