[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没有做顶点工厂的正确编译判断。

材质面板Usage属性

        从ShaderType考虑,首先可以在设置中使用一些官方提供的Shader Permutation。

Project Settings

        如果想要提供更精细的控制,可以在每个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)

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值