コポうぇぶろぐ

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

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

はじめに

USF(グローバルシェーダ)をこれさえやっておけばすぐに使えるという手順をまとめてみました。モジュール化を前提とした構成の記事もございますが、プラグインとして構成した環境構築の紹介です。この記事は自分がまたUSFを使う時が来たら見るための備忘録記事となります。(今のところRHIコマンドを自由に挿し込めないようで、RenderTargetに書き込むかComputeShader くらいの用途しかなさそう…?)

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

プロジェクト作成

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

プラグイン作成

上部メニューバーの Edit > Plugins を開き、NewPlugin から新規作成します。
f:id:coposuke:20191209234332j:plain
f:id:coposuke:20191209234343j:plain

プラグイン名はUSFを用いて実現したい内容に沿った名前を付けるのが一般的かと思います。今回は絵を描くだけなので Sketch としています。
f:id:coposuke:20191209234524j:plain

生成が完了すると、このようなディレクトリ構成となります。基本的にこれからの手順は Plugins/Sketch 以下の内容となります。
f:id:coposuke:20191209234652j:plain

Shader環境構築

フォルダ&ファイル作成

Plugins/Sketch フォルダの子階層に Shaders フォルダを作成し、更に Private と Public フォルダを作成します。そして Private フォルダ以下にテキストを作成し、"SketchShader.usf"とリネームします。

モジュールのロードタイミングを変える

作成したプラグイン Sketch.uplugin をテキストで開いて編集します。
LoadingPhase が Default となっているところを PostConfigInit に変更します。

{
    "FileVersion": 3,
    "Version": 1,
    "VersionName": "1.0",
    "FriendlyName": "Sketch",
    "Description": "",
    "Category": "Other",
    "CreatedBy": "",
    "CreatedByURL": "",
    "DocsURL": "",
    "MarketplaceURL": "",
    "SupportURL": "",
    "CanContainContent": true,
    "IsBetaVersion": false,
    "Installed": false,
    "Modules": [
        {
            "Name": "Sketch",
            "Type": "Runtime",
            "LoadingPhase": "PostConfigInit" // ←ここ
        }
    ]
}

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

作成した Shader フォルダのパスを通します。そのためにプラグインのModuleを変更します。/Project/Plugins/Sketch/Source/Sketch にある下記の3つのファイルを変更します。
f:id:coposuke:20191209235943j:plain

// Sketch.Build.cs
using UnrealBuildTool;

public class Sketch : ModuleRules
{
    public Sketch(ReadOnlyTargetRules Target) : base(Target)
    {
        ...            
        
        PublicDependencyModuleNames.AddRange(
            new string[]
            {
                "Core",
                "Projects",       // ←追記
                "RenderCore",     // ←追記
                "RHI",            // ←あとで必要になるので追記(パス追加に関係ない)
            }
            );
            
        ...        
    }
}
// Sketch.h
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FSketchModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
// Sketch.cpp
#include "Sketch.h"
#include "Interfaces/IPluginManager.h"

#define LOCTEXT_NAMESPACE "FSketchModule"

void FSketchModule::StartupModule()
{
#if (ENGINE_MINOR_VERSION >= 21)
    FString PluginDirectory = IPluginManager::Get().FindPlugin(TEXT("Sketch"))->GetBaseDir();
    if(!AllShaderSourceDirectoryMappings().Contains("/Plugins/Sketch"))
        AddShaderSourceDirectoryMapping("/Plugins/Sketch", PluginDirectory);
#endif
}

void FSketchModule::ShutdownModule()
{
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FSketchModule, Sketch)

記入したらコンパイルしてください。これで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作成

先程Shaders フォルダ以下に作成した 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 とします。

// 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("/Plugins/Sketch/Shaders/SketchShader.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(, FSketchShaderPS, TEXT("/Plugins/Sketch/Shaders/SketchShader.usf"), TEXT("MainPS"), SF_Pixel);

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

USFを使って描画

RHIコマンドを使ってレンダーテクスチャに描画していきます。Sketch プラグイン上に ActorComponent を継承したC++クラスを作成します。

// SketchComponent.h
#pragma once

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

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SKETCH_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 "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

まとめ

Plugin編ということで、Usfを用いた解説のほとんどがプラグインだったりするのですが、恐らく一番メジャーなやり方のようです。Usf はロードやコンパイル順の都合上、Plugin および Module の中で Usf 構築を進めていくことになります。Plugin化することで、外部に持っていきやすいというメリットがあるので、プロジェクト内でのModule化はあまりオススメされていないのかな?と思いました。(個人的にはModule化の方が好き)