当前位置: 首页 > news >正文

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
http://www.jsqmd.com/news/1096762/

相关文章:

  • MATLAB Profiler实战指南:从性能瓶颈定位到仿真加速
  • 实战解析:基于74LS194与Quartus的1101序列检测器设计与验证
  • 法治教育警示展厅设备【全民反诈跑酷答题】
  • 从公开信息到数据拼图:构建与防范视角下的社工库实践
  • SteamShutdown终极指南:智能监控Steam下载完成后自动关机
  • 2026阿坝黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • 毕业季救星!2026亲测好用的6款AI论文写作软件,初稿轻松搞定
  • 上市公司茶文化指数数据集
  • 技术解析 (二十三):基于注意力机制的深度多示例学习模型 (2018)
  • 终极免费Markdown Viewer:在浏览器中优雅阅读Markdown的完整指南
  • 【机器学习】从TF-IDF到TF-IWF:算法演进与实战调优指南
  • 庖丁解牛:从docker.io到containerd.io,拆解Docker生态核心组件与插件
  • 破解金融数据获取难题:efinance Python量化交易数据解决方案完全实战指南
  • HoRain云--揭秘C++ vector核心机制与高效用法
  • 『STC8H8K64U』实战:从零构建你的第一个智能硬件项目
  • Kettle(二):实战SQL Server数据同步与清洗
  • 非结构化数据清洗实战:从 HTML 到干净 JSON 的完整管道
  • 在VMware Workstation上构建vSphere 7.0实验环境:从ESXi到vCenter Server的完整实践
  • Qt (PyQt) 构建 Markdown 实时预览编辑器
  • Cadence PSpice Model Editor实战:IBIS模型转换与仿真库创建全流程
  • 从‘找得准’到‘找得全’:一文读懂目标检测中的AP与mAP
  • 【FI-GL 主数据实战】FS00总账科目创建:从零到一的企业财务基石配置
  • 深度学习实战:一致性评价方法的选择与应用(从皮尔森到Kappa)
  • 从字典构建到实战破解:Hydra与Medusa在渗透测试中的高效应用指南
  • MultiFunPlayer入门指南:3步掌握设备同步核心能力
  • Claude Code 用 grep,Cursor 用 RAG
  • MM配置实战-主数据-物料状态(OMS4)的精细化管控与业务场景解析
  • 实战电赛:从AD9959到AD9910,掌握DDS信号发生器的核心开发技巧
  • 迅为RK3568开发板Buildroot系统屏幕旋转全流程解析:从设备树配置到UI适配
  • Qt6数据类型深度解析:从qint8到double的跨平台精度与性能考量