15.シェーダーを自作する(Dx11版)

1501.コンピュートシェーダーで計算する(1)


 このサンプルはFullSample501というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルは現在Dx12版はありません。

 

図1501a

 


 シェーダーの自作はシンプルバージョンでは必須になっていますがフルバージョンの場合は、ほとんどの場合描画コンポーネントに隠されているので、あまり目に触れることはありません。
 しかし、特別な演出など必要になったときに、いつでもシェーダーが書ける環境にないと不便です。
 そこでこの章ではフルバージョンでのシェーダの作成方法を説明します。
 まず手始めにコンピュートシェーダーを紹介します。
 というのは頂点シェーダーピクセルシェーダーはGoogle先生に聞けば、いろんなテクニックが紹介されています。
 BaseCross64でもシンプルバージョンで基本的な頂点シェーダーピクセルシェーダーは紹介されています。
 しかしコンピュートシェーダーはなかなかサンプルがありません。あっても、ちょっと高度な計算(わざわざGPUに計算させるので当たり前といえば当たり前なのですが)が多いのも事実です。
 そのために初心者がとっつきやすいように、本当に最低限の計算をするコンピュートシェーダーを紹介します。

コンピュートシェーダーとは何か

 そもそもコンピュートシェーダーとは何でしょうか?
 計算シェーダとも言われ、ようはGPUで計算をしてもらう仕組みです。
 GPUは内部で高度な浮動小数点演算を高速で行うので、計算が得意と言えば得意なチップです。
 しかしほとんどの場合は描画処理に使われるので、例えばUpdate処理の間は、休んでいます。
 しかし、Update処理の中では、衝突判定や物理計算、あるいは大量のデータのソートなど、CPUの計算ではアップアップする演算が続きます。
 コンピュートシェーダーを使うことで、大忙しのCPUの処理をある程度任せることができます。

最低限の計算

 このサンプルではGPUに最低限の計算をさせています。どのような計算かというと、sin計算です。いったりきたりするオブジェクトはよくサインカーブを使いますが、それをGPUに計算させます。
 ですから、この処理は通常は全くと言っていいほどコンピュートシェーダーを使う理由はありません。
 しかし、単純な計算を体験することでコンピュートシェーダーの理解の助けになればと思っています。

シェーダーの作成

 以下サンプルではすでに実装済みですが、フルバージョンにおけるシェーダーの作成は、いろいろ方法がありますが、まず、コンテンツのGameSourcesディレクトリの中にShadersといったシェーダー専用ディレクトリを作るといいでしょう。
 そしてソリューションエクスプローラGameSourcesフィルタShadersフィルタを作成し、右クリックでその中の追加-新しい項目で、以下の画面でCSCalcbody.hlslという名の計算シェーダを作成します。

 

図1501b

 

 作成したシェーダーのプロパティの全般でシェーダモデルShader Model 5.0 (/5_0)にし、出力ファイルが以下のようになるのを確認してください。

 

図1501c

 

 そして以下を記述します。
//--------------------------------------------------------------------------------------
// コンスタントバッファ
//--------------------------------------------------------------------------------------
cbuffer CB : register(b0)
{
    float4  g_paramf;
};

//--------------------------------------------------------------------------------------
// エレメントデータ
//--------------------------------------------------------------------------------------
struct ElemData
{
    float4 pos;
};

//入出力用バッファ
RWStructuredBuffer<ElemData> posData : register(u0);
//共有変数
groupshared float4 sharedPos;


[numthreads(1, 1, 1)]
void main( uint3 DTid : SV_DispatchThreadID )
{
    sharedPos = posData[DTid.x].pos;
    GroupMemoryBarrierWithGroupSync();
    sharedPos.x = sin(g_paramf.x);
    GroupMemoryBarrierWithGroupSync();
    posData[DTid.x].pos = sharedPos;
}
 一番重要な部分は赤くなっているところです。ここでサインカーブをポジションのXに計算します。
 ようはこの1行を書くために、GPUとのインターフェイスを作成するのがこのサンプルです。

コンスタントバッファとシェーダー作成

 しれではCPP側を記述します。Character.h/cppMoveBoxというオブジェクトを記述しますが、その前に、シェーダークラスとコンスタントバッファクラスを作成します。
 作成するといってもそれぞれマクロを実装するだけです。
 まずコンスタントバッファクラスですが、Character.hに以下を記述します。
    struct CB
    {
        // paramf[0] == Totaltime
        float paramf[4];
    };
    //コンスタントバッファのヘッダ部
    DECLARE_DX11_CONSTANT_BUFFER(ConstantBufferCalcbody, CB)
 マクロを追いかけていくとわかりますが、これでConstantBufferCalcbodyクラスが宣言されます。
 つづいて実体です。Character.cppに以下を記述します。
    //コンスタントバッファの実体部
    IMPLEMENT_DX11_CONSTANT_BUFFER(ConstantBufferCalcbody)
 これで終わりです。
 続いてコンピュートシェーダクラスです。Character.hに以下を記述します。
    DECLARE_DX11_COMPUTE_SHADER(ComputeSaderCalcbody)
 続いて実体ですが、Character.cppに以下を記述します。
    //CSの実体部
    IMPLEMENT_DX11_COMPUTE_SHADER(ComputeSaderCalcbody, App::GetApp()->GetShadersPath() + L"CSCalcbody.cso")
 これでしまいです。シェーダーの実体では、コンパイルされたシェーダである.csoファイルが指定されているのがわかります。
 このようにBaseCross64では、コンスタントバッファやシェーダー(頂点、ピクセル、ジオメトリ、コンピュートシェーダ)は同様のマクロで作成します。

ゲームオブジェクト宣言

 ゲームオブジェクトはMoveBoxです。まずヘッダ部ですが
class MoveBox : public GameObject {
    // エレメントデータの構造体
    struct Element
    {
        XMFLOAT4 pos;
    };
    //エレメントバッファ
    ComPtr<ID3D11Buffer> m_Buffer;
    //アクセスビュー
    ComPtr < ID3D11UnorderedAccessView>  m_UAV;
    //リードバックバッファ
    ComPtr<ID3D11Buffer> m_ReadBackBuffer;
    //トータルアイム
    float m_TotalTime;
    Vec3 m_Scale;
    Vec3 m_Rotation;
    Vec3 m_Position;
    Vec3 m_Velocity;
public:
    //構築と破棄
    MoveBox(const shared_ptr<Stage>& StagePtr,
        const Vec3& Scale,
        const Vec3& Rotation,
        const Vec3& Position
    );
    virtual ~MoveBox();
    //初期化
    virtual void OnCreate() override;
    //操作
    virtual void OnUpdate() override;
};
 赤くなっている部分がコンピュートシェーダを実装するために最低限必要なオブジェクト(インターフェイス)です。
 それぞれコメントにありますが
    // エレメントデータの構造体
    struct Element
    {
        XMFLOAT4 pos;
    };
 はシェーダに渡すデータの構造体です。メンバはposとなってますように、位置情報です。
    //エレメントバッファ
    ComPtr<ID3D11Buffer> m_Buffer;
 は構造体のデータそのものです。ID3D11Bufferタイプですが、ここにElementと同じサイズのバッファを構築します。
    //アクセスビュー
    ComPtr < ID3D11UnorderedAccessView>  m_UAV;
 はm_Bufferを読み書きするビューです。Dx11はビューを通して読み書きします。
    //リードバックバッファ
    ComPtr<ID3D11Buffer> m_ReadBackBuffer;
 はアクセスビューから、計算結果を読みだすインターフェイスです。

ゲームオブジェクトの構築

 では、これらのオブジェクトの構築を見てみましょう。MoveBox::OnCreate()関数です。
void MoveBox::OnCreate() {

    //中略

    auto Dev = App::GetApp()->GetDeviceResources();
    auto pDx11Device = Dev->GetD3DDevice();
    auto pID3D11DeviceContext = Dev->GetD3DDeviceContext();
    //エレメントバッファ
    D3D11_BUFFER_DESC buffer_desc = {};
    buffer_desc.ByteWidth = sizeof(Element);
    buffer_desc.Usage = D3D11_USAGE_DEFAULT;
    buffer_desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;
    buffer_desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
    buffer_desc.StructureByteStride = sizeof(Element);
    ThrowIfFailed(
        pDx11Device->CreateBuffer(&buffer_desc, nullptr, &m_Buffer),
        L"エレメントバッファ作成に失敗しました",
        L"pDx11Device->CreateBuffer()",
        L"MoveBox::OnCreate()"
    );
    //アクセスビュー
    D3D11_UNORDERED_ACCESS_VIEW_DESC uavbuffer_desc = {};
    uavbuffer_desc.Format = DXGI_FORMAT_UNKNOWN;
    uavbuffer_desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
    uavbuffer_desc.Buffer.NumElements = 1;
    ThrowIfFailed(
        pDx11Device->CreateUnorderedAccessView(m_Buffer.Get(), &uavbuffer_desc, &m_UAV),
        L"アクセスビュー作成に失敗しました",
        L"pDx11Device->CreateUnorderedAccessView()",
        L"MoveBox::OnCreate()"
    );
    //リードバックバッファ
    D3D11_BUFFER_DESC readback_buffer_desc = {};
    readback_buffer_desc.ByteWidth = sizeof(Element);
    readback_buffer_desc.Usage = D3D11_USAGE_STAGING;
    readback_buffer_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    readback_buffer_desc.StructureByteStride = sizeof(Element);
    ThrowIfFailed(
        pDx11Device->CreateBuffer(&readback_buffer_desc, nullptr, &m_ReadBackBuffer),
        L"リードバックバッファ作成に失敗しました",
        L"pDx11Device->CreateBuffer()",
        L"MoveBox::OnCreate()"
    );
}
 シェーダに関係ない部分は省略してあります。
 エレメントバッファ、アクセスビュー、リードバックバッファの3つのインターフェイスを構築します。
 Dx11では、インターフェイスはCOMの形で提供されています。
 手順的には
1、定義用の構造体(D3D11_BUFFER_DESCなど)を作成
2、構築用の関数を呼び出す。その際戻り値をチェックする
 となります。ThrowIfFailed()関数は追いかけていくとわかりますがBaseCross64のユーティリティ的な関数です。失敗した時にエラーメッセージを設定することができます。
 もっと複雑な計算や計算結果を別のシェーダ(例えば頂点シェーダ)に渡す場合は、このほかにシェーダーリソースビューも使います。
 今回のサンプルではシェーダーリソースビューは使っていません。

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

 更新処理はMoveBox::OnUpdate()関数です。以下に紹介します。
void MoveBox::OnUpdate() {
    float elapsedTime = App::GetApp()->GetElapsedTime();
    m_TotalTime += elapsedTime;
    if (m_TotalTime >= XM_2PI) {
        m_TotalTime = 0;
    }
    //デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    auto pID3D11DeviceContext = Dev->GetD3DDeviceContext();
    //コンスタントバッファの設定
    CB cb = {};
    cb.paramf[0] = m_TotalTime;
    ID3D11Buffer* pConstantBuffer = ConstantBufferCalcbody::GetPtr()->GetBuffer();
    pID3D11DeviceContext->UpdateSubresource(pConstantBuffer, 0, nullptr, &cb, 0, 0);
    pID3D11DeviceContext->CSSetConstantBuffers(0, 1, &pConstantBuffer);
    //現在の位置情報の取得
    auto ptrTransform = GetComponent<Transform>();
    //エレメントの入力
    Element elemData;
    Vec4 pos4(ptrTransform->GetPosition(), 0);
    elemData.pos = pos4;
    pID3D11DeviceContext->UpdateSubresource(m_Buffer.Get(), 0, nullptr, &elemData, 0, 0);
    //CSの設定
    pID3D11DeviceContext->CSSetShader(ComputeSaderCalcbody::GetPtr()->GetShader(), nullptr, 0);
    //アクセスビューの設定
    pID3D11DeviceContext->CSSetUnorderedAccessViews(0, 1, m_UAV.GetAddressOf(), nullptr);
    //CSの実行
    pID3D11DeviceContext->Dispatch(1, 1, 1);
    //結果の読み取り
    D3D11_MAPPED_SUBRESOURCE MappedResource = { 0 };
    pID3D11DeviceContext->CopyResource(m_ReadBackBuffer.Get(), m_Buffer.Get());
    if (SUCCEEDED(pID3D11DeviceContext->Map(m_ReadBackBuffer.Get(), 0, D3D11_MAP_READ, 0, &MappedResource)))
    {
        memcpy(&elemData, MappedResource.pData, sizeof(Element));
        pID3D11DeviceContext->Unmap(m_ReadBackBuffer.Get(), 0);
        Vec3 resuPos;
        resuPos.x = elemData.pos.x;
        resuPos.y = elemData.pos.y;
        resuPos.z = elemData.pos.z;
        ptrTransform->SetPosition(resuPos);
    }
}
 まず
    float elapsedTime = App::GetApp()->GetElapsedTime();
    m_TotalTime += elapsedTime;
    if (m_TotalTime >= XM_2PI) {
        m_TotalTime = 0;
    }
 の部分でelapsedTimeを取り出しm_TotalTimeに加算します。これがXM_2PIを超えればsinカーブは一周しますので0に初期化します。
 続くコードがシェーダとのやり取りです
    //デバイスの取得
    auto Dev = App::GetApp()->GetDeviceResources();
    auto pID3D11DeviceContext = Dev->GetD3DDeviceContext();
 でDx11デバイスを取得します。ここで利用するのはpID3D11DeviceContextです。
 続く
    //コンスタントバッファの設定
    CB cb = {};
    cb.paramf[0] = m_TotalTime;
    ID3D11Buffer* pConstantBuffer = ConstantBufferCalcbody::GetPtr()->GetBuffer();
    pID3D11DeviceContext->UpdateSubresource(pConstantBuffer, 0, nullptr, &cb, 0, 0);
    pID3D11DeviceContext->CSSetConstantBuffers(0, 1, &pConstantBuffer);
 でコンスタントバッファをシェーダに渡します
ConstantBufferCalcbody::GetPtr()->GetBuffer();
 でコンスタントバッファのインターフェイスのポインタを取得できます。
 BaseCross64ではコンスタントバッファはシングルトンとして作成します。(ほかのインターフェイルのように直接構築することも可能です)
 続く
    //現在の位置情報の取得
    auto ptrTransform = GetComponent<Transform>();
    //エレメントの入力
    Element elemData;
    Vec4 pos4(ptrTransform->GetPosition(), 0);
    elemData.pos = pos4;
    pID3D11DeviceContext->UpdateSubresource(m_Buffer.Get(), 0, nullptr, &elemData, 0, 0);
 で、位置情報をシェーダに渡します。
 続く
    //CSの設定
    pID3D11DeviceContext->CSSetShader(ComputeSaderCalcbody::GetPtr()->GetShader(), nullptr, 0);
 でシェーダーを設定します。BaseCross64でシェーダーはシングルトンとして作成します。
 ComputeSaderCalcbody::GetPtr()->GetShader()で、コンピュートシェーダのポインタを取得できます。
 続く
    //アクセスビューの設定
    pID3D11DeviceContext->CSSetUnorderedAccessViews(0, 1, m_UAV.GetAddressOf(), nullptr);
 で、入出力用のビューを渡します。 m_UAVインターフェイスは構築時にm_Buffer用のビューとして作成しますので、m_Bufferそのものはシェーダに渡す必要はありません(渡すことはできない)。
 続く
    //CSの実行
    pID3D11DeviceContext->Dispatch(1, 1, 1);
 とコンピュートシェーダを実行します。今回は計算する個数は、1個です。パラメータの1, 1, 1はそういう意味です。
 一度に複数の計算(複数のオブジェクトに対する同じ計算)を行う場合は、このパラパラメータを10,1,1のようにします。
 パラメータの詳しい意味は次項以降で説明します。
 計算しましたので、結果を読み出します。
    //結果の読み取り
    D3D11_MAPPED_SUBRESOURCE MappedResource = { 0 };
    pID3D11DeviceContext->CopyResource(m_ReadBackBuffer.Get(), m_Buffer.Get());
    if (SUCCEEDED(pID3D11DeviceContext->Map(m_ReadBackBuffer.Get(), 0, D3D11_MAP_READ, 0, &MappedResource)))
    {
        memcpy(&elemData, MappedResource.pData, sizeof(Element));
        pID3D11DeviceContext->Unmap(m_ReadBackBuffer.Get(), 0);
        Vec3 resuPos;
        resuPos.x = elemData.pos.x;
        resuPos.y = elemData.pos.y;
        resuPos.z = elemData.pos.z;
        ptrTransform->SetPosition(resuPos);
    }
 です。結果はm_Bufferに入っているのですが、直接は読めません。
1、読み出し用のインターフェイスにm_Bufferをコピーする
2、読み出し用のインターフェイスをmapする
3、マップされたリソースから、CPU側のデータにコピーする
4、マップを開放する
 という手順を踏みます。
 読み出し用のインターフェイスは構築時に作成したm_ReadBackBufferです。
 ここに
    pID3D11DeviceContext->CopyResource(m_ReadBackBuffer.Get(), m_Buffer.Get());
 という形でコピーします。コピーしたら
    if (SUCCEEDED(pID3D11DeviceContext->Map(m_ReadBackBuffer.Get(), 0, D3D11_MAP_READ, 0, &MappedResource)))
 とマップします。マップが成功したら
        memcpy(&elemData, MappedResource.pData, sizeof(Element));
 とCPU側変数elemDataにコピーします。そして
        pID3D11DeviceContext->Unmap(m_ReadBackBuffer.Get(), 0);
 とアンマップします。そのあとはelemDataを操作できますので、
        Vec3 resuPos;
        resuPos.x = elemData.pos.x;
        resuPos.y = elemData.pos.y;
        resuPos.z = elemData.pos.z;
        ptrTransform->SetPosition(resuPos);
 と位置情報を書き換えます。

まとめ

 この項ではコンピュートシェーダーの一番簡単な使い方を説明するために、通常はやらないであろうような簡単な計算を実装してみました。
 決して簡単に記述できるとは言いませんが、構築と更新を丁寧に読んでもらえると、決して難解な処理をしてるのではないと思います。準備すべきインターフェイスやオブジェクトが複数必要なので、そのために若干複雑になります。
 次項ではもう少し複雑な(コンピュートシェーダーらしい)計算を実装します。