UE材质的if节点并不是一个真正的分支,而是将A > B,A == B,A < B三个分支都计算一遍再最终选择一个结果,如果需要真正带[branch]的if还需要自己实现一个,虽然大多数时候分支代码指令数不多时不推荐使用,但偶尔可能还是有必要,至少是一个选择。
不同材质节点的实现方式和难度都不太一样,简单的自定义节点可能只需要在Compile函数中定义自己要转换输出的HLSL代码,但DynamicIf不仅仅是输出一个[branch]字符串那么简单,它需要将之前生成的代码都移到自己的分支中,也要考虑怎么让使用者直观定义哪些表达式放在if外哪些放在if内,需要考虑表达式被其它地方引用,这些改动都会涉及UE材质节点编译的一些细节。
在具体的修改代码之前先简介下UE的一些代码。
1.UE的材质节点编译是从结果开始递归调用函数编译,从根节点开始,不断调用子节点生成,其入口在这里
材质节点编译入口
2.每编译一个节点,如果生成表达式,那么都会加入到当前CodeChunk数组中
new(*CurrentScopeChunks) FShaderCodeChunk(Hash, *LocalVariableDefinition,SymbolName,Type,false);
CodeChunk中的Definition即为其对应的HLSL代码
每个ShaderFrequency(即SF_Vertex、Pixel之类的)对应一个CodeChunk数组
3.递归过程中每个子节点都会向父节点返回一个CodeID对应当前CodeChunk数组下标,会有两种检查检查是否重复,尽量不生成重复的CodeChunk
一个是根据ExpressionKey的查重
也就是同一个表达式连接多个地方的情况
另一个是CodeChunk本身的查重,即生成的字符串是否相同,如果相同则不重复生成
4.全部生成完后遍历每个MP_XXX的Chunk,按序将从Start到End编号的CodeChunk Definition连接起来
由前面的分析其实可以感觉到,DynamicIf实现的关键就在于知道每个分支都有哪些ChunkID,并将它们移到if内,同时移除if外。
问题1:如何知道每个分支下有哪些ChunkID?
我们知道节点是递归编译的,所以很容易想到能不能在子节点返回的时候不仅返回一个结果的CodeChunkID,而是返回一个代表所有子节点的数组?
理论上是可行的,可惜改动量非常大,需要改到每一个节点的Compile函数(会改到200+个文件),有毅力去改可能都是小事,更多是引擎升级维护等工程上的问题,有点不太现实。
另外一种做法就在每个分支编译前后调用特殊的函数开始与结束记录
只要开始了记录,那么每次AddCodeChunkInner后都会将新生成的节点加入Trace数组
注意
(*CurrentScopeChunks)[CodeIndex].Trace = *LastTrace;
这样做是因为一旦ExpressionKey重复,那么后续的编译是不会继续的,需要使用上次的结果
问题2:如何生成DynamicIf HLSL代码?
有了CodeChunkID后就好做多了,我们只需要对应字符串连接起来即可
FString AGreaterThanBDefs = GetDefinitions(*CurrentScopeChunks, AGreaterThanB);
FString AEqualsBDefs = GetDefinitions(*CurrentScopeChunks, AEqualsB);
FString ALessThanBDefs = GetDefinitions(*CurrentScopeChunks, ALessThanB);
FString UniqueCode = FString::Printf(TEXT("DynamicIfUniqueKey%d"), UniqueID);
int32 Result = AddCodeChunk(ResultType, *UniqueCode);
FShaderCodeChunk& CodeChunk = (*CurrentScopeChunks)[Result];
CodeChunk.bCompilingPreviousFrame = bCompilingPreviousFrame;
CodeChunk.Definition = CodeChunk.Definition.Replace(*UniqueCode, TEXT("0"));
FString IfCode;
if(bGreaterExpression && bEqualExpression && bLessExpression)
{
IfCode = FString::Printf(
TEXT("\
[branch]\n\
if(abs(%s - %s) < %s)\n\
{\n\
%s\
%s = %s;\n\
}\n\
else if(%s > %s)\n\
{\n\
%s\
%s = %s;\n\
}\n\
else\n\
{\n\
%s\
%s = %s;\n\
}\n\
"),
*GetParameterCode(A),
*GetParameterCode(B),
*GetParameterCode(ThresholdArg),\
*AEqualsBDefs,
*CodeChunk.SymbolName,
*GetParameterCode(AEqualsB.Last()),
*GetParameterCode(A),
*GetParameterCode(B),
*AGreaterThanBDefs,
*CodeChunk.SymbolName,
*GetParameterCode(AGreaterThanB.Last()),
*ALessThanBDefs,
*CodeChunk.SymbolName,
*GetParameterCode(ALessThanB.Last())
);
}
原生的GetDefinitions不太满足要求,所以自定义了一个
FString FHLSLMaterialTranslator::GetDefinitions(TArray<FShaderCodeChunk>& CodeChunks, const TArray<int32> &GetChunks) const
{
FString Definitions;
TSet<int32> Added;
for (int32 ChunkIndex : GetChunks)
{
if(ChunkIndex == INDEX_NONE)
{
continue;
}
const FShaderCodeChunk& CodeChunk = CodeChunks[ChunkIndex];
if(CodeChunk.bSkip)
{
continue;
}
// Uniform expressions (both constant and variable) and inline expressions don't have definitions.
if (!CodeChunk.UniformExpression && !CodeChunk.bInline && !Added.Contains(ChunkIndex))
{
Definitions += CodeChunk.Definition;
Added.Add(ChunkIndex);
}
}
return Definitions;
}
问题3:一些细节
问题1、2讲了最关键的思路,有了关键思路后大家应该也都能自己摸索着实现出来,这里再讲一些细节。
留意前面的代码也会发现,我这边给CodeChunk加了一些成员变量,比如bSkip,在if用完后if用到的代码均不应该被使用,否则会发现最终if内的代码在if前也都会生成一次,所以做一个标记,这样在后续GetDefinitions的时候方便跳过。
Trace前面也有提到了,就是方便查找已经生成的节点子树生成的结果是哪些。
UniqueID是DynamicIf节点本身的标志ID(函数指针),这个和我个人对用法的设计有关,为了避免普通用户歧义,这边直接禁止掉当前DynamicIf内的节点连接到DynamicIf外。
const auto LastTrace = GetLastTrace();
if(LastTrace)
{
if(CodeChunk.UniqueID == 0 || CodeChunk.UniqueID != GetLastTraceID())
{
return Compiler->Errorf(TEXT("To avoid ambiguity, expressions under dynamicif must not be connected outside current dynamicif."));
}
LastTrace->Append(CodeChunk.Trace);
LastTrace->Add(*ExistingCodeIndex);
}
else
{
if(CodeChunk.UniqueID != 0)
{
return Compiler->Errorf(TEXT("To avoid ambiguity, expressions under dynamicif must not be connected outside current dynamicif."));
}
}
bCompilingPreviousFrame的话和UE编译的机制有关,对于连接WorldPosition Offset的节点,UE其实会生成两次,一次bCompilingPreviousFrame为false,一次bCompilingPreviousFrame为true,生成出来分别对应GetMaterialWorldPositionOffset和GetMaterialPreviousWorldPositionOffset函数。这里UE4本身有个Bug,如果是WorldPosition Offset的话生成出来的代码会重复,本身来说是不影响因为最后编译会优化掉。
但我们如果要取一个CodeChunk的定义并修改的话就得注意了,
FShaderCodeChunk& CodeChunk = (*CurrentScopeChunks)[Result];
CodeChunk.bCompilingPreviousFrame = bCompilingPreviousFrame;
一定不要拿到同样的CodeChunk,否则WorldPosition Offset下生成出来的代码就是错的。
if ((*CurrentScopeChunks)[i].Hash == Hash && (*CurrentScopeChunks)[i].bCompilingPreviousFrame == bCompilingPreviousFrame && TraceIDValid)
问题4:使用者如何自定义哪些节点在if外?
现在我们可以正常生成branch if的表达式了,但使用上还有点问题,特殊情况下我们无法定义哪些节点放在if外,因此我们再加个节点,叫做BeginDynamicIf。
这个节点实现的核心就在于编译到BeginDynamicIf时对其子节点停用掉当前的跟踪ID函数。
int32 UMaterialExpressionBeginDynamicIf::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
if(!Compiler->PauseLastTraceRecord())
{
return Compiler->Errorf(TEXT("BeginDynamicIf out dynamicif nesting levels."));
}
int32 Result = Input.Compile(Compiler);
Compiler->ResumeLastTraceRecord();
return Compiler->BeginDynamicIf(Result, reinterpret_cast<intptr_t>(this));
}
int32 FHLSLMaterialTranslator::BeginDynamicIf(int32 Input, intptr_t UniqueID)
{
if(Input == INDEX_NONE)
{
return INDEX_NONE;
}
FString UniqueCode = FString::Printf(TEXT("BeginDynamicIfUniqueKey%d"), UniqueID);
int32 Result = AddCodeChunk(GetParameterType(Input), *UniqueCode);
FShaderCodeChunk& CodeChunk = (*CurrentScopeChunks)[Result];
CodeChunk.bCompilingPreviousFrame = bCompilingPreviousFrame;
CodeChunk.Definition = CodeChunk.Definition.Replace(*UniqueCode, *GetParameterCode(Input));
return Result;
}
保证BeginDynamicIf一定会被编译:
int32 FHLSLMaterialTranslator::CallExpression(FMaterialExpressionKey ExpressionKey,FMaterialCompiler* Compiler)
{
...
const UMaterialExpressionBeginDynamicIf* BeginDynamicIf = Cast<UMaterialExpressionBeginDynamicIf>(ExpressionKey.Expression);
if (ExistingCodeIndex && BeginDynamicIf == nullptr)
材质蓝图:
HLSL:
DXBC:
备注:如果if嵌套的话偶尔会报duplicate attribute branch的错误(反正可能也这么写的也少),取决于hlsl编译器,这点官方说明似乎也比较少,如果有了解的大佬欢迎说明了。