I wanted to be able to save out my procedural meshes as static ones so I copied the code from ProceduralMeshComponentDetails.cpp
FReply FProceduralMeshComponentDetails::ClickedOnConvertToStaticMesh()
{
// Find first selected ProcMeshComp
UProceduralMeshComponent* ProcMeshComp = GetFirstSelectedProcMeshComp();
if (ProcMeshComp != nullptr)
{
FString NewNameSuggestion = FString(TEXT("ProcMesh"));
FString PackageName = FString(TEXT("/Game/Meshes/")) + NewNameSuggestion;
FString Name;
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
AssetToolsModule.Get().CreateUniqueAssetName(PackageName, TEXT(""), PackageName, Name);
TSharedPtr<SDlgPickAssetPath> PickAssetPathWidget =
SNew(SDlgPickAssetPath)
.Title(LOCTEXT("ConvertToStaticMeshPickName", "Choose New StaticMesh Location"))
.DefaultAssetPath(FText::FromString(PackageName));
if (PickAssetPathWidget->ShowModal() == EAppReturnType::Ok)
{
// Get the full name of where we want to create the physics asset.
FString UserPackageName = PickAssetPathWidget->GetFullAssetPath().ToString();
FName MeshName(*FPackageName::GetLongPackageAssetName(UserPackageName));
// Check if the user inputed a valid asset name, if they did not, give it the generated default name
if (MeshName == NAME_None)
{
// Use the defaults that were already generated.
UserPackageName = PackageName;
MeshName = *Name;
}
// Raw mesh data we are filling in
FRawMesh RawMesh;
// Materials to apply to new mesh
TArray<UMaterialInterface*> MeshMaterials;
const int32 NumSections = ProcMeshComp->GetNumSections();
int32 VertexBase = 0;
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
{
FProcMeshSection* ProcSection = ProcMeshComp->GetProcMeshSection(SectionIdx);
// Copy verts
for (FProcMeshVertex& Vert : ProcSection->ProcVertexBuffer)
{
RawMesh.VertexPositions.Add(Vert.Position);
}
// Copy 'wedge' info
int32 NumIndices = ProcSection->ProcIndexBuffer.Num();
for (int32 IndexIdx=0; IndexIdx < NumIndices; IndexIdx++)
{
int32 Index = ProcSection->ProcIndexBuffer[IndexIdx];
RawMesh.WedgeIndices.Add(Index + VertexBase);
FProcMeshVertex& ProcVertex = ProcSection->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(ProcMeshComp->GetMaterial(SectionIdx));
// Update offset for creating one big index/vertex buffer
VertexBase += ProcSection->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, *UserPackageName);
check(Package);
// Create StaticMesh object
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, MeshName, 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->RawMeshBulkData->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 FReply::Handled();
}
Then I rewrote it into a blueprint library function
UStaticMesh* UWebEZBPFunctionLibrary::SaveProceduralWebMesh(UProceduralMeshComponent* ProcMesh)
{
//UStaticMesh = UStaticMesh::create
// Find first selected ProcMeshComp
UProceduralMeshComponent* ProcMeshComp = ProcMesh;
if (ProcMeshComp != nullptr)
{
FString ActorName = ProcMesh->GetOwner()->GetName();
FString LevelName = ProcMesh->GetWorld()->GetMapName();
FString AssetName = FString(TEXT("SM_")) + LevelName + FString(TEXT("_") + ActorName);
FString PathName = FString(TEXT("/Game/WebEZMeshes/"));
FString PackageName = PathName + AssetName;
// Raw mesh data we are filling in
FRawMesh RawMesh;
// Materials to apply to new mesh
TArray<UMaterialInterface*> MeshMaterials;
const int32 NumSections = ProcMeshComp->GetNumSections();
int32 VertexBase = 0;
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
{
FProcMeshSection* ProcSection = ProcMeshComp->GetProcMeshSection(SectionIdx);
// Copy verts
for (FProcMeshVertex& Vert : ProcSection->ProcVertexBuffer)
{
RawMesh.VertexPositions.Add(Vert.Position);
}
// Copy 'wedge' info
int32 NumIndices = ProcSection->ProcIndexBuffer.Num();
for (int32 IndexIdx = 0; IndexIdx < NumIndices; IndexIdx++)
{
int32 Index = ProcSection->ProcIndexBuffer[IndexIdx];
RawMesh.WedgeIndices.Add(Index + VertexBase);
FProcMeshVertex& ProcVertex = ProcSection->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(ProcMeshComp->GetMaterial(SectionIdx));
// Update offset for creating one big index/vertex buffer
VertexBase += ProcSection->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<UStaticMesh>(Package, FName(*AssetName), 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->RawMeshBulkData->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;
}
}
}
else
{
return nullptr;
}
}
However, I get a different result. When I compare directly from the same procedural mesh, it looks like my version is only saving two of the mesh’s sections. Any ideas why?
I guess you also had to use RawMesh.h to implement this function. Have you ever tried to package your game using this functionalities for the creation of static meshes?
nope ! it cant solve the problem.while the rawmesh which is a developer library. so you cant solve this problem unless u transfer the developer source code to the runtime mode.
I think I found a working solution, which works fine in UE_5.0.3 game runtime.
Mostly it was copied from original FProceduralMeshComponentDetails::ClickedOnConvertToStaticMesh() source code. Unfortunatly it failed to package game binaries. So I spent 2 days looking for alternative solution.
Generally I replaced original mesh description commitment [starting from AddSourceModel()], which for whatever reason works only in Editor.
Also make sure you AddCollisionConvexMesh supplied with convex mesh vertices (the same mesh vertices for me) to ProceduralMeshComponent before converting it to StaticMesh if you want your simple collision to work properly. Toggle bUseComplexAsSimpleCollision flag on your ProceduralMeshComponent to false as well.
// PrivateDependencyModuleNames.AddRange(new string[] { "ProceduralMeshComponent", "MeshDescription", });
#include "ProceduralMeshComponent.h"
#include "ProceduralMeshConversion.h"
#include "StaticMeshDescription.h"
UStaticMesh* ULifeUtils::TestFunc(UProceduralMeshComponent* ProcMesh)
{
if (!ProcMesh)
return nullptr;
FMeshDescription MeshDescription = BuildMeshDescription(ProcMesh);
// If we got some valid data.
if (MeshDescription.Polygons().Num() > 0)
{
// Create StaticMesh object
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(ProcMesh/*Package, MeshName, RF_Public | RF_Standalone*/);
StaticMesh->InitResources();
StaticMesh->SetLightingGuid();
// Add source to new StaticMesh
auto Desc = StaticMesh->CreateStaticMeshDescription();
Desc->SetMeshDescription(MeshDescription);
// buildSimpleCol = false, cause it creates box collision based on mesh bounds or whatever :(
StaticMesh->BuildFromStaticMeshDescriptions({ Desc }, false);
//// SIMPLE COLLISION
if (!ProcMesh->bUseComplexAsSimpleCollision )
{
StaticMesh->CreateBodySetup();
UBodySetup* NewBodySetup = StaticMesh->GetBodySetup();
NewBodySetup->BodySetupGuid = FGuid::NewGuid();
NewBodySetup->AggGeom.ConvexElems = ProcMesh->ProcMeshBodySetup->AggGeom.ConvexElems;
NewBodySetup->bGenerateMirroredCollision = false;
NewBodySetup->bDoubleSidedGeometry = true;
// Play around with the flag below if you struggle with collision not working
NewBodySetup->CollisionTraceFlag = CTF_UseDefault;
NewBodySetup->CreatePhysicsMeshes();
}
/* Commented out cause I don't need it
//// MATERIALS
TSet<UMaterialInterface*> UniqueMaterials;
const int32 NumSections = ProcMesh->GetNumSections();
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
{
FProcMeshSection *ProcSection =
ProcMesh->GetProcMeshSection(SectionIdx);
UMaterialInterface *Material = ProcMesh->GetMaterial(SectionIdx);
UniqueMaterials.Add(Material);
}
// Copy materials to new mesh
for (auto* Material : UniqueMaterials)
{
StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material));
}
*/
// Uncallable in game runtime
// StaticMesh->Build(false);
return StaticMesh;
}
return nullptr;
}
Hi everyone.
I just came here to paste the piece of code that worked for me in UE 5.0.3 (Editor only) based on @BlackFangTech and @Anonymous_0d335be758da64420a78fdc616adb403 's code. It’s probably full of errors since I am no C++ programmer, but it can serve as a template with plenty of space to improve. At this moment I am not using this anymore since I need it to work in packaged builds.
Cheers!
UStaticMesh* UBPFL_DatavizUtils::ConvertProceduralMeshToStaticMesh(UProceduralMeshComponent* ProcMesh) {
//UStaticMesh = UStaticMesh::create
// Find first selected ProcMeshComp
UProceduralMeshComponent* ProcMeshComp = ProcMesh;
if (ProcMeshComp != nullptr)
{
FString ActorName = ProcMesh->GetOwner()->GetName();
FString LevelName = ProcMesh->GetWorld()->GetMapName();
FString AssetName = FString(TEXT("SM_")) + LevelName + FString(TEXT("_") + ActorName);
FString PathName = FString(TEXT("/Game/WebEZMeshes/"));
FString PackageName = PathName + AssetName;
// Raw mesh data we are filling in
FRawMesh RawMesh;
// Materials to apply to new mesh
TArray<UMaterialInterface*> MeshMaterials;
const int32 NumSections = ProcMeshComp->GetNumSections();
int32 VertexBase = 0;
for (int32 SectionIdx = 0; SectionIdx < NumSections; SectionIdx++) {
FProcMeshSection* ProcSection = ProcMeshComp->GetProcMeshSection(SectionIdx);
// Copy verts
for (FProcMeshVertex& Vert : ProcSection->ProcVertexBuffer) {
RawMesh.VertexPositions.Add(FVector3f(Vert.Position));
}
// Copy 'wedge' info
int32 NumIndices = ProcSection->ProcIndexBuffer.Num();
for (int32 IndexIdx = 0; IndexIdx < NumIndices; IndexIdx++) {
int32 Index = ProcSection->ProcIndexBuffer[IndexIdx];
RawMesh.WedgeIndices.Add(Index + VertexBase);
FProcMeshVertex& ProcVertex = ProcSection->ProcVertexBuffer[Index];
FVector3f TangentX = FVector3f(ProcVertex.Tangent.TangentX);
FVector3f TangentZ = FVector3f(ProcVertex.Normal);
FVector3f TangentY = FVector3f((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(FVector2f(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(ProcMeshComp->GetMaterial(SectionIdx));
// Update offset for creating one big index/vertex buffer
VertexBase += ProcSection->ProcVertexBuffer.Num();
// If we got some valid data.
if (RawMesh.VertexPositions.Num() > 3 && RawMesh.WedgeIndices.Num() > 3) {
// Then find/create it.
UPackage* Package = CreatePackage(*PackageName);
check(Package);
// Create StaticMesh object
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, FName(*AssetName), RF_Public | RF_Standalone);
StaticMesh->InitResources();
FGuid::NewGuid() = StaticMesh->GetLightingGuid();
//StaticMesh->GetLightingGuid() = FGuid::NewGuid();
// Add source to new StaticMesh
FStaticMeshSourceModel& SrcModel = StaticMesh->AddSourceModel();
//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);
//SrcModel.RawMeshBulkData->SaveRawMesh(RawMesh);
// Copy materials to new mesh
for (UMaterialInterface* Material : MeshMaterials)
{
StaticMesh->GetStaticMaterials().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;
}
else return nullptr;
}
return nullptr;
}
else {
return nullptr;
}
}
Anyway to get this working at runtime? Been tinkering and can’t get it to compile the packaged build! Thanks!
I believe this is the issue:
UATHelper Packaging (Windows): C:\MCD_TMS\Source\MCD_TMS\Private\TestMesh.cpp(90): error C2039: 'AddSourceModel': is not a member of 'UStaticMesh'
Log UATHelper Packaging (Windows): C:\MCD_TMS\Intermediate\Build\Win64\UnrealGame\Inc\MCD_TMS\TestMesh.generated.h(11): note: see declaration of ‘UStaticMesh’