UE4:MergeActors修改

前言

这次是给TA大佬打打杂~

针对使用需要修改了一下UE的MergeActors方法,方便美术使用,提高美术制作效率。

主要问题有:

  1. 工具Merge之后的Actor的中心轴点是(0,0,0),场景中分批合并后所有的Actor的中心轴点全都在中间,有的距离会非常远,不方便使用
  2. Merge后的Actor的名字是类类型+数字,和原本的不好对上,不直观
  3. 没有返回值,希望批量化处理的时候能拿到Merge的Actors进行其他的处理
  4. 按照我们的设置,只会Merge出一个Actor,但是由于StaticMeshComponents的不相同,每个合并出的Actor上会有不同的UInstancedStaticMeshComponent实例,不如直接合并成不相同的Actors,更方便一点

版本 4.27

相关结构体

FMeshInstancingSettings

一个参数结构体,用来方便传递参数,具体参数意义可以看下面的源码部分的注释

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
/** Mesh instance-replacement settings */
USTRUCT(Blueprintable)
struct FMeshInstancingSettings
{
GENERATED_BODY()

FMeshInstancingSettings()
: ActorClassToUse(AActor::StaticClass())
, InstanceReplacementThreshold(2)
, MeshReplacementMethod(EMeshInstancingReplacementMethod::KeepOriginalActorsAsEditorOnly)
, bSkipMeshesWithVertexColors(true)
, bUseHLODVolumes(true)
, ISMComponentToUse(UInstancedStaticMeshComponent::StaticClass())
{}

// 要将合并后的instance static mesh components附加到的actor的类
UPROPERTY(BlueprintReadWrite, EditAnywhere, NoClear, Category="Instancing")
TSubclassOf<AActor> ActorClassToUse;

// 如果static mesh数量小于这个值不会执行合并
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Instancing", meta=(ClampMin=1))
int32 InstanceReplacementThreshold;

// 是删掉原来的static mesh actor还是留在原地 这个枚举只有两个值
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Instancing")
EMeshInstancingReplacementMethod MeshReplacementMethod;

// 是否跳过有定点颜色的static mesh,实例静态网格不支持每个实例的顶点颜色,合并的话将丢失此数据
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Instancing")
bool bSkipMeshesWithVertexColors;

// 是否根据实instanced static mesh components与HLOD体积的交点拆分instanced static mesh components
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Instancing", meta=(DisplayName="Use HLOD Volumes"))
bool bUseHLODVolumes;

// 指定一下要用的InstancedStaticMeshComponent的类
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Instancing", meta = (DisplayName = "Select the type of Instanced Component", DisallowedClasses = "FoliageInstancedStaticMeshComponent"))
TSubclassOf<UInstancedStaticMeshComponent> ISMComponentToUse;
};

FComponentEntry

一个局部定义的结构体,主要目的是存储UStaticMeshComponent的一些数据,方便合并出的新的Component使用
没什么好说的,变量名都很直观

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
/** Helper struct representing a spawned ISMC */
struct FComponentEntry
{
FComponentEntry(UStaticMeshComponent* InComponent)
{
StaticMesh = InComponent->GetStaticMesh();
InComponent->GetUsedMaterials(Materials);
bReverseCulling = InComponent->GetComponentTransform().ToMatrixWithScale().Determinant() < 0.0f;
CollisionProfileName = InComponent->GetCollisionProfileName();
CollisionEnabled = InComponent->GetCollisionEnabled();
OriginalComponents.Add(InComponent);
}

bool operator==(const FComponentEntry& InOther) const
{
return
StaticMesh == InOther.StaticMesh &&
Materials == InOther.Materials &&
bReverseCulling == InOther.bReverseCulling &&
CollisionProfileName == InOther.CollisionProfileName &&
CollisionEnabled == InOther.CollisionEnabled;
}

UStaticMesh* StaticMesh;

TArray<UMaterialInterface*> Materials;

TArray<UStaticMeshComponent*> OriginalComponents;

FName CollisionProfileName;

bool bReverseCulling;

ECollisionEnabled::Type CollisionEnabled;
};

FActorEntry

一个局部结构体,要保存StaticMeshComponents生成的FComponentEntry,有一个MergedActor成员

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
/** Helper struct representing a spawned ISMC-containing actor */
struct FActorEntry
{
FActorEntry(UStaticMeshComponent* InComponent, ULevel* InLevel)
: MergedActor(nullptr)
{
// intersect with HLOD volumes if we have a level
if(InLevel)
{
for (AActor* Actor : InLevel->Actors)
{
if (AHierarchicalLODVolume* HierarchicalLODVolume = Cast<AHierarchicalLODVolume>(Actor))
{
FBox BoundingBox = InComponent->Bounds.GetBox();
FBox VolumeBox = HierarchicalLODVolume->GetComponentsBoundingBox(true);

if (VolumeBox.IsInside(BoundingBox) || (HierarchicalLODVolume->bIncludeOverlappingActors && VolumeBox.Intersect(BoundingBox)))
{
HLODVolume = HierarchicalLODVolume;
break;
}
}
}
}
}

bool operator==(const FActorEntry& InOther) const
{
return HLODVolume == InOther.HLODVolume;
}

AActor* MergedActor;
AHierarchicalLODVolume* HLODVolume;
TArray<FComponentEntry> ComponentEntries;
};

原处理流程

处理Static Mesh Components

  1. 收集有效的Static Mesh Components,根据需要选择是否跳过有顶点颜色的Component
  2. 生成StaticMeshComponent的FActorEntry
  3. 对所有有效的Components生成对应FComponentEntry
  4. 将ComponentEntry添加到ActorEntry的ComponentEntries中
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
// 收集有效的Components
// 确认是否有顶点颜色
auto HasInstanceVertexColors = [](UStaticMeshComponent* StaticMeshComponent)
{
for (const FStaticMeshComponentLODInfo& CurrentLODInfo : StaticMeshComponent->LODData)
{
if(CurrentLODInfo.OverrideVertexColors != nullptr || CurrentLODInfo.PaintedVertices.Num() > 0)
{
return true;
}
}
return false;
};
// 存放Components
TArray<UStaticMeshComponent*> ValidComponents;
//
for(UPrimitiveComponent* ComponentToMerge : ComponentsToMerge)
{
if(UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(ComponentToMerge))
{
// 如果Components的父类类型和我们要用来挂InstanceMesh的类类型一样的话就不合并
// 不太明白为什么要有这一步处理,但是避免选了一堆StaticMeshActors然后要合并成一个StaticMeshActor的这种操作= =
// 毕竟,理论上来讲
// 要是用MergeActors这个功能就是为了把static mesh actors都合并成instances
if(StaticMeshComponent->GetOwner()->GetClass() != MeshInstancingSettings.ActorClassToUse.Get())
{
// 这个检查是不是要跳过有顶点颜色的Actor
if( !MeshInstancingSettings.bSkipMeshesWithVertexColors || !HasInstanceVertexColors(StaticMeshComponent))
{
ValidComponents.Add(StaticMeshComponent);
}
}
}
}

生成ActorEntries,这里如果bUseHLODVolumes是false的情况,那就只会有一个ActorEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// FActorEntry的容器
TArray<FActorEntry> ActorEntries;
for(UStaticMeshComponent* StaticMeshComponent : ValidComponents)
{
int32 ActorEntryIndex = ActorEntries.AddUnique(FActorEntry(StaticMeshComponent, InSettings.bUseHLODVolumes ? Level : nullptr));
FActorEntry& ActorEntry = ActorEntries[ActorEntryIndex];

// FComponentEntry构造的时候StaticMeshComponent就会add到OriginalComponents中
FComponentEntry ComponentEntry(StaticMeshComponent);

// 如果ActorEntry中已经有了一样的FComponentEntry,就直接添加对应的FComponentEntry中就可以
// 不然的话就是一个新的FComponentEntry
if(FComponentEntry* ExistingComponentEntry = ActorEntry.ComponentEntries.FindByKey(ComponentEntry))
{
ExistingComponentEntry->OriginalComponents.Add(StaticMeshComponent);
}
else
{
ActorEntry.ComponentEntries.Add(ComponentEntry);
}
}

筛选ActorEntries

1
2
3
4
5
6
7
8
9
10
11
// 返回OriginalComponents数量大于设置中要求的阈值的ActorEntry的ComponentEntry
for(FActorEntry& ActorEntry : ActorEntries)
{
ActorEntry.ComponentEntries = ActorEntry.ComponentEntries.FilterByPredicate([&InSettings](const FComponentEntry& InEntry)
{
return InEntry.OriginalComponents.Num() >= InSettings.InstanceReplacementThreshold;
});
}

// 如果ActorEntry的ComponentEntries是空的就去掉
ActorEntries.RemoveAll([](const FActorEntry& ActorEntry){ return ActorEntry.ComponentEntries.Num() == 0; });

保存原始StaticMeshActors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 通过OriginalComponents获取到原本的Static Mesh Actor
// 后面会根据Setting的参数决定这些是否要删除掉
TArray<AActor*> ActorsToCleanUp;
for(FActorEntry& ActorEntry : ActorEntries)
{
for(const FComponentEntry& ComponentEntry : ActorEntry.ComponentEntries)
{
for(UStaticMeshComponent* OriginalComponent : ComponentEntry.OriginalComponents)
{
if(AActor* OriginalActor = OriginalComponent->GetOwner())
{
ActorsToCleanUp.AddUnique(OriginalActor);
}
}
}
}

合并StaticMeshComponents

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Level->Modify();

FActorSpawnParameters Params;
Params.OverrideLevel = Level;

// We now have the set of component data we want to apply
for(FActorEntry& ActorEntry : ActorEntries)
{
// 创建新的Actor,这各Actor类类型默认是AActor
ActorEntry.MergedActor = World->SpawnActor<AActor>(InSettings.ActorClassToUse.Get(), Params);
// 遍历ComponentEntries
// 若果合并的Actors不完全相同,那么ComponentEntries就不会只有一个
// 最后的结果是一个Actor上会有多个UInstancedStaticMeshComponent
for(const FComponentEntry& ComponentEntry : ActorEntry.ComponentEntries)
{
UInstancedStaticMeshComponent* NewComponent = nullptr;

// ISMComponentToUse的默认值是UInstancedStaticMeshComponent
// 这里先从Actor身上试图获取一个UInstancedStaticMeshComponent
NewComponent = (UInstancedStaticMeshComponent*)ActorEntry.MergedActor->FindComponentByClass(InSettings.ISMComponentToUse.Get());
// 如果发现确实存在UInstancedStaticMeshComponent但是数据不是空的,还是回选择去新建一个
if (NewComponent && NewComponent->PerInstanceSMData.Num() > 0)
{
NewComponent = nullptr;
}

if (NewComponent == nullptr)
{
// 创建一个新的Component
NewComponent = NewObject<UInstancedStaticMeshComponent>(ActorEntry.MergedActor, InSettings.ISMComponentToUse.Get());

if (ActorEntry.MergedActor->GetRootComponent())
{
// 有root的话可以直接Attach
NewComponent->AttachToComponent(ActorEntry.MergedActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
}
else
{
// 没有root那么你就是root!
ActorEntry.MergedActor->SetRootComponent(NewComponent);
}

// Take 'instanced' ownership so it persists with this actor
ActorEntry.MergedActor->RemoveOwnedComponent(NewComponent);
NewComponent->CreationMethod = EComponentCreationMethod::Instance;
ActorEntry.MergedActor->AddOwnedComponent(NewComponent);

}

// 把原先的属性赋值给新的Component
NewComponent->SetStaticMesh(ComponentEntry.StaticMesh);
for(int32 MaterialIndex = 0; MaterialIndex < ComponentEntry.Materials.Num(); ++MaterialIndex)
{
NewComponent->SetMaterial(MaterialIndex, ComponentEntry.Materials[MaterialIndex]);
}
NewComponent->SetReverseCulling(ComponentEntry.bReverseCulling);
NewComponent->SetCollisionProfileName(ComponentEntry.CollisionProfileName);
NewComponent->SetCollisionEnabled(ComponentEntry.CollisionEnabled);
NewComponent->SetMobility(EComponentMobility::Static);
// 这里,就是把多个相同的原来的StaticMeshComponents合并成了一个UInstancedStaticMeshComponent,AddInstance
for(UStaticMeshComponent* OriginalComponent : ComponentEntry.OriginalComponents)
{
NewComponent->AddInstance(OriginalComponent->GetComponentTransform());
}

NewComponent->RegisterComponent();
}

World->UpdateCullDistanceVolumes(ActorEntry.MergedActor);
}

清理原始Actors

就是很简单的根据设置看看需不需要把被合并的那些Actors删掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(AActor* ActorToCleanUp : ActorsToCleanUp)
{
if(InSettings.MeshReplacementMethod == EMeshInstancingReplacementMethod::RemoveOriginalActors)
{
ActorToCleanUp->Destroy();
}
else if(InSettings.MeshReplacementMethod == EMeshInstancingReplacementMethod::KeepOriginalActorsAsEditorOnly)
{
ActorToCleanUp->Modify();
ActorToCleanUp->bIsEditorOnlyActor = true;
ActorToCleanUp->SetHidden(true);
ActorToCleanUp->bHiddenEd = true;
ActorToCleanUp->SetIsTemporarilyHiddenInEditor(true);
}
}

针对问题做出的改动

根据需求这里没有直接修改源码哦

Actor名字问题

这个容易,但是美术需要的其实是outline里面显示的那个名字,SpawnActor的Params的Name是Name不是显示出来的这个名字
这里直接取会被合并的一堆里面的第一个名字直接加后缀

1
2
3
4
5
6
7
8
9
10
11
12
if (ActorEntry.ComponentEntry.OriginalComponents.Num() > 0)
{
// 省略部分代码
MergedActorName = ActorEntry.ComponentEntry.OriginalComponents[0]->GetOuter()->GetName() + FString(TEXT("_merged"));
}
else
{
// 省略部分代码
MergedActorName = ActorsToCleanUp[0]->GetName() + FString(TEXT("_merged"));
}
ActorEntry.MergedActor->SetActorLabel(MergedActorName);

分成多个Actor

如果要分成多个Actor,那就说明每个Actor只有一个Component
先把结构体给改掉:
把之前的Array改为了一个对象,因为只需要一个

1
2
3
4
5
6
7
8
9
// 省略部分源码
struct FComponentEntry
{
AActor* MergedActor;
AHierarchicalLODVolume* HLODVolume;
- TArray<FComponentEntry> ComponentEntries;
+ FComponentEntry ComponentEntry;
};

然后还是生成ActorEntries
只有当Component生成的ComponentEntry完全相同的时候才会合并到一个Actor上
否则就会创建一个新的Actor

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
// Gather a list of components to merge
TArray<FActorEntry> ActorEntries;

auto GenActorEntry = [](FComponentEntry& ComponentEntry, TArray<FActorEntry>& ActorEntries, UStaticMeshComponent* StaticMeshComponent)
{
for (auto& ActorEntry : ActorEntries)
{
if (ActorEntry.ComponentEntry == ComponentEntry)
{
ActorEntry.ComponentEntry.OriginalComponents.Add(StaticMeshComponent);
return true;
}
}
FActorEntry ActorEntry = FActorEntry(StaticMeshComponent, nullptr);
ActorEntry.ComponentEntry = ComponentEntry;
ActorEntries.Add(ActorEntry);
return false;
};

for(UStaticMeshComponent* StaticMeshComponent : ValidComponents)
{
FComponentEntry ComponentEntry(StaticMeshComponent);
GenActorEntry(ComponentEntry, ActorEntries, StaticMeshComponent);
}

生成的Actor轴心点位置问题

不出意外的情况下,理论上OriginalComponents不可能是空的
对所有的OriginalComponents的位置去一个平均值,然后取整方便后续的计算使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (ActorEntry.ComponentEntry.OriginalComponents.Num() > 0)
{
for (auto& OriginalComponent:ActorEntry.ComponentEntry.OriginalComponents)
{
auto Actor = Cast<AActor>(OriginalComponent->GetOuter());
Position += Actor->GetActorLocation();
}
Position = Position / ActorEntry.ComponentEntry.OriginalComponents.Num();
Position = FVector(static_cast<int>(Position.X), static_cast<int>(Position.Y), static_cast<int>(Position.Z));
Position.Z = static_cast<int>(Cast<AActor>(ActorEntry.ComponentEntry.OriginalComponents[0]->GetOuter())->GetActorLocation().Z);
}
else
{
for (auto& Actor: ActorsToCleanUp)
{
Position += Actor->GetActorLocation();
}
Position = Position / ActorsToCleanUp.Num();
Position = FVector(static_cast<int>(Position.X), static_cast<int>(Position.Y), static_cast<int>(Position.Z));
Position.Z = static_cast<int>(ActorsToCleanUp[0]->GetActorLocation().Z);
}

之后在MergedActor确认存在RootComponent后就可以设置位置了

1
ActorEntry.MergedActor->GetRootComponent()->SetWorldLocation(Position);

设置Actor位置后,原本按照KeepRelativeTransform方式添加的其他Component就会偏离原本在世界中的位置,需要处理一下

1
2
3
4
5
6
7
for(UStaticMeshComponent* OriginalComponent : ComponentEntry.OriginalComponents)
{
FTransform OriginalComponentTransform = OriginalComponent->GetComponentTransform();
FVector OriginalComponentLocation = OriginalComponentTransform.GetLocation() - Position;
OriginalComponentTransform.SetLocation(OriginalComponentLocation);
NewComponent->AddInstance(OriginalComponentTransform);
}

这样就可以了

返回值问题

这个不用多说,简单

总结

原本引擎提供的MergeActor并没有办法通过蓝图调用到函数,也没有返回值,在需要批量化处理场景的时候会不太方便,但是其中具体的实现相对简单,稍微修改就可以满足需求

源码位置:MeshMergeUtilities.cpp


UE4:MergeActors修改
http://muchenhen.com/posts/7750/
作者
木尘痕
发布于
2022年7月5日
许可协议