在虚幻引擎 4 中,Global Shaders 是一种即可以在 C++ 端使用,也能在渲染的后处理效果中使用,调度计算其他 Shaders,清除屏幕等(比如,那些并不工作于某个具体材质或者某个具体模型)。有些时候,为了达到想要的效果,更高级一些的功能需要被实现,并且要一个自定义的 Shader 处理过程。要实现这些也比较简单,我将在这篇文章中说明。
Unreal Shader Files 是什么,以及如何使用
UE4 会从 Engine/Shaders 目录读取 .usf 文件(Unreal Shader Files)。任何新的 Shader 都需要将相应的源文件放在该目录。在 4.17 中,Shader 也可以从插件中读取(Plugin/Shaders)。为了开发方便,我推荐在 ConsoleVariables.ini 文件中启用 r.ShaderDevelopmentMode=1。查看 这篇文档 来获取更多信息。
让我们先从在 Engine/Shaders 目录中添加一个新的 .usf 文件开始。我们把它命名为 MyTest.usf。然后添加一个简单的过程用于 Vertex Shader 和 Pixel Shader,并返回一个自定义颜色:
// MyTest.usf // Simple pass-through vertex shader void MainVS( in float4 InPosition : ATTRIBUTE0, out float4 Output : SV_POSITION ) { Output = InPosition; } // Simple solid color pixel shader float4 MyColor; float4 MainPS() : SV_Target0 { return MyColor; }
然后,为了让 UE4 能够读取并编译它,我们需要声明一个 C++ 类,我们先从 Vertex Shader 开始:
// This can go on a header or cpp file class FMyTestVS : public FGlobalShader { DECLARE_EXPORTED_SHADER_TYPE(FMyTestVS, Global, /*MYMODULE_API*/); FMyTestVS() { } FMyTestVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { } static bool ShouldCache(EShaderPlatform Platform) { return true; } };
有几点需要注意:
- 它需要是 FGlobalShader 的子类。这么做,它便会在 Global Shader Map 里(也就是说并不需要某个具体材质便能使用到它)。
- 用 DECLARE_EXPORTED_SHADER_TYPE() 这个宏可以生成导出信息,对序列化该 shader 类型是必须的。在需要的时候,第三个参数是代码模块的外部链接类型,该 shader 模块位于这个代码模块中。(比如,C++ 代码并不为与渲染器模块中)。
- 两个构造函数,分别是默认构造和用于序列化。
- ShouldCache() 函数需要用来判定该 Shader 是否需要在特定情形下进行编译(比如,我们应该并不希望在一个没有计算 Shader 能力的 RHI 上编译一个需要计算能力的 Shader)。
类的声明完成后,便可以将该 Shader Type 注册到 UE4 的列表中:
// This needs to go on a cpp file IMPLEMENT_SHADER_TYPE(, FMyTestVS, TEXT("MyTest"), TEXT("MainVS"), SF_Vertex);
这个宏将类型 (FMyTestVS) 映射到 .usf 文件 (MyTest.usf) 上,该 Shader 的入口 (MainVS),以及 frequency/shader stage (SF_Vertex)。并且它也使得该 Shader 在 ShouldCache() 方法返回 true 时,能够加入到编译列表中。
注意:无论是在 FGlobalShader 中添加了什么模块,该模块都必须在引擎启动前完成加载,否则会得到以下这样的警告:“Shader type was loaded after engine init, use ELoadingPhase::PostConfigInit on your module to cause it to load earlier.(引擎初始化后加载了 Shader Type,请在模块中实现 ELoadingPhase::PostConfigInit 以便更早的加载)”。目前并不能在游戏或编辑器启动后再加载动态模块来添加 Shader Type。
让我们在声明更有意思一些的 Pixel Shader:
class FMyTestPS : public FGlobalShader { DECLARE_EXPORTED_SHADER_TYPE(FMyTestPS, Global, /*MYMODULE_API*/); FShaderParameter MyColorParameter; FMyTestPS() { } FMyTestPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { MyColorParameter.Bind(Initializer.ParameterMap, TEXT("MyColor"), SPF_Mandatory); } static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment); // Add your own defines for the shader code OutEnvironment.SetDefine(TEXT("MY_DEFINE"), 1); } static bool ShouldCache(EShaderPlatform Platform) { // Could skip compiling for Platform == SP_METAL for example return true; } // FShader interface. virtual bool Serialize(FArchive& Ar) override { bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar); Ar << MyColorParameter; return bShaderHasOutdatedParameters; } void SetColor(FRHICommandList& RHICmdList, const FLinearColor& Color) { SetShaderValue(RHICmdList, GetPixelShader(), MyColorParameter, Color); } }; // Same source file as before, different entry point IMPLEMENT_SHADER_TYPE(, FMyTestPS, TEXT("MyTest"), TEXT("MainPS"), SF_Pixel);
在这个类中,我们从 .usf 文件内暴露出 Shader 参数 MyColor:
- 在这个类中的 FShaderParameter MyColorParameter 成员变量,在运行时用于处理绑定关系,并能实时修改参数的数值。
- 在序列化构造函数中,Bind() 函数将该参数通过名字绑定到 ParameterMap 上,这里必须和 .usf 文件名保持一致。
- 新的 ModifyCompilationEnvironment() 函数用于当同一个 C++ 类定义了不同的行为,并能在 shader 中设置 #define 数值。
- Serialize() 方法是必须的。在序列化的过程中,在 SHader 绑定的编译、Cook 中从该实现内实时的读取或者写回数据。
- 最后,我们有一个自定义的 SetColor() 函数,示例了如何在运行时设置 MyColor 这一参数。
现在,让我们写一个简单的函数,利用这些 Shader Types 来画一个全屏的方块:
void RenderMyTest(FRHICommandList& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, const FLinearColor& Color) { // Get the collection of Global Shaders auto ShaderMap = GetGlobalShaderMap(FeatureLevel); // Get the actual shader instances off the ShaderMap TShaderMapRefMyVS(ShaderMap); TShaderMapRef MyPS(ShaderMap); // Declare a bound shader state using those shaders and apply it to the command list static FGlobalBoundShaderState MyTestBoundShaderState; SetGlobalBoundShaderState(RHICmdList, FeatureLevel, MyTestBoundShaderState, GetVertexDeclarationFVector4(), *MyVS, *MyPS); // Call our function to set up parameters MyPS->SetColor(RHICmdList, Color); // Setup the GPU in prep for drawing a solid quad RHICmdList.SetRasterizerState(TStaticRasterizerState ::GetRHI()); RHICmdList.SetBlendState(TStaticBlendState<>::GetRHI()); RHICmdList.SetDepthStencilState(TStaticDepthStencilState ::GetRHI(), 0); // Setup the vertices FVector4 Vertices[4]; Vertices[0].Set(-1.0f, 1.0f, 0, 1.0f); Vertices[1].Set(1.0f, 1.0f, 0, 1.0f); Vertices[2].Set(-1.0f, -1.0f, 0, 1.0f); Vertices[3].Set(1.0f, -1.0f, 0, 1.0f); // Draw the quad DrawPrimitiveUP(RHICmdList, PT_TriangleStrip, 2, Vertices, sizeof(Vertices[0])); }
如果想要测试一下这个代码,可以声明一个控制台变量,能像下面所示的这样在运行是开关:
static TAutoConsoleVariableCVarMyTest( TEXT("r.MyTest"), 0, TEXT("Test My Global Shader, set it to 0 to disable, or to 1, 2 or 3 for fun!"), ECVF_RenderThreadSafe ); void FDeferredShadingSceneRenderer::RenderFinish(FRHICommandListImmediate& RHICmdList) { [...] // *** // Inserted code, just before finishing rendering, so we can overwrite the screen’s contents! int32 MyTestValue = CVarMyTest.GetValueOnAnyThread(); if (MyTestValue != 0) { FLinearColor Color(MyTestValue == 1, MyTestValue == 2, MyTestValue == 3, 1); RenderMyTest(RHICmdList, FeatureLevel, Color); } // End Inserted code // *** FSceneRenderer::RenderFinish(RHICmdList); [...] }
到此为止,便能够测试这个新的 Global Shader 了。运行项目,通过波浪号(~)打开控制台,并输入 r.MyTest 1。然后输入 r.MyTest 2 或者在试一下 r.MyTest 3 来改变颜色。输入 r.MyTest 0 来禁用这个处理过程。
调试自动生成的代码
如果想要调试 .usf 文件编译,或者查看生成的文件的话,请浏览这篇博客文章:调试 Shader 编译过程。
额外内容!
在运行非cook 版本的游戏或者编辑器时,可以实时修改 .usf 文件,并用热键 Ctrl+Shift+. (period) 或者在控制台输入 recompileshaders changed,便能重新读取并构建 shader,以做到快速开发迭代!
祝各位愉快!
名词说明
Global Shaders - 那些并非由材质编辑器创建的 Shader,通常是 compute shader 或者 post-process shader。这是基础类型的一种(比如:Global,Material,Mesh),其他一些基础类型将来再介绍。
FShaderType - 有 Shader 代码定义的一个“模板”或者“类”,通过代码中的定义,映射到一个具体的 C++ 类上。
FShaderResource - 编译后的 Shader 中间码以及它运行时 RHI 的资源。
FShader - 编译后的一个 FShaderType 的实例,根据一个 FShaderResource 的信息编译获得。
TShaderMap - 一组不同的 FShaderType 组成的集合,但有着相同的元信息(Global)。