从插件开始的UE渲染开发0x00: Shader路径重定向
UE 渲染开发一直以来都是个很蛋疼的话题, 拜屎山一般的渲染系统所赐, 想要加点什么东西基本就只有改源码一条路可走, 以至于网上搜十个改源码的文章可能九个都是教你怎么加 ShadingModel 或 MeshPass.
我自己也曾在这类重复且枯燥的工作上花费大量的时间, 深切感受到这玩意到底有多恶心. 而且源码修改一时爽, 后续维护火葬场, 将来如果要升级引擎版本, 面对茫茫多的 diff 那才叫一个痛不欲生.
所以这个系列从 UE 的插件系统(Plugin)入手, 尝试探索在不改动源码的情况下, 如何最大限度的自定义渲染.
虽说不改 C++ 源码, 但 Shader 该改还是要改. 不过直接去引擎路径下改 Shader 就很恶心, 可能我只想为当前项目改动某部分 Shader, 但引擎路径下 Shader 的改动却会影响到本地的所有项目. 如果能像 Unity 那样每个项目自己有一份 Shader 就好了——正确的, 我们就先来把这个功能做了.
在动工之前, 需要先理解 UE Shader 的路径引用原理.
UE shader 代码中 include 的文件路径并非真实的路径, 而是"虚拟路径". 比如在 Shader 中:
1 |
然而 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 编译
- PreInitPreStartupScreen()
- GEngineLoop.PreInit()
- EnginePreInit()
- GuardedMain()
- LaunchWindowsStartup()
在模块的 StartupModule()函数中拿到 GShaderSourceDirectoryMappings 并强行替换掉虚拟路径"/Engine"对应的 shader 实际路径即可.
1 | void FSnowyFalconModule::StartupModule() |