using System.Collections; using System.Collections.Generic; using UnityEngine; using NavMeshSurface = UnityEngine.AI.NavMeshSurface; using KairoEngine.Core; using KairoEngine.Chunks; using KairoEngine.NoiseUtilities; using Utilities = KairoEngine.Utility.Utilities; using Sirenix.OdinInspector; using UniRx; namespace KairoEngine.TerrainEngine { [HideMonoScript] public class ChunkTerrainGenerator : MonoBehaviour { [BoxGroup("Configurations")] public Vector3 voxelSize = new Vector3(1f, 1f, 1f); [BoxGroup("Configurations")] public Vector3Int chunkSize = new Vector3Int(16, 16, 16); [BoxGroup("Configurations")] public Vector3Int chunkLimit = new Vector3Int(2, 1, 2); [BoxGroup("Configurations")] public StartConfig startConfiguration = StartConfig.DoNothing; [BoxGroup("Configurations"), ShowIf("@startConfiguration == StartConfig.WaitForEvent")] public string generateWorldEvent = "GenerateWorld"; [BoxGroup("Configurations")] public int dataProcessLimit = 4; [BoxGroup("Configurations")] public int parallelMeshJobs = 4; [HorizontalGroup("Configurations/NavMesh")] public bool processNavMesh = false; [HorizontalGroup("Configurations/NavMesh"), ShowIf("@processNavMesh"), HideLabel] public NavMeshSurface navMeshSurface; [HorizontalGroup("Configurations/OnChunkGeneratedEvent")] public bool triggerOnChunkGeneratedEvent = true; [HorizontalGroup("Configurations/OnChunkGeneratedEvent"), ShowIf("@triggerOnChunkGeneratedEvent"), HideLabel] public string onChunkGeneratedEvent; [HorizontalGroup("Configurations/OnTerrainGeneratedEvent")] public bool triggerOnTerrainGeneratedEvent = true; [HorizontalGroup("Configurations/OnTerrainGeneratedEvent"), ShowIf("@triggerOnTerrainGeneratedEvent"), HideLabel] public string onTerrainGeneratedEvent; [BoxGroup("Configurations")] public bool showChunkData = false; [BoxGroup("Configurations")] public bool showDebug = false; [InlineProperty, HideLabel, BoxGroup("Marching Cubes")] public MarchingCubes4 marchingCubes = new(); [BoxGroup("Map Data Generator")] public bool useRandomSeed = false; [BoxGroup("Map Data Generator"), InlineProperty, HideLabel, SerializeReference] public IMapDataGenerator mapDataGenerator; [ShowIf("@showChunkData"), BoxGroup("Chunk System"), InlineProperty, HideLabel] public ChunkSystem chunkSystem; public Dictionary terrainMeshes = new Dictionary(); [ShowInInspector] private List chunkDataBuffer = new List(); // Store list of chunks for populating with data [ShowInInspector] private List chunkMeshBuffer = new List(); // Store list of chunks for generating the mesh [ShowInInspector] private List chunkMeshProcessBuffer = new List(); // Store list of chunks that are waiting for their meshes private bool initialized = false; private bool buildingNavmesh = false; public class VoxelUpdateData { public Vector3 position; public BlockBase block; public VoxelUpdateData(Vector3 position, BlockBase block) { this.position = position; this.block = block; } } private Dictionary> chunkUpdates = new Dictionary>(); private void Start() { if(startConfiguration == StartConfig.GenerateOnStart) GenerateWorld(); else if(startConfiguration == StartConfig.WaitForEvent) GenericEvents.StartListening(generateWorldEvent, GenerateWorld); } private void Update() { marchingCubes.UpdateFinishedJobs(); } private void OnDisable() { if(startConfiguration == StartConfig.WaitForEvent) GenericEvents.StopListening(generateWorldEvent, GenerateWorld); } private void OnDestroy() { marchingCubes.OnDestroy(); } public void Initialize () { chunkSystem = new ChunkSystem(chunkSize,voxelSize, true); initialized = true; if(useRandomSeed) mapDataGenerator.SetSeed(Random.Range(0, 100000)); } public bool IsInitialized() => initialized; [Button("Generate World"), ButtonGroup("Buttons")] void GenerateWorld() { if(mapDataGenerator == null) { Debug.LogError("Missing planet template", this.gameObject); return; } DestroyTerrain(); Debug.Log("Starting chunk generation"); Initialize(); for (int x = 0; x < chunkLimit.x; x++) { for (int z = 0; z < chunkLimit.z; z++) { for (int y = 0; y < chunkLimit.y; y++) { Vector3 chunkGlobalPos = new Vector3(x * chunkSize.x * voxelSize.x, y * chunkSize.y * voxelSize.y, z * chunkSize.z * voxelSize.z); Vector3Int chunkPos = new Vector3Int(x * chunkSize.x, y * chunkSize.y, z * chunkSize.z); chunkSystem.CreateChunk(chunkGlobalPos, chunkPos, (Chunk c, Vector3Int pos) => new BlockBase(0, 0)); PopulateChunkData(chunkGlobalPos); } } } for (int x = 0; x < chunkLimit.x; x++) { for (int z = 0; z < chunkLimit.z; z++) { for (int y = 0; y < chunkLimit.y; y++) { //Vector3Int chunkPos = new Vector3Int(x * chunkSize.x, y * chunkSize.y, z * chunkSize.z); Vector3 chunkPos = new Vector3(x * chunkSize.x * voxelSize.x, y * chunkSize.y * voxelSize.y, z * chunkSize.z * voxelSize.z); GenerateTerrainMesh(chunkPos); //else if(marchingCubes2 != null) marchingCubes2.Generate(chunkSystem, chunkPos); } } } #if UNITY_EDITOR UpdateEditor (); #endif } public void GenerateChunk(Vector3 position) { position = chunkSystem.GetChunkGlobalPositionFromGlobalPosition(position); bool buffered = false; for (int i = 0; i < chunkDataBuffer.Count; i++) { if(chunkDataBuffer[i] == position) buffered = true; } for (int i = 0; i < chunkMeshBuffer.Count; i++) { if(chunkMeshBuffer[i] == position) buffered = true; } //if(chunkSystem.ChunkExists(position)) buffered = true; //bool meshGenerated = terrainMeshes.TryGetValue(position, out GameObject obj); //if(meshGenerated) buffered = true; if(buffered) return; chunkDataBuffer.Add(position); } void PopulateChunkData(Vector3 initialPos) { float width = (float)(chunkLimit.x * chunkSize.x * voxelSize.x); float height = (float)(chunkLimit.y * chunkSize.y * voxelSize.y); float length = (float)(chunkLimit.z * chunkSize.z * voxelSize.z); for (int x = 0; x < chunkSize.x; x++) { for (int z = 0; z < chunkSize.z; z++) { for (int y = 0; y < chunkSize.y; y++) { // Get a terrain height using regular old Perlin noise. Vector3 pos = initialPos + new Vector3(x * voxelSize.x, y * voxelSize.y, z * voxelSize.z); // Old example noise: //float thisHeight = height * Mathf.PerlinNoise((float)pos.x / width * noiseMultiplier + 0.001f, (float)pos.z / length * noiseMultiplier + 0.001f); float elevation = mapDataGenerator.SamplePoint((float)pos.x / width, (float)pos.z / length); float thisHeight = height * elevation; uint code = mapDataGenerator.SamplePointColor((float)pos.x / width, (float)pos.z / length, elevation); BlockBase block = new BlockBase(code, (uint)Mathf.FloorToInt(thisHeight * 1000)); chunkSystem.SetBlock(pos, block); } } } chunkUpdates.TryGetValue(initialPos, out List updates); if(updates == null) return; for (int i = 0; i < updates.Count; i++) { chunkSystem.SetBlock(updates[i].position, updates[i].block); } } public void GenerateChunkData(Vector3 position) { position = chunkSystem.GetChunkGlobalPositionFromGlobalPosition(position); Vector3Int chunkPos = chunkSystem.GetChunkPositionFromGlobalPosition(position); if(!chunkSystem.ChunkExists(position)) { //Debug.Log($"Generating Terrain Data ({position} | {chunkPos})"); chunkSystem.CreateChunk(position, chunkPos, (Chunk c, Vector3Int pos) => new BlockBase(0, 0)); PopulateChunkData(position); } } public void GenerateTerrainMesh(Vector3 position, bool overwrite = false) { bool meshExists = terrainMeshes.TryGetValue(position, out GameObject obj); if(meshExists && !overwrite && obj != null) return; if(!overwrite) terrainMeshes.Add(position, null); if(marchingCubes != null) { chunkMeshProcessBuffer.Add(position); marchingCubes.Generate(chunkSystem, this, position); } } public void UpdateGenerator() { if(marchingCubes.GetJobCount() == 0) ProcessChunkDataBuffer(); ProcessChunkMeshBuffer(); } public void UpdateEditor () { Timer.ExecuteRealTime(50, () => { if(marchingCubes != null) { UpdateGenerator(); if(!marchingCubes.IsGeneratorDone()) UpdateEditor(); } }); } private void ProcessChunkDataBuffer() { int limit = chunkDataBuffer.Count < dataProcessLimit ? chunkDataBuffer.Count : dataProcessLimit; for (int i = 0; i < limit; i++) { Vector3 position = chunkDataBuffer[i]; List positions = new List{ new Vector3( 0, 0, 0), new Vector3( 1, 0, 0), new Vector3( 1, 0, -1), new Vector3( 0, 0, -1), new Vector3(-1, 0, -1), new Vector3(-1, 0, 0), new Vector3(-1, 0, 1), new Vector3( 0, 0, 1), new Vector3( 1, 0, 1), new Vector3( 0, 1, 0), new Vector3( 1, 1, 0), new Vector3( 1, 1, -1), new Vector3( 0, 1, -1), new Vector3(-1, 1, -1), new Vector3(-1, 1, 0), new Vector3(-1, 1, 1), new Vector3( 0, 1, 1), new Vector3( 1, 1, 1), new Vector3( 0, -1, 0), new Vector3( 1, -1, 0), new Vector3( 1, -1, -1), new Vector3( 0, -1, -1), new Vector3(-1, -1, -1), new Vector3(-1, -1, 0), new Vector3(-1, -1, 1), new Vector3( 0, -1, 1), new Vector3( 1, -1, 1), }; for (int a = 0; a < positions.Count; a++) { //position = new Vector3(position.x + chunkSize.x * voxelSize.x, position.y, position.z); Vector3 chunkPos = new Vector3( position.x + (positions[a].x * chunkSize.x * voxelSize.x), position.y + (positions[a].y * chunkSize.y * voxelSize.y), position.z + (positions[a].z * chunkSize.z * voxelSize.z) ); GenerateChunkData(chunkPos); } chunkMeshBuffer.Add(chunkDataBuffer[i]); } chunkDataBuffer.RemoveRange(0, limit); } private void ProcessChunkMeshBuffer() { marchingCubes.UpdateFinishedJobs(); int limit = marchingCubes.GetJobCount() < parallelMeshJobs ? parallelMeshJobs - marchingCubes.GetJobCount() : 0; limit = limit < chunkMeshBuffer.Count ? limit : chunkMeshBuffer.Count; for (int i = 0; i < limit; i++) { GenerateTerrainMesh(chunkMeshBuffer[i]); } chunkMeshBuffer.RemoveRange(0, limit); } public void DestroyChunk(Vector3 position) { DestroyChunkData(position); DestroyChunkMesh(position); } public void DestroyChunkData(Vector3 position) { chunkSystem.DestroyChunk(position); } public void DestroyChunkMesh(Vector3 position) { terrainMeshes.TryGetValue(position, out GameObject obj); if(obj != null) { Destroy(obj); #if UNITY_EDITOR DestroyImmediate(obj); #endif } terrainMeshes.Remove(position); } public void ClearChunkDataBuffer() => chunkDataBuffer.Clear(); [Button("Destroy"), ButtonGroup("Buttons")] public void DestroyTerrain() { var childTransforms = this.gameObject.transform.GetComponentsInChildren(); if(childTransforms.Length > 0) Debug.Log($"Destroying {childTransforms.Length} terrain objects"); for (int i = 0; i < childTransforms.Length; i++) { if(childTransforms[i] == this.transform) continue; #if UNITY_EDITOR DestroyImmediate(childTransforms[i].gameObject); #else Destroy(childTransforms[i].gameObject); #endif } terrainMeshes = new Dictionary(); chunkDataBuffer = new List(); chunkMeshBuffer = new List(); chunkSystem.chunks = new Dictionary>(); chunkSystem.chunkList = new List(); } public enum VoxelOperation { Add, Remove, Level } public void ChangeVoxels(Vector3 pos, uint code, VoxelOperation operation, int radius = 3) { int startHeigth = 0, endHeigth = 0, targetHeigth = 0, updatedVoxels = 0; switch (operation) { case VoxelOperation.Add: startHeigth = -1; endHeigth = radius; targetHeigth = (int)(((chunkSystem.GetBlock(pos).value/1000) + (radius * voxelSize.y)) * 1000); break; case VoxelOperation.Remove: startHeigth = -1 * (radius + 1); endHeigth = radius + 1; targetHeigth = (int)(Mathf.Clamp((chunkSystem.GetBlock(pos).value/1000) - (radius * voxelSize.y), 0, 10000) * 1000); break; case VoxelOperation.Level: startHeigth = -1 * radius; endHeigth = radius; targetHeigth = (int)chunkSystem.GetBlock(pos).value; break; default: return; } Vector3 chunkPosition = chunkSystem.GetChunkGlobalPositionFromGlobalPosition(pos); List points = Utilities.GetPointsInRadius(radius, true); List chunkPositions = new List(); for (int j = 0; j < points.Count; j++) { for (int y = startHeigth; y < endHeigth + 1; y++) { updatedVoxels += 1; Vector3 voxelPos = new Vector3(pos.x + points[j].x * voxelSize.x, pos.y + y * voxelSize.y, pos.z + points[j].y * voxelSize.z); BlockBase block = chunkSystem.GetBlock(voxelPos); block.code = code; block.value = (uint)targetHeigth; chunkSystem.SetBlock(voxelPos, block); // Get positions Vector3Int localVoxelPos = chunkSystem.GetVoxelFromGlobalPosition(voxelPos); Vector3 chunkPos = chunkSystem.GetChunkGlobalPositionFromGlobalPosition(voxelPos); // Save data update VoxelUpdateData updateData = new VoxelUpdateData(voxelPos, block); chunkUpdates.TryGetValue(chunkPos, out List chunkUpdate); if(chunkUpdate != null) { bool exists = false; for (int h = 0; h < chunkUpdate.Count; h++) { if(chunkUpdate[h].position.Equals(updateData.position)) { exists = true; chunkUpdate[h].block = updateData.block; } } if(!exists) chunkUpdate.Add(updateData); } else chunkUpdates.Add(chunkPos, new List{updateData}); // Check if changed voxels are in the edge of the chunk and if true add the neightboring chunk to the update list List chunks = new List(); chunks.Add(chunkPos); if(localVoxelPos.x == 0) chunks.Add(new Vector3(chunkPos.x - chunkSize.x * voxelSize.x, chunkPos.y, chunkPos.z)); if(localVoxelPos.x == 16) chunks.Add(new Vector3(chunkPos.x + chunkSize.x * voxelSize.x, chunkPos.y, chunkPos.z)); if(localVoxelPos.y == 0) chunks.Add(new Vector3(chunkPos.x, chunkPos.y - chunkSize.y * voxelSize.y, chunkPos.z)); if(localVoxelPos.y == 16) chunks.Add(new Vector3(chunkPos.x, chunkPos.y + chunkSize.y * voxelSize.y, chunkPos.z)); if(localVoxelPos.z == 0) chunks.Add(new Vector3(chunkPos.x, chunkPos.y, chunkPos.z - chunkSize.z * voxelSize.z)); if(localVoxelPos.z == 16) chunks.Add(new Vector3(chunkPos.x , chunkPos.y, chunkPos.z + chunkSize.z * voxelSize.z)); for (int i = 0; i < chunks.Count; i++) { bool duplicate = false; for (int k = 0; k < chunkPositions.Count; k++) { if(chunkPositions[k].Equals(chunks[i])) { duplicate = true; break; } } if(!duplicate) chunkPositions.Add(chunks[i]); } } } for (int c = 0; c < chunkPositions.Count; c++) GenerateTerrainMesh(chunkPositions[c], true); Debug.Log($"Changing voxel at {pos}, updated {updatedVoxels} voxels and {chunkPositions.Count} chunks"); } public void AddGeneratedTerrainMesh(Vector3 chunkPosition, GameObject meshObj) { chunkMeshProcessBuffer.Remove(chunkPosition); bool meshExists = terrainMeshes.TryGetValue(chunkPosition, out GameObject currentMesh); if(currentMesh != null) { //Debug.Log($"Updating chunk mesh at {chunkPosition}"); DestroyChunkMesh(chunkPosition); terrainMeshes[chunkPosition] = meshObj; } else { //Debug.Log($"Adding chunk mesh at {chunkPosition}"); //terrainMeshes.Add(chunkPosition, meshObj); terrainMeshes[chunkPosition] = meshObj; } if(triggerOnChunkGeneratedEvent) GenericEvents.Trigger(onChunkGeneratedEvent, chunkPosition); if(!buildingNavmesh && chunkMeshProcessBuffer.Count == 0 && chunkMeshBuffer.Count == 0 && chunkDataBuffer.Count == 0) { buildingNavmesh = true; Timer.ExecuteRealTime(50, () => { if(processNavMesh && navMeshSurface != null) { var cron = new Cronometer(); navMeshSurface.BuildNavMesh(); //navMeshSurface.UpdateNavMesh(navMeshSurface.navMeshData); cron.Stop(); if(showDebug) Debug.Log($"Generated Navmesh in {cron.Print()}"); buildingNavmesh = false; } if(triggerOnTerrainGeneratedEvent) GenericEvents.Trigger(onTerrainGeneratedEvent); }); } } [Button("Save"), ButtonGroup("Buttons")] public void SaveChunkUpdateData() { } public enum StartConfig { DoNothing, GenerateOnStart, WaitForEvent } } }