[ue4] 材质和Shader变体(Shader Permutation)
Posted ZJU_fish1996
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ue4] 材质和Shader变体(Shader Permutation)相关的知识,希望对你有一定的参考价值。
[本文大纲] 材质种类 材质与Shader的关系 材质与材质实例 会生成哪些材质变体 多个Shader变体的原因 Shader变体的优化 Shader编译的过程 材质包体和内存 |
材质种类
ue4中的材质主要有两个类型,一种是与mesh相关的,比如物体的表面材质;另一种是mesh无关的,比如后处理材质。
材质与Shader的关系
材质属于美术资产,ue4底层会将材质节点翻译成HLSL代码,并根据模板HLSL代码和相关宏编译成最终的Shader Code。
对于mesh无关的材质而言,通常只对应着一个Shader。
对于mesh相关的材质而言,则可能会生成非常多的Shader Code,我们称之为Shader变体。
Shader变体的膨胀会导致包体增加,内存占用增加。
材质与材质实例
对于场景中的物体,我们应该将材质设计为母材质和对应材质实例的形式。母材质描述的是一类材质,当前类材质仅在部分参数上有差异。
ue4中内置的PBR材质一方面的意义在于,它提供了基于物理的真实材质表现;另一方面,就在于它提供了一个通用接口,能够使用一个模型表达丰富多彩的材质。这意味着使用PBR材质我们能够描述场景中绝大多数物体,只有一些特殊材质,如角色材质/天空/水等需要特殊制作。
对于材质实例而言:
如果使用了Static Parameter Set,会生成新的Shader变体;
如果仅修改颜色、贴图等参数,则不会生成Shader变体;
会生成哪些Shader变体
对于场景中的一个普通小物件而言,它的材质可能达到几十或上百个。
一.从ShaderType考虑:
(1)Shadow Depth Pass Shader。如平行光阴影、点光源阴影、聚光灯阴影等。
(2)Depth Pass Shader。如果开启了prepass,可能包含写入颜色和仅写入深度的Shader。
(3)Base Pass Shader。前向光照下,可能包含LDR/HDR的,有动态Skylight/无动态Skylight的,有阴影/无阴影的,使用VLM作为间接光/使用LightMap作为间接光/无间接光,动态方向光/静态方向光,0~4个动态光数量等。
等等。
二.从VertexFactory考虑:
如果一个物体被标记为特殊的顶点类型,比如skin, morph, cloth, instance等,它还会对每个顶点工厂都生成对应的Shader Code。
整体而言,可以用如下公式大致描述,对于特定MaterialShaderMapId, 所有可能Shader变体的数量:
(VertexFactoryType * MeshShaderType + MaterialShaderType + ShaderPipelineType * StageTypes)
VertexFactoryType:顶点工厂;MeshShaderType:Mesh相关的ShaderType; MaterialShaderType:Mesh无关的ShaderType; 后两者是新特性。
实际过程中,其中的部分Shader是可以通过判断条件选择不去编译的,所以上述只是可能出现的数量,并不是最终生成的数量。可优化空间最大的就是MeshShaderType。
三.从MaterialShaderMapId考虑:
对于一个母材质而言,所有的Shader存在ShaderMap中,它的键值是一个Id,Value对应一种材质,对于Value而言,我们可以用刚刚提到的公式去计算它的变体。母材质所有的变体是map中所有变体数的累加。
ue4中的FMaterialShaderMapId记录在MaterialShared.h中,大致长下面这个样子,这就意味着当我们实际使用了多少静态参数的排列组合(比如StaticSwitchParameter),就会产生多少个Id。
完善一下变体公式:
Permutation * (VertexFactoryType * MeshShaderType + MaterialShaderType + ShaderPipelineType * StageTypes)
/** Contains all the information needed to uniquely identify a FMaterialShaderMap. */
class FMaterialShaderMapId
// ...
/** Relevant portions of StaticParameterSet from material. */
TArray<FStaticSwitchParameter> StaticSwitchParameters;
TArray<FStaticComponentMaskParameter> StaticComponentMaskParameters;
TArray<FStaticTerrainLayerWeightParameter> TerrainLayerWeightParameters;
TArray<FStaticMaterialLayersParameter::ID> MaterialLayersParameterIDs;
// ...
;
多个Shader变体的原因
ue4会生成多个变体,如BasePass种的各种光照类型组合。但实际上,每个物体在同一时间下只会用到一个Shader。多余的Shader可以分为以下三种类型考虑:
(1)会在不同时间使用。比如CSM阴影有一定距离,相机靠近物体时,使用有阴影的Shader;相机远离物体时,使用无阴影的Shader。
(2)只可能使用到其中一个。比如动态物体的间接光通常使用VLM,静态物体的间接光通常使用LightMap,但编译材质时并不知道材质会应用在哪种类型的物体上,所以会生成每种类型的。
(3)场景中不会使用。在某个特定的项目中,永远不会用到某个特性。比如只会产生平行光阴影。
需要明确的一点是,ue4的材质编译(从材质到Shader Code)是一个离线的过程,它通常是在项目启动或打包时进行。这意味着它无法获得一些运行时的数据,比如它无法知道哪些Shader是项目中不可能用到的。
Shader变体的优化
从VertexFactory考虑,需要通过勾选Usage来选择对应的顶点工厂。此时如果发生异常,要么是美术勾选有误,要么是新增的ShaderType没有做顶点工厂的正确编译判断。
从ShaderType考虑,首先可以在设置中使用一些官方提供的Shader Permutation。
如果想要提供更精细的控制,可以在每个ShaderType类中的ShouldCompilePermutation中进行更细致的修改。把特定项目中的不会使用的在代码中关闭。
另一个优化的思路是,不使用变体,而是使用分支来控制。此时我们可能只有一份Shader Code,但通过if...else...来判断执行哪段逻辑。
这实际上是一种取舍,使用变体可能会带来内存膨胀,而使用分支由于指令不再统一,会打断warp的并行。如果我们保证生成的分支是静态分支,则对性能的影响较少,此时可以考虑使用分支来优化。
Shader编译的过程
此处的Shader编译也就是收集所有变体,然后逐个将材质节点结合Shader模板翻译成HLSL代码的过程。
编译好的Shader Code信息记录在DDC中,当检测到引用某个未编译的材质时,会先从DDC中查找,找不到或强制编译时,会触发编译。编译通常是多线程进行的,编译好的数据会缓存在ShaderMap结构里。
最外层循环在MaterialShared.cpp中:
(1)可能有多个不同类型的材质会调用到这里,包括材质实例、编辑器预览材质、母材质等。
① 首先会收集静态参数,得到ShaderMapId,如下图中的GetShaderMapId函数;
② 接下来看这个ShaderMapId是否已经存在于ShaderMap,不存在就要请求编译。
在MaterialShader.cpp中,FMaterialShaderMap::Compile函数中,看到遍历特定ShaderMapId的所有变体并编译的逻辑:
(2)这里是收集所有可能Mesh相关的Shader。这里主要是两个for循环,先遍历所有VertexFactory,再遍历所有MeshShader,收集所有变体。然后再在箭头所指的地方执行编译。
这里的MeshShader这个结构实际上已经在前面筛选过一轮了,仅保留了顶点工厂相关的Shader;在调用BeginCompile后,如果跟踪相关逻辑,会发现Shader还会再筛选一轮。
(3)接下来是Mesh无关的Shader,此处只有一轮循环:
(4)第三部分将编译所有ShaderPipelineType。
材质包体和内存
材质对于包体的影响主要体现在以下几个方面:
(1)母材质的个数
(2)Shader变体的数量
(3)Shader模板的大小
以上三者对包体空间的影响是乘法关系,可以作为优化的参考思路。
此外,ue4的配置文件支持将Shader存储为Shared Code形式,公共的代码作为Library仅存储一份,也会进一步减小包体。
材质/材质实例本质上是UObject,它的加载和其它对象一样基于对象池,卸载基于gc。这意味着打包了但运行时未引用的材质不会影响内存。
材质对于内存的影响主要体现在两个方面:
(1)对CPU内存的影响。加载材质时会自动加载对应的所有变体,且在材质被释放前,不会主动释放;
(2)对GPU显存的影响。渲染线程中引用到某个Shader后,会从材质中读取Shader Code,并进行运行时的编译,生成显存中的Shader Program;如果已经生成了则读取缓存。第一次访问时可能带来卡顿。
因此,在没有做任何优化的情况下,Shader变体并不会按需加载,变体数量会直接影响内存占用。这样的设计可能是出于这样的考虑,如果不在一开始加载所有的Shader Code,就需要在渲染线程请求生成对应Shader Program时触发IO操作,造成更严重的卡顿。
优化的思路是通过运行时预处理所有可能用到的Shader变体,记录到列表中,并在加载场景时进行Shader编译(指生成Shader Program),此时就可以不在内存中缓存Shader Code,也避免了运行时卡顿。(此处可以参考官方文档PSO Caching)
以上是关于[ue4] 材质和Shader变体(Shader Permutation)的主要内容,如果未能解决你的问题,请参考以下文章