Files
UnrealEngine/Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessWeightedSampleSum.cpp
2025-05-18 13:04:45 +08:00

566 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PostProcess/PostProcessWeightedSampleSum.h"
#include "PixelShaderUtils.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "SceneRendering.h"
// maximum number of sample using the shader that has the dynamic loop
#define MAX_FILTER_SAMPLES 128
#define MAX_PACKED_SAMPLES_OFFSET ((MAX_FILTER_SAMPLES + 1) / 2)
// maximum number of sample available using unrolled loop shaders
#define MAX_FILTER_COMPILE_TIME_SAMPLES 32
#define MAX_FILTER_COMPILE_TIME_SAMPLES_IOS 15
#define MAX_FILTER_COMPILE_TIME_SAMPLES_ES3_1 7
namespace
{
const int32 GFilterComputeTileSizeX = 8;
const int32 GFilterComputeTileSizeY = 8;
TAutoConsoleVariable<int32> CVarLoopMode(
TEXT("r.Filter.LoopMode"),
0,
TEXT("Controls when to use either dynamic or unrolled loops to iterates over the Gaussian filtering.\n")
TEXT("This passes is used for Gaussian Blur, Bloom and Depth of Field. The dynamic loop allows\n")
TEXT("up to 128 samples versus the 32 samples of unrolled loops, but add an additional cost for\n")
TEXT("the loop's stop test at every iterations.\n")
TEXT(" 0: Unrolled loop only (default; limited to 32 samples).\n")
TEXT(" 1: Fall back to dynamic loop if needs more than 32 samples.\n")
TEXT(" 2: Dynamic loop only."),
ECVF_Scalability | ECVF_RenderThreadSafe);
TAutoConsoleVariable<float> CVarFilterSizeScale(
TEXT("r.Filter.SizeScale"),
1.0f,
TEXT("Allows to scale down or up the sample count used for bloom and Gaussian depth of field (scale is clamped to give reasonable results).\n")
TEXT("Values down to 0.6 are hard to notice\n")
TEXT(" 1 full quality (default)\n")
TEXT(" >1 more samples (slower)\n")
TEXT(" <1 less samples (faster, artifacts with HDR content or boxy results with GaussianDOF)"),
ECVF_Scalability | ECVF_RenderThreadSafe);
TAutoConsoleVariable<float> CVarFastBlurThreshold(
TEXT("r.FastBlurThreshold"),
7.0f,
TEXT("Defines at what radius the Gaussian blur optimization kicks in (estimated 25% - 40% faster).\n")
TEXT("The optimization uses slightly less memory and has a quality loss on smallblur radius.\n")
TEXT(" 0: use the optimization always (fastest, lowest quality)\n")
TEXT(" 3: use the optimization starting at a 3 pixel radius (quite fast)\n")
TEXT(" 7: use the optimization starting at a 7 pixel radius (default)\n")
TEXT(">15: barely ever use the optimization (high quality)"),
ECVF_Scalability | ECVF_RenderThreadSafe);
BEGIN_SHADER_PARAMETER_STRUCT(FFilterParameters, )
SHADER_PARAMETER_STRUCT(FScreenPassTextureSliceInput, Filter)
SHADER_PARAMETER_STRUCT(FScreenPassTextureInput, Additive)
SHADER_PARAMETER_ARRAY(FVector4f, SampleOffsets, [MAX_PACKED_SAMPLES_OFFSET])
SHADER_PARAMETER_ARRAY(FLinearColor, SampleWeights, [MAX_FILTER_SAMPLES])
SHADER_PARAMETER(int32, SampleCount)
END_SHADER_PARAMETER_STRUCT()
void GetFilterParameters(
FFilterParameters& OutParameters,
const FScreenPassTextureSliceInput& Filter,
const FScreenPassTextureInput& Additive,
TArrayView<const FVector2f> SampleOffsets,
TArrayView<const FLinearColor> SampleWeights)
{
OutParameters.Filter = Filter;
OutParameters.Additive = Additive;
for (int32 SampleIndex = 0; SampleIndex < SampleOffsets.Num(); SampleIndex += 2)
{
OutParameters.SampleOffsets[SampleIndex / 2].X = SampleOffsets[SampleIndex + 0].X;
OutParameters.SampleOffsets[SampleIndex / 2].Y = SampleOffsets[SampleIndex + 0].Y;
if (SampleIndex + 1 < SampleOffsets.Num())
{
OutParameters.SampleOffsets[SampleIndex / 2].Z = SampleOffsets[SampleIndex + 1].X;
OutParameters.SampleOffsets[SampleIndex / 2].W = SampleOffsets[SampleIndex + 1].Y;
}
}
for (int32 SampleIndex = 0; SampleIndex < SampleWeights.Num(); ++SampleIndex)
{
OutParameters.SampleWeights[SampleIndex] = SampleWeights[SampleIndex];
}
OutParameters.SampleCount = SampleOffsets.Num();
}
// Evaluates an unnormalized normal distribution PDF around 0 at given X with Variance.
float NormalDistributionUnscaled(float X, float Sigma, float CrossCenterWeight)
{
const float DX = FMath::Abs(X);
const float ClampedOneMinusDX = FMath::Max(0.0f, 1.0f - DX);
// Tweak the gaussian shape e.g. "r.Bloom.Cross 3.5"
if (CrossCenterWeight > 1.0f)
{
return FMath::Pow(ClampedOneMinusDX, CrossCenterWeight);
}
else
{
// Constant is tweaked give a similar look to UE before we fixed the scale bug (Some content tweaking might be needed).
// The value defines how much of the Gaussian clipped by the sample window.
// r.Filter.SizeScale allows to tweak that for performance/quality.
const float LegacyCompatibilityConstant = -16.7f;
const float Gaussian = FMath::Exp(LegacyCompatibilityConstant * FMath::Square(DX / Sigma));
return FMath::Lerp(Gaussian, ClampedOneMinusDX, CrossCenterWeight);
}
}
uint32 GetSampleCountMax(ERHIFeatureLevel::Type InFeatureLevel, EShaderPlatform InPlatform)
{
if (CVarLoopMode.GetValueOnRenderThread() != 0)
{
return MAX_FILTER_SAMPLES;
}
else if (IsMetalMRTPlatform(InPlatform))
{
return MAX_FILTER_COMPILE_TIME_SAMPLES_IOS;
}
else if (InFeatureLevel < ERHIFeatureLevel::SM5)
{
return MAX_FILTER_COMPILE_TIME_SAMPLES_ES3_1;
}
else
{
return MAX_FILTER_COMPILE_TIME_SAMPLES;
}
}
float GetClampedKernelRadius(uint32 SampleCountMax, float KernelRadius)
{
return FMath::Clamp<float>(KernelRadius, DELTA, SampleCountMax - 1);
}
int GetIntegerKernelRadius(uint32 SampleCountMax, float KernelRadius)
{
// Smallest radius will be 1.
return FMath::Min<int32>(FMath::CeilToInt(GetClampedKernelRadius(SampleCountMax, KernelRadius)), SampleCountMax - 1);
}
uint32 Compute1DGaussianFilterKernel(FVector2f OutOffsetAndWeight[MAX_FILTER_SAMPLES], uint32 SampleCountMax, float KernelRadius, float CrossCenterWeight)
{
const float FilterSizeScale = FMath::Clamp(CVarFilterSizeScale.GetValueOnRenderThread(), 0.1f, 10.0f);
const float ClampedKernelRadius = GetClampedKernelRadius(SampleCountMax, KernelRadius);
const int32 IntegerKernelRadius = GetIntegerKernelRadius(SampleCountMax, KernelRadius * FilterSizeScale);
uint32 SampleCount = 0;
float WeightSum = 0.0f;
for (int32 SampleIndex = -IntegerKernelRadius; SampleIndex <= IntegerKernelRadius; SampleIndex += 2)
{
float Weight0 = NormalDistributionUnscaled(SampleIndex, ClampedKernelRadius, CrossCenterWeight);
float Weight1 = 0.0f;
// We use the bilinear filter optimization for gaussian blur. However, we don't want to bias the
// last sample off the edge of the filter kernel, so the very last tap just is on the pixel center.
if(SampleIndex != IntegerKernelRadius)
{
Weight1 = NormalDistributionUnscaled(SampleIndex + 1, ClampedKernelRadius, CrossCenterWeight);
}
const float TotalWeight = Weight0 + Weight1;
OutOffsetAndWeight[SampleCount].X = SampleIndex + (Weight1 / TotalWeight);
OutOffsetAndWeight[SampleCount].Y = TotalWeight;
WeightSum += TotalWeight;
SampleCount++;
}
// Normalize blur weights.
const float WeightSumInverse = 1.0f / WeightSum;
for (uint32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
{
OutOffsetAndWeight[SampleIndex].Y *= WeightSumInverse;
}
return SampleCount;
}
float GetBlurRadius(uint32 ViewSize, float KernelSizePercent)
{
const float PercentToScale = 0.01f;
const float DiameterToRadius = 0.5f;
return static_cast<float>(ViewSize) * KernelSizePercent * PercentToScale * DiameterToRadius;
}
bool IsFastBlurEnabled(float BlurRadius)
{
const float FastBlurRadiusThreshold = CVarFastBlurThreshold.GetValueOnRenderThread();
return BlurRadius >= FastBlurRadiusThreshold;
}
uint32 GetStaticSampleCount(uint32 RequiredSampleCount)
{
const int32 LoopMode = CVarLoopMode.GetValueOnRenderThread();
const bool bForceStaticLoop = LoopMode == 0;
const bool bForceDynamicLoop = LoopMode == 2;
if (!bForceDynamicLoop && (bForceStaticLoop || RequiredSampleCount <= MAX_FILTER_COMPILE_TIME_SAMPLES))
{
return RequiredSampleCount;
}
else
{
return 0;
}
}
class FFilterShader : public FGlobalShader
{
public:
class FStaticSampleCount : SHADER_PERMUTATION_INT("STATIC_SAMPLE_COUNT", MAX_FILTER_COMPILE_TIME_SAMPLES + 1);
class FCombineAdditive : SHADER_PERMUTATION_BOOL("USE_COMBINE_ADDITIVE");
class FManualUVBorder : SHADER_PERMUTATION_BOOL("USE_MANUAL_UV_BORDER");
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("SAMPLE_COUNT_MAX"), MAX_FILTER_SAMPLES);
}
static bool ShouldCompilePermutation(EShaderPlatform Platform, uint32 StaticSampleCount)
{
if (IsMetalMRTPlatform(Platform))
{
return StaticSampleCount <= MAX_FILTER_COMPILE_TIME_SAMPLES_IOS;
}
else if (IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5))
{
return true;
}
else
{
return StaticSampleCount <= MAX_FILTER_COMPILE_TIME_SAMPLES_ES3_1;
}
}
FFilterShader() = default;
FFilterShader(const CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{}
};
class FFilterPS : public FFilterShader
{
public:
DECLARE_GLOBAL_SHADER(FFilterPS);
SHADER_USE_PARAMETER_STRUCT(FFilterPS, FFilterShader);
using FPermutationDomain = TShaderPermutationDomain<FStaticSampleCount, FCombineAdditive, FManualUVBorder>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FFilterParameters, Filter)
SHADER_PARAMETER(FScreenTransform, SvPositionToTextureUV)
SHADER_PARAMETER(FScreenTransform, ViewportUVToTextureUV)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
const FPermutationDomain PermutationVector(Parameters.PermutationId);
return FFilterShader::ShouldCompilePermutation(Parameters.Platform, PermutationVector.Get<FStaticSampleCount>());
}
};
class FFilterVS : public FFilterShader
{
public:
DECLARE_GLOBAL_SHADER(FFilterVS);
SHADER_USE_PARAMETER_STRUCT(FFilterVS, FFilterShader);
using FPermutationDomain = TShaderPermutationDomain<FStaticSampleCount>;
using FParameters = FFilterPS::FParameters;
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
const FPermutationDomain PermutationVector(Parameters.PermutationId);
const uint32 StaticSampleCount = PermutationVector.Get<FStaticSampleCount>();
if (StaticSampleCount == 0)
{
return false;
}
else
{
return FFilterShader::ShouldCompilePermutation(Parameters.Platform, StaticSampleCount);
}
}
};
IMPLEMENT_GLOBAL_SHADER(FFilterVS, "/Engine/Private/FilterVertexShader.usf", "MainVS", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FFilterPS, "/Engine/Private/FilterPixelShader.usf", "MainPS", SF_Pixel);
class FFilterCS : public FFilterShader
{
public:
DECLARE_GLOBAL_SHADER(FFilterCS);
SHADER_USE_PARAMETER_STRUCT(FFilterCS, FFilterShader);
using FPermutationDomain = TShaderPermutationDomain<FStaticSampleCount, FCombineAdditive, FManualUVBorder>;
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FFilterParameters, Filter)
SHADER_PARAMETER_STRUCT(FScreenPassTextureViewportParameters, Output)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, RWOutputTexture)
END_SHADER_PARAMETER_STRUCT()
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FFilterShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), GFilterComputeTileSizeX);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), GFilterComputeTileSizeY);
}
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}
};
IMPLEMENT_GLOBAL_SHADER(FFilterCS, "/Engine/Private/FilterPixelShader.usf", "MainCS", SF_Compute);
} //! namespace
FScreenPassTexture AddGaussianBlurPass(
FRDGBuilder& GraphBuilder,
const FViewInfo& View,
const TCHAR* Name,
FScreenPassTextureViewport OutputViewport,
FScreenPassTextureSlice Filter,
FScreenPassTexture Additive,
TArrayView<const FVector2f> SampleOffsets,
TArrayView<const FLinearColor> SampleWeights,
bool UseMirrorAddressMode)
{
check(!OutputViewport.IsEmpty());
check(Filter.IsValid());
check(SampleOffsets.Num() == SampleWeights.Num());
check(SampleOffsets.Num() != 0);
const bool bIsComputePass = View.bUseComputePasses;
const bool bCombineAdditive = Additive.IsValid();
const bool bManualUVBorder = Filter.ViewRect.Min != FIntPoint::ZeroValue || Filter.ViewRect.Max != Filter.TextureSRV->Desc.Texture->Desc.Extent;
const uint32 SampleCount = SampleOffsets.Num();
const uint32 StaticSampleCount = GetStaticSampleCount(SampleCount);
FRHISamplerState* SamplerState;
if (UseMirrorAddressMode)
{
SamplerState = TStaticSamplerState<SF_Bilinear, AM_Mirror, AM_Mirror, AM_Clamp>::GetRHI();
}
else
{
SamplerState = TStaticSamplerState<SF_Bilinear, AM_Border, AM_Border, AM_Clamp>::GetRHI();
}
const FScreenPassTextureSliceInput FilterInput = GetScreenPassTextureInput(Filter, SamplerState);
FScreenPassTextureInput AdditiveInput;
if (bCombineAdditive)
{
AdditiveInput = GetScreenPassTextureInput(Additive, SamplerState);
}
const FRDGTextureDesc& InputDesc = Filter.TextureSRV->Desc.Texture->Desc;
FRDGTextureDesc OutputDesc = FRDGTextureDesc::Create2D(
OutputViewport.Extent,
InputDesc.Format,
FClearValueBinding(FLinearColor::Transparent),
/* InFlags = */ TexCreate_ShaderResource | (bIsComputePass ? TexCreate_UAV : (TexCreate_RenderTargetable | TexCreate_NoFastClear)) | (InputDesc.Flags & (TexCreate_FastVRAM | TexCreate_FastVRAMPartialAlloc)));
const FScreenPassRenderTarget Output(GraphBuilder.CreateTexture(OutputDesc, Name), OutputViewport.Rect, ERenderTargetLoadAction::ENoAction);
if (bIsComputePass)
{
FFilterCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FFilterCS::FParameters>();
GetFilterParameters(PassParameters->Filter, FilterInput, AdditiveInput, SampleOffsets, SampleWeights);
PassParameters->Output = GetScreenPassTextureViewportParameters(FScreenPassTextureViewport(Output));
PassParameters->RWOutputTexture = GraphBuilder.CreateUAV(Output.Texture);
FFilterCS::FPermutationDomain PermutationVector;
PermutationVector.Set<FFilterCS::FCombineAdditive>(bCombineAdditive);
PermutationVector.Set<FFilterCS::FStaticSampleCount>(StaticSampleCount);
PermutationVector.Set<FFilterPS::FManualUVBorder>(bManualUVBorder);
TShaderMapRef<FFilterCS> ComputeShader(View.ShaderMap, PermutationVector);
FComputeShaderUtils::AddPass(
GraphBuilder,
RDG_EVENT_NAME("GaussianBlur.%s %dx%d (CS)", Name, OutputViewport.Rect.Width(), OutputViewport.Rect.Height()),
ComputeShader,
PassParameters,
FComputeShaderUtils::GetGroupCount(OutputViewport.Rect.Size(), FIntPoint(GFilterComputeTileSizeX, GFilterComputeTileSizeY)));
}
else
{
FFilterPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FFilterPS::FParameters>();
GetFilterParameters(PassParameters->Filter, FilterInput, AdditiveInput, SampleOffsets, SampleWeights);
PassParameters->ViewportUVToTextureUV = (
FScreenTransform::ChangeTextureBasisFromTo(FScreenPassTextureViewport(Filter), FScreenTransform::ETextureBasis::ViewportUV, FScreenTransform::ETextureBasis::TextureUV));
PassParameters->SvPositionToTextureUV = (
FScreenTransform::ChangeTextureBasisFromTo(FScreenPassTextureViewport(Output), FScreenTransform::ETextureBasis::TexelPosition, FScreenTransform::ETextureBasis::ViewportUV) *
PassParameters->ViewportUVToTextureUV);
PassParameters->RenderTargets[0] = Output.GetRenderTargetBinding();
FFilterPS::FPermutationDomain PixelPermutationVector;
PixelPermutationVector.Set<FFilterPS::FCombineAdditive>(bCombineAdditive);
PixelPermutationVector.Set<FFilterPS::FStaticSampleCount>(StaticSampleCount);
PixelPermutationVector.Set<FFilterPS::FManualUVBorder>(bManualUVBorder);
TShaderMapRef<FFilterPS> PixelShader(View.ShaderMap, PixelPermutationVector);
if (StaticSampleCount != 0)
{
FFilterVS::FPermutationDomain VertexPermutationVector;
VertexPermutationVector.Set<FFilterVS::FStaticSampleCount>(StaticSampleCount);
TShaderMapRef<FFilterVS> VertexShader(View.ShaderMap, VertexPermutationVector);
AddDrawScreenPass(
GraphBuilder,
RDG_EVENT_NAME("GaussianBlur.%s %dx%d (PS, Static)", Name, OutputViewport.Rect.Width(), OutputViewport.Rect.Height()),
View,
OutputViewport,
FScreenPassTextureViewport(Filter),
FScreenPassPipelineState(VertexShader, PixelShader),
PassParameters,
[VertexShader, PixelShader, PassParameters] (FRHICommandList& RHICmdList)
{
SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), *PassParameters);
SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *PassParameters);
});
}
else
{
FPixelShaderUtils::AddFullscreenPass(
GraphBuilder,
View.ShaderMap,
RDG_EVENT_NAME("GaussianBlur.%s %dx%d (PS, Dynamic)", Name, OutputViewport.Rect.Width(), OutputViewport.Rect.Height()),
PixelShader,
PassParameters,
Output.ViewRect);
}
}
return FScreenPassTexture(Output);
}
FScreenPassTexture AddGaussianBlurPass(
FRDGBuilder& GraphBuilder,
const FViewInfo& View,
const FGaussianBlurInputs& Inputs)
{
check(Inputs.Filter.IsValid());
const FScreenPassTextureViewport FilterViewport(Inputs.Filter);
const float BlurRadius = GetBlurRadius(FilterViewport.Rect.Width(), Inputs.KernelSizePercent);
const uint32 SampleCountMax = GetSampleCountMax(View.GetFeatureLevel(), View.GetShaderPlatform());
const FVector2f InverseFilterTextureExtent(
1.0f / static_cast<float>(FilterViewport.Extent.X),
1.0f / static_cast<float>(FilterViewport.Extent.Y));
const bool bFastBlurEnabled = IsFastBlurEnabled(BlurRadius);
// Downscale output width by half when using fast blur optimization.
const FScreenPassTextureViewport HorizontalOutputViewport = bFastBlurEnabled
? GetDownscaledViewport(FilterViewport, FIntPoint(2, 1))
: FilterViewport;
FScreenPassTexture HorizontalOutput;
// Horizontal Pass
{
FVector2f OffsetAndWeight[MAX_FILTER_SAMPLES];
FLinearColor SampleWeights[MAX_FILTER_SAMPLES];
FVector2f SampleOffsets[MAX_FILTER_SAMPLES];
const uint32 SampleCount = Compute1DGaussianFilterKernel(OffsetAndWeight, SampleCountMax, BlurRadius, Inputs.CrossCenterWeight.X);
// Weights multiplied by a white tint.
for (uint32 i = 0; i < SampleCount; ++i)
{
const float Weight = OffsetAndWeight[i].Y;
SampleWeights[i] = FLinearColor(Weight, Weight, Weight, Weight);
}
for (uint32 i = 0; i < SampleCount; ++i)
{
const float Offset = OffsetAndWeight[i].X;
SampleOffsets[i] = FVector2f(InverseFilterTextureExtent.X * Offset, 0.0f);
}
// Horizontal pass doesn't use additive combine.
FScreenPassTexture Additive;
HorizontalOutput = AddGaussianBlurPass(
GraphBuilder,
View,
Inputs.NameX,
HorizontalOutputViewport,
Inputs.Filter,
Additive,
TArrayView<const FVector2f>(SampleOffsets, SampleCount),
TArrayView<const FLinearColor>(SampleWeights, SampleCount),
Inputs.UseMirrorAddressMode);
}
// Vertical Pass
{
FVector2f OffsetAndWeight[MAX_FILTER_SAMPLES];
FLinearColor SampleWeights[MAX_FILTER_SAMPLES];
FVector2f SampleOffsets[MAX_FILTER_SAMPLES];
const uint32 SampleCount = Compute1DGaussianFilterKernel(OffsetAndWeight, SampleCountMax, BlurRadius, Inputs.CrossCenterWeight.Y);
// Weights multiplied by a input tint color.
for (uint32 i = 0; i < SampleCount; ++i)
{
const float Weight = OffsetAndWeight[i].Y;
SampleWeights[i] = Inputs.TintColor * Weight;
}
for (uint32 i = 0; i < SampleCount; ++i)
{
const float Offset = OffsetAndWeight[i].X;
SampleOffsets[i] = FVector2f(0, InverseFilterTextureExtent.Y * Offset);
}
return AddGaussianBlurPass(
GraphBuilder,
View,
Inputs.NameY,
FilterViewport,
FScreenPassTextureSlice::CreateFromScreenPassTexture(GraphBuilder, HorizontalOutput),
Inputs.Additive,
TArrayView<const FVector2f>(SampleOffsets, SampleCount),
TArrayView<const FLinearColor>(SampleWeights, SampleCount),
Inputs.UseMirrorAddressMode);
}
}