UE4/UE5 Runtime下的Mesh合并

2024-06-16 02:38
文章标签 ue4 合并 runtime ue5 mesh

本文主要是介绍UE4/UE5 Runtime下的Mesh合并,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原创文章,转载请注明出处。

StaticMesh的合并

  • 前言
  • 类图
  • Editor下的实现
  • Runtime下的实现
    • 难点1,StaticMesh的RenderData转FMeshDescription
    • 难点2,StaticMesh构建复杂碰撞
    • 难点3,多Section的StaticMesh合并之后材质出错
  • 插件封装,现有功能介绍以及后续计划
  • 参考文章

前言

引擎版本: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明显提高跳转观看

UE4/UE5 Runtime下合并Mesh

类图

在这里插入图片描述

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 /*= false*/, 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))
{// Error, TEXT("MergeStaticMeshActors failed. No mesh was created.continue;
}for (auto obj : OutAssetsToSync)
{auto umesh = Cast<UStaticMesh>(obj);if (!umesh)continue;/*auto mat0 = umesh->GetMaterial(0);if (!UKismetSystemLibrary::IsValid(mat0))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){//umesh->SetStaticMaterials(pSTM->GetStaticMesh()->GetStaticMaterials());}}GWorld->UpdateCullDistanceVolumes(MergedActor, MergedActor->GetStaticMeshComponent());MergedActor->AttachToActor(RootActor, FAttachmentTransformRules::KeepWorldTransform);
#if WITH_EDITORMergedActor->SetActorLabel(UKismetSystemLibrary::GetDisplayName(umesh));
#endif // endif}//删除被替代的RootActor
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:方法
static void AppendMeshDescription(const FMeshDescription& SourceMesh, FMeshDescription& TargetMesh, FAppendSettings& AppendSettings);
3>调用BuildFromStaticMeshDescriptions

难点2,StaticMesh构建复杂碰撞

要构建复杂碰撞,那么就要调用
UBodySetup->CreatePhysicsMeshes(),如果仔细跟过的话,进去后会发现,在Runtime下,build碰撞会调用ProcessFormatData_PhysX或者ProcessFormatData_Chaos,但是前提条件必须满足IsRuntime的判断。
我发现这块的原因就是,合并好之后,我在创建UStaticMesh对象的时候写法就是普通的,NewObect<UStaticMesh>(xxxxxxx),结果在IsRuntime的判断那里一直为false。

		if (IsRuntime(this)){
#if WITH_PHYSX  && PHYSICS_INTERFACE_PHYSXbClearMeshes = !RuntimeCookPhysics_PhysX();
#elif WITH_CHAOSbClearMeshes = !RuntimeCookPhysics_Chaos();
#endif}
void UBodySetup::CreatePhysicsMeshes()
{TRACE_CPUPROFILER_EVENT_SCOPE(UBodySetup::CreatePhysicsMeshes);SCOPE_CYCLE_COUNTER(STAT_CreatePhysicsMeshes);// Create meshes from cooked data if not already doneif(bCreatedPhysicsMeshes){return;}// If we don't have any convex/trimesh data we can skip this whole functionif (bNeverNeedsCookedCollisionData){return;}bool bClearMeshes = true;// Find or create cooked physics datastatic FName PhysicsFormatName(FPlatformProperties::GetPhysicsFormat());FByteBulkData* FormatData = GetCookedData(PhysicsFormatName);// On dedicated servers we may be cooking generic data and sharing itif (FormatData == nullptr && IsRunningDedicatedServer()){FormatData = GetCookedData(FGenericPlatformProperties::GetPhysicsFormat());}if (FormatData){
#if WITH_PHYSX  && PHYSICS_INTERFACE_PHYSXbClearMeshes = !ProcessFormatData_PhysX(FormatData);
#elif WITH_CHAOSbClearMeshes = !ProcessFormatData_Chaos(FormatData);
#endif}else{if (IsRuntime(this))//这个地方在Runtime下如果你用的是UStaticMesh的话,是无法通过的。{
#if WITH_PHYSX  && PHYSICS_INTERFACE_PHYSXbClearMeshes = !RuntimeCookPhysics_PhysX();
#elif WITH_CHAOSbClearMeshes = !RuntimeCookPhysics_Chaos();
#endif}}// fix up invalid transform to use identity// this can be here because BodySetup isn't blueprintableif ( 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 drawing of convex elements we require an index buffer, previously we could// get this from a PxConvexMesh but Chaos doesn't maintain that data. Instead now// it is a part of the element rather than the physics geometry, if we load in an// element without that data present, generate a convex hull from the convex vert// data and extract the index data from there.for(FKConvexElem& Convex : AggGeom.ConvexElems){Convex.ComputeChaosConvexIndices();}
#endifif(bClearMeshes){ClearPhysicsMeshes();}bCreatedPhysicsMeshes = true;}

费了半天劲,才找导解决方案。
1>首先需要从UStaticMesh派生一个类出来;
2>并且这个类的bAllowCPUAccess必须为true;
3>并且要重载一下GetWorld();
然后自己在加一个SetWorld方法;
这个类的具体代码如下 :

/*
* 从UStaticMesh派生的类,允许在运行时烹饪碰撞网格
* 要做到这一点,bAllowCPUAccess必须为true,并且方法GetWorld()必须返回一个有效的world
* 否则在Cook的时候有个IsRuntime()的判断是一直为假
*/
UCLASS()
class EASYKITRUNTIMEMERGEMESH_API UEKRMM_RuntimeMesh : public UStaticMesh
{GENERATED_BODY()public:UEKRMM_RuntimeMesh(): World(nullptr){// 设置bAllowCPUAccess为true,允许将渲染数据三角形复制到碰撞网格中  bAllowCPUAccess = true;}// UObject覆盖//覆盖允许烹饪碰撞网格,简单和复杂,从静态网格在运行时  virtual UWorld* GetWorld() const override { return World ? World : UStaticMesh::GetWorld(); }// 结束UObject覆盖//使用有效的世界,允许碰撞网格烹饪,简单和复杂,从静态网格在运行时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,多Section的StaticMesh合并之后材质出错

当一个UStaticMesh具有多材质的这种,其实RenderData里面是分了多个Section的。
每个Section的结构体为FSectionInfo

下面的代码是从StaticMesh的RenderData里面提取Section,OutSections数组是多个的,那么就证明你的StaticMesh是多材质的,依次对应的关系。为什么说 这个是个难点,请继续往下看。

void UEKRMM_BlueprintFunctionLibrary::EKRMM_ExtractSections(const UStaticMeshComponent* Component, int32 LODIndex, TArray<FSectionInfo>& OutSections)
{static UMaterialInterface* DefaultMaterial = UMaterial::GetDefaultMaterial(MD_Surface);const UStaticMesh* StaticMesh = Component->GetStaticMesh();TArray<FName> MaterialSlotNames;for (const FStaticMaterial& StaticMaterial : StaticMesh->GetStaticMaterials()){
#if WITH_EDITORMaterialSlotNames.Add(StaticMaterial.ImportedMaterialSlotName);
#elseMaterialSlotNames.Add(StaticMaterial.MaterialSlotName);
#endif}for (const FStaticMeshSection& MeshSection : StaticMesh->GetRenderData()->LODResources[LODIndex].Sections){// Skip empty sectionsif (MeshSection.NumTriangles == 0){continue;}// Retrieve material for this sectionUMaterialInterface* StoredMaterial = Component->GetMaterial(MeshSection.MaterialIndex);// Make sure the resource actual exists, otherwise use default materialStoredMaterial = (StoredMaterial != nullptr) && StoredMaterial->GetMaterialResource(GMaxRHIFeatureLevel) ? StoredMaterial : DefaultMaterial;// Populate section dataFSectionInfo SectionInfo;SectionInfo.Material = StoredMaterial;SectionInfo.MaterialIndex = MeshSection.MaterialIndex;SectionInfo.MaterialSlotName = MaterialSlotNames.IsValidIndex(MeshSection.MaterialIndex) ? MaterialSlotNames[MeshSection.MaterialIndex] : NAME_None;SectionInfo.StartIndex = MeshSection.FirstIndex / 3;SectionInfo.EndIndex = SectionInfo.StartIndex + MeshSection.NumTriangles;if (MeshSection.bEnableCollision){SectionInfo.EnabledProperties.Add(GET_MEMBER_NAME_CHECKED(FStaticMeshSection, bEnableCollision));}if (MeshSection.bCastShadow && Component->CastShadow){SectionInfo.EnabledProperties.Add(GET_MEMBER_NAME_CHECKED(FStaticMeshSection, bCastShadow));}OutSections.Add(SectionInfo);}
}

上面难点一的提取数据中说道,从RenderData里面提取完每个Mesh的MeshDescription之后,调用static void AppendMeshDescription(const FMeshDescription& SourceMesh, FMeshDescription& TargetMesh, FAppendSettings& AppendSettings);这个静态方法才可以将数据拼接到一起。这里注意第三个参数,FAppendSettings这个结构体参数传入的时候如果传入不慎,会导致Section就有一个,原本多个section,合并完了变成1个了,那不就出错了。主要是AppendSettings里面的Lambda需要指定,这个藏的比较深。并且逻辑要正确,因为AppendMeshDescription里面需要用到那个lambda。这是第一个会导致材质section出错的问题。
如何验证是否正确呢? 合并完之后可以看 输出参数FMeshDescription& TargetMesh的PolygonGroups是几个,如果原先数组内有多个索引比如0和1,合并完了数组内的索引都变成0了,那就是错了,正确的应该保持不变。

如果发现最终效果你发现还不对
但是PolygonGroups通过AppendMeshDescription发现数据都是正确的。
我们断点看一下UStaticMesh最终渲染数据的Section的索引是否是正确的,

StaticMesh->GetRenderData()->LODResources[LODIndex].Sections)

我遇到的情况是Sections虽然是数组没错,但是索引都是0,那么效果肯定就不对了
原因是因为BuildFromMeshDescription下面代码里面的MaterialIndex 指定的位置都赋值了0,那么为什么是0呢,因为MaterialIndex的赋值是通过GetStaticMaterials().IndexOfByPredicate,里面看函数体是用的材质槽的名称,由于我的材质槽名称都是一样的,所以找到的下标都是0。
到这就知道怎么做了:在build之前,将staticmesh的材质槽首先要分配好,并且名称要具有唯一性,不能重复。和NewObject<UEKRMM_RuntimeMesh>传入的名称一样,必须具有唯一性。

源码下面这个方法里面
void UStaticMesh::BuildFromMeshDescription(const FMeshDescription& MeshDescription, FStaticMeshLODResources& LODResources)
{//省略好多代码Section.NumTriangles = TriangleCount;Section.MinVertexIndex = MinVertexIndex;Section.MaxVertexIndex = MaxVertexIndex;const int32 MaterialIndex = GetStaticMaterials().IndexOfByPredicate([&MaterialSlotName = MaterialSlotNames[PolygonGroupID]](const FStaticMaterial& StaticMaterial) { return StaticMaterial.MaterialSlotName == MaterialSlotName; });Section.MaterialIndex = MaterialIndex;  注意这里的MaterialIndex,是通过IndexOfByPredicate这个方法来算出来的,这个是啥意思?就是在我这个TArray里面,遍历它每一个元素,找到条件第一个符合函数体的内容,返回出去。Section.bEnableCollision = true;Section.bCastShadow = true;if (MaxVertexIndex > TNumericLimits<uint16>::Max()){IndexBufferStride = EIndexBufferStride::Force32Bit;}SectionIndex++;//省略好多代码
}

插件封装,现有功能介绍以及后续计划

目前支持的功能:
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 分析

<( ̄︶ ̄)>谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走吧!

这篇关于UE4/UE5 Runtime下的Mesh合并的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1065268

相关文章

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

day-51 合并零之间的节点

思路 直接遍历链表即可,遇到val=0跳过,val非零则加在一起,最后返回即可 解题过程 返回链表可以有头结点,方便插入,返回head.next Code /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}*

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟)

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟) 题目描述 给定一个链表,链表中的每个节点代表一个整数。链表中的整数由 0 分隔开,表示不同的区间。链表的开始和结束节点的值都为 0。任务是将每两个相邻的 0 之间的所有节点合并成一个节点,新节点的值为原区间内所有节点值的和。合并后,需要移除所有的 0,并返回修改后的链表头节点。 思路分析 初始化:创建一个虚拟头节点

UE5 半透明阴影 快速解决方案

Step 1: 打开该选项 Step 2: 将半透明材质给到模型后,设置光照的Shadow Resolution Scale,越大,阴影的效果越好

Golang进程权限调度包runtime

关于 runtime 包几个方法: Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行GOMAXPROCS:设置最大的可同时使用的 CPU 核数Goexit:退出当前 goroutine(但是defer语句会照常执行)NumGoroutine:返回正在执行和排队的任务总数GOOS:目标操作系统NumCPU:返回当前系统的 CPU 核数量 p

【Python从入门到进阶】64、Pandas如何实现数据的Concat合并

接上篇《63.Pandas如何实现数据的Merge》 上一篇我们学习了Pandas如何实现数据的Merge,本篇我们来继续学习Pandas如何实现数据的Concat合并。 一、引言 在数据处理过程中,经常需要将多个数据集合并为一个统一的数据集,以便进行进一步的分析或建模。这种需求在多种场景下都非常常见,比如合并不同来源的数据集以获取更全面的信息、将时间序列数据按时间顺序拼接起来以观察长期趋势等

数据集 3DPW-开源户外三维人体建模-姿态估计-人体关键点-人体mesh建模 >> DataBall

3DPW 3DPW-开源户外三维人体建模数据集-姿态估计-人体关键点-人体mesh建模 开源户外三维人体数据集 @inproceedings{vonMarcard2018, title = {Recovering Accurate 3D Human Pose in The Wild Using IMUs and a Moving Camera}, author = {von Marc

线性表中顺序表的合并

对两个顺序表进行合并,算法的复杂度为O(La.size+Lb.size)。 已知: 顺序线性表La和Lb的元素按值非递减排列 归并La和Lb得到的顺序线性表Lc,Lc的元素也按值非递减排列。 代码定义: void mergeList(SeqList *La,SeqList *Lb,SeqList *Lc){Lc->capacity = La->size + Lb->size;Lc->b

为libpng不同架构创建构建目录、编译、安装以及合并库文件的所有步骤。

好的。既然你已经有了 libpng 的源代码,并且当前处在它的目录下,我们可以简化脚本,不再需要下载和解压源代码这一步。以下是修改后的脚本:```sh#!/bin/bash# 当前目录即 libpng 源代码目录LIBPNG_SRC_DIR=$(pwd)# 设置工作目录WORK_DIR=$(pwd)/libpng_buildBUILD_DIR_X86_64="$WORK_DIR/build