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下