Quick answer: Terrain tiles show seams and holes at boundaries because their neighbor connections are not set, their heightmap edges do not match, or their LOD levels are out of sync. Call Terrain.SetNeighbors() on every tile, stitch shared heightmap edges, and ensure uniform terrain settings across the grid.

Here is how to fix Unity terrain holes appearing at chunk boundaries. You split your world into a grid of terrain tiles, everything looks perfect in the editor with the camera close, but as soon as you pull back or move to the edge of a tile, ugly gaps and seams appear between chunks. Sometimes you see straight through to the skybox. Sometimes it is a flickering line that dances as the camera moves. The terrain data itself is correct — the problem is how Unity renders the boundaries between tiles when it does not know they are supposed to be connected.

The Symptom

You have a terrain split into multiple tiles, either manually or through Unity’s Terrain Group tooling. At runtime, visible gaps appear along the edges where two terrain tiles meet. The gaps are often one pixel wide and show whatever is behind the terrain — typically the skybox or a solid background color. The seams may appear only at certain camera distances or angles, disappearing when you zoom in and reappearing when you zoom out. This is because the gaps are caused by LOD transitions, where one tile reduces its mesh detail while its neighbor does not.

In the Scene view, the terrain may look fine because the editor often renders at maximum detail. The problem only manifests in the Game view or at runtime when the LOD system activates. If you enable wireframe rendering, you can see that the edge vertices of adjacent tiles do not line up — one tile has fewer vertices along the shared edge than the other, leaving triangles that do not connect.

What Causes This

Missing neighbor connections. Unity’s terrain system has a built-in mechanism for stitching adjacent tiles, but it is not automatic. Each Terrain component must be told which terrains are its neighbors via Terrain.SetNeighbors(). Without this call, Unity treats each tile as an isolated surface and makes no attempt to match vertices at shared edges. When the LOD system reduces vertex density on one tile, the adjacent tile’s full-resolution edge no longer aligns with anything, producing visible gaps.

Heightmap edge mismatch. Even with neighbors set, seams can appear if the heightmap values along shared edges are not identical. Each terrain tile stores its own heightmap data independently. If the last row of samples on Tile A does not exactly match the first row on Tile B, there is a height discontinuity at the boundary. At close range this might look like a subtle ridge. At longer range, when LOD simplification kicks in, it becomes an outright hole because the simplified mesh interpolates across the mismatched values differently on each side.

LOD settings divergence. The Pixel Error setting on the Terrain component controls how aggressively Unity reduces terrain detail at distance. If two adjacent tiles have different pixel error values, they transition to lower LODs at different camera distances. One tile might drop to half resolution while its neighbor stays at full resolution, and the edge vertex counts no longer match. The same issue occurs with Base Map Distance and Detail Resolution when those values differ between tiles.

Terrain Group misconfiguration. Unity’s TerrainGroup component is supposed to manage neighbor relationships and shared settings automatically. However, if tiles are added to the group after initial creation, or if the group’s tile size does not match the actual terrain dimensions, the automatic stitching silently fails. The group reports everything as connected, but the runtime behavior shows gaps.

The Fix

Step 1: Set neighbors explicitly for every tile. Do not rely on the editor or TerrainGroup to handle this. Write a script that runs at scene load and calls SetNeighbors() on each terrain tile with the correct references.

using UnityEngine;

public class TerrainStitcher : MonoBehaviour
{
    [SerializeField] private Terrain[,] grid;
    [SerializeField] private int gridWidth = 4;
    [SerializeField] private int gridHeight = 4;

    void Start()
    {
        // Collect all terrains and sort into a grid by position
        Terrain[] allTerrains = Terrain.activeTerrains;
        grid = new Terrain[gridWidth, gridHeight];

        foreach (Terrain t in allTerrains)
        {
            Vector3 pos = t.transform.position;
            int x = Mathf.RoundToInt(pos.x / t.terrainData.size.x);
            int z = Mathf.RoundToInt(pos.z / t.terrainData.size.z);
            if (x >= 0 && x < gridWidth && z >= 0 && z < gridHeight)
                grid[x, z] = t;
        }

        // Set neighbors: left, top, right, bottom
        for (int x = 0; x < gridWidth; x++)
        {
            for (int z = 0; z < gridHeight; z++)
            {
                Terrain left  = x > 0 ? grid[x - 1, z] : null;
                Terrain right = x < gridWidth - 1 ? grid[x + 1, z] : null;
                Terrain top   = z < gridHeight - 1 ? grid[x, z + 1] : null;
                Terrain bottom = z > 0 ? grid[x, z - 1] : null;

                grid[x, z].SetNeighbors(left, top, right, bottom);
                grid[x, z].Flush();
            }
        }

        Debug.Log($"Stitched {allTerrains.Length} terrain tiles");
    }
}

The Flush() call is critical. Without it, the neighbor data is stored but not applied to the current render state. Some developers skip Flush() and wonder why the stitching does not take effect until the camera moves far enough to trigger a full LOD recalculation.

Step 2: Stitch heightmap edges. After setting neighbors, ensure that the heightmap values along shared edges are identical. The easiest approach is to average the edge samples from both tiles and write the result back to each.

public static void StitchHeightmapEdge(Terrain a, Terrain b, bool horizontal)
{
    int res = a.terrainData.heightmapResolution;
    if (horizontal)
    {
        // Stitch right edge of A to left edge of B
        float[,] edgeA = a.terrainData.GetHeights(res - 1, 0, 1, res);
        float[,] edgeB = b.terrainData.GetHeights(0, 0, 1, res);

        for (int i = 0; i < res; i++)
        {
            float avg = (edgeA[i, 0] + edgeB[i, 0]) * 0.5f;
            edgeA[i, 0] = avg;
            edgeB[i, 0] = avg;
        }

        a.terrainData.SetHeights(res - 1, 0, edgeA);
        b.terrainData.SetHeights(0, 0, edgeB);
    }
    else
    {
        // Stitch top edge of A to bottom edge of B
        float[,] edgeA = a.terrainData.GetHeights(0, res - 1, res, 1);
        float[,] edgeB = b.terrainData.GetHeights(0, 0, res, 1);

        for (int i = 0; i < res; i++)
        {
            float avg = (edgeA[0, i] + edgeB[0, i]) * 0.5f;
            edgeA[0, i] = avg;
            edgeB[0, i] = avg;
        }

        a.terrainData.SetHeights(0, res - 1, edgeA);
        b.terrainData.SetHeights(0, 0, edgeB);
    }
}

Run this stitching pass once during level load or as a build-time preprocessing step. For procedurally generated terrain, stitch edges immediately after generating each tile’s heightmap data, before calling SetNeighbors().

Synchronize LOD Settings

Every terrain tile in your grid must share identical settings for the LOD system to produce matching vertex counts at shared edges. The critical properties are:

If you are using Unity’s TerrainGroup, set these values on the group component itself rather than on individual tiles. The group will propagate them to all children. If you are managing tiles manually, write an editor script that enforces uniform settings across all terrain assets.

Edge Cases and Runtime Terrain

If you generate terrain at runtime or stream tiles in and out, you need to re-run the neighbor setup every time a tile is loaded or unloaded. A common mistake is to set neighbors once at start and never update them as the player moves through the world and new tiles are instantiated. When a new tile loads adjacent to an existing one, call SetNeighbors() on both the new tile and its existing neighbors, then Flush() all affected tiles.

Another subtle issue: Unity’s terrain LOD is camera-distance-based. If you have multiple cameras (e.g., a minimap camera), each camera can trigger different LOD states. The stitching is only correct for the main camera. If your minimap camera is positioned far above the terrain, it may see lower-LOD versions that reveal seams the main camera does not. The workaround is to set the minimap camera’s terrain layer to use a simplified mesh that does not rely on the LOD system.

“Terrain stitching in Unity is not optional — it is a manual step that the engine quietly expects you to do. The API exists, it works well, but it will never call itself.”

Related Issues

If your terrain textures are blending incorrectly at tile boundaries, see Terrain Texture Blending Seams for splat map alignment techniques. If terrain holes are caused by physics collider mismatches rather than rendering, check Terrain Collider Not Matching Visual Mesh for collider bake timing issues.

Always call Terrain.Flush() after SetNeighbors — without it, stitching is deferred indefinitely.