コポうぇぶろぐ

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

【UE4】USF(Unreal Shader File) をすぐに始める環境設定 Project編

はじめに

USF(グローバルシェーダ)をこれさえやっておけばすぐに使えるという手順をまとめてみました。プラグイン化を前提とした構成の記事もございますが、今回はモジュールとして構成した環境構築の紹介です。この記事は自分がまたUSFを使う時が来たら見るための備忘録記事となります。

完成品(といいますかテンプレートプロジェクト)を置きました。
環境:UE4 4.22.3
github.com

プロジェクト作成

ではまず、普段通りプロジェクトを作成します。
今回は C++ ソースを多用するので、C++プロジェクトで作成します。
f:id:coposuke:20191208231931j:plain

モジュール作成

モジュールを作成していきます。今回は ShaderModule という名前のモジュールを作成していきます。
モジュールは最低でも 〇〇.Build.cs、〇〇.cpp、〇〇.h の3つのファイルが必要になります。まずはこちらを用意していきましょう。

Source フォルダ以下に ShaderModuleフォルダを作成し、プロジェクトに既にある UsfProject.Build.cs 等をコピー&リネームしていきます。

UsfProject.Build.cs ShaderModule.Build.cs
UsfProject.cpp ShaderModule.cpp
UsfProject.h ShaderModule.h

f:id:coposuke:20200609010443p:plain

Shader環境構築

フォルダ&ファイル作成

Content フォルダと同階層に Shader フォルダを作成し、更に Private と Public フォルダを作成します。そして Private フォルダ以下にテキストを作成し、"SketchShader.usf"とリネームします。

モジュールをプロジェクトに加える

作成したプロジェクト UsfProject.uproject をテキストで開いて編集します。
Modules の配列に ShaderModule を追加し、LoadingPhase を PostConfigInit に変更します。

{
    "FileVersion": 3,
    "EngineAssociation": "4.22",
    "Category": "",
    "Description": "",
    "Modules": [
        {
            "Name": "UsfProject",
            "Type": "Runtime",
            "LoadingPhase": "Default"
        },
        {
            "Name": "ShaderModule",
            "Type": "Runtime",
            "LoadingPhase": "PostConfigInit"
        }
    ]
}

モジュールからパスを通す

作成した Shader フォルダのパスを通します。最初に作成したモジュールの /Project/Source/ShaderModule にある3つのファイルを変更します。
f:id:coposuke:20200609013009p:plain

// ShaderModule.Build.cs
using UnrealBuildTool;

public class ShaderModule : ModuleRules // ←クラス名リネーム
{
    public ShaderModule(ReadOnlyTargetRules Target) : base(Target) // ←クラス名リネーム
    {
        ...
    
        PublicDependencyModuleNames.AddRange(new string[] {
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "RenderCore",  // ←追記
            "RHI",         // ←あとで必要になるので追記(パス追加に関係ない)
        });

        ...
    }
}
// UsfProject.h
#pragma once

#include "CoreMinimal.h"
#include "ModuleManager.h"

class FShaderModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
// UsfProject.cpp
#include "FShaderModule.h"
#include "Modules/ModuleManager.h"

void FShaderModule::StartupModule()
{
#if (ENGINE_MINOR_VERSION >= 21)
    FString ShaderDirectory = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shader"));
    if(!AllShaderSourceDirectoryMappings().Contains("/Project"))
        AddShaderSourceDirectoryMapping("/Project", ShaderDirectory);
#endif
}

void FShaderModule::ShutdownModule()
{
}

IMPLEMENT_GAME_MODULE( FShaderModule, ShaderModule,  );

記入したらコンパイルしてください。これでUSFファイルを見つけれもらえる状態になりました。
Shaderフォルダがまだ存在しない場合はハングしますのでご注意ください。

Engine の iniファイル編集

USFを作成&編集しやすいように ini ファイルを編集します。
これにより下記の機能が有効になり、作りやすくなります。

  • Ctrl+Sfhit+. でのリロード
  • エラー時のリロード
  • シェーダ関連のログ&警告の表示 ( Console )

この機能は ConsoleCommand の "r.ShaderDevelopmentMode 1" で有効になりますが、毎回やるのは面倒なので、Engine の ConsoleVariable.ini ファイルを編集し、起動時に必ず有効になるようにします。インストール先のエンジンは通常なら "C:\Program Files\Epic Games\UE_4.22\Engine\Config" といったパスにあります。

// ConsoleVariables.ini
...
; Uncomment to get detailed logs on shader compiles and the opportunity to retry on errors
r.ShaderDevelopmentMode=1
...

USF作成

先程Shader フォルダ以下に作成した SketchShader.usf" を編集します。
ひとまず分かりやすいように UV を RG値に入れたシェーダを書きます。

#include "/Engine/Private/Common.ush"

void MainVS(
    in float4 InPosition :ATTRIBUTE0, 
    in float2 InUV : ATTRIBUTE1,
    out float2 OutUV : TEXCOORD0,
    out float4 OutPosition :SV_POSITION)
{
    OutPosition = InPosition;
    OutUV = InUV;
}

void MainPS(
    in float2 UV : TEXCOORD0,
    out float4 OutColor : SV_Target0)
{
    OutColor = float4(UV, 1, 1);
}

USFを使用する(対応する)Cppクラス作成

SketchShader に対応するクラスを作成します。
メニューバーの File > New C++ Class をクリックし、継承なし ( None ) のクラスを作成します。クラス名は SketchShader とします。
作成したモジュールに含める為、ファイルは ShaderModule フォルダ以下に作成してください。

// SketchShader.h
#pragma once

#include "GlobalShader.h"
#include "UniformBuffer.h"

class FSketchShaderVS : public FGlobalShader
{
    DECLARE_SHADER_TYPE(FSketchShaderVS, Global);
    
public:
    FSketchShaderVS() {}
    explicit FSketchShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer);

    static bool ShouldCache(EShaderPlatform Platform) { return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::ES2); }
    static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& PermutationParams) { return true; }
    static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment);
    virtual bool Serialize(FArchive& Ar) override;
};

class FSketchShaderPS : public FGlobalShader
{
    DECLARE_SHADER_TYPE(FSketchShaderPS, Global);
    
public:
    FSketchShaderPS() {}
    explicit FSketchShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer);

    static bool ShouldCache(EShaderPlatform Platform) { return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::ES2); }
    static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& PermutationParams) { return true; }
    static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment);
    virtual bool Serialize(FArchive& Ar) override;
};

struct FSketchVertex
{
    FVector4    Position;
    FVector2D   UV;
};

class FSketchVertexDeclaration : public FRenderResource
{
public:
    FVertexDeclarationRHIRef VertexDeclarationRHI;

    virtual void InitRHI() override
    {
        FVertexDeclarationElementList Elements;
        uint32 Stride = sizeof(FSketchVertex);
        Elements.Add(FVertexElement(0, STRUCT_OFFSET(FSketchVertex, Position), VET_Float4, 0, Stride));
        Elements.Add(FVertexElement(0, STRUCT_OFFSET(FSketchVertex, UV), VET_Float2, 1, Stride));
        VertexDeclarationRHI = RHICreateVertexDeclaration(Elements);
    }
  
    virtual void ReleaseRHI() override
    {
        VertexDeclarationRHI->Release();
    }
};
// SketchShader.cpp
#include "SketchShader.h"

FSketchShaderVS::FSketchShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
    : FGlobalShader(Initializer)
{
}

void FSketchShaderVS::ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
    FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
    OutEnvironment.CompilerFlags.Add(CFLAG_StandardOptimization);
}

bool FSketchShaderVS::Serialize(FArchive& Ar)
{
    return FGlobalShader::Serialize(Ar);
}

FSketchShaderPS::FSketchShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
    : FGlobalShader(Initializer)
{
}

void FSketchShaderPS::ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
    FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
    OutEnvironment.CompilerFlags.Add(CFLAG_StandardOptimization);
}

bool FSketchShaderPS::Serialize(FArchive& Ar)
{
    return FGlobalShader::Serialize(Ar);
}

IMPLEMENT_SHADER_TYPE(, FSketchShaderVS, TEXT("/Project/SketchShader.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(, FSketchShaderPS, TEXT("/Project/SketchShader.usf"), TEXT("MainPS"), SF_Pixel);

ちょっと見辛いですが、VertexShader と PixelShader の2つのクラスを定義しています。これが一番シンプルな状態です。 FSketchVertex は描画する際の頂点情報です。FSketchVertexDeclaration によって頂点フォーマットを調整しています。ここでは UV 情報を頂点情報に加えています。

USFを使って描画

RHIコマンドを使ってレンダーテクスチャに描画していきます。
メニューバーの File > New C++ Class をクリックし、UActorComponentを継承したクラスを作成します。クラス名は USketchComponent とします。
作成したモジュールに含める為、ファイルは ShaderModule フォルダ以下に作成してください。

// SketchComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SketchComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SHADERMODULE_API USketchComponent : public UActorComponent
{
    GENERATED_BODY()

public:    
    USketchComponent();

    UPROPERTY(EditAnywhere, AdvancedDisplay, Category=Sketch)
    class UTextureRenderTarget2D* renderTexture;

protected:
    virtual void BeginPlay() override;

public:    
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

private:
    void ExecuteInRenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutputRenderTargetResource);
    void DrawIndexedPrimitiveUP(
        FRHICommandList& RHICmdList,
        uint32 PrimitiveType,
        uint32 MinVertexIndex,
        uint32 NumVertices,
        uint32 NumPrimitives,
        const void* IndexData,
        uint32 IndexDataStride,
        const void* VertexData,
        uint32 VertexDataStride );
};
#include "SketchComponent.h"
#include "SketchShader.h"
#include "Classes/Engine/TextureRenderTarget2D.h"  
//#include "RHICommandList.h" // RHI module
#include "RHIResources.h"

USketchComponent::USketchComponent()
{
    PrimaryComponentTick.bCanEverTick = true;
}

void USketchComponent::BeginPlay()
{
    Super::BeginPlay();
}

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

    auto This = this;
    auto RenderTargetResource = this->renderTexture->GameThread_GetRenderTargetResource();
    ENQUEUE_RENDER_COMMAND(FRaymarchingPostprocess)(
        [This, RenderTargetResource](FRHICommandListImmediate& RHICmdList)
        {
            This->ExecuteInRenderThread(RHICmdList, RenderTargetResource);
        }
    );
}

void USketchComponent::ExecuteInRenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutputRenderTargetResource)
{
    check(IsInRenderingThread());
    
#if WANTS_DRAW_MESH_EVENTS
    FString EventName;
    this->renderTexture->GetFName().ToString(EventName);
    SCOPED_DRAW_EVENTF(RHICmdList, SceneCapture, TEXT("SketchShader %s"), *EventName);
#else
    SCOPED_DRAW_EVENT(RHICmdList, DrawUVDisplacementToRenderTarget_RenderThread);
#endif

    // SetRenderTarget
    FRHIRenderPassInfo rpInfo(OutputRenderTargetResource->GetRenderTargetTexture(), ERenderTargetActions::DontLoad_DontStore);
    RHICmdList.BeginRenderPass(rpInfo, TEXT("Sketch"));

    // Shader setup
    auto shaderMap = GetGlobalShaderMap(ERHIFeatureLevel::SM5);
    TShaderMapRef<FSketchShaderVS> shaderVS(shaderMap);
    TShaderMapRef<FSketchShaderPS> shaderPS(shaderMap);
    
    FSketchVertexDeclaration VertexDec;
    VertexDec.InitRHI();

    //Declare a pipeline state object that holds all the rendering state
    FGraphicsPipelineStateInitializer PSOInitializer;
    RHICmdList.ApplyCachedRenderTargets(PSOInitializer);
    PSOInitializer.PrimitiveType = PT_TriangleList;
    PSOInitializer.BoundShaderState.VertexDeclarationRHI = VertexDec.VertexDeclarationRHI;
    PSOInitializer.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*shaderVS);
    PSOInitializer.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*shaderPS);
    PSOInitializer.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
    PSOInitializer.BlendState = TStaticBlendState<>::GetRHI();
    PSOInitializer.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
    SetGraphicsPipelineState(RHICmdList, PSOInitializer);

    static const FSketchVertex Vertices[4] = {
        { FVector4(-1.0f,  1.0f, 0.0f, 1.0f), FVector2D(0.0f, 0.0f)},
        { FVector4( 1.0f,  1.0f, 0.0f, 1.0f), FVector2D(1.0f, 0.0f)},
        { FVector4(-1.0f, -1.0f, 0.0f, 1.0f), FVector2D(0.0f, 1.0f)},
        { FVector4( 1.0f, -1.0f, 0.0f, 1.0f), FVector2D(1.0f, 1.0f)},
    };
    
    static const uint16 Indices[6] =
    {
        0, 1, 2,
        2, 1, 3
    };

    DrawIndexedPrimitiveUP(RHICmdList, PT_TriangleList, 0, ARRAY_COUNT(Vertices), 2, Indices, sizeof(Indices[0]), Vertices, sizeof(Vertices[0]));

    // Resolve render target.  
    RHICmdList.CopyToResolveTarget(  
        OutputRenderTargetResource->GetRenderTargetTexture(),  
        OutputRenderTargetResource->TextureRHI,
        FResolveParams());

    RHICmdList.EndRenderPass();
}

void USketchComponent::DrawIndexedPrimitiveUP(
    FRHICommandList& RHICmdList,
    uint32 PrimitiveType,
    uint32 MinVertexIndex,
    uint32 NumVertices,
    uint32 NumPrimitives,
    const void* IndexData,
    uint32 IndexDataStride,
    const void* VertexData,
    uint32 VertexDataStride )
{
    const uint32 NumIndices = GetVertexCountForPrimitiveCount( NumPrimitives, PrimitiveType );

    FRHIResourceCreateInfo CreateInfo;
    FVertexBufferRHIRef VertexBufferRHI = RHICreateVertexBuffer(VertexDataStride * NumVertices, BUF_Volatile, CreateInfo);
    void* VoidPtr = RHILockVertexBuffer(VertexBufferRHI, 0, VertexDataStride * NumVertices, RLM_WriteOnly);
    FPlatformMemory::Memcpy(VoidPtr, VertexData, VertexDataStride * NumVertices);
    RHIUnlockVertexBuffer(VertexBufferRHI);

    FIndexBufferRHIRef IndexBufferRHI = RHICreateIndexBuffer(IndexDataStride, IndexDataStride * NumIndices, BUF_Volatile, CreateInfo);
    void* VoidPtr2 = RHILockIndexBuffer(IndexBufferRHI, 0, IndexDataStride * NumIndices, RLM_WriteOnly);
    FPlatformMemory::Memcpy(VoidPtr2, IndexData, IndexDataStride * NumIndices);
    RHIUnlockIndexBuffer(IndexBufferRHI);

    RHICmdList.SetStreamSource(0, VertexBufferRHI, 0);
    RHICmdList.DrawIndexedPrimitive(IndexBufferRHI, MinVertexIndex, 0, NumVertices, 0, NumPrimitives, 1);

    IndexBufferRHI.SafeRelease();
    VertexBufferRHI.SafeRelease();
}

これでプログラム側の準備が整いました。最後に Editor 側でセッティングします。

UE4 Editor にて RenderTargetTexture を作成し、空のアクターに SketchComponent をアタッチし、RenderTargetTexture を設定します。
f:id:coposuke:20191209222222j:plain
f:id:coposuke:20191209222254j:plain

実行するとこのように描画されます。
f:id:coposuke:20191209222456j:plain
書き込まれた RenderTargetTexture を PostprocessMaterial に渡して全画面に描画してスケッチしたり、足跡なんかを書き込んで Displacement で凹ませるとかもいいかもしれないですね。

USFにパラメータを渡す

別記事にてまとめました。
coposuke.hateblo.jp

おまけ(タイミングについて)

Ctrl+Shift+, で GPUVisualizer を開くと、WorldTickのタイミングで処理されていることが分かります。
f:id:coposuke:20191209222723j:plain

まとめ

Project編ということで、あんまり他所で解説が少ない Module でのアプローチを試してみました。Usf はロードやコンパイル順の都合上、Plugin および Module の中で Usf 構築を進めていくことになります。個人的には 外部共有を前提としていないのであれば モジュール化したほうがスッキリしてて好きかも。