Wrong materials and geometry on LODs

We are have a nanite enabled project that has to run on a non nanite platform.

We used the trick described in the docs to have a custom nanite fallback mesh (so we have an imported LOD 1, Minimum Lod = 1, Percent Triangles = 0).

When a nanite-enabled mesh has the custom nanite fallback mesh, standard LODs cannot be really used since they are usually completely broken.

In the attached project you can see a mesh with lods with wrong materials (with the lod is generated with LOD 1 as base lod) or with wrong geometry (if base lod is 0).

The mesh shows other strange data, such as LOD 0 with just 63 triangles.

The nanite custom fallback mesh is a bit hacky and create a mess (even for other internal tools). It would be great if we can just have a simple “Import Nanite fallback mesh” that doesn’t mess with existing data and pipelines (ie it does not interact with standard lods).

Steps to Reproduce
Load Map and look at SM_Test_BaseLod0 and SM_Test_BaseLod1 instances.

Their LODs have wrong materials or are generate starting from wrong geometry

Hi,

thanks for the repro project, I was able to replicate the issue on my side. I experimented with some settings and found that toggling “Auto Compute LOD Distance” on the SM_Test_Base_Lod1 instance seems to fix the broken materials. The LOD does change when moving the camera away from the scene (this can be verified by checking the Fallback triangles), but it requires to move the camera quite far away. Can you please try this and see if this fixes your issue?

Thanks,

Sam

Hi, thanks for clarifying that, I see what you mean. I will file a bug report with Epic and post a link here once the issue is made available on the public issue tracker.

Thanks,

Sam

Hi,

the issue is now public at this link.

Please let me know if you have any further questions or comments.

Thanks,

Sam

Hi Marco,

I’m investigating why the materials get mixed up. Hopefully I’ll have a solution for that for the UE 5.7 release. I just wanted to pop in here to clear up a couple of things. First off, if it wasn’t immediately obvious, the reason SM_Test_BaseLod0 in your project has overly simplified geometry is because the reduction settings are using LOD 0 (the generated Nanite fallback) as the source. This means it’s starting with the generated fallback (which has the minimum triangle count of 64) and trying to reduce it further, which will basically keep those 64 triangles. The geometry for SM_Test_BaseLod1 is correct because the reduction settings is using the proper Base LOD (your non-Nanite source geometry for LOD 1). So this is all behaving as expected. The materials changing is obviously not.

The main reason I popped in here is I also wanted to point out that if you prefer to not use this “LOD 0 is Nanite, LOD 1 is non-Nanite” workflow, you could instead import your non-Nanite resolution mesh (your manual fallback), enable Nanite in its Nanite Settings, and use “Source Import Filename” under Nanite Settings to import a higher resolution mesh to be used only for Nanite. This way, you don’t have to set the Minimum LOD or mess with the base mesh in your reduction settings.

Hope that helps!

-Jamie

I may found a solution (and a reprostep).

When I have a 2 LODs mesh (just reduce the attached model to 2 lods) and add a third LOD, the section info map is initialized with progressive IDs (probably taken from LOD 0).

When I chose that the Base Reduction lod of this third LOD to be LOD 1 (and not LOD 0) the sections material binding gets corrupted after the reduction.

This happens in FStaticMeshBuilder::Build.

Existing section info map is useless (since the base reduction lod changed and triangles have been reduced) but if the total number of sections does not change, the code consider the section info valid and does not change it.

I fixed this by setting bIsOldMappingInvalid and bHasValidLODInfoMap to false

Hey Marco,

Would you be willing to submit your changes as a pull request so I can give it a good review? If it fixes these issues for us and the next engine release is easy for you to integrate, then win/win.

-Jamie

Thanks, Marco!

I’ll make sure I go over this as soon as I’m able.

-Jamie

Hi. It didn’t fix the issue on my side: the materials are still broken, both in mesh editor (choose the lod to render explicitly from to toolbar) and in map main view map (just stay close to the actor, and force each lod index, setting “Forced Lod Model” from 1 to 8, the greatest difference will be noticed setting it ti 3 and then 4).

What is the proper way to force a rebuild of all meshes (local/DDC/Zen)?

However there might be other issues too. Such as, BeforeBuildSectionInfoMap: why the code just consider the sections that are present before the actual reduction and not the progressively added sections?

Consider these 2 reprosteps:

1) import lod 0, creation of 3 more lods (during the reduction process BeforeBuildSectionInfoMap contains only lod 0, so lod 1,2,3 has bValidBaseSectionInfoMap false, so each section’s material index will be equal to section index that is not correct if the sections change order or some of them are removed due to reduction with DeletePolygonGroup in ReduceMeshDescription)

2) add 4 more lods (this time the reduction of lod 1,2,3 has bValidBaseSectionInfoMap true, so the each section’s material index will be correct).

Another thing I don’t understand is the code that updates GetOriginalSectionInfoMap. In one of the occurrencies you update LodIndex’s sections using BaseSectionIndex and not SectionIndex: is this correct? Shouldn’t it be always SectionIndex?

Another bug may be in FStaticMeshBuilder::Build when you call GetMaterialIndexFromImportedMaterialSlotName on LodIndex and MaterialIndex is INDEX_NONE. This happened when BaseReduceLodIndex lod has no Imported Material Slot Names (this happens on meshes generated with modelling tools).

We have a mesh with 6 material slots (0..5) and a Lod 0 with 6 sections (in the same slots order).

Lod 1 is generated and section/slot 4 is not needed (0 triangles) so this 5 sections map slots 0,1,2,3,5.

Lod 2, generated from Lod 1, has 5 sections (with less triangles) that should map the same slots (0,1,2,3,5).

However GetMaterialIndexFromImportedMaterialSlotName always return INDEX_NONE, so UniqueMaterialIndex will be (0,1,2,3,4), not (0,1,2,3,5).

Probably, if new lod’s PolygonGroupID are referred to BaseReduceLodIndex indices, if GetMaterialIndexFromImportedMaterialSlotName is INDEX_NONE, I should use populate UniqueMaterialIndex with BaseUniqueMaterialIndexes[PolygonGroupID.GetValue()] and not PolygonGroupID.GetValue().

Hi Marco,

> What is the proper way to force a rebuild of all meshes (local/DDC/Zen)?

You can use -ddc=cold if you just want it to rebuild the first time, or -DDC-All-MissTypes=StaticMesh if you want to always force static meshes to build. Though the issue I’m seeing in my debugging of your building asset seems to be stemmed from when the mesh is actually cached.

What I see happening is in FStaticMeshRenderData::ResolveSectionInfo, the LOD render data’s material index gets stomped with LOD0 section order when the reduced LOD is not represented in the section info map. If I change the inner for loop in there to the follow, it seems to fix it:

for (; LODIndex < MaxLODs; ++LODIndex)
	{
		FStaticMeshLODResources& LOD = LODResources[LODIndex];
		const FMeshSectionInfoMap& SectionInfoMap = Owner->GetSectionInfoMap();
		for (int32 SectionIndex = 0; SectionIndex < LOD.Sections.Num(); ++SectionIndex)
		{
			if (SectionInfoMap.IsValidSection(LODIndex, SectionIndex))
			{
				FMeshSectionInfo Info = SectionInfoMap.Get(LODIndex,SectionIndex);
				FStaticMeshSection& Section = LOD.Sections[SectionIndex];
				Section.MaterialIndex = Info.MaterialIndex;
				Section.bEnableCollision = Info.bEnableCollision;
				Section.bCastShadow = Info.bCastShadow;
				Section.bVisibleInRayTracing = Info.bVisibleInRayTracing;
				Section.bAffectDistanceFieldLighting = Info.bAffectDistanceFieldLighting;
				Section.bForceOpaque = Info.bForceOpaque;
			}
		}

Maybe this is causing some of the other issues you’re seeing on your end? I’m still unsure if that’s a proper fix (I’m only just now getting familiarized with this code myself). It may be the case that we should be fixing up the section info map well before this.

Some of your other findings raise questions for me as well. It seems we have some long-standing issues in the static mesh LOD code that assumes all imported LODs have the same mesh section material ordering. Seems these issues you’re facing are stemmed from the fact the the order in the LOD0 and LOD1 source meshes are not the same, and I think this may just be a scenario we’ve not properly accounted for in some places in the code. Seems from your latest post that there may also be an issue when creating reductions from reductions that have completely simplified away a material.

I think the “right” fix for this is going to be a bit more involved and require some careful thought than I anticipated, so unfortunately we’ll have to address this in a release after 5.7. I might suggest in the meantime trying to get your pipeline/DCC tool to generate source FBX files that maintain material order of the mesh sections for both imported LODs. If you were to use “Nanite Settings > Source Import Filename” instead of doing the MinLOD=1 trick, the mesh section material order and count between that file and the LOD 0 file is also expected to match (otherwise, build warnings/errors occur and the higher res source file is ignored). Also seems recommended to always set up reduction LODs to simplify from an imported base LOD to work around issues with materials simplifying away.

Thanks for your insight! I’ll add some of these details to the issue tracker. Keep an eye on UE-326827 for fixes for these issues in an upcoming release.

Thanks!

-Jamie

Thanks for your detail answer.

I looked at the code a bit more and tried to fix it in the way I think it should manage the sections and polygon group ids in the source data. We are going to try the modded version for some time.

In FStaticMeshBuilder::Build instead of using GetMaterialIndexFromImportedMaterialSlotName path we simply bind the sections to slots by applying to the generated lod sections the same material slot of the source lod sections by matching polygon group id:

int32 LodSectionIndex = 0;
for (const FPolygonGroupID LodPolygonGroupID : MeshDescriptions[LodIndex].PolygonGroups().GetElementIDs())
{
				bool bSectionInfoSet = false;
 
				int32 BaseLodSectionIndex = 0;
				for (const FPolygonGroupID BaseLodPolygonGroupID : MeshDescriptions[BaseReduceLodIndex].PolygonGroups().GetElementIDs())
				{
					if (LodPolygonGroupID == BaseLodPolygonGroupID)
					{
						//Copy the base sectionInfoMap
						FMeshSectionInfo SectionInfo = StaticMesh->GetSectionInfoMap().Get(BaseReduceLodIndex, BaseLodSectionIndex);
						FMeshSectionInfo OriginalSectionInfo = StaticMesh->GetOriginalSectionInfoMap().Get(BaseReduceLodIndex, BaseLodSectionIndex);
						StaticMesh->GetSectionInfoMap().Set(LodIndex, LodSectionIndex, SectionInfo);
						StaticMesh->GetOriginalSectionInfoMap().Set(LodIndex, LodSectionIndex, OriginalSectionInfo);
						bSectionInfoSet = true;
						break;
					}
					BaseLodSectionIndex++;
				}
 
				ensureAlways(bSectionInfoSet);
				LodSectionIndex++;
}

So we had to be certain that reduction code does not mess you those.

In QuadricMeshReduction.cpp we changed the “Fill the PolygonGroups from the InMesh” loop to be the only code to manage polygon group ids:

//Fill the PolygonGroups from the InMesh
for (const FPolygonGroupID PolygonGroupID : InMesh.PolygonGroups().GetElementIDs())
{
	OutReducedMesh.CreatePolygonGroupWithID(PolygonGroupID);
	OutPolygonGroupMaterialNames[PolygonGroupID] = InPolygonGroupMaterialNames[PolygonGroupID];
 
	{
		// Copy all attributes from the base polygon group to the new polygon group
		InMesh.PolygonGroupAttributes().ForEach(
			[&OutReducedMesh, PolygonGroupID](const FName Name, const auto ArrayRef)
			{
				for (int32 Index = 0; Index < ArrayRef.GetNumChannels(); ++Index)
				{
					// Only copy shared attribute values, since input mesh description can differ from output mesh description
					const auto& Value = ArrayRef.Get(PolygonGroupID, Index);
					if (OutReducedMesh.PolygonGroupAttributes().HasAttribute(Name))
					{
						OutReducedMesh.PolygonGroupAttributes().SetAttribute(PolygonGroupID, Name, Index, Value);
					}
}
			}
		);
	}
}

so the following “// material index” if/else block is much simpler:

FPolygonGroupID MaterialPolygonGroupID = FPolygonGroupID(MaterialIndexes[TriangleIndex]); this works because MaterialIndexes are not really indices, but just polygon group ids casted to int (in the previous “MaterialIndexes.Add( PolygonGroupID.GetValue() );”).

This seems to fix all our problems. This works on static meshes without proper impoterted material names too.

I will create the pull request as soon as possible.

Just an additional warning in the mean time: the same bugs are in the skeletal mesh lod material management, even if the code seems pretty different.

Here you go:

https://github.com/EpicGames/UnrealEngine/pull/13981

Thanks