From 7c2491c1eea8a844ea4be39e106c7ea1a2439df1 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Tue, 31 Mar 2026 11:48:09 +0700 Subject: [PATCH] [Add] Batch optimization --- .../VoxelWorld/Runtime/VoxelWorldGenerator.cs | 292 +++++++++++++++++- 1 file changed, 277 insertions(+), 15 deletions(-) diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs index e1c68bc7..52ceaa25 100644 --- a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs @@ -42,21 +42,31 @@ namespace InfiniteWorld.VoxelWorld [Header("Runtime")] [SerializeField, Min(1)] private int maxAsyncChunkJobs = 2; [SerializeField, Min(1)] private int maxChunkBuildsPerFrame = 1; + [SerializeField, Min(1)] private int renderRegionSizeInChunks = 4; + [SerializeField, Min(1)] private int maxRegionBuildsPerFrame = 1; private readonly Dictionary chunks = new Dictionary(); + private readonly Dictionary regions = new Dictionary(); private readonly Queue completedBuilds = new Queue(); + private readonly Queue dirtyRegions = new Queue(); + private readonly HashSet queuedRegions = new HashSet(); private readonly object generationLock = new object(); private Transform chunkRoot; + private Transform regionRoot; private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue); private int activeGenerationJobs; + private int generationSession; private VoxelWorldAtlas atlas; private int atlasBiomeCount; + private bool regionRebuildLoopRunning; private void Awake() { + generationSession++; EnsureRuntimeData(); EnsureChunkRoot(); + EnsureRegionRoot(); TryResolveStreamTarget(); } @@ -64,6 +74,7 @@ namespace InfiniteWorld.VoxelWorld { EnsureRuntimeData(); EnsureChunkRoot(); + EnsureRegionRoot(); if (!TryResolveStreamTarget()) { return; @@ -83,7 +94,14 @@ namespace InfiniteWorld.VoxelWorld private void OnDisable() { + generationSession++; + lock (generationLock) + { + completedBuilds.Clear(); + } + CleanupChunks(); + CleanupRegions(); atlas?.Dispose(); atlas = null; } @@ -92,6 +110,8 @@ namespace InfiniteWorld.VoxelWorld { maxMountainHeight = Mathf.Max(1, maxMountainHeight); chunkSize = Mathf.Max(8, chunkSize); + renderRegionSizeInChunks = Mathf.Max(1, renderRegionSizeInChunks); + maxRegionBuildsPerFrame = Mathf.Max(1, maxRegionBuildsPerFrame); int configuredBiomeCount = CountConfiguredBiomes(); if (atlas != null && atlasBiomeCount == configuredBiomeCount) @@ -102,6 +122,7 @@ namespace InfiniteWorld.VoxelWorld atlas?.Dispose(); atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles); atlasBiomeCount = atlas.BiomeCount; + RefreshRegionMaterials(); } private void EnsureChunkRoot() @@ -123,6 +144,41 @@ namespace InfiniteWorld.VoxelWorld chunkRoot = root.transform; } + private void EnsureRegionRoot() + { + if (regionRoot != null) + { + return; + } + + Transform existing = transform.Find("VoxelRenderRegions"); + if (existing != null) + { + regionRoot = existing; + return; + } + + GameObject root = new GameObject("VoxelRenderRegions"); + root.transform.SetParent(transform, false); + regionRoot = root.transform; + } + + private void RefreshRegionMaterials() + { + if (atlas == null) + { + return; + } + + foreach (KeyValuePair pair in regions) + { + if (pair.Value.Renderer != null) + { + pair.Value.Renderer.sharedMaterial = atlas.Material; + } + } + } + private bool TryResolveStreamTarget() { if (streamTarget != null) @@ -164,7 +220,7 @@ namespace InfiniteWorld.VoxelWorld runtime.State = ChunkState.Generating; runtime.Version++; - GenerateChunkDataAsync(coord, runtime.Version).Forget(); + GenerateChunkDataAsync(coord, runtime.Version, generationSession).Forget(); } } @@ -187,7 +243,7 @@ namespace InfiniteWorld.VoxelWorld runtime.Version++; runtime.State = ChunkState.SyncBuilding; - ApplyBuildResult(GenerateChunkData(coord, runtime.Version)); + ApplyBuildResult(GenerateChunkData(coord, runtime.Version, generationSession)); RenderChunk(coord); RefreshNeighborBorders(coord); } @@ -214,13 +270,14 @@ namespace InfiniteWorld.VoxelWorld continue; } + MarkRegionDirty(coord); chunks.Remove(coord); runtime.Dispose(); RefreshNeighborBorders(coord); } } - private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version) + private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version, int session) { int margin = Mathf.Max(2, smoothingPasses + 1); int sampleSize = chunkSize + margin * 2; @@ -260,7 +317,7 @@ namespace InfiniteWorld.VoxelWorld } } - return new ChunkBuildResult(coord, heights, biomeIndices, version); + return new ChunkBuildResult(coord, heights, biomeIndices, version, session); } private bool[,] SmoothSampledMask(bool[,] source) @@ -380,10 +437,11 @@ namespace InfiniteWorld.VoxelWorld return; } - runtime.EnsureCreated(coord, chunkRoot, chunkSize, atlas.Material); + runtime.EnsureCreated(coord, chunkRoot, chunkSize); ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices); runtime.ApplyMeshes(meshBuild, chunkSize); runtime.State = ChunkState.Rendered; + MarkRegionDirty(coord); } private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices) @@ -826,11 +884,11 @@ namespace InfiniteWorld.VoxelWorld return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localZ); } - private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version) + private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version, int session) { try { - ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version)); + ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version, session)); lock (generationLock) { completedBuilds.Enqueue(result); @@ -893,8 +951,139 @@ namespace InfiniteWorld.VoxelWorld RenderChunk(coord); } + private void MarkRegionDirty(Vector2Int chunkCoord) + { + Vector2Int regionCoord = ChunkToRegion(chunkCoord); + if (!queuedRegions.Add(regionCoord)) + { + return; + } + + dirtyRegions.Enqueue(regionCoord); + EnsureRegionRebuildLoop(); + } + + private void EnsureRegionRebuildLoop() + { + if (regionRebuildLoopRunning) + { + return; + } + + regionRebuildLoopRunning = true; + ProcessDirtyRegionsAsync().Forget(); + } + + private async UniTaskVoid ProcessDirtyRegionsAsync() + { + try + { + while (dirtyRegions.Count > 0) + { + if (!this || !isActiveAndEnabled) + { + break; + } + + int buildsThisFrame = 0; + while (buildsThisFrame < maxRegionBuildsPerFrame && dirtyRegions.Count > 0) + { + Vector2Int regionCoord = dirtyRegions.Dequeue(); + queuedRegions.Remove(regionCoord); + RebuildRegion(regionCoord); + buildsThisFrame++; + } + + if (dirtyRegions.Count > 0) + { + await UniTask.Yield(PlayerLoopTiming.Update); + } + } + } + finally + { + regionRebuildLoopRunning = false; + if (dirtyRegions.Count > 0) + { + EnsureRegionRebuildLoop(); + } + } + } + + private void RebuildRegion(Vector2Int regionCoord) + { + if (atlas == null || regionRoot == null) + { + return; + } + + Vector2Int baseChunk = new Vector2Int(regionCoord.x * renderRegionSizeInChunks, regionCoord.y * renderRegionSizeInChunks); + List combineInstances = new List(renderRegionSizeInChunks * renderRegionSizeInChunks); + + for (int z = 0; z < renderRegionSizeInChunks; z++) + { + for (int x = 0; x < renderRegionSizeInChunks; x++) + { + Vector2Int chunkCoord = new Vector2Int(baseChunk.x + x, baseChunk.y + z); + if (!chunks.TryGetValue(chunkCoord, out ChunkRuntime runtime) || runtime.RenderMesh == null || runtime.RenderMesh.vertexCount == 0) + { + continue; + } + + combineInstances.Add(new CombineInstance + { + mesh = runtime.RenderMesh, + transform = Matrix4x4.Translate(new Vector3(x * chunkSize, 0f, z * chunkSize)) + }); + } + } + + if (combineInstances.Count == 0) + { + if (regions.TryGetValue(regionCoord, out RegionRuntime regionToRemove)) + { + regionToRemove.Dispose(); + regions.Remove(regionCoord); + } + + return; + } + + RegionRuntime region = GetOrCreateRegionRuntime(regionCoord); + region.EnsureCreated(regionCoord, regionRoot, chunkSize, renderRegionSizeInChunks, atlas.Material); + + Mesh combinedMesh = new Mesh { name = $"Region_{regionCoord.x}_{regionCoord.y}" }; + combinedMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; + combinedMesh.CombineMeshes(combineInstances.ToArray(), true, true, false); + combinedMesh.RecalculateBounds(); + region.ApplyMesh(combinedMesh); + } + + private Vector2Int ChunkToRegion(Vector2Int chunkCoord) + { + return new Vector2Int( + Mathf.FloorToInt(chunkCoord.x / (float)renderRegionSizeInChunks), + Mathf.FloorToInt(chunkCoord.y / (float)renderRegionSizeInChunks)); + } + + private RegionRuntime GetOrCreateRegionRuntime(Vector2Int regionCoord) + { + if (!regions.TryGetValue(regionCoord, out RegionRuntime region)) + { + region = new RegionRuntime(); + regions.Add(regionCoord, region); + } + + return region; + } + private bool ApplyBuildResult(ChunkBuildResult result) { + if (result.Session != generationSession) + { + return false; + } + if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime)) { return false; @@ -999,6 +1188,19 @@ namespace InfiniteWorld.VoxelWorld chunks.Clear(); } + private void CleanupRegions() + { + foreach (KeyValuePair pair in regions) + { + pair.Value.Dispose(); + } + + regions.Clear(); + dirtyRegions.Clear(); + queuedRegions.Clear(); + regionRebuildLoopRunning = false; + } + private int CountConfiguredBiomes() { int count = 0; @@ -1015,18 +1217,20 @@ namespace InfiniteWorld.VoxelWorld private readonly struct ChunkBuildResult { - public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version) + public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version, int session) { Coord = coord; Heights = heights; BiomeIndices = biomeIndices; Version = version; + Session = session; } public Vector2Int Coord { get; } public int[] Heights { get; } public byte[] BiomeIndices { get; } public int Version { get; } + public int Session { get; } } private readonly struct ChunkMeshBuild @@ -1080,8 +1284,6 @@ namespace InfiniteWorld.VoxelWorld private sealed class ChunkRuntime { public Transform Root; - public MeshFilter Filter; - public MeshRenderer Renderer; public MeshCollider MountainCollider; public BoxCollider GroundCollider; public Mesh RenderMesh; @@ -1094,21 +1296,18 @@ namespace InfiniteWorld.VoxelWorld public bool HasData => Heights != null && BiomeIndices != null; public bool IsRendered => State == ChunkState.Rendered && Root != null; - public void EnsureCreated(Vector2Int coord, Transform parent, int chunkSize, Material material) + public void EnsureCreated(Vector2Int coord, Transform parent, int chunkSize) { if (Root == null) { GameObject chunkObject = new GameObject($"VoxelChunk_{coord.x}_{coord.y}"); chunkObject.transform.SetParent(parent, false); Root = chunkObject.transform; - Filter = chunkObject.AddComponent(); - Renderer = chunkObject.AddComponent(); MountainCollider = chunkObject.AddComponent(); GroundCollider = chunkObject.AddComponent(); } Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize); - Renderer.sharedMaterial = material; GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize); GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f); } @@ -1127,7 +1326,6 @@ namespace InfiniteWorld.VoxelWorld RenderMesh = build.RenderMesh; ColliderMesh = build.ColliderMesh; - Filter.sharedMesh = RenderMesh; MountainCollider.sharedMesh = null; MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : null; } @@ -1168,6 +1366,70 @@ namespace InfiniteWorld.VoxelWorld } } + private sealed class RegionRuntime + { + public Transform Root; + public MeshFilter Filter; + public MeshRenderer Renderer; + public Mesh RenderMesh; + + public void EnsureCreated(Vector2Int regionCoord, Transform parent, int chunkSize, int regionSizeInChunks, Material material) + { + if (Root == null) + { + GameObject regionObject = new GameObject($"VoxelRegion_{regionCoord.x}_{regionCoord.y}"); + regionObject.transform.SetParent(parent, false); + Root = regionObject.transform; + Filter = regionObject.AddComponent(); + Renderer = regionObject.AddComponent(); + } + + Root.localPosition = new Vector3(regionCoord.x * regionSizeInChunks * chunkSize, 0f, regionCoord.y * regionSizeInChunks * chunkSize); + Renderer.sharedMaterial = material; + } + + public void ApplyMesh(Mesh mesh) + { + DestroyMesh(RenderMesh); + RenderMesh = mesh; + Filter.sharedMesh = RenderMesh; + } + + public void Dispose() + { + if (Root != null) + { + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(Root.gameObject); + } + else + { + UnityEngine.Object.DestroyImmediate(Root.gameObject); + } + } + + DestroyMesh(RenderMesh); + } + + private static void DestroyMesh(Mesh mesh) + { + if (mesh == null) + { + return; + } + + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(mesh); + } + else + { + UnityEngine.Object.DestroyImmediate(mesh); + } + } + } + private enum ChunkState { None,