UE4使用插件创建Global Shader

UE4的Global Shader在很久之前的版本就有了,并且底层的渲染管线也是使用Global Shader渲染Light Shaft、Volumetric Fog等等。而且由于版本的更迭,很多设置参数的方式和调用的函数都渐渐改变。虽然旧的一些特性依然使用原来的方式,但新的一些特性如4.24的大气系统都开始使用新的方式。本文记录的就是4.24版本使用较新的方式设置参数,传递参数以及调用函数的方法。

创建插件

使用UE4.24版本创建空插件模板,命名CustomGlobalShader。
1

之后会将渲染逻辑和游戏逻辑分别写在两个模块中,CustomGlobalShader.uplugin中填写需要加载的模块。
2

为两个模块分别创建两个文件夹,并且创建Shaders文件夹作为shader路径
3

ShaderDeclaration模块

创建模块

首先修改.Build.cs文件,添加依赖的模块,如Renderer, RendererCore, RHI和Projects。

1
2
3
4
5
6
7
8
9
10
11
12
using UnrealBuildTool;

public class ShaderDeclaration : ModuleRules
{
public ShaderDeclaration(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PrivateDependencyModuleNames.AddRange( new string[]
{ "Core", "CoreUObject", "Engine", "Renderer", "RenderCore", "RHI", "Projects" });
}
}

ShaderDeclaration.h

因为在插件中已经有了模块接口,直接用它与Renderer交互即可。并且提供了将游戏模块与渲染hook解耦的优点,允许更安全和简单的清理。随着4.22的到来,API中可以看到Renderer被大改成更现代的使用方式,就像DX12一样。最大的区别是现在必须将渲染代码封装到一个Render Graph或者Render Pass中。两者各有优点。

  • Render graphs
    在UE4中,如果要处理较大的渲染作业并使用引擎池的渲染目标,通常会使用graph。由于引擎现在为了渲染任务专门使用任务graph。使用前从案例中学习也是更简单正确的方式。然而与UObject渲染资源(如Utextures)交互时就非常难用,所以可以使用Render pass来代替。graph通常在应对大体量作业时比较好,对于小规模的作业可以使用Render pass。

  • Render passes
    Passes与之前的图形API类似,现在使用光栅化的时候会使用Pass。现在可以创建一个渲染Pass,而不是为光栅化操作设置渲染目标。不使用光栅化的操作(如计算、拷贝和其他操作)可以像之前一样直接使用RHICommandList。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

#include "Runtime/Engine/Classes/Engine/TextureRenderTarget2D.h" // UTextureRenderTarget2D
#include "RenderGraphResources.h" // IPooledRenderTarget

struct FShaderUsageParameters
{
UTextureRenderTarget2D* RenderTarget;
FColor InputColor;

FIntPoint GetRenderTargetSize() const
{
return CachedRenderTargetSize;
}

FShaderUsageParameters() {}
FShaderUsageParameters(UTextureRenderTarget2D* InRenderTarget)
: RenderTarget(InRenderTarget)
, InputColor(FColor::White)
{
CachedRenderTargetSize = RenderTarget ? FIntPoint(RenderTarget->SizeX, RenderTarget->SizeY) : FIntPoint::ZeroValue;
}

private:
FIntPoint CachedRenderTargetSize;
};

class SHADERDECLARATION_API FShaderDeclarationModule : public IModuleInterface
{
public:
static inline FShaderDeclarationModule& Get()
{
return FModuleManager::LoadModuleChecked<FShaderDeclarationModule>("ShaderDeclaration");
}
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("ShaderDeclaration");
}

virtual void StartupModule() override;
virtual void ShutdownModule() override;

public:
// 想hook renderer开始渲染时可以调用该方法。每帧都会执行着色器。
void BeginRendering();

// 结束时,调用停止绘制函数
void EndRendering();

// 需要分享新参数时就调用该方法。可以在保存locking和GPU传输时的不同间隙设置它来更新不同的属性集
void UpdateParameters(FShaderUsageParameters& DrawParameters);

private:
TRefCountPtr<IPooledRenderTarget> UserRenderTarget;
FShaderUsageParameters CachedShaderUsageParameters;
FDelegateHandle OnPostResolvedSceneColorHanndle;
FCriticalSection RenderEveryFrameLock;
volatile bool bCachedParametersValid;

void PostResolveSceneColor_RenderThread(FRHICommandListImmediate& RHICmdList, class FSceneRenderTargets& SceneContext);
void Draw_RenderThread(const FShaderUsageParameters& DrawParameters);
};

ShaderDeclaration.cpp

游戏线程会调用以下的方法,开始渲染、停止渲染、更新参数等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include "ShaderDeclaration.h"
#include "SimpleColor.h"

#include "Misc/Paths.h" // FPath
#include "Interfaces/IPluginManager.h" // IPluginManager

#include "RenderGraphBuilder.h" // Debug stat macro
#include "RenderTargetPool.h" // GRenderTargetPool


IMPLEMENT_MODULE(FShaderDeclarationModule, ShaderDeclaration)

// 声明一些GPU统计数据,后续方便追踪
DECLARE_GPU_STAT_NAMED(GlobalShaderPlugin_Render, TEXT("GlobalShaderPlugin: Root Render"));
DECLARE_GPU_STAT_NAMED(GlobalShaderPlugin_Pixel, TEXT("GlobalShaderPlugin: Render Pixel Shader"));

void FShaderDeclarationModule::StartupModule()
{
OnPostResolvedSceneColorHanndle.Reset();
bCachedParametersValid = false;

// 映射虚拟的着色器资源路径到插件实际的着色器路径
FString GlobalShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("CustomGlobalShader"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/CustomShaders"), GlobalShaderDir);
}

void FShaderDeclarationModule::ShutdownModule()
{
EndRendering();
}

void FShaderDeclarationModule::BeginRendering()
{
if (OnPostResolvedSceneColorHanndle.IsValid())
{
return;
}

bCachedParametersValid = false;

// 获取Renderer模块
const FName RendererModuleName("Renderer");
IRendererModule* RendererModule = FModuleManager::GetModulePtr<IRendererModule>(RendererModuleName);
if (RendererModule)
{
OnPostResolvedSceneColorHanndle = RendererModule->GetResolvedSceneColorCallbacks().AddRaw(this, &FShaderDeclarationModule::PostResolveSceneColor_RenderThread);
}
}

void FShaderDeclarationModule::EndRendering()
{
if (!OnPostResolvedSceneColorHanndle.IsValid())
{
return;
}

const FName RendererModuleName("Renderer");
IRendererModule* RendererModule = FModuleManager::GetModulePtr<IRendererModule>(RendererModuleName);
if (RendererModule)
{
RendererModule->GetResolvedSceneColorCallbacks().Remove(OnPostResolvedSceneColorHanndle);
}

OnPostResolvedSceneColorHanndle.Reset();
}

void FShaderDeclarationModule::UpdateParameters(FShaderUsageParameters& DrawParameters)
{
RenderEveryFrameLock.Lock();
CachedShaderUsageParameters = DrawParameters;
bCachedParametersValid = true;
RenderEveryFrameLock.Unlock();
}

void FShaderDeclarationModule::PostResolveSceneColor_RenderThread(FRHICommandListImmediate& RHICmdList, class FSceneRenderTargets& SceneContext)
{
if (!bCachedParametersValid)
{
return;
}

// 根据数据可以选择是否锁,添加此代码只是为了演示如何锁
RenderEveryFrameLock.Lock();
FShaderUsageParameters Copy = CachedShaderUsageParameters;
RenderEveryFrameLock.Unlock();
Draw_RenderThread(Copy);
}

void FShaderDeclarationModule::Draw_RenderThread(const FShaderUsageParameters& DrawParameters)
{
check(IsInRenderingThread());
if (!DrawParameters.RenderTarget)
{
return;
}

FRHICommandListImmediate& RHICmdList = GRHICommandList.GetImmediateCommandList();

QUICK_SCOPE_CYCLE_COUNTER(STAT_GlobalShaderPlugin_Render); // 为UE4前端收集CPU分析数据
SCOPED_DRAW_EVENT(RHICmdList, GlobalShaderPlugin_Render); // 为分析GPU活动添加的元数据,可以用RenderDoc这种工具查看

if (!UserRenderTarget.IsValid())
{
// 创建缓存RT描述符,FPooledRenderTarget是渲染线程允许共享和纹理可视化的渲染目标。
FPooledRenderTargetDesc UserRenderTargetDesc(FPooledRenderTargetDesc::Create2DDesc(DrawParameters.GetRenderTargetSize(), PF_R32_UINT, FClearValueBinding::None, TexCreate_None, TexCreate_ShaderResource | TexCreate_UAV, false));
UserRenderTargetDesc.DebugName = TEXT("GlobalShaderPlugin_UserRenderTarget");
// 如果旧元素依然有效则返回true, 分配了新元素则返回false
GRenderTargetPool.FindFreeElement(RHICmdList, UserRenderTargetDesc, UserRenderTarget, TEXT("GlobalShaderPlugin_UserRenderTarget"));
}

FSimpleColor::DrawToRenderTarget_RenderThread(RHICmdList, DrawParameters, UserRenderTarget->GetRenderTargetItem().TargetableTexture);
}

SimpleColor.h

声明一个简单着色器的绘制函数

1
2
3
4
5
6
7
8
9
10
#pragma once

#include "CoreMinimal.h"
#include "ShaderDeclaration.h"

class FSimpleColor
{
public:
static void DrawToRenderTarget_RenderThread(FRHICommandListImmediate& RHICmdList, const FShaderUsageParameters& DrawParameters, FTextureRHIRef UserRenderTarget);
};

SimpleColor.cpp

此处会创建两个Global Shader,分别是顶点着色器和像素着色器。并且通过IMPLEMENT_GLOBAL_SHADER()进行绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include "SimpleColor.h"

#include "Containers/DynamicRHIResourceArray.h" // TResourceArray
#include "Runtime/RenderCore/Public/PixelShaderUtils.h" // FFilterVertex

// 基础的静态顶点缓冲
class FSimpleScreenVertexBuffer : public FVertexBuffer
{
public:
// 为渲染资源初始化RHI
void InitRHI()
{
// FFilterVertex 表示顶点数据是用来过滤纹理的, VERTEXBUFFER_ALIGNMENT 是内存分配时的对齐方式
TResourceArray<FFilterVertex, VERTEXBUFFER_ALIGNMENT> Vertices;
Vertices.SetNumUninitialized(6);

Vertices[0].Position = FVector4(-1, 1, 0, 1);
Vertices[0].UV = FVector2D(0, 0);

Vertices[1].Position = FVector4(1, 1, 0, 1);
Vertices[1].UV = FVector2D(1, 0);

Vertices[2].Position = FVector4(-1, -1, 0, 1);
Vertices[2].UV = FVector2D(0, 1);

Vertices[3].Position = FVector4(1, -1, 0, 1);
Vertices[3].UV = FVector2D(1, 1);

// 创建顶点缓冲区, 用上方创建的初始数据填充该缓冲
FRHIResourceCreateInfo CreateInfo(&Vertices);
VertexBufferRHI = RHICreateVertexBuffer(Vertices.GetResourceDataSize(), BUF_Static, CreateInfo);
}
};
// 声明一个顶点缓冲
TGlobalResource<FSimpleScreenVertexBuffer> GSimpleScreenVertexBuffer;

// 基础的顶点着色器
class FSimpleColorVS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FSimpleColorVS);

static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}

static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
}

FSimpleColorVS() {}
FSimpleColorVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) {}
};

// 像素着色器,参数作为cpp和HLSL的桥梁
class FSimpleColorPS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FSimpleColorPS);
SHADER_USE_PARAMETER_STRUCT(FSimpleColorPS, FGlobalShader);
// 和游戏线程的参数一一对应
BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
SHADER_PARAMETER(FVector4, InputColor)
END_SHADER_PARAMETER_STRUCT()

static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}

static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
}
};

// 告诉引擎创建着色器以及着色器的入口位置
// 着色器类型 着色器路径 着色器函数名 类型
IMPLEMENT_GLOBAL_SHADER(FSimpleColorVS, "/CustomShaders/PixelShader.usf", "MainVertexShader", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FSimpleColorPS, "/CustomShaders/PixelShader.usf", "MainPixelShader", SF_Pixel);


void FSimpleColor::DrawToRenderTarget_RenderThread(FRHICommandListImmediate& RHICmdList, const FShaderUsageParameters& DrawParameters, FTextureRHIRef UserRenderTarget)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_CustomGlobalShaderPlugin_PixelShader); // 为UE4前端收集CPU分析数据
SCOPED_DRAW_EVENT(RHICmdList, CustomGlobalShaderPlugin_Pixel); // 为分析GPU活动添加的元数据,可以用RenderDoc这种工具查看

FRHIRenderPassInfo RenderPassInfo(DrawParameters.RenderTarget->GetRenderTargetResource()->GetRenderTargetTexture(), ERenderTargetActions::Clear_Store);
RHICmdList.BeginRenderPass(RenderPassInfo, TEXT("CustomGlobalShaderPlugin_OutputToRenderTarget"));

auto ShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
TShaderMapRef<FSimpleColorVS> VertexShader(ShaderMap);
TShaderMapRef<FSimpleColorPS> PixelShader(ShaderMap);

// 设置图形管线状态
FGraphicsPipelineStateInitializer GraphicsPSOInit;
RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*VertexShader);
GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*PixelShader);
GraphicsPSOInit.PrimitiveType = PT_TriangleStrip;
SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);

// 设置像素着色器颜色,将游戏线程的参数传进来
FSimpleColorPS::FParameters PassParameters;
PassParameters.InputColor = FVector4(DrawParameters.InputColor.R, DrawParameters.InputColor.G, DrawParameters.InputColor.B, DrawParameters.InputColor.A) / 255.0f;
// 提交参数到RHI命令列表
SetShaderParameters(RHICmdList, *PixelShader, PixelShader->GetPixelShader(), PassParameters);

// 绘制
RHICmdList.SetStreamSource(0, GSimpleScreenVertexBuffer.VertexBufferRHI, 0);
RHICmdList.DrawPrimitive(0, 2, 1);

// 完成渲染目标
RHICmdList.CopyToResolveTarget(DrawParameters.RenderTarget->GetRenderTargetResource()->GetRenderTargetTexture(), DrawParameters.RenderTarget->GetRenderTargetResource()->TextureRHI, FResolveParams());

RHICmdList.EndRenderPass();
}

PixelShader.usf

着色器的声明模块基本结束。需要创建与之对应的着色器文件.usf放在Shaders文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// VERTEX SHADER
void MainVertexShader(float4 InPosition : ATTRIBUTE0, float2 InUV : ATTRIBUTE1, out float2 OutUV : TEXCOORD0, out float4 OutPosition : SV_POSITION)
{
OutPosition = InPosition;
OutUV = InUV;
}

// PIXEL SHADER
float4 InputColor;

void MainPixelShader(in float2 uv : TEXCOORD0, out float4 OutColor : SV_Target0)
{
OutColor = InputColor;
}

着色器应用模块

创建模块

首先修改.Build.cs文件,添加依赖的模块,如上面创建的ShaderDeclaration

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnrealBuildTool;

public class ShaderUsage : ModuleRules
{
public ShaderUsage(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicDependencyModuleNames.AddRange(
new string[]
{ "Core", "CoreUObject", "Engine", "RHI", "ShaderDeclaration" });
}
}

ShaderUsage.h

基本的模块功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"


class FShaderUsageModule : public IModuleInterface
{
public:
static inline FShaderUsageModule& Get()
{
return FModuleManager::LoadModuleChecked<FShaderUsageModule>("ShaderUsage");
}
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("ShaderUsage");
}
};

ShaderUsage.cpp

1
2
3
#include "ShaderUsage.h"

IMPLEMENT_MODULE(FShaderUsageModule, ShaderUsage)

TestActor.h

创建一个测试Actor类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

UCLASS()
class ATestActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GlobalShader)
FColor InputColor;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GlobalShader)
class UTextureRenderTarget2D* UserRenderTarget;

public:
virtual void BeginPlay() override;
virtual void BeginDestroy() override;
virtual void Tick(float DeltaTime) override;
};

TestActor.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "TestActor.h"
#include "ShaderDeclaration.h"

void ATestActor::BeginPlay()
{
Super::BeginPlay();
FShaderDeclarationModule::Get().BeginRendering();
}

void ATestActor::BeginDestroy()
{
FShaderDeclarationModule::Get().EndRendering();
Super::BeginDestroy();
}

void ATestActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

FShaderUsageParameters DrawParameters(UserRenderTarget);
{
DrawParameters.InputColor = InputColor;
}

FShaderDeclarationModule::Get().UpdateParameters(DrawParameters);
}

目前应用模块也已经完成,编译完成后,在引擎中测试效果。

测试结果

首先以TestActor为父类创建蓝图类。然后创建一个RT,和一个使用该RT的材质。蓝图类中添加一个Box,并使用该测试材质。EventTick每帧传入随机的颜色,Play一下便可以看到颜色不断变化的Box了。
4 5 6 7

总要恰饭的嘛