材质与着色器编译

自动关联目录:材质与着色器编译

官方依据:Shader Development(UE 5.8 Documentation)。本页覆盖该页全部 17 个条目。PSO 相关的另一半(首帧卡顿的直接成因)见 PSO 预缓存。

1. 定位

本页回答"一个材质是怎么变成 GPU 上的着色器的",以及"为什么编译这么慢"。

一句话定位:UE 的编译是异步流式的——材质加载时把编译请求入队,编译结果一可用就套上去,不阻塞引擎。这解释了几乎所有相关的现象:为什么编辑器里物体一开始是默认材质、为什么打包后第一次启动会卡。

2. 结构:三层着色器

类型定义编译份数例子
Global Shader操作固定几何(如全屏四边形),不与材质打交道任何给定类型在内存里只有一份阴影过滤、后处理
FMaterialShaderTypePass 专用,需要材质属性但不需要网格属性每个材质一份Light Function pass
FMeshMaterialShaderTypePass 专用,既依赖材质属性又依赖网格类型每个材质 × 每个 Vertex Factory 一份TBasePassVS / TBasePassPS

Vertex Factory

Vertex Factory 是"材质能应用到不同网格类型"的机制:FVertexFactoryType 代表一种网格类型,FVertexFactory 实例持有该类型所需的每实例数据(如 FGPUSkinVertexFactory 持有骨骼矩阵与顶点缓冲引用)。

它的着色器代码是被各 pass 着色器用来抽象网格类型差异的隐式接口。关键函数:

函数作用
FVertexFactoryInput顶点着色器需要什么输入(必须与 C++ 侧的顶点声明匹配)
FVertexFactoryIntermediates缓存多个 VF 函数共用的中间数据(如 TangentToLocal 矩阵)
FVertexFactoryInterpolantsVSToPSVS 传给 PS 的数据
VertexFactoryGetWorldPosition取世界空间位置:静态网格只做 LocalToWorld 变换;GPU 蒙皮网格先蒙皮再变换
VertexFactoryGetInterpolantsVSToPS转成插值量
GetMaterialPixelParameters在 PS 里把 VF 特有的插值量转成 FMaterialPixelParameters

FMaterialShaderMap:变体是从哪来的

FMaterialShaderMap
   ├── FLightFunctionPixelShader          (FMaterialShaderType)
   ├── FLocalVertexFactory                (FVertexFactoryType)
   │      ├── TDepthOnlyPS                (FMeshMaterialShaderType)
   │      ├── TDepthOnlyVS
   │      ├── TBasePassPS
   │      └── TBasePassVS
   └── FGPUSkinVertexFactory
          └── (同样一套)

这是一个稀疏矩阵:Vertex Factory 依据自身的 ShouldCache(取决于材质用途,如 bUsedWithSkeletalMesh 为真就纳入 GPU skin VF)加入;FMeshMaterialShaderType 依据自己的 ShouldCache(取决于材质与 VF 属性)加入。

官方的说明很坦白:

这种稀疏矩阵方式很快就会累积出大量的着色器,占用内存并增加编译时间。相比"存一份真正需要用到的着色器列表"的做法,它的主要优势是不需要生成那份列表——因此在主机上,运行时需要的着色器总是已经提前编译好了。

UE 用两个办法缓解:

  • 内存问题 → 着色器压缩
  • 编译时间问题 → 多核着色器编译

3. 编译机制

异步流式编译

材质加载(没有已缓存的 shader map)
   → 编译请求入队
   → Shader Compile Worker 进程编译
   → 结果可用时套用(不阻塞引擎)

为什么用独立的 Worker 进程:平台的着色器编译函数(如 D3DCompile)内部常含临界区,在单个进程里无法多核扩展。所以真正的编译工作放在 Shader Compile Workers 辅助进程里做。

缓存与 Cook

阶段行为
编译完成存入 DDC
缓存 key包含着色器源文件在内的所有编译输入的哈希 → 改了 .usf 会自动被识别
Cook材质着色器内联进材质的包;全局着色器单独存一份全局着色器文件,以便引擎启动早期加载

改 FShader 的 Serialize 函数不需要处理向后兼容——只要在被引用的着色器文件里加个空格即可(key 变了就重编)。

4. 参数与控制台变量

CVar / 设置作用备注
r.ShaderDevelopmentMode设为 1:开启出错重试与着色器开发相关日志/警告建议写进 ConsoleVariables.ini,每次启动都生效
recompileshaders changed / Ctrl+Shift+.重编译改动过的着色器改了 .usf 并保存后执行
r.DumpShaderDebugInfo=1 把所有被编译的着色器导出到磁盘输出到 GameName/Saved/ShaderDebugInfo,含源文件/includes、预处理版本、可复现编译的 bat。一直开着会塞满硬盘
bAllowCompilingThroughWorkers([DevOptions.Shaders])是否启动 SCW 调编译器 DLL关掉 → 单核编译
bAllowAsynchronousShaderCompiling是否在 UE 内的另一线程编译关掉 → 编译阻塞

想直接单步进编译器 DLL(如 CompileD3D11Shader),把上面两个都设为 false,并确保其它着色器都已缓存(编译会非常慢)。

特殊引擎材质

UMaterial::bUsedAsSpecialEngineMaterial:允许材质用于任何 vertex factory 类型 → 所有 VF 都会与该材质一起编译,集合非常大。适用于:

用途例子
视图模式用材质lighting only 等
编译出错时的回退材质DefaultDecalMaterial、DefaultMaterial
用来减少需要缓存的着色器数量不透明材质的 depth-only 输出与 DefaultMaterial 相同 → 直接用 DefaultMaterial 的着色器,该材质跳过 depth-only 着色器的缓存

5. 迭代与调试

场景最快的方式
改 global shaderrecompileshaders changed / Ctrl+Shift+.**;编译太慢就在 ModifyCompilationEnvironment 里指定 CFLAG_StandardOptimization
改 material shader(如 BasePassPixelShader.usf)在单个材质上迭代更快——材质编辑器点 Apply 会重新读盘并只重编该材质
改了被大量着色器 include 的文件(如 common.usf)会很久,要有心理准备
调试着色器改着色器输出中间量,再用 VisualizeTexture 看。例如验证世界坐标:OutColor = frac(WorldPosition / 1000);

输出中间值的方式对构建复杂数据结构的着色器扩展性不好——那种要靠 r.DumpShaderDebugInfo 拿预处理产物。

编译出错重试

开了 r.ShaderDevelopmentMode 后:

  • Debug + 调试器附加 → 命中断点,错误输出在 VS 输出窗口,双击错误日志直接跳到出错行
  • 否则弹 Yes/No 对话框

对 global shader 尤其重要——它们编译失败是 fatal error。

HLSL 交叉编译器

HLSL Cross Compiler 在离线着色器编译阶段把 HLSL 自动转成 GLSL(OpenGL 平台),让着色器只写一次;同时做一些 OpenGL 驱动常缺失的优化。

AsyncCompute

部分 API / GPU 支持的硬件特性,让任务交错执行以更好利用 GPU 硬件单元。

注意:异步计算开着时 GPU 计时不可靠——分析时要 r.RDG.AsyncCompute 0(仅用于分析,发布版绝不能关,见 虚拟阴影贴图)。

6. 代价与权衡

设计收益代价什么时候不该用
稀疏矩阵缓存主机上无需生成列表,运行时要的都已编译好着色器数量膨胀 → 内存 + 编译时间内存极紧的平台要压缩与裁剪
异步编译加载不被阻塞物体先以默认材质/跳过绘制出现需要"一定立刻正确"的场景要配 PSO 预缓存等待
SCW 多进程绕开编译器 DLL 的临界区,能多核额外进程与内存调试时反而要关掉
特殊引擎材质减少缓存数量该材质变体爆炸式增长只用于官方列出的三类场景
DDC key 含源码哈希改源码自动失效任何源码改动都触发重编大项目必须配共享 DDC

7. 踩坑与排查

坑现象怎么验证
改了 .usf 没重编行为没变是否执行了 recompileshaders changed
改了 common.usf编译巨慢该文件被大量着色器 include,正常
一直开 r.DumpShaderDebugInfo硬盘被塞满几万个小文件检查 Saved/ShaderDebugInfo
编译出错直接崩global shader 编译失败是 fatal开 r.ShaderDevelopmentMode 拿重试与日志
内存占用高着色器数量膨胀检查是否误设 bUsedAsSpecialEngineMaterial;用着色器压缩
每次换机器都重编DDC 未共享配共享 DDC(见 Cook 与打包流程)
首帧/切场景卡顿PSO 未预缓存走 PSO 预缓存
编辑器里物体先是默认材质异步编译未就绪设计如此;发布版靠 PSO 预缓存 + 加载屏等待
GPU 计时忽高忽低异步计算开着r.RDG.AsyncCompute 0 再测(只用于分析)

8. 排查顺序

1. 卡在哪?编译慢 → 共享 DDC;运行时卡 → PSO 预缓存
2. 编译错误 → r.ShaderDevelopmentMode = 1,看重试与日志
3. 着色器结果不对 → 输出中间量 + VisualizeTexture
4. 内存大 → 查变体数量与特殊引擎材质
5. 首次启动体验 → -clearPSODriverCache 复现(见 PSO 篇)

参考