前期已经做过单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;
