[Question] World Partitioned Navigation Mesh while controlling Z subdivision

Hi,

We are using Unreal Engine 5.6.1 with:

  • WorldPartitionRuntimeHashSet,
  • RuntimePartitionLHGrid,
  • RuntimePartitionCellBoundsMethod = UseMinContentCellBounds.

​​

[Situation]

We expected that only the DataChunkActors overlapping with the Streaming Source loading range to be loaded.

However, NavMeshTiles that located far above or below the Landscape (e.g., in the sky or underground ) cause the Navigation tiles to be loaded across a much larger area than the Streaming Source range.

[Suspected Cause]

It seems that Z size of ChunkActorBounds is used as Z size for TileBounds.

  • Makes CellBounds much taller than GridSize,
  • ChunkActors is assigned to larger GridLevel cells with the same CellCoord,
  • As a result, 2–4× larger loading ranges than our intended Streaming Source range.

This leads to unnecessary loading of NavigationDataChunkActors and NavMesh tiles.

[Desired]

In UWorldPartitionNavigationDataBuilder::GenerateNavigationData, we want to adjust the logic so that:

  • ​All ActorBounds within the same XY region use a uniform GridSize,
  • spawn Max(Ceil((TilesBounds.Max.Z - TilesBounds.Min.Z) / GridSize), 1) NavigationDataChunkActors.

This would maintain consistent XY partition sizes while controlling Z subdivision.

[Questions]

1. Would this approach be considered reasonable or recommended?

2.Are there any plans to support uniform XY GridSize with multi Z-cell handling in future engine releases?

Thank you in advance.

Hello!

I do not believe this would work currently in the current setup for two reasons. Adding a tile at the same XY location searches if one currently exists and removing the existing tile if one is found. When removing tiles, it removes all tiles at the XY location without regard to a specific layer.

There are no plans or work in flight to currently add this to the WP navmesh builder. It may be possible to make work, but it would require some engine-level changes to support.

-James

You can try this out. When we were working on the original WP navmesh, I do not believe the 3D grid had been made. If you are using a static navmesh, I believe there should not be any issues with removing tiles as they are streamed out, but dynamic WP navmesh removes all layers of the tile at an XY when streaming out ANY layer of the tile. I also do not recommend Dynamic Modifiers Only mode as it had little-to-none testing, and there are a few known issues with it.

Hi,

To explain the situation where additional NavMeshTiles are being loaded at specific XY positions.

Even though the Streaming Source, Loading Range, and other related values are identical, this seems to be caused by the Z-axis Many NavMeshTiles.

When the following code is evaluated:

----------------------------

MaxLength = FMath::Max(NavigationDataChunkGridSize, TilesBounds.Max.Z - TilesBounds.Min.Z);

----------------------------

When the Z range becomes larger, MaxLength also increases accordingly, which results in a higher GridLevel value.

Because of this, more ActorSetInstance (ANavigationDataChunkActor) entries are added per CellCoord, and causing both ContentBounds and CellBounds to expand

This behavior is also visible in the WorldPartition tool and in the generated logs (Logs\WorldPartition\StreamingGeneration-%s%s.log)

bool UWorldPartitionNavigationDataBuilder::GenerateNavigationData(UWorldPartition* WorldPartition, const FBox& LoadedBounds, const FBox& GeneratingBounds) const
{
...
 
			FBox TilesBounds(EForceInit::ForceInit);
			DataChunkActor->CollectNavData(QueryBounds, TilesBounds);
 
			FBox ChunkActorBounds(FVector(QueryBounds.Min.X, QueryBounds.Min.Y, TilesBounds.Min.Z), FVector(QueryBounds.Max.X, QueryBounds.Max.Y, TilesBounds.Max.Z));
			ChunkActorBounds = ChunkActorBounds.ExpandBy(FVector(-1.f, -1.f, 1.f)); //reduce XY by 1cm to avoid precision issues causing potential overflow on neighboring cell, add 1cm in Z to have a minimum of volume.
			UE_LOG(LogWorldPartitionNavigationDataBuilder, VeryVerbose, TEXT("Setting ChunkActorBounds to %s"), *ChunkActorBounds.ToString());
			DataChunkActor->SetDataChunkActorBounds(ChunkActorBounds);
 
...
}
 
bool URuntimePartitionLHGrid::GenerateStreaming(const FGenerateStreamingParams& InParams, FGenerateStreamingResult& OutResult)
{
...
			const int32 GridLevel = FCellCoord::GetLevelForBox(ActorSetInstanceBounds, CellSize, FVector(Origin.X, Origin.Y, bIs2D ? 0 : Origin.Z));
			const FCellCoord CellCoord = FCellCoord::GetCellCoords(ActorSetInstanceBounds.GetCenter(), CellSize, GridLevel, FVector(Origin.X, Origin.Y, bIs2D ? 0 : Origin.Z));
			CellsActorSetInstances.FindOrAdd(CellCoord).Add(ActorSetInstance);
...
}
 
static inline int32 GetLevelForBox(const FBox& InBox, int32 InCellSize, const FVector& InOrigin)
{
...
    const FVector Extent = InBox.GetSize();
    const FVector::FReal MaxLength = Extent.GetMax();
    const int32 Level = FMath::CeilToInt32(FMath::Max<FVector::FReal>(FMath::Log2(MaxLength / InCellSize), 0));
 
...
}

[Image Removed]

Thank you in advance.