UE4 材质结点生成 HLSL 代码源码浅析
使用 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 代码
最后调用 FMaterialShaderMap
的 Compile
方法编译这段代码并保存结果
节选关键代码如下
// ...
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);
接下来,我们着重看 FHLSLMaterialTranslator
的 Translate
方法,在里面会发现一个很显眼的东西,没错这个就是材质结果结点的各个输入插槽
// 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);
于是我们去看 FMaterialResource
的 CompilePropertyAndSetMaterialProperty
方法
注意一下 FMaterialResource
是 FMaterial
接口的一个实现,它里面会存一个 UMaterial
实例或者 UMaterialInstance
实例,这两种实例都是继承自 UMaterialInterface
接口
这个方法里面有一个 switch
,对于不同类型的输入插槽(或者说 EMaterialProperty
),它有不同的处理,不过基本都是调用 UMaterialInterface
的 CompileProperty
方法,代码太乱就不贴了
我们直接去看 UMaterialInterface
的 CompileProperty
方法,它调用了 CompilePropertyEx
方法,这个方法是个虚函数,发现 UMaterial
和 UMaterialInterface
都有自己的实现,但是 UMaterialInterface
是基于 UMaterial
的因此我们直接看 UMaterial
的 CompilePropertyEx
方法就行
发现里面又是一个 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
方法,比如说 BaseColor
,BaseColor
变量是一个 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);
}
发现有常量和表达式两种情况,常量就是一个常量的颜色结点,表达式就是使用了混合结点或者纹理结点等等复杂的结点
对于表达式我们看到它调用了基类 FExpressionInput
的 Compile
方法
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;
}
又调用了 FMaterialCompiler
的 CallExpression
方法,而且把表达式传过去了
在 CallExpression
方法里,它会调用表达式 ExpressionKey.Expression
的 Compile
方法
然后我们可以看到 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);
}
可以看到递归是在这里实现的,然后最后一行 Complier
的 Add
方法是由 FHLSLMaterialTranslator
提供的
返回值是这个表达式的唯一索引值,可以通过索引找到表达式
生成 HLSL 代码的逻辑就在 FHLSLMaterialTranslator
的 Add
方法里,我们也可以看一下
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));
}
}
类的关系整理
最后我整理了一下上面提到的各个类之间的关系,总的来说还是比较清晰的
FMaterial
和 FMaterialResource
是逻辑上的材质类,一个 FMaterialResource
对象可以管理一个 UMaterial
实例或 UMaterialInstance
实例,UMaterial
上挂着不同类型的 FExpressionInput
插槽,这些 FExpressionInput
实现类又包含 UMaterialExpression
表达式,表达式会层层嵌套形成一张有向无环图
最后 FMaterialCompiler
的实现类 FHLSLMaterialTranslator
主要用于生成 HLSL 代码