Capsule and Sphere Nav Modifiers Oversized and Incorrectly Bounded

When a UCapsuleComponent or USphereComponent has bDynamicObstacle set to true, the engine generates a cylinder-shaped navigation area modifier via UShapeComponent::GetNavigationData, which calls FCompositeNavModifier::CreateAreaModifiers(UPrimitiveComponent*, …). Three separate bugs in this code path cause the generated nav modifier to be significantly larger than the actual physics collision body.

Bug 1: SphylElem.Length Misused as CapsuleHalfHeight

File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp

Function: FCompositeNavModifier::CreateAreaModifiers (SphylElems loop)

FKSphylElem.Length stores the total length of the cylindrical portion of the capsule shape, equal to 2 * (CapsuleHalfHeight - CapsuleRadius). The code incorrectly treats this value as if it were CapsuleHalfHeight. As a result:

- The vertical offset applied to find the cylinder bottom is too large by (CapsuleRadius) units.

- The total height passed to FAreaNavModifier is SphylElem.Length * 2.0f, which equals 4 * (CapsuleHalfHeight - CapsuleRadius), instead of the correct 2 * CapsuleHalfHeight.

Example with CapsuleRadius = 80, CapsuleHalfHeight = 200:

SphylElem.Length = 2 * (200 - 80) = 240

Generated modifier: bottom offset = -240, height = 480

Correct values: bottom offset = -200, height = 400

The modifier is 20% taller than the capsule and positioned 40 units too low.

The second CreateAreaModifiers overload (FCollisionShape, FTransform) handles the equivalent capsule case correctly using CapsuleHalfHeight directly, which confirms this is unintentional.

Bug 2: Radius Scaled by max(X, Y) Instead of min(X, Y)

File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp

Function: FAreaNavModifier::FAreaNavModifier(float Radius, float Height, FTransform)

The cylinder FAreaNavModifier constructor scales the input radius by FMath::Max(Scale3D.X, Scale3D.Y), producing the circumscribed circle of the non-uniformly scaled shape. However:

- UCapsuleComponent::GetScaledCapsuleRadius() scales by min(X, Y) (inscribed circle)

- USphereComponent::GetScaledSphereRadius() scales by GetMinimumAxisScale() which is min(X, Y, Z) (largest inscribed sphere)

Both component APIs explicitly document using the minimum axis scale so that the physics collision remains conservative and fits inside the shape. The nav modifier using the maximum axis scale produces an obstacle radius that exceeds the actual physics collision radius for any component with non-uniform XY scaling.

UBoxComponent::GetScaledBoxExtent() multiplies each axis independently and its path through SetBox transforms all 8 corners by the full component transform, so the box shape is not affected by this bug.

Bug 3: Cylinder Bounding Box Centered at Bottom Instead of Midpoint

File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp

Function: FAreaNavModifier::FAreaNavModifier(float Radius, float Height, FTransform)

The FAreaNavModifier cylinder convention (matching dtMarkCylinderArea in Recast) stores Points[0] as the cylinder bottom and Height as the full height going upward. However the bounding box is built as: FBox::BuildAABB(Points[0], FVector(RadiusScaled, RadiusScaled, HeightScaled)).

FBox::BuildAABB takes a center and half-extents. Using the cylinder bottom as the center and the full height as the half-extent causes the bounding box to extend a full HeightScaled distance below the cylinder bottom, doubling the total Z extent.

For the HitCapsule example (CapsuleRadius = 80, CapsuleHalfHeight = 200) after applying Bug 1’s incorrect offset, the bounding box spans 960 cm in Z while the actual capsule is only 400 cm tall, a factor of 2.4x.

Even after fixing Bug 1, the bounding box with the correct 400 cm height would still extend 400 cm below the cylinder bottom unnecessarily, producing a total Z span of 800 cm instead of the correct 400 cm. This causes far more navmesh tiles to be marked as dirty than necessary whenever the actor moves, resulting in excessive navmesh rebuild cost.

The correct bounding box should use the cylinder midpoint as center and HeightScaled/2 as the Z half-extent.

重现步骤
1. Create a Blueprint Actor with a UCapsuleComponent, set CapsuleRadius = 80,

CapsuleHalfHeight = 200, bCanEverAffectNavigation = true, bDynamicObstacle = true,

AreaClassOverride = NavArea_Obstacle.

2. Place the actor in a level with a RecastNavMesh set to Dynamic generation.

3. Enable navigation debug visualization (nav show command or editor Show flags).

4. Observe the generated cylinder obstacle on the navmesh.

Expected: The obstacle cylinder matches the capsule dimensions (radius 80, height 400).

Observed: The cylinder has height 480 and is offset 40 units lower than the capsule.

The bounding box (visible in dirty area debug) spans approximately 960 units

in Z, 2.4x taller than the capsule.

For Bug 1, I believe this should be fixed in main. If you want to check our fix, it is at CL 48290527. I thought it should have been in 5.7, but perhaps it missed the branching of the release stream.

I don’t believe Bug 2 is an actual bug. Yes, it does use max instead of min for the scaled bounds, but that matches how the physics body is done. The nav modifier uses the AggGeom from the element which does use max. This also prevents generating walkable navmesh that would bump into the actual geometry. If the modifier was smaller than the actual geometry, AI would rub around the edge of the mesh and possibly get stuck as they attempt to continue pathing along navmesh that is inside the collision of the mesh.

Bug 3 is certainly a bug. I don’t see any safety justifications for it, but likely something that has gone unnoticed or at least not reported until now.

-James

Your approach for bug 2 is perfectly understandable. I was speaking with one of our nav programmers and I think we just had not bumped into a need for a better fitting, non-uniformly scaled cylinder or sphere at Epic. So it went unnoticed by us for a long time. Proper handling for non-uniformly scaled meshes is something we may look into adding, but it is not a priority for the team right now.

I believe the fix for bug 3 is exactly what you describe. It is not fixed by the other change for cylinders. I will look into getting something into main at the least, but it may not be until after Unreal Fest next week since I will be in attendance. I did create a bug report for that specific issue, and will set a reminder for myself to investigate it once I am back in the office.

-James

Thanks for the detailed response. A few follow-ups:

Bug 1: Confirmed. We are on 5.7, and the fix is not present in any 5.7.x — I verified against the 5.7.4-release tag, where the buggy SphylElems code is still intact. The fix commit (9fb0b550783e, “…not using the proper length for a FKSphylElem… cylinder constructor not handling non vertical cylinders”, synced CL 48151812) is only reachable from 5.8.0-preview-1, not from any 5.7 tag. So it did indeed miss the 5.7 release branch. For now we’ve applied the equivalent half-height fix (Length/2 + Radius) locally.

Bug 2: Understood and agreed — we confirmed that FKSphylElem::GetScaledRadius uses max(X, Y) and that this is what builds the actual simulated Chaos capsule, so the nav modifier is consistent with the simulated body, and shrinking it would risk AI pathing inside the collision.

the “matches the physics body” justification holds for capsules (FKSphylElem::GetScaledRadius uses max(X,Y)), but not for spheres. FKSphereElem::GetFinalScaled scales the radius by MinScaleAbs (min(|X|,|Y|,|Z|)), and USphereComponent::GetScaledSphereRadius also uses the minimum axis scale. So for a non-uniformly scaled sphere, the simulated body, the query collision, and the debug draw all use min, while the nav modifier alone uses max — making the obstacle strictly larger than the actual physics body. For spheres, min would be the consistent choice across all systems. (We don’t simulate these obstacles, so we’ve switched the cylinder modifier to min locally; for capsules we accept that this makes the modifier smaller than the simulated body, which is fine since nothing simulates them.)

Of course, a more fundamental solution is to prohibit the use of non-uniformly scaled spheres in our project.

Bug 3: Thanks for confirming. Since it only affects the dirtied-tile bounds (and thus navmesh rebuild cost), not the marked area itself, we’ve patched it locally by centering the AABB on the cylinder midpoint with Height/2 as the Z half-extent. Is a fix planned for main / a future release? It doesn’t appear to be addressed by the 5.8 cylinder-constructor change.