407.ジオメトリシェーダによる描画


ジオメトリシェーダとは

 BaseCrossには、頂点シェーダ、ピクセルシェーダのほかにジオメトリシェーダと計算シェーダ用のクラスを作成できる仕組みが用意されています。
 このほかにDirectX11が持ってるシェーダにはハルシェーダとドメインシェーダがありますが、こちら用にはクラスが用意されてません。ですのでハルシェーダとドメインシェーダの実装は不可能ではありませんが、0からシェーダへのアクセスクラスを作る必要があります。
 ジオメトリシェーダと計算シェーダは、クラスが用意されていますが、OnDraw()に直結するのはジオメトリシェーダです。計算シェーダは、計算処理をCPUで行うのではなく、GPUに計算させる手法で、扱いが別になります。

 というわけで、この項ではジオメトリシェーダのサンプルの紹介をするわけですが、ではジオメトリシェーダとは何か、と簡単に説明します。
 これまでのサンプルで、頂点処理は頂点シェーダピクセル処理はピクセルシェーダが行い、それぞれどのような内容を記述すべきかが見えてきたと思います。このセットは単位を頂点、もしくはピクセルで扱うことで、細かな制御が可能になります。
 しかし、時には、描画プリミティブ単位で考えたいことがあります。描画プリミティブ単位とは三角形とかとかです。
 DirectX11は、描画するときに、渡した頂点バッファを、どのようなプリミティブ単位で描画すべきかを指定します。一般的な3Dや2Dであれば、三角形単位、です。
 ところが頂点シェーダが扱うのは点単位のみです。すると、シェーダを記述する際、いまどの頂点に対する計算を行おうとしているのか、あるいは、現在の頂点は、三角形のどの位置にあるものなのかわかりません。(インデックスは持ってこれるのですが、ここでほしいのは、三角形の中のどの頂点かということです)
 ジオメトリシェーダはこの不満に答えてくれます。そして、これこそジオメトリシェーダの機能なのですが、シェーダ内で頂点を増やすことができるのです。
 このことにより、たとえば、レーシングゲームなどで、同じシーンを複数のカメラで表示することもあると思います。
 このとき、メインのプレイヤーのワールド行列等情報と、他のビューのカメラ情報があれば、同時に複数のビューに記述することが可能です。
 また、1つの三角形を細かい三角形に分割することにより、1つのオブジェクトをバラバラにすることも可能です。
 そのほか、アイディア次第では、表現方法が大幅に増えます。
 なおジオメトリシェーダは、頂点シェーダとピクセルシェーダの間に実装されます。

ジオメトリシェーダの実装

 ではFullSample4071ディレクトリのソリューションを開き、リビルド、実行してみましょう。以下の画面が現れます。

 

図0407a

 

 この画面は、FullSample4071の画面です。ここでは、ジオメトリックシェーダを使って、3つ描画していいます。
 今回は1回(センターのオブジェクトの描画)時に、3つのワールド行列を渡します。そして、そのワールド行列の数だけ(つまり3つに)三角形を増やします。
 まず、シェーダーを記述します。GameSourcesプロジェクトShaderSourcesフィルタに記述し ます。以下はSimpleInc.hlsli(ヘッダファイル)です。
cbuffer SimpleConstantBuffer : register(b0)
{
    float4x4 World[3] : packoffset(c0);
    float4x4 View   : packoffset(c12);
    float4x4 Projection : packoffset(c16);
    float4 LightDir : packoffset(c20);
    float4 Param : packoffset(c21);
};


struct VertexShaderInput
{
    float4 pos : SV_Position;
    float3 norm : NORMAL;
    float4 color : COLOR0;
};

typedef VertexShaderInput GeometryShaderInput;

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float4 color : COLOR0;
};
 ここではコンスタントバッファ(シェーダ側)、頂点定義などが宣言されています。
 赤くなっているところは注意点です。ワールド行列が3つ入力されます。

 続いて、頂点シェーダです。VSSimpleBase.hlslです。
#include "SimpleInc.hlsli"

GeometryShaderInput main(VertexShaderInput input)
{
    //ジオメトリシェーダーに渡す変数
    //入力をそのまま出力する
    GeometryShaderInput vertexShaderOutput
        = (GeometryShaderInput)input;
    return vertexShaderOutput;
}
 このように、そのままジオメトリシェーダに渡しています。

 続いて、ジオメトリシェーダです。GSSimpleBase.hlslとなります。
#include "SimpleInc.hlsli"

[maxvertexcount(9)]
void main(
    triangle GeometryShaderInput input[3],
    inout TriangleStream< PixelShaderInput > output
)
{
    for (uint count = 0; count < 3; count++){
        for (uint i = 0; i < 3; i++)
        {
            PixelShaderInput element;
            //頂点の位置を変換
            float4 pos = float4(input[i].pos.xyz, 1.0f);
            float4 Col;
            if (pos.y > 0){
                switch (count){
                case 0:
                    Col = float4(1.0f, 0.0f, 0.0f, 0.0f);
                    break;
                case 1:
                    Col = float4(0.0f, 1.0f, 0.0f, 0.0f);
                    break;
                case 2:
                    Col = float4(0.0f, 0.0f, 1.0f, 0.0f);
                    break;
                default:
                    Col = float4(1.0f, 1.0f, 1.0f, 1.0f);
                    break;
                }
            }
            else if (pos.y < 0){
                switch (count){
                case 0:
                    Col = float4(0.0f, 1.0f, 0.0f, 0.0f);
                    break;
                case 1:
                    Col = float4(0.0f, 0.0f, 1.0f, 0.0f);
                    break;
                case 2:
                    Col = float4(1.0f, 0.0f, 0.0f, 0.0f);
                    break;
                default:
                    Col = float4(1.0f, 1.0f, 1.0f, 1.0f);
                    break;
                }
            }
            else{
                Col = float4(1.0f, 1.0f, 1.0f, 1.0f);
            }
            //ワールド変換
            pos = mul(pos, World[count]);
            //ビュー変換
            pos = mul(pos, View);
            //射影変換
            pos = mul(pos, Projection);
            //ピクセルシェーダに渡す変数に設定
            element.pos = pos;
            //ライティング用に法線をワールド変換して設定
            element.norm = mul(input[i].norm, (float3x3)World[count]);
            //頂点色を設定
            element.color = Col;
            //頂点を追加
            output.Append(element);
        }
        //一つの三角形をストリームに送る
        output.RestartStrip();
    }
}
 コードの全般は、左右のオブジェクトの色を変更する処理です。注意点は赤くなっているところです。
[maxvertexcount(9)]
void main(
    triangle GeometryShaderInput input[3],
    inout TriangleStream< PixelShaderInput > output
)
 はmain()に渡す変数の宣言ですが、[maxvertexcount(9)]というのは作成する頂点の最大数です。ここでは、3つの頂点(つまり三角形)から3つの三角形を作りますので9になります。
 triangle GeometryShaderInput input[3]というのは、まさに入力される三角形の頂点データです。
 inout TriangleStream< PixelShaderInput > outputはピクセルシェーダに渡す変数です。構造体の型を記述しておきます。  内容的には、1つの三角形を元に3つの三角形を作成して、ピクセルシェーダに渡すわけですが、1つの頂点ができた段階で
    //頂点を追加
    output.Append(element);
 のように頂点を出力に送ります。
 また、1つの三角形分の頂点を送った段階で、
    //一つの三角形をストリームに送る
    output.RestartStrip();
 のように、いったん締めます。そうして3個の三角形をoutputに送ります。

 ピクセルシェーダについては、特別な記述はありませんので省略します。

シェーダクラスの実装

 前項までで説明しましたように、この後、C++側とのインターフェイスとしてコンスタントバッファ構造体とシェーダクラスを作成します。ProjectShader.h/cppに記述します。以下はヘッダ部ですが
//カスタムシャドウマップ用コンスタントバッファ構造体
struct CustomShadowmapConstantBuffer
{
    Mat4x4 mWorld[3];
    Mat4x4 mView;
    Mat4x4 mProj;
    CustomShadowmapConstantBuffer() {
        memset(this, 0, sizeof(CustomShadowmapConstantBuffer));
    };
};
//シェーダ宣言(マクロ使用)
DECLARE_DX11_CONSTANT_BUFFER(CBCustomShadowmap, CustomShadowmapConstantBuffer)
DECLARE_DX11_VERTEX_SHADER(VSCustomShadowmap, VertexPositionNormalColor)
DECLARE_DX11_GEOMETRY_SHADER(GSCustomShadowmap)
//カスタム描画コンスタントバッファ構造体
struct CustomDrawConstantBuffer
{
    Mat4x4 World[3];
    Mat4x4 View;
    Mat4x4 Projection;
    Vec4 LightDir;
    Vec4 Param; //汎用パラメータ
    CustomDrawConstantBuffer() {
        memset(this, 0, sizeof(CustomDrawConstantBuffer));
    };
};
//シェーダ宣言(マクロ使用)
DECLARE_DX11_CONSTANT_BUFFER(CBCustomDraw, CustomDrawConstantBuffer)
DECLARE_DX11_VERTEX_SHADER(VSCustomDraw, VertexPositionNormalColor)
DECLARE_DX11_GEOMETRY_SHADER(GSCustomDraw)
DECLARE_DX11_PIXEL_SHADER(PSCustomDraw)
 ここにはシャドウマップ用のも記述がありますが、説明は本体部分とします。赤くなっている部分がジオメトリシェーダ用クラスのマクロです。以下は実体部です。
//シェーダ定義(マクロ使用)
IMPLEMENT_DX11_CONSTANT_BUFFER(CBCustomShadowmap)
IMPLEMENT_DX11_VERTEX_SHADER(VSCustomShadowmap, 
    App::GetApp()->GetShadersPath() + L"VSCustomShadowmap.cso")
IMPLEMENT_DX11_GEOMETRY_SHADER(GSCustomShadowmap, 
    App::GetApp()->GetShadersPath() + L"GSCustomShadowmap.cso")

IMPLEMENT_DX11_CONSTANT_BUFFER(CBCustomDraw)
IMPLEMENT_DX11_VERTEX_SHADER(VSCustomDraw, 
    App::GetApp()->GetShadersPath() + L"VSSimpleBase.cso")
IMPLEMENT_DX11_GEOMETRY_SHADER(GSCustomDraw, 
    App::GetApp()->GetShadersPath() + L"GSSimpleBase.cso")
IMPLEMENT_DX11_PIXEL_SHADER(PSCustomDraw, 
    App::App::GetApp()->GetShadersPath() + L"PSSimpleBase.cso")
 このようにシェーダクラスを作成します。

描画コンポーネントの作成

 この項では描画コンポーネント作成のサンプルも兼ねております。ですのでOnDraw()関数に記述するのではなく描画コンポーネントをオブジェクトに実装します。
 ここで作成するコンポーネントシャドウマップ用オブジェクト描画用です。説明はオブジェクト描画用のみ行います。
 以下はCharacter.hにあります、CustomPNCStaticDrawコンポーネントのヘッダ部です。
//--------------------------------------------------------------------------------------
//  ジオメトリシェーダを使った独自の描画コンポーネント
//--------------------------------------------------------------------------------------
class CustomPNCStaticDraw : public StaticBaseDraw {
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief  コンストラクタ
    @param[in]  GameObjectPtr   ゲームオブジェクト
    */
    //--------------------------------------------------------------------------------------
    explicit CustomPNCStaticDraw(const shared_ptr<GameObject>& GameObjectPtr);
    //--------------------------------------------------------------------------------------
    /*!
    @brief  デストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~CustomPNCStaticDraw() {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  OnCreate処理
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate()override {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  OnUpdate処理(空関数)
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnUpdate()override {}
    //--------------------------------------------------------------------------------------
    /*!
    @brief  OnDraw処理
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnDraw()override;
};
 親クラスとなっていますStaticBaseDrawDrawComponentの派生クラスとなっています。
 DrawComponentの派生クラス(子や孫でも可)として継承クラスを作ることで描画コンポーネントになります。
 以下は実体部です。Character.cppに記述があります。
void CustomPNCStaticDraw::OnDraw() {
    auto PtrGameObject = GetGameObject();
    //メッシュがなければ描画しない
    auto MeshRes = GetMeshResource();
    if (!MeshRes) {
        throw BaseException(
            L"メッシュが作成されていません",
            L"if (!MeshRes)",
            L"CustomPNCStaticDraw::OnDraw()"
        );
    }

    auto Dev = App::GetApp()->GetDeviceResources();
    auto pID3D11DeviceContext = Dev->GetD3DDeviceContext();
    auto RenderState = Dev->GetRenderState();

    auto PtrT = PtrGameObject->GetComponent<Transform>();
    //カメラを得る
    auto PtrCamera = PtrGameObject->OnGetDrawCamera();
    //カメラの取得
    Mat4x4 View, Proj, WorldViewProj;
    View = PtrCamera->GetViewMatrix();
    Proj = PtrCamera->GetProjMatrix();

    //コンスタントバッファの設定
    CustomDrawConstantBuffer cb1;
    //行列の設定(転置する)
    cb1.World[0] = transpose(PtrT->GetWorldMatrix());
    Mat4x4 Left;
    Left.translation(Vec3(-5.0f, 0, 0));
    Left = PtrT->GetWorldMatrix() * Left;
    cb1.World[1] = transpose(Left);
    Mat4x4 Right;
    Right.translation(Vec3(5.0f, 0, 0));
    Right = PtrT->GetWorldMatrix() * Right;
    cb1.World[2] = transpose(Right);
    cb1.View = transpose(View);
    cb1.Projection = transpose(Proj);
    //ライトの設定
    auto PtrLight = PtrGameObject->OnGetDrawLight();
    cb1.LightDir = PtrLight.m_Directional;
    cb1.LightDir.w = 1.0f;

    //コンスタントバッファの更新
    pID3D11DeviceContext->UpdateSubresource(CBCustomDraw::GetPtr()->GetBuffer(), 0, nullptr, &cb1, 0, 0);
    //ストライドとオフセット
    UINT stride = MeshRes->GetNumStride();
    UINT offset = 0;
    //頂点バッファの設定
    pID3D11DeviceContext->IASetVertexBuffers(0, 1, MeshRes->GetVertexBuffer().GetAddressOf(), &stride, &offset);
    //インデックスバッファのセット
    pID3D11DeviceContext->IASetIndexBuffer(MeshRes->GetIndexBuffer().Get(), DXGI_FORMAT_R16_UINT, 0);
    //描画方法(3角形)
    pID3D11DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    //ステータスのポインタ
    //テクスチャを取得
    ID3D11ShaderResourceView* pNull[1] = { 0 };
    ID3D11SamplerState* pNullSR[1] = { 0 };
    //半透明処理
    pID3D11DeviceContext->OMSetBlendState(RenderState->GetAlphaBlendEx(), nullptr, 0xffffffff);
    //デプスステンシルは使用する
    pID3D11DeviceContext->OMSetDepthStencilState(RenderState->GetDepthDefault(), 0);
    //シェーダの設定
    pID3D11DeviceContext->VSSetShader(VSCustomDraw::GetPtr()->GetShader(), nullptr, 0);
    pID3D11DeviceContext->GSSetShader(GSCustomDraw::GetPtr()->GetShader(), nullptr, 0);
    pID3D11DeviceContext->PSSetShader(PSCustomDraw::GetPtr()->GetShader(), nullptr, 0);
    //インプットレイアウトの設定
    pID3D11DeviceContext->IASetInputLayout(VSCustomDraw::GetPtr()->GetInputLayout());
    //コンスタントバッファの設定
    ID3D11Buffer* pConstantBuffer = CBCustomDraw::GetPtr()->GetBuffer();
    pID3D11DeviceContext->VSSetConstantBuffers(0, 1, &pConstantBuffer);
    pID3D11DeviceContext->GSSetConstantBuffers(0, 1, &pConstantBuffer);
    pID3D11DeviceContext->PSSetConstantBuffers(0, 1, &pConstantBuffer);

    //レンダリングステート
    pID3D11DeviceContext->RSSetState(RenderState->GetCullFront());
    //描画
    pID3D11DeviceContext->DrawIndexed(MeshRes->GetNumIndicis(), 0, 0);
    //レンダリングステート
    pID3D11DeviceContext->RSSetState(RenderState->GetCullBack());
    //描画
    pID3D11DeviceContext->DrawIndexed(MeshRes->GetNumIndicis(), 0, 0);
    //後始末
    Dev->InitializeStates();
}
 赤くなっているところがジオメトリシェーダに関連する部分です。

描画コンポーネントのオブジェクトへの実装

 このように描画コンポーネントを作成すればほかのコンポーネントのように利用できます。またこのようにしておくとほかのオブジェクトでも、汎用的に使用できるようになります。以下は、コンポーネントの実装です。CustomDrawOctahedron::OnCreate()関数です。
void CustomDrawOctahedron::OnCreate() {
    auto Ptr = AddComponent<Transform>();
    Ptr->SetScale(m_StartScale);
    Ptr->SetPosition(m_StartPos);
    Ptr->SetRotation(m_StartRotation);
    vector<VertexPositionNormalTexture> vertices;
    vector<uint16_t> indices;
    //正8面体
    MeshUtill::CreateOctahedron(1.0f, vertices, indices);
    for (size_t i = 0; i < vertices.size(); i++) {
        VertexPositionNormalColor new_v;
        new_v.position = vertices[i].position;
        new_v.normal = vertices[i].normal;
        new_v.color = Col4(1.0f, 1.0f, 1.0f, 1.0f);
        m_BackupVertices.push_back(new_v);
    }
    auto PtrDraw = AddComponent<CustomPNCStaticDraw>();
    PtrDraw->CreateOriginalMesh(m_BackupVertices, indices);
    PtrDraw->SetOriginalMeshUse(true);
    //影をつける(シャドウマップを描画する)
    auto ShadowPtr = AddComponent<CustomShadowmap>();
    ShadowPtr->SetMeshResource(PtrDraw->GetMeshResource());
    //透明処理(描画順制御のため)
    SetAlphaActive(true);
}
 このように実装します。

ジオメトリシェーダによる3角形の分割

 続くFullSample40723角形の分割のサンプルです。
 こちらは描画コンポーネントは作成していません。FullSample4072のソリューションを開くと以下の画面が出ます。

 

図0407b

 

 このシェーダはマイクロソフト社のジオメトリシェーダのサンプルを引用したものです。各三角形を分割して4つの三角形を作成しています。
 詳細はコードを参照ください。
 今項はジオメトリシェーダの使い方を説明しました。