已有文章中简单提到过,这里再以另外的视角更详细地描述整个过程
第一次写UE shader大概都会疑惑,usf、ush里面的那些“View“、“BasePass”Uniform变量到底是哪来的?好像没有include怎么直接写一个字符串就可以了,或者莫名其妙的include怎么也找不到的/Engine/Generated/UniformBuffers/xxx,还有为啥c++定义了一下它们就能绑定上?这些到底是怎么魔法般运行的?
我们知道每个FShader类会有一个static的ShaderType描述这个Shader类型
类似的,每个UniformBuffer也会有一个static的FShaderParametersMetadata来描述这个Parameter类型,通过BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT宏定义
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FMobileBasePassUniformParameters, )
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT_WITH_CONSTRUCTOR(FViewUniformShaderParameters, ENGINE_API)
然后通过
IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FMobileBasePassUniformParameters, "MobileBasePass");
去初始化这个FShaderParametersMetadata,在FShaderParametersMetadata的构造函数中会将自己加入的一个全局的链表中
声明成员时通过宏前后串联
最终在构造时通过GetMembers进入FShaderParametersMetadata的Members中
引擎启动PreInitPreStartupScene阶段会收集所有的UniformBuffer信息
大概步骤是
1.BuildShaderFileToUniformBufferMap
搜集所有ShaderType(FVertexFactoryType和FShaderType和MaterialTemplate.ush等部分特殊文件)的Shader路径(包括Include文件),字符串搜索的方式查找每个文件中用到了哪些UniformBuffer名称,建立一个usf、ush路径名到UniformBuffer名的映射关系
2.ShaderType::Initialize
再次遍历,构造每个ShaderType所引用的UniformBuffer(包括Include文件),存储在Type->ReferencedUniformBufferStructsCache中
注意以上计算Include文件时会忽略Generated路径
FCachedUniformBufferDeclaration实际上是一个由UniformBuffer名称到其声明(Declaration成员)的TMap
TMap<const TCHAR*, FCachedUniformBufferDeclaration> ReferencedUniformBufferStructsCache;
启动阶段收集了每个ShaderType所使用的UniformBuffer(TMap的Key)名称,它们的声明会在必要时(一般是材质编译时)生成,同样也是字符串
这对应最终shader中的内容
至于为什么会是注释,这里后面还会再提
得到Declaration后,在编译前会将Declaration加入到Environment成员IncludeVirtualPathToExternalContentsMap中
这里是Path映射到Contents,虚拟路径直接映射到内容,所以找不到真实文件是正常的
这里不详细介绍Shader编译的其它内容,简而言之,组装好信息后传给Compile模块(Worker或Block单线程编译)的是一个Input,其中主要也就包括FShaderCompilerEnvironment
以D3DCompile为例,在进入CompileD3DShader函数之后主要工作是根据Input信息合成PreprocessedShaderSource文本
前面的VirtualPath映射会在这里查找转换合成文本内容(mcpp会遍历包含头文件)
最终传给编译器的是一个合并了所有内容的字符串,长度,文件名,入口点等信息
值得一提的是,在提交给编译器前会对合并后的文本进行一个RemoveUniformBuffersFromSource的操作(UE5 SM6除外)
如UE注释所述,它会将所有类似View.WorldToClip的写法替换成View_WorldToClip,并将原先的Struct声明注释掉
这个替换方法是先得到View的成员比如WorldToClip再去搜索使用到View.WorldToClip的地方
所以如果不小心使用了一个Uniform结构不存在的成员,那么它不会进行替换,最终的编译报错同样会是 undeclared identifier xxx(结构名),这在不留意的时候可能会非常令人迷惑
以上过程UE4和UE5大同小异,除了个别类成员的名称有些许变化外整体逻辑差不多是一致的