[UE4] エンジンを拡張してポストプロセスを実装する PS編

1 0
この記事はUE4 Advent Calendar 2016の15日目の記事です。

UE4ではポストプロセスの作成にポストプロセスマテリアルを使用します。
通常のマテリアルと同様にマテリアルエディタで作成でき、マテリアルインスタンスも使用できます。
ポストプロセスのパラメータをランタイムで変更する場合はパラメータコレクションを利用する必要がありますが、BPを利用すればレベルごとの設定などもそれほど難しくなく便利です。

しかしいくつかの問題点が存在します。
そのうちの1つで、個人的に最も大きいのがオフスクリーンバッファを使用できないことです。

ポストプロセスを本格的に作成する場合、複数の画像をコンポジットする必要が出てきます。
ポストプロセスマテリアルでもG-Bufferとして保存されている画像はコンポジットすることが出来ますが、それらから生成した複数の画像をコンポジットするということが出来ません。
なのでポストプロセスマテリアル内でそれらすべての画像の加工からコンポジットまで行う必要があります。
それで実装可能であれば問題ないのでは?と思う方もおられるかもしれませんが、加工が重い処理の場合は速度的にどうなの?とか、クオリティは保てるの?などの疑問も出てきます。

それらを解決する方法としてエンジン側にポストプロセスを作成、埋め込むという方法があります。
もちろんエンジン改造となるので改造コストは安くないですし、特に改造後のエンジンバージョンアップに対応するコストが馬鹿になりません。
ただ、ポストプロセス部分は割と独立してる感のある部分なので、既存のポストプロセスの順番を入れ替えるよりマージコストは低いのではないかと思います。

というわけで、今回は以前ポストプロセスマテリアルで実装したディフュージョンフィルタを、エンジン改造によってよりクオリティの高いものにする実験を行いました。
ディフュージョンフィルタは以下のようなパスで実装されます。

ue411.jpg

元の画像の色を2乗して、このバッファをガウスブラー、2乗カラー画像とブラー画像をスクリーンブレンドし、最後に元画像との間で色の最大値を取ります。

ポストプロセスマテリアルの実装ではカラーの2乗~比較(明)までの処理が1パスで処理されていました。
問題点を上げるとするなら、ガウスブラーを行う前にかならずサンプリングしたテクセルを2乗する必要があるという点です。
2乗した画像が別に存在するなら、ただそれをガウスブラーすればいいだけなのですが、ポストプロセスマテリアルでは1パスですべての処理を終わらせなければならないのでこのような実装になります。

また、ガウスブラーの実装にも問題があります。
ガウスブラーのGPU実装はX軸方向のガウスフィルタとY軸方向のガウスフィルタを別パスで、つまり2パスで行うことが多いです。
この方が高速で、且つ綺麗にブラー出来ます。
ポストプロセスマテリアルではこの手法は使えないので、ボックスブラーよろしくボックス状にテクセルをサンプリングしてガウスブラーを掛けなければなりません。
綺麗にしようと思うとサンプリング数がかなり増加して速度に問題が発生します。

最後の問題点は縮小バッファが使えないことです。
ガウスブラーは画面をぼかしてしまうので、低解像度でもさほど問題ない場合があります。
カラーの2乗を行う際に縮小バッファに描画し、これをガウスブラーするのであればより高速ですが、ポストプロセスマテリアルではそのようなことが不可能です。

今回の実装はボカシの綺麗さだけではなく、速度的にも解像度によっては有利です。
こちらは比較的高解像度での処理速度比較です。

ue412.jpg

DiffusionPow2~Diffusionまでが今回入れた処理です。
2乗カラーをガウスブラーしてダウンサンプルしてガウスブラー…という感じでガウスブラーを3回行った結果をぼかし画像として使用しています。
合計が0.57msでその下にあるポストプロセスマテリアルでの実装より少しだけ高速です。
ただし、解像度が低い場合はポストプロセスマテリアル実装のほうが高速です。
もちろん、クオリティはエンジン改造のほうが高いですね。

というわけで、続きからこの実装方法について書いていきます。
なお、.usf, .h, .cppファイルをそれぞれ1つずつ追加している点にご注意ください。
追加してプロジェクトに登録する方法は書いていません。

まずは既存のエンジン部分を改造していきます。
特に困るのがポストプロセスパラメータの追加で、これを行うとエンジンコードがほぼフルビルドとなります。
できるだけ1回で済ませたいところですが、現実にはそうも行きません。
とは言え、今回は改造内容が決まっていますので、とりあえずここから進めてしまいましょう。

追加するパラメータは2つだけ。
ディフュージョンフィルタのクオリティとブラーのオフセットです。
クオリティはLow、Middle、Highの3種類とNone(なし)を選べるようにしましょう。

Engine/Source/Runtime/Engine/Classes/Engine/Scene.h (44行目付近)
UENUM()
enum EAutoExposureMethod
{
// 略
};

UENUM()
enum EDiffusionQuality
{
DFQ_None UMETA(DisplayName = "None"),
DFQ_Low UMETA(DisplayName = "Low"),
DFQ_Middle UMETA(DisplayName = "Middle"),
DFQ_High UMETA(DisplayName = "High"),
DFQ_MAX,
};

同 (510行目付近)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_ScreenSpaceReflectionRoughnessScale:1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_DiffusionQuality:1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_DiffusionScale:1;

同 (1060行目付近)
/** Until what roughness we fade the screen space reflections, 0.8 works well, smaller can run faster */
UPROPERTY(interp, BlueprintReadWrite, Category=ScreenSpaceReflections, meta=(ClampMin = "0.01", ClampMax = "1.0", editcondition = "bOverride_ScreenSpaceReflectionMaxRoughness", DisplayName = "Max Roughness"))
float ScreenSpaceReflectionMaxRoughness;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Diffusion, meta=(editcondition = "bOverride_DiffusionQuality", DisplayName = "Quality"))
TEnumAsByte<enum EDiffusionQuality> DiffusionQuality;
UPROPERTY(interp, BlueprintReadWrite, Category=Diffusion, meta=(ClampMin = "1.0", UIMax = "8.0", editcondition = "bOverride_DiffusionScale", DisplayName = "Scale"))
float DiffusionScale;

Engine/Source/Runtime/Engine/Private/SceneView.cpp (1323行目付近)
LERP_PP(ScreenSpaceReflectionIntensity);
LERP_PP(ScreenSpaceReflectionMaxRoughness);
LERP_PP(DiffusionScale);

同 (1385行目付近)
if (Src.bOverride_AmbientOcclusionRadiusInWS)
{
Dest.AmbientOcclusionRadiusInWS = Src.AmbientOcclusionRadiusInWS;
}

if (Src.bOverride_DiffusionQuality)
{
Dest.DiffusionQuality = Src.DiffusionQuality;
}

次にファイルを追加しましょう。
ファイルはシェーダ、ヘッダファイル、cppファイルの3つで、どれもディフュージョンフィルタ用のものです。

まずはシェーダファイル。他のポストプロセスと同じ命名規則を用いるため、PostProcessDiffusion.usf とします。
このファイルは "Engine/Shaders" フォルダの下に置きましょう。

PostProcessDiffusion.usf
#include "Common.usf"
#include "PostProcessCommon.usf"

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

void MainPow2PS(noperspective float2 InUV : TEXCOORD0, out float4 OutColor : SV_Target0)
{
OutColor = Texture2DSample(PostprocessInput0, PostprocessInput0Sampler, InUV);
OutColor.rgb *= OutColor.rgb;
}

void MainPS(noperspective float2 InUV : TEXCOORD0, out float4 OutColor : SV_Target0)
{
float4 baseColor = Texture2DSample(PostprocessInput0, PostprocessInput0Sampler, InUV);
float4 pow2Color = baseColor * baseColor;
float4 blurColor = Texture2DSample(PostprocessInput1, PostprocessInput1Sampler, InUV);

#if QUALITY == 1
#elif QUALITY == 2
#else // QUALITY == 0
#endif

float4 screenColor = (1.0f - ((1.0f - pow2Color) * (1.0f - blurColor)));

OutColor.rgb = max(baseColor.rgb, screenColor.rgb);
OutColor.a = baseColor.a;
}

QUALITYという定数によって処理を分けられるようにしてみたのですが、今回は不要でした。
#if QUALITY~ のくだりはその名残ですね。

ヘッダファイルとcppファイルはどちらもファイル名を PostProcessDiffusion.h, .cpp とします。
置き場所は "Engine/Source/Runtime/Renderer/Private/PostProcess" の下です。
ファイルを作成したらVisual Studioでプロジェクト内にも登録しましょう。

PostProcessDiffusion.h
#pragma once

#include "RenderingCompositionGraph.h"
#include "PostProcessing.h"

/**
* ePId_Input0: main input texture
*/
class FRCPassPostProcessDiffusionPow2 : public TRenderingCompositePassBase<1, 1>
{
public:
// constructor
FRCPassPostProcessDiffusionPow2(const TCHAR* InDebugName = TEXT("DiffusionPow2"));

// interface FRenderingCompositePass ---------
virtual void Process(FRenderingCompositePassContext& Context) override;
virtual void Release() override { delete this; }
virtual FPooledRenderTargetDesc ComputeOutputDesc(EPassOutputId InPassOutputId) const override;

private:
const TCHAR* DebugName;
};

/**
* ePId_Input0: main input texture
* ePId_Input1: blur texture
*/
class FRCPassPostProcessDiffusion : public TRenderingCompositePassBase<2, 1>
{
public:
// constructor
FRCPassPostProcessDiffusion(const TCHAR* InDebugName = TEXT("Diffusion"), int32 quality = 0);

// interface FRenderingCompositePass ---------
virtual void Process(FRenderingCompositePassContext& Context) override;
virtual void Release() override { delete this; }
virtual FPooledRenderTargetDesc ComputeOutputDesc(EPassOutputId InPassOutputId) const override;

private:
template <uint32 Quality> static void SetShader(const FRenderingCompositePassContext& Context, const FPooledRenderTargetDesc* InputDesc0, const FPooledRenderTargetDesc* InputDesc1);

const TCHAR* DebugName;
int32 Quality;
};

PostProcessDiffusion.cpp
#include "RendererPrivate.h"
#include "ScenePrivate.h"
#include "SceneFilterRendering.h"
#include "PostProcessDiffusion.h"
#include "SceneUtils.h"

class FPostProcessDiffusionPow2PS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessDiffusionPow2PS, Global);

static bool ShouldCache(EShaderPlatform Platform)
{
return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
}

static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment);
}

FPostProcessDiffusionPow2PS() {}

public:
FPostProcessPassParameters PostprocessParameter;

FPostProcessDiffusionPow2PS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
PostprocessParameter.Bind(Initializer.ParameterMap);
}

virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
Ar << PostprocessParameter;
return bShaderHasOutdatedParameters;
}

void SetParameters(const FRenderingCompositePassContext& Context, const FPooledRenderTargetDesc* InputDesc)
{
const FPixelShaderRHIParamRef ShaderRHI = GetPixelShader();

FGlobalShader::SetParameters(Context.RHICmdList, ShaderRHI, Context.View);

// filter only if needed for better performance
FSamplerStateRHIParamRef Filter = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();

PostprocessParameter.SetPS(ShaderRHI, Context, Filter);
}

static const TCHAR* GetSourceFilename()
{
return TEXT("PostProcessDiffusion");
}

static const TCHAR* GetFunctionName()
{
return TEXT("MainPow2PS");
}
};

template <uint32 Quality>
class FPostProcessDiffusionPS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessDiffusionPS, Global);

static bool ShouldCache(EShaderPlatform Platform)
{
return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
}

static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment);
OutEnvironment.SetDefine(TEXT("QUALITY"), Quality);
}

FPostProcessDiffusionPS() {}

public:
FPostProcessPassParameters PostprocessParameter;

FPostProcessDiffusionPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
PostprocessParameter.Bind(Initializer.ParameterMap);
}

virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
Ar << PostprocessParameter;
return bShaderHasOutdatedParameters;
}

void SetParameters(const FRenderingCompositePassContext& Context, const FPooledRenderTargetDesc* InputDesc)
{
const FPixelShaderRHIParamRef ShaderRHI = GetPixelShader();

FGlobalShader::SetParameters(Context.RHICmdList, ShaderRHI, Context.View);

FSamplerStateRHIParamRef Filter = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();

PostprocessParameter.SetPS(ShaderRHI, Context, Filter);
}

static const TCHAR* GetSourceFilename()
{
return TEXT("PostProcessDiffusion");
}

static const TCHAR* GetFunctionName()
{
return TEXT("MainPS");
}
};

IMPLEMENT_SHADER_TYPE(, FPostProcessDiffusionPow2PS, TEXT("PostProcessDiffusion"), TEXT("MainPow2PS"), SF_Pixel);
#define VARIATION1(A) typedef FPostProcessDiffusionPS<A> FPostProcessDiffusionPS##A; \
IMPLEMENT_SHADER_TYPE2(FPostProcessDiffusionPS##A, SF_Pixel);

VARIATION1(0) VARIATION1(1) VARIATION1(2)
#undef VARIATION1


class FPostProcessDiffusionVS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessDiffusionVS, Global);
public:

static bool ShouldCache(EShaderPlatform Platform)
{
return true;
}

/** Default constructor. */
FPostProcessDiffusionVS() {}

/** Initialization constructor. */
FPostProcessDiffusionVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) :
FGlobalShader(Initializer)
{
}

/** Serializer */
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);

return bShaderHasOutdatedParameters;
}

void SetParameters(const FRenderingCompositePassContext& Context)
{
const FVertexShaderRHIParamRef ShaderRHI = GetVertexShader();

FGlobalShader::SetParameters(Context.RHICmdList, ShaderRHI, Context.View);

const FPooledRenderTargetDesc* InputDesc = Context.Pass->GetInputDesc(ePId_Input0);

if (!InputDesc)
{
return;
}
}
};

IMPLEMENT_SHADER_TYPE(, FPostProcessDiffusionVS, TEXT("PostProcessDiffusion"), TEXT("MainVS"), SF_Vertex);



FRCPassPostProcessDiffusionPow2::FRCPassPostProcessDiffusionPow2(const TCHAR *InDebugName)
: DebugName(InDebugName)
{
}

void FRCPassPostProcessDiffusionPow2::Process(FRenderingCompositePassContext& Context)
{
const FPooledRenderTargetDesc* InputDesc = GetInputDesc(ePId_Input0);

if (!InputDesc)
{
return;
}

const FSceneView& View = Context.View;
const FSceneViewFamily& ViewFamily = *(View.Family);

FIntPoint SrcSize = InputDesc->Extent;
FIntPoint DestSize = PassOutputs[0].RenderTargetDesc.Extent;

uint32 ScaleFactor = FMath::DivideAndRoundUp(FSceneRenderTargets::Get(Context.RHICmdList).GetBufferSizeXY().Y, SrcSize.Y);

FIntRect SrcRect = View.ViewRect / ScaleFactor;
FIntRect DestRect = FIntRect::DivideAndRoundUp(SrcRect, 2);
SrcRect = DestRect * 2;

SCOPED_DRAW_EVENTF(Context.RHICmdList, DiffusionPow2, TEXT("DiffusionPow2"));

const FSceneRenderTargetItem& DestRenderTarget = PassOutputs[0].RequestSurface(Context);

auto FeatureLevel = Context.View.GetFeatureLevel();
if (FeatureLevel == ERHIFeatureLevel::ES2 || FeatureLevel == ERHIFeatureLevel::ES3_1)
{
SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef(), ESimpleRenderTargetMode::EClearColorAndDepth);
Context.SetViewportAndCallRHI(0, 0, 0.0f, DestSize.X, DestSize.Y, 1.0f);
}
else
{
SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef(), ESimpleRenderTargetMode::EExistingColorAndDepth);
Context.SetViewportAndCallRHI(0, 0, 0.0f, DestSize.X, DestSize.Y, 1.0f);
DrawClearQuad(Context.RHICmdList, Context.GetFeatureLevel(), true, FLinearColor(0, 0, 0, 0), false, 1.0f, false, 0, DestSize, DestRect);
}

Context.RHICmdList.SetBlendState(TStaticBlendState<>::GetRHI());
Context.RHICmdList.SetRasterizerState(TStaticRasterizerState<>::GetRHI());
Context.RHICmdList.SetDepthStencilState(TStaticDepthStencilState<false, CF_Always>::GetRHI());

{
auto ShaderMap = Context.GetShaderMap();
TShaderMapRef<FPostProcessDiffusionVS> VertexShader(ShaderMap);
TShaderMapRef<FPostProcessDiffusionPow2PS> PixelShader(ShaderMap);

static FGlobalBoundShaderState BoundShaderState;

SetGlobalBoundShaderState(Context.RHICmdList, Context.GetFeatureLevel(), BoundShaderState, GFilterVertexDeclaration.VertexDeclarationRHI, *VertexShader, *PixelShader);

PixelShader->SetParameters(Context, InputDesc);
VertexShader->SetParameters(Context);
}

TShaderMapRef<FPostProcessDiffusionVS> VertexShader(Context.GetShaderMap());

DrawPostProcessPass(
Context.RHICmdList,
DestRect.Min.X, DestRect.Min.Y,
DestRect.Width(), DestRect.Height(),
SrcRect.Min.X, SrcRect.Min.Y,
SrcRect.Width(), SrcRect.Height(),
DestSize,
SrcSize,
*VertexShader,
View.StereoPass,
false,
EDRF_UseTriangleOptimization);

Context.RHICmdList.CopyToResolveTarget(DestRenderTarget.TargetableTexture, DestRenderTarget.ShaderResourceTexture, false, FResolveParams());
}

FPooledRenderTargetDesc FRCPassPostProcessDiffusionPow2::ComputeOutputDesc(EPassOutputId InPassOutputId) const
{
FPooledRenderTargetDesc Ret = GetInput(ePId_Input0)->GetOutput()->RenderTargetDesc;

Ret.Reset();

Ret.Extent = FIntPoint::DivideAndRoundUp(Ret.Extent, 2);
Ret.Extent.X = FMath::Max(1, Ret.Extent.X);
Ret.Extent.Y = FMath::Max(1, Ret.Extent.Y);
Ret.TargetableFlags &= ~TexCreate_UAV;
Ret.TargetableFlags |= TexCreate_RenderTargetable;
Ret.AutoWritable = false;
Ret.DebugName = DebugName;

Ret.ClearValue = FClearValueBinding(FLinearColor(0, 0, 0, 0));

return Ret;
}


FRCPassPostProcessDiffusion::FRCPassPostProcessDiffusion(const TCHAR *InDebugName, int32 quality)
: DebugName(InDebugName)
, Quality(quality)
{
}

template <uint32 Quality>
void FRCPassPostProcessDiffusion::SetShader(const FRenderingCompositePassContext& Context, const FPooledRenderTargetDesc* InputDesc0, const FPooledRenderTargetDesc* InputDesc1)
{
auto ShaderMap = Context.GetShaderMap();
TShaderMapRef<FPostProcessDiffusionVS> VertexShader(ShaderMap);
TShaderMapRef<FPostProcessDiffusionPS<Quality> > PixelShader(ShaderMap);

static FGlobalBoundShaderState BoundShaderState;

SetGlobalBoundShaderState(Context.RHICmdList, Context.GetFeatureLevel(), BoundShaderState, GFilterVertexDeclaration.VertexDeclarationRHI, *VertexShader, *PixelShader);

PixelShader->SetParameters(Context, InputDesc0);
PixelShader->SetParameters(Context, InputDesc1);
VertexShader->SetParameters(Context);
}

void FRCPassPostProcessDiffusion::Process(FRenderingCompositePassContext& Context)
{
const FPooledRenderTargetDesc* InputDesc0 = GetInputDesc(ePId_Input0);
const FPooledRenderTargetDesc* InputDesc1 = GetInputDesc(ePId_Input1);

if (!InputDesc0 || !InputDesc1)
{
return;
}

const FSceneView& View = Context.View;
const FSceneViewFamily& ViewFamily = *(View.Family);

FIntPoint SrcSize = InputDesc0->Extent;
FIntPoint DestSize = PassOutputs[0].RenderTargetDesc.Extent;

uint32 ScaleFactor = FMath::DivideAndRoundUp(FSceneRenderTargets::Get(Context.RHICmdList).GetBufferSizeXY().Y, SrcSize.Y);

FIntRect SrcRect = View.ViewRect / ScaleFactor;
FIntRect DestRect = SrcRect;

SCOPED_DRAW_EVENTF(Context.RHICmdList, Diffusion, TEXT("Diffusion"));

const FSceneRenderTargetItem& DestRenderTarget = PassOutputs[0].RequestSurface(Context);

auto FeatureLevel = Context.View.GetFeatureLevel();
if (FeatureLevel == ERHIFeatureLevel::ES2 || FeatureLevel == ERHIFeatureLevel::ES3_1)
{
SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef(), ESimpleRenderTargetMode::EClearColorAndDepth);
Context.SetViewportAndCallRHI(0, 0, 0.0f, DestSize.X, DestSize.Y, 1.0f);
}
else
{
SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef(), ESimpleRenderTargetMode::EExistingColorAndDepth);
Context.SetViewportAndCallRHI(0, 0, 0.0f, DestSize.X, DestSize.Y, 1.0f);
DrawClearQuad(Context.RHICmdList, Context.GetFeatureLevel(), true, FLinearColor(0, 0, 0, 0), false, 1.0f, false, 0, DestSize, DestRect);
}

Context.RHICmdList.SetBlendState(TStaticBlendState<>::GetRHI());
Context.RHICmdList.SetRasterizerState(TStaticRasterizerState<>::GetRHI());
Context.RHICmdList.SetDepthStencilState(TStaticDepthStencilState<false, CF_Always>::GetRHI());

if (Quality == DFQ_Low)
{
SetShader<0>(Context, InputDesc0, InputDesc1);
}
else if (Quality == DFQ_Middle)
{
SetShader<1>(Context, InputDesc0, InputDesc1);
}
else
{
SetShader<2>(Context, InputDesc0, InputDesc1);
}

TShaderMapRef<FPostProcessDiffusionVS> VertexShader(Context.GetShaderMap());

DrawPostProcessPass(
Context.RHICmdList,
DestRect.Min.X, DestRect.Min.Y,
DestRect.Width(), DestRect.Height(),
SrcRect.Min.X, SrcRect.Min.Y,
SrcRect.Width(), SrcRect.Height(),
DestSize,
SrcSize,
*VertexShader,
View.StereoPass,
false,
EDRF_UseTriangleOptimization);

Context.RHICmdList.CopyToResolveTarget(DestRenderTarget.TargetableTexture, DestRenderTarget.ShaderResourceTexture, false, FResolveParams());
}

FPooledRenderTargetDesc FRCPassPostProcessDiffusion::ComputeOutputDesc(EPassOutputId InPassOutputId) const
{
FPooledRenderTargetDesc Ret = GetInput(ePId_Input0)->GetOutput()->RenderTargetDesc;

Ret.Reset();

Ret.TargetableFlags &= ~TexCreate_UAV;
Ret.TargetableFlags |= TexCreate_RenderTargetable;
Ret.AutoWritable = false;
Ret.DebugName = DebugName;

Ret.ClearValue = FClearValueBinding(FLinearColor(0, 0, 0, 0));

return Ret;
}

大変長いですが、試すだけならコピペすれば大丈夫です。

簡単に解説を行います。
まず、今回のディフュージョンフィルタ用には2つのポストプロセスパスを追加しました。
1つはカラーの2乗を縮小バッファに対して行うパスです。FRCPassPostProcessDiffusionPow2 がこれにあたります。
もう1つはブラーの結果を利用して最終的な処理を行うパスです。FRCPassPostProcessDiffusion がこれですね。
ブラーとダウンサンプルはUE4に存在しているものをそのまま流用しています。

描画パスは、
 1.縦横1/2の縮小バッファに対してカラーの2乗を作成するパス
 2.ガウスブラーX、Y (2パス)
 3.クオリティ設定によっては更に1/2にするダウンサンプル、ガウスブラーX、Y (3パス、最大2回)
 4.ブラー結果と元画像をコンポジットするパス(最終パス)
となります。

ランタイム内でのシェーダオブジェクト生成は PostProcessDiffusion.cpp 内の FPostProcessDiffusionPow2PS, FPostProcessDiffusionPS, FPostProcessDiffusionVS の3つのクラスで行います。
単一能のシェーダはこのようなクラスを作成し、IMPLEMENT_SHADER_TYPE() マクロを利用すれば生成できます。
あとは各処理内で呼び出せばOKです。

ポストプロセスの実装は FRenderingCompositePass のクラスが持っている純粋仮想関数 Process() 内で行います。
各種ステート設定~描画までをこの命令で実装します。

ComputeOutputDesc() 関数はこのポストプロセスで使用するレンダーターゲットのフォーマットやサイズなどを指定しています。
今回は FRCPassPostProcessDiffusionPow2::ComputeOutputDesc() でバッファを縮小するように設定している以外はほぼ入力バッファそのままの情報を利用します。

最後にディフュージョンフィルタをポストプロセスパスに登録します。

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp (55行目付近)
#include "PostProcessStreamingAccuracyLegend.h"
#include "PostProcessDiffusion.h"

同 (1660行目付近)
if(bAllowTonemapper)
{
// 中略

// Add a pass-through as tonemapper will be forced LDR if final pass in chain
if (bHDRTonemapperOutput)
{
FRenderingCompositePass* PassthroughNode = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessPassThrough(nullptr));
PassthroughNode->SetInput(ePId_Input0, FRenderingCompositeOutputRef(Context.FinalOutput));
Context.FinalOutput = FRenderingCompositeOutputRef(PassthroughNode);
}
}

EDiffusionQuality diffusionQuality = View.FinalPostProcessSettings.DiffusionQuality;
if (diffusionQuality != DFQ_None)
{
FRCPassPostProcessDiffusionPow2* DiffusionPow2 = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessDiffusionPow2(TEXT("DiffusionPow2")));
DiffusionPow2->SetInput(ePId_Input0, Context.FinalOutput);

FRenderingCompositeOutputRef BlurOutput = RenderGaussianBlur(Context, TEXT("DiffusionBlurX1"), TEXT("DiffusionBlurY1"), FRenderingCompositeOutputRef(DiffusionPow2), View.FinalPostProcessSettings.DiffusionScale);
if (diffusionQuality != DFQ_Low)
{
FRCPassPostProcessDownsample* downSample = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessDownsample());
downSample->SetInput(ePId_Input0, BlurOutput);
BlurOutput = RenderGaussianBlur(Context, TEXT("DiffusionBlurX2"), TEXT("DiffusionBlurY2"), FRenderingCompositeOutputRef(downSample), View.FinalPostProcessSettings.DiffusionScale);

if (diffusionQuality != DFQ_Middle)
{
downSample = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessDownsample());
downSample->SetInput(ePId_Input0, BlurOutput);
BlurOutput = RenderGaussianBlur(Context, TEXT("DiffusionBlurX3"), TEXT("DiffusionBlurY3"), FRenderingCompositeOutputRef(downSample), View.FinalPostProcessSettings.DiffusionScale);
}
}

FRCPassPostProcessDiffusion* Diffusion = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessDiffusion(TEXT("Diffusion"), diffusionQuality));
Diffusion->SetInput(ePId_Input0, Context.FinalOutput);
Diffusion->SetInput(ePId_Input1, BlurOutput);
Context.FinalOutput = FRenderingCompositeOutputRef(Diffusion);
}

追加位置はFXAAの処理を行う直前にしています。
クオリティは Low, Middle, High の3つがあり、それぞれガウスブラーの回数が 1, 2, 3 回となっています。

ここまで追加すればエンジンコードをビルドして、このエンジンで作成したプロジェクトでポストプロセスボリュームを選択してみてください。

ue413.jpg

このようにDiffusionの項目が追加されています。
あとは Quality パラメータをNone以外にすればディフュージョンフィルタがかかるはずです。
Scale を 8.0 まで上げるとブラーが広く出るのがわかりますが、ポストプロセスマテリアルでの実装より綺麗にブラーがかかるでしょう。

ue414.jpg

わかりやすい部分を拡大してみました。
左がポストプロセスマテリアルによる実装、右がエンジン改造の実装です。
左はブロックっぽいノイズが出てしまっていますね。

同じような実装をすればディフュージョンフィルタ以外ももちろん実装できますし、処理する順番も自由に入れ替え可能です。
ポストプロセスの実装はエンジン改造の中でも比較的易しく、マージコストも低くなると思います。
ポストプロセス自体の実装は別のファイルに分離できますし、既存コードへの追加は最小限、且つ変更に対しても柔軟な対応ができます。

ただし、元々あるポストプロセスを変更したり、それらの順番を入れ替えたりはコストが高くなりがちなので注意してください。

15日目の記事はここまでです。
明日はこなべさんによる「UE4でふっとばし甲斐のあるものをふっとばす」です。
ページトップ