UE5 PostProcess多Pass联调 Bloom效果

前期已经做过单pass效果

这次不讲其他功能,注重多pass部分:


一、创建新文件:

复制代码
Engine/Source/Runtime/Renderer/Private/PostProcess/LearningBloom/
    LearningBloom.h
    LearningBloom.cpp

Engine/Shaders/Private/PostProcess/LearningBloom/
    LearningBloom.usf

LearningBloom.h:

复制代码
// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "ScreenPass.h"

class FRDGBuilder;
class FViewInfo;

struct FLearningBloomInputs
{
	FScreenPassTexture SceneColor;
};

FScreenPassTexture AddLearningBloomPass(
	FRDGBuilder& GraphBuilder,
	const FViewInfo& View,
	const FLearningBloomInputs& Inputs);

LearningBloom.cpp:

复制代码
// Copyright Epic Games, Inc. All Rights Reserved.

#include "PostProcess/LearningBloom/LearningBloom.h"

#include "DataDrivenShaderPlatformInfo.h"
#include "PixelShaderUtils.h"
#include "PostProcess/PostProcessWeightedSampleSum.h"
#include "SceneRendering.h"

namespace
{
	TAutoConsoleVariable<int32> CVarLearningBloomEnable(
		TEXT("r.LearningBloom.Enable"),
		0,
		TEXT("Enable the learning bloom pass.\n")
		TEXT("0: Disabled\n")
		TEXT("1: Enabled"),
		ECVF_RenderThreadSafe);

	TAutoConsoleVariable<float> CVarLearningBloomThreshold(
		TEXT("r.LearningBloom.Threshold"),
		1.0f,
		TEXT("HDR luminance threshold."),
		ECVF_RenderThreadSafe);

	TAutoConsoleVariable<float> CVarLearningBloomBlurSize(
		TEXT("r.LearningBloom.BlurSize"),
		2.0f,
		TEXT("Gaussian blur kernel size, as percentage of screen width."),
		ECVF_RenderThreadSafe);

	TAutoConsoleVariable<float> CVarLearningBloomIntensity(
		TEXT("r.LearningBloom.Intensity"),
		1.0f,
		TEXT("Intensity applied during composition."),
		ECVF_RenderThreadSafe);

	TAutoConsoleVariable<int32> CVarLearningBloomDebug(
		TEXT("r.LearningBloom.Debug"),
		0,
		TEXT("Debug output.\n")
		TEXT("0: Final composite\n")
		TEXT("1: Threshold result\n")
		TEXT("2: Blurred result"),
		ECVF_RenderThreadSafe);

	class FLearningBloomThresholdPS : public FGlobalShader
	{
	public:
		DECLARE_GLOBAL_SHADER(FLearningBloomThresholdPS);
		SHADER_USE_PARAMETER_STRUCT(
			FLearningBloomThresholdPS,
			FGlobalShader);

		BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
			SHADER_PARAMETER_STRUCT_REF(
				FViewUniformShaderParameters,
				View)

			SHADER_PARAMETER_RDG_TEXTURE(
				Texture2D,
				InputTexture)

			SHADER_PARAMETER_SAMPLER(
				SamplerState,
				InputSampler)

			SHADER_PARAMETER(
				FScreenTransform,
				SvPositionToInputTextureUV)

			SHADER_PARAMETER(float, Threshold)

			RENDER_TARGET_BINDING_SLOTS()
		END_SHADER_PARAMETER_STRUCT()

		static bool ShouldCompilePermutation(
			const FGlobalShaderPermutationParameters& Parameters)
		{
			return IsFeatureLevelSupported(
				Parameters.Platform,
				ERHIFeatureLevel::SM5);
		}
	};

	IMPLEMENT_GLOBAL_SHADER(
		FLearningBloomThresholdPS,
		"/Engine/Private/PostProcess/LearningBloom/LearningBloom.usf",
		"LearningBloomThresholdPS",
		SF_Pixel);

	class FLearningBloomCompositePS : public FGlobalShader
	{
	public:
		DECLARE_GLOBAL_SHADER(FLearningBloomCompositePS);
		SHADER_USE_PARAMETER_STRUCT(
			FLearningBloomCompositePS,
			FGlobalShader);

		BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
			SHADER_PARAMETER_RDG_TEXTURE(
				Texture2D,
				SceneColorTexture)

			SHADER_PARAMETER_SAMPLER(
				SamplerState,
				SceneColorSampler)

			SHADER_PARAMETER_RDG_TEXTURE(
				Texture2D,
				BloomTexture)

			SHADER_PARAMETER_SAMPLER(
				SamplerState,
				BloomSampler)

			SHADER_PARAMETER(
				FScreenTransform,
				SvPositionToSceneColorUV)

			SHADER_PARAMETER(
				FScreenTransform,
				SvPositionToBloomUV)

			SHADER_PARAMETER(float, Intensity)

			RENDER_TARGET_BINDING_SLOTS()
		END_SHADER_PARAMETER_STRUCT()

		static bool ShouldCompilePermutation(
			const FGlobalShaderPermutationParameters& Parameters)
		{
			return IsFeatureLevelSupported(
				Parameters.Platform,
				ERHIFeatureLevel::SM5);
		}
	};

	IMPLEMENT_GLOBAL_SHADER(
		FLearningBloomCompositePS,
		"/Engine/Private/PostProcess/LearningBloom/LearningBloom.usf",
		"LearningBloomCompositePS",
		SF_Pixel);

	FScreenTransform GetSvPositionToTextureUV(
		const FScreenPassTextureViewport& OutputViewport,
		const FScreenPassTextureViewport& InputViewport)
	{
		return
			FScreenTransform::ChangeTextureBasisFromTo(
				OutputViewport,
				FScreenTransform::ETextureBasis::TexelPosition,
				FScreenTransform::ETextureBasis::ViewportUV)
			*
			FScreenTransform::ChangeTextureBasisFromTo(
				InputViewport,
				FScreenTransform::ETextureBasis::ViewportUV,
				FScreenTransform::ETextureBasis::TextureUV);
	}

	FScreenPassTexture AddThresholdPass(
		FRDGBuilder& GraphBuilder,
		const FViewInfo& View,
		const FScreenPassTexture& Input,
		float Threshold)
	{
		FScreenPassRenderTarget Output =
			FScreenPassRenderTarget::CreateFromInput(
				GraphBuilder,
				Input,
				View.GetOverwriteLoadAction(),
				TEXT("LearningBloom.Threshold"));

		const FScreenPassTextureViewport InputViewport(Input);
		const FScreenPassTextureViewport OutputViewport(Output);

		FLearningBloomThresholdPS::FParameters* PassParameters =
			GraphBuilder.AllocParameters<
				FLearningBloomThresholdPS::FParameters>();

		PassParameters->View = View.ViewUniformBuffer;
		PassParameters->InputTexture = Input.Texture;
		PassParameters->InputSampler =
			TStaticSamplerState<
				SF_Bilinear,
				AM_Clamp,
				AM_Clamp,
				AM_Clamp>::GetRHI();

		PassParameters->Threshold = Threshold;

		PassParameters->SvPositionToInputTextureUV =
			GetSvPositionToTextureUV(
				OutputViewport,
				InputViewport);

		PassParameters->RenderTargets[0] =
			Output.GetRenderTargetBinding();

		TShaderMapRef<FLearningBloomThresholdPS> PixelShader(
			View.ShaderMap);

		FPixelShaderUtils::AddFullscreenPass(
			GraphBuilder,
			View.ShaderMap,
			RDG_EVENT_NAME("LearningBloom Threshold"),
			PixelShader,
			PassParameters,
			Output.ViewRect);

		return FScreenPassTexture(Output);
	}

	FScreenPassTexture AddCompositePass(
		FRDGBuilder& GraphBuilder,
		const FViewInfo& View,
		const FScreenPassTexture& SceneColor,
		const FScreenPassTexture& Bloom,
		float Intensity)
	{
		FScreenPassRenderTarget Output =
			FScreenPassRenderTarget::CreateFromInput(
				GraphBuilder,
				SceneColor,
				View.GetOverwriteLoadAction(),
				TEXT("LearningBloom.Composite"));

		const FScreenPassTextureViewport OutputViewport(Output);
		const FScreenPassTextureViewport SceneColorViewport(SceneColor);
		const FScreenPassTextureViewport BloomViewport(Bloom);

		FLearningBloomCompositePS::FParameters* PassParameters =
			GraphBuilder.AllocParameters<
				FLearningBloomCompositePS::FParameters>();

		FRHISamplerState* BilinearClampSampler =
			TStaticSamplerState<
				SF_Bilinear,
				AM_Clamp,
				AM_Clamp,
				AM_Clamp>::GetRHI();

		PassParameters->SceneColorTexture = SceneColor.Texture;
		PassParameters->SceneColorSampler = BilinearClampSampler;

		PassParameters->BloomTexture = Bloom.Texture;
		PassParameters->BloomSampler = BilinearClampSampler;

		PassParameters->Intensity = Intensity;

		PassParameters->SvPositionToSceneColorUV =
			GetSvPositionToTextureUV(
				OutputViewport,
				SceneColorViewport);

		PassParameters->SvPositionToBloomUV =
			GetSvPositionToTextureUV(
				OutputViewport,
				BloomViewport);

		PassParameters->RenderTargets[0] =
			Output.GetRenderTargetBinding();

		TShaderMapRef<FLearningBloomCompositePS> PixelShader(
			View.ShaderMap);

		FPixelShaderUtils::AddFullscreenPass(
			GraphBuilder,
			View.ShaderMap,
			RDG_EVENT_NAME("LearningBloom Composite"),
			PixelShader,
			PassParameters,
			Output.ViewRect);

		return FScreenPassTexture(Output);
	}
}

FScreenPassTexture AddLearningBloomPass(
	FRDGBuilder& GraphBuilder,
	const FViewInfo& View,
	const FLearningBloomInputs& Inputs)
{
	check(Inputs.SceneColor.IsValid());

	if (CVarLearningBloomEnable.GetValueOnRenderThread() == 0)
	{
		return Inputs.SceneColor;
	}

	const float Threshold =
		FMath::Max(
			0.0f,
			CVarLearningBloomThreshold.GetValueOnRenderThread());

	const float BlurSize =
		FMath::Clamp(
			CVarLearningBloomBlurSize.GetValueOnRenderThread(),
			0.1f,
			20.0f);

	const float Intensity =
		FMath::Max(
			0.0f,
			CVarLearningBloomIntensity.GetValueOnRenderThread());

	const int32 DebugMode =
		CVarLearningBloomDebug.GetValueOnRenderThread();

	const FScreenPassTexture ThresholdOutput =
		AddThresholdPass(
			GraphBuilder,
			View,
			Inputs.SceneColor,
			Threshold);

	if (DebugMode == 1)
	{
		return ThresholdOutput;
	}

	FGaussianBlurInputs BlurInputs;
	BlurInputs.NameX = TEXT("LearningBloom Blur X");
	BlurInputs.NameY = TEXT("LearningBloom Blur Y");
	BlurInputs.Filter =
		FScreenPassTextureSlice::CreateFromScreenPassTexture(
			GraphBuilder,
			ThresholdOutput);

	BlurInputs.TintColor = FLinearColor::White;
	BlurInputs.KernelSizePercent = BlurSize;
	BlurInputs.UseMirrorAddressMode = false;

	const FScreenPassTexture BlurOutput =
		AddGaussianBlurPass(
			GraphBuilder,
			View,
			BlurInputs);

	if (DebugMode == 2)
	{
		return BlurOutput;
	}

	return AddCompositePass(
		GraphBuilder,
		View,
		Inputs.SceneColor,
		BlurOutput,
		Intensity);
}

这里注意,上段代码有多个PS的IMPLEMENT_GLOBAL_SHADER里面绑定的是同一个.usf文件的不同函数

FScreenPassTextureSlice是什么?

它只有两个核心成员:

复制代码
FRDGTextureSRVRef TextureSRV;
FIntRect ViewRect;

纹理:1920 × 1080
┌──────────────┬──────────────┐
│              │              │
│  其他视图     │ 当前 ViewRect │
│              │              │
└──────────────┴──────────────┘

所以:

复制代码
TextureSRV = 从哪张纹理、哪个 array slice 读取
ViewRect   = 在这个 slice 中处理哪个矩形区域

复制代码
BlurInputs.TintColor = FLinearColor::White;
BlurInputs.KernelSizePercent = BlurSize;
BlurInputs.UseMirrorAddressMode = false;

1. TintColor:模糊结果的颜色倍率

复制代码
BlurInputs.TintColor = FLinearColor::White;

White 等于:

复制代码
FLinearColor(1, 1, 1, 1)

意思是不改变模糊结果的颜色和强度:

复制代码
最终模糊颜色 = GaussianBlur结果 × TintColor

复制代码
BlurSize = 2.0f;
屏幕宽度 = 1920;

模糊半径:

复制代码
Radius = 1920 × 2% × 0.5
       = 19.2 像素

创建 LearningBloom.usf

复制代码
// Copyright Epic Games, Inc. All Rights Reserved.

#include "Common.ush"
#include "ScreenPass.ush"

Texture2D InputTexture;
SamplerState InputSampler;

FScreenTransform SvPositionToInputTextureUV;
float Threshold;

void LearningBloomThresholdPS(
	float4 SvPosition : SV_POSITION,
	out float4 OutColor : SV_Target0)
{
	const float2 UV = ApplyScreenTransform(
		SvPosition.xy,
		SvPositionToInputTextureUV);

	const float3 PreExposedColor =
		Texture2DSample(InputTexture, InputSampler, UV).rgb;

	const float3 AbsoluteColor =
		PreExposedColor * View.OneOverPreExposure;

	const float LuminanceValue = dot(
		AbsoluteColor,
		float3(0.2126f, 0.7152f, 0.0722f));

	const float BrightMask =
		step(Threshold, LuminanceValue);

	const float3 BrightColor =
		AbsoluteColor * BrightMask * View.PreExposure;

	OutColor = float4(BrightColor, 0.0f);
}


// Composite pass

Texture2D SceneColorTexture;
SamplerState SceneColorSampler;

Texture2D BloomTexture;
SamplerState BloomSampler;

FScreenTransform SvPositionToSceneColorUV;
FScreenTransform SvPositionToBloomUV;

float Intensity;

void LearningBloomCompositePS(
	float4 SvPosition : SV_POSITION,
	out float4 OutColor : SV_Target0)
{
	const float2 SceneColorUV = ApplyScreenTransform(
		SvPosition.xy,
		SvPositionToSceneColorUV);

	const float2 BloomUV = ApplyScreenTransform(
		SvPosition.xy,
		SvPositionToBloomUV);

	const float4 SceneColor = Texture2DSample(
		SceneColorTexture,
		SceneColorSampler,
		SceneColorUV);

	const float3 BloomColor = Texture2DSample(
		BloomTexture,
		BloomSampler,
		BloomUV).rgb;

	OutColor = float4(
		SceneColor.rgb + BloomColor * Intensity,
		SceneColor.a);
}

SvPosition 不是 C++ 手动传进来的,而是 GPU 光栅化阶段自动传给 Pixel Shader 的。

AddFullscreenPass() 会绘制一个覆盖整个画面的全屏三角形,流程是:

复制代码
C++ AddFullscreenPass
        ↓
全屏 Vertex Shader
        ↓
输出顶点 SV_POSITION
        ↓
GPU 光栅化器生成像素
        ↓
Pixel Shader 接收到当前像素的 SV_POSITION
        ↓
LearningBloomThresholdPS

ApplyScreenTransform() 是 Unreal Engine Shader 自带的辅助函数。

PreExposure 可以直白理解成:

UE 在写入 HDR SceneColor 之前,先把颜色整体乘一个系数,让 GPU 中间纹理里的数字不要过大或过小。

它主要是数值稳定手段,不是额外的美术曝光效果。

为什么需要它

真实 HDR 场景颜色范围可能非常夸张:

复制代码
黑暗房间:0.001
普通物体:1
太阳、高亮反射:100000+

如果全部直接写进 SceneColor:

  • 大数值可能溢出;
  • 小数值精度不足;
  • R11G11B10 等格式更容易出现问题;
  • Temporal History 跨帧处理也更困难。

所以 UE 提前缩放:

复制代码
SceneColor中存储的颜色
= 原始场景颜色 × PreExposure

Tonemap 时会除掉 PreExposure。

假设太阳像素的原始 HDR 颜色是:

复制代码
100000

相机最终曝光是:

复制代码
Exposure = 0.001

不使用 PreExposure

SceneColor 先保存:

复制代码
100000

Tonemap 前再计算:

复制代码
100000 × 0.001 = 100

最终数字没问题,但 SceneColor、TAA、Bloom、MotionBlur 等中间过程一直在处理 100000

可能出现:

  • HDR 格式范围或精度问题;
  • 高亮计算不稳定;
  • Blur 累加大数值;
  • Temporal History 精度问题。

把 Pass 插入 PostProcessing.cpp

打开:

PostProcessing.cpp:

在 include 区域添加:

复制代码
#include "PostProcess/LearningBloom/LearningBloom.h"

然后找到大约 1400 行附近:

复制代码
SceneColorBeforeTonemapSlice = SceneColorSlice;

if (PassSequence.IsEnabled(EPass::Tonemap))

在它前面插入:

复制代码
{
	FLearningBloomInputs LearningBloomInputs;

	LearningBloomInputs.SceneColor =
		FScreenPassTexture::CopyFromSlice(
			GraphBuilder,
			SceneColorSlice);

	const FScreenPassTexture LearningBloomOutput =
		AddLearningBloomPass(
			GraphBuilder,
			View,
			LearningBloomInputs);

	SceneColorSlice =
		FScreenPassTextureSlice::CreateFromScreenPassTexture(
			GraphBuilder,
			LearningBloomOutput);
}

SceneColorBeforeTonemapSlice = SceneColorSlice;