024.3D描画に必要な機能とは(Dx11版)

 このサンプルはSimplSample024というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルはDx11版しかありません。Dx12版はありませんのでご了承ください。
 サンプルを起動すると以下の画面が現れます。コントローラで操作でき、Aボタンでジャンプします。Bボタンで、いわゆるステージの切り替えができます。

 

図0024a

 

【サンプルのポイント】

 このサンプルには、いわゆる3D描画に必要な最低限の機能が実装されています。(サウンド及び、データ読み込み等はのぞく)。
 あらかじめ言っておきたいのは、この項に紹介する実装方法は、無限の実装方法の1例にすぎない、ということです。
 ゲームに限らず、アプリケーションを作成する場合、様々な機能を個別に実装していたのではコードが重なる部分が出てきます。そのため、よく使われる部分は一つのクラスや関数にまとめ、効率化をはかります。
 こういうよく使われる機能をまとめておくのが、いわゆるライブラリです。これが大きくなっていけばフレームワークと言ったりします。物理エンジンなんてエンジンという言い方をする場合もあります。
 個別のコンテンツ(アプリケーション)を作成する場合、そういったライブラリの存在は、とっても便利ですし、効率的に開発を進めることができます。
 しかし、往々にしてそれらのライブラリ個別の実装方法狭める結果となりうます。
 また実際に書けばかなり厄介な部分を、ライブラリ関数を呼び出したりクラスを実体化することで実装できてしまうので、プログラミングの学習には不向きな面も出てきてしまいます。

 BaseCrossでは、フルバージョンにおいては、初心者でもある程度記述できるようにライブラリの部分を強化してあります。しかし、フルバージョンの機能に頼りきってしまうと、自分の実装という部分が欠落していきます。
 そのためできるだけライブラリの部分を最小化したシンプルバージョンが存在するわけですが、本来ならSimpleSample021などをひな型にして、シェーダや基本的な設計を独自に考えてもらいたいわけですが、それは初心者には敷居が高いと思われますし、基本的なエンジンであるDX11の知識もある程度必要になります。
 そのためフルバージョンシンプルバージョンの中間にいちするようなサンプルを記述した次第です。
 このサンプルには様々な実装がされているわけですがLibsディレクトリにはありません。コンテンツ側に記述がされています。ぜひ実装をどんどん修正カスタマイズしてみてください。

■描画オブジェクト■

 このサンプルで実装される描画オブジェクトSimpleSample021から023で紹介したものと基本的には同じような実装ですが、汎用的に使えるように修正がされています。
 つまり、PNT頂点PCT頂点のメッシュの描画を全く別の描画オブジェクトを使うのではなく描画オブジェクトを階層的に記述し、Dx11へのアクセス部分を複数記述しなくても済むような設計になっています。
 具体的には以下の表を見てください。これらはDrawObjects.h/cppに記述されています。
系列 Simple系 Basic系 シャドウマップ
コンスタントバッファ SimpleConstants構造体 BasicConstants構造体 ShadowConstants構造体
描画データ(親) DrawObjectBase構造体 なし
描画データ(派生) SimpleDrawObject構造体 BcDrawObject構造体 ShadowmapObject構造体
描画オブジェクト(親) SimpleRendererクラス BcRendererクラス なし
描画オブジェクト(PCTStatic) SimplePCTStaticRendererクラス なし なし
描画オブジェクト(PNTStatic)
シャドウマップ無し
SimplePNTStaticRendererクラス BcPNTStaticRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTStatic)
シャドウマップあり
SimplePNTStaticRenderer2クラス BcPNTStaticRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTStaticModel)
シャドウマップ無し
SimplePNTStaticModelRendererクラス BcPNTStaticModelRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTStaticModel)
シャドウマップあり
SimplePNTStaticModelRenderer2クラス BcPNTStaticModelRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTBone)
シャドウマップ無し
SimplePNTBoneModelRendererクラス BcPNTBoneModelRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTBone)
シャドウマップあり
SimplePNTBoneModelRenderer2クラス BcPNTBoneModelRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTnTStatic) なし BcPNTnTStaticRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTnTStaticModel) なし BcPNTnTStaticModelRendererクラス ShadowmapRendererクラス
描画オブジェクト(PNTnTBoneModel) なし BcPNTnTBoneModelRendererクラス ShadowmapRendererクラス
 描画に使うクラスには系列があります。Simple系Basic系です。また、シャドウマップの描画クラスは別になります。
 Simple系は単純なシェーダです。ライティングをする場合PNT頂点でも、ライトは1個です。Basic系はリアルな表現をします。ライトは3つです。DirectXTKのシェーダをベースに、影表現を加えた形になっています。
 表のは、コスタントバッファ、描画データ、そして各頂点フォーマットごとの使用されるシェーダを表したものです。
 系列ごとに、コンスタントバッファは共通になっています。Simple系Basic系そしてシャドウマップ用コンスタントバッファがあります。
 コンスタントバッファを各ゲームオブジェクトが意識することはほとんどありません。描画データに適切なデータを指定すれば、その内容がコンスタントバッファに反映されます。
 描画データは系列共通の親として、DrawObjectBase構造体があります。このクラスを直接使用することはありません。派生クラスであるSimpleDrawObject構造体(Simple系)もしくはBcDrawObject構造体(Basic系)を使用します。シャドウマップの描画データは親クラスはありません。ShadowmapObjectを使用します。
 描画オブジェクトは各系列の派生クラスを使用します。頂点フォーマットやシャドウマップの有り無しなどで使用する描画オブジェクトが変わります。
 シャドウマップの描画オブジェクトはすべて共通(ShadowmapRenderer)です。PCT頂点の場合は、そもそもライティングをしませんのでシャドウマップは使用しません。
 各頂点ごとの描画オブジェクトは表のとおりです。例えばSimple系でシャドウマップ付きのスタティックなオブジェクトを実装したい場合は、描画データはSimpleDrawObject構造体、描画オブジェクトはSimplePNTStaticRenderer2クラスを使用します。

■描画ブジェクトの実装■

 描画オブジェクトGameObjectの派生クラスとして実装します。形式的には、配置されるキャラクタ等と同じです。しかし、配置オブジェクトとの違いは、自分自身はなにもUpdateしないしDrawもしないということです。OnUpdate関数空関数になってますしOnDraw()関数は、登録された(蓄積した)描画データを描画するという実装になります。描画データの登録毎ターン毎に初期化されます。その関係で、描画オブジェクトのAddGameObject()は、すべての配置オブジェクトの後に行います。GameObjectはステージ上の配列として管理されます。ですので、後にAddGameObject()されたオブジェクトは、OnUpdateにせよOnDrawにせよ、後に呼ばれることになります。
 実際にはGameStage::CreateDrawObjects()関数にまとめてあります。この関数はすべての3Dオブジェクトの配置が終わった後で呼ぶようにします。なお、2Dオブジェクト(スプライト)であるRotateSpriteクラスなどは、3Dの描画オブジェクトのあとに配置します。

■ゲームオブジェクトの実装■

 サンプルを起動すると、上記のような様々な表現が表示されますが、センターから右側の赤いボーンキャラ女の子そして2つの球体Simple系になります。Simple系Basic系の違いを確認するのはカメラを大きく引いてみるとよいでしょうフォグが実装されているのがBasic系です。
 ではゲームオブジェクトの基本的なつくりについて述べます。例として、Simple系のスタティックなモデルである、右側の女の子を例に説明します。以下の図版の女の子です。

 

図0024b

 

 この2つのゲームオブジェクトはStaticCharaクラスですCharacter.h/cppに記述があります。以下がヘッダ部です。
//--------------------------------------------------------------------------------------
/// Staticキャラ
//--------------------------------------------------------------------------------------
class StaticChara : public GameObject {
    Vec3 m_Scale;               ///<スケーリング
    Quat m_Qt;          ///<回転
    Vec3 m_Pos;             ///<位置
    Mat4x4 m_MeshToTransformMatrix;
    bool m_OwnShadowActive;
    //描画データ
    shared_ptr<SimpleDrawObject> m_PtrObj;
    //描画オブジェクト(weak_ptr)
    weak_ptr<SimplePNTStaticModelRenderer2> m_Renderer;
    //シャドウマップ用描画データ
    shared_ptr<ShadowmapObject> m_PtrShadowmapObj;
    //シャドウマップ描画オブジェクト(weak_ptr)
    weak_ptr<ShadowmapRenderer> m_ShadowmapRenderer;
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief コンストラクタ
    @param[in]  StagePtr    ステージのポインタ
    @param[in]  StartPos    位置
    @param[in]  OwnShadowActive 影描画するかどうか
    */
    //--------------------------------------------------------------------------------------
    StaticChara(const shared_ptr<Stage>& StagePtr, const Vec3& StartPos,bool OwnShadowActive);
    //--------------------------------------------------------------------------------------
    /*!
    @brief デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~StaticChara();
    //--------------------------------------------------------------------------------------
    /*!
    @brief 初期化
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate() override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 更新
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnUpdate()override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief  シャドウマップの描画処理(仮想関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnDrawShadowmap() override;
    //--------------------------------------------------------------------------------------
    /*!
    @brief 描画
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnDraw()override;
};
 いろんなプライベートデータがあります。まずm_Scale、m_Qt、m_Posですが、それぞれ、スケーリング、回転、位置の初期値を入れておく変数です。このゲームオブジェクトはRigidbodyを持っています。ですので、ゲーム動作中はこれらの変数を使用することはありません。しかし、リスポーンする場合などに初期値に戻るのような処理をする場合にこれらの変数はとっておくとよいでしょう。
 m_MeshToTransformMatrixは、モデルRigidbody上のデータの差分を記述しておく行列です。多くのケースで、ゲーム上の単位モデルの単位が違うことがあります。しかしその差分を行列化しておくことで、単位の変換が容易になります。
 m_OwnShadowActive影を受け止めるかどうかです。シャドウマップではありません。シャドウマップ影を出すかどうかです。前のサンプルで述べた通り、影には出す側と受ける側があります。このフラグは受ける側のフラグです。描画されている女の子は2体ありまして、向かって右のほうは影を受け止めます。このフラグはコンストラクタで渡されます。
 m_PtrObjは描画データのポインタ(shared_ptr)です。前のサンプルでは、関数内のローカル変数になってましたが、shared_ptrに最適化しました。こうしておくことで、描画オブジェクトにポインタを渡せますので、負荷は最小限になります。
 m_Renderer描画オブジェクトのポインタです。こちらはweak_ptrとなっています。描画オブジェクトはゲームステージにshared_ptrとして実装されます。各オブジェクトでは自分自身を描画するオブジェクトを保持しておくと便利ですが、そのままshared_ptrで持つと、持ち合いになってしまう危険(現時点ではならないが、実装を進める中でなってしまう危険)があります。ですので、weak_ptrとして持ちます。
 shared_ptrweak_ptrのどちらで持つかは、設計の考え方に深く影響します。このサンプルでは、一部の例外を除き、実装はshared_ptr、その参照はweak_ptrという考え方で実装されます。この考え方を徹底しておくと、まずメモリリークが発生することはありません。
 シャドウマップ用のものm_PtrShadowmapObjとm_ShadowmapRendererも同様の考え方です。描画データはshared_ptr、描画オブジェクトはweak_ptrです。
 さて、実体ですが、まず、ポイントはStaticChara::OnCreate()関数です。
void StaticChara::OnCreate() {

    //Rigidbodyの初期化
    auto PtrGameStage = GetStage<GameStage>();
    Rigidbody body;
    body.m_Owner = GetThis<GameObject>();
    body.m_Mass = 1.0f;
    body.m_Scale = m_Scale;
    body.m_Quat = m_Qt;
    body.m_Pos = m_Pos;
    body.m_CollType = CollType::typeCAPSULE;
    body.m_IsFixed = true;
//      body.m_IsDrawActive = true;
    body.SetToBefore();
    PtrGameStage->AddRigidbody(body);

    //メッシュとトランスフォームの差分の設定
    m_MeshToTransformMatrix.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, -1.0f, 0.0f)
    );
    //行列の定義
    Mat4x4 World;
    World.affineTransformation(
        m_Scale,
        Vec3(0, 0, 0),
        m_Qt,
        m_Pos
    );
    //差分を計算
    World = m_MeshToTransformMatrix * World;
    //メッシュ
    auto MeshPtr = App::GetApp()->GetResource<MeshResource>(L"MODEL_MESH");

    //描画データの構築
    m_PtrObj = make_shared<SimpleDrawObject>();
    m_PtrObj->m_MeshRes = MeshPtr;
    //アフィン変換
    m_PtrObj->m_WorldMatrix = World;
    m_PtrObj->m_Camera = GetStage<Stage>()->GetCamera();
    m_PtrObj->m_UsedModelColor = false;
    m_PtrObj->m_UsedModelTextre = true;
    m_PtrObj->m_OwnShadowmapActive = m_OwnShadowActive;
    m_PtrObj->m_ShadowmapUse = true;

    //シャドウマップ描画データの構築
    m_PtrShadowmapObj = make_shared<ShadowmapObject>();
    m_PtrShadowmapObj->m_MeshRes = MeshPtr;
    //描画データの行列をコピー
    m_PtrShadowmapObj->m_WorldMatrix = World;
    m_PtrShadowmapObj->m_Camera = GetStage<Stage>()->GetCamera();
}
 まず述べておきたいのはシンプルバージョンのサンプルですので、座標変換の基本は行列になります。フルバージョンではスケーリング、回転、位置を個別にコンポーネントに指定すれば、ワールド行列の作成はライブラリがやってくれますが、このサンプルではそうはいきません。最終的に描画に使用するワールド行列は各オブジェクトごとに設定します。
 ワールド行列を設定してるのは、m_PtrObjとm_PtrShadowmapObjの初期化です。また、ここでは、このオブジェクトを描画するのに使用するカメラも設定します。このカメラの値を、ステージから持ってくるのではなく独自の設定にすると、オブジェクト特有のカメラを使用できます。
 また、このキャラクタはRigidbodyを使用してます。衝突判定のコリジョンを表示したい場合は、
、
        body.m_IsDrawActive = true;


 のようにコメントを削除します。

■ゲームオブジェクトの更新■

 このキャラクタは動きませんが、動かす場合はStaticChara::OnUpdate()に記述するか、あるいはStaticChara::OnUpdate2()を多重定義して記述します。そのあたりはプレイヤーが参考になるでしょう。

■ゲームオブジェクトの描画■

 描画はシャドウマップの描画とオブジェクトの描画があります。
、
void StaticChara::OnDrawShadowmap() {
    m_PtrShadowmapObj->m_Camera = GetStage<Stage>()->GetCamera();
    auto shptr = m_ShadowmapRenderer.lock();
    if (!shptr) {
        shptr = 
        GetStage<Stage>()->FindTagGameObject<ShadowmapRenderer>(L"ShadowmapRenderer");
        m_ShadowmapRenderer = shptr;
    }
    shptr->AddDrawObject(m_PtrShadowmapObj);
}

void StaticChara::OnDraw() {
    
    m_PtrObj->m_Camera = GetStage<Stage>()->GetCamera();
    auto shptr = m_Renderer.lock();
    if (!shptr) {
        shptr = 
        GetStage<Stage>()->FindTagGameObject<SimplePNTStaticModelRenderer2>(L"SimplePNTStaticModelRenderer2");
        m_Renderer = shptr;
    }
    shptr->AddDrawObject(m_PtrObj);
}
 ここで気を付けたいのは、カメラの設定を忘れないようにすることです。オブジェクトは静止していても、カメラは動いている可能性があります。