UE4:自定义材质属性节点规避材质编译问题

前言

本文是一次问题和解决的记录,其中相关的具体细节有部分我并不完全理解,无法作出有效总结……

问题

新增ShaderingModel之后,使用了原本的CustomData0和CustomData1材质属性,结果发现在编译后的metal文件中,存在相同的局部变量,进行了重复采样,导致在ios上相关的材质GPU消耗直接翻倍

拿一段生成的metal来做例子,如下:

1
2
3
4
5
float4 _197 = Material_Texture2D_1.sample(Material_Texture2D_1Sampler, float2(in_var_TEXCOORD0[0].x, in_var_TEXCOORD0[0].y));
half4 _198 = half4(_197);
float4 _214 = Material_Texture2D_2.sample(Material_Texture2D_2Sampler, float2(half2(min(max(_193 - (half(1.0) - half(Material.Material_ScalarExpressions[0].x * float(_198.x))), half(0.0)), half(1.0)), half(0.5))));
float4 _280 = Material_Texture2D_1.sample(Material_Texture2D_1Sampler, float2(in_var_TEXCOORD0[0].x, in_var_TEXCOORD0[0].y));
float4 _293 = Material_Texture2D_2.sample(Material_Texture2D_2Sampler, float2(half2(min(max(_193 - (half(1.0) - half(Material.Material_ScalarExpressions[0].x * float(half4(_280).x))), half(0.0)), half(1.0)), half(0.5))));

仔细看一下就会发现
把_198替换成half4(_197)

1
2
float4 _214 = Material_Texture2D_2.sample(Material_Texture2D_2Sampler, float2(half2(min(max(_193 - (half(1.0) - half(Material.Material_ScalarExpressions[0].x * float(half4(_197).x))), half(0.0)), half(1.0)), half(0.5))));
float4 _293 = Material_Texture2D_2.sample(Material_Texture2D_2Sampler, float2(half2(min(max(_193 - (half(1.0) - half(Material.Material_ScalarExpressions[0].x * float(half4(_280).x))), half(0.0)), half(1.0)), half(0.5))));

此时214和293只有197和280这俩不一样
再看一下197和280

1
2
float4 _197 = Material_Texture2D_1.sample(Material_Texture2D_1Sampler, float2(in_var_TEXCOORD0[0].x, in_var_TEXCOORD0[0].y));
float4 _280 = Material_Texture2D_1.sample(Material_Texture2D_1Sampler, float2(in_var_TEXCOORD0[0].x, in_var_TEXCOORD0[0].y));

一模一样啊……
_280与_197一样……
也就是说_293与_214一模一样……
sample直接重复了一次,没有合并优化掉…

据leader和大佬们分析,是因为我们在自定义ShadingModel的时候使用了原本EMaterialProperty的MP_CustomData0和MP_CustomData1,所以最后在metal文件中这两个相关的值没有成功的被优化合并。

尝试策略

leader让我添加新的自定义的材质属性,但是要注意不要和customData0和customData1一致,参考一下基础的几个材质属性,看一下这样能不能成功在metal里优化掉这个问题

打开引擎的r.DumpShaderDebugInfo选项后在材质编译生成的.rewritten.hlsl里看到

1
half3 GetMaterialCustomData0(FMaterialPixelParameters Parameters)

CustomData0是在FMaterialPixelParameters结构体里面的

这个结构体在MaterialTemplate.ush

1
2
3
4
5
6
7
8
/** 
* Parameters needed by pixel shader material inputs, related to Geometry.
* These are independent of vertex factory.
*/
struct FMaterialPixelParameters
{
// 太长了......
};

但是有些属性比如透明度是这样的:

1
half GetMaterialOpacity(FPixelMaterialInputs PixelMaterialInputs)

是在结构体FPixelMaterialInputs里的

这个结构体也在aterialTemplate.ush

1
2
3
4
5
6
7
/** 
* Parameters calculated from the pixel material inputs.
*/
struct FPixelMaterialInputs
{
%s
};

那么如果我们自定义一个材质属性用来给自定义的ShadingModel输入值的话,将这个材质属性和透明度保持近似,也放在FPixelMaterialInputs结构体里面,应该可以成功优化掉,而不是像customData一样。

FDeferredShadingSceneRenderer::Render()中会调用RenderBasePass(),之后在BasePassPixelShader.usf中会计算GBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FGBufferData GBuffer = (FGBufferData)0;

GBuffer.GBufferAO = MaterialAO;
GBuffer.PerObjectGBufferData = GetPrimitiveData(MaterialParameters.PrimitiveId).PerObjectGBufferData;
GBuffer.Depth = MaterialParameters.ScreenPosition.w;
GBuffer.PrecomputedShadowFactors = GetPrecomputedShadowMasks(LightmapVTPageTableResult, Interpolants, MaterialParameters.PrimitiveId, MaterialParameters.AbsoluteWorldPosition, VolumetricLightmapBrickTextureUVs);

const float GBufferDither = InterleavedGradientNoise(MaterialParameters.SvPosition.xy, View.StateFrameIndexMod8);
// Use GBuffer.ShadingModelID after SetGBufferForShadingModel(..) because the ShadingModel input might not be the same as the output
SetGBufferForShadingModel(
GBuffer,
MaterialParameters,
Opacity,
BaseColor,
Metallic,
Specular,
Roughness,
Anisotropy,
SubsurfaceColor,
SubsurfaceProfile,
GBufferDither,
ShadingModel
);

SetGBufferForShadingModel函数位于ShadingModelsMaterial.ush中

在SetGBufferForShadingModel的函数体中可以看到部分参数是直接赋值的,但是GBuffer.CustomData要根据不同的ShaderModel进行赋值。

在UE4本身原有的ShaderModel中,SHADINGMODELID_CLEAR_COAT有使用CustomData0和CustomData1,代码是这样的:

1
2
3
4
5
6
7
8
9
else if (ShadingModel == SHADINGMODELID_CLEAR_COAT)
{
float ClearCoat = saturate( GetMaterialCustomData0(MaterialParameters) );
float ClearCoatRoughness = saturate( GetMaterialCustomData1(MaterialParameters) );
GBuffer.CustomData.x = ClearCoat;
GBuffer.CustomData.y = ClearCoatRoughness;

// ……省略部分代码
}

而里面调用到的GetMaterialCustomData0和GetMaterialCustomData1来自MaterialTemplate.ush

1
2
3
4
5
6
7
8
9
half GetMaterialCustomData0(FMaterialPixelParameters Parameters)
{
%s;
}

half GetMaterialCustomData1(FMaterialPixelParameters Parameters)
{
%s;
}

可以看出这里参数是从FMaterialPixelParameters获取的

而像透明度之类的参数是这样的:

1
2
3
4
half GetMaterialOpacityRaw(FPixelMaterialInputs PixelMaterialInputs)
{
return PixelMaterialInputs.Opacity;
}

这里参数是FPixelMaterialInputs

那么把自定义参数也加到这个里面去,并提供类似的Get函数

1
2
3
4
5
6
7
8
9
half3 GetMaterialCustomData3(FPixelMaterialInputs PixelMaterialInputs)
{
return PixelMaterialInputs.CustomData3;
}

half GetMaterialCustomData4(FPixelMaterialInputs PixelMaterialInputs)
{
return PixelMaterialInputs.CustomData4;
}

在BasePassPixelShader.usf中,调用SetGBufferForShadingModel的时候将新增加的变量也传过去

在ShadingModelsMaterial.ush中

void SetGBufferForShadingModel函数

根据不同的ShadingModel,GBuffer需要有不同的赋值操作,对于我们新增的着色模型,之前是通过CustomData0和customData1获得的,在这里改成直接传进来的两个新增的自定义参数

完成以上步骤之后,再次检查了编译后的debug信息以及对metal平台进行测试,原本没有被优化掉的sample成功优化掉了

总结

在自定义ShadingModel的时候,如果使用CustomData0和CustomData1两个材质属性,可能,有可能会在部分平台(比如metal)存在shader编译的优化问题,导致性能表现较差,可以考虑使用自定义材质属性节点来使材质编译能够正确优化。

参考

关于如果自定义ShadingModel和新增材质属性,这篇大佬写的很仔细,手把手教学型:
UE4 shadingmodel终极版之如何新增材质接口(4.27)


UE4:自定义材质属性节点规避材质编译问题
http://muchenhen.com/posts/7908/
作者
木尘痕
发布于
2022年7月22日
许可协议