Unity: Procedurally generated chunks void issue

I’m currently working on a college project focused on procedural generation, particularly in creating an endless terrain simulation. I’ve managed to generate the terrain using a method I implemented. Initially, everything worked smoothly. However, upon relaunching the simulation, I encountered an issue where the chunks are now separated by void margins. Despite thoroughly inspecting every script and ensuring the correct initialization of chunk sizes as well as the noise generation script, I’m unable to pinpoint the source of the problem.

I’ve included my project scripts below for reference. I’ve added numerous comments to aid understanding, although they may appear redundant. I’d deeply appreciate any assistance or insights into resolving this issue. Thank you in advance for your help!

Noise.cs

using UnityEngine;
using System.Collections;

// A static class to generate noise maps using Perlin noise
public static class Noise {

    // Enumeration to specify the normalization mode
    public enum NormalizeMode { Local, Global };

    // Method to generate a 2D noise map
    public static float[,] GenerateNoiseMap(int mapWidth, int mapHeight, int seed, float scale, int octaves, float persistance, float lacunarity, Vector2 offset, NormalizeMode normalizeMode) {
        // Create a 2D array to store the noise map
        float[,] noiseMap = new float[mapWidth,mapHeight];

        // Initialize a pseudo-random number generator with the given seed
        System.Random prng = new System.Random (seed);
        // Array to store offsets for each octave
        Vector2[] octaveOffsets = new Vector2[octaves];

        // Variables to calculate maximum possible height for normalization
        float maxPossibleHeight = 0;
        float amplitude = 1;
        float frequency = 1;

        // Calculate octave offsets and maximum possible height
        for (int i = 0; i < octaves; i++) {
            float offsetX = prng.Next (-100000, 100000) + offset.x;
            float offsetY = prng.Next (-100000, 100000) - offset.y;
            octaveOffsets [i] = new Vector2 (offsetX, offsetY);

            maxPossibleHeight += amplitude;
            amplitude *= persistance;
        }

        // Ensure scale is not zero to prevent division by zero
        if (scale <= 0) {
            scale = 0.0001f;
        }

        // Variables to track local min and max noise heights
        float maxLocalNoiseHeight = float.MinValue;
        float minLocalNoiseHeight = float.MaxValue;

        // Calculate half-width and half-height for centering the map
        float halfWidth = mapWidth / 2f;
        float halfHeight = mapHeight / 2f;

        // Generate noise values for each point in the map
        for (int y = 0; y < mapHeight; y++) {
            for (int x = 0; x < mapWidth; x++) {

                amplitude = 1;
                frequency = 1;
                float noiseHeight = 0;

                // Combine octaves to generate Perlin noise
                for (int i = 0; i < octaves; i++) {
                    float sampleX = (x - halfWidth + octaveOffsets[i].x) / scale * frequency;
                    float sampleY = (y - halfHeight + octaveOffsets[i].y) / scale * frequency;

                    float perlinValue = Mathf.PerlinNoise (sampleX, sampleY) * 2 - 1;
                    noiseHeight += perlinValue * amplitude;

                    amplitude *= persistance;
                    frequency *= lacunarity;
                }

                // Update local min and max noise heights
                if (noiseHeight > maxLocalNoiseHeight) {
                    maxLocalNoiseHeight = noiseHeight;
                } else if (noiseHeight < minLocalNoiseHeight) {
                    minLocalNoiseHeight = noiseHeight;
                }
                // Store the noise value in the map
                noiseMap [x, y] = noiseHeight;
            }
        }

        // Normalize the noise map based on the specified mode
        for (int y = 0; y < mapHeight; y++) {
            for (int x = 0; x < mapWidth; x++) {
                if (normalizeMode == NormalizeMode.Local) {
                    // Normalize locally between min and max noise heights
                    noiseMap [x, y] = Mathf.InverseLerp (minLocalNoiseHeight, maxLocalNoiseHeight, noiseMap [x, y]);
                } else {
                    // Normalize globally using max possible height
                    float normalizedHeight = (noiseMap [x, y] + 1) / (maxPossibleHeight / 0.9f);
                    noiseMap [x, y] = Mathf.Clamp(normalizedHeight, 0, int.MaxValue);
                }
            }
        }

        // Return the generated noise map
        return noiseMap;
    }

}

MeshGenerator.cs

using UnityEngine;
using System.Collections;

// A static class responsible for generating terrain meshes
public static class MeshGenerator {

    // Method to generate a terrain mesh based on a height map
    public static MeshData GenerateTerrainMesh(float[,] heightMap, float heightMultiplier, AnimationCurve _heightCurve, int levelOfDetail, bool useFlatShading) {
        // Create a new animation curve from the provided keys
        AnimationCurve heightCurve = new AnimationCurve (_heightCurve.keys);

        // Determine the mesh simplification increment based on the level of detail
        int meshSimplificationIncrement = (levelOfDetail == 0)?1:levelOfDetail * 2;

        // Calculate the bordered size of the height map
        int borderedSize = heightMap.GetLength (0);
        int meshSize = borderedSize - 2*meshSimplificationIncrement;
        int meshSizeUnsimplified = borderedSize - 2;

        // Calculate the coordinates of the top-left corner of the mesh
        float topLeftX = (meshSizeUnsimplified - 1) / -2f;
        float topLeftZ = (meshSizeUnsimplified - 1) / 2f;

        // Calculate the number of vertices per line in the mesh
        int verticesPerLine = (meshSize - 1) / meshSimplificationIncrement + 1;

        // Create a new MeshData object to store mesh information
        MeshData meshData = new MeshData (verticesPerLine,useFlatShading);

        // Create a mapping of vertex indices for border vertices
        int[,] vertexIndicesMap = new int[borderedSize,borderedSize];
        int meshVertexIndex = 0;
        int borderVertexIndex = -1;

        // Populate the vertex indices map
        for (int y = 0; y < borderedSize; y += meshSimplificationIncrement) {
            for (int x = 0; x < borderedSize; x += meshSimplificationIncrement) {
                bool isBorderVertex = y == 0 || y == borderedSize - 1 || x == 0 || x == borderedSize - 1;

                if (isBorderVertex) {
                    vertexIndicesMap [x, y] = borderVertexIndex;
                    borderVertexIndex--;
                } else {
                    vertexIndicesMap [x, y] = meshVertexIndex;
                    meshVertexIndex++;
                }
            }
        }

        // Generate vertices, UVs, and triangles for the mesh
        for (int y = 0; y < borderedSize; y += meshSimplificationIncrement) {
            for (int x = 0; x < borderedSize; x += meshSimplificationIncrement) {
                int vertexIndex = vertexIndicesMap [x, y];
                Vector2 percent = new Vector2 ((x-meshSimplificationIncrement) / (float)meshSize, (y-meshSimplificationIncrement) / (float)meshSize);
                float height = heightCurve.Evaluate (heightMap [x, y]) * heightMultiplier;
                Vector3 vertexPosition = new Vector3 (topLeftX + percent.x * meshSizeUnsimplified, height, topLeftZ - percent.y * meshSizeUnsimplified);

                meshData.AddVertex (vertexPosition, percent, vertexIndex);

                if (x < borderedSize - 1 && y < borderedSize - 1) {
                    int a = vertexIndicesMap [x, y];
                    int b = vertexIndicesMap [x + meshSimplificationIncrement, y];
                    int c = vertexIndicesMap [x, y + meshSimplificationIncrement];
                    int d = vertexIndicesMap [x + meshSimplificationIncrement, y + meshSimplificationIncrement];
                    meshData.AddTriangle (a,d,c);
                    meshData.AddTriangle (d,a,b);
                }

                vertexIndex++;
            }
        }

        // Process the mesh data (flat shading or normal baking)
        meshData.ProcessMesh ();

        // Return the generated mesh data
        return meshData;

    }
}

// A class representing mesh data (vertices, triangles, and UVs)
public class MeshData {
    Vector3[] vertices;
    int[] triangles;
    Vector2[] uvs;
    Vector3[] bakedNormals;

    Vector3[] borderVertices;
    int[] borderTriangles;

    int triangleIndex;
    int borderTriangleIndex;

    bool useFlatShading;

    // Constructor to initialize mesh data arrays
    public MeshData(int verticesPerLine, bool useFlatShading) {
        this.useFlatShading = useFlatShading;

        vertices = new Vector3[verticesPerLine * verticesPerLine];
        uvs = new Vector2[verticesPerLine * verticesPerLine];
        triangles = new int[(verticesPerLine-1)*(verticesPerLine-1)*6];

        borderVertices = new Vector3[verticesPerLine * 4 + 4];
        borderTriangles = new int[24 * verticesPerLine];
    }

    // Method to add a vertex to the mesh data
    public void AddVertex(Vector3 vertexPosition, Vector2 uv, int vertexIndex) {
        if (vertexIndex < 0) {
            borderVertices [-vertexIndex - 1] = vertexPosition;
        } else {
            vertices [vertexIndex] = vertexPosition;
            uvs [vertexIndex] = uv;
        }
    }

    // Method to add a triangle to the mesh data
    public void AddTriangle(int a, int b, int c) {
        if (a < 0 || b < 0 || c < 0) {
            borderTriangles [borderTriangleIndex] = a;
            borderTriangles [borderTriangleIndex + 1] = b;
            borderTriangles [borderTriangleIndex + 2] = c;
            borderTriangleIndex += 3;
        } else {
            triangles [triangleIndex] = a;
            triangles [triangleIndex + 1] = b;
            triangles [triangleIndex + 2] = c;
            triangleIndex += 3;
        }
    }

    // Method to calculate vertex normals
    Vector3[] CalculateNormals() {

        Vector3[] vertexNormals = new Vector3[vertices.Length];
        int triangleCount = triangles.Length / 3;
        for (int i = 0; i < triangleCount; i++) {
            int normalTriangleIndex = i * 3;
            int vertexIndexA = triangles [normalTriangleIndex];
            int vertexIndexB = triangles [normalTriangleIndex + 1];
            int vertexIndexC = triangles [normalTriangleIndex + 2];

            Vector3 triangleNormal = SurfaceNormalFromIndices (vertexIndexA, vertexIndexB, vertexIndexC);
            vertexNormals [vertexIndexA] += triangleNormal;
            vertexNormals [vertexIndexB] += triangleNormal;
            vertexNormals [vertexIndexC] += triangleNormal;
        }

        int borderTriangleCount = borderTriangles.Length / 3;
        for (int i = 0; i < borderTriangleCount; i++) {
            int normalTriangleIndex = i * 3;
            int vertexIndexA = borderTriangles [normalTriangleIndex];
            int vertexIndexB = borderTriangles [normalTriangleIndex + 1];
            int vertexIndexC = borderTriangles [normalTriangleIndex + 2];

            Vector3 triangleNormal = SurfaceNormalFromIndices (vertexIndexA, vertexIndexB, vertexIndexC);
            if (vertexIndexA >= 0) {
                vertexNormals [vertexIndexA] += triangleNormal;
            }
            if (vertexIndexB >= 0) {
                vertexNormals [vertexIndexB] += triangleNormal;
            }
            if (vertexIndexC >= 0) {
                vertexNormals [vertexIndexC] += triangleNormal;
            }
        }


        for (int i = 0; i < vertexNormals.Length; i++) {
            vertexNormals [i].Normalize ();
        }

        return vertexNormals;

    }

    Vector3 SurfaceNormalFromIndices(int indexA, int indexB, int indexC) {
        Vector3 pointA = (indexA < 0)?borderVertices[-indexA-1] : vertices [indexA];
        Vector3 pointB = (indexB < 0)?borderVertices[-indexB-1] : vertices [indexB];
        Vector3 pointC = (indexC < 0)?borderVertices[-indexC-1] : vertices [indexC];

        Vector3 sideAB = pointB - pointA;
        Vector3 sideAC = pointC - pointA;
        return Vector3.Cross (sideAB, sideAC).normalized;
    }

    // Method to process the mesh data (flat shading or normal baking)
    public void ProcessMesh() {
        if (useFlatShading) {
            FlatShading ();
        } else {
            BakeNormals ();
        }
    }

    // Method to bake normals
    void BakeNormals() {
        bakedNormals = CalculateNormals ();
    }

    // Method to perform flat shading
    void FlatShading() {
        Vector3[] flatShadedVertices = new Vector3[triangles.Length];
        Vector2[] flatShadedUvs = new Vector2[triangles.Length];

        for (int i = 0; i < triangles.Length; i++) {
            flatShadedVertices [i] = vertices [triangles [i]];
            flatShadedUvs [i] = uvs [triangles [i]];
            triangles [i] = i;
        }

        vertices = flatShadedVertices;
        uvs = flatShadedUvs;
    }

    // Method to create a mesh from the mesh data
    public Mesh CreateMesh() {
        Mesh mesh = new Mesh ();
        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.uv = uvs;
        if (useFlatShading) {
            mesh.RecalculateNormals ();
        } else {
            mesh.normals = bakedNormals;
        }
        return mesh;
    }

}

MapGenerator.cs

using UnityEngine;
using System.Collections;
using System;
using System.Threading;
using System.Collections.Generic;

public class MapGenerator : MonoBehaviour {

    // Enumeration for different draw modes
    public enum DrawMode {NoiseMap, ColourMap, Mesh};
    public DrawMode drawMode;

    // Normalization mode for noise generation
    public Noise.NormalizeMode normalizeMode;

    // public const int mapChunkSize = 95;

    // Boolean to enable/disable flat shading
    public bool useFlatShading;

    // Editor preview level of detail (LOD)
    [Range(0,6)]
    public int editorPreviewLOD;

    // Scale of the noise
    public float noiseScale;

    // Parameters for noise generation
    public int octaves;
    [Range(0,1)]
    public float persistance;
    public float lacunarity;

    // Seed for random number generation
    public int seed;
    
    // Offset for noise generation
    public Vector2 offset;

    // Multiplier for mesh height
    public float meshHeightMultiplier;

    // Curve for mapping noise values to mesh heights
    public AnimationCurve meshHeightCurve;

    // Boolean to auto-update the map
    public bool autoUpdate;

    // Array of terrain types
    public TerrainType[] regions;

// Queues for storing map and mesh thread information
    Queue<MapThreadInfo<MapData>> mapDataThreadInfoQueue = new Queue<MapThreadInfo<MapData>>();
    Queue<MapThreadInfo<MeshData>> meshDataThreadInfoQueue = new Queue<MapThreadInfo<MeshData>>();

    // Property to get map chunk size based on flat shading
    public static int mapChunkSize {
        get {
                // Property to get map chunk size based on flat shading
            if (Instance == null) {
                Instance = FindObjectOfType<MapGenerator> ();
            }
            // Return chunk size based on flat shading
            if (Instance.useFlatShading) {
                return 95;
            }
            return 239;
        }
    
    }

    // Method to draw the map in the editor
    public void DrawMapInEditor() {
        // Generate map data
        MapData mapData = GenerateMapData (Vector2.zero);

        // Get the MapDisplay instance
        MapDisplay display = FindObjectOfType<MapDisplay> ();
        // Draw based on draw mode
        if (drawMode == DrawMode.NoiseMap) {
            display.DrawTexture (TextureGenerator.TextureFromHeightMap (mapData.heightMap));
        } else if (drawMode == DrawMode.ColourMap) {
            display.DrawTexture (TextureGenerator.TextureFromColourMap (mapData.colourMap, mapChunkSize, mapChunkSize));
        } else if (drawMode == DrawMode.Mesh) {
            display.DrawMesh (MeshGenerator.GenerateTerrainMesh (mapData.heightMap, meshHeightMultiplier, meshHeightCurve, editorPreviewLOD, useFlatShading), TextureGenerator.TextureFromColourMap (mapData.colourMap, mapChunkSize, mapChunkSize));
        }
    }

    // Method to request map data
    public void RequestMapData(Vector2 centre, Action<MapData> callback) {
        // Start a new thread for map data generation
        ThreadStart threadStart = delegate {
            MapDataThread (centre, callback);
        };

        new Thread (threadStart).Start ();
    }

    // Method for map data thread
    void MapDataThread(Vector2 centre, Action<MapData> callback) {
        // Generate map data
        MapData mapData = GenerateMapData (centre);
        // Enqueue map data thread info
        lock (mapDataThreadInfoQueue) {
            mapDataThreadInfoQueue.Enqueue (new MapThreadInfo<MapData> (callback, mapData));
        }
    }

    // Method to request mesh data
    public void RequestMeshData(MapData mapData, int lod, Action<MeshData> callback) {
        // Start a new thread for mesh data generation
        ThreadStart threadStart = delegate {
            MeshDataThread (mapData, lod, callback);
        };

        new Thread (threadStart).Start ();
    }

    // Method for mesh data thread
    void MeshDataThread(MapData mapData, int lod, Action<MeshData> callback) {
        // Generate mesh data
        MeshData meshData = MeshGenerator.GenerateTerrainMesh (mapData.heightMap, meshHeightMultiplier, meshHeightCurve, lod, useFlatShading);
        // Enqueue mesh data thread info
        lock (meshDataThreadInfoQueue) {
            meshDataThreadInfoQueue.Enqueue (new MapThreadInfo<MeshData> (callback, meshData));
        }
    }

    // Update method to process thread info queues
    void Update() {
        if (mapDataThreadInfoQueue.Count > 0) {
            for (int i = 0; i < mapDataThreadInfoQueue.Count; i++) {
                MapThreadInfo<MapData> threadInfo = mapDataThreadInfoQueue.Dequeue ();
                threadInfo.callback (threadInfo.parameter);
            }
        }

        if (meshDataThreadInfoQueue.Count > 0) {
            for (int i = 0; i < meshDataThreadInfoQueue.Count; i++) {
                MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue ();
                threadInfo.callback (threadInfo.parameter);
            }
        }
    }

    // Method to generate map data
    MapData GenerateMapData(Vector2 centre) {
        // Generate noise map
        float[,] noiseMap = Noise.GenerateNoiseMap (mapChunkSize, mapChunkSize, seed, noiseScale, octaves, persistance, lacunarity, centre + offset, normalizeMode);

        // Generate color map
        Color[] colourMap = new Color[mapChunkSize * mapChunkSize];
        for (int y = 0; y < mapChunkSize; y++) {
            for (int x = 0; x < mapChunkSize; x++) {
                float currentHeight = noiseMap [x, y];
                for (int i = 0; i < regions.Length; i++) {
                    if (currentHeight >= regions [i].height) {
                        colourMap [y * mapChunkSize + x] = regions [i].colour;
                    } else {
                        break;
                    }
                }
            }
        }

        // Return the generated map data
        return new MapData (noiseMap, colourMap);
    }

    // Method to ensure valid values for parameters
    void OnValidate() {
        if (lacunarity < 1) {
            lacunarity = 1;
        }
        if (octaves < 0) {
            octaves = 0;
        }
    }

    // Method to set the seed for random number generation
    public void SetSeed(string seedValue, Vector2 centre)
    {
        // if (long.TryParse(seedValue, out long parsedSeed))
        // {
        //     seed = (int)parsedSeed;
        //  GenerateMapData();
        //  Update();
        // }
        seed = int.Parse(seedValue);
        GenerateMapData(centre);
        Update();
    }

    public static MapGenerator Instance { get; private set; } // Singletone property instance

    // Awake method to handle object instantiation
    void Awake()
    {
        // Check if an instance already exists
        if (Instance == null)
        {
            // Check if an instance already exists
            Instance = this;
            // Don't destroy this object when loading a new scene
            DontDestroyOnLoad(gameObject); // This ensures the object isn't destroyed when loading a new scene
        }
        else
        {
            // Destroy the duplicate instance
            Destroy(gameObject);
        }
    }
    
    // Structure to store thread information
    struct MapThreadInfo<T> {
        public readonly Action<T> callback;
        public readonly T parameter;

        // Constructor
        public MapThreadInfo (Action<T> callback, T parameter)
        {
            this.callback = callback;
            this.parameter = parameter;
        }
        
    }

}

// Serializable struct for defining terrain types
[System.Serializable]
public struct TerrainType {
    public string name;
    public float height;
    public Color colour;
}

// Structure to store map data
public struct MapData {
    public readonly float[,] heightMap;
    public readonly Color[] colourMap;

    // Constructor
    public MapData (float[,] heightMap, Color[] colourMap)
    {
        this.heightMap = heightMap;
        this.colourMap = colourMap;
    }
}

GenerateTerrain.cs (EndlessChunks)

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class GenerateTerrain : MonoBehaviour {

    // Scale factor for positioning
    const float scale = 5f;

    // Thresholds for updating chunks based on viewer movement
    const float viewerMoveThresholdForChunkUpdate = 25f;
    const float sqrViewerMoveThresholdForChunkUpdate = viewerMoveThresholdForChunkUpdate * viewerMoveThresholdForChunkUpdate;

    // Thresholds for updating chunks based on viewer movement// Array of detail levels for LOD (Level of Detail)
    public LODInfo[] detailLevels;

    // Maximum view distance
    public static float maxViewDst;

    // Reference to the viewer's transform
    public Transform viewer;

    // Material to apply to the terrain
    public Material mapMaterial;

    // Current position of the viewer
    public static Vector2 viewerPosition;

    // Previous position of the viewer
    Vector2 viewerPositionOld;

    // Reference to the MapGenerator script 
    static MapGenerator mapGenerator;

    // Size of each terrain chunk
    int chunkSize;

    // Number of visible chunks in view distance
    int chunksVisibleInViewDst;

    // Dictionary to store terrain chunks
    Dictionary<Vector2, TerrainChunk> terrainChunkDictionary = new Dictionary<Vector2, TerrainChunk>();
    
    // List of terrain chunks visible in the last update
    static List<TerrainChunk> terrainChunksVisibleLastUpdate = new List<TerrainChunk>();

    void Start() {
        // List of terrain chunks visible in the last update
        mapGenerator = FindObjectOfType<MapGenerator> ();

        // Set the maximum view distance to the threshold of the last detail level
        maxViewDst = detailLevels [detailLevels.Length - 1].visibleDstThreshold;
        // Calculate chunk size based on map chunk size
        chunkSize = MapGenerator.mapChunkSize - 1;
        // Calculate the number of chunks visible in view distance
        chunksVisibleInViewDst = Mathf.RoundToInt(maxViewDst / chunkSize);

        // Update visible chunks when starting
        UpdateVisibleChunks ();
    }

    void Update() {
        // Update viewer position
        viewerPosition = new Vector2 (viewer.position.x, viewer.position.z) / scale;

        // Check if viewer has moved enough to update visible chunks
        if ((viewerPositionOld - viewerPosition).sqrMagnitude > sqrViewerMoveThresholdForChunkUpdate) {
            viewerPositionOld = viewerPosition;
            UpdateVisibleChunks ();
        }
    }
        
    void UpdateVisibleChunks() {

        // Hide all terrain chunks from the last update
        for (int i = 0; i < terrainChunksVisibleLastUpdate.Count; i++) {
            terrainChunksVisibleLastUpdate [i].SetVisible (false);
        }
        terrainChunksVisibleLastUpdate.Clear ();

        // Calculate current chunk coordinates based on viewer position 
        int currentChunkCoordX = Mathf.RoundToInt (viewerPosition.x / chunkSize);
        int currentChunkCoordY = Mathf.RoundToInt (viewerPosition.y / chunkSize);

        // Loop through visible chunks in view distance
        for (int yOffset = -chunksVisibleInViewDst; yOffset <= chunksVisibleInViewDst; yOffset++) {
            for (int xOffset = -chunksVisibleInViewDst; xOffset <= chunksVisibleInViewDst; xOffset++) {
                Vector2 viewedChunkCoord = new Vector2 (currentChunkCoordX + xOffset, currentChunkCoordY + yOffset);

                // Check if the chunk is already generated (If the chunk is in the dictionary, update it)
                if (terrainChunkDictionary.ContainsKey (viewedChunkCoord)) {
                    terrainChunkDictionary [viewedChunkCoord].UpdateTerrainChunk ();
                } else {
                    // Generate new terrain chunk if not found
                    terrainChunkDictionary.Add (viewedChunkCoord, new TerrainChunk (viewedChunkCoord, chunkSize, detailLevels, transform, mapMaterial));
                }

            }
        }
    }

    // Class to represent a terrain chunk
    public class TerrainChunk {

        GameObject meshObject;
        Vector2 position;
        Bounds bounds;

        MeshRenderer meshRenderer;
        MeshFilter meshFilter;
        MeshCollider meshCollider;

        LODInfo[] detailLevels;
        LODMesh[] lodMeshes;

        MapData mapData;
        bool mapDataReceived;
        int previousLODIndex = -1;

        // Constructor for terrain chunk
        public TerrainChunk(Vector2 coord, int size, LODInfo[] detailLevels, Transform parent, Material material) {
            this.detailLevels = detailLevels;

            position = coord * size;
            bounds = new Bounds(position,Vector2.one * size);
            Vector3 positionV3 = new Vector3(position.x,0,position.y);

            // Create mesh object and components
            meshObject = new GameObject("Terrain Chunk");
            meshRenderer = meshObject.AddComponent<MeshRenderer>();
            meshFilter = meshObject.AddComponent<MeshFilter>();
            meshCollider = meshObject.AddComponent<MeshCollider>();
            meshRenderer.material = material;

            // Set mesh object's position and parent
            meshObject.transform.position = positionV3 * scale;
            meshObject.transform.parent = parent;
            meshObject.transform.localScale = Vector3.one * scale;
            SetVisible(false);

            // Initialize LOD meshes
            lodMeshes = new LODMesh[detailLevels.Length];
            for (int i = 0; i < detailLevels.Length; i++) {
                lodMeshes[i] = new LODMesh(detailLevels[i].lod, UpdateTerrainChunk);
            }

            // Request map data
            mapGenerator.RequestMapData(position,OnMapDataReceived);
        }

        // Callback for when map data is received
        void OnMapDataReceived(MapData mapData) {
            this.mapData = mapData;
            mapDataReceived = true;

            Texture2D texture = TextureGenerator.TextureFromColourMap (mapData.colourMap, MapGenerator.mapChunkSize, MapGenerator.mapChunkSize);
            meshRenderer.material.mainTexture = texture;

            UpdateTerrainChunk ();
        }

    
        // Method to update the terrain chunk based on LOD and visibility
        public void UpdateTerrainChunk() {
            if (mapDataReceived) {
                float viewerDstFromNearestEdge = Mathf.Sqrt (bounds.SqrDistance (viewerPosition));
                bool visible = viewerDstFromNearestEdge <= maxViewDst;

                if (visible) {
                    int lodIndex = 0;

                    for (int i = 0; i < detailLevels.Length - 1; i++) {
                        if (viewerDstFromNearestEdge > detailLevels [i].visibleDstThreshold) {
                            lodIndex = i + 1;
                        } else {
                            break;
                        }
                    }

                    if (lodIndex != previousLODIndex) {
                        LODMesh lodMesh = lodMeshes [lodIndex];
                        if (lodMesh.hasMesh) {
                            previousLODIndex = lodIndex;
                            meshFilter.mesh = lodMesh.mesh;
                            meshCollider.sharedMesh = lodMesh.mesh;
                        } else if (!lodMesh.hasRequestedMesh) {
                            lodMesh.RequestMesh (mapData);
                        }
                    }

                    terrainChunksVisibleLastUpdate.Add (this);
                }

                SetVisible (visible);
            }
        }

        // Method to set the visibility of the terrain chunk
        public void SetVisible(bool visible) {
            meshObject.SetActive (visible);
        }

        // Method to check if the terrain chunk is visible
        public bool IsVisible() {
            return meshObject.activeSelf;
        }

    }

    // Class for LOD mesh
    class LODMesh {

        public Mesh mesh;
        public bool hasRequestedMesh;
        public bool hasMesh;
        int lod;
        System.Action updateCallback;

        // Constructor for LODMesh
        public LODMesh(int lod, System.Action updateCallback) {
            this.lod = lod;
            this.updateCallback = updateCallback;
        }

        // Callback for when mesh data is received
        void OnMeshDataReceived(MeshData meshData) {
            mesh = meshData.CreateMesh ();
            hasMesh = true;

            updateCallback ();
        }

        // Method to request mesh data
        public void RequestMesh(MapData mapData) {
            hasRequestedMesh = true;
            mapGenerator.RequestMeshData (mapData, lod, OnMeshDataReceived);
        }

    }

    // Struct for LOD information
    [System.Serializable]
    public struct LODInfo {
        public int lod;
        public float visibleDstThreshold;
    }

}


Here are screenshots of the gap in question:
Without flatshading
With flatshading

When I ran into this problem, I searched everywhere for mistakes in how I set the chunk sizes and how I generated noise, but I couldn’t find anything.

New contributor

Rayan Derradji is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật