需求
需要检查每一层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长这样:
注意这个加号按钮,说明他是可以自定义添加新的材质的。
再说会上面那个图,每个Section的MaterialSlot中显示的分别是【索引+SlotName】
如果我换一个,他后面就标注上了Modified
但是只有在LOD Auto的情况下会是这样,切换成某一个LOD级别之后就没有default和modify的标记了。
这里我把该变量改为在Editor中可见,修改了LOD0的三个Section的MaterialSlot,如下图:
尝试一
回到需求,需要对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(); 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; for (int LODSectionIndex = 0; LODSectionIndex < LODSections.Num(); LODSectionIndex++) { UMaterialInterface* Material = nullptr; int32 index = 0; 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())) { 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,并且Section0的材质修改掉了,按照之前的想法我应该有一个[1,-1]的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(); 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; 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())) { 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; 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的地方的具体实现:
发现这里有检查索引有效与否,那就没有问题了。
最后的版本中,遍历一遍所有的材质,确认哪些是高消耗材质,再去确认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); } } int LODInfoNum = SkeletalMesh->GetLODInfoArray().Num(); for(int LODIndex = 0; LODIndex < LODInfoNum; LODIndex++) { TArray<int32> CostlySectionIndices; FSkeletalMeshLODInfo* LODInfoPtr = SkinnedMeshComp->SkeletalMesh->GetLODInfo(LODIndex); const TArray<int32>& LODMaterialMap = LODInfoPtr->LODMaterialMap; if (LODMaterialMap.Num() == 0) { CostlySectionIndices.Append(CostlyMaterialIndex); } 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:
- 导入时,Material Slot和Section一一对应
- Material Slot可以添加,但是被引用后不能删除,需要先解除引用
- 如果Section使用的Material不是默认的索引,会产生LODMaterialMap数组作为Re-Mapping
- LODMaterialMap数组中,-1表示和Default相同
- LODMaterialMap数组的长度可能小于Section的数量
- 即使该等级的LOD的Section使用的都是Default的索引,依然会有可能出现一个全是-1的LODMaterialMap
- 获取Sections的方法和相关结构体是WITH_EDITOR的