Bugs in Static Mesh Conversion

In the function FStaticMeshOperations::ConvertFromRawMesh(), in the file StaticMeshOperations.cpp, the new polygon groups are created for the new mesh.

//Create the PolygonGroups
for (int32 MaterialIndex : SourceRawMesh.FaceMaterialIndices)
{
	if (!MaterialIndexToPolygonGroup.Contains(MaterialIndex))
	{
		FPolygonGroupID PolygonGroupID(MaterialIndex);
		DestinationMeshDescription.CreatePolygonGroupWithID(PolygonGroupID);
		if (MaterialMap.Contains(MaterialIndex))
		{
			PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = MaterialMap[MaterialIndex];
		}
		else
		{
			PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = FName(*FString::Printf(TEXT("MaterialSlot_%d"), MaterialIndex));
		}
		PolygonGroups.Add(PolygonGroupID);
		MaterialIndexToPolygonGroup.Add(MaterialIndex, PolygonGroupID);
	}
}

Inside the first if statement, the PolygonGroupID is being created, but it is created based on the Material Index, as opposed to the polygon group that the original polygon is a part of.

This will cause the polygons to be placed in the group associated with the material index that it is mapped to. In this case, for LOD 0, the head polygons were moved from section 0 to section 7, as can be seen above. This resulted in the head using a different section to mapping. In this case section 7 mapped to material 5 - the lacrimal fluid.

There is also another issue that appears once this issue is resolved, although it was hidden in the higher LODs until this issue is resolved.

In the function FMeshUtilities::ConvertMeshesToStaticMesh(), from the file MeshUtilties.cpp, we have the section to material mapping being created:

int32 SectionIndex = 0;
for (int32 UniqueMaterialIndex : UniqueMaterialIndices)
{
	if (RawMesh.MaterialIndexToImportIndex.IsValidIndex(SectionIndex))
	{
		StaticMesh->GetSectionInfoMap().Set(RawMeshLODIndex, SectionIndex, FMeshSectionInfo(RawMesh.MaterialIndexToImportIndex[SectionIndex]));
	}
	else
	{
		StaticMesh->GetSectionInfoMap().Set(RawMeshLODIndex, SectionIndex, FMeshSectionInfo(UniqueMaterialIndex));
	}
	SectionIndex++;
}


For all the sections, it runs through all the unique material indices and tries to remap the indices if there is a mapping available. In the case where there is no mapping, it is because it is a 1:1 mapping (this is the else part). There it correctly uses the UniqueMaterialIndex to do the mapping. However, in the if part, it uses the SectionIndex as a map into the MaterialIndexToImportIndex instead of the UniqueMaterialIndex. This can result in incorrect section to material mappings in the cases where a MaterialIndexToImportIndex was needed, which is actually the case for the higher LODs.

It didn’t result in messed up materials in the particular cases for the higher LODs because the mapping just so happened to work out. (The head was moved to the bottom section, which just so happened to need a higher material slot mapping. And the other polygons all got moved up and their mappings also just happened to be in order.)

These issues are easily resolved using the correct variables for the indices.

//Create the PolygonGroups
int32 ActualPolygonSection = 0;
for (int32 MaterialIndex : SourceRawMesh.FaceMaterialIndices)
{
	if (!MaterialIndexToPolygonGroup.Contains(MaterialIndex))
	{
		//FPolygonGroupID PolygonGroupID(MaterialIndex);
		FPolygonGroupID PolygonGroupID(ActualPolygonSection);
	DestinationMeshDescription.CreatePolygonGroupWithID(PolygonGroupID);
		ActualPolygonSection++;
		if (MaterialMap.Contains(MaterialIndex))
		{
			PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = MaterialMap[MaterialIndex];
		}
		else
		{
				PolygonGroupImportedMaterialSlotNames[PolygonGroupID] = FName(*FString::Printf(TEXT("MaterialSlot_%d"), MaterialIndex));
		}
		PolygonGroups.Add(PolygonGroupID);
		MaterialIndexToPolygonGroup.Add(MaterialIndex, PolygonGroupID);
	}
}


For the second issue, just use UniqueMaterialIndex instead of SectionID for the MaterialIndexToImportedIndex array.

Steps to Reproduce
The issue can be reproduced most easily using an exported (assembled) MetaHuman head. Create a MetaHuman and export with “UE Optimized” and “High” quality.

The Face Skeletal Mesh that results can be opened, and a static mesh can be created using the “Make Static Mesh” button. Note in the LOD 0 Sections area that Section 0 holds the head polygons, which are mapped to material slot 7.

Making the static mesh results in the following for LOD0:

The materials are clearly incorrect. Note that Section 0 still maps to material slot 7. While section 7 maps to material slot 5.

Isolating just section 0, it is clear that section 0 contains the polygons for the teeth.

Isolating just section 7 shows that it contains the head, and is clear because it has the lacrimal fluid material mapped to it.

Note that this is all at LOD 0.

For LOD 1 (and others), they appear to be fine, as shown below.

Skeletal Mesh – LOD1

Static Mesh – LOD1

However, you can see that the not only are the polygons in different sections, the material mappings are also different between skeletal mesh and static mesh. While the result is correct, looking at the code it seems that it was lucky that the mesh turned out looking correct.

I have made 2 PRs

Use Polygon Section for PolygonID instead of MaterialIndex by daniel-stewart · Pull Request #14562 · EpicGames/UnrealEngine

Use UniqueMaterialIndex as index into MaterialIndexToImportIndex by daniel-stewart · Pull Request #14561 · EpicGames/UnrealEngine

Hi,

So sorry for the delay, and thanks for sharing those pull requests. I’ve linked the internal tracking on those PRs to this post for additional context, we’ll respond on the individual PRs once they’ve been reviewed. Thanks again!