使用 UE4 的材质编辑器的时候我们可以为材质的颜色、粗糙度、金属度等连接一个常量或者纹理结点,如果要实现复杂效果可能还会用到混合结点,最后就可能看到一个完整的材质其实是由一堆结点和它们之间的连线组成

然后每一个结点可以理解为一段 HLSL 代码,然后结点之间的连线则是变量的传递

如果想看 HLSL 代码,可以依次点击窗口着色器代码HLSL 代码

这里举一个简单的例子。

// Now the rest of the inputs
MaterialFloat3 Local0 = lerp(MaterialFloat3(0.00000000,0.00000000,0.00000000),Material.VectorExpressions[1].rgb,MaterialFloat(Material.ScalarExpressions[0].x));
MaterialFloat4 Local1 = ProcessMaterialColorTextureLookup(Texture2DSampleBias(Material.Texture2D_0, Material.Texture2D_0Sampler,Parameters.TexCoords[0].xy,View.MaterialTextureMipBias));
MaterialFloat3 Local2 = (1.00000000 - Local1.rgb);
MaterialFloat4 Local3 = ProcessMaterialColorTextureLookup(Texture2DSampleBias(Material.Texture2D_1, Material.Texture2D_1Sampler,Parameters.TexCoords[0].xy,View.MaterialTextureMipBias));
MaterialFloat3 Local4 = (Local2 + Local3.r);
MaterialFloat3 Local5 = (1.00000000 - Local4);

我们容易发现,要把材质结点翻译成 HLSL 代码,可以从左边出发拓扑排序,也可以从右边的材质结果结点出发递归往左(显然递归比拓排好写)
然后 UE4 是用递归的

接下来我们结合源码看一下 UE4 是怎么实现的

编译过程

我们从 FMaterial 的一个成员函数开始

/**
* Compiles this material for Platform, storing the result in OutShaderMap if the compile was synchronous
*/
bool BeginCompileShaderMap(
    const FMaterialShaderMapId& ShaderMapId,
    const FStaticParameterSet &StaticParameterSet,
    EShaderPlatform Platform,
    TRefCountPtr<class FMaterialShaderMap>& OutShaderMap,
    const ITargetPlatform* TargetPlatform = nullptr);

这个函数会把材质的编译结果放到 OutShaderMap
并且每个材质 ShaderMapId 都是唯一的

它会先创建一个 FMaterialShaderMap 对象
接下来创建 FHLSLMaterialTranslator 对象并用它生成着色器的 HLSL 代码
最后调用 FMaterialShaderMapCompile 方法编译这段代码并保存结果

节选关键代码如下

// ...

TRefCountPtr<FMaterialShaderMap> NewShaderMap = new FMaterialShaderMap();

// ...

// 生成着色器代码
FMaterialCompilationOutput NewCompilationOutput;
FHLSLMaterialTranslator MaterialTranslator(this, NewCompilationOutput, StaticParameterSet, Platform,GetQualityLevel(), ShaderMapId.FeatureLevel, TargetPlatform);
bSuccess = MaterialTranslator.Translate();

// ...

// 为材质编译着色器
NewShaderMap->Compile(this, ShaderMapId, MaterialEnvironment, NewCompilationOutput, Platform, bSynchronousCompile);

接下来,我们着重看 FHLSLMaterialTranslatorTranslate 方法,在里面会发现一个很显眼的东西,没错这个就是材质结果结点的各个输入插槽

// Rest of properties
Chunk[MP_EmissiveColor]= Material->CompilePropertyAndSetMaterialProperty(MP_EmissiveColor,this);
Chunk[MP_DiffuseColor]= Material->CompilePropertyAndSetMaterialProperty(MP_DiffuseColor,this);
Chunk[MP_SpecularColor]= Material->CompilePropertyAndSetMaterialProperty(MP_SpecularColor,this);
Chunk[MP_BaseColor]= Material->CompilePropertyAndSetMaterialProperty(MP_BaseColor,this);
Chunk[MP_Metallic]= Material->CompilePropertyAndSetMaterialProperty(MP_Metallic,this);
Chunk[MP_Specular]= Material->CompilePropertyAndSetMaterialProperty(MP_Specular,this);
Chunk[MP_Roughness]= Material->CompilePropertyAndSetMaterialProperty(MP_Roughness,this);
Chunk[MP_Opacity]= Material->CompilePropertyAndSetMaterialProperty(MP_Opacity,this);
Chunk[MP_OpacityMask]= Material->CompilePropertyAndSetMaterialProperty(MP_OpacityMask,this);
Chunk[MP_WorldPositionOffset]= Material->CompilePropertyAndSetMaterialProperty(MP_WorldPositionOffset,this);
Chunk[MP_WorldDisplacement]= Material->CompilePropertyAndSetMaterialProperty(MP_WorldDisplacement,this);
Chunk[MP_TessellationMultiplier]= Material->CompilePropertyAndSetMaterialProperty(MP_TessellationMultiplier,this);

于是我们去看 FMaterialResourceCompilePropertyAndSetMaterialProperty 方法

注意一下 FMaterialResourceFMaterial 接口的一个实现,它里面会存一个 UMaterial 实例或者 UMaterialInstance 实例,这两种实例都是继承自 UMaterialInterface 接口

这个方法里面有一个 switch,对于不同类型的输入插槽(或者说 EMaterialProperty),它有不同的处理,不过基本都是调用 UMaterialInterfaceCompileProperty 方法,代码太乱就不贴了

我们直接去看 UMaterialInterfaceCompileProperty 方法,它调用了 CompilePropertyEx 方法,这个方法是个虚函数,发现 UMaterialUMaterialInterface 都有自己的实现,但是 UMaterialInterface 是基于 UMaterial 的因此我们直接看 UMaterialCompilePropertyEx 方法就行

发现里面又是一个 switch

int32 UMaterial::CompilePropertyEx( FMaterialCompiler* Compiler, const FGuid& AttributeID )
{
    const EMaterialProperty Property = FMaterialAttributeDefinitionMap::GetProperty(AttributeID);

    if( bUseMaterialAttributes && MP_DiffuseColor != Property && MP_SpecularColor != Property )
    {
        return MaterialAttributes.CompileWithDefault(Compiler, AttributeID);
    }

    switch (Property)
    {
        case MP_Opacity:return Opacity.CompileWithDefault(Compiler, Property);
        case MP_OpacityMask:return OpacityMask.CompileWithDefault(Compiler, Property);
        case MP_Metallic:return Metallic.CompileWithDefault(Compiler, Property);
        case MP_Specular:return Specular.CompileWithDefault(Compiler, Property);
        case MP_Roughness:return Roughness.CompileWithDefault(Compiler, Property);
        case MP_TessellationMultiplier:return TessellationMultiplier.CompileWithDefault(Compiler, Property);
        case MP_CustomData0:return ClearCoat.CompileWithDefault(Compiler, Property);
        case MP_CustomData1:return ClearCoatRoughness.CompileWithDefault(Compiler, Property);
        case MP_AmbientOcclusion:return AmbientOcclusion.CompileWithDefault(Compiler, Property);
        case MP_Refraction:return Refraction.CompileWithDefault(Compiler, Property);
        case MP_EmissiveColor:return EmissiveColor.CompileWithDefault(Compiler, Property);
        case MP_BaseColor:return BaseColor.CompileWithDefault(Compiler, Property);
        case MP_SubsurfaceColor:return SubsurfaceColor.CompileWithDefault(Compiler, Property);
        case MP_Normal:return Normal.CompileWithDefault(Compiler, Property);
        case MP_WorldPositionOffset:return WorldPositionOffset.CompileWithDefault(Compiler, Property);
        case MP_WorldDisplacement:return WorldDisplacement.CompileWithDefault(Compiler, Property);
        case MP_PixelDepthOffset:return PixelDepthOffset.CompileWithDefault(Compiler, Property);
        case MP_ShadingModel:return ShadingModelFromMaterialExpression.CompileWithDefault(Compiler, Property);

        default:
            if (Property >= MP_CustomizedUVs0 && Property <= MP_CustomizedUVs7)
            {
                const int32 TextureCoordinateIndex = Property - MP_CustomizedUVs0;

                if (CustomizedUVs[TextureCoordinateIndex].Expression && TextureCoordinateIndex < NumCustomizedUVs)
                {
                    return CustomizedUVs[TextureCoordinateIndex].CompileWithDefault(Compiler, Property);
                }
                else
                {
                    // The user did not customize this UV, pass through the vertex texture coordinates
                    return Compiler->TextureCoordinate(TextureCoordinateIndex, false, false);
                }
            }

    }

    check(0);
    return INDEX_NONE;
}

于是我们随便看一个插槽的 CompileWithDefault 方法,比如说 BaseColorBaseColor 变量是一个 FColorMaterialInput 类型的插槽

int32 FColorMaterialInput::CompileWithDefault(class FMaterialCompiler* Compiler, EMaterialProperty Property)
{
    if (UseConstant)
    {
        FLinearColor LinearColor(Constant);
        return Compiler->Constant3(LinearColor.R, LinearColor.G, LinearColor.B);
    }
    else if (Expression)
    {
        int32 ResultIndex = FExpressionInput::Compile(Compiler);
        if (ResultIndex != INDEX_NONE)
        {
            return ResultIndex;
        }
    }

    return Compiler->ForceCast(FMaterialAttributeDefinitionMap::CompileDefaultExpression(Compiler, Property), MCT_Float3);
}

发现有常量和表达式两种情况,常量就是一个常量的颜色结点,表达式就是使用了混合结点或者纹理结点等等复杂的结点

对于表达式我们看到它调用了基类 FExpressionInputCompile 方法

int32 FExpressionInput::Compile(class FMaterialCompiler* Compiler)
{
    if(Expression)
    {
        Expression->ValidateState();

        int32 ExpressionResult = Compiler->CallExpression(FMaterialExpressionKey(Expression, OutputIndex, Compiler->GetMaterialAttribute(), Compiler->IsCurrentlyCompilingForPreviousFrame()),Compiler);

        if(Mask && ExpressionResult != INDEX_NONE)
        {
            return Compiler->ComponentMask(
                ExpressionResult,
                !!MaskR,!!MaskG,!!MaskB,!!MaskA
            );
        }
        else
        {
            return ExpressionResult;
        }
    }
    else
        return INDEX_NONE;
}

又调用了 FMaterialCompilerCallExpression 方法,而且把表达式传过去了

CallExpression 方法里,它会调用表达式 ExpressionKey.ExpressionCompile 方法

然后我们可以看到 Expression 是一个 UMaterialExpression 接口的实例,它有多种实现

稍微看一下加法表达式的 Compile 实现

int32 UMaterialExpressionAdd::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg1 = A.GetTracedInput().Expression ? A.Compile(Compiler) : Compiler->Constant(ConstA);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Arg2 = B.GetTracedInput().Expression ? B.Compile(Compiler) : Compiler->Constant(ConstB);

    return Compiler->Add(Arg1, Arg2);
}

可以看到递归是在这里实现的,然后最后一行 ComplierAdd 方法是由 FHLSLMaterialTranslator 提供的
返回值是这个表达式的唯一索引值,可以通过索引找到表达式

生成 HLSL 代码的逻辑就在 FHLSLMaterialTranslatorAdd 方法里,我们也可以看一下

virtual int32 Add(int32 A,int32 B) override
{
    if(A == INDEX_NONE  B == INDEX_NONE)
    {
        return INDEX_NONE;
    }

    const uint64 Hash = CityHash128to64({ GetParameterHash(A), GetParameterHash(B) });
    if(GetParameterUniformExpression(A) && GetParameterUniformExpression(B))
    {
        return AddUniformExpressionWithHash(Hash, new FMaterialUniformExpressionFoldedMath(GetParameterUniformExpression(A),GetParameterUniformExpression(B),FMO_Add),GetArithmeticResultType(A,B),TEXT("(%s + %s)"),*GetParameterCode(A),*GetParameterCode(B));
    }
    else
    {
        return AddCodeChunkWithHash(Hash, GetArithmeticResultType(A,B),TEXT("(%s + %s)"),*GetParameterCode(A),*GetParameterCode(B));
    }
}

类的关系整理

最后我整理了一下上面提到的各个类之间的关系,总的来说还是比较清晰的

FMaterialFMaterialResource 是逻辑上的材质类,一个 FMaterialResource 对象可以管理一个 UMaterial 实例或 UMaterialInstance 实例,UMaterial 上挂着不同类型的 FExpressionInput 插槽,这些 FExpressionInput 实现类又包含 UMaterialExpression 表达式,表达式会层层嵌套形成一张有向无环图
最后 FMaterialCompiler 的实现类 FHLSLMaterialTranslator 主要用于生成 HLSL 代码