Procedural mesh not saving all of its sections to static mesh

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?

Oh. Its because I am returning from inside the section for loop.

I had the same issue, could you share your modified code? thx.

@BlackRang666 That’s awesome.
i tried replacing your code in ProceduralMeshComponentDetails.cpp

But i got error as ‘UWebEZBPFunctionLibrary’: is not a class or namespace name. How do i do that ?

can it be possible by blueprint node? can you please share it?

Just replace the “UWebEZBPFunctionLibrary” to the namespace of your C++ Blueprint Function Library class and this should be fixed.

Is there a tutorial for converting procedural mesh into static mesh in c++?

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?

I have the same question ,cause RawMesh belongs to developer folders.

Step 1: In xxxxx Build.cs
PublicDependencyModuleNames.AddRange(
new string[]
{

“RawMesh”,
“AssetTools”,

                // ... add other public dependencies that you statically link with here ...
            }
            );

Step 2: in your xx.h

UFUNCTION(BlueprintCallable, Category = "")
static UStaticMesh* SaveProceduralMeshToStaticMesh(UProceduralMeshComponent* ProcMesh, FString AssetName = FString(TEXT("test")), FString PathName = FString(TEXT("/Game/RobotMeshes/")));

Step3 :in your xx.cpp

#include “Developer/RawMesh/Public/RawMesh.h”

#include “AssetRegistryModule.h”

#include “Classes\Engine\StaticMesh.h”

Step4 :in your xx.cpp some different

UStaticMesh* URuntimeMeshImportExportLibrary::SaveProceduralMeshToStaticMesh(UProceduralMeshComponent* ProcMesh, FString AssetName, FString PathName)
{

//FString AssetName = FString(TEXT(“SM_”)) + LevelName + FString(TEXT("_") + ActorName);
//FString PathName = FString(TEXT("/Game/RobotMeshes/"));
FString PackageName = PathName + AssetName;

			FStaticMeshSourceModel& SrcModel = StaticMesh->AddSourceModel();
			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);

			return StaticMesh;
		}
	}
}

return nullptr;

}

By the way: I test in 4.23.1 ,It work!

sorry i can’t upload txt file,because type invaild

其实,问问题的楼主的源码就可以用,只是需要引入你说的那几个模块就可以解决。
值得关注的是这些方法是在editorMode下使用的,运行的时候就不能用了,因为引用了Developer和只能在编辑器下运行的模块,比如说rawmesh,所以适合开发编辑器的插件,暂时不适用运行状态。

Is this answer about how to package game while using RawMesh.h?

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.

是这样的,我也在找方法将Developer下的代码移到Runtime下

Hi everyone! I am looking for the same thing and I have two questions:

  1. Does this work with UE5?
  2. Does this works with packaged games?

Thanks!

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;
}
2 Likes

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;
	}
}
2 Likes

Thank you very much! The code works fine in the compiled game.

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’