Overlay Material + Nanite

When attempting to set an overlay material on a mesh that has nanite enabled on it, the overlay material will *not* get rendered.

Is it possible to fix this so that if overlay material is set on the mesh when it has nanite enabled, it uses the nanite fallback mesh to render it in the non-nanite rendering pass?

This way, we could at least have the overlay material node work correctly as it is. Nanite does not support translucent, so I assume this is why this is not attempted to render at all but this means that i have to disable nanite on mesh in game mesh to see the overlay which makes no sense.

Repro steps:

Make a simple material that shows red color as output,

set it into overlay slot

if nanite on mesh, nothing will render, disable nanite on mesh, it will show red

[Attachment Removed]

Hello,

Thanks for providing this feedback, I’ve forwarded it to our team. Using the fallback mesh may be possible, though that would incur additional overhead for the LODs, unless you just use the simplest LOD available.

Have you already tried adding this support to verify it it would suit your needs? Something like:

Modify NaniteResources.cpp

#include "Engine/MaterialOverlayHelper.h"
...
 FSceneProxy::FSceneProxy(const FMaterialAudit& MaterialAudit, const FStaticMeshSceneProxyDesc& ProxyDesc, const TSharedPtr<FInstanceDataSceneProxy, ESPMode::ThreadSafe>& InInstanceDataSceneProxy)
: FSceneProxyBase(ProxyDesc)
...
#if NANITE_ENABLE_DEBUG_RENDERING
	{
		// Pre-allocate FallbackLODs. Dynamic resize is unsafe as the FFallbackLODInfo constructor queues up a rendering command with a reference to itself.
		FallbackLODs.Reserve(RenderData->LODResources.Num());
 
		for (int32 LODIndex = 0; LODIndex < RenderData->LODResources.Num(); LODIndex++)
		{
			const FStaticMeshLODResources& LOD = RenderData->LODResources[LODIndex];
 
			new (FallbackLODs) FFallbackLODInfo(ProxyDesc, LOD.VertexBuffers, LOD.Sections, RenderData->LODVertexFactories[LODIndex], LODIndex, ClampedMinLOD);
		}
	}
#endif
 
	// Initialize overlay material support
	OverlayMaterial = ProxyDesc.GetOverlayMaterial();
	OverlayMaterialMaxDrawDistance = ProxyDesc.GetOverlayMaterialMaxDrawDistance();
	ProxyDesc.GetMaterialSlotsOverlayMaterial(MaterialSlotsOverlayMaterial);
 
	// Build a single coarsest-LOD fallback for rendering overlay materials outside of Nanite's pipeline.
	if (OverlayMaterial != nullptr || MaterialSlotsOverlayMaterial.Num() > 0)
	{
		// Use the last (coarsest) available LOD
		OverlayFallbackLODIndex = RenderData->LODResources.Num() - 1;
		const FStaticMeshLODResources& LOD = RenderData->LODResources[OverlayFallbackLODIndex];
		OverlayFallbackLOD.Emplace(ProxyDesc, LOD.VertexBuffers, LOD.Sections, RenderData->LODVertexFactories[OverlayFallbackLODIndex], OverlayFallbackLODIndex, 0);
 
		// Merge overlay material relevance into CombinedMaterialRelevance so the renderer
		// knows this primitive participates in translucency/etc. passes the overlay needs.
		const EShaderPlatform OverlayShaderPlatform = GetScene().GetShaderPlatform();
		if (OverlayMaterial)
		{
			CombinedMaterialRelevance |= OverlayMaterial->GetRelevance_Concurrent(OverlayShaderPlatform);
		}
		for (const TObjectPtr<UMaterialInterface>& SlotOverlay : MaterialSlotsOverlayMaterial)
		{
			if (SlotOverlay)
			{
				CombinedMaterialRelevance |= SlotOverlay->GetRelevance_Concurrent(OverlayShaderPlatform);
			}
		}
	}
 
...
 
void FSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
	const FLightCacheInterface* LCI = &MeshInfo;
	DrawStaticElementsInternal(PDI, LCI);
 
	// Submit overlay material mesh batches using the coarsest LOD fallback.
	// Nanite renders the base mesh through its own GPU-driven pipeline, but overlay
	// materials need to go through the traditional static mesh draw path.
	if (!OverlayFallbackLOD.IsSet())
	{
		return;
	}
 
	const int32 LODIndex = OverlayFallbackLODIndex;
	const FStaticMeshLODResources& LOD = RenderData->LODResources[LODIndex];
	const FStaticMeshVertexFactories& VFs = RenderData->LODVertexFactories[LODIndex];
	const FFallbackLODInfo& ProxyLODInfo = OverlayFallbackLOD.GetValue();
	const ESceneDepthPriorityGroup PrimitiveDPG = GetStaticDepthPriorityGroup();
 
	for (int32 SectionIndex = 0; SectionIndex < LOD.Sections.Num(); SectionIndex++)
	{
		const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
 
		if (Section.NumTriangles == 0)
		{
			continue;
		}
 
		// Determine the overlay material for this section
		UMaterialInterface* SpecifiedOverlayMaterial = OverlayMaterial;
		if (UMaterialInterface* SectionOverlayMaterial = FMaterialOverlayHelper::GetOverlayMaterial(MaterialSlotsOverlayMaterial, Section.MaterialIndex))
		{
			SpecifiedOverlayMaterial = SectionOverlayMaterial;
		}
 
		if (SpecifiedOverlayMaterial == nullptr)
		{
			continue;
		}
 
		// Negative cull distance disables overlay rendering
		if (OverlayMaterialMaxDrawDistance < 0.0f)
		{
			continue;
		}
 
		FMeshBatch OverlayMeshBatch;
 
		const bool bWireframe = false;
		const bool bUseReversedIndices = false;
 
		SetMeshElementGeometrySource(Section, ProxyLODInfo.Sections[SectionIndex], LOD.IndexBuffer, LOD.AdditionalIndexBuffers, nullptr, bWireframe, bUseReversedIndices, OverlayMeshBatch);
 
		FMeshBatchElement& BatchElement = OverlayMeshBatch.Elements[0];
 
		// Set up vertex factory
		const ::FVertexFactory* VertexFactory = nullptr;
		if (ProxyLODInfo.OverrideColorVertexBuffer)
		{
			VertexFactory = &VFs.VertexFactoryOverrideColorVertexBuffer;
			BatchElement.VertexFactoryUserData = ProxyLODInfo.OverrideColorVFUniformBuffer.GetReference();
		}
		else
		{
			VertexFactory = &VFs.VertexFactory;
			BatchElement.VertexFactoryUserData = VFs.VertexFactory.GetUniformBuffer();
		}
 
		if (BatchElement.NumPrimitives == 0)
		{
			continue;
		}
 
		BatchElement.MinVertexIndex = Section.MinVertexIndex;
		BatchElement.MaxVertexIndex = Section.MaxVertexIndex;
 
		OverlayMeshBatch.LODIndex = LODIndex;
		OverlayMeshBatch.VertexFactory = VertexFactory;
		OverlayMeshBatch.LCI = LCI;
		OverlayMeshBatch.ReverseCulling = IsReversedCullingNeeded(bUseReversedIndices);
		OverlayMeshBatch.DepthPriorityGroup = (ESceneDepthPriorityGroup)PrimitiveDPG;
		OverlayMeshBatch.MaterialRenderProxy = SpecifiedOverlayMaterial->GetRenderProxy();
 
		// Mark as overlay
		OverlayMeshBatch.bOverlayMaterial = true;
		OverlayMeshBatch.CastShadow = false;
		OverlayMeshBatch.bSelectable = false;
		// Offset MeshIdInPrimitive so overlay renders on top of base mesh
		OverlayMeshBatch.MeshIdInPrimitive += LOD.Sections.Num();
 
		// Use OverlayMaterialMaxDrawDistance as the screen size for distance culling
		float OverlayMeshScreenSize = OverlayMaterialMaxDrawDistance;
		PDI->DrawMesh(OverlayMeshBatch, OverlayMeshScreenSize);
	}
}
 

and in NaniteSceneProxy.h

class FSceneProxy : public FSceneProxyBase
{
public:
...
protected:
	/** Overlay material for rendering on top of the Nanite mesh using a non-Nanite fallback mesh. */
	UMaterialInterface* OverlayMaterial = nullptr;
	float OverlayMaterialMaxDrawDistance = 0.0f;
	TArray<TObjectPtr<UMaterialInterface>> MaterialSlotsOverlayMaterial;
 
...
 
	class FFallbackLODInfo
	{
...
	};
 
	/** Single coarsest-LOD fallback used for rendering overlay materials outside of Nanite's pipeline. */
	TOptional<FFallbackLODInfo> OverlayFallbackLOD;
	int32 OverlayFallbackLODIndex = INDEX_NONE;

[Attachment Removed]

Let me give this a try, because currently I’m creating a new static mesh / skeletal mesh / instanced static mesh at runtime and disabling nanite on it to get this to work including skeletal meshes (I had to fix the engine to support force disabling nanite on them for this case).

Althought I never checked what LOD was really being used in my case, but let me give this code a try and than I can report back

[Attachment Removed]

[mention removed]​ Thanks for sharing this code, I was able to give it a try just now and it looks it is actually working at least for static meshes, do you know what I need to do to make this also work for skeletal meshes? and would this also work instanced static meshes

[Attachment Removed]

I was able to fix the instanced static meshes by adding this:

if (InstanceDataSceneProxy.IsValid())

{

BatchElement.bForceInstanceCulling = true;

BatchElement.bFetchInstanceCountFromScene = true;

BatchElement.NumInstances = GetInstanceDataHeader().NumInstances;

const ERHIFeatureLevel::Type FeatureLevel = GetScene().GetFeatureLevel();

if (FeatureLevel > ERHIFeatureLevel::ES3_1)

{

const FMaterial& Material = OverlayMeshBatch.MaterialRenderProxy->GetIncompleteMaterialWithFallback(FeatureLevel);

BatchElement.bPreserveInstanceOrder = IsTranslucentBlendMode(Material);

}

}

but would be nice to know how to get this to work with skeletal meshes as well

[Attachment Removed]

Hello,

You might be able to modify FSkinnedSceneProxy::GetDynamicMeshElements( in NaniteResources.cpp to get the VertexFactory and add it to the Collector.

const FVertexFactory* VertexFactory = MeshObject->GetSkinVertexFactory(View, LODIndex, SectionIndex);You would also need to ensure you return true in FSkinnedSceneProxy::GetViewRelevance if there’s an overlay material to draw.

	const auto IsDynamic = [&]
	{
		if (bHasOverlayMaterial)
		{
			return true;
		}

In theory the implementation wouldn’t be much different from the static mesh implementation, but I haven’t had time yet to test it and see what dragons may exist.

[Attachment Removed]

Also for some odd reason, the static mesh code is working perfectly fine in editor, but in package I do not see anything rendered, any ideas?

[Attachment Removed]

Apologies for the delay, I haven’t had an opportunity to look into what might be going on in packaged builds, but the first thought is that the fallback mesh wasn’t loaded or was stripped during packaging.

[Attachment Removed]

I’m a bit doubtful it got stripped since my *fall*back solution of duplicating the mesh component + setting the original mesh and disabling naninte on it still works in same package its as if something isnt being rendered either the overlay material somewhere down in the rendering pipeline..

[Attachment Removed]

I was able to test this locally and initially the overlay wasn’t appearing in PIE or standalone builds, but modifying SceneVisibility.cpp and changing FDrawCommandRelevancePacket::AddCommandsForMesh fixed the issue for me:

	---if (bIsNaniteMesh && Scene.PrimitivesAlwaysVisibleOffset != ~0u)
	+++if (bIsNaniteMesh && Scene.PrimitivesAlwaysVisibleOffset != ~0u && !StaticMeshRelevance.bOverlayMaterial)

I haven’t tested Nanite Skeletal mesh overlays.

Also, you may have noticed UE 5.8 includes experimental support for Nanite Translucency - so there may be an entirely different unexplored route of supporting overlays without using the fallback.

[Attachment Removed]

I will give that a try, but unfortunately we are shipping on 5.7 so I won’t have access to 5.8 for launch so need to try to get it to work like this for now

[Attachment Removed]

Ok that seems to have worked for the packaged builds now, thanks!

[Attachment Removed]

Glad to hear it works - how much of an impact is it having on memory/performance for you, and is it looking acceptable?

[Attachment Removed]

I’m not sure that I really have an answer on the perf / memory thing since I was previously duplicating a mesh component and setting th mesh on it and force disabling nanite so in this case it’s pretty much the same thing. Only change I made was that I’m using lod0, because we don’t have that many lods anyways because it’s just the fallback mesh, but it’s saving me from duplicating the mesh and keeping an entire separate component around which is great because we were having hitches when we would “ping/pulse/scan” in our game since I would create it at that time so for me it’s mostly fixing hitching than anything else

[Attachment Removed]

OK, thanks for the feedback, glad to hear it’s working for you! If it’s OK with you I’ll go ahead and close this issue.

[Attachment Removed]

*Before* you close it, any hope of skeletal mesh support for this kind of pattern?

[Attachment Removed]

Excellent, and thank you for the reminder to apply the fix for ISMs!

[Attachment Removed]

Apologies for the delay. I’ve attached a diff of a version that seems to support Nanite skeletal and static meshes OK in UE 5.7 based on my limited testing.

[Image Removed]Related post: [Content removed]

Hope this helps!

[Attachment Removed]

Thank you :slight_smile: I will try it out this week and report back

[Attachment Removed]

Just tried it and it’s working, THANK YOU so much [mention removed]​

(I still had to put my fix for getting instanced static meshes to work but that is no different than I posted above)

[Attachment Removed]