UE4:SkeletalMesh的Material Slot和LOD Section

需求

需要检查每一层LOD的所有Section用到的材质
如果材质是消耗比较大的并且LOD大的情况下,直接关闭消耗较大的section

需要注意的是这里的一个机制
在SkeletalMesh中有MaterialSlots,每一级LOD的每一个Section可以使用Slots中的某一个材质

在默认的情况下,材质Slot的数量是等于Section的数量的,为了方便在不同的LOD层级切换材质,UE是允许在编辑器中给SkeletalMesh增加材质Slot的。
默认情况下,section和材质Slot一一对应,索引一致。如果某一级LOD用到了非默认的材质,会在该级LODInfo中产生一个remapping的数组,叫做LODMaterialMap,大小等于Section数量,如果使用的还是默认的MaterialSlot对应的材质则index为-1,否则就是指定的MaterialSlot的index

举个例子

这里有一个默认三个Section的SkeletalMesh,在编辑器中的默认情况如下:

MaterialSlot

其中MaterialSlot长这样:

MaterialSlot

注意这个加号按钮,说明他是可以自定义添加新的材质的。

再说会上面那个图,每个Section的MaterialSlot中显示的分别是【索引+SlotName】

Index

Name

如果我换一个,他后面就标注上了Modified

Modified

但是只有在LOD Auto的情况下会是这样,切换成某一个LOD级别之后就没有default和modify的标记了。
这里我把该变量改为在Editor中可见,修改了LOD0的三个Section的MaterialSlot,如下图:

LODMaterialMap

尝试一

回到需求,需要对LOD的指定级的每一个Section检查,确认其真正使用的材质是否是消耗较大的材质,是的话直接关闭该Section

最后从游戏的逻辑层调用时,仅会传递LOD层级和是否显示两个参数。

从LOD的层级,获取到该层级对应的LODInfo中的LODMaterialMap,
如果该Map是空的,说明每一个Section是顺序对应所有的MaterialSlot的,直接遍历MaterialSlot

如果有值,则确认LODMaterialMap每一个对应的Slot,应该是这样的:

1
LODMaterialMap[SectionIndex] = MaterialSlotIndex

如果MaterialSlotIndex是-1,则说明这个Section对应的实际上的MaterialSlotIndex其实是等于SectionIndex的,也就是说如果MaterialSlotIndex不是-1,就去检查MaterialSlotIndex对应的那个材质,如果是-1,就去检查这个MaterialSlotIndex的SectionIndex的值对应的MaterialSlot的那个材质。

为什么不直接就写上呢,re-mapping不累吗……(划掉)

考虑直接把映射关系一步到位:

SkeletalMesh

  • LODs
    • LOD0
      • Section0 - RealMaterialIndexOfSection0
      • Section1 - RealMaterialIndexOfSection1
      • Section2 - RealMaterialIndexOfSection2
    • LOD1
      • ……

由于最后的使用只需要SectionIndex,因为是根据材质的消耗情况开关Section,所以最后只记录了SectionIndex。

第一个版本的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
CostlyLODSectionMap.Empty();
for (USkinnedMeshComponent* SkinnedMeshComp : SkinnedMeshComps)
{
TMap<int32, TArray<int32>> LODIndexToCostlySectionIndex;
int LODInfoNum = SkinnedMeshComp->SkeletalMesh->GetLODInfoArray().Num();
// 检查每一级的LOD
for(int LODIndex = 0; LODIndex < LODInfoNum; LODIndex++)
{
TArray<int32> CostlySectionIndices;
const TArray<FSkelMeshSection>& LODSections = SkinnedMeshComp->SkeletalMesh->GetImportedModel()->LODModels[LODIndex].Sections;
FSkeletalMeshLODInfo* LODInfoPtr = SkinnedMeshComp->SkeletalMesh->GetLODInfo(LODIndex);
const TArray<int32>& LODMaterialMap = LODInfoPtr->LODMaterialMap;
// 检查该层级的所有Section
for (int LODSectionIndex = 0; LODSectionIndex < LODSections.Num(); LODSectionIndex++)
{
UMaterialInterface* Material = nullptr;

int32 index = 0;
// 如果LODMaterialMap不是空的,并且这一个Section的对应的值是-1,说明该Section对应的真正的MaterialSlot的索引等于SectionIndex
if (LODMaterialMap.Num() > 0 && LODMaterialMap[LODSectionIndex] != -1)
{
Material = SkinnedMeshComp->GetMaterial(LODMaterialMap[LODSectionIndex]);
index = LODMaterialMap[LODSectionIndex];
}
else
{
Material = SkinnedMeshComp->GetMaterial(LODSectionIndex);
index = LODSectionIndex;
}
// 检查这个材质是不是高消耗
UMaterialInterface* MaterialInterface = Material ? Material->GetBaseMaterial() : Material;
if (MaterialInterface && Settings->CostlySKMaterialFNameSet.Contains(MaterialInterface->GetFName()))
{
// 最后记录的是Section的index,因为要关闭显示的是Section,而且可以重新通过Section的索引获取对应的材质的slot索引
CostlySectionIndices.Add(LODSectionIndex);
}
}
if (CostlySectionIndices.Num() > 0)
{
LODIndexToCostlySectionIndex.Add(LODIndex, CostlySectionIndices);
}
}
if (LODIndexToCostlySectionIndex.Num() > 0)
{
CostlyLODSectionMap.Emplace(SkinnedMeshComp, LODIndexToCostlySectionIndex);
}
}

尝试二

在完成第一个版本之后,遇到一个SkeletalMesh之后崩掉了……问题是数组越界,在LODMaterialMap[LODSectionIndex]这里数组越界了。

检查后发现,存在以下使用情况,在第一个版本中没有考虑到。

在我们将资源导入到编辑器中的时候,Section的数量和MaterialSlot的数量是一致的,Slot可以点加号来增加,增加的Slot后面有X号可以点击删除。

但如果我们Add的这个Slot被某一个Section引用了,那么删除按钮就会隐藏。

如果Section的材质不是默认的顺序的index,在这种情况下,虽然有LODMaterialMap数组,但是数组大小并不等于Section的数量。

比如现在这个情况:

LOD1

可以看到,这是LOD1,并且Section0的材质修改掉了,按照之前的想法我应该有一个[1,-1]的LODMaterialMap,但是事实上是这样的:

LODMaterialMap

他只有一个元素。

具体为什么会有这样的情况,还没有细究……
总之目前可以知道的是,LODMaterialMap的数组大小并不一定等于Section的数量

版本2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
CostlyLODSectionMap.Empty();
for (USkinnedMeshComponent* SkinnedMeshComp : SkinnedMeshComps)
{
if (! IsValid(SkinnedMeshComp->SkeletalMesh))
{
continue;
}
TMap<int32, TArray<int32>> LODIndexToCostlySectionIndex;
int LODInfoNum = SkinnedMeshComp->SkeletalMesh->GetLODInfoArray().Num();
// 检查每一级的LOD
for(int LODIndex = 0; LODIndex < LODInfoNum; LODIndex++)
{
TArray<int32> CostlySectionIndices;
const TArray<FSkelMeshSection>& LODSections = SkinnedMeshComp->SkeletalMesh->GetImportedModel()->LODModels[LODIndex].Sections;
FSkeletalMeshLODInfo* LODInfoPtr = SkinnedMeshComp->SkeletalMesh->GetLODInfo(LODIndex);
const TArray<int32>& LODMaterialMap = LODInfoPtr->LODMaterialMap;
// 检查该层级的所有Section
for (int LODSectionIndex = 0; LODSectionIndex < LODSections.Num(); LODSectionIndex++)
{
int32 MaterialIndex = GetMaterialIndexByLODSection(LODMaterialMap, LODSectionIndex);
UMaterialInterface* Material = SkinnedMeshComp->GetMaterial(MaterialIndex);

// 检查这个材质是不是高消耗
UMaterialInterface* MaterialInterface = Material ? Material->GetBaseMaterial() : Material;
if (MaterialInterface && Settings->CostlySKMaterialFNameSet.Contains(MaterialInterface->GetFName()))
{
// 最后记录的是Section的index,因为要关闭显示的是Section,而且可以重新通过Section的索引获取对应的材质的slot索引
CostlySectionIndices.Add(LODSectionIndex);
}
}
if (CostlySectionIndices.Num() > 0)
{
LODIndexToCostlySectionIndex.Add(LODIndex, CostlySectionIndices);
}
}
if (LODIndexToCostlySectionIndex.Num() > 0)
{
CostlyLODSectionMap.Emplace(SkinnedMeshComp, LODIndexToCostlySectionIndex);
}
}

其中抽出来的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int32 GetMaterialIndexByLODSection(const TArray<int32>& LODMaterialMap, const int32& LODSectionIndex)
{
int32 MaterialIndex = 0;
// 如果LODMaterialMap不是空的,并且这一个Section的对应的值是-1,说明该Section对应的真正的MaterialSlot的索引等于SectionIndex
if (LODMaterialMap.Num() > 0 && LODMaterialMap.IsValidIndex(LODSectionIndex))
{
if(LODMaterialMap[LODSectionIndex] != -1)
{
MaterialIndex = LODMaterialMap[LODSectionIndex];
}
else
{
MaterialIndex = LODSectionIndex;
}
}
else
{
MaterialIndex = LODSectionIndex;
}
return MaterialIndex;
}

尝试三

然后打包编译的时候又出问题了……

在获取Section数量的时候,使用到了

1
const TArray<FSkelMeshSection>& LODSections = SkinnedMeshComp->SkeletalMesh->GetImportedModel()->LODModels[LODIndex].Sections;

这个方法和获取到的结构体都是WITH_EDITOR的,打包的时候就报错了。

那么意味着上面的思路就得改掉了,因为我没发现有在别的地方可以获取到Section的数量等相关信息了,于是换了个思路:

由于发现了LODMaterialMap不是等长的,就不能直接按照它的长度来遍历Section,可能会有遗漏,那目前能确认的是Materials的数量,而且能够确保,它的数量一定大于等于Section的数量,这样就不会在检查的时候出现遗漏,但是会有获取的Index大于Section数量造成数组越界的情况。

于是检查了一下在开关Section的地方的具体实现:

SectionShow

发现这里有检查索引有效与否,那就没有问题了。

最后的版本中,遍历一遍所有的材质,确认哪些是高消耗材质,再去确认LODMaterialMap,如果该等级的LOD的LODMaterialMap是空的,说明每一个Section用的都是Default的索引,那直接Append获取到的高消耗的材质的数组。

如果有的话,那就对LODMaterialMap遍历,-1去获得默认值,但是遍历的长度必须要是Materials的长度,不然可能会有遗漏。

至于超出的部分,会在使用的时候检查有效性,完毕。

版本3的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
CostlyLODSectionMap.Empty();
for (USkinnedMeshComponent* SkinnedMeshComp : SkinnedMeshComps)
{
USkeletalMesh* SkeletalMesh = SkinnedMeshComp->SkeletalMesh;
if (! IsValid(SkeletalMesh))
{
continue;
}
TMap<int32, TArray<int32>> LODIndexToCostlySectionIndex;
// 遍历一遍所有的材质,确认哪些是高消耗材质
int32 MaterialNum = SkeletalMesh->GetMaterials().Num();
TArray<int32> CostlyMaterialIndex;
for (int i = 0; i < MaterialNum; i++)
{
UMaterialInterface* Material = SkinnedMeshComp->GetMaterial(i);
UMaterialInterface* MaterialInterface = Material ? Material->GetBaseMaterial() : Material;
if (MaterialInterface && Settings->CostlySKMaterialFNameSet.Contains(MaterialInterface->GetFName()))
{
CostlyMaterialIndex.Add(i);
}
}
// 检查每一级的LOD
int LODInfoNum = SkeletalMesh->GetLODInfoArray().Num();
for(int LODIndex = 0; LODIndex < LODInfoNum; LODIndex++)
{
TArray<int32> CostlySectionIndices;
// 运行时无法获得Section数量等信息
FSkeletalMeshLODInfo* LODInfoPtr = SkinnedMeshComp->SkeletalMesh->GetLODInfo(LODIndex);
const TArray<int32>& LODMaterialMap = LODInfoPtr->LODMaterialMap;
if (LODMaterialMap.Num() == 0)
{
CostlySectionIndices.Append(CostlyMaterialIndex);
}
// LODMaterialMap的Num可能既不等于Section数量也不等于Materials的数量
// 需要注意LODMaterialMap的Num比Section数量小的情况
// 这里记录的SectionIndex可能会比实际的Section数量大,造成越界,但是没关系,使用到的时候有valid检查
for (int i = 0; i < MaterialNum; i++)
{
int32 LODSectionIndex = i;
int32 MaterialIndex = GetMaterialIndexByLODSection(LODMaterialMap, LODSectionIndex);
if(CostlyMaterialIndex.Contains(MaterialIndex))
{
CostlySectionIndices.Add(LODSectionIndex);
}
}
if (CostlySectionIndices.Num() > 0)
{
LODIndexToCostlySectionIndex.Add(LODIndex, CostlySectionIndices);
}
}
if (LODIndexToCostlySectionIndex.Num() > 0)
{
CostlyLODSectionMap.Emplace(SkinnedMeshComp, LODIndexToCostlySectionIndex);
}
}

总结

对于SkeletalMesh:

  1. 导入时,Material Slot和Section一一对应
  2. Material Slot可以添加,但是被引用后不能删除,需要先解除引用
  3. 如果Section使用的Material不是默认的索引,会产生LODMaterialMap数组作为Re-Mapping
  4. LODMaterialMap数组中,-1表示和Default相同
  5. LODMaterialMap数组的长度可能小于Section的数量
  6. 即使该等级的LOD的Section使用的都是Default的索引,依然会有可能出现一个全是-1的LODMaterialMap
  7. 获取Sections的方法和相关结构体是WITH_EDITOR的

UE4:SkeletalMesh的Material Slot和LOD Section
http://muchenhen.com/posts/36616/
作者
木尘痕
发布于
2022年8月31日
许可协议