Questions on Nanite Landscape Ray Tracing Proxy Design and Static Mesh Generation

Hey Epic Team,

We are currently investigating Nanite Landscape performance within Ray Tracing (RT) scenes and noticed that its behavior deviates from standard Nanite static meshes. We have a few questions regarding its implementation:

1. Dynamic vs. Static Ray Tracing Proxies Unlike typical static meshes, Nanite Landscape appears to be treated as dynamic. We noted that bFastBuild and bAllowUpdate are set to true in FLandscapeRayTracingImpl::FindOrCreateRayTracingState. Additionally, FLandscapeComponentSceneProxy::GetDynamicRayTracingInstances updates the proxy dynamically as the camera moves, leading to frequent BLAS updates. Given that landscape geometry is static, what was the design rationale behind making it dynamic for ray tracing?

2. BLAS Compaction and Memory Footprint Our analysis via D3D12.DumpRayTracingGeometries shows that Landscape BLAS is unable to be compacted (Compaction flag is 0), resulting in a substantial memory footprint. When we manually forced bAllowUpdate and bFastBuild to false, the memory usage dropped by approximately 50% due to successful compaction. In your view, is disabling these dynamic updates a viable path for optimizing memory, or are there hidden risks we should be aware of?

3. Nanite Landscape Data Storage Could you clarify how Nanite Landscape geometry is stored? When Nanite is enabled for a landscape, and then we hit the “Build” button in the editor, does the engine generate and store a static Nanite mesh, or is it still fundamentally a heightfield map that generates mesh data on the fly?

[Attachment Removed]

Hello,

Given that landscape geometry is static, what was the design rationale behind making it dynamic for ray tracing?

That assumption is a misconception, since the geometry is effectively static, but we need to update the LOD based on the viewer, which requires rebuilding the BLAS if you use hardware raytracing. Therefore, the primitives cannot be marked as static.

In your view, is disabling these dynamic updates a viable path for optimizing memory, or are there hidden risks we should be aware of?

There are two caveats to disabling updates and fast building:

  1. Every LOD change will require a full BLAS rebuild instead of a refit. Rebuilds are significantly more expensive than refits.
  2. Without bFastBuild, we build the BVH to PREFER_FAST_TRACE, which produces a higher-quality BVH but takes longer to build.

Coupled with the need to update the Landscape geometry when LOD changes occur, you will also start seeing artifacts as you move the camera.

Could you clarify how Nanite Landscape geometry is stored?

When you use a Nanite Landscape mesh, we build out a full Nanite representation that is stored on disk (see ULandscapeNaniteComponent::InitializeForLandscapeAsync). However, for HWRT, Nanite mesh data is not used. Instead, we keep a second classical Landscape with a triangle mesh + heightfield, which is fed into the BVH. This fact implies you will effectively have two representations of your landscape at runtime, with extra memory overhead. This caveat is called out in our docs for Nanite Landscapes: https://dev.epicgames.com/documentation/en\-us/unreal\-engine/using\-nanite\-with\-landscapes\-in\-unreal\-engine\#enable\-nanite\-on\-a\-landscape

I hope those answers help clear up your questions, but please let me know if you still need more details.

[Attachment Removed]

Hi Jing,

In the case of hardware raytracing for Landscape, you are going to have a heightfield map that is used to generate vertices dynamically, so you don’t have discrete vertex positions similar to a static mesh with fixed LODs. I didn’t mean to confuse it with the term LOD there. If you want to check the code itself, there is a snippet from LandscapeVertexFactory.ush:

float LODCalculated = CalcLOD(ComponentIndex, xy, ...);
float LodValue = floor(LODCalculated);        // integer LOD (e.g., 2)
float MorphAlpha = LODCalculated - LodValue;   // fractional part (e.g., 0.37)
 
// Sample heightmap at current LOD and next LOD
float Height = SampleHeightmap(SampleCoords, LodValue);
float HeightNextLOD = SampleHeightmap(SampleCoordsNextLOD, LodValue + 1);
 
// Smoothly blend between the two
LocalPosition = lerp(
    float3(InputPositionLODAdjusted, Height),
    float3(InputPositionNextLOD, HeightNextLOD),
    MorphAlpha
);

As for BLAS refit and rebuild, I would like to know how does the refit work for Landscape when it changes LOD?

The RT compute shader (RayTracingDynamicMesh.usf) calls GetVertexFactoryIntermediates(), which runs this same morph logic. It generates new vertex positions into a dynamic vertex buffer, then updates the BLAS. But there are two distinct cases:

1. Refit (same integer LOD, morph alpha changed):

When MorphAlpha changes but the integer LOD stays the same, the vertex count and topology don’t change only the positions shift. The code in RayTracingDynamicGeometryUpdateManager.cpp detects this:

bool bRefit = true;
 
// If vertex count changed, can't refit — must rebuild
if (Geometry.Initializer.TotalPrimitiveCount != UpdateParams.NumTriangles || bVertexCountChanges)
{
    // ...rebuild segment data...
    bRefit = false;
}

When bRefit stays true, the BLAS build mode is EAccelerationStructureBuildMode::Update. This is a DXR refit that updates the BVH bounding boxes to fit the new vertex positions without rebuilding the tree structure. This is fast.

2. Rebuild (integer LOD changed):

When the camera moves enough that the integer LOD changes, the vertex count changes because each LOD halves the grid: ((SubsectionSizeVerts / NumRayTracingSections) >> LODIndex) + 1. The index buffer also changes (SharedBuffers->ZeroOffsetIndexBuffers[CurrentLODIndex]). Since the topology is different, a refit is not possible, the code sets bRefit = false, and the BLAS is fully rebuilt via EAccelerationStructureBuildMode::Build. The GetDynamicRayTracingInstances code also releases the dynamic vertex buffer on LOD change (LandscapeRender.cpp):

if (bLODChanged)
{
    SectionRayTracingState.RayTracingDynamicVertexBuffer.Release();
}

Do you mean Landscape has LOD morphing so the BLAS can refit, or the Landscape has discrete LODs, the LOD N can refit to LOD N+1 or LOD N-1?

In short, no, the Landscape cannot refit from LOD N to LOD N+1 or LOD N-1. Those have different vertex counts and require full rebuilds. Refits only happen for morph-alpha changes within the same integer LOD level.

I hope that answers your questions, but let me know if anything is still unclear.

[Attachment Removed]

Glad to hear that! I’ll consider this case as resolved and close out the ticket. If you have any further questions, please do not hesitate to open a new ticket.

[Attachment Removed]

Thanks for your answers very much. I can understand the landscape better now, while I still have some questions:

You mentioned LOD update, is the LOD update of landscape different from other static meshes? Non-nanite static meshes’ LODs are prebuilt offline and streamed in as needed. Their BLAS is probably built just once upon streaming. I used to expect landscape to follow the same logic, may be it does not? Does landscape do LOD morphing instead of discrete LODs, so its BLAS must be dynamic?

As for BLAS refit and rebuild, I would like to know how does the refit work for landscape when it changes LOD? Do you mean landscape has LOD morphing so the BLAS can refit, or the landscape has discrete LODs, the LOD N can refit to LOD N+1 or LOD N-1?

The main point focuses on how landscape LODs work. Hope these questions make sense. Thanks again for your help!

[Attachment Removed]

Thank you so much, I got the perfect answers.

[Attachment Removed]