材质的基础概念

UMaterial, UMaterialInterface, UMaterialInstance

UMaterial是属于引擎层的概念,对应着材质编辑器编辑的uasset资源文件,可以被应用到网格上,以便控制它在场景中的视觉效果。

UMaterial继承自UMaterialInterface。UMaterialInterface是材质的基础接口类,定义了大量材质相关的数据和接口, 部分接口是空实现或未实现的接口。

class UMaterialInterface : public UObject, public IBlendableInterface, public IInterface_AssetUserData

class UMaterial : public UMaterialInterface

UMaterialInstance是材质实例,不能单独存在,而需要依赖UMaterialInterface类型的父类,其父类可以是UMaterialInterface的任意一个子类,但最上层的父类必须是UMaterial。

class UMaterialInstance : public UMaterialInterface

它只能覆盖UMaterial的一小部分参数,通常不会被单独创建,而是以它的两个子类UMaterialInstanceConstant和UMaterialInstanceDynamic被创建

UMaterialInstanceConstant是用于编辑器预先创建和编辑好的材质实例资源,它是为了避免运行时修改材质参数引起重新编译而存在的。如果不重新编译,就无法支持对材质的常规修改,因此实例只能更改预定义的材质参数的值。 这里的参数就是在材质编辑器内定义的唯一的名称、类型和默认值静态定义。

UMaterialInstanceDynamic与UMaterialInstanceConstant不同,它提供了可以在运行时代码动态创建和修改材质属性的功能,并且同样不会引起材质重新编译。

FMaterialRenderProxy

图元、网格、光源等场景的类型都有其相应的GameThread代表和RenderThread代表,材质也是如此。
UMaterialInterface对应的渲染线程代表便是FMaterialRenderProxy。FMaterialRenderProxy负责接收游戏线程代表的数据,然后传递给渲染器去处理和渲染。

FMaterialRenderProxy是个抽象类,定义了一个静态全局的材质渲染代理映射表和获取FMaterial渲染实例的接口。具体的逻辑由子类完成,它的子类有:

  • FDefaultMaterialInstance:渲染UMaterial的默认代表实例。
  • FMaterialInstanceResource:渲染UMaterialInstance实例的代表。
  • FColoredMaterialRenderProxy:覆盖材质颜色向量参数的材质渲染代表。
  • FLandscapeMaskMaterialRenderProxy:地貌遮罩材质渲染代表。
  • FLightmassMaterialProxy:Lightmass材质渲染代理。

FMaterialRenderProxy既会被GameThread处理,又会被RenderThread处理,需要小心注意它们之间的数据访问和接口调用。带有GameThread的属性和接口是专用于游戏线程,带有RenderThread的专用于渲染线程,如果没有特别说明,一般(非绝对)用于渲染线程。

FMaterial, FMaterialResource

FMaterial有3个功能:

  • 表示材质到材质的编译过程,并提供可扩展性钩子(CompileProperty等) 。
  • 将材质数据传递到渲染器,并使用函数访问材质属性。
  • 存储缓存的shader map,和其他来自编译的瞬态输出,这对异步着色器编译是必要的。

FMaterial包括了材质、Shader、VertexFactory、ShaderPipeline、ShaderMap等各种数据和操作接口。FMaterial是个抽象类,它的子类只有FMaterialResource。

FMaterialResource只是实现了FMaterial未实现的接口,用于渲染UMaterial或UMaterialInstance,它存储了UMaterial或UMaterialInstance的实例。如果UMaterialInstance和UMaterial的实例都有效的情况下,那么它们重叠的数据会优先取UMaterialInstance的数据。

渲染资源除了FMaterial之外,还有个比较核心的概念就是FMaterialRenderContext,它保存了FMaterialRenderProxy和FMaterial之间的关联配对。

材质类型体系总结

MaterialSystemClasses

UMaterialInterface和它的子类是引擎模块在游戏线程的代表。UMaterialInterface继承UOjbect,提供了材质的抽象接口,为子类提供了一致的行为和规范,也好统一不同类型的子类之间的差异。

子类UMaterial则对应着用材质编辑器生成的材质蓝图的资源,保存了各种表达式节点及各种参数。

另一个子类UMaterialInstance则抽象了材质实例的接口,是为了支持修改材质参数后不引发材质重新编译而存在的,同时统一和规范固定实例(UMaterialInstanceConstant)和动态实例(UMaterialInstanceDynamic)两种子类的数据和行为。

UMaterialInstanceConstant在编辑器期间创建和修改好材质参数,运行时不可修改,提升数据更新和渲染的性能;UMaterialInstanceDynamic则可以运行时创建实例和修改数据,提升材质的扩展性和可定制性,但性能较UMaterialInstanceConstant差一些。UMaterialInstance需要指定一个父类,最顶层的父类要求是UMaterial实例。

FMaterialRenderProxy是UMaterialInterface的渲染线程的代表,类似于UPrimitiveComponent和FPrimitiveSceneProxy的关系。

FMaterialRenderProxy将UMaterialInterface实例的数据搬运(拷贝)到渲染线程,但同时也会在游戏线程被访问到,是两个线程的耦合类型,需要谨慎处理它们的数据和接口调用。FMaterialRenderProxy的子类对应着UMaterialInterface的子类,以便将UMaterialInterface的子类数据被精准地搬运(拷贝)到渲染线程,避免游戏线程和渲染线程的竞争。FMaterialRenderProxy及其子类都是引擎模块的类型。

既然已经有了FMaterialRenderProxy的渲染线程代表,为什么还要存在FMaterial和FMaterialResource呢?答案有两点:

  • FMaterialRenderProxy及其子类是引擎模块的类型,是游戏线程和渲染线程的胶囊类,需要谨慎处理两个线程的数据和接口调用,渲染模块无法真正完全拥有它的管辖权。
  • FMaterialRenderProxy的数据由UMaterialInterface传递而来,意味着FMaterialRenderProxy的信息有限,无法包含使用了材质的网格的其它信息,如顶点工厂、ShaderMap、ShaderPipelineline、FShader及各种着色器参数等。

FMaterial同是引擎模块的类型,但存储了游戏线程和渲染线程的两个ShaderMap,意味着渲染模块可以自由地访问渲染线程的ShaderMap,而又不影响游戏线程的访问。而且FMaterial包含了渲染材质所需的所有数据,渲染器的其它地方,只要拿到网格的FMaterial,便可以正常地获取材质数据,从而提交绘制指令。

材质渲染

材质数据的发起者是GameThread的资源,一般是从磁盘加载的二进制资源,然后序列化成UMaterialInterface实例,或者由运行时动态创建并设置材质数据。不过绝大多数是由磁盘加载而来。在GameThread阶段,材质的各种类型的实例已经被加载、设置和创建。

ProcessSerializedInlineShaderMaps函数以及UMaterial和UMaterialInterface中的部分接口会触发FMaterialResource的创建,而ProcessSerializedInlineShaderMaps和FMaterial中的部分接口会触发RenderingThreadShaderMap的设置。而一旦RenderingThreadShaderMap被设置,材质相关的其它众多数据将被渲染线程和渲染器自由地读取,从而完成渲染。

材质编译

UMaterialExpression

UMaterialExpression就是表达式,每个材质节点UMaterialGraphNode都有一个UMaterialExpression实例。

UE内置了很多材质节点,因此继承自UMaterialExpression的子类非常多。

UMaterialExpression对象在Compile时,会调用传入的FMaterialCompiler对象的对应方法。FMaterialCompiler是一个抽象类,其中对应的方法由其子类实现。例如:UMaterialExpressionAdd::Compile会调用FHLSLMaterialTranslator中的Add。

总而言之,材质表达式的编译,实际上就是对参数和对应的函数序列化成HLSL片段。

UMaterialGraphNode

UMaterialGraphNode即在材质编辑器中创建的材质节点,包含了图形界面的信息和对应的表达式。

UMaterialGraphNode继承自UMaterialGraphNode_Base、UEdGraphNode。

UMaterialGraph

UMaterialGraph是UMaterial的一个成员,用来存储编辑器产生的材质节点和参数。

UMaterialGraph中包含指向其对应的材质实例、材质函数以及根节点的指针。

FHLSLMaterialTranslator

FHLSLMaterialTranslator继承自FMaterialCompiler,作用就是将材质的表达式转译成HLSL代码,填充到MaterialTemplate.ush的宏和空缺代码段。

FHLSLMaterialTranslator实现了FMaterialCompiler的所有抽象接口,它的核心核心成员和接口如下:

-FMaterial* Material:编译的目标材质。
-FMaterialCompilationOutput& MaterialCompilationOutput:编译后的结果。
-FString MaterialTemplate:待填充或填充后的MaterialTemplate.ush字符串。
-Translate():执行HLSL转译,将表达式转译成代码块保存到对应的属性槽中。
-GetMaterialShaderCode():将材质的宏、属性、表达式等数据填充到MaterialTemplate.ush并返回结果。

MaterialTemplate.ush

MaterialTemplate.usf是材质shader模板,内涵大量%s的空缺和待替换的宏,由FHLSLMaterialTranslator::GetMaterialShaderCode负责填充。

MaterialTemplate.ush包含了大量的数据和接口,主要有几类:

  • 基础shader模块引用。
  • 待填充的宏定义。
  • 待填充的接口实现。
  • 顶点、像素、材质属性等结构体定义。部分结构体待填充。
  • 材质属性、数据处理、表达式、工具类接口定义。部分接口待填充。

材质编译流程

材质ShaderMap的编译入口在FMaterial的以下两个接口:

  • FMaterial::BeginCompileShaderMap
  • FMaterial::GetMaterialExpressionSource
// Engine\Source\Runtime\Engine\Private\Materials\MaterialShared.cpp

bool FMaterial::BeginCompileShaderMap(const FMaterialShaderMapId& ShaderMapId, const FStaticParameterSet &StaticParameterSet,
 EShaderPlatform Platform, TRefCountPtr<FMaterialShaderMap>& OutShaderMap, const ITargetPlatform* TargetPlatform)
{
    // 注意只在编辑器期间才会执行.
#if WITH_EDITORONLY_DATA
    bool bSuccess = false;
    // 新建shader map.
    TRefCountPtr<FMaterialShaderMap> NewShaderMap = new FMaterialShaderMap();

#if WITH_EDITOR
    NewShaderMap->AssociateWithAsset(GetAssetPath());
#endif
    
    // 生成材质shader代码.
    // 输出结果.
    FMaterialCompilationOutput NewCompilationOutput;
    // 转换器.
    FHLSLMaterialTranslator MaterialTranslator(this, NewCompilationOutput, StaticParameterSet, Platform,GetQualityLevel(), ShaderMapId.FeatureLevel, TargetPlatform);
    // 执行表达式转换, 填充到MaterialTemplate.ush.
    bSuccess = MaterialTranslator.Translate();

    // 表达式转换成功才需要执行后续操作.
    if(bSuccess)
    {
        // 为材质创建一个着色器编译环境,所有的编译作业将共享此材质.
        TRefCountPtr<FShaderCompilerEnvironment> MaterialEnvironment = new FShaderCompilerEnvironment();
        MaterialEnvironment->TargetPlatform = TargetPlatform;
        // 获取材质环境.
        MaterialTranslator.GetMaterialEnvironment(Platform, *MaterialEnvironment);
        // 获取材质shader代码.
        const FString MaterialShaderCode = MaterialTranslator.GetMaterialShaderCode();
        
        const bool bSynchronousCompile = RequiresSynchronousCompilation() || !GShaderCompilingManager->AllowAsynchronousShaderCompiling();

        // 包含虚拟的材质文件路径.
        MaterialEnvironment->IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/Material.ush"), MaterialShaderCode);

        // 编译材质的shader代码.
        NewShaderMap->Compile(this, ShaderMapId, MaterialEnvironment, NewCompilationOutput, Platform, bSynchronousCompile);

        if (bSynchronousCompile) // 同步编译
        {
            // 同步模式, 直接赋值给OutShaderMap.
            OutShaderMap = NewShaderMap->CompiledSuccessfully() ? NewShaderMap : nullptr;
        }
        else // 异步编译
        {
            // 先将NewShaderMap放到等待编译结束的列表.
            OutstandingCompileShaderMapIds.AddUnique( NewShaderMap->GetCompilingId() );
            // 异步模式, OutShaderMap先设为null, 会回退到默认的材质.
            OutShaderMap = nullptr;
        }
    }

    return bSuccess;
#else
    UE_LOG(LogMaterial, Fatal,TEXT("Not supported."));
    return false;
#endif
}

其中使用的FHLSLMaterialTranslator的重要接口:

  • Translate:转译材质蓝图的材质节点表达式,将所有材质属性的编译结果填充到格子的FShaderCodeChunk中。
  • GetMaterialEnvironment:处理材质蓝图的编译环境(宏定义)。
  • GetMaterialShaderCode:填充MaterialTemplate.ush的空缺代码,根据Translate编译的FShaderCodeChunk对应的材质属性接口,以及其它的宏定义、结构体、工具类接口。

经过FHLSLMaterialTranslator的编译之后,将获得完整的材质Shader代码,便会送入FMaterialShaderMap::Compile接口进行编译,编译后的shader代码保存到FMaterialShaderMap之中。

编译不同类型的shader时,需要的数据不完全一样:

Shader类型 所需的数据
GlobalShader Shader_x.usf
MaterialShader Shader_x.usf + MaterialTemplate_x.usf
MeshMaterialShader Shader_x.usf + MaterialTemplate_x.usf + VertexFactory_x.usf

其中:

  • Shader_x.usf:引擎Shader目录下的已有文件,如DeferredLightVertexShaders.usf、DeferredLightPixelShaders.usf。
  • MaterialTemplate_x.usf:FHLSLMaterialTranslator编译材质蓝图后填充MaterialTemplate.ush的代码。
  • VertexFactory_x.usf:引擎Shader目录下的已有顶点工厂文件代码,如LocalVertexFactory.ush、GpuSkinVertexFactory.ush。

材质编译流程图

MaterialCompileDiagram1

MaterialCompileDiagram2

Reference

Unreal Engine Documentation: Materials

剖析虚幻渲染体系(09)- 材质体系

UE4材质系统源码剖析