この記事は裏UE4 Advecnt Calender 2016の15日目の記事です。
UE4のポストプロセスはマテリアルを利用して比較的簡単に拡張が可能です。
しかしながら、いくつかの問題点も存在しています。
問題の1つにCompute Shaderが使用できない、というものがあります。
Compute Shaderはラスタライザを使用せずにGPUの高速な演算機能を使用する手段で、DX11世代以降で追加されたシェーダです。
特にDX12やVulkanではグラフィクス処理と並列に処理を行うことが出来るコンピュートパイプと呼ばれるパイプラインが存在し、これを利用した非同期コンピュートはグラフィクス用途でもそれ以外の用途でも使用できます。
グラフィクス処理を行うパイプラインでも使用することは出来ますが、ここで使用する場合はコンテキストスイッチなどが発生するようで、場合によってはラスタライザを経由するより処理速度が落ちます。
しかし、Compute Shaderには共有メモリが存在します。
Pixel Shaderは各ピクセルごとに独立して処理が行われます。
そのため、テクスチャのサンプリングはそれぞれのピクセルごとに行われ、別々のピクセルで同一テクセルをサンプリングしたとしてもその情報を共有することは出来ません。
Compute Shaderは一連の処理を複数回行う際、複数の処理の束をグループとしてまとめることが出来ます。
グループ内の処理は基本的には独立しているのですが、グループ単位で少ないながらも共有メモリが使用でき、また、グループ内の処理の同期をとることが出来ます。
これを利用することで、別々のピクセルでの処理に同一テクセルを大量にサンプリングされる場面で共有メモリを介して高速化することが可能です。
今回は以前ポストプロセスマテリアルで実装したSymmetric Nearest Neighbourフィルタをエンジンを改造してCompute Shaderで実装しました。
ただ、大変残念なお知らせですが、普通にPixel Shader使うより遅いです。
コンテキストスイッチが発生してるなどで処理速度が遅くなっている可能性が高いですが、まだ何が原因かは調べていません。
余裕があったら調べようとは思いますが、わかっているのはPixel Shaderでの実装より倍遅かったです。
メモリ帯域が狭いGPUの場合は共有メモリによる高速化が効いてくるはずですが、GTX1070ではさほど効果がないようです。
PostProcessSNNが今回実装したCS版SNNで、その下の "M_SNN_Filter" がポストプロセスマテリアルで実装したPixel Shader版のSNNです。
SNNCopyという描画パスについては後にご説明します。
では、続きからで実際の実装方法を見ていきましょう。
まずはポストプロセスパラメータを追加します。
今回追加するのはSNNのクオリティを指定するためのenumだけです。
Engine/Source/Runtime/Engine/Classes/Engine/Scene.h (44行目付近)
UENUM()enum EAutoExposureMethod
{
// 中略
};
UENUM()
enum ESNNQuality
{
SNN_None UMETA(DisplayName = "None"),
SNN_Low UMETA(DisplayName = "Low"),
SNN_Middle UMETA(DisplayName = "Middle"),
SNN_High UMETA(DisplayName = "High"),
SNN_MAX,
};
同 (515行目付近)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))uint32 bOverride_ScreenSpaceReflectionRoughnessScale:1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Overrides, meta=(PinHiddenByDefault, InlineEditConditionToggle))
uint32 bOverride_SNNQuality : 1;
同 (1060行目付近)
UPROPERTY(interp, BlueprintReadWrite, Category=ScreenSpaceReflections, meta=(ClampMin = "0.01", ClampMax = "1.0", editcondition = "bOverride_ScreenSpaceReflectionMaxRoughness", DisplayName = "Max Roughness"))float ScreenSpaceReflectionMaxRoughness;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=SNN, meta = (editcondition = "bOverride_SNNQuality", DisplayName = "Quality"))
TEnumAsByte<enum ESNNQuality> SNNQuality;
同 (1300行目付近)
ScreenSpaceReflectionMaxRoughness = 0.6f;bMobileHQGaussian = false;
SNNQuality = SNN_None;
Engine/Source/Runtime/Engine/Private/SceneView.cpp (1390行目付近)
if (Src.bOverride_AmbientOcclusionRadiusInWS){
Dest.AmbientOcclusionRadiusInWS = Src.AmbientOcclusionRadiusInWS;
}
if (Src.bOverride_SNNQuality)
{
Dest.SNNQuality = Src.SNNQuality;
}
これでポストプロセスボリュームにパラメータが追加されます。
SNNのクオリティはLowが5x5、Middleが7x7、Highが9x9とします。
次にファイルを追加します。追加するのは .usf, .h. .cppの3つです。
まず、"Engine/Shaders"フォルダに"PostProcessSNN.usf"を追加します。これがシェーダファイルとなります。
次に"Engine/Source/Runtime/Renderer/Private/PostProcess"フォルダに"PostProcessSNN.cpp", "PostProcessSNN.h"を追加します。
これらのファイルはそれぞれVSプロジェクトに追加しておきましょう。
では、シェーダコードから見ていきましょう。
PostProcessSNN.usf には次のように記述します。
#include "Common.usf"#include "PostProcessCommon.usf"
/*
SNN_BOX_SIZE : Box size of snn fileter
THREADGROUP_SIZEX, Y : threadgroup pixel size
*/
uint2 TextureSize;
float BoxWidth;
float2 PixelOffset;
// Output filtered texture
RWTexture2D<float4> FilteredRWTexture;
groupshared float4 SharedColor[(THREADGROUP_SIZEX + SNN_BOX_SIZE - 1) * (THREADGROUP_SIZEY + SNN_BOX_SIZE - 1)];
float4 SelectNeighbourColor(float4 center, float4 c1, float4 c2)
{
float l1 = abs(center.g - c1.g);
float l2 = abs(center.g - c2.g);
return (l1 < l2) ? c1 : c2;
}
[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, 1)]
void MainCS(
uint3 GroupId : SV_GroupID,
uint3 DispatchThreadId : SV_DispatchThreadID,
uint3 GroupThreadId : SV_GroupThreadID,
uint GroupIndex: SV_GroupIndex)
{
// block info
const int block_width = THREADGROUP_SIZEX + SNN_BOX_SIZE - 1;
const int num_pixel = THREADGROUP_SIZEX * THREADGROUP_SIZEY;
const int num_block_pixel = (THREADGROUP_SIZEX + SNN_BOX_SIZE - 1) * (THREADGROUP_SIZEY + SNN_BOX_SIZE - 1);
const int2 box_half_size = int2(SNN_BOX_SIZE / 2, SNN_BOX_SIZE / 2);
int2 left_top = int2(GroupId.xy) * int2(THREADGROUP_SIZEX, THREADGROUP_SIZEY) - box_half_size;
// load pixel color
for(int i = GroupIndex; i < num_block_pixel; i += num_pixel)
{
int2 texel_pos = left_top + int2(i % block_width, i / block_width);
SharedColor[i] = PostprocessInput0.Load(int3(texel_pos, 0));
}
// sync
GroupMemoryBarrierWithGroupSync();
if ((DispatchThreadId.x >= TextureSize.x) || (DispatchThreadId.y >= TextureSize.y))
{
return;
}
// filtering
int2 center_offset = int2(GroupThreadId.xy) + box_half_size;
float4 center = SharedColor[center_offset.y * block_width + center_offset.x];
float4 final = center;
for (int y = -(SNN_BOX_SIZE / 2); y < 0; ++y)
{
for (int x = -(SNN_BOX_SIZE / 2); x <= (SNN_BOX_SIZE / 2); ++x)
{
int2 c1_offset = center_offset + int2(x, y);
int2 c2_offset = center_offset - int2(x, y);
float4 c1 = SharedColor[c1_offset.y * block_width + c1_offset.x];
float4 c2 = SharedColor[c2_offset.y * block_width + c2_offset.x];
final += SelectNeighbourColor(center, c1, c2) * 2.0;
}
}
for (int x = -(SNN_BOX_SIZE / 2); x < 0; ++x)
{
int2 c1_offset = center_offset + int2(x, 0);
int2 c2_offset = center_offset - int2(x, 0);
float4 c1 = SharedColor[c1_offset.y * block_width + c1_offset.x];
float4 c2 = SharedColor[c2_offset.y * block_width + c2_offset.x];
final += SelectNeighbourColor(center, c1, c2) * 2.0;
}
final /= float(SNN_BOX_SIZE * SNN_BOX_SIZE);
FilteredRWTexture[DispatchThreadId.xy] = float4(final.rgb, center.a);
}
// vertex shader entry point
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);
}
// copy only
void MainCopyPS(noperspective float2 InUV : TEXCOORD0, out float4 OutColor : SV_Target0)
{
OutColor = Texture2DSample(PostprocessInput0, PostprocessInput0Sampler, InUV);
}
MainCS関数が今回のキモのCompute Shaderになりますが、何をしているかというとまず最初にフィルタリングに必要なテクセルをサンプリングしています。
サンプリングしたテクセルは一時的に共有メモリに格納されます。
サンプリングはスレッドグループ全体で行い、それぞれのスレッドが割り当てられたテクセルをサンプリングし終えたらスレッドグループ全体で同期を行います。
同期が完了するとフィルタリングに必要なすべてのピクセルが共有メモリに格納されるので、あとはそれを利用してSNNフィルタを掛けるだけです。
その他に、MainVSとMainCopyPSという、ただスクリーンバッファをコピーするだけのシェーダが存在しますが、これらの用途については後述します。
では、C++コードを見ていきましょう。まずはヘッダファイルから。
PostProcessSNN.h
#pragma once#include "RenderingCompositionGraph.h"
class FRCPassPostProcessSNN : public TRenderingCompositePassBase<1, 1>
{
public:
FRCPassPostProcessSNN(ESNNQuality quality)
: Quality(quality)
{}
virtual void Process(FRenderingCompositePassContext& Context) override;
virtual void Release() override { delete this; }
virtual FPooledRenderTargetDesc ComputeOutputDesc(EPassOutputId InPassOutputId) const override;
static const uint32 ThreadGroupSizeX = 16;
static const uint32 ThreadGroupSizeY = 16;
private:
template <uint32 Quality> static void Dispatch(const FRenderingCompositePassContext& Context, const FSceneRenderTargetItem& DestTarget, const FIntPoint& SrcSize, const FIntRect& SrcRect);
ESNNQuality Quality;
};
class FRCPassPostProcessSNNCopy : public TRenderingCompositePassBase<1, 1>
{
public:
virtual void Process(FRenderingCompositePassContext& Context) override;
virtual void Release() override { delete this; }
virtual FPooledRenderTargetDesc ComputeOutputDesc(EPassOutputId InPassOutputId) const override;
};
2つのポストプロセスパスを作成しています。
FRCPassPostProcessSNN がフィルタの本体です。
次は.cppファイル
PostProcessSNN.cpp
#include "RendererPrivate.h"#include "ScenePrivate.h"
#include "PostProcessSNN.h"
#include "PostProcessing.h"
#include "SceneUtils.h"
#include "SceneFilterRendering.h"
template <int BoxSize>
class FPostProcessSNNCS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessSNNCS, Global);
static bool ShouldCache(EShaderPlatform Platform)
{
return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5);
}
static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), FRCPassPostProcessSNN::ThreadGroupSizeX);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), FRCPassPostProcessSNN::ThreadGroupSizeY);
OutEnvironment.SetDefine(TEXT("SNN_BOX_SIZE"), BoxSize);
OutEnvironment.CompilerFlags.Add( CFLAG_StandardOptimization );
}
/** Default constructor. */
FPostProcessSNNCS() {}
public:
FPostProcessPassParameters PostprocessParameter;
FShaderResourceParameter FilteredRWTexture;
FShaderParameter TextureSize;
/** Initialization constructor. */
FPostProcessSNNCS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
PostprocessParameter.Bind(Initializer.ParameterMap);
FilteredRWTexture.Bind(Initializer.ParameterMap, TEXT("FilteredRWTexture"));
TextureSize.Bind(Initializer.ParameterMap, TEXT("TextureSize"));
}
void SetCS(FRHICommandList& RHICmdList, const FRenderingCompositePassContext& Context, FIntPoint TextureSizeValue)
{
const FComputeShaderRHIParamRef ShaderRHI = GetComputeShader();
FGlobalShader::SetParameters(RHICmdList, ShaderRHI, Context.View);
PostprocessParameter.SetCS(ShaderRHI, Context, Context.RHICmdList, TStaticSamplerState<SF_Point,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI());
SetShaderValue(RHICmdList, ShaderRHI, TextureSize, TextureSizeValue);
}
// FShader interface.
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
Ar << PostprocessParameter << FilteredRWTexture << TextureSize;
return bShaderHasOutdatedParameters;
}
static const TCHAR* GetSourceFilename()
{
return TEXT("PostProcessSNN");
}
static const TCHAR* GetFunctionName()
{
return TEXT("MainCS");
}
};
#define VARIATION1(A) typedef FPostProcessSNNCS<A> FPostProcessSNNCS##A; \
IMPLEMENT_SHADER_TYPE2(FPostProcessSNNCS##A, SF_Compute);
VARIATION1(5) VARIATION1(7) VARIATION1(9)
#undef VARIATION1
// copy only pixel shader
class FPostProcessSNNCopyPS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessSNNCopyPS, 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("THREADGROUP_SIZEX"), FRCPassPostProcessSNN::ThreadGroupSizeX);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), FRCPassPostProcessSNN::ThreadGroupSizeY);
OutEnvironment.SetDefine(TEXT("SNN_BOX_SIZE"), 3);
OutEnvironment.CompilerFlags.Add(CFLAG_StandardOptimization);
}
/** Default constructor. */
FPostProcessSNNCopyPS() {}
public:
FPostProcessPassParameters PostprocessParameter;
/** Initialization constructor. */
FPostProcessSNNCopyPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
PostprocessParameter.Bind(Initializer.ParameterMap);
}
// FShader interface.
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_Point, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
PostprocessParameter.SetPS(ShaderRHI, Context, Filter);
}
};
IMPLEMENT_SHADER_TYPE(, FPostProcessSNNCopyPS, TEXT("PostProcessSNN"), TEXT("MainCopyPS"), SF_Pixel);
// default vertex shader.
class FPostProcessSNNCopyVS : public FGlobalShader
{
DECLARE_SHADER_TYPE(FPostProcessSNNCopyVS, Global);
public:
static bool ShouldCache(EShaderPlatform Platform)
{
return true;
}
static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), FRCPassPostProcessSNN::ThreadGroupSizeX);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), FRCPassPostProcessSNN::ThreadGroupSizeY);
OutEnvironment.SetDefine(TEXT("SNN_BOX_SIZE"), 3);
OutEnvironment.CompilerFlags.Add(CFLAG_StandardOptimization);
}
/** Default constructor. */
FPostProcessSNNCopyVS() {}
/** Initialization constructor. */
FPostProcessSNNCopyVS(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)
{
// input is not hooked up correctly
return;
}
}
};
IMPLEMENT_SHADER_TYPE(, FPostProcessSNNCopyVS, TEXT("PostProcessSNN"), TEXT("MainVS"), SF_Vertex);
template <uint32 Quality>
void FRCPassPostProcessSNN::Dispatch(const FRenderingCompositePassContext& Context, const FSceneRenderTargetItem& DestTarget, const FIntPoint& SrcSize, const FIntRect& SrcRect)
{
TShaderMapRef<FPostProcessSNNCS<Quality>> ComputeShader(Context.GetShaderMap());
SetRenderTarget(Context.RHICmdList, FTextureRHIRef(), FTextureRHIRef());
Context.RHICmdList.SetComputeShader(ComputeShader->GetComputeShader());
// set destination
check(DestTarget.UAV);
Context.RHICmdList.TransitionResource(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EGfxToCompute, DestTarget.UAV);
Context.RHICmdList.SetUAVParameter(ComputeShader->GetComputeShader(), ComputeShader->FilteredRWTexture.GetBaseIndex(), DestTarget.UAV);
ComputeShader->SetCS(Context.RHICmdList, Context, SrcSize);
int32 DispatchX = (SrcRect.Width() + ThreadGroupSizeX - 1) / ThreadGroupSizeX;
int32 DispatchY = (SrcRect.Height() + ThreadGroupSizeY - 1) / ThreadGroupSizeY;
DispatchComputeShader(Context.RHICmdList, *ComputeShader, DispatchX, DispatchY, 1);
// un-set destination
Context.RHICmdList.SetUAVParameter(ComputeShader->GetComputeShader(), ComputeShader->FilteredRWTexture.GetBaseIndex(), NULL);
}
void FRCPassPostProcessSNN::Process(FRenderingCompositePassContext& Context)
{
SCOPED_DRAW_EVENT(Context.RHICmdList, PostProcessSNN);
const FPooledRenderTargetDesc* InputDesc = GetInputDesc(ePId_Input0);
if(!InputDesc)
{
// input is not hooked up correctly
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 = SrcRect;
const FSceneRenderTargetItem& DestRenderTarget = PassOutputs[0].RequestSurface(Context);
if (Quality == SNN_Low)
{
Dispatch<5>(Context, DestRenderTarget, SrcSize, SrcRect);
}
else if (Quality == SNN_Middle)
{
Dispatch<7>(Context, DestRenderTarget, SrcSize, SrcRect);
}
else
{
Dispatch<9>(Context, DestRenderTarget, SrcSize, SrcRect);
}
Context.RHICmdList.TransitionResource(EResourceTransitionAccess::EReadable, EResourceTransitionPipeline::EComputeToGfx, DestRenderTarget.UAV);
ensureMsgf(DestRenderTarget.TargetableTexture == DestRenderTarget.ShaderResourceTexture, TEXT("%s should be resolved to a separate SRV"), *DestRenderTarget.TargetableTexture->GetName().ToString());
}
FPooledRenderTargetDesc FRCPassPostProcessSNN::ComputeOutputDesc(EPassOutputId InPassOutputId) const
{
FPooledRenderTargetDesc Ret = GetInput(ePId_Input0)->GetOutput()->RenderTargetDesc;
Ret.Reset();
Ret.ClearValue = FClearValueBinding::None;
Ret.Format = PF_R8G8B8A8;
Ret.TargetableFlags |= TexCreate_UAV;
Ret.TargetableFlags |= TexCreate_RenderTargetable;
Ret.DebugName = TEXT("SNN");
return Ret;
}
void FRCPassPostProcessSNNCopy::Process(FRenderingCompositePassContext& Context)
{
const FPooledRenderTargetDesc* InputDesc = GetInputDesc(ePId_Input0);
if (!InputDesc)
{
// input is not hooked up correctly
return;
}
const FSceneView& View = Context.View;
const FSceneViewFamily& ViewFamily = *(View.Family);
FIntPoint SrcSize = InputDesc->Extent;
FIntPoint DestSize = PassOutputs[0].RenderTargetDesc.Extent;
// e.g. 4 means the input texture is 4x smaller than the buffer size
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, DiffusionPow2, TEXT("SNNCopy"));
const FSceneRenderTargetItem& DestRenderTarget = PassOutputs[0].RequestSurface(Context);
// check if we have to clear the whole surface.
// Otherwise perform the clear when the dest rectangle has been computed.
auto FeatureLevel = Context.View.GetFeatureLevel();
if (FeatureLevel == ERHIFeatureLevel::ES2 || FeatureLevel == ERHIFeatureLevel::ES3_1)
{
// Set the view family's render target/viewport.
SetRenderTarget(Context.RHICmdList, DestRenderTarget.TargetableTexture, FTextureRHIRef(), ESimpleRenderTargetMode::EClearColorAndDepth);
Context.SetViewportAndCallRHI(0, 0, 0.0f, DestSize.X, DestSize.Y, 1.0f);
}
else
{
// Set the view family's render target/viewport.
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);
}
// set the state
Context.RHICmdList.SetBlendState(TStaticBlendState<>::GetRHI());
Context.RHICmdList.SetRasterizerState(TStaticRasterizerState<>::GetRHI());
Context.RHICmdList.SetDepthStencilState(TStaticDepthStencilState<false, CF_Always>::GetRHI());
// set shader
{
auto ShaderMap = Context.GetShaderMap();
TShaderMapRef<FPostProcessSNNCopyVS> VertexShader(ShaderMap);
TShaderMapRef<FPostProcessSNNCopyPS> PixelShader(ShaderMap);
static FGlobalBoundShaderState BoundShaderState;
SetGlobalBoundShaderState(Context.RHICmdList, Context.GetFeatureLevel(), BoundShaderState, GFilterVertexDeclaration.VertexDeclarationRHI, *VertexShader, *PixelShader);
PixelShader->SetParameters(Context, InputDesc);
VertexShader->SetParameters(Context);
}
TShaderMapRef<FPostProcessSNNCopyVS> 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, // This pass is input for passes that can't use the hmd mask, so we need to disable it to ensure valid input data
EDRF_UseTriangleOptimization);
Context.RHICmdList.CopyToResolveTarget(DestRenderTarget.TargetableTexture, DestRenderTarget.ShaderResourceTexture, false, FResolveParams());
}
FPooledRenderTargetDesc FRCPassPostProcessSNNCopy::ComputeOutputDesc(EPassOutputId InPassOutputId) const
{
FPooledRenderTargetDesc Ret = GetInput(ePId_Input0)->GetOutput()->RenderTargetDesc;
Ret.Reset();
Ret.Format = PF_B8G8R8A8;
Ret.TargetableFlags &= ~TexCreate_UAV;
Ret.TargetableFlags |= TexCreate_RenderTargetable;
Ret.AutoWritable = false;
Ret.DebugName = TEXT("SNNCopy");
Ret.ClearValue = FClearValueBinding(FLinearColor(0, 0, 0, 0));
return Ret;
}
大変長いですが、コピペすれば大丈夫です。
Compute Shaderを利用する場合はDraw系の命令は使用しません。
DispatchComputeShaderという命令がありますが、こちらがCompute Shaderを駆動させるための命令です。
この記事ではCompute Shaderの詳しい使用方法などは書きません。興味がある方はDirectXのヘルプなどをお読みください。
これでレンダーパスの実装は完了したので、実際に呼び出しましょう。
Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp (54行目付近)
#include "PostProcessStreamingAccuracyLegend.h"#include "PostProcessSNN.h"
同 (1630行目付近)
if(bAllowTonemapper){
auto Node = AddSinglePostProcessMaterial(Context, BL_ReplacingTonemapper);
// 中略
}
ESNNQuality snnQuality = View.FinalPostProcessSettings.SNNQuality;
if (snnQuality != SNN_None)
{
FRCPassPostProcessSNN* SNN = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessSNN(snnQuality));
SNN->SetInput(ePId_Input0, Context.FinalOutput);
FRCPassPostProcessSNNCopy* SNNCopy = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessSNNCopy());
SNNCopy->SetInput(ePId_Input0, FRenderingCompositeOutputRef(SNN));
Context.FinalOutput = FRenderingCompositeOutputRef(SNNCopy);
}
これでビルドが通ればバッチリ使えるようになります。
さて、ここで問題になるのはSNNCopyというレンダーパスです。
SNNフィルタ自体はSNNパスで行われるのですが、わざわざコピーを行っているのはなぜでしょう?
Compute ShaderはPixel Shaderと違って明確な出力が存在しません。
Compute Shaderによる出力は、Unordered Access View (UAV) と呼ばれるViewが示す読み書き可能なバッファに対してのみ行われます。
このUAVとして設定可能なバッファのフォーマットにはいくつか条件があり、UE4がフレームバッファとして使用しているB8G8R8A8フォーマットはUAVとして設定できません。
なぜ?と言われると、ハードウェアかDirectXの仕様だと思われます。
つまり、UE4のフレームバッファをCompute Shaderの出力として設定するとUAVとして使用できないためクラッシュしてしまうのです。
ですが、ポストプロセスパスは基本的にオフスクリーンのバッファを割り当て、そこに描画を行います。
つまり、SNNフィルタに対してもオフスクリーンバッファを指定してやればいいわけで、それ自体は FRenderingCompositeOutputRef を使用すれば簡単に対応できます。
が、ここで罠が。
ポストプロセスパスの最終レンダーパスでは、オフスクリーンバッファのフォーマットや形式をどのように指定しても自動的にフレームバッファが設定されるようになっているのです。
普通に考えれば当たり前なのですが、SNNが最後のレンダーパスになってしまうとUAVとして使用できないバッファが出力として設定されることになってしまうのです。
結果、UAVが作成できないのでサヨナラ!爆発四散!となってしまうのです。
これを回避するため、SNNフィルタを適用した後に、そのバッファをコピーするレンダーパスを設けました。
SNNCopyパスはPixel Shaderなので、UAVが指定できないフレームバッファが出力となっても問題ない、ということです。
最後に。
今回の実装ではCompute Shaderがグラフィクスパイプでしか動作しません。
つまり、非同期コンピュートは使えません。
SNNフィルタ自体が非同期コンピュートで実装して嬉しいものでもないので仕方ないと言えば仕方ないですが、UE4で非同期コンピュートをやりたい!という方も多いのではないかと思います。
残念ながら、これについては調べていないので、どなたか調べたら教えていただけるとありがたいです。
これにて15日目の記事は終了です。
明日はhannover_blossさんによる「UE4のPlanar Reflectionについて」です。