using UnityEngine; using System.Collections.Generic; using Unity.VisualScripting; public class EndlessTerrain : MonoBehaviour { const float viewerMoveThreshHoldForChunkUpdate = 25f; const float sqrViewerMoveThreshHoldForChunkUpdate = viewerMoveThreshHoldForChunkUpdate * viewerMoveThreshHoldForChunkUpdate; public LODInfo[] detailLevels; public static float maxViewDst; public Transform viewer; public Material mapMaterial; public static Vector2 viewerPosition; Vector2 viewerPositionOld; static MapGenerator mapGenerator; int chunkSize; int chunksVisibleInViewDst; Dictionary terrainChunkDictionnary = new Dictionary(); List terrainChunksVisibleLastUpdate = new List(); private void Start() { mapGenerator = FindFirstObjectByType(); maxViewDst = detailLevels[detailLevels.Length-1].visibleDstThreshold; chunkSize = MapGenerator.mapChunkSize - 1; chunksVisibleInViewDst = Mathf.RoundToInt(maxViewDst / chunkSize); UpdateVisibleChunks(); } private void Update() { viewerPosition = new(viewer.position.x, viewer.position.z); if ((viewerPositionOld - viewerPosition).sqrMagnitude > sqrViewerMoveThreshHoldForChunkUpdate) { viewerPositionOld = viewerPosition; UpdateVisibleChunks(); } } void UpdateVisibleChunks() { for (int i = 0; i < terrainChunksVisibleLastUpdate.Count; i++) { terrainChunksVisibleLastUpdate[i].SetVisible(false); } terrainChunksVisibleLastUpdate.Clear(); int currentChunkCoordX = Mathf.RoundToInt(viewerPosition.x / chunkSize); int currentChunkCoordY = Mathf.RoundToInt(viewerPosition.y / chunkSize); for (int yOffset = -chunksVisibleInViewDst; yOffset <=chunksVisibleInViewDst; yOffset++) { for (int xOffset = -chunksVisibleInViewDst; xOffset <= chunksVisibleInViewDst; xOffset++) { Vector2 viewedChunkCoord = new(currentChunkCoordX + xOffset, currentChunkCoordY + yOffset); if (terrainChunkDictionnary.ContainsKey(viewedChunkCoord)) { terrainChunkDictionnary[viewedChunkCoord].UpdateTerrainChunk(); if (terrainChunkDictionnary[viewedChunkCoord].IsVisible()) { terrainChunksVisibleLastUpdate.Add(terrainChunkDictionnary[viewedChunkCoord]); } } else { terrainChunkDictionnary.Add(viewedChunkCoord, new TerrainChunk(viewedChunkCoord, chunkSize, detailLevels, transform, mapMaterial)); } } } } public class TerrainChunk { GameObject meshObject; Vector2 position; Bounds bounds; MeshRenderer meshRenderer; MeshFilter meshFilter; LODInfo[] detailLevels; LODMesh[] lodMeshes; MapData mapData; bool mapDataReceived; int previousLODIndex = -1; 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(position.x, 0, position.y); meshObject = new GameObject("TerrainChunk"); meshRenderer = meshObject.AddComponent(); meshFilter = meshObject.AddComponent(); meshRenderer.material = material; meshObject.transform.position = positionV3; meshObject.transform.parent = parent; SetVisible(false); lodMeshes = new LODMesh[detailLevels.Length]; for (int i = 0; i < detailLevels.Length; i++) { lodMeshes[i] = new LODMesh(detailLevels[i].lod, UpdateTerrainChunk); } mapGenerator.RequestMapData(position, OnMapDataReceived); } void OnMapDataReceived(MapData mapData) { this.mapData = mapData; mapDataReceived = true; Texture2D texture = TextureGenerator.TextureFromColourMap(mapData.colourMap, MapGenerator.mapChunkSize, MapGenerator.mapChunkSize); meshRenderer.material.mainTexture = texture; UpdateTerrainChunk(); } public void UpdateTerrainChunk() { if (mapDataReceived) { float viewDstFromNearestEdge = Mathf.Sqrt(bounds.SqrDistance(viewerPosition)); bool visible = viewDstFromNearestEdge <= maxViewDst; if (visible) { int lodIndex = 0; for (int i = 0; i < detailLevels.Length - 1; i++) { if (viewDstFromNearestEdge > detailLevels[i].visibleDstThreshold) { lodIndex = i + 1; } else { break; } } if (lodIndex != previousLODIndex) { LODMesh lodMesh = lodMeshes[lodIndex]; if (lodMesh.hasMesh) { previousLODIndex = lodIndex; meshFilter.mesh = lodMesh.mesh; } else if (!lodMesh.hasRequestedMesh) { lodMesh.RequestMesh(mapData); } } } SetVisible(visible); } } public void SetVisible(bool visible) { meshObject.SetActive(visible); } public bool IsVisible() { return meshObject.activeSelf; } } class LODMesh { public Mesh mesh; public bool hasRequestedMesh; public bool hasMesh; int lod; System.Action updateCallback; public LODMesh(int lod, System.Action updateCallback) { this.lod = lod; this.updateCallback = updateCallback; } void OnMeshDataReceived(MeshData meshData) { mesh = meshData.CreateMesh(); hasMesh = true; updateCallback(); } public void RequestMesh(MapData mapData) { hasRequestedMesh = true; mapGenerator.RequestMeshData(mapData, lod, OnMeshDataReceived); } } [System.Serializable] public struct LODInfo { public int lod; public float visibleDstThreshold; } }