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

📅 2026/6/30 15:20:29
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.ViewExtensionsIsActiveThisFrame_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 TAutoConsoleVariableint32 CVarLearningComputeEnable( TEXT(r.LearningPostProcess.Compute.Enable), 1, TEXT(Enable LearningPostProcess compute shader.), ECVF_RenderThreadSafe); static TAutoConsoleVariablefloat 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( RWTexture2Dfloat4, 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 TShaderMapRefFLearningPostProcessCS 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 ViewSRV绑定给 Shader供 Shader 读取FRDGTextureRef OutputTexture GraphBuilder.CreateTexture( OutputDesc, TEXT(LearningPostProcess.ComputeOutput));通过才建立的FRDGTextureDesc来创建对应的FRDGTexture记住是通过GraphBuilderconst FGlobalShaderMap* ShaderMap GetGlobalShaderMap( View.GetShaderPlatform()); const TShaderMapRefFLearningPostProcessCS ComputeShader(ShaderMap);因为函数输入是FSceneView没有FViewInfo那么多信息所以需要从FSceneView里面获取GlobalShaderMap而FViewInfo可以直接View.ShaderMapLearningPostProcessCS.usf#include /Engine/Private/Common.ush Texture2D InputTexture; RWTexture2Dfloat4 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.FSceneView和FViewInfo这个区别主要来自接口层级不是 Shader 类型FSceneView较公开、通用的视图接口View Extension 常用。FViewInfoRenderer 内部使用继承自FSceneView包含更多渲染器内部数据。因此 Compute Shader 和 Pixel Shader 都可以接收FSceneView或FViewInfo取决于调用位置。2. 底层资源其实都是FRDGTextureFScreenPassTexture FScreenPassRenderTarget都是对FRDGTexture的 Screen Pass 包装FScreenPassTexture FRDGTexture ViewRect FScreenPassRenderTarget FRDGTexture ViewRect LoadActionFScreenPassTextureViewport则主要描述纹理的尺寸、视口和坐标转换不是另一种纹理资源。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 手动判断边界硬件混合支持通常需要手动实现随机访问较受限制灵活读写 UAVAsync Compute不支持条件满足时可支持缩写全称权限/用途常见阶段SRVShader Resource ViewShader 只读VS/PS/CS 等UAVUnordered Access ViewShader 随机读写CS偶尔 PSRTVRender Target View光栅化颜色输出Pixel ShaderDSVDepth Stencil View深度/模板读写光栅化阶段允许渲染管线写入CBVConstant Buffer View只读常量参数所有 Shader严格来说RTV 不是在 Pixel Shader 阶段写入而是在 Pixel Shader 之后的 Output Merger输出合并阶段写入。完整流程顶点数据 ↓ Vertex Shader ↓ 光栅化器生成像素片元 ↓ Pixel Shader 计算颜色 ↓ Output Merger 进行混合 ↓ RTVPixel 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 深度/模板测试结果 ↓ 最终写入 RTVHLSL Pixel Shader 输出的是SV_TargetC 后处理函数返回的是FScreenPassTexture它们不是同一层面的返回值。Pixel Shader 输出 SV_Target ↓ Output Merger 写入 RTV ↓ RTV 对应一张 FRDGTexture ↓ C 用 FScreenPassTexture 包装并返回它PSSRV 读取 → PS 计算 → RTV 输出 CSSRV 读取 → 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