原创文章,转载请注明出处。
前言
引擎版本:4.27.2 合并的前提:在UE4.26.2之后,允许了在运行时构件UStaticmesh。之前的方式只支持编辑器下导入, 导入后转成UStaticMesh的RenderData进行渲染;
为什么要做合并:
需求的来源是软件我们想利用UE4.27.2的runtime下的udatasmith导入功能, 但是因为datasmith的设计初衷呢就是尽可能小的拆分模型,粒度很小。 所以有的udatasmith导入后会在关卡中有上万个,帧率极低。 所以才会有了该篇文章 在运行时来合并StaticMesh。
合批的优化方案如下
其实这里面是有很多可以做合批的。 所以,这里我就针对udatasmith导入这个功能,研究了一下合批的方案;
方案 | 优点 | 缺点 |
---|
1>修改datasmith导入部分插件的代码 | 效率最高 | 不易维护 | 2>自己做一套 | 效率低于方案1,易维护 | 易维护 | 3>修改datasmith的导出插件 | 不确定 | 不易维护 |
使用哪一种方案?
方案1来讲的话,改DataSmith的源码,效率我认为是最好的。 为什么: 首先说方案1的做法:在一个个的actor还没有spawn,mesh还没有build,collsion,材质等这些信息还没有build之前, 我们提前过滤好哪些Mesh可以被合并,之后再spawn MeshActor,build StaticMesh的vertex,collision,material。 再说方案2的做法:所有的actor已经在世界中spawn出来了,StaticMesh的vertex,collision,material这些信息都已经build好了。再去过滤好哪些Mesh可以被合并,之后再spawn MeshActor,build StaticMesh的vertex,collision,material。
暂时实现了方案2
对比看的话,方案1是要比方案2效率高的。但方案1改起来比较麻烦,并且我认为不容易维护。看了一天之后,我先实现了方案2。
视频效果:Merge之后的帧率和DC明显提高跳转观看
类图
Editor下的实现
参考MergeActor Tool
利用编辑器下的MergeActorTool工具的功能,很快就能在编辑器下实现合并的逻辑。Standlone下也可以合并。 但是需要注意的是这个只能在编辑器下用,打包就歇菜。 编辑器下合并具体的代码如下,作为参考:
void UMyBlueprintFunctionLibrary::MergeMy(const TArray<UPrimitiveComponent*>& ComponentsToMerge, UWorld* World,
const FMeshMergingSettings& InSettings, UMaterialInterface* InBaseMaterial,
UPackage* InOuter, const FString& InBasePackageName,
TArray<UObject*>& OutAssetsToSync, FVector& OutMergedActorLocation,
const float ScreenSize, bool bSilent , FString AppendName)
{
const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().
LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities").GetUtilities();
MeshUtilities.MergeComponentsToStaticMeshWithName(ComponentsToMerge, GWorld, InSettings, InBaseMaterial, InOuter, InBasePackageName,
OutAssetsToSync, OutMergedActorLocation, ScreenSize, bSilent, AppendName);
}
//合并具体逻辑,将相同材质的Mesh传进去即可完成合并。
TArray<UObject*> OutAssetsToSync;
FVector OutMergedActorLocation;
const float ScreenAreaSize = TNumericLimits<float>::Max();
FMeshMergingSettings setting;
setting.bMergePhysicsData = 1;
MergeMy(mergedata.Value, GWorld,
setting, nullptr, GetTransientPackage(), FString(),
OutAssetsToSync, OutMergedActorLocation,
ScreenAreaSize, true, mergedata.Key);
UStaticMesh* UtilitiesMergedMesh = nullptr;
if (!OutAssetsToSync.FindItemByClass(&UtilitiesMergedMesh))
{
continue;
}
for (auto obj : OutAssetsToSync)
{
auto umesh = Cast<UStaticMesh>(obj);
if (!umesh)
continue;
OutMergedActorLocation+=FVector(0,0,500);
auto MergedActor = GWorld->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), OutMergedActorLocation, FRotator(0, 0, 0));
if (MergedActor)
{
MergedActor->SetMobility(EComponentMobility::Movable);
if (!MergedActor->GetStaticMeshComponent())
continue;
MergedActor->GetStaticMeshComponent()->SetStaticMesh(umesh);
if (mergedata.Value.Num() > 0)
{
UStaticMeshComponent* pSTM = Cast<UStaticMeshComponent>(mergedata.Value[0]);
if (pSTM)
{
}
}
GWorld->UpdateCullDistanceVolumes(MergedActor, MergedActor->GetStaticMeshComponent());
MergedActor->AttachToActor(RootActor, FAttachmentTransformRules::KeepWorldTransform);
#if WITH_EDITOR
MergedActor->SetActorLabel(UKismetSystemLibrary::GetDisplayName(umesh));
#endif
}
for (auto willremovecomp : mergedata.Value)
{
if(!IsValid(DeleteActorArray[willremovecomp]))
continue;
if(!DeleteActorArray[willremovecomp]->IsValidLowLevel())
continue;
TArray<UActorComponent*> OutComponent;
OutComponent = DeleteActorArray[willremovecomp]->K2_GetComponentsByClass(UStaticMeshComponent::StaticClass());
if (OutComponent.Num() < 2)
{
GWorld->DestroyActor(DeleteActorArray[willremovecomp]);
}
else
{
willremovecomp->DestroyComponent();
}
}
}
Runtime下的实现
难点1,StaticMesh的RenderData转FMeshDescription
其实这个如果看过StaticMesh的人应该了解,在编辑器下合并的代码都是用的编辑器下StaticMesh独有的数据来合并的,就是下面的图。用到的变量为 SourceModels 并且,编辑器下对StaticMesh的构建是最终会调用Build方法,但这些都在运行时无法使用。 我们需要使用引擎中新版本中的 BuildFromStaticMeshDescriptions来生成UStaticMesh。 BuildFromStaticMeshDescriptions该方法需要的是FMeshDescription,FMeshDescription在编辑器下导入之后就有了,但是运行时UStaticMesh的SourceModels不存在了,怎么办? 我们需要反推,最终渲染的数据都存在UStaticMesh的RenderData中,所以我们就从RenderData里面把数据转成FMeshDescription数组就好了。
依次将每一个可以合并的Mesh的数据从RenderData转换成FMeshDescription,接着再将这些 FMeshDescription加到一次,再给到UStaticMesh的BuildFromStaticMeshDescriptions传进去就搞定了(此处需要注意数据的大小,UE的序列化不能超2G,但是好在这块都是我们自己写,再拼接FMeshDescription的时候我们把内存控制好就行了,这块也关系到合并的速度)
具体步骤概括一下其实就是: 1>RenderData转FMeshDescription 2>拼接所有的FMeshDescription 3>调用BuildFromStaticMeshDescriptions
难点2,StaticMesh构建复杂碰撞
要构建复杂碰撞,那么就要调用 UBodySetup->CreatePhysicsMeshes(),如果仔细跟过的话,进去后会发现,在Runtime下,build碰撞会调用ProcessFormatData_PhysX或者ProcessFormatData_Chaos,但是前提条件必须满足IsRuntime的判断。 我发现这块的原因就是,合并好之后,我在创建UStaticMesh对象的时候写法就是普通的,NewObect(xxxxxxx),结果在IsRuntime的判断那里一直为false。
if (IsRuntime(this))
{
#if WITH_PHYSX && PHYSICS_INTERFACE_PHYSX
bClearMeshes = !RuntimeCookPhysics_PhysX();
#elif WITH_CHAOS
bClearMeshes = !RuntimeCookPhysics_Chaos();
#endif
}
void UBodySetup::CreatePhysicsMeshes()
{
TRACE_CPUPROFILER_EVENT_SCOPE(UBodySetup::CreatePhysicsMeshes);
SCOPE_CYCLE_COUNTER(STAT_CreatePhysicsMeshes);
if(bCreatedPhysicsMeshes)
{
return;
}
if (bNeverNeedsCookedCollisionData)
{
return;
}
bool bClearMeshes = true;
static FName PhysicsFormatName(FPlatformProperties::GetPhysicsFormat());
FByteBulkData* FormatData = GetCookedData(PhysicsFormatName);
if (FormatData == nullptr && IsRunningDedicatedServer())
{
FormatData = GetCookedData(FGenericPlatformProperties::GetPhysicsFormat());
}
if (FormatData)
{
#if WITH_PHYSX && PHYSICS_INTERFACE_PHYSX
bClearMeshes = !ProcessFormatData_PhysX(FormatData);
#elif WITH_CHAOS
bClearMeshes = !ProcessFormatData_Chaos(FormatData);
#endif
}
else
{
if (IsRuntime(this))
{
#if WITH_PHYSX && PHYSICS_INTERFACE_PHYSX
bClearMeshes = !RuntimeCookPhysics_PhysX();
#elif WITH_CHAOS
bClearMeshes = !RuntimeCookPhysics_Chaos();
#endif
}
}
if ( GetLinkerUE4Version() < VER_UE4_FIXUP_BODYSETUP_INVALID_CONVEX_TRANSFORM )
{
for (int32 i=0; i<AggGeom.ConvexElems.Num(); ++i)
{
if ( AggGeom.ConvexElems[i].GetTransform().IsValid() == false )
{
AggGeom.ConvexElems[i].SetTransform(FTransform::Identity);
}
}
}
#if WITH_CHAOS
for(FKConvexElem& Convex : AggGeom.ConvexElems)
{
Convex.ComputeChaosConvexIndices();
}
#endif
if(bClearMeshes)
{
ClearPhysicsMeshes();
}
bCreatedPhysicsMeshes = true;
}
经过看代码,跟代码,我找到了解决方案。 1>首先需要从UStaticMesh派生一个类出来; 2>并且这个类的bAllowCPUAccess必须为true; 3>并且要重载一下GetWorld(); 然后自己在加一个SetWorld方法; 这个类的具体代码如下 :
UCLASS()
class EASYKITRUNTIMEMERGEMESH_API UEKRMM_RuntimeMesh : public UStaticMesh
{
GENERATED_BODY()
public:
UEKRMM_RuntimeMesh()
: World(nullptr)
{
bAllowCPUAccess = true;
}
virtual UWorld* GetWorld() const override { return World ? World : UStaticMesh::GetWorld(); }
void SetWorld(UWorld* InWorld) { World = InWorld; }
private:
UWorld* World;
};
用法就比较简单了,如下,之后再去调用UBodySetup->CreatePhysicsMeshes()就OK了:
UEKRMM_RuntimeMesh* StaticMesh = NewObject< UEKRMM_RuntimeMesh >(GetTransientPackage(), MeshName, RF_Public | RF_Standalone);
if(!StaticMesh)
continue;
StaticMesh->InitResources();
StaticMesh->SetWorld(RootActor->GetWorld());
难点3,StaticMesh材质
插件封装,现有功能介绍以及后续计划
目前支持的功能: 1>所有相同材质的mesh合并到一起:传入一个AActor对象作为RootActor,能够将RootActor下的所有材质相同的UStaticmeshComponent合并成单个UStaticMesh; 2>材质正确 3>保证有复杂碰撞 4>坐标正确 5>待添加:合并之前的大小计算,分块:主要目的是满足序列化以及兼顾合并效率 6>待添加:减面插件,合并时候可以动态减面,我准备同样弄一个插件出来,运行时的减面算法 7>待添加:USkeletalMesh的Merge 8>待添加:序列化
参考文章
Datasmith Runtime 官方的Blog Unreal Engine 4.27 Datasmith Runtime Import UE – StaticMesh 分析
<( ̄︶ ̄)>谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走吧!
|