Serializing Static Mesh

Static Mesh Utility C++ Code

The following is my static mesh utility code to support runtime static mesh and hierarchical instanced static mesh. The code has been tested and runs correctly at packaged build time which is the difficult part to code since epic’s build time codebase is different to its editor.

Ensure you have these dependencies in your .build.cs file

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "RenderCore", "InputCore" });

		PrivateDependencyModuleNames.AddRange(new string[] { "Json", "JsonUtilities", "MeshDescription", "StaticMeshDescription", "MeshConversion" });


#pragma once

#include "CoreMinimal.h"
#include "FStaticMeshData.generated.h"

USTRUCT()
struct FStaticMeshData
{
	GENERATED_BODY()
public:
	TArray<TArray<FVector3f>> Vertices;				// Vertices per LOD
	TArray<TArray<uint32>> PolygonGroupTriSizes;	// Array of polygon group triangle counts per LOD, The array size is the number of polygon groups for each LOD
	TArray<TArray<uint32>> TriangleIndices;			// Triangle Indices per LOD
	TArray<TArray<FVector3f>> Normals;				// Normals per LOD
	TArray<TArray<FVector3f>> Tangents;				// Tangents per LOD
	TArray<TArray<FColor>> Colors;					// Vertex colors per LOD
	TArray<uint32> UVLayers;						// Number of UV Layers per LOD
	TArray<TArray<FVector2f>> UV0;					// UV0s per LOD
	TArray<TArray<FVector2f>> UV1;					// UV1s per LOD
	TArray<uint32> MaterialIndexes;					// Material index order for LOD 0 - assumes all other lods are the same
	bool AutoLODScreenSizes;						// Automatically calculate LOD screen sizes
	TArray<float> LODScreenSizes;					// Screen size per LOD
	int32 CollisionType;							// 0 - No collider, 1 - Simple box collider, 2 - Mesh collider
	
	// Custom serialization function
	friend FArchive& operator<<(FArchive& Ar, FStaticMeshData& MyStruct)
	{
		Ar << MyStruct.Vertices;
		Ar << MyStruct.PolygonGroupTriSizes;
		Ar << MyStruct.TriangleIndices;
		Ar << MyStruct.Normals;
		Ar << MyStruct.Tangents;
		Ar << MyStruct.Colors;
		Ar << MyStruct.UVLayers;
		Ar << MyStruct.UV0;
		Ar << MyStruct.UV1;
		Ar << MyStruct.MaterialIndexes;
		Ar << MyStruct.AutoLODScreenSizes;
		Ar << MyStruct.LODScreenSizes;
		Ar << MyStruct.CollisionType;
	
		return Ar;
	}
};

#pragma once
#include "FStaticMeshData.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Model/FVTransform.h"
#include "Engine/StaticMesh.h"

class SMUtility
{
public:
	static TArray<uint8> GetStaticMeshAsByteArray(const UStaticMesh* StaticMesh, FStaticMeshData& MeshData);
	static void GetMeshData(const UStaticMesh* StaticMesh, FStaticMeshData& MeshData);
	static void DeserializeSM(UWorld* World, UStaticMeshComponent* SMComp, FStaticMeshData MeshData, TArray<FStaticMaterial> Materials);
	static void DeserializeHISM(UWorld* World, UHierarchicalInstancedStaticMeshComponent* HISMComp, FStaticMeshData MeshData, TArray<FVTransform> InstanceTransforms, TArray<UMaterialInstanceDynamic*> Materials);
	static void CheckLODSettings(UStaticMesh* StaticMesh);
private:
	static void GetVertexBufferData(const UStaticMesh* StaticMesh, TArray<TArray<FVector3f>>& OutVertexArray, int32 LODCount);
	static void GetTriangleIndices(const UStaticMesh* StaticMesh, TArray<TArray<uint32>>& OutPolygonGroupTriSizes, TArray<TArray<uint32>>& OutTriangleIndices, TArray<uint32>& OutMaterialIndexes, int32 LODCount);
	static void GetNormalsTangents(const UStaticMesh* StaticMesh, TArray<TArray<FVector3f>>& OutNormals, TArray<TArray<FVector3f>>& OutTangents, int32 LODCount);
	static void GetColors(const UStaticMesh* StaticMesh, TArray<TArray<FColor>>& OutColors, int32 LODCount);
	static void GetUVLayers(const UStaticMesh* StaticMesh, TArray<uint32>& OutUVLayers, int32 LODCount);
	static void GetUVs(const UStaticMesh* StaticMesh, TArray<uint32> OutUVLayers, TArray<TArray<FVector2f>>& OutUVs, uint32 channel, int32 LODCount);
	static void GetLODScreenSizes(const UStaticMesh* StaticMesh, bool& OutAutoLODScreenSizes, TArray<float>& OutScreenSizes);
	static void GetCollisionType(const UStaticMesh* StaticMesh, int32& OutCollisionType);
};

#include "SMUtility.h"

#include "MaterialDomain.h"
#include "MeshDescription.h"
#include "MeshDescriptionBuilder.h"
#include "StaticMeshAttributes.h"
#include "StaticMeshResources.h"
#include "UVStaticMesh.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/CollisionProfile.h"
#include "Engine/StaticMesh.h"
#include "Model/FVTransform.h"
#include "PhysicsEngine/BodySetup.h"

#if WITH_EDITOR
/**
 * Get the static mesh data byte array
 * @param StaticMesh - Static mesh
 * @param MeshData - Mesh data
 * @return Static mesh byte array
 */
TArray<uint8> SMUtility::GetStaticMeshAsByteArray(const UStaticMesh* StaticMesh, FStaticMeshData& MeshData)
{
	GetMeshData(StaticMesh, MeshData);

	TArray<uint8> SaveData;
	FMemoryWriter MemoryWriter(SaveData, true);
	MemoryWriter << MeshData;

	return SaveData;
}


// Get the static mesh data and assign to the static mesh data ustruct
void SMUtility::GetMeshData(const UStaticMesh* StaticMesh, FStaticMeshData& MeshData)
{
	if (!StaticMesh)
	{
		return;
	}

	// Find the static mesh LOD count
	int32 LODCount = StaticMesh->GetNumLODs();

	GetVertexBufferData(StaticMesh, MeshData.Vertices, LODCount);
	GetTriangleIndices(StaticMesh, MeshData.PolygonGroupTriSizes, MeshData.TriangleIndices, MeshData.MaterialIndexes, LODCount);
	GetNormalsTangents(StaticMesh, MeshData.Normals, MeshData.Tangents, LODCount);
	GetColors(StaticMesh, MeshData.Colors, LODCount);
	GetUVLayers(StaticMesh, MeshData.UVLayers, LODCount);
	GetUVs(StaticMesh, MeshData.UVLayers, MeshData.UV0, 0, LODCount);
	GetUVs(StaticMesh, MeshData.UVLayers, MeshData.UV1, 1, LODCount);
	GetLODScreenSizes(StaticMesh, MeshData.AutoLODScreenSizes, MeshData.LODScreenSizes);
	GetCollisionType(StaticMesh, MeshData.CollisionType);
}


// Get the vertex array from the static mesh
void SMUtility::GetVertexBufferData(const UStaticMesh* StaticMesh, TArray<TArray<FVector3f>>& OutVertexArray, const int32 LODCount)
{
	// Allocate memory for vertices for all LODs
	OutVertexArray.SetNum(LODCount);

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FPositionVertexBuffer& VertexBuffer = LODResources.VertexBuffers.PositionVertexBuffer;

		OutVertexArray[LODIndex].SetNum(VertexBuffer.GetNumVertices());

		// Iterate through the vertex buffer and extract vertex positions
		for (uint32 i = 0; i < VertexBuffer.GetNumVertices(); i++)
			OutVertexArray[LODIndex][i] = VertexBuffer.VertexPosition(i);
	}
}


// Get the triangle index array for each polygon group from the static mesh
void SMUtility::GetTriangleIndices(const UStaticMesh* StaticMesh, TArray<TArray<uint32>>& OutPolygonGroupTriSizes, TArray<TArray<uint32>>& OutTriangleIndices, TArray<uint32>& OutMaterialIndexes, const int32 LODCount)
{
	// Allocate polygon group tri size array for all LODs
	OutPolygonGroupTriSizes.SetNum(LODCount);

	// Allocate triangle indices array for all LODs
	OutTriangleIndices.SetNum(LODCount);

	// Allocate material indexes
	OutMaterialIndexes.SetNum(StaticMesh->GetRenderData()->LODResources[0].Sections.Num());

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FIndexArrayView& IndexArrayView = LODResources.IndexBuffer.GetArrayView();

		// Ensure the output array is empty before copying
		OutPolygonGroupTriSizes[LODIndex].SetNum(LODResources.Sections.Num());
		OutTriangleIndices[LODIndex].SetNum(IndexArrayView.Num());

		int32 Index = 0;
		// For each polygon group, copy the triangle indices to the triangle indices array
		for (int32 SectionIndex = 0; SectionIndex < LODResources.Sections.Num(); SectionIndex++)
		{
			const FStaticMeshSection& Section = LODResources.Sections[SectionIndex];

			if (LODIndex == 0)
				OutMaterialIndexes[SectionIndex] = Section.MaterialIndex;

			OutPolygonGroupTriSizes[LODIndex][SectionIndex] = Section.NumTriangles;

			// Get the start and end index for the polygon group section
			int32 StartIndex = Section.FirstIndex;
			int32 EndIndex = Section.FirstIndex + Section.NumTriangles * 3;

			for (int32 i = StartIndex; i < EndIndex; i++)
			{
				OutTriangleIndices[LODIndex][Index] = IndexArrayView[i];
				Index++;
			}
		}
	}
}


// Get the normals from the static mesh
void SMUtility::GetNormalsTangents(const UStaticMesh* StaticMesh, TArray<TArray<FVector3f>>& OutNormals, TArray<TArray<FVector3f>>& OutTangents, const int32 LODCount)
{
	OutNormals.SetNum(LODCount);
	OutTangents.SetNum(LODCount);

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FStaticMeshVertexBuffer& VertexBuffer = LODResources.VertexBuffers.StaticMeshVertexBuffer;

		// Ensure the output array is empty before copying
		OutNormals[LODIndex].SetNum(VertexBuffer.GetNumVertices());
		OutTangents[LODIndex].SetNum(VertexBuffer.GetNumVertices());

		// Copy the normals to the output array
		for (uint32 i = 0; i < VertexBuffer.GetNumVertices(); i++)
		{
			OutNormals[LODIndex][i] = VertexBuffer.VertexTangentZ(i);
			OutTangents[LODIndex][i] = VertexBuffer.VertexTangentX(i);
		}
	}
}

// Get the color array from the static mesh
void SMUtility::GetColors(const UStaticMesh* StaticMesh, TArray<TArray<FColor>>& OutColors, const int32 LODCount)
{
	OutColors.SetNum(LODCount);

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FColorVertexBuffer& ColorVertexBuffer = LODResources.VertexBuffers.ColorVertexBuffer;

		// Ensure the output array is empty before copying
		OutColors[LODIndex].SetNum(ColorVertexBuffer.GetNumVertices());

		// Copy the vertex colors to the output array
		for (uint32 i = 0; i < ColorVertexBuffer.GetNumVertices(); ++i)
			OutColors[LODIndex][i] = ColorVertexBuffer.VertexColor(i);
	}
}

// Get the uv layers for all LODs
void SMUtility::GetUVLayers(const UStaticMesh* StaticMesh, TArray<uint32>& OutUVLayers, const int32 LODCount)
{
	OutUVLayers.SetNum(LODCount);

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FStaticMeshVertexBuffer& VertexBuffer = LODResources.VertexBuffers.StaticMeshVertexBuffer;
		OutUVLayers[LODIndex] = VertexBuffer.GetNumTexCoords();
	}
}

// Get the UV channel array from the static mesh all LODs
void SMUtility::GetUVs(const UStaticMesh* StaticMesh, TArray<uint32> OutUVLayers, TArray<TArray<FVector2f>>& OutUVs, const uint32 channel, const int32 LODCount)
{
	OutUVs.SetNum(LODCount);

	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		const FStaticMeshLODResources& LODResources = StaticMesh->GetRenderData()->LODResources[LODIndex];
		const FStaticMeshVertexBuffer& VertexBuffer = LODResources.VertexBuffers.StaticMeshVertexBuffer;

		if (channel < OutUVLayers[LODIndex])
		{
			OutUVs[LODIndex].SetNum(VertexBuffer.GetNumVertices());

			// Get the vertex UVs for a specific channel
			for (uint32 i = 0; i < VertexBuffer.GetNumVertices(); i++)
				OutUVs[LODIndex][i] = VertexBuffer.GetVertexUV(i, channel);
		}
		else
		{
			OutUVs[LODIndex].SetNum(0);
		}
	}
}

void SMUtility::GetLODScreenSizes(const UStaticMesh* StaticMesh, bool& OutAutoLODScreenSizes, TArray<float>& OutScreenSizes)
{
	const TArray<FStaticMeshSourceModel>& SourceModels = StaticMesh->GetSourceModels();

	OutAutoLODScreenSizes = StaticMesh->bAutoComputeLODScreenSize == 1;

	if (OutAutoLODScreenSizes)
	{
		OutScreenSizes.SetNum(0);
	}
	else
	{
		OutScreenSizes.SetNum(SourceModels.Num());

		for (int32 LODIndex = 0; LODIndex < SourceModels.Num(); LODIndex++)
		{
			OutScreenSizes[LODIndex] = SourceModels[LODIndex].ScreenSize.GetValue();
			//UE_LOG(LogTemp, Log, TEXT("LOD Index: %d  Screen Size : %f"), LODIndex, SourceModels[LODIndex].ScreenSize.GetValue());
		}
	}

	//UE_LOG(LogTemp, Log, TEXT("Auto compute screen size : %s"), OutAutoLODScreenSizes ? TEXT("true") : TEXT("false"));
}

void SMUtility::GetCollisionType(const UStaticMesh* StaticMesh, int32& OutCollisionType)
{
	// Simple box or sphere collision
	if(StaticMesh->GetBodySetup()->AggGeom.BoxElems.Num() == 1 || StaticMesh->GetBodySetup()->AggGeom.SphereElems.Num() == 1)
		OutCollisionType = 1;
	else
	// Consider multiple aggregated simple geometry as complex	
	if (StaticMesh->GetBodySetup()->AggGeom.BoxElems.Num() > 0 ||
		StaticMesh->GetBodySetup()->AggGeom.SphereElems.Num() > 0 ||
		StaticMesh->GetBodySetup()->AggGeom.SphylElems.Num() > 0 ||
		StaticMesh->GetBodySetup()->AggGeom.ConvexElems.Num() > 0)
		OutCollisionType = 2;
	// Complex as simple is complex
	else if (StaticMesh->GetBodySetup()->CollisionTraceFlag == CTF_UseComplexAsSimple)
		OutCollisionType = 2;
	else
		OutCollisionType = 0;
}

#endif

/**
 * Deserialize static mesh - both editor and package build
 * @param World - Reference to the world
 * @param SMComp - Static mesh component
 * @param MeshData - Static mesh data
 * @param Materials - Static material array
 */
void SMUtility::DeserializeSM(UWorld* World, UStaticMeshComponent* SMComp, FStaticMeshData MeshData, TArray<FStaticMaterial> Materials)
{
	int32 LODCount = MeshData.Vertices.Num();

	// Create static mesh
	UVStaticMesh* StaticMesh = NewObject<UVStaticMesh>(GetTransientPackage(), NAME_None, RF_Public | RF_Standalone);
	StaticMesh->SetWorld(World);

	// Add default materials and material slot names
	TArray<FName> MaterialSlotNames;
	for (int MatIndex = 0; MatIndex < Materials.Num(); MatIndex++)
	{
		FName MaterialSlotName = StaticMesh->AddMaterial(UMaterial::GetDefaultMaterial(EMaterialDomain::MD_Surface));
		MaterialSlotNames.Add(MaterialSlotName);
	}

	// LOD mesh description array to contain the geometry, uv, normals, tangents, etc.
	FMeshDescription meshDescriptions[6];

	// Iterate through the lod index
	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		// Register the static mesh attributes
		FStaticMeshAttributes Attributes(meshDescriptions[LODIndex]);
		Attributes.Register();

		// Setup a mesh description builder
		FMeshDescriptionBuilder meshDescBuilder;
		meshDescBuilder.SetMeshDescription(&meshDescriptions[LODIndex]);
		meshDescBuilder.EnablePolyGroups();
		meshDescBuilder.SetNumUVLayers(MeshData.UVLayers[LODIndex]);

		// Allocate vertices
		TArray<FVertexID> VertexIDs;
		VertexIDs.SetNum(MeshData.Vertices[LODIndex].Num());
		for (int i = 0; i < VertexIDs.Num(); i++)
			VertexIDs[i] = meshDescBuilder.AppendVertex(FVector(MeshData.Vertices[LODIndex][i]));

		// Allocate vertex instances (3 per face)
		TArray<FVertexInstanceID> VertexInsts;

		for (int i = 0; i < VertexIDs.Num(); i++)
		{
			FVertexInstanceID instance = meshDescBuilder.AppendInstance(VertexIDs[i]);
			meshDescBuilder.SetInstanceTangentSpace(instance, FVector(MeshData.Normals[LODIndex][VertexIDs[i]]), FVector(MeshData.Tangents[LODIndex][VertexIDs[i]]), 1);

			if (MeshData.UVLayers[LODIndex] == 1)
			{
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV0[LODIndex][VertexIDs[i]]), 0);
			}
			else if (MeshData.UVLayers[LODIndex] == 2)
			{
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV0[LODIndex][VertexIDs[i]]), 0);
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV1[LODIndex][VertexIDs[i]]), 1);
			}

			VertexInsts.Add(instance);
		}

		// Allocate a polygon groups
		FPolygonGroupID PolygonGroupIDs[10];
		for (int i = 0; i < Materials.Num(); i++)
			PolygonGroupIDs[i] = meshDescBuilder.AppendPolygonGroup();

		// Add triangle indices by polygon group id
		uint32 EndIndex = 0;
		for (int32 PolyGroupIndex = 0; PolyGroupIndex < MeshData.PolygonGroupTriSizes[LODIndex].Num(); PolyGroupIndex++)
		{
			const uint32 StartIndex = EndIndex;
			EndIndex = EndIndex + MeshData.PolygonGroupTriSizes[LODIndex][PolyGroupIndex] * 3;

			for (uint32 i = StartIndex; i < EndIndex; i += 3)
			{
				meshDescBuilder.AppendTriangle(
					VertexInsts[MeshData.TriangleIndices[LODIndex][i]],
					VertexInsts[MeshData.TriangleIndices[LODIndex][i + 1]],
					VertexInsts[MeshData.TriangleIndices[LODIndex][i + 2]],
					PolygonGroupIDs[MeshData.MaterialIndexes[PolyGroupIndex]]);
			}
		}

		// Assign the material slot names to the polygon group attributes
		for (FPolygonGroupID PolygonGroupID : meshDescriptions[LODIndex].PolygonGroups().GetElementIDs())
		{
			meshDescriptions[LODIndex].PolygonGroupAttributes().SetAttribute(PolygonGroupID, MeshAttribute::PolygonGroup::ImportedMaterialSlotName, 0, MaterialSlotNames[PolygonGroupID.GetValue()]);
		}
	}

	// Create the mesh description pointer array for each LOD 
	TArray<const FMeshDescription*> MeshDescPtrs;
	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
		MeshDescPtrs.Add(&meshDescriptions[LODIndex]);

	// Setup a static mesh for rendering
	StaticMesh->CreateBodySetup();
	StaticMesh->InitResources();

	// Build the static mesh
	UStaticMesh::FBuildMeshDescriptionsParams mdParams;
	mdParams.bBuildSimpleCollision = MeshData.CollisionType == 1;
	mdParams.bAllowCpuAccess = true;
	mdParams.bFastBuild = true;
	StaticMesh->NeverStream = true;
	StaticMesh->bAllowCPUAccess = true;
	StaticMesh->BuildFromMeshDescriptions(MeshDescPtrs, mdParams);

	// Set the LOD screen sizes
	if (!MeshData.AutoLODScreenSizes)
	{
		for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
			StaticMesh->GetRenderData()->ScreenSize[LODIndex] = MeshData.LODScreenSizes[LODIndex];
	}

	// Create a complex collision for mesh
	if (MeshData.CollisionType == 2)
	{
		StaticMesh->GetBodySetup()->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseComplexAsSimple;
		StaticMesh->GetBodySetup()->CreatePhysicsMeshes();
		SMComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
	}

	// Assign the materials to the static mesh in the correct index order
	StaticMesh->SetStaticMaterials(Materials);

	// Set the static mesh on the component
	SMComp->SetStaticMesh(StaticMesh);
}

/**
 * Deserialize hierarchical instance static mesh component - both editor and package build
 * @param World - World reference
 * @param HISMComp - Hierarchical instance static mesh component
 * @param MeshData - Static mesh data
 * @param InstanceTransforms - Instance transforms
 * @param Materials - Static material array
 */
void SMUtility::DeserializeHISM(UWorld* World, UHierarchicalInstancedStaticMeshComponent* HISMComp, FStaticMeshData MeshData, TArray<FVTransform> InstanceTransforms, TArray<UMaterialInstanceDynamic*> Materials)
{
	int32 LODCount = MeshData.Vertices.Num();

	// Create static mesh
	UVStaticMesh* StaticMesh = NewObject<UVStaticMesh>(GetTransientPackage(), NAME_None, RF_Public | RF_Standalone);
	StaticMesh->SetWorld(World);

	// Add default materials and material slot names
	TArray<FName> MaterialSlotNames;
	for (int32 MatIndex = 0; MatIndex < Materials.Num(); MatIndex++)
	{
		FName MaterialSlotName = StaticMesh->AddMaterial(UMaterial::GetDefaultMaterial(MD_Surface));
		MaterialSlotNames.Add(MaterialSlotName);
	}

	// LOD mesh description array to contain the geometry, uv, normals, tangents, etc.
	FMeshDescription meshDescriptions[6];

	// Iterate through the lod index
	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
	{
		// Register the static mesh attributes
		FStaticMeshAttributes Attributes(meshDescriptions[LODIndex]);
		Attributes.Register();

		// Setup a mesh description builder
		FMeshDescriptionBuilder meshDescBuilder;
		meshDescBuilder.SetMeshDescription(&meshDescriptions[LODIndex]);
		meshDescBuilder.EnablePolyGroups();
		meshDescBuilder.SetNumUVLayers(MeshData.UVLayers[LODIndex]);

		// Allocate vertices
		TArray<FVertexID> VertexIDs;
		VertexIDs.SetNum(MeshData.Vertices[LODIndex].Num());
		for (int i = 0; i < VertexIDs.Num(); i++)
			VertexIDs[i] = meshDescBuilder.AppendVertex(FVector(MeshData.Vertices[LODIndex][i]));

		// Allocate vertex instances (3 per face)
		TArray<FVertexInstanceID> VertexInsts;

		for (int i = 0; i < VertexIDs.Num(); i++)
		{
			FVertexInstanceID instance = meshDescBuilder.AppendInstance(VertexIDs[i]);
			meshDescBuilder.SetInstanceTangentSpace(instance, FVector(MeshData.Normals[LODIndex][VertexIDs[i]]), FVector(MeshData.Tangents[LODIndex][VertexIDs[i]]), 1);

			if (MeshData.UVLayers[LODIndex] == 1)
			{
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV0[LODIndex][VertexIDs[i]]), 0);
			}
			else if (MeshData.UVLayers[LODIndex] == 2)
			{
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV0[LODIndex][VertexIDs[i]]), 0);
				meshDescBuilder.SetInstanceUV(instance, FVector2D(MeshData.UV1[LODIndex][VertexIDs[i]]), 1);
			}

			VertexInsts.Add(instance);
		}

		// Allocate a polygon groups
		FPolygonGroupID PolygonGroupIDs[10];
		for (int i = 0; i < Materials.Num(); i++)
			PolygonGroupIDs[i] = meshDescBuilder.AppendPolygonGroup();

		// Add triangle indices by polygon group id
		uint32 EndIndex = 0;
		for (int32 PolyGroupIndex = 0; PolyGroupIndex < MeshData.PolygonGroupTriSizes[LODIndex].Num(); PolyGroupIndex++)
		{
			const uint32 StartIndex = EndIndex;
			EndIndex = EndIndex + MeshData.PolygonGroupTriSizes[LODIndex][PolyGroupIndex] * 3;

			for (uint32 i = StartIndex; i < EndIndex; i += 3)
			{
				meshDescBuilder.AppendTriangle(
					VertexInsts[MeshData.TriangleIndices[LODIndex][i]],
					VertexInsts[MeshData.TriangleIndices[LODIndex][i + 1]],
					VertexInsts[MeshData.TriangleIndices[LODIndex][i + 2]],
					PolygonGroupIDs[MeshData.MaterialIndexes[PolyGroupIndex]]);
			}
		}

		// Assign the material slot names to the polygon group attributes
		for (FPolygonGroupID PolygonGroupID : meshDescriptions[LODIndex].PolygonGroups().GetElementIDs())
		{
			meshDescriptions[LODIndex].PolygonGroupAttributes().SetAttribute(PolygonGroupID, MeshAttribute::PolygonGroup::ImportedMaterialSlotName, 0, MaterialSlotNames[PolygonGroupID.GetValue()]);
		}
	}

	// Create the mesh description pointer array for each LOD 
	TArray<const FMeshDescription*> MeshDescPtrs;
	for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
		MeshDescPtrs.Add(&meshDescriptions[LODIndex]);

	// Setup a static mesh for rendering
	StaticMesh->CreateBodySetup();
	StaticMesh->InitResources();

	// Build the static mesh
	UStaticMesh::FBuildMeshDescriptionsParams mdParams;
	mdParams.bBuildSimpleCollision = MeshData.CollisionType == 1;
	mdParams.bAllowCpuAccess = true;
	mdParams.bFastBuild = true;
	StaticMesh->NeverStream = true;
	StaticMesh->bAllowCPUAccess = true;
	StaticMesh->BuildFromMeshDescriptions(MeshDescPtrs, mdParams);

	// Set the LOD screen sizes
	if (!MeshData.AutoLODScreenSizes)
	{
		for (int32 LODIndex = 0; LODIndex < LODCount; LODIndex++)
			StaticMesh->GetRenderData()->ScreenSize[LODIndex] = MeshData.LODScreenSizes[LODIndex];
	}

	// Create a complex collision for mesh
	if (MeshData.CollisionType == 2)
	{
		StaticMesh->GetBodySetup()->CollisionTraceFlag = ECollisionTraceFlag::CTF_UseComplexAsSimple;
		StaticMesh->GetBodySetup()->CreatePhysicsMeshes();
		HISMComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
	}

	// Set the static mesh on the component
	HISMComp->SetStaticMesh(StaticMesh);

	// Create the instance transforms
	for (FVTransform transform : InstanceTransforms)
	{
		HISMComp->AddInstance(FTransform(
			FRotator(transform.rotation.Y, transform.rotation.Z, transform.rotation.X),
			FVector(transform.position), FVector(transform.scale)));
	}

	// Assign materials
	for (int i = 0; i < Materials.Num(); i++)
		HISMComp->SetMaterial(i, Materials[i]);
}

/**
 * Check lod settings
 * @param StaticMesh - Static mesh
 */
void SMUtility::CheckLODSettings(UStaticMesh* StaticMesh)
{
	if (!StaticMesh)
	{
		UE_LOG(LogTemp, Warning, TEXT("Missing static mesh"));
		return;
	}

	int32 NumLODs = StaticMesh->GetNumLODs();
	UE_LOG(LogTemp, Log, TEXT("Number of LODs available: %d"), NumLODs);

	for (int32 LODIndex = 0; LODIndex < NumLODs; LODIndex++)
	{
		UE_LOG(LogTemp, Log, TEXT("LOD %d ScreenSize: %f"), LODIndex, StaticMesh->GetRenderData()->ScreenSize[LODIndex].GetValue());
	}
}

To support collisions in packaged build we create a UVStaticMesh which inherits from UStaticMesh. From here we can override the World and set the world.

#pragma once

#include "UVStaticMesh.generated.h"

UCLASS()
class VIRTUOSOCITYPRJ_API UVStaticMesh : public UStaticMesh
{
	GENERATED_BODY()
	
public:
	UVStaticMesh()
		: World(nullptr)
	{
		bAllowCPUAccess = true;
	}

	virtual UWorld* GetWorld() const override { return World ? World : UStaticMesh::GetWorld(); }

	void SetWorld(UWorld* InWorld) { World = InWorld; }

private:
	UWorld* World;
};

Spawning Static Mesh Actors

// Define spawn location, rotation, and scale
FVector Location(ComponentData.transform.position);
FRotator Rotation(ComponentData.transform.rotation.Y, ComponentData.transform.rotation.Z, ComponentData.transform.rotation.X);
FVector Scale(ComponentData.transform.scale);

if (AStaticMeshActor* SpawnedActor = GetWorld()->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), FTransform(Rotation, Location, Scale), SpawnParams))
		{
			SpawnedActorMap.Add(SpawnedActor->GetUniqueID(), SpawnedActor);

			SpawnedActor->GetRootComponent()->SetMobility(EComponentMobility::Type::Movable);

// Deserialize the byte array data

TArray<uint8> Data = <LOAD YOUR DATA  FILE / HTTP ETC>;
FStaticMeshData MeshData;

if (!Data.IsEmpty())
{
	// Deserialize the raw data into mesh data struct
	FMemoryReader MemoryReader(Data, true);
	MemoryReader << MeshData;
}

// Create the dynamic material instance array
TArray<FStaticMaterial> MaterialInstances;

// Create a material instance and supply its parent (master material)
UMaterialInstanceDynamic* MaterialInstance = UMaterialInstanceDynamic::Create(ParentMaterial, GetTransientPackage(), FName(material.name));

FStaticMaterial StaticMaterial = FStaticMaterial(MaterialInstance);
StaticMaterial.UVChannelData.bInitialized = true;
MaterialInstances.Add(StaticMaterial);

MaterialInstances.Add(StaticMaterial)

.. Add as many materials using the above approach as  you need for the mesh


SMUtility::DeserializeSM(SpawnedActor->GetWorld(), SpawnedActor->GetRootComponent(), MeshData, MaterialInstances);

}
	

Hierarchical Instanced Static Mesh

	UHierarchicalInstancedStaticMeshComponent* HISMComp = NewObject<UHierarchicalInstancedStaticMeshComponent>(this, TEXT("HISMComponent"));
	if (HISMComp)
	{
		HISMComp->RegisterComponent();
		HISMComp->AttachToComponent(Component, FAttachmentTransformRules::KeepRelativeTransform);
	}


TArray<FVTransform> Transforms;

// Populate the transforms where your instances should be located

	SMUtility::DeserializeHISM(World, HISMComp, MeshData, Transforms, MaterialInstances);
#pragma once

#include "CoreMinimal.h"
#include "FVTransform.generated.h"

USTRUCT()
struct FVTransform
{
	GENERATED_BODY()

	UPROPERTY()
	FVector3d position;

	UPROPERTY()
	FVector3d rotation;

	UPROPERTY()
	FVector3d scale;

	FVTransform()
	: position(FVector3d::ZeroVector)
	, rotation(FVector3d::ZeroVector)
	, scale(FVector3d::ZeroVector)
	{
	}

	FVTransform(const FVector3d& Position, const FVector3d& Rotation, const FVector3d& Scale):
		position(Position),
		rotation(Rotation),
		scale(Scale)
	{
	}
};

1 Like