概要
Unreal EngineのHLSLで、C++からHLSLに構造体の配列を渡したい場合、StructuredBufferが使えます。Unreal Engineの概念であるUniform BufferやShader ParameterとはStructuredBufferは準備するリソースや宣言の方法がまた異なっており、それを記録すべく記事にまとめました。
StructuredBufferで頂点カラーを渡して三角形を描画してみました。
Uniform BufferやShader Parameter等、usfの扱い方に関しては別途記事を書いてます。
やりかた
同一の構造体をHLSLとC++に定義します。HLSLでStructuredBufferは囲った構造体の配列のように振る舞います。
struct FTestBuffer
{
float3 Color;
float Intensity;
};
StructuredBuffer<FTestBuffer> TestBuffer;
struct FTestBuffer
{
FVector3f Color;
float Intensity;
};
C++からShader parameterとしてStructedBufferを定義します。SHADER_PARAMETER_SRVの行がそれです。
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
SHADER_PARAMETER_SRV(StructuredBuffer<FTestBuffer>, TestBuffer)
END_SHADER_PARAMETER_STRUCT()
クラスのメンバ変数として、それぞれの実体を定義します。FBufferRHIRefがバッファの実体、FShaderResourceViewRHIRefがshader parameterにバインドするSRV、TResourceArrayがGPUにアップロードするCPU側のバッファを定義する配列です。
FBufferRHIRef TestBuffer;
FShaderResourceViewRHIRef TestBufferSRV;
TResourceArray<FTestBuffer> TestBufferArray;
アップロード処理は例えば以下のように書けます。以下処理は必ずレンダースレッドから実行します。(ゲームスレッドから実行するとエラーになる)バッファの内容が変わらない場合は作りっぱなしでOKです。
FTestBuffer Temp;
Temp.Color = FVector3f(0.0f, 0.0f, 1.0f);
Temp.Intensity = 0.f;
TestBufferArray.Add(Temp);
Temp.Color = FVector3f(0.0f, 1.0f, 0.0f);
Temp.Intensity = 0.f;
TestBufferArray.Add(Temp);
Temp.Color = FVector3f(1.0f, 0.0f, 0.0f);
Temp.Intensity = 1.f;
TestBufferArray.Add(Temp);
FRHICommandListBase& RHICmdList = FRHICommandListImmediate::Get();
FRHIResourceCreateInfo CreateInfo(TEXT("MyTestBuffer"));
CreateInfo.GPUMask = FRHIGPUMask::FromIndex(0);
CreateInfo.ResourceArray = &TestBufferArray;
TestBuffer = RHICmdList.CreateStructuredBuffer(sizeof(FTestBuffer), TestBufferArray.GetResourceDataSize(), BUF_Static | BUF_ShaderResource, CreateInfo);
TestBufferSRV = RHICmdList.CreateShaderResourceView(TestBuffer);
TResourceArrayはTArrayを継承しており、上のように要素追加の際の書き方はTArrayそのものです。
shader parameterへのバインドは、SRV(FShaderResourceViewRHIRef)を代入することでできます。
// InViewは「FSceneView& InView」
check(InView.bIsViewInfo);
const FViewInfo& ViewInfo = static_cast<const FViewInfo&>(InView);
FDrawInWorldVS::FParameters* VSParameters = GraphBuilder.AllocParameters<FDrawInWorldVS::FParameters>();
VSParameters->View = ViewInfo.ViewUniformBuffer;
VSParameters->TestBuffer = TestBufferSRV; // SRVをバインド
HLSLから使用する場合、以下のようにインデックスで構造体の配列要素にアクセスすることができます。例として各頂点ごとに一つの構造体を割り当てており、VertexIDをインデックスとしています。
void DrawInWorldVS(
uint VertexID : SV_VertexID, ...)
{
ResolvedView = ResolveView();
FTestBuffer VertexSpecific = TestBuffer[VertexID]; // 配列要素にアクセス
float3 Color = VertexSpecific.Color;
float Intensity = VertexSpecific.Intensity;
...
アラインメント等
StructuredBufferはConstant Bufferとは異なり、Constant Bufferにあるようなアラインメントルールの制約を受けず、比較的自由に構造体を定義できます。例えば以下のような16バイト境界を無視した構造体定義は許容されます。
struct FTestBuffer
{
float3 Color;
// float Padding; // 不要
float2 Test; // 16バイト境界から始まっていないがOK
float Intensity;
};
StructuredBuffer<FTestBuffer> TestBuffer;
struct FTestBuffer
{
FVector3f Color;
// float Padding; // 不要
FVector2f Test; // 16バイト境界から始まっていないがOK
float Intensity;
};
...
TResourceArray<FTestBuffer> TestBufferArray; // 1要素が24バイトだがOK
アラインメントから自由であれば、構造体の入れ子や配列などを比較的自由に定義できることを意味します。例えば以下のような宣言も可能です。
struct FInnerStructure
{
float3 InnnerTest[3];
};
struct FTestBuffer
{
float3 Color;
FInnerStructure Test[2];
float Intensity;
};
struct FInnerStructure
{
FVector3f InnnerTest[3];
};
struct FTestBuffer
{
FVector3f Color;
FInnerStructure Test[2];
float Intensity;
};
但し、パフォーマンスのためには構造体単位で16バイト境界を意識したほうがよいかもしれません。古い記事ですが、NVIDIAのサイトで言及されています。
Unreal EngineのUniform BufferやShader Parameterの変数はConstant Bufferのアラインメントルールの制約下にありますが、エンジン内部に自動で並べ替える機構が備わっており、使う際はそのルールを意識する必要がありません。https://epicgames.ent.box.com/s/ul1h44ozs0t2850ug0hrohlzm53kxwrz が参考になります。
Uniform Bufferにはバインドできない
試した限り、SHADER_PARAMETER_SRV は BEGIN_SHADER_PARAMETER_STRUCT の中に書く必要があります。BEGIN_UNIFORM_BUFFER_STRUCTの中に書いても使えませんでした。つまり、HLSL内ではStructuredBufferとStructuredBufferで使用する構造体の宣言は必ず自分で書く必要があります。
もしBEGIN_UNIFORM_BUFFER_STRUCTの中にSHADER_PARAMETER_SRVを書けると仮定すると StructuredBufferを宣言するHLSLが自動生成されるはずであり、自動定義されるStructuredBufferが使用する構造体を何らかの形で先行して定義する必要がありますが、それが出来るようには見えませんでした。