一、动机
这篇文章次要是我对UE4中Shader编译过程以及变种的了解,理解这一块还是挺有必要的,毕竟动辄几千上万个Shader的编译在UE里几乎是粗茶淡饭。理解它底层的实现机制后心田虚浮一点,如果要去批改,大方向也不会错。
这部分工作是我之前就做好的,文章里波及外部批改的中央都被我阉割掉了。所以这篇文章次要用于常识遍及,分享给宽广被UE4中的Shader编译折磨的码农们,凑活着看,看完其实应该就理解了。
二、UE4中Shader的组织和获取
在讲具体的Shader编译过程时,先讲UE4的渲染过程,渲染过程中是怎么拿Shader的,最初再讲这些Shader是怎么生成的。
空幻引擎中讲到线程次要有三个:游戏线程、渲染线程和RHI线程。
其中咱们平时关怀的比拟多的就是游戏线程和渲染线程了,至于RHI线程偏差于底层硬件接口,是甚少关怀的,个别状况下也很少有须要改变到RHI线程的货色。
1. 渲染线程
空幻引擎在FEngineLoop::PreInit中对渲染线程进行初始化。
具体的地位是在StartRenderingThread函数外面,此时空幻引擎主窗口是尚未被绘制进去的,渲染线程的启动位于StartRenderingThread函数外面,这个函数大略做了以下几件事:
1)通过FRunnableThread::Create函数创立渲染线程
2)期待渲染线程筹备好从本人的TaskGraph取出工作并执行
3)注册渲染线程
4)创立渲染线程心跳更新线程
2. 渲染线程的运行
在UE4的体系中,渲染线程的次要执行内容在全局函数RenderingThreadMain(RenderingThread.cpp)中。
从实质上来讲他更像是一个员工,等着老板给他派工作,老板塞给他的工作都会放在TaskMap中,他则负责一直地提取这些工作去执行。
老板能够通过ENQUEUE_RENDER_COMMAND系列宏,给员工派发工作(增加到TaskMap中),下图阐明了这个过程:
具体代码调用实例如下,这个宏是在游戏线程中调用的,有时候游戏线程中有一些资源产生了变动,或者增加了一些新的资源,抑或是因为一些逻辑而要去改到渲染线程的一些操作,都须要有一种办法去告诉到渲染线程,就像是两艘并行飞驰的船,各自走本人的路,另一艘船上产生了什么是齐全不晓得的,而UE4就通过设置一系列宏为两艘船之间的通信提供了办法。
员工执行工作时也不是间接向GPU发送指令,而是将渲染命令增加到RHICommandList,也就是RHI命令列表中,由RHI线程一直取出指令,向GPU发送,并阻塞期待后果。
此时RHI线程尽管阻塞,然而渲染线程仍然失常工作,能够持续解决向RHI命令列表填充指令。
3. 渲染过程中Shader的起源及抉择
明确了上述那些概念咱们晓得,屏幕后果就像是咱们最终要做进去的产品,老板就像是产品经理,通知员工这个产品要怎么做,并交给员工对应的资源,员工依据这些资源,和老板的命令去实现最终的产品(绘制到屏幕上)。
首先讲这些资源在UE4中对应的是什么,以及员工在实现不同的工作阶段(绘制Pass)时是如何从这么多资源中拿到本人想要的资源的,再去讲这些资源的生成。
3.1 资源的组织:ShaderMap
那么屏幕上的画面到底是如何出现的呢?员工是怎么样去用这些资源的呢,换句话说就是老板给员工的资源,员工是怎么解决成最终能用的资源的?这些资源是怎么组织的?这里就波及到一个名词:ShaderMap。
用过空幻4的渲染的都晓得,空幻引擎中的着色器数量是十分宏大的,如果改变一个材质,常常就须要编几千个甚至上万个Shader,其实也就是说单个材质会编译出多个Shader,这一点是十分重要的。
用一个简略点的概念来了解ShaderMap,能够把它了解成一个三维矩阵,长度为每个材质类型,宽度为每个渲染阶段,高度为每个顶点工厂类型,矩阵的每一个方格都对应了一组着色器组合(顶点着色器,像素着色器),材质也不肯定参加全副阶段,所以这个三维矩阵中是存在有很多空缺的。
顶点工厂在UE4中的含意是负责形象顶点数据以供前面的着色器获取,从而让着色器可能疏忽因为顶点类型造成的差别,比如说一般的动态网格物体和应用GPU进行蒙皮的物体,二者的顶点数据不同,然而通过顶点工厂进行形象后,提供对立的数据获取接口,供前面的着色器应用。
3.2 资源的抉择:怎么从ShaderMap中拿到想要的Shader
当初是第二个问题,如何依据以后阶段,以后的材质类型,以后顶点工厂类型,从这个三维矩阵中取得须要的着色器组合。
以一个StaticMesh物体的渲染为例(动静物体不同),对着色器数据抉择的过程如下:
1)渲染线程把这个物体增加进场景AddToScene。
2)更新场景的动态物体绘制列表AddStaticMeshes。
3)调用CacheMeshDrawCommands,开始生成以后物体的绘制命令MeshDrawCommands并缓存住。
4)遍历所有的Rendering Pass类型,获取以后场景的CachedDrawLists生成Drawlistcontext。
5)调用不同Pass(以BasePass为例)的AddMeshBatch函数,并将Drawlistcontext作为参数传入(不便之后把生成的绘制命令缓存住)。
6)通过一系列参数判断该Mesh应不应该在以后Pass(BasePass为例)生成绘制命令,如果验证通过,那么调用以后Pass的Process函数。
7)获取该Mesh在以后Pass绘制须要的Shaders,绘制状态,光栅化状态,并最终生成该Mesh的绘制命令。
所以到这一步就讲清楚了渲染时怎么去拿Shader的流程,须要去看不同Pass的GetShaders函数,联合之前对ShaderMap的剖析来看它的传入参数,MaterialResource对应它应用的材质资源,VertexFactory的type对应所用到的顶点工厂类型,最初还有用到的顶点和像素着色器。
最终失去顶点着色器和像素着色器的调用如下(此时材质类型和渲染Pass曾经确定):
材质的GetShader函数首先以以后顶点工厂类型的ID为索引,通过GetMeshShaderMap函数从OrderedMeshShaderMaps成员变量中查问到对应顶点工厂类型的MeshShaderMap,随后调用以后MeshShaderMap的GetShader函数,以以后着色器类型为参数查问,查问到理论对应的着色器。
总结如下:本质上获取一组着色器组合须要的三个变量:渲染Pass、顶点工厂类型和材质类型,这也就不难理解UE4中对资源的组织模式了。
三、UE4中Shader的生成
1. MaterialShader的编译
在第二局部的内容中曾经说分明了UE4中Shader的组织模式以及具体是怎么去获取,那么接下来的问题就是如何去生成这些Shader,及材质如何编译,产生ShaderMap并缓存起来。
当HLSL代码生成后就须要进入到真正的着色器编译阶段。材质节点图生成的HLSL代码只是一批函数,并不具备残缺的着色器信息,这些代码会镶嵌到真正的着色器编译环境中(FShaderCompilerEnvironment),从新编译成最终的ShaderMap中每一个着色器,次要流程如下:
1)保留材质并编译以后材质,触发Shader编译,调用FMaterial::BeginCompileShaderMap()。
2)新建一个ShaderMap实例,调用HLSLTranslator把材质节点翻译成HLSL代码。
3)初始化着色器编译环境,FShaderCompilerEnvironment通过MaterialTraslator::GetMaterialEnvironment初始化实例,次要就是去设置宏。
3.1)依据以后Material的各种属性,初始化各种着色器宏定义,从而管制编译过程中的各种宏开关是否启动。
3.2)依据FHLSLMaterialTranslator在解析过程中得出以后的参数汇合,增加参数定义到环境中。
4)开始理论的编译工作
4.1)调用NewShaderMap的Compile函数:
a. 调用FMaterial::SetupMaterialEnvironment函数,设置以后的编译环境,这外面也会去设置各种宏定义。
b. 获取所有顶点工厂类型,对于每一种顶点工厂类型,查看该类型对应的ShaderMap是不是曾经被应用,如果被应用就去BeginCompile。
c. BeginCompile函数中会去遍历所有的ShaderType,两头会调到实例类的ModifyCompilationEnvironment,最终调用全局函数GlobalBeginCompileShader,这个全局函数会去填充FShaderCompileJob,包含设置shader格局、usf门路、注入宏等等。
d. 真正执行编译工作的是把所有FShaderCompileJob交给FShaderCompilingManager,并且让其马上执行编译并返回。
2. 如何实现Shader变种?
FMeshMaterialShaderType继承自FShaderType,他存有模板类的两个动态函数指针:ModifyCompilationEnvironment和ShouldCompilePermutation,因而每次遍历咱们都能够拜访到这两个函数。
上文中的C阶段会先调用ShouldCompilePermutation询问TMobileBasePassPS是否为以后Template、VertexFactory、Material组合编译Shader。
如果须要编译,则调用ModifyCompilationEnvironment注入该以后模板确定的宏,以此实现Shader的变种。
3. GlobalShader的编译
在应用编辑器的时候,常常会有须要改变到Shader文件,并且须要在编辑器中查看成果的需要,与材质编辑器中的材质Shader不一样,材质编辑器提供了编译按钮,对材质的改变都能够保留并编译出Shader保留到ShaderMap中,所以如果改变了目录下的Shader文件怎么通知引擎去帮咱们编译批改后的Shader。
空幻针对这个性能曾经提供了相应的指令:
recompileshaders changed ,recompileshaders global,recompileshaders material ,recompileshaders all,recompileshaders
如果不晓得这些指令,一个比拟死的方法天然是重启编辑器,让它重编改变过的Shader,当然也能够不重启编辑器来重编这些改变过的Shader,比方应用Recompileshaders changed,这里首先讲通过指令重编的办法,它的具体流程是怎么?
3.1 动静重编Shader 不须要重开编辑器
1)批改Shader文件,保留,在控制台输出Recompileshaders changed。
2)调用RecompileShaders,依据指令的内容进入不同的分支,先去匹配具体的命令内容。
3)寻找过期的Shader文件(改变过的Shader)。
4)如果以后对Shader文件(.usf)没有任何改变,间接返回No Shader changes found,如果有改变,调用BeginRecompileGlobalShaders。
a. 调用FlushRenderingCommands,期待渲染线程执行完所有挂起的渲染命令。
b. 依据以后平台失去GlobalShaderMap,GetGlobalShaderMap(ShaderPlatform),这里也能够看进去不同的ShaderType是存在不同的ShaderMap中的。
c. 从ShaderMap中移除过期的CurrentGlobalShaderType和ShaderPipline(顶点还是像素着色器等等..)的Shader。
d. 调用VerifyGlobalShaders重编ShaderMap中的Shader。
5)实现GlobalShader的重编,调用FinishRecompileGlobalShaders(),该函数会阻塞直到所有的Global Shaders被编译和处理完毕。
3.2 重开编辑器
1)在引擎的preinit函数中调用CompileGlobalShaderMap。
2)新建一个GlobalShaderMap实例。
3)查看Shader缓存DDC中的内容与设定的KeyString是否统一,如果不统一阐明缓存中对应局部的内容曾经生效了,UE就会去重编这部分内容(对应最开始说到的重编Shader问题),并且去从新生成这部分的DDC。
4)从DDC中反序列化进去GlobalShaderMap实例的内容。
5)接下来就是一些Shader资源的初始化操作。
3.3 UE4中材质Cook保留的是什么
所谓的Cook是指把平台无关的编辑向数据转化为特定平台运行时所需的数据,对于材质来说就是把上述的usf文件和材质连线编译成安卓运行时须要的GLSL源码。
1)Cook Commandlet会首先调用一个Package外面所有的UObject的BeginCacheForCookedPlatformData(const ITargetPlatform *TargetPlatform)办法,该办法由各个UObject派生类各自实现,目标是生成特定所需数据并缓存下来,对于材质来讲就是UMaterial的BeginCacheForCookedPlatformData。
a. 开始为指标平台缓存着色器,并将正在编译的材质资源存储到CachedMaterialResourcesForCooking中。
b. 为以后ShaderFormat/FeatureLevel、QualityLevel生成一个FMaterialResource数组,并调用CacheShadersForResources填充其内容。
2)之后Cook Commandlet会保留该Package,也就是是去执行到UMaterial外面的Serialize办法。
实际上后面局部提到的usf文件和材质连线都通过CacheShadersForResources被转化成了一个个FMaterialResource,所以FMaterialResource到底是什么货色?
在UMaterial能找到如下成员:
联合之前的剖析,不难得出UMaterial持有QualityLevelNum * FeatureLevelNum个FMaterialResource,能够通过QualityLevel和FeatureLevel索引到FMaterialResource。
FMaterialResource里有一个要害的成员FMaterialShaderMap,FMaterialShaderMap能够通过FVertexFactoryType::GetId()来索引到FMeshMaterialShaderMap;而FMeshMaterialShaderMap能够通过FShaderType来索引FShader。
因而FMaterialResource外面寄存的实际上是FShader的汇合,而FShader外面寄存的就是最终应用的Shader代码了。
文末,再次感激YiQiu的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/xie-jie-62-1,作者也是U Sparkle流动参与者,UWA欢送更多开发敌人退出U Sparkle开发者打算,这个舞台有你更精彩!