コポうぇぶろぐ

コポコポによる備忘録&Tipsブログ

【UE4】USF(Unreal Shader File) へパラメータを渡す

工事中の記事です。

はじめに

USF(グローバルシェーダ)へのパラメータの渡し方をまとめてみました(試したものだけ)。USF でどんなことが出来るのかを検証してみた備忘録のような記事となっております。この記事は UE 4.22.3 で進められています。

  • Texture
  • ConstantBuffer ( UniformBuffer )
  • StructuredBuffer

プロジェクト環境

プロジェクトの環境は別記事の状態を前提としています。
Sketch という新規プラグインの中に USF や関連する C++ ソースがある状態です。
f:id:coposuke:20191209234652j:plain

USF と C++ の準備

こちらも別記事の状態を前提としています。

  • /Project/Plugins/Sketch/Shaders フォルダ以下に SketchShader.usf" を作成
  • SketchShader.usf は UV を RG値に入れた状態のシェーダ
  • SketchShader 用の VertexShader と PixelShader の C++クラスを実装(FSketchShaderVS / PSクラス)
  • RHICommand で RenderTargetTexture に書き込む C++クラスを実装(SketchComponentクラス)

USF 即席環境がありますので、確認しつつ読み進めたい方はこちらです。
github.com

ほぼほぼの完成品が欲しい方はこちらです。
github.com

Texture を渡す

PixelShader に対して Texture を渡していきます。早速ソースをご覧ください。

// SketchShader.h
...

class FSketchShaderPS : public FGlobalShader
{
    ...
public:
    void SetTexture(FRHICommandList& commandList, FTextureRHIParamRef& texture);
private:
    FShaderResourceParameter srpTexture;
}
...
// SketchShader.cpp
...
FSketchShaderPS::FSketchShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
    : FGlobalShader(Initializer)
{
    srpTexture.Bind(Initializer.ParameterMap, TEXT("mainTexture"));
}

...

void FSketchShaderPS::SetTexture(FRHICommandList& commandList, FTextureRHIParamRef& texture)
{
    SetTextureParameter(commandList, GetPixelShader(), srpTexture, texture);
}
...

コンストラクタで FShaderResourceParameter srpTexture と USF内の "mainTexture" との紐づけ(Bind)することで USF との情報のやり取りをします。

後は使う側を実装するだけです。

// SketchComponent.h
#include "SketchShader.h"

...

class SKETCH_API USketchComponent : public UActorComponent
{
    ...

public:
    UPROPERTY(EditAnywhere, AdvancedDisplay, Category=Sketch)
    class UTexture2D* texture2D;
}
// SketchComponent.cpp
void USketchComponent::ExecuteInRenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutputRenderTargetResource)
{
    check(IsInRenderingThread());

    ...

    // Shader setup
    auto shaderMap = GetGlobalShaderMap(ERHIFeatureLevel::SM5);
    TShaderMapRef<FSketchShaderVS> shaderVS(shaderMap);
    TShaderMapRef<FSketchShaderPS> shaderPS(shaderMap);
    
    FTextureRHIParamRef texture = this->texture2D->TextureReference.TextureReferenceRHI;
    shaderPS->SetTexture(RHICmdList, texture);
    
    ...
}

これでテクスチャを渡す実装が完了しました。
実際に texture2D をエディタで設定して確認してみます。

// SketchShader.usf
...

sampler2D mainTexture;

...

void MainPS(
    in float2 UV : TEXCOORD0,
    out float4 OutColor : SV_Target0)
{
    float2 uv = (UV * 2.0 - 1.0) * float2(1.0, 0.56); // aspect
    float4 color = tex2D(mainTexture, frac(uv - 0.5));
    OutColor = color * color.a;
}

これで下の絵のようになります。
f:id:coposuke:20191210213041j:plain

ConstantBuffer (UniformBuffer) を渡す

UE4 の中では UniformBuffer という名前で定義されています(OpenGLなまり)。
まずは ConstantBuffer の構造体を定義します。

// SketchShader.h
...

BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FConstantParameters, )
 SHADER_PARAMETER(int, actorsNum)
 SHADER_PARAMETER(FVector4, resolution)
 SHADER_PARAMETER(FVector4, viewOrigin)
 SHADER_PARAMETER(FMatrix, viewMatrix)
 SHADER_PARAMETER(FMatrix, projMatrix)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FVariableParameters, )
 SHADER_PARAMETER(float, time)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

typedef TUniformBufferRef<FConstantParameters> FConstantParametersRef;
typedef TUniformBufferRef<FVariableParameters> FVariableParametersRef;

...
// SketchShader.cpp
...

IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FConstantParameters, "constants");
IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FVariableParameters, "variables");

...

今回は分かりやすく2つの ConstantBuffer を定義してみました。
頂点シェーダ/ピクセルシェーダに対して、この構造体のバッファを渡します。

// SketchShader.h
...

class FSketchShaderVS : public FGlobalShader
{
    ...
public:
    void SetUniformBuffers(FRHICommandList& commandList, FConstantParameters& constants, FVariableParameters& variables);
}

class FSketchShaderPS : public FGlobalShader
{
    ...
public:
    void SetUniformBuffers(FRHICommandList& commandList, FConstantParameters& constants, FVariableParameters& variables);
}
...
// SketchShader.cpp
...

void FSketchShaderVS::SetUniformBuffers(FRHICommandList& commandList, FConstantParameters& constants, FVariableParameters& variables)
{
    SetUniformBufferParameter(commandList, GetVertexShader(), GetUniformBufferParameter<FConstantParameters>(),
        FConstantParametersRef::CreateUniformBufferImmediate(constants, UniformBuffer_SingleDraw));
    SetUniformBufferParameter(commandList, GetVertexShader(), GetUniformBufferParameter<FVariableParameters>(),
        FVariableParametersRef::CreateUniformBufferImmediate(variables, UniformBuffer_SingleDraw));
}

...

void FSketchShaderPS::SetUniformBuffers(FRHICommandList& commandList, FConstantParameters& constants, FVariableParameters& variables)
{
    SetUniformBufferParameter(commandList, GetPixelShader(), GetUniformBufferParameter<FConstantParameters>(),
        FConstantParametersRef::CreateUniformBufferImmediate(constants, UniformBuffer_SingleDraw));
    SetUniformBufferParameter(commandList, GetPixelShader(), GetUniformBufferParameter<FVariableParameters>(),
        FVariableParametersRef::CreateUniformBufferImmediate(variables, UniformBuffer_SingleDraw));
}
...

引数で渡ってきた ConstantBuffer の構造体を CreateUniformBufferImmediate でバッファを作り、RHICommandList を通して 各シェーダに渡していきます。ここでよく間違えやすいのは、SetUniformBufferParameter()の第二引数のGetVertexShader()とGetPixelShader()をコピペしてそのままのケースになってしまいがちです。十分ご留意ください。

後は使う側を実装するだけです。

// SketchComponent.h
#include "SketchShader.h"

...

class SKETCH_API USketchComponent : public UActorComponent
{
    ...

private:
    FConstantParameters constantParameters;
    FVariableParameters variableParameters;
}
// SketchComponent.cpp
void USketchComponent::ExecuteInRenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutputRenderTargetResource)
{
    check(IsInRenderingThread());

    ...

    // Shader setup
    auto shaderMap = GetGlobalShaderMap(ERHIFeatureLevel::SM5);
    TShaderMapRef<FSketchShaderVS> shaderVS(shaderMap);
    TShaderMapRef<FSketchShaderPS> shaderPS(shaderMap);
    
    shaderVS->SetUniformBuffers(RHICmdList, constantParameters, variableParameters); // ←追記
    shaderPS->SetUniformBuffers(RHICmdList, constantParameters, variableParameters); // ←追記
    
    ...
}

これでパラメータを渡す実装が完了しました。
実際に constantParameters や variableParameters の変数を変更して確認してみます。

// SketchComponent.cpp
...

void USketchComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    this->constantParameters.actorsNum++;
    this->constantParameters.resolution.X = GSystemResolution.ResX;
    this->constantParameters.resolution.Y = GSystemResolution.ResY;
    this->variableParameters.time += DeltaTime;
    
    ...
}
...
// SketchShader.usf
...

void MainPS(
    in float2 UV : TEXCOORD0,
    out float4 OutColor : SV_Target0)
{
    float2 uv = (UV * 2.0 - 1.0) * constants.resolution.xy / min(constants.resolution.x, constants.resolution.y);
    float4 color = float4(0, 0, 0, 1);
    color.rgb = step(distance(uv, float2(0,0)), sin(variables.time * 10.0) * 0.1 + 0.5);
    OutColor = color;
}

これで下の絵のように動くようになります。
f:id:coposuke:20191210030212g:plain
(resolution に代入している値に問題がありますが…正しくはFamilyViewのRectが良き)

StructuredBuffer を渡す

Texture と同じように FShaderResourceParameter で紐づけしていきますが、使う側( Component側 )で専用のバッファ ShaderResourceView(RHICreateShaderResourceView)を用いて作る必要があります。まずはシェーダ側の編集です。

// SketchShader.h
...

class FSketchShaderVS : public FGlobalShader
{
    ...
public:
    void SetStructuredBuffers(FRHICommandList& commandList, FShaderResourceViewRHIRef& structuredBuffer);

private:
    FShaderResourceParameter srpBuffer;
}

class FSketchShaderPS : public FGlobalShader
{
    ...
public:
    void SetStructuredBuffers(FRHICommandList& commandList, FShaderResourceViewRHIRef& structuredBuffer);

private:
    FShaderResourceParameter srpBuffer;
}
...
// SketchShader.cpp
...

FSketchShaderVS::FSketchShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
    : FGlobalShader(Initializer)
{
    srpBuffer.Bind(Initializer.ParameterMap, TEXT("sketchData"));
}

void FSketchShaderVS::SetStructuredBuffers(FRHICommandList& commandList, FShaderResourceViewRHIRef& structuredBuffer)
{
    if (srpBuffer.IsBound())
        commandList.SetShaderResourceViewParameter(GetVertexShader(), srpBuffer.GetBaseIndex(), structuredBuffer);
}

FSketchShaderPS::FSketchShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
    : FGlobalShader(Initializer)
{
    srpBuffer.Bind(Initializer.ParameterMap, TEXT("sketchData"));
}

void FSketchShaderPS::SetStructuredBuffers(FRHICommandList& commandList, FShaderResourceViewRHIRef& structuredBuffer)
{
    if (srpBuffer.IsBound())
        commandList.SetShaderResourceViewParameter(GetPixelShader(), srpBuffer.GetBaseIndex(), structuredBuffer);
}

...

コンストラクタで FShaderResourceParameter srpBuffer と USF内の "sketchData" との紐づけ(Bind)することで USF との情報のやり取りをします。アクセスするのは PixelShader のみですが、USF に VertexShader にまとめて定義されているので、こちらにも必要になります。

続きまして使う側の実装です。

// SketchComponent.h
#include "SketchShader.h"

...

class SKETCH_API USketchComponent : public UActorComponent
{
    ...

protected:
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; // ←追記

private:
    struct SketchData
    {
        FVector2D position;
        FVector2D acceleration;
    };

private:
    TResourceArray<SketchData> structuredBufferResourceArray;  // ←追記
    FStructuredBufferRHIRef structuredBuffer;                  // ←追記
    FShaderResourceViewRHIRef structuredBufferSRV;             // ←追記
}
// SketchComponent.cpp

void USketchComponent::BeginPlay()
{
    Super::BeginPlay();
	
    for(int i=0 ; i<10 ; ++i)
        this->structuredBufferResourceArray.Add(SketchData()); // dummydata

    FRHIResourceCreateInfo info(&this->structuredBufferResourceArray);
    this->structuredBuffer = RHICreateStructuredBuffer(sizeof(SketchData), sizeof(SketchData) * 10, BUF_ShaderResource | BUF_Static, info);
    this->structuredBufferSRV = RHICreateShaderResourceView(this->structuredBuffer);
}

void USketchComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	this->structuredBufferSRV.SafeRelease();
	this->structuredBuffer.SafeRelease();

	Super::EndPlay(EndPlayReason);
}

void USketchComponent::ExecuteInRenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutputRenderTargetResource)
{
    check(IsInRenderingThread());

    ...

    // Shader setup
    auto shaderMap = GetGlobalShaderMap(ERHIFeatureLevel::SM5);
    TShaderMapRef<FSketchShaderVS> shaderVS(shaderMap);
    TShaderMapRef<FSketchShaderPS> shaderPS(shaderMap);
    
    // ちょっと無理しすぎな処理
    SketchData* ptr = (SketchData*)RHILockStructuredBuffer(this->structuredBuffer, 0, sizeof(SketchData), EResourceLockMode::RLM_WriteOnly);
    FMemory::Memcpy(ptr, this->structuredBufferResourceArray.GetData(), sizeof(SketchData) * 10);
    RHIUnlockStructuredBuffer(this->structuredBuffer.GetReference());

    shaderPS->SetStructuredBuffers(RHICmdList, this->structuredBufferSRV);  // ←追記
    
    ...
}

これでパラメータを渡す実装が完了しました。
ExecuteInRenderThread にて Buffer を Lock して内容を書き込む処理では、ちょっと急ぎ足で実装している為、ResourceArray の連続性等を確認しておりません(怖)。ただ、StructuredBuffer を書き換えるだけで USF 側に伝わるというところがポイントになっています。
では、実際に structuredBufferResourceArray 及び structuredBuffer を書き換えて確認してみます。

// SketchComponent.cpp
...

void USketchComponent::BeginPlay()
{
    ...

    // remove.
    // for(int i=0 ; i<10 ; ++i)
    //     this->structuredBufferResourceArray.Add(SketchData()); // dummydata

    for(int i=0 ; i<10 ; ++i){
        auto data = SketchData();
        data.position.X = FMath::RandRange(-0.0f, 0.0f);
        data.position.Y = FMath::RandRange(-0.0f, 0.0f);
        data.acceleration.X = FMath::RandRange(-1.0f, 1.0f);
        data.acceleration.Y = FMath::RandRange(-1.0f, 1.0f);
        data.acceleration.Normalize();
        this->structuredBufferResourceArray.Add(data);
    }

    ...
}

void USketchComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    for(int i=0 ; i<10 ; ++i)
    {
        SketchData data = this->structuredBufferResourceArray[i];
        data.position += data.acceleration * DeltaTime * 0.5f;
        if(1.0f <= FMath::Abs(data.position.X))
            data.acceleration.X *= -1.0f;
        if(1.0f <= FMath::Abs(data.position.Y))
            data.acceleration.Y *= -1.0f;
        this->structuredBufferResourceArray[i] = data;
    }

    ...
}
...
// SketchShader.usf
...

struct SketchData
{
    float2 position;
    float2 acceleration;
};

StructuredBuffer<SketchData> sketchData;

...

void MainPS(
    in float2 UV : TEXCOORD0,
    out float4 OutColor : SV_Target0)
{
    float2 uv = (UV * 2.0 - 1.0);
    float3 result = float3(1e+4, 0, 0); // dist, r, g;

    for(int i=0 ; i<10 ; ++i)
    {
        SketchData data = sketchData[i];
        float dist = distance(data.position, uv);
        result = (result.x < dist) ? result : float3(dist, data.acceleration);
    }

    float4 color = float4(1,1,1,1);
    color.rgb = step(result.x, 0.05);
    color.rg *= result.yz;
    OutColor = color;
}

これで下の絵のように動くようになります。
これを使えば特定の Actor の姿勢を渡すことが出来ますし、UniformBuffer で要素数を渡せば、増減を決めることも可能です。
f:id:coposuke:20191210234312g:plain

StructuredBuffer に書き込む

ComputeShader でバッファに書き込む方法です。
書き込みはUnorderedAccessView(CreateUnorderedAccessView)を使います。
工事中

まとめ

昨今はこの辺りのエンジンソースが変わっているようで、ころころインターフェースやマクロ名などが変わっているようです。時間を見つけ次第追記いたします。