[ue4] 材质和Shader变体(Shader Permutation)

2024-03-18 11:40

本文主要是介绍[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)

这篇关于[ue4] 材质和Shader变体(Shader Permutation)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/822224

相关文章

Unity3D Shader详解:只画顶点或只画线框

在Unity3D开发中,Shader是控制渲染过程的关键组件,它允许开发者自定义物体的渲染方式。有时,为了特定的视觉效果,我们可能需要只渲染模型的顶点或者只显示其线框。下面,我们将详细探讨这两种效果的技术实现,并给出相应的代码示例。 对惹,这里有一个游戏开发交流小组,大家可以点击进来一起交流一下开发经验呀! 只画顶点 在Unity中直接“只画顶点”的概念可能不是非常直观,因为顶点本身只是模型

【UE4源代码观察】观察Core模块

话题 Core模块是整个引擎中最核心的模块,在之前的博客【UE4源代码观察】可视化所有模块的依赖情况中有统计,它被983个模块引用,恐怕除了第三方的模块外基本所有模块都有引用。我想首先观察其中的内容,然后再做测试:将Core模块拷贝到之前【UE4源代码观察】手动建立一个使用UBT进行编译的空白工程建立的空白工程中,看能否将它成功编译,理论上讲,“核心”不应再依赖太多其他的东西,所以我应该不会再需

【UE4源代码观察】在空白工程中测试跨模块调用函数

目的 在之前的博客【UE4源代码观察】手动建立一个使用UBT进行编译的空白工程中,成功让UBT工作起来了。现在我想要测试编译出的多个模块之间是否能互相调用,我记录下测试的过程。最终工程见 工程GIT链接。 实践 第一部分 首先仿照TestA模块建立TestB模块。 在TestB.h文件中声明了一个函数 int TestBFunc(int x, int y); 并在TestB.cpp中

【UE4源代码观察】观察FEngineLoop中各函数的调用顺序

当启动编辑器时,Launch模块是启动模块,在Windows平台中 LaunchWindows.cpp 的 WinMain函数是入口,而 FEngineLoop 管理了程序的初始化与主循环。他的成员函数如下: public:/*** Pre-Initialize the main loop, and generates the commandline from standard ArgC/Arg

【UE4源代码观察】手动建立一个使用UBT进行编译的空白工程

我想观察UE4是怎么编译的,于是查阅官方文档,了解到UE4有一套自己的编译工具:UnrealBuildTool,简称UBT。关于UBT的官方文档参阅:虚幻编译工具。我想尝试自己手动建立一个使用UBT进行编译的空白工程。不过首先,先了解下UBT的编译流程中一些文件所扮演的角色 UBT的编译流程中一些文件所扮演的角色 模块 每个模块都由一个 .build.cs 文件声明,它存储在 Source

【UE4源代码观察】可视化所有模块的依赖情况

思路 UE4引擎的模块非常多,我想要观察他们的依赖情况。于是我写了程序读取所有模块的.Build.cs文件,记录每个模块依赖的模块(当前是看PublicDependencyModuleNames和PrivateDependencyModuleNames。但后来经评论提醒我发现AddEngineThirdPartyPrivateStaticDependencies也应该视作是“依赖”但没统计,后续

简单梳理UE4的Houdini官方插件代码

前言 Houdini官方插件名字叫 “Houdini Engine”,它搭建了Houdini数据与UE4数据间的桥梁。我接触这个插件已经有段时间了,我想是时候梳理一下插件的结构了。(当前我用的UE4版本是4.24.2,Houdini版本18.0.348) 需要说明的是,这篇博客主要是从代码出发的。我准备先分析插件整体的代码结构,再逐个翻阅每个文件试图搞明白他角色。但如果不准备研究代码结构和实现

【UE4源代码观察】观察TargetPlatform模块

前情提要与本次目标 在之前的博客《【UE4源代码观察】观察 RHI、D3D11RHI、RenderCore 这三个模块的依赖关系》中,我将RHI、D3D11RHI、RenderCore这三个模块加入了我的空白工程中并确保可以成功编译。然而当时RenderCore模块有一个比较大的缺失:没有让shader相关的功能正常编译,因为它需要TargetPlatform模块中的内容。 因此,这次我想观察

【UE4源代码观察】尝试调试UBT

前言 在之前的博客《【UE4源代码观察】手动建立一个使用UBT进行编译的空白工程》中我尝试动手搭建了一个用UBT进行编译的空白的工程。但是对UBT其中的逻辑并不理解。 后来在学习UE4源代码的过程中,又了解了它的一些行为。目前,对我影响较大的是:1.他会有一些逻辑去添加一些宏。2.他会有一些逻辑去给修改ModuleRules(和.build.cs中内容的角色一样)。这时候我发现一些和预期不太一

试用UE4的纹理数组(UTexture2DArray)

UTexture2DArray 我发现在我目前使用的版本(4.25)中,官方已经实现了纹理数组(可能在4.24或更早版本就已经实现了)。 纹理数组,其含义不言而喻。一个重要作用是可以使用更多的纹理而不受制于sampler数目限制。 这一篇里我想对官方的纹理数组进行一下简单的试用。 试用 0. 启用纹理数组 虽然我看到了代码中有UTexture2DArray这个类,不过一开始并没有在编辑器