欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深度解析虚幻渲染体系(第07章)- 后处理的解析

最编程 2024-08-14 10:49:00
...


7.1.2 后处理重要性

可能有些同学会疑惑,后处理不就是对渲染完的图片进行处理吗?跟图形渲染的紧密性有那么大么?可以忽略学习后处理么?

为了解答以上疑问,也体现后处理的重要性,以及其在UE或图形渲染的关联和地位,特地开辟了此小节。

以UE的默认场景为例,它的画面如下:

剖析虚幻渲染体系(07)- 后处理_UE4

现在用以下命令行关闭所有后处理:

ShowFlag.PostProcessing 0


结果画面变成了下面这般模样:

剖析虚幻渲染体系(07)- 后处理_Unreal_02

用RenderDoc截帧,发现上面的画面其实还应用了后处理阶段的Gamma校正,好家伙,把它也关了,由此得到彻底没有后处理的画面:

剖析虚幻渲染体系(07)- 后处理_UE4_03

对比第一幅图,看到差别了么?是不是颜色的亮度、对比度、色彩、还有锯齿都不一样?

这也印证了,即便你没有对后处理做任何设置或更改,UE依然在默认情况下执行了很多后处理,才使得渲染画面最终正常地呈现在屏幕前。

由此可知,后处理之于渲染、之于UE,有着何等重要的位置。有了后处理,我们便可如虎添翼,画龙点睛,让画面才更加可信、生动、有趣。

实际上,后处理的应用远不止于此,结合深度、法线等屏幕空间的信息之后,将拥有更广阔更丰富的魔法世界。

 

7.2 后处理基础

本章将阐述后处理的一些基础知识点和概念及UE的操作使用。

7.2.1 后处理简介

艺术家和设计师使用虚幻引擎提供的后期处理效果,可以调整场景的整体外观和感觉。

默认情况下,UE会开启抗锯齿、自动曝光、Bloom、色调映射和Gamma校正等后处理:

剖析虚幻渲染体系(07)- 后处理_UE4_04

当然,可以通过场景视图的Show/Post Processing菜单下的选项动态开启关闭后处理,以便观察指定后处理的对场景产生的效果和变化。

剖析虚幻渲染体系(07)- 后处理_UE4_05

也可以通过之前提及的控制台命令开启或关闭后处理。

7.2.2 后处理体积

对于艺术家,更通用且方便的方法是往场景拖曳后处理体积(Post Processing Volume),以便精确地控制后处理效果和参数。

剖析虚幻渲染体系(07)- 后处理_Unreal_06

后处理体积涉及的类型和参数非常多,下面是后处理体积的属性分类:

剖析虚幻渲染体系(07)- 后处理_Unreal_07

其中Lens是镜头相关的后处理效果,包含Bloom、Exposure、Flares、DOF等效果;Color Grading是颜色分级,包含白平衡、全局、阴影、中调、高调等效果;Film就是电影色调映射,可以调整斜度、低调、黑色、肩部、白色等曲线参数;Rendering Feature包含了渲染管线相关的效果,包含后处理材质、环境立方图、AO、光追相关特性、GI、运动模糊、LPV、反射、SSR、透明、路径追踪以及屏幕百分比;最后是Post Processing Volume Setting,可以指定优先级、混合权重、设定是否影响无限范围(上图)。

同一个场景可以同时存在多个后处理体积,但为了性能和可维护性,应该保持一个场景只有一个全局后处理(Infinite Extent),其余的设置为局部范围。

7.2.3 后处理材质

虽然后处理体积提供了很多内置的后处理效果,但渲染的效果千变万化,它们肯定无法全部满足应用的实际需求。UE的后处理材质(Post Processing Material)便满足自定义要求,可以利用材质编辑器实现自定义的后处理效果。

添加后处理材质也不复杂,新建材质,将材质域(Material Domain)设置为Post Process,此时材质引脚只有Emissive Color被点亮:

剖析虚幻渲染体系(07)- 后处理_计算机图形学_08

并且Post Process Material属性栏处于可编辑状态:

剖析虚幻渲染体系(07)- 后处理_Unreal Engine_09

这些参数的含义说明如下:

  • Blendable Location:材质混合位置,可选的有After Tonemapping(色调映射之后)、Before Tonemapping(色调映射之前)、Before Translucency(透明之前)、Replacing Tonemapping(替换色调映射)、SSR Input(屏幕空间反射输入),默认是After Tonemapping(色调映射之后)。
  • Output Alpha:是否输出Alpha,如果是,则需要正确处理和输出Emissive Color的Alpha通道。默认不开启。
  • Blendable Priority:混合优先级,数值越高,将越优先被渲染。默认是0。
  • Is Blendable:是否可混合,如果不可混合,将不能和其它后处理或场景颜色混合过渡。默认是可以。
  • Enable Stencil Test:是否开启模板测试,如果开启,可以设置比较方式和参考值。默认不开启。

编辑好后处理材质之后,为了将它应用到场景上,可以在后处理体积的Rendering Feature属性栏的Post Process Material列表中设置:

剖析虚幻渲染体系(07)- 后处理_Unreal Engine_10

还可以调整每个材质的混合权重和顺序(拖曳权重左边的点阵)。

值得注意的是,在后处理材质中,SceneTexture材质节点的SceneColor无法访问,否则会报错:

剖析虚幻渲染体系(07)- 后处理_Unreal_11

后处理材质中无法访问SceneColor,提示SceneColor只能在Surface材质域中使用。

解决这个问题就是选中SceneTexture节点,在属性栏的Scene Texture Id选择PostProcessInput0:

剖析虚幻渲染体系(07)- 后处理_计算机图形学_12

除了PostProcessInput0,还有其它很多屏幕空间的数据(GBuffer)可以被后处理材质读取:

剖析虚幻渲染体系(07)- 后处理_UE4_13

但是,在多数后处理通道中,PostProcessInput1~PostProcessInput6是空纹理。

 

7.3 后处理流程

本章将进入UE的后处理代码进行分析。

7.3.1 AddPostProcessingPasses

后处理的主入口是​​AddPostProcessingPasses​​,位于的​​FDeferred​​末尾:

void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
(......)

RenderTranslucency(RHICmdList, ...);

(......)

// 后处理阶段。
if (ViewFamily.bResolveScene)
{
GRenderTargetPool.AddPhaseEvent(TEXT("PostProcessing"));

(......)

// 后处理的输入参数.
FPostProcessingInputs PostProcessingInputs;
PostProcessingInputs.ViewFamilyTexture = ViewFamilyTexture;
PostProcessingInputs.SeparateTranslucencyTextures = &SeparateTranslucencyTextures;
PostProcessingInputs.SceneTextures = SceneTextures;

(......)

{
// 遍历所有view, 每个view增加后处理Pass.
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
FViewInfo& View = Views[ViewIndex];
// 增加后处理通道.
AddPostProcessingPasses(GraphBuilder, View, PostProcessingInputs);
}
}

// 将场景上下文的场景颜色纹理置空.
AddPass(GraphBuilder, [this, &SceneContext](FRHICommandListImmediate&)
{
SceneContext.SetSceneColor(nullptr);
});
}

(......)
}


​AddPostProcessingPasses​​是处理UE内置后处理序列的,涉及的代码量大,不过下面先分析其主要流程:

// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

void AddPostProcessingPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FPostProcessingInputs& Inputs)
{
Inputs.Validate();

// 获取纹理和视图数据.
const FIntRect PrimaryViewRect = View.ViewRect;
const FSceneTextureParameters SceneTextureParameters = GetSceneTextureParameters(GraphBuilder, Inputs.SceneTextures);
const FScreenPassRenderTarget ViewFamilyOutput = FScreenPassRenderTarget::CreateViewFamilyOutput(Inputs.ViewFamilyTexture, View);
const FScreenPassTexture SceneDepth(SceneTextureParameters.SceneDepthTexture, PrimaryViewRect);
const FScreenPassTexture SeparateTranslucency(Inputs.SeparateTranslucencyTextures->GetColorForRead(GraphBuilder), PrimaryViewRect);
const FScreenPassTexture CustomDepth((*Inputs.SceneTextures)->CustomDepthTexture, PrimaryViewRect);
const FScreenPassTexture Velocity(SceneTextureParameters.GBufferVelocityTexture, PrimaryViewRect);
const FScreenPassTexture BlackDummy(GSystemTextures.GetBlackDummy(GraphBuilder));

// 场景颜色.
FScreenPassTexture SceneColor((*Inputs.SceneTextures)->SceneColorTexture, PrimaryViewRect);
FScreenPassTexture SceneColorBeforeTonemap;
FScreenPassTexture SceneColorAfterTonemap;
const FScreenPassTexture OriginalSceneColor = SceneColor;

// 初始化纹理.
const FEyeAdaptationParameters EyeAdaptationParameters = GetEyeAdaptationParameters(View, ERHIFeatureLevel::SM5);
FRDGTextureRef LastEyeAdaptationTexture = GetEyeAdaptationTexture(GraphBuilder, View);
FRDGTextureRef EyeAdaptationTexture = LastEyeAdaptationTexture;
FRDGTextureRef HistogramTexture = BlackDummy.Texture;

// 处理后处理开启标记.
const FEngineShowFlags& EngineShowFlags = View.Family->EngineShowFlags;
const bool bVisualizeHDR = EngineShowFlags.VisualizeHDR;
const bool bViewFamilyOutputInHDR = GRHISupportsHDROutput && IsHDREnabled();
const bool bVisualizeGBufferOverview = IsVisualizeGBufferOverviewEnabled(View);
const bool bVisualizeGBufferDumpToFile = IsVisualizeGBufferDumpToFileEnabled(View);
const bool bVisualizeGBufferDumpToPIpe = IsVisualizeGBufferDumpToPipeEnabled(View);
const bool bOutputInHDR = IsPostProcessingOutputInHDR();

const FPaniniProjectionConfig PaniniConfig(View);

// 后处理特定Pass.
enum class EPass : uint32
{
MotionBlur, // 运动模糊
Tonemap, // 色调映射
FXAA, // FXAA抗锯齿
PostProcessMaterialAfterTonemapping, // 色调映射之后的后处理
VisualizeDepthOfField,
VisualizeStationaryLightOverlap,
VisualizeLightCulling,
SelectionOutline,
EditorPrimitive,
VisualizeShadingModels,
VisualizeGBufferHints,
VisualizeSubsurface,
VisualizeGBufferOverview,
VisualizeHDR,
PixelInspector,
HMDDistortion,
HighResolutionScreenshotMask,
PrimaryUpscale, // 主放大
SecondaryUpscale, // 次放大
MAX
};

(......)

// 后处理特定Pass对应的名字.
const TCHAR* PassNames[] =
{
TEXT("MotionBlur"),
TEXT("Tonemap"),
TEXT("FXAA"),
TEXT("PostProcessMaterial (AfterTonemapping)"),
TEXT("VisualizeDepthOfField"),
TEXT("VisualizeStationaryLightOverlap"),
TEXT("VisualizeLightCulling"),
TEXT("SelectionOutline"),
TEXT("EditorPrimitive"),
TEXT("VisualizeShadingModels"),
TEXT("VisualizeGBufferHints"),
TEXT("VisualizeSubsurface"),
TEXT("VisualizeGBufferOverview"),
TEXT("VisualizeHDR"),
TEXT("PixelInspector"),
TEXT("HMDDistortion"),
TEXT("HighResolutionScreenshotMask"),
TEXT("PrimaryUpscale"),
TEXT("SecondaryUpscale")
};

static_assert(static_cast<uint32>(EPass::MAX) == UE_ARRAY_COUNT(PassNames), "EPass does not match PassNames.");

// 声明后处理序列PassSequence实例.
TOverridePassSequence<EPass> PassSequence(ViewFamilyOutput);
PassSequence.SetNames(PassNames, UE_ARRAY_COUNT(PassNames));

// 开启或关闭指定Pass.
PassSequence.SetEnabled(EPass::VisualizeStationaryLightOverlap, EngineShowFlags.StationaryLightOverlap);
PassSequence.SetEnabled(EPass::VisualizeLightCulling, EngineShowFlags.VisualizeLightCulling);
PassSequence.SetEnabled(EPass::SelectionOutline, false);
PassSequence.SetEnabled(EPass::EditorPrimitive, false);
PassSequence.SetEnabled(EPass::VisualizeShadingModels, EngineShowFlags.VisualizeShadingModels);
PassSequence.SetEnabled(EPass::VisualizeGBufferHints, EngineShowFlags.GBufferHints);
PassSequence.SetEnabled(EPass::VisualizeSubsurface, EngineShowFlags.VisualizeSSS);
PassSequence.SetEnabled(EPass::VisualizeGBufferOverview, bVisualizeGBufferOverview || bVisualizeGBufferDumpToFile || bVisualizeGBufferDumpToPIpe);
PassSequence.SetEnabled(EPass::VisualizeHDR, EngineShowFlags.VisualizeHDR);
PassSequence.SetEnabled(EPass::PixelInspector, false);
PassSequence.SetEnabled(EPass::HMDDistortion, EngineShowFlags.StereoRendering && EngineShowFlags.HMDDistortion);
PassSequence.SetEnabled(EPass::HighResolutionScreenshotMask, IsHighResolutionScreenshotMaskEnabled(View));
PassSequence.SetEnabled(EPass::PrimaryUpscale, PaniniConfig.IsEnabled() || (View.PrimaryScreenPercentageMethod == EPrimaryScreenPercentageMethod::SpatialUpscale && PrimaryViewRect.Size() != View.GetSecondaryViewRectSize()));
PassSequence.SetEnabled(EPass::SecondaryUpscale, View.RequiresSecondaryUpscale());

(......)

if (IsPostProcessingEnabled(View)) // 视图启用后处理
{
const EStereoscopicPass StereoPass = View.StereoPass;

// 处理数据和标记.
const bool bPrimaryView = IStereoRendering::IsAPrimaryView(View);
const bool bHasViewState = View.ViewState != nullptr;
const bool bDepthOfFieldEnabled = DiaphragmDOF::IsEnabled(View);
const bool bVisualizeDepthOfField = bDepthOfFieldEnabled && EngineShowFlags.VisualizeDOF;
const bool bVisualizeMotionBlur = IsVisualizeMotionBlurEnabled(View);
const EAutoExposureMethod AutoExposureMethod = GetAutoExposureMethod(View);
const EAntiAliasingMethod AntiAliasingMethod = !bVisualizeDepthOfField ? View.AntiAliasingMethod : AAM_None;
const EDownsampleQuality DownsampleQuality = GetDownsampleQuality();
const EPixelFormat DownsampleOverrideFormat = PF_FloatRGB;
const bool bMotionBlurEnabled = !bVisualizeMotionBlur && IsMotionBlurEnabled(View);
const bool bTonemapEnabled = !bVisualizeMotionBlur;
const bool bTonemapOutputInHDR = View.Family->SceneCaptureSource == SCS_FinalColorHDR || View.Family->SceneCaptureSource == SCS_FinalToneCurveHDR || bOutputInHDR || bViewFamilyOutputInHDR;
const bool bEyeAdaptationEnabled = bHasViewState && bPrimaryView;
const bool bHistogramEnabled = bVisualizeHDR || (bEyeAdaptationEnabled && AutoExposureMethod == EAutoExposureMethod::AEM_Histogram && View.FinalPostProcessSettings.AutoExposureMinBrightness < View.FinalPostProcessSettings.AutoExposureMaxBrightness);
const bool bBloomEnabled = View.FinalPostProcessSettings.BloomIntensity > 0.0f;

// 色调映射之后的后处理材质.
const FPostProcessMaterialChain PostProcessMaterialAfterTonemappingChain = GetPostProcessMaterialChain(View, BL_AfterTonemapping);

PassSequence.SetEnabled(EPass::MotionBlur, bVisualizeMotionBlur || bMotionBlurEnabled);
PassSequence.SetEnabled(EPass::Tonemap, bTonemapEnabled);
PassSequence.SetEnabled(EPass::FXAA, AntiAliasingMethod == AAM_FXAA);
PassSequence.SetEnabled(EPass::PostProcessMaterialAfterTonemapping, PostProcessMaterialAfterTonemappingChain.Num() != 0);
PassSequence.SetEnabled(EPass::VisualizeDepthOfField, bVisualizeDepthOfField);

// 插件后处理回调.
for (int32 ViewExt = 0; ViewExt < View.Family->ViewExtensions.Num(); ++ViewExt)
{
for (int32 SceneViewPassId = 0; SceneViewPassId != static_cast<int>(ISceneViewExtension::EPostProcessingPass::MAX); SceneViewPassId++)
{
ISceneViewExtension::EPostProcessingPass SceneViewPass = static_cast<ISceneViewExtension::EPostProcessingPass>(SceneViewPassId);
EPass PostProcessingPass = TranslatePass(SceneViewPass);

View.Family->ViewExtensions[ViewExt]->SubscribeToPostProcessingPass(
SceneViewPass,
PassSequence.GetAfterPassCallbacks(PostProcessingPass),
PassSequence.IsEnabled(PostProcessingPass));
}
}

// 后处理序列开启或关闭处理完毕.
PassSequence.Finalize();

// 后处理材质链 - 透明混合之前(Before Translucency)
{
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTranslucency);

if (MaterialChain.Num())
{
SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
}
}

// 光圈DOF
{
FRDGTextureRef LocalSceneColorTexture = SceneColor.Texture;

if (bDepthOfFieldEnabled)
{
LocalSceneColorTexture = DiaphragmDOF::AddPasses(GraphBuilder, SceneTextureParameters, View, SceneColor.Texture, *Inputs.SeparateTranslucencyTextures);
}

if (LocalSceneColorTexture == SceneColor.Texture)
{
LocalSceneColorTexture = AddSeparateTranslucencyCompositionPass(GraphBuilder, View, SceneColor.Texture, *Inputs.SeparateTranslucencyTextures);
}

SceneColor.Texture = LocalSceneColorTexture;
}

// 后处理材质链 - 色调映射之前(Before Tonemapping)
{
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTonemapping);

if (MaterialChain.Num())
{
SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
}
}

FScreenPassTexture HalfResolutionSceneColor;

// 主视图区域.
FIntRect SecondaryViewRect = PrimaryViewRect;

// 时间抗锯齿TAA.
if (AntiAliasingMethod == AAM_TemporalAA)
{
// 是否允许场景颜色下采样.
const bool bAllowSceneDownsample =
IsTemporalAASceneDownsampleAllowed(View) &&
// We can only merge if the normal downsample pass would happen immediately after.
!bMotionBlurEnabled && !bVisualizeMotionBlur &&
// TemporalAA is only able to match the low quality mode (box filter).
GetDownsampleQuality() == EDownsampleQuality::Low;

int32 UpscaleMode = ITemporalUpscaler::GetTemporalUpscalerMode();

const ITemporalUpscaler* DefaultTemporalUpscaler = ITemporalUpscaler::GetDefaultTemporalUpscaler();
const ITemporalUpscaler* UpscalerToUse = ( UpscaleMode == 0 || !View.Family->GetTemporalUpscalerInterface())? DefaultTemporalUpscaler : View.Family->GetTemporalUpscalerInterface();

const TCHAR* UpscalerName = UpscalerToUse->GetDebugName();

(......)

ITemporalUpscaler::FPassInputs UpscalerPassInputs;

UpscalerPassInputs.bAllowDownsampleSceneColor = bAllowSceneDownsample;
UpscalerPassInputs.DownsampleOverrideFormat = DownsampleOverrideFormat;
UpscalerPassInputs.SceneColorTexture = SceneColor.Texture;
UpscalerPassInputs.SceneDepthTexture = SceneDepth.Texture;
UpscalerPassInputs.SceneVelocityTexture = Velocity.Texture;
UpscalerPassInputs.EyeAdaptationTexture = GetEyeAdaptationTexture(GraphBuilder, View);

// 增加TAA Pass.
UpscalerToUse->AddPasses(
GraphBuilder,
View,
UpscalerPassInputs,
&SceneColor.Texture,
&SecondaryViewRect,
&HalfResolutionSceneColor.Texture,
&HalfResolutionSceneColor.ViewRect);
}
// 屏幕空间反射(SSR).
else if (ShouldRenderScreenSpaceReflections(View))
{
if (!View.bStatePrevViewInfoIsReadOnly)
{
check(View.ViewState);
FTemporalAAHistory& OutputHistory = View.ViewState->PrevFrameViewInfo.TemporalAAHistory;
GraphBuilder.QueueTextureExtraction(SceneColor.Texture, &OutputHistory.RT[0]);

FTAAPassParameters TAAInputs(View);
TAAInputs.SceneColorInput = SceneColor.Texture;
TAAInputs.SetupViewRect(View);
OutputHistory.ViewportRect = TAAInputs.OutputViewRect;
OutputHistory.ReferenceBufferSize = TAAInputs.GetOutputExtent() * TAAInputs.ResolutionDivisor;
}
}

// 场景颜色视图区域编程次视图的.
SceneColor.ViewRect = SecondaryViewRect;

// 后处理材质链 - 屏幕空间反射输入(SSR Input)
if (View.ViewState && !View.bStatePrevViewInfoIsReadOnly)
{
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_SSRInput);

if (MaterialChain.Num())
{
// 保存SSR的后处理输出给下一帧使用.
FScreenPassTexture PassOutput = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
GraphBuilder.QueueTextureExtraction(PassOutput.Texture, &View.ViewState->PrevFrameViewInfo.CustomSSRInput);
}
}

// 运动模糊.
if (PassSequence.IsEnabled(EPass::MotionBlur))
{
FMotionBlurInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::MotionBlur, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.SceneDepth = SceneDepth;
PassInputs.SceneVelocity = Velocity;
PassInputs.Quality = GetMotionBlurQuality();
PassInputs.Filter = GetMotionBlurFilter();

if (bVisualizeMotionBlur)
{
SceneColor = AddVisualizeMotionBlurPass(GraphBuilder, View, PassInputs);
}
else
{
SceneColor = AddMotionBlurPass(GraphBuilder, View, PassInputs);
}
}

SceneColor = AddAfterPass(EPass::MotionBlur, SceneColor);

// 如果TAA没有下采样场景颜色, 这里将采用半尺寸分辨率执行之.
if (!HalfResolutionSceneColor.Texture)
{
FDownsamplePassInputs PassInputs;
PassInputs.Name = TEXT("HalfResolutionSceneColor");
PassInputs.SceneColor = SceneColor;
PassInputs.Quality = DownsampleQuality;
PassInputs.FormatOverride = DownsampleOverrideFormat;

HalfResolutionSceneColor = AddDownsamplePass(GraphBuilder, View, PassInputs);
}

// 保存半尺寸分辨率的场景颜色到历史中.
extern int32 GSSRHalfResSceneColor;
if (ShouldRenderScreenSpaceReflections(View) && !View.bStatePrevViewInfoIsReadOnly && GSSRHalfResSceneColor)
{
check(View.ViewState);
GraphBuilder.QueueTextureExtraction(HalfResolutionSceneColor.Texture, &View.ViewState->PrevFrameViewInfo.HalfResTemporalAAHistory);
}

FSceneDownsampleChain SceneDownsampleChain;

// 直方图.
if (bHistogramEnabled)
{
HistogramTexture = AddHistogramPass(GraphBuilder, View, EyeAdaptationParameters, HalfResolutionSceneColor, LastEyeAdaptationTexture);
}

// 人眼适应(自动曝光).
if (bEyeAdaptationEnabled)
{
const bool bBasicEyeAdaptationEnabled = bEyeAdaptationEnabled && (AutoExposureMethod == EAutoExposureMethod::AEM_Basic);

if (bBasicEyeAdaptationEnabled)
{
const bool bLogLumaInAlpha = true;
SceneDownsampleChain.Init(GraphBuilder, View, EyeAdaptationParameters, HalfResolutionSceneColor, DownsampleQuality, bLogLumaInAlpha);

// Use the alpha channel in the last downsample (smallest) to compute eye adaptations values.
EyeAdaptationTexture = AddBasicEyeAdaptationPass(GraphBuilder, View, EyeAdaptationParameters, SceneDownsampleChain.GetLastTexture(), LastEyeAdaptationTexture);
}
// Add histogram eye adaptation pass even if no histogram exists to support the manual clamping mode.
else
{
EyeAdaptationTexture = AddHistogramEyeAdaptationPass(GraphBuilder, View, EyeAdaptationParameters, HistogramTexture);
}
}

FScreenPassTexture Bloom;

// 泛光.
if (bBloomEnabled)
{
FSceneDownsampleChain BloomDownsampleChain;

FBloomInputs PassInputs;
PassInputs.SceneColor = SceneColor;

const bool bBloomThresholdEnabled = View.FinalPostProcessSettings.BloomThreshold > -1.0f;

// Reuse the main scene downsample chain if a threshold isn't required for bloom.
if (SceneDownsampleChain.IsInitialized() && !bBloomThresholdEnabled)
{
PassInputs.SceneDownsampleChain = &SceneDownsampleChain;
}
else
{
FScreenPassTexture DownsampleInput = HalfResolutionSceneColor;

if (bBloomThresholdEnabled)
{
const float BloomThreshold = View.FinalPostProcessSettings.BloomThreshold;

FBloomSetupInputs SetupPassInputs;
SetupPassInputs.SceneColor = DownsampleInput;
SetupPassInputs.EyeAdaptationTexture = EyeAdaptationTexture;
SetupPassInputs.Threshold = BloomThreshold;

DownsampleInput = AddBloomSetupPass(GraphBuilder, View, SetupPassInputs);
}

const bool bLogLumaInAlpha = false;
BloomDownsampleChain.Init(GraphBuilder, View, EyeAdaptationParameters, DownsampleInput, DownsampleQuality, bLogLumaInAlpha);

PassInputs.SceneDownsampleChain = &BloomDownsampleChain;
}

FBloomOutputs PassOutputs = AddBloomPass(GraphBuilder, View, PassInputs);
SceneColor = PassOutputs.SceneColor;
Bloom = PassOutputs.Bloom;

FScreenPassTexture LensFlares = AddLensFlaresPass(GraphBuilder, View, Bloom, *PassInputs.SceneDownsampleChain);

if (LensFlares.IsValid())
{
Bloom = LensFlares;
}
}

if (!Bloom.IsValid())
{
Bloom = BlackDummy;
}

SceneColorBeforeTonemap = SceneColor;

// 色调映射.
if (PassSequence.IsEnabled(EPass::Tonemap))
{
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_ReplacingTonemapper);

if (MaterialChain.Num())
{
const UMaterialInterface* HighestPriorityMaterial = MaterialChain[0];

FPostProcessMaterialInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
PassInputs.SetInput(EPostProcessMaterialInput::SceneColor, SceneColor);
PassInputs.SetInput(EPostProcessMaterialInput::SeparateTranslucency, SeparateTranslucency);
PassInputs.SetInput(EPostProcessMaterialInput::CombinedBloom, Bloom);
PassInputs.SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);
PassInputs.CustomDepthTexture = CustomDepth.Texture;

SceneColor = AddPostProcessMaterialPass(GraphBuilder, View, PassInputs, HighestPriorityMaterial);
}
else
{
FRDGTextureRef ColorGradingTexture = nullptr;

if (bPrimaryView)
{
ColorGradingTexture = AddCombineLUTPass(GraphBuilder, View);
}
// We can re-use the color grading texture from the primary view.
else if (View.GetTonemappingLUT())
{
ColorGradingTexture = TryRegisterExternalTexture(GraphBuilder, View.GetTonemappingLUT());
}
else
{
const FViewInfo* PrimaryView = static_cast<const FViewInfo*>(View.Family->Views[0]);
ColorGradingTexture = TryRegisterExternalTexture(GraphBuilder, PrimaryView->GetTonemappingLUT());
}

FTonemapInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.Bloom = Bloom;
PassInputs.EyeAdaptationTexture = EyeAdaptationTexture;
PassInputs.ColorGradingTexture = ColorGradingTexture;
PassInputs.bWriteAlphaChannel = AntiAliasingMethod == AAM_FXAA || IsPostProcessingWithAlphaChannelSupported();
PassInputs.bOutputInHDR = bTonemapOutputInHDR;

SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs);
}
}

SceneColor = AddAfterPass(EPass::Tonemap, SceneColor);

SceneColorAfterTonemap = SceneColor;

// FXAA抗锯齿.
if (PassSequence.IsEnabled(EPass::FXAA))
{
FFXAAInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::FXAA, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.Quality = GetFXAAQuality();

SceneColor = AddFXAAPass(GraphBuilder, View, PassInputs);
}

SceneColor = AddAfterPass(EPass::FXAA, SceneColor);

// 后处理材质链 - 色调映射之后(After Tonemapping)
if (PassSequence.IsEnabled(EPass::PostProcessMaterialAfterTonemapping))
{
FPostProcessMaterialInputs PassInputs = GetPostProcessMaterialInputs(SceneColor);
PassSequence.AcceptOverrideIfLastPass(EPass::PostProcessMaterialAfterTonemapping, PassInputs.OverrideOutput);
PassInputs.SetInput(EPostProcessMaterialInput::PreTonemapHDRColor, SceneColorBeforeTonemap);
PassInputs.SetInput(EPostProcessMaterialInput::PostTonemapHDRColor, SceneColorAfterTonemap);
PassInputs.SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);

SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, PassInputs, PostProcessMaterialAfterTonemappingChain);
}

(......)

SceneColor = AddAfterPass(EPass::VisualizeDepthOfField, SceneColor);
}
else // 视图不启用后处理, 则最小化后处理序列: 只混合透明纹理和Gamma校正.
{
PassSequence.SetEnabled(EPass::MotionBlur, false);
PassSequence.SetEnabled(EPass::Tonemap, true);
PassSequence.SetEnabled(EPass::FXAA, false);
PassSequence.SetEnabled(EPass::PostProcessMaterialAfterTonemapping, false);
PassSequence.SetEnabled(EPass::VisualizeDepthOfField, false);
PassSequence.Finalize();

SceneColor.Texture = AddSeparateTranslucencyCompositionPass(GraphBuilder, View, SceneColor.Texture, *Inputs.SeparateTranslucencyTextures);

SceneColorBeforeTonemap = SceneColor;

if (PassSequence.IsEnabled(EPass::Tonemap))
{
FTonemapInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.EyeAdaptationTexture = EyeAdaptationTexture;
PassInputs.bOutputInHDR = bViewFamilyOutputInHDR;
PassInputs.bGammaOnly = true;

SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs);
}

SceneColor = AddAfterPass(EPass::Tonemap, SceneColor);

SceneColorAfterTonemap = SceneColor;
}

// 可视化后处理Pass.
if (PassSequence.IsEnabled(EPass::VisualizeStationaryLightOverlap))
{
(......)

SceneColor = AddVisualizeComplexityPass(GraphBuilder, View, PassInputs);
}

(......) // 忽略编辑器或可视化代码.

// 主放大Pass
if (PassSequence.IsEnabled(EPass::PrimaryUpscale))
{
FUpscaleInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::PrimaryUpscale, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.Method = GetUpscaleMethod();
PassInputs.Stage = PassSequence.IsEnabled(EPass::SecondaryUpscale) ? EUpscaleStage::PrimaryToSecondary : EUpscaleStage::PrimaryToOutput;

// 帕尼尼投影(Panini projection)由主放大通道处理。
PassInputs.PaniniConfig = PaniniConfig;

SceneColor = AddUpscalePass(GraphBuilder, View, PassInputs);
}

// 次放大Pass
if (PassSequence.IsEnabled(EPass::SecondaryUpscale))
{
FUpscaleInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::SecondaryUpscale, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.Method = View.Family->SecondaryScreenPercentageMethod == ESecondaryScreenPercentageMethod::LowerPixelDensitySimulation ? EUpscaleMethod::SmoothStep : EUpscaleMethod::Nearest;
PassInputs.Stage = EUpscaleStage::SecondaryToOutput;

SceneColor = AddUpscalePass(GraphBuilder, View, PassInputs);
}
}


后处理材质会先声明一个TOverridePassSequence实例,然后按需开启或关闭它们,之后会根据视图是否启用后处理进入两个分支:如果启用,则按序列先后处理每个后处理效果;如果不启用,则只保留最小化的后处理序列,仅包含透明纹理混合和Gamma校正。

至于判断视图是否开启后处理,由以下接口实现:

bool IsPostProcessingEnabled(const FViewInfo& View)
{
if (View.GetFeatureLevel() >= ERHIFeatureLevel::SM5) // 高于SM5的设备
{
return
// 视图家族开启后处理
View.Family->EngineShowFlags.PostProcessing &&
// 并且不是可视化调试模式.
!View.Family->EngineShowFlags.VisualizeDistanceFieldAO &&
!View.Family->EngineShowFlags.VisualizeShadingModels &&
!View.Family->EngineShowFlags.VisualizeMeshDistanceFields &&
!View.Family->EngineShowFlags.VisualizeGlobalDistanceField &&
!View.Family->EngineShowFlags.ShaderComplexity;
}
// < SM5的设备
else
{
// 视图家族开启后处理且不开启着色复杂度调试模式且是移动端HDR管线.
return View.Family->EngineShowFlags.PostProcessing && !View.Family->EngineShowFlags.ShaderComplexity && IsMobileHDR();
}
}


这些特定后处理Pass都有统一的形式:输入纹理、输入参数、输出纹理,输入纹理必然包含SceneColor,输出纹理通常也是SceneColor,并且上一个后处理Pass的SceneColor输出作为下一个后处理Pass的SceneColor输入,以实现不同后处理效果的叠加。

另外,需要注意的是,有一些后处理(屏幕空间)的效果并没有在PassSequence中体现,而是安插在后处理渲染管线的特定位置中。

7.3.2 TOverridePassSequence

由于后处理效果是叠加状态,意味着它们的混合(处理)顺序相关,如果处理顺序不当,将得到非预想的结果。

TOverridePassSequence就是给定一个枚举类型,按照特殊规则管理和有序地执行所有的Pass。它的定义如下:

// Engine\Source\Runtime\Renderer\Private\OverridePassSequence.h

template <typename EPass>
class TOverridePassSequence final
{
public:
TOverridePassSequence(const FScreenPassRenderTarget& InOverrideOutput)
: OverrideOutput(InOverrideOutput)
{}

~TOverridePassSequence();

// 设置名字
void SetName(EPass Pass, const TCHAR* Name);
void SetNames(const TCHAR* const* Names, uint32 NameCount);

// 开启指定Pass.
void SetEnabled(EPass Pass, bool bEnabled);
bool IsEnabled(EPass Pass) const;

// 是否最后一个Pass.
bool IsLastPass(EPass Pass) const;

// 接受Pass, 如果没有按顺序, 则会报错.
void AcceptPass(EPass Pass)
{
#if RDG_ENABLE_DEBUG
const int32 PassIndex = (int32)Pass;

check(bFinalized);
checkf(NextPass == Pass, TEXT("Pass was accepted out of order: %s. Expected %s."), Passes[PassIndex].Name, Passes[(int32)NextPass].Name);
checkf(Passes[PassIndex].bEnabled, TEXT("Only accepted passes can be enabled: %s."), Passes[PassIndex].Name);

Passes[PassIndex].bAccepted = true;

// Walk the remaining passes until we hit one that's enabled. This will be the next pass to add.
for (int32 NextPassIndex = int32(NextPass) + 1; NextPassIndex < PassCountMax; ++NextPassIndex)
{
if (Passes[NextPassIndex].bEnabled)
{
NextPass = EPass(NextPassIndex);
break;
}
}
#endif
}

// 如果Pass是最后一个, 则接受覆盖的RT.
bool AcceptOverrideIfLastPass(EPass Pass, FScreenPassRenderTarget& OutTargetToOverride, const TOptional<int32>& AfterPassCallbackIndex = TOptional<int32>())
{
bool bLastAfterPass = AfterPass[(int32)Pass].Num() == 0;

if (AfterPassCallbackIndex)
{
bLastAfterPass = AfterPassCallbackIndex.GetValue() == AfterPass[(int32)Pass].Num() - 1;
}
else
{
// Display debug information for a Pass unless it is an after pass.
AcceptPass(Pass);
}

// We need to override output only if this is the last pass and the last after pass.
if (IsLastPass(Pass) && bLastAfterPass)
{
OutTargetToOverride = OverrideOutput;
return true;
}

return false;
}

// Pass开启结束.
void Finalize()
{
#if RDG_ENABLE_DEBUG
check(!bFinalized);
bFinalized = true;

for (int32 PassIndex = 0; PassIndex < PassCountMax; ++PassIndex)
{
checkf(Passes[PassIndex].bAssigned, TEXT("Pass was not assigned to enabled or disabled: %s."), Passes[PassIndex].Name);
}
#endif

bool bFirstPass = true;

for (int32 PassIndex = 0; PassIndex < PassCountMax; ++PassIndex)
{
if (Passes[PassIndex].bEnabled)
{
if (bFirstPass)
{
#if RDG_ENABLE_DEBUG
NextPass = (EPass)PassIndex;
#endif
bFirstPass = false;
}
LastPass = (EPass)PassIndex;
}
}
}

FAfterPassCallbackDelegateArray& GetAfterPassCallbacks(EPass Pass);

private:
static const int32 PassCountMax = (int32)EPass::MAX;

struct FPassInfo
{
#if RDG_ENABLE_DEBUG
const TCHAR* Name = nullptr;
bool bAssigned = false;
bool bAccepted = false;
#endif
bool bEnabled = false;
};

FScreenPassRenderTarget OverrideOutput;
TStaticArray<FPassInfo, PassCountMax> Passes;
TStaticArray<FAfterPassCallbackDelegateArray, PassCountMax> AfterPass;
EPass LastPass = EPass::MAX;

#if RDG_ENABLE_DEBUG
EPass NextPass = EPass(0);
bool bFinalized = false;
#endif
};


通过TOverridePassSequence可以方便地实现、管理、执行一组有序的后处理效果。但也需要注意以下几点:

  • PassSequence在处理完通道开启和关闭之后,需要手动调用一次Finalize,否则开发者模式下会报错。
  • TOverridePassSequence需要显示开启和关闭指定通道,如果开启或关闭的通道和实际加入的Pass不一致,则会报错。下面详细地按某些情形讨论PassSequence是否会产生报错(开发模式下):
  • 通道A被开启,没有向GraphBuilder添加Pass,没有调用​​AcceptOverrideIfLastPass​​,会报错。
  • 通道A被开启,向GraphBuilder添加了Pass,没有调用​​AcceptOverrideIfLastPass​​,会报错。
  • 通道A被开启,没有向GraphBuilder添加Pass,有调用​​AcceptOverrideIfLastPass​​,不会报错。PassSequence无法察觉到这种情况的异常!!
  • 通道A被关闭,向GraphBuilder添加了Pass,有调用​​AcceptOverrideIfLastPass​​,会报错。
  • 通道A被关闭,向GraphBuilder添加了Pass,没有调用​​AcceptOverrideIfLastPass​​,不会报错。PassSequence无法察觉到这种情况的异常!!
  • 如果通道A和B都被开启,但B在A之前向GraphBuilder添加了Pass并调用​​AcceptOverrideIfLastPass​​,会报错。
  • 举个具体的例子,有以下代码:
    // 关闭通道序列的FXAA. PassSequence.SetEnabled(EPass::FXAA, false); (......) // 构造FXAA输入参数 FFXAAInputs PassInputs; // 调用Pass接受. PassSequence.AcceptOverrideIfLastPass(EPass::FXAA, PassInputs.OverrideOutput); PassInputs.SceneColor = SceneColor; PassInputs.Quality = GetFXAAQuality(); // 加入FXAA通道. SceneColor = AddFXAAPass(GraphBuilder, View, PassInputs);
    由于以上代码中已经关闭了PassSequence的FXAA通道,但又尝试进行对其添加通道并调用AcceptOverrideIfLastPass,则开发者模式下会报以下错误:
    意思是说Pass没有按照开启的Pass顺序被接受,是在AcceptOverrideIfLastPass内部触发的。

7.3.3 BlendableLocation

BlendableLocation就是后处理材质的混合位置,它的定义如下:

enum EBlendableLocation
{
// 色调映射之后.
BL_AfterTonemapping,
// 色调映射之前.
BL_BeforeTonemapping,
// 半透明组合之前.
BL_BeforeTranslucency,
// 替换掉色调映射.
BL_ReplacingTonemapper,
// SSR输入.
BL_SSRInput,

BL_MAX,
};


​EBlendableLocation​​可在材质编辑器的属性面板中指定:

剖析虚幻渲染体系(07)- 后处理_游戏引擎_14

默认的混合阶段是在色调映射之后(After Tonemapping),但是,我们可以改变混合位置来实现不同的效果。比如,我们的后处理效果需要用到色调映射之前的场景颜色,那么就需要将混合位置改成Before Tonemapping;如果需要自定义色调映射算法,以代替UE的默认色调映射效果,那么可以改成Replacing the Tonemapper;如果我们的后处理效果不希望影响透明物体,则可以改成Before Translucency;如果想实现自定义的SSR算法,则可以改成SSR Input。

为了阐明BlendableLocation在后处理管线中的作用及处理过程,抽取其相关的类型、代码和步骤:

// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessMaterial.h

using FPostProcessMaterialChain = TArray<const UMaterialInterface*, TInlineAllocator<10>>;
FPostProcessMaterialChain GetPostProcessMaterialChain(const FViewInfo& View, EBlendableLocation Location);

// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessMaterial.cpp

FPostProcessMaterialChain GetPostProcessMaterialChain(const FViewInfo& View, EBlendableLocation Location)
{
if (!IsPostProcessMaterialsEnabledForView(View))
{
return {};
}

const FSceneViewFamily& ViewFamily = *View.Family;

TArray<FPostProcessMaterialNode, TInlineAllocator<10>> Nodes;
FBlendableEntry* Iterator = nullptr;

(......)

// 遍历视图的后处理设置, 获取所有后处理材质节点. 注意, 这里的迭代器已经指明了Location, 意味着添加到Nodes的都是在Location的材质.
while (FPostProcessMaterialNode* Data = IteratePostProcessMaterialNodes(View.FinalPostProcessSettings, Location, Iterator))
{
check(Data->GetMaterialInterface());
Nodes.Add(*Data);
}

if (!Nodes.Num())
{
return {};
}

// 按优先级排序.
::Sort(Nodes.GetData(), Nodes.Num(), FPostProcessMaterialNode::FCompare());

FPostProcessMaterialChain OutputChain;
OutputChain.Reserve(Nodes.Num());

// 添加材质到输出列表.
for (const FPostProcessMaterialNode& Node : Nodes)
{
OutputChain.Add(Node.GetMaterialInterface());
}

return OutputChain;
}

// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

void AddPostProcessingPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FPostProcessingInputs& Inputs)
{
(......)

const FPostProcessMaterialChain PostProcessMaterialAfterTonemappingChain = GetPostProcessMaterialChain(View, BL_AfterTonemapping);

(......)

// 后处理材质链 - Before Translucency
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTranslucency);
SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);

(......)

// 组合半透明纹理到场景颜色纹理中.
LocalSceneColorTexture = AddSeparateTranslucencyCompositionPass(GraphBuilder, View, SceneColor.Texture, *Inputs.SeparateTranslucencyTextures);

(......)

// 后处理材质链 - Before Tonemapping
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTonemapping);
SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);

(......)

// 后处理材质链 - SSR Input
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_SSRInput);
GraphBuilder.QueueTextureExtraction(PassOutput.Texture, &View.ViewState->PrevFrameViewInfo.CustomSSRInput);

(......)

// 色调映射
if (PassSequence.IsEnabled(EPass::Tonemap))
{
const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_ReplacingTonemapper);

// 如果存在需要替换UE默认色调映射的材质, 则执行之.
if (MaterialChain.Num())
{
SceneColor = AddPostProcessMaterialPass(GraphBuilder, View, PassInputs, HighestPriorityMaterial);
}
// 不存在需要替换UE默认色调映射的材质, 执行UE默认的色调映射.
else
{
SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs);
}
}

// 后处理材质链 - After Tonemapping
SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, PassInputs, PostProcessMaterialAfterTonemappingChain);

(......)
}


由以上代码可知,BlendableLocation名符其实,单看Location的字面意思就已经知道它的运行位置。其中最重要的是色调映射,在之前、之中、之后都可以自定义后处理材质,为引擎可扩展性添砖加瓦。

7.3.4 PostProcessMaterial

PostProcessMaterial就是处理和渲染BlendableLocation的后处理材质类,其定义和相关类型如下:

// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessMaterial.h

// 后处理材质输入槽.
enum class EPostProcessMaterialInput : uint32
{
SceneColor = 0, // 场景颜色, 总是激活(可用)状态. 来自上一个后处理的输出.
SeparateTranslucency = 1, // 透明纹理, 总是激活状态.
CombinedBloom = 2, // 组合的泛光.

// 仅用于可视化.
PreTonemapHDRColor = 2,
PostTonemapHDRColor = 3,

// 速度.
Velocity = 4
};