材质的基础概念
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之间的关联配对。
材质类型体系总结
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。