UE5 如何使用 compute shader 增加一个 postprocess pass

UE5 如何使用 compute shader 增加一个 postprocess pass

效果:


目录结构:

Plugins/LearningPostProcess/ ├─ Shaders/Private/ │ └─ LearningPostProcessCS.usf // 新增 └─ Source/LearningPostProcess/ ├─ Public/ │ ├─ LearningPostProcess.h // 修改 │ └─ LearningComputeViewExtension.h // 新增 ├─ Private/ │ ├─ LearningPostProcess.cpp // 修改 │ └─ LearningComputeViewExtension.cpp // 新增 └─ LearningPostProcess.Build.cs

代码:

LearningComputeViewExtension.h:

#pragma once #include "SceneViewExtension.h" class FLearningComputeViewExtension final : public FSceneViewExtensionBase { public: explicit FLearningComputeViewExtension( const FAutoRegister& AutoRegister); virtual bool IsActiveThisFrame_Internal( const FSceneViewExtensionContext& Context) const override; virtual void SubscribeToPostProcessingPass( EPostProcessingPass PassId, const FSceneView& View, FPostProcessingPassDelegateArray& InOutPassCallbacks, bool bIsPassEnabled) override; private: FScreenPassTexture PostProcessCompute_RenderThread( FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessMaterialInputs& Inputs); };
explicit FLearningComputeViewExtension( const FAutoRegister& AutoRegister);

相当于view extension在构造函数就已经注册在全局注册表了

创建 View Extension ↓ RegisterExtension() ↓ 注册到全局 KnownExtensions ↓ 创建某个 ViewFamily ↓ 调用 IsActiveThisFrame(Context) ↓ true 加入 ViewFamily.ViewExtensions
IsActiveThisFrame_Internal(Context) == false ↓ 该扩展不会加入 ViewFamily.ViewExtensions ↓ 不会调用 SubscribeToPostProcessingPass() ↓ 无法向 InOutPassCallbacks 添加委托 ↓ PostProcessCompute_RenderThread() 不会被调用

LearningComputeViewExtension.cpp:

#include "LearningComputeViewExtension.h" #include "DataDrivenShaderPlatformInfo.h" #include "GlobalShader.h" #include "HAL/IConsoleManager.h" #include "PostProcess/PostProcessMaterialInputs.h" #include "RenderGraphBuilder.h" #include "RenderGraphUtils.h" #include "SceneView.h" #include "ShaderParameterStruct.h" static constexpr int32 GLearningComputeThreadGroupSize = 8; static TAutoConsoleVariable<int32> CVarLearningComputeEnable( TEXT("r.LearningPostProcess.Compute.Enable"), 1, TEXT("Enable LearningPostProcess compute shader."), ECVF_RenderThreadSafe); static TAutoConsoleVariable<float> CVarLearningComputeIntensity( TEXT("r.LearningPostProcess.Compute.Intensity"), 1.0f, TEXT("Compute shader grayscale intensity."), ECVF_RenderThreadSafe); class FLearningPostProcessCS final : public FGlobalShader { public: DECLARE_GLOBAL_SHADER(FLearningPostProcessCS); SHADER_USE_PARAMETER_STRUCT( FLearningPostProcessCS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER_RDG_TEXTURE( Texture2D, InputTexture) SHADER_PARAMETER_RDG_TEXTURE_UAV( RWTexture2D<float4>, OutputTexture) SHADER_PARAMETER( FIntPoint, ViewRectMin) SHADER_PARAMETER( FIntPoint, ViewRectSize) SHADER_PARAMETER( float, Intensity) END_SHADER_PARAMETER_STRUCT() static bool ShouldCompilePermutation( const FGlobalShaderPermutationParameters& Parameters) { return IsFeatureLevelSupported( Parameters.Platform, ERHIFeatureLevel::SM5); } }; IMPLEMENT_GLOBAL_SHADER( FLearningPostProcessCS, "/Plugin/LearningPostProcess/Private/LearningPostProcessCS.usf", "LearningPostProcessCS", SF_Compute); FLearningComputeViewExtension:: FLearningComputeViewExtension( const FAutoRegister& AutoRegister) : FSceneViewExtensionBase(AutoRegister) { } bool FLearningComputeViewExtension:: IsActiveThisFrame_Internal( const FSceneViewExtensionContext& Context) const { return CVarLearningComputeEnable.GetValueOnAnyThread() != 0; } void FLearningComputeViewExtension:: SubscribeToPostProcessingPass( EPostProcessingPass PassId, const FSceneView& View, FPostProcessingPassDelegateArray& InOutPassCallbacks, bool bIsPassEnabled) { // CS 独立插入到 AfterDOF。 if (PassId != EPostProcessingPass::AfterDOF) { return; } // 当前示例只支持 SM5 及以上。 if (!IsFeatureLevelSupported( View.GetShaderPlatform(), ERHIFeatureLevel::SM5)) { return; } InOutPassCallbacks.Add( FPostProcessingPassDelegate::CreateRaw( this, &FLearningComputeViewExtension:: PostProcessCompute_RenderThread)); } FScreenPassTexture FLearningComputeViewExtension:: PostProcessCompute_RenderThread( FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessMaterialInputs& Inputs) { const FScreenPassTexture SceneColor = FScreenPassTexture::CopyFromSlice( GraphBuilder, Inputs.GetInput( EPostProcessMaterialInput::SceneColor)); check(SceneColor.IsValid()); // 创建支持 UAV 的输出纹理。 const FRDGTextureDesc OutputDesc = FRDGTextureDesc::Create2D( SceneColor.Texture->Desc.Extent, PF_FloatRGBA, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV); FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture( OutputDesc, TEXT("LearningPostProcess.ComputeOutput")); FLearningPostProcessCS::FParameters* PassParameters = GraphBuilder.AllocParameters< FLearningPostProcessCS::FParameters>(); PassParameters->InputTexture = SceneColor.Texture; PassParameters->OutputTexture = GraphBuilder.CreateUAV( FRDGTextureUAVDesc(OutputTexture)); PassParameters->ViewRectMin = SceneColor.ViewRect.Min; PassParameters->ViewRectSize = SceneColor.ViewRect.Size(); PassParameters->Intensity = FMath::Clamp( CVarLearningComputeIntensity .GetValueOnRenderThread(), 0.0f, 1.0f); const FGlobalShaderMap* ShaderMap = GetGlobalShaderMap( View.GetShaderPlatform()); const TShaderMapRef<FLearningPostProcessCS> ComputeShader(ShaderMap); const FIntVector GroupCount = FComputeShaderUtils::GetGroupCount( SceneColor.ViewRect.Size(), FIntPoint( GLearningComputeThreadGroupSize, GLearningComputeThreadGroupSize)); FComputeShaderUtils::AddPass( GraphBuilder, RDG_EVENT_NAME( "LearningPostProcess Grayscale CS"), ComputeShader, PassParameters, GroupCount); return FScreenPassTexture( OutputTexture, SceneColor.ViewRect); }
// 创建支持 UAV 的输出纹理。 const FRDGTextureDesc OutputDesc = FRDGTextureDesc::Create2D( SceneColor.Texture->Desc.Extent, PF_FloatRGBA, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);

TexCreate_ShaderResource表示:

创建的纹理允许作为Shader Resource View(SRV)绑定给 Shader,供 Shader 读取

FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture( OutputDesc, TEXT("LearningPostProcess.ComputeOutput"));

通过才建立的FRDGTextureDesc来创建对应的FRDGTexture,记住是通过GraphBuilder!

const FGlobalShaderMap* ShaderMap = GetGlobalShaderMap( View.GetShaderPlatform()); const TShaderMapRef<FLearningPostProcessCS> ComputeShader(ShaderMap);

因为函数输入是FSceneView,没有FViewInfo那么多信息,所以需要从FSceneView里面获取GlobalShaderMap,而FViewInfo可以直接View.ShaderMap


LearningPostProcessCS.usf:

#include "/Engine/Private/Common.ush" Texture2D InputTexture; RWTexture2D<float4> OutputTexture; int2 ViewRectMin; int2 ViewRectSize; float Intensity; [numthreads(8, 8, 1)] void LearningPostProcessCS( uint3 DispatchThreadId : SV_DispatchThreadID) { const uint2 LocalPixel = DispatchThreadId.xy; // Dispatch 数量向上取整,需要排除边缘外的线程。 if (LocalPixel.x >= (uint) ViewRectSize.x || LocalPixel.y >= (uint) ViewRectSize.y) { return; } const int2 PixelPosition = ViewRectMin + int2(LocalPixel); const float4 SceneColor = InputTexture.Load( int3(PixelPosition, 0)); const float Luminance = dot( SceneColor.rgb, float3( 0.2126f, 0.7152f, 0.0722f)); const float3 Grayscale = Luminance.xxx; OutputTexture[PixelPosition] = float4( lerp( SceneColor.rgb, Grayscale, saturate(Intensity)), SceneColor.a); }
if (LocalPixel.x >= (uint)ViewRectSize.x || LocalPixel.y >= (uint)ViewRectSize.y) { return; }

Pixel Shader 不需要做这个检查,因为光栅化阶段会根据设置的 Viewport/Scissor Rect 只生成覆盖范围内的像素片元。


总结:

PS和CS两者都在 RDG 中添加 Pass,读取输入纹理、执行 Shader、输出结果。但有几处区别不是由 CS/PS 直接决定的。

1.FSceneViewFViewInfo

这个区别主要来自接口层级,不是 Shader 类型:

  • FSceneView:较公开、通用的视图接口,View Extension 常用。
  • FViewInfo:Renderer 内部使用,继承自FSceneView,包含更多渲染器内部数据。

因此 Compute Shader 和 Pixel Shader 都可以接收FSceneViewFViewInfo,取决于调用位置。

2. 底层资源其实都是FRDGTexture

FScreenPassTexture FScreenPassRenderTarget

都是对FRDGTexture的 Screen Pass 包装:

FScreenPassTexture = FRDGTexture + ViewRect FScreenPassRenderTarget = FRDGTexture + ViewRect + LoadAction

FScreenPassTextureViewport则主要描述纹理的尺寸、视口和坐标转换,不是另一种纹理资源。

3. Pixel Shader 通常走光栅化管线

SRV 读取输入 ↓ 绘制全屏三角形 ↓ Pixel Shader ↓ 通过 RTV 写入输出

常用接口:

AddDrawScreenPass(...)

输出一般是:

FScreenPassRenderTarget

它还可以利用硬件混合、颜色写掩码等固定功能。

4. Compute Shader 通常走计算管线

SRV 读取输入 ↓ Dispatch Thread Groups ↓ Compute Shader ↓ 通过 UAV 随机写入输出

常用接口:

FComputeShaderUtils::AddPass(...)

它没有传统的 RTV、全屏三角形和硬件混合,需要根据线程 ID 自己计算像素位置,并自行处理边界:

if (DispatchThreadId.x >= Width || DispatchThreadId.y >= Height) { return; }

关键区别

项目Pixel ShaderCompute Shader
输入通常 SRV通常 SRV
输出通常 RTV通常 UAV
执行方式Draw 全屏三角形Dispatch 线程组
像素范围Viewport 自动裁剪Shader 手动判断边界
硬件混合支持通常需要手动实现
随机访问较受限制灵活读写 UAV
Async Compute不支持条件满足时可支持
缩写全称权限/用途常见阶段
SRVShader Resource ViewShader 只读VS/PS/CS 等
UAVUnordered Access ViewShader 随机读写CS,偶尔 PS
RTVRender Target View光栅化颜色输出Pixel Shader
DSVDepth Stencil View深度/模板读写光栅化阶段,允许渲染管线写入
CBVConstant Buffer View只读常量参数所有 Shader

严格来说,RTV 不是在 Pixel Shader 阶段写入,而是在 Pixel Shader 之后的 Output Merger(输出合并)阶段写入。

完整流程:

顶点数据 ↓ Vertex Shader ↓ 光栅化器生成像素片元 ↓ Pixel Shader 计算颜色 ↓ Output Merger 进行混合 ↓ RTV

Pixel Shader 输出:

float4 MainPS(...) : SV_Target0 { return float4(1, 0, 0, 1); }

这里的SV_Target0表示:

把这个颜色发送到绑定的第 0 个 Render Target。

之后 Output Merger 会做:

PS 输出颜色 + RTV 中已有颜色 + Blend State + Color Write Mask + 深度/模板测试结果 ↓ 最终写入 RTV
  • HLSL Pixel Shader 输出的是SV_Target
  • C++ 后处理函数返回的是FScreenPassTexture

它们不是同一层面的返回值。

Pixel Shader 输出 SV_Target ↓ Output Merger 写入 RTV ↓ RTV 对应一张 FRDGTexture ↓ C++ 用 FScreenPassTexture 包装并返回它
PS:SRV 读取 → PS 计算 → RTV 输出 CS:SRV 读取 → CS 计算 → UAV 输出

Post Process 的 VS 和光栅化器不是重新处理场景模型,而是在绘制一个覆盖屏幕的三角形,用它来启动每个屏幕像素的 PS。

VS 做什么

通常只处理 3 个顶点,生成一个全屏大三角形:

(-1,-1) ───────── (3,-1) │ / │ / │ 屏幕区域/ │ / (-1,3)

然后取三角形里面的正方形做屏幕

Post Process Pixel Shader 流程: SceneColor 已经由前面的场景渲染生成 ↓ VS 生成一个覆盖整个屏幕的超大三角形 ↓ 裁剪、Viewport、Scissor 将有效范围限制在屏幕长方形内 (超出屏幕的部分不绘制) ↓ 光栅化器按照输出分辨率,将有效范围转换成像素片元,长方形去填充对应分辨率的像素 ↓ 为每个片元生成 SV_Position,并插值 UV 等数据 ↓ 每个片元执行一次 Pixel Shader ↓ Pixel Shader 通过 SRV 读取 SceneColor ↓ Pixel Shader 计算并输出 SV_Target ↓ Output Merger 将结果写入 RTV ↓ RTV 对应的 FRDGTexture 被包装成 FScreenPassTexture ↓ 交给下一道 Post Process Pass