[Add] Batch optimization

This commit is contained in:
2026-03-31 11:48:09 +07:00
parent fa36c49583
commit 7c2491c1ee
@@ -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<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
private readonly Dictionary<Vector2Int, RegionRuntime> regions = new Dictionary<Vector2Int, RegionRuntime>();
private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
private readonly Queue<Vector2Int> dirtyRegions = new Queue<Vector2Int>();
private readonly HashSet<Vector2Int> queuedRegions = new HashSet<Vector2Int>();
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<Vector2Int, RegionRuntime> 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<CombineInstance> combineInstances = new List<CombineInstance>(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<Vector2Int, RegionRuntime> 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<MeshFilter>();
Renderer = chunkObject.AddComponent<MeshRenderer>();
MountainCollider = chunkObject.AddComponent<MeshCollider>();
GroundCollider = chunkObject.AddComponent<BoxCollider>();
}
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<MeshFilter>();
Renderer = regionObject.AddComponent<MeshRenderer>();
}
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,