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
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;
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
[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
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.
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.
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..
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:
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.
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
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
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.