效果:

目录结构:
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. FSceneView 和 FViewInfo
这个区别主要来自接口层级,不是 Shader 类型:
FSceneView:较公开、通用的视图接口,View Extension 常用。FViewInfo:Renderer 内部使用,继承自FSceneView,包含更多渲染器内部数据。
因此 Compute Shader 和 Pixel Shader 都可以接收 FSceneView 或 FViewInfo,取决于调用位置。
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