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 Shader Compute Shader
输入 通常 SRV 通常 SRV
输出 通常 RTV 通常 UAV
执行方式 Draw 全屏三角形 Dispatch 线程组
像素范围 Viewport 自动裁剪 Shader 手动判断边界
硬件混合 支持 通常需要手动实现
随机访问 较受限制 灵活读写 UAV
Async Compute 不支持 条件满足时可支持
缩写 全称 权限/用途 常见阶段
SRV Shader Resource View Shader 只读 VS/PS/CS 等
UAV Unordered Access View Shader 随机读写 CS,偶尔 PS
RTV Render Target View 光栅化颜色输出 Pixel Shader
DSV Depth Stencil View 深度/模板读写 光栅化阶段,允许渲染管线写入
CBV Constant 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