从插件开始的UE渲染开发0x00: Shader路径重定向

UE 渲染开发一直以来都是个很蛋疼的话题, 拜屎山一般的渲染系统所赐, 想要加点什么东西基本就只有改源码一条路可走, 以至于网上搜十个改源码的文章可能九个都是教你怎么加 ShadingModel 或 MeshPass.

我自己也曾在这类重复且枯燥的工作上花费大量的时间, 深切感受到这玩意到底有多恶心. 而且源码修改一时爽, 后续维护火葬场, 将来如果要升级引擎版本, 面对茫茫多的 diff 那才叫一个痛不欲生.

所以这个系列从 UE 的插件系统(Plugin)入手, 尝试探索在不改动源码的情况下, 如何最大限度的自定义渲染.

虽说不改 C++ 源码, 但 Shader 该改还是要改. 不过直接去引擎路径下改 Shader 就很恶心, 可能我只想为当前项目改动某部分 Shader, 但引擎路径下 Shader 的改动却会影响到本地的所有项目. 如果能像 Unity 那样每个项目自己有一份 Shader 就好了——正确的, 我们就先来把这个功能做了.

在动工之前, 需要先理解 UE Shader 的路径引用原理.

UE shader 代码中 include 的文件路径并非真实的路径, 而是”虚拟路径”. 比如在 Shader 中:

1
#include "/Engine/Private/Common.ush"

然而 Common.ush 文件真实路径是 ($LocalPath)/Engine/Shaders/Private/Common.ush , 这其中就是由 UE 做了一遍 Shader 虚拟路径到真实路径的映射.

把 shader 文件中使用的”/Engine”路径映射到了本地环境下的”($LocalPath)/Engine/Shaders”.

通过翻阅源码, 可以得知引擎在启动阶段会通过 AddShaderSourceDirectoryMapping()函数注册全部的 Shader 路径映射到一个全局变量 GShaderSourceDirectoryMappings 中:

ShaderCore.h

ShaderCore.cpp

包括各个插件的自定义 Shader 也均通过这个接口向引擎注册自己的 Shader:

MobileFSRModule.cpp

而引擎自己的内置 Shader 则是在 LaunchEngineLoop.cpp 中的 FEngineLoop::PreInitPreStartupScreen()函数中注册:

那这样就好办了, 只需要在引擎 Shader 注册之后、Shader 编译之前把”/Engine”对应的 shader 实际路径替换成项目自己魔改过后的 shader 路径即可.

而我们也正好可以通过插件中模块启动时的 StartupModule()函数做到这一点, 只需要将模块的 LoadingPhase 设为 PostConfigInit.

UE 启动调用链:

  • WinMain()
    • LaunchWindowsStartup()
      • GuardedMain()
        • EnginePreInit()
          • GEngineLoop.PreInit()
            • PreInitPreStartupScreen()
              • AddShaderSourceDirectoryMapping(TEXT(“/Engine”), FPlatformProcess::ShaderDir()) ← 引擎 Shader 虚拟路径注册
              • AppInit()
                • ProjectManager.LoadModulesForProject(ELoadingPhase::PostConfigInit)** ← PostConfigInit 模块加载**
              • CompileGlobalShaderMap() ← 全局 Shader 编译

在模块的 StartupModule()函数中拿到 GShaderSourceDirectoryMappings 并强行替换掉虚拟路径”/Engine”对应的 shader 实际路径即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FSnowyFalconModule::StartupModule()
{
auto&& ShaderSourceDirectoryMappings = const_cast<TMap<FString, FString>&>(AllShaderSourceDirectoryMappings());

FString ShaderRemappingRealDir = //项目自定义Shader路径
if (FPaths::DirectoryExists(ShaderRemappingRealDir))
{
ShaderSourceDirectoryMappings[EngineShaderVirtualDir] = ShaderRemappingRealDir;
}
}

void FSnowyFalconModule::ShutdownModule()
{
auto&& ShaderSourceDirectoryMappings = const_cast<TMap<FString, FString>&>(AllShaderSourceDirectoryMappings());
ShaderSourceDirectoryMappings[EngineShaderVirtualDir] = EngineShaderRealDir;
}