目录
一、功能需求
二、绘制结果
三、转化算法
3.1 灵感来源
3.2 算法难点
3.3 算法源码
四、绘制步骤
4.1 制作点面数据表格
4.2 处理存储表格数据
4.3 绘制并转换模型
4.4 重置化本地化ID
五、批量添加凸包碰撞
Github源工程:https://github.com/ColorGalaxy/UE4-Batch-Draw-Mesh-And-OpenGL-Get-Model-Data
觉得赞,记得点Star⭐
本人做毕设时,想使用UE4进行练习,导师给的题目是矿山爆破模拟(后来觉得用Houdini做比较好,但当时执着于UE4的技能提升)。
他会提供给我9300块矿山爆破后的碎石模型数据,是使用C++(导师擅长opengl进行可视化)输出的13万条顶点坐标与三角面索引。
我需要在UE4中重建该矿山模型,才能使用后续导师给的爆破速度数据。不使用可破坏网格体插件模拟也是出于各种原因。
因此研究了在UE4中批量绘制模型该问题。
后续:之后学习了Opengl,研究了如何输出模型的顶点与面索引,可参考下文。
【OpenGL C++ UE4】获取模型顶点及面索引数据,并优化存储结构供UE4绘制
虽说看着都很像,但是他们的轴心都在(0,0,0),放置在场景中就能拼凑成一个完整的矿山模型。
引擎是带有该功能的,但是仅支持在编辑器运行状态下,通过按钮点击才可以。
详见【UE4】ProceduralMeshComponent绘制自定义模型并转为StaticMesh该文章,有简单的教程。
因此,对引擎的源码进行了研究,找到了该功能的源码。
当时由于UE4 C++的开发经验不多,也没想到在引用头文件RawMesh.h与ProceduralMeshComponent.h时会报错,就连绝对路径都不行。一番折腾与苦找后,玄学解决了,但现在重试想弄个Demo的时候那个法子不行了....活见鬼系列。国外大佬还是多,如今找到了正确的解决办法,并做了总结,大家可以看一下。
【UE4 C++】RawMesh.h引用报错
【UE4 C++】无法打开源文件"ProceduralMeshComponent.h"的解决办法
新建继承自Blueprint Function Library类的C++文件,用于编写将程序化模型转化为静态网格体的函数,暴露成节点给蓝图调用。鉴于担心刚入坑的萌新不知道复制在哪,我贴出了全部代码,剩余的都在蓝图中处理。
算法的核心就是提取Procedural Mesh中的信息,经由Raw Mesh将模型信息转入Static Mesh对象,最后AssetRegisterModule创建本地资源(目前还是临时资源,只存在于缓存中而不在本地磁盘中,关闭项目再次打开模型就不见了)。
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "PMConvertSM.generated.h"
class UStaticMesh;
class UProceduralMeshComponent;
UCLASS()
class MINEBLAST_API UPMConvertSM : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Procedural Mesh Component")
static UStaticMesh* ProceduralMeshConvertToStaticMesh(UProceduralMeshComponent* proMeshComp,FString outMeshName);//自定义绘制程序网格体转化为静态网格体
};
#include "PMConvertSM.h"
#include "ProceduralMeshComponent.h"
/*在项目.build.cs和.uproject的plugin下添加"ProceduralMeshComponent"解决头文件找不到路径*/
#include "Engine/StaticMesh.h"
#include "RawMesh/Public/RawMesh.h"
/*在项目.build.cs下添加"RawMesh"解决头文件找不到路径和编译错误问题*/
#include "AssetRegistryModule.h"
//改写自引擎源代码中的转化静态网格体按钮OnClickConvertToStaticMesh
UStaticMesh* UPMConvertSM::ProceduralMeshConvertToStaticMesh(UProceduralMeshComponent* proMeshComp,FString outMeshName)
{
UProceduralMeshComponent * ProMesh = proMeshComp;
if (ProMesh != nullptr)
{
FString PathName = FString(TEXT("/Game/Mesh/"));
FString PackageName = PathName + outMeshName;
//Raw mesh data we are filling in
FRawMesh RawMesh;
// Materials to apply to new mesh
TArray MeshMaterials;
const int32 NumSections = ProMesh->GetNumSections();
int32 VertexBase = 0;
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
{
FProcMeshSection* ProSection = ProMesh->GetProcMeshSection(SectionIdx);
// Copy verts
for (FProcMeshVertex& Vert : ProSection->ProcVertexBuffer)
{
RawMesh.VertexPositions.Add(Vert.Position);
}
// Copy 'wedge' info
int32 NumIndices = ProSection->ProcIndexBuffer.Num();
for (int32 IndexIdx = 0; IndexIdx < NumIndices; IndexIdx++)
{
int32 Index = ProSection->ProcIndexBuffer[IndexIdx];
RawMesh.WedgeIndices.Add(Index + VertexBase);
FProcMeshVertex& ProcVertex = ProSection->ProcVertexBuffer[Index];
FVector TangentX = ProcVertex.Tangent.TangentX;
FVector TangentZ = ProcVertex.Normal;
FVector TangentY = (TangentX ^ TangentZ).GetSafeNormal() * (ProcVertex.Tangent.bFlipTangentY ? -1.f : 1.f);
RawMesh.WedgeTangentX.Add(TangentX);
RawMesh.WedgeTangentY.Add(TangentY);
RawMesh.WedgeTangentZ.Add(TangentZ);
RawMesh.WedgeTexCoords[0].Add(ProcVertex.UV0);
RawMesh.WedgeColors.Add(ProcVertex.Color);
}
// copy face info
int32 NumTris = NumIndices / 3;
for (int32 TriIdx = 0; TriIdx < NumTris; TriIdx++)
{
RawMesh.FaceMaterialIndices.Add(SectionIdx);
RawMesh.FaceSmoothingMasks.Add(0); // Assume this is ignored as bRecomputeNormals is false
}
// Remember material
MeshMaterials.Add(ProMesh->GetMaterial(SectionIdx));
// Update offset for creating one big index/vertex buffer
VertexBase += ProSection->ProcVertexBuffer.Num();
// If we got some valid data.
if (RawMesh.VertexPositions.Num() > 3 && RawMesh.WedgeIndices.Num() > 3)
{
// Then find/create it.
UPackage* Package = CreatePackage(NULL, *PackageName);
check(Package);
// Create StaticMesh object
UStaticMesh* StaticMesh = NewObject(Package, FName(*outMeshName), RF_Public | RF_Standalone);
StaticMesh->InitResources();
StaticMesh->LightingGuid = FGuid::NewGuid();
// Add source to new StaticMesh
FStaticMeshSourceModel* SrcModel = new (StaticMesh->SourceModels) FStaticMeshSourceModel();
SrcModel->BuildSettings.bRecomputeNormals = false;
SrcModel->BuildSettings.bRecomputeTangents = false;
SrcModel->BuildSettings.bRemoveDegenerates = false;
SrcModel->BuildSettings.bUseHighPrecisionTangentBasis = false;
SrcModel->BuildSettings.bUseFullPrecisionUVs = false;
SrcModel->BuildSettings.bGenerateLightmapUVs = true;
SrcModel->BuildSettings.SrcLightmapIndex = 0;
SrcModel->BuildSettings.DstLightmapIndex = 1;
SrcModel->SaveRawMesh(RawMesh);
// Copy materials to new mesh
for (UMaterialInterface* Material : MeshMaterials)
{
StaticMesh->StaticMaterials.Add(FStaticMaterial(Material));
}
//Set the Imported version before calling the build
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
// Build mesh from source
StaticMesh->Build(false);
StaticMesh->PostEditChange();
// Notify asset registry of new asset
FAssetRegistryModule::AssetCreated(StaticMesh);
return StaticMesh;
}
}
}
return nullptr;
}
将顶点和三角面索引数据做成CSV格式的Excel表格,在UE中创建相同的表头结构体后,导入Excel做成数据表格。
通过表格中的0行分割不同的模型,将表格中所有模型的顶点和面片信息存入两个二维的数组中,蓝图详见Github Demo工程。
调用Procedural Mesh Component的Create Mesh Section函数,输入顶点数组三角面数组创建模型。调用之前C++编写的模型类型转换节点,传入模型名称与Procedural Mesh组件,就会创建本地临时模型资源(能够在内容浏览器中看到,但是在系统文件夹中看不到)。
注:如果数据量过大,可能开始运行后,会卡住一段时间,但其实是在绘制的,比如我的毕设就将9300个模型分成了6组,每组1500个左右,绘制一组半个小时吧...(可能我电脑比较LJ)
通过尝试,只有将模型的本地化ID重置,才能够重新保存,使模型保存在硬盘上。
这时保存所有,就能够在文件夹中看到这些资源了,下次打开项目依旧存在,它属于你啦。
由于绘制的模型包围碰撞盒不够精确,而我的毕设需要碎石们模拟真实的碰撞滚动,物理这块用的是引擎自带的开启物理模拟。使用复杂碰撞无法开启物理模拟,因而逐一为9300个模型手动添加简单碰撞盒-凸包碰撞是不可能的,因而我们开启插件Editor Scripting Utilities,使用特殊的Actor在构造器中完成这一操作。
可以参考【UE4】如何使用Edit Scripting Utilities在蓝图中为大量静态网格体设置自动凸包碰撞