Quick answer: NavMeshSurface.BuildNavMesh() silently drops tiles when the voxelized triangle count exceeds the per-tile budget. Use a coarser voxel size, assign a simplified collision proxy via the source collector, tile the world with overrideTileSize, and cache baked tiles on disk so only dirty regions rebuild.
You have a 4 km² open-world level, your environment meshes total 12 million triangles, and when you hit Bake the NavMesh either produces a checkerboard of missing chunks or errors out with “Failed to allocate voxelization grid.” The agents walk fine in a small test scene, so the logic is right; the pipeline just cannot digest the full world in one pass. Here is how to get a complete, correct NavMesh on a dense environment without waiting 45 minutes for a bake.
Why the Bake Fails
Unity’s NavMesh system is built on Recast, which works in three stages: voxelization (rasterize all source meshes into a volumetric grid), region generation (flood-fill walkable cells into regions), and polygonization (convert regions into a navigation mesh). Each stage has hard memory and count limits. The two failure modes you will actually hit are:
- Voxel grid too large. At a default voxel size of 0.1667 units, a 4000 × 4000 × 200 area is ~57 billion voxels. Even 24-bit flags per voxel is hundreds of gigabytes. Unity will either OOM or silently clamp.
- Tile triangle overflow. Each NavMesh tile has a maximum triangle count (65,536 by default). Tiles that exceed this limit are dropped entirely — not clipped — which is why you see rectangular holes in your NavMesh.
Both problems get worse with high-poly rendered meshes because Recast rasterizes whatever collision or render geometry you feed it. A foliage card that is 8 triangles but appears a thousand times is a million triangles to the voxelizer.
Step 1: Use a Simplified Source Mesh
The single largest win is to feed the NavMesh bake a proxy mesh rather than the rendered environment. Create a version of your level geometry with no foliage cards, no decals, no interior detail — just floors, walls, and static props. Assign it to a dedicated layer called NavMeshSource, then set your NavMeshSurface to collect only that layer.
using UnityEngine.AI;
public class NavMeshBakeController : MonoBehaviour {
public NavMeshSurface surface;
public void Bake() {
surface.layerMask = LayerMask.GetMask("NavMeshSource");
surface.useGeometry = NavMeshCollectGeometry.PhysicsColliders;
surface.collectObjects = CollectObjects.Volume;
surface.BuildNavMesh();
}
}
Using PhysicsColliders as the geometry source is the fastest path: you almost certainly already have simplified colliders on your environment props, and they make excellent NavMesh input. Avoid RenderMeshes unless you really mean it — a UV-unwrapped LOD0 mesh is not what you want Recast to chew on.
Step 2: Tile the World Explicitly
For anything larger than a single room, enable tile-based baking on the NavMeshSurface. In the inspector, expand Advanced, check Override Tile Size, and set it to 64 or 128 voxels. A smaller tile size means more tiles and slightly more overhead per tile, but each individual tile stays well under the triangle budget.
// Runtime-configure tile size before BuildNavMesh
var settings = NavMesh.GetSettingsByIndex(0);
settings.overrideTileSize = true;
settings.tileSize = 128;
settings.overrideVoxelSize = true;
settings.voxelSize = settings.agentRadius / 3f;
var bounds = new Bounds(Vector3.zero, new Vector3(4000, 200, 4000));
var sources = new List<NavMeshBuildSource>();
NavMeshBuilder.CollectSources(bounds, navMeshLayerMask,
NavMeshCollectGeometry.PhysicsColliders, 0, new List<NavMeshBuildMarkup>(), sources);
var data = NavMeshBuilder.BuildNavMeshData(settings, sources, bounds,
Vector3.zero, Quaternion.identity);
NavMesh.AddNavMeshData(data);
The voxelSize = agentRadius / 3 rule keeps detail fine enough to capture agent-traversable edges without ballooning grid memory. If your agentRadius is 0.5, voxelSize is ~0.17 and a 128-voxel tile is ~21 world units on a side, which is a reasonable chunk for most open worlds.
Step 3: Cache Tiles and Rebuild Incrementally
A 4 km² bake should never have to run from scratch during a design iteration. Serialize each NavMeshData to a .asset file keyed by tile coordinate. At runtime, load only tiles within a radius of the player; when a piece of geometry changes, mark the overlapping tiles dirty and rebuild only those.
public IEnumerator RebuildTile(Vector2Int coord) {
var bounds = TileBounds(coord);
var sources = new List<NavMeshBuildSource>();
NavMeshBuilder.CollectSources(bounds, sourceMask,
NavMeshCollectGeometry.PhysicsColliders, 0,
new List<NavMeshBuildMarkup>(), sources);
var op = NavMeshBuilder.UpdateNavMeshDataAsync(
loadedTiles[coord], settings, sources, bounds);
yield return op;
SaveTileAsset(coord, loadedTiles[coord]);
}
UpdateNavMeshDataAsync runs the voxelization on a worker thread, so a tile rebuild during gameplay does not stall the main thread for more than a frame of bookkeeping.
Step 4: Filter Out Noise Geometry
Even with tiled baking, a single high-poly prop can blow out one tile. Use NavMeshModifier with Ignore From Build on decorative meshes like foliage, rubble piles, or thin fences you want agents to path around via their colliders rather than the mesh itself. For props where you want agents to treat the bounds as blocking, add a NavMeshModifierVolume with area type Not Walkable — it is a single volume instead of thousands of triangles.
“If your bake is slow or incomplete, the answer is almost never a setting inside the bake — it is the geometry you are feeding it.”
Verifying the Result
Enable Show NavMesh in the AI window and fly over the world. Missing rectangular chunks mean a tile still exceeded the triangle budget — check that tile’s source list with NavMeshBuilder.CollectSources and a debug gizmo. Thin strips of missing NavMesh at tile boundaries usually mean your voxel size does not divide evenly into the tile size, so round to a clean multiple.
Related Issues
If agents bake fine but fail to find paths across the world, see NavMesh Agents Stuck at Tile Seams. For performance spikes during runtime bakes, read Async NavMesh Bake Frame Hitches.
Simplified proxy mesh + explicit tileSize + on-disk tile cache = no more checkerboard NavMeshes.