007.頂点の変更とテクスチャラッピング(Dx12版)

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

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

 

図0007a

 


 動画は以下になります。

 

 

【サンプルのポイント】

 今項のサンプルのポイントは同じ描画を複数のオブジェクトで行う場合のサンプルです。例えばテクスチャやシェーダに渡すパラメータのみが違う、別のオブジェクトの場合、描画処理は1つのクラスで行ったほうが、同じようなコードをいくつも書く必要がなくなります。
 また、頂点の変更の方法も今項のテーマと言えます。ポジションを動的に変更する場合もありますが、一番ポピュラーなのはテクスチャUV値の変更でしょう。上記動画を見ればわかる通り、中心にあるオブジェクトは、テクスチャが流れるように描画されます。これは動的にテクスチャUV値の変更を行っています。

【共通解説】

 Dx12、Dx11両方に共通なのはシェーダーです。DxSharedプロジェクト内にシェーダファイルというフィルタがあり、そこに記述されてます。
 今回使用するシェーダは頂点シェーダとピクセルシェーダです。VertexPositionTexture型の頂点を持ち、コンスタントバッファからの入力で、位置を変更させています。
 またテクスチャも描画します。

 更新処理は動きは同じですが、Dx12版の更新処理で説明します。OnUpdate()関数には、それぞれの四角形を個別に更新する方法が記述されています。

【Dx12版解説】

 BaseCrossDx12.slnを開くと、BaseCrossDx12というメインプロジェクトがあります。この中のCharacter.h/cppが主な記述個所になります。

■初期化■

 今項で表示するオブジェクトは2種類です。すなわち、奥の壁になってるオブジェクトと、中心にあるテクスチャが流れるオブジェクトです。
 前者はWallSpriteクラスです。後者はSquareSpriteクラスです。どちらも、頂点とテクスチャUVを持つVertexPositionTexture型の頂点を持っています。コンスタントバッファも同じ型です。
 描画方法が同じなので、描画クラスを別に作成して各オブジェクトに、メンバとしてインスタンスを持たせます。そのクラスがPTSpriteDrawクラスということになります。
 Dx12版の初期化頂点の作成DX12リソースの初期化が主な仕事です。前者は各オブジェクトで行い、後者は描画クラスがあるので、そちらのほうで初期化を行います。
 頂点の初期化処理は、WallSpriteクラスは以下のようになってます。
float HelfSize = 0.5f;
//頂点配列(縦横10個ずつ表示)
vector<VertexPositionTexture> vertices = {
    { VertexPositionTexture(Vec3(-HelfSize, HelfSize, 0), Vec2(0.0f, 0.0f)) },
    { VertexPositionTexture(Vec3(HelfSize, HelfSize, 0), Vec2(10, 0.0f)) },
    { VertexPositionTexture(Vec3(-HelfSize, -HelfSize, 0), Vec2(0.0f, 10)) },
    { VertexPositionTexture(Vec3(HelfSize, -HelfSize, 0), Vec2(10, 10)) },
};
//インデックス配列
vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
//メッシュの作成(変更できない)
m_SquareMesh = MeshResource::CreateMeshResource(vertices, indices, false);
 ここでテクスチャUV値の設定である、赤になっている部分を見てみましょう。この記述で横に10個、縦に10個テクスチャを連続して張り付ける設定になります。

 SquareSpriteクラスの頂点は以下になります。
float HelfSize = 0.5f;
//頂点配列
m_BackupVertices = {
    { VertexPositionTexture(Vec3(-HelfSize, HelfSize, 0), Vec2(0.0f, 0.0f)) },
    { VertexPositionTexture(Vec3(HelfSize, HelfSize, 0), Vec2(4.0f, 0.0f)) },
    { VertexPositionTexture(Vec3(-HelfSize, -HelfSize, 0), Vec2(0.0f, 1.0f)) },
    { VertexPositionTexture(Vec3(HelfSize, -HelfSize, 0), Vec2(4.0f, 1.0f)) },
};
//インデックス配列
vector<uint16_t> indices = { 0, 1, 2, 1, 3, 2 };
//メッシュの作成(変更できる)
m_SquareMesh = MeshResource::CreateMeshResource(m_BackupVertices, indices, true);
 この頂点はWallSpriteクラスとは少し違います。まずテクスチャUV値(初期値)ですが、縦は1.0横は4.0に並ぶように作成します。また、頂点を変更するので頂点のバックアップを作成しておく必要があります。ですので、ローカル上に頂点の配列を作成するのではなく、m_BackupVerticesというメンバ変数に作成します。インデックスのほうは変更しないので、そのままローカル上で問題ありません。
 また、メッシュリソース作成時に頂点変更できるというフラグを設定して作成します。

■ルートシグネチャ作成■

 ルートシグネチャPTSpriteDraw::CreateRootSignature()関数で作成します。前項と同じ内容です。

■デスクプリタヒープ作成■

 PTSpriteDraw::CreateDescriptorHeap()関数で作成します。前項と同じ内容です。

■サンプラー作成■

 サンプラーPTSpriteDraw::CreateSampler()関数で作成します。今項はラッピング(包み込む)設定で行います。以下がその設定です。
void PTSpriteDraw::CreateSampler() {
    auto SamplerDescriptorHandle = m_SamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
    //ラッピングサンプラー
    DynamicSampler::CreateSampler(SamplerState::LinearWrap, SamplerDescriptorHandle);
}

■シェーダーリソースビュー作成■

 PTSpriteDraw::CreateShaderResourceView()関数で行います。前項と同じです。PTSpriteDrawクラスにはあらかじめテクスチャリソースを初期化しておく必要があります。それはPTSpriteDraw::OnCreate()関数Dx12リソース作成前に作成してます。

■コンスタントバッファ作成■

 PTSpriteDraw::CreateConstantBuffer()関数で行います。前項と同じです。

■パイプラインステート作成■

 PTSpriteDraw::CreatePipelineState()関数で行います。前項と同じです。
br />

■コマンドリスト作成■

 PTSpriteDraw::CreateCommandList()関数です。デフォルト処理です。

■コンスタントバッファの更新■

 PTSpriteDraw::UpdateConstantBuffer()関数です。パラメータにDiffuseSpriteConstantBuffer& CBuffを渡すようになってます。

■更新処理■

 このサンプルの更新処理は頂点の変更です。SquareSpriteクラスのみ頂点を変更します。実装はSquareSprite::UpdateVertex()関数で行います。
void SquareSprite::UpdateVertex(float ElapsedTime) {
    m_TotalTime += ElapsedTime;
    if (m_TotalTime > 1.0f) {
        m_TotalTime = 0;
    }

    auto Dev = App::GetApp()->GetDeviceResources();
    //頂点の変更
    vector<VertexPositionTexture> new_vertices;
    for (size_t i = 0; i < m_SquareMesh->GetNumVertices(); i++) {
        Vector2 UV = m_BackupVertices[i].textureCoordinate;
        if (UV.x == 0.0f) {
            UV.x = m_TotalTime;
        }
        else if (UV.x == 4.0f) {
            UV.x += m_TotalTime;
        }
        VertexPositionTexture v = VertexPositionTexture(
            m_BackupVertices[i].position,
            UV
        );
        new_vertices.push_back(v);
    }
    //メッシュの頂点の変更
    m_SquareMesh->UpdateVirtex(new_vertices);
}
 赤になっているところが頂点の変更です。バックアップの頂点と、変更箇所(UV値)を合わせて、new_vertices配列を作成します。最後にm_SquareMesh->UpdateVirtex()関数に渡します。

■描画処理■

 描画はPTSpriteDraw::DrawObject()関数で行います。この関数はメッシュリソースを引数に持ちます。コンスタントバッファの更新はその前に行っておきます。以下が今回の注目点です。
void PTSpriteDraw::DrawObject(const shared_ptr<MeshResource>& Mesh) {
    //中略
    Mesh->UpdateResources<VertexPositionTexture>(m_CommandList);
    //中略
}
 ここでは、メッシュのUpdateResources()関数を呼び出しています。ここの関数は、更新時に呼び出したUpdateVirtex()関数とは違います。メッシュリソースとテクスチャリソースは動的に中身を変更できるわけですが、その変更と、描画直前に呼び出すUpdateResources()関数は別物です。前者はコマンドリストがなくても実行できます。つまりメッシュリソースの内部に保持してあるバッファを更新するだけです。しかし、UpdateResources()関数はそのバッファとシェーダに渡すGPU向けリソースを結びつけます。そのため、描画直前に、メッシュと(必要であればテクスチャも)UpdateResources()関数を呼ぶ必要があります。

 以上、Dx12側の説明は終わりです。

【まとめ】

 今回は同じシェーダで描画できるオブジェクトを一つの描画クラスで描画する方法をサンプル化しました。
 これはフルバージョンにおける描画コンポーネントと同じような動きになります。
 Dx11にせよDx12にせよ描画処理は、GPUに対していろんな設定(パイプライン設定)を行わなければいけないので、各オブジェクトごとに描画をしていたのでは同じようなコードをあちこちに書かなければならないので大変です。バグのもとにもなります。
 ですので同じシェーダは同じ描画という設計にしたほうが、シンプルになると思います。