[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")] [Header("Runtime")]
[SerializeField, Min(1)] private int maxAsyncChunkJobs = 2; [SerializeField, Min(1)] private int maxAsyncChunkJobs = 2;
[SerializeField, Min(1)] private int maxChunkBuildsPerFrame = 1; [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, 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<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 readonly object generationLock = new object();
private Transform chunkRoot; private Transform chunkRoot;
private Transform regionRoot;
private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue); private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
private int activeGenerationJobs; private int activeGenerationJobs;
private int generationSession;
private VoxelWorldAtlas atlas; private VoxelWorldAtlas atlas;
private int atlasBiomeCount; private int atlasBiomeCount;
private bool regionRebuildLoopRunning;
private void Awake() private void Awake()
{ {
generationSession++;
EnsureRuntimeData(); EnsureRuntimeData();
EnsureChunkRoot(); EnsureChunkRoot();
EnsureRegionRoot();
TryResolveStreamTarget(); TryResolveStreamTarget();
} }
@@ -64,6 +74,7 @@ namespace InfiniteWorld.VoxelWorld
{ {
EnsureRuntimeData(); EnsureRuntimeData();
EnsureChunkRoot(); EnsureChunkRoot();
EnsureRegionRoot();
if (!TryResolveStreamTarget()) if (!TryResolveStreamTarget())
{ {
return; return;
@@ -83,7 +94,14 @@ namespace InfiniteWorld.VoxelWorld
private void OnDisable() private void OnDisable()
{ {
generationSession++;
lock (generationLock)
{
completedBuilds.Clear();
}
CleanupChunks(); CleanupChunks();
CleanupRegions();
atlas?.Dispose(); atlas?.Dispose();
atlas = null; atlas = null;
} }
@@ -92,6 +110,8 @@ namespace InfiniteWorld.VoxelWorld
{ {
maxMountainHeight = Mathf.Max(1, maxMountainHeight); maxMountainHeight = Mathf.Max(1, maxMountainHeight);
chunkSize = Mathf.Max(8, chunkSize); chunkSize = Mathf.Max(8, chunkSize);
renderRegionSizeInChunks = Mathf.Max(1, renderRegionSizeInChunks);
maxRegionBuildsPerFrame = Mathf.Max(1, maxRegionBuildsPerFrame);
int configuredBiomeCount = CountConfiguredBiomes(); int configuredBiomeCount = CountConfiguredBiomes();
if (atlas != null && atlasBiomeCount == configuredBiomeCount) if (atlas != null && atlasBiomeCount == configuredBiomeCount)
@@ -102,6 +122,7 @@ namespace InfiniteWorld.VoxelWorld
atlas?.Dispose(); atlas?.Dispose();
atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles); atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles);
atlasBiomeCount = atlas.BiomeCount; atlasBiomeCount = atlas.BiomeCount;
RefreshRegionMaterials();
} }
private void EnsureChunkRoot() private void EnsureChunkRoot()
@@ -123,6 +144,41 @@ namespace InfiniteWorld.VoxelWorld
chunkRoot = root.transform; 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() private bool TryResolveStreamTarget()
{ {
if (streamTarget != null) if (streamTarget != null)
@@ -164,7 +220,7 @@ namespace InfiniteWorld.VoxelWorld
runtime.State = ChunkState.Generating; runtime.State = ChunkState.Generating;
runtime.Version++; runtime.Version++;
GenerateChunkDataAsync(coord, runtime.Version).Forget(); GenerateChunkDataAsync(coord, runtime.Version, generationSession).Forget();
} }
} }
@@ -187,7 +243,7 @@ namespace InfiniteWorld.VoxelWorld
runtime.Version++; runtime.Version++;
runtime.State = ChunkState.SyncBuilding; runtime.State = ChunkState.SyncBuilding;
ApplyBuildResult(GenerateChunkData(coord, runtime.Version)); ApplyBuildResult(GenerateChunkData(coord, runtime.Version, generationSession));
RenderChunk(coord); RenderChunk(coord);
RefreshNeighborBorders(coord); RefreshNeighborBorders(coord);
} }
@@ -214,13 +270,14 @@ namespace InfiniteWorld.VoxelWorld
continue; continue;
} }
MarkRegionDirty(coord);
chunks.Remove(coord); chunks.Remove(coord);
runtime.Dispose(); runtime.Dispose();
RefreshNeighborBorders(coord); 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 margin = Mathf.Max(2, smoothingPasses + 1);
int sampleSize = chunkSize + margin * 2; 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) private bool[,] SmoothSampledMask(bool[,] source)
@@ -380,10 +437,11 @@ namespace InfiniteWorld.VoxelWorld
return; return;
} }
runtime.EnsureCreated(coord, chunkRoot, chunkSize, atlas.Material); runtime.EnsureCreated(coord, chunkRoot, chunkSize);
ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices); ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices);
runtime.ApplyMeshes(meshBuild, chunkSize); runtime.ApplyMeshes(meshBuild, chunkSize);
runtime.State = ChunkState.Rendered; runtime.State = ChunkState.Rendered;
MarkRegionDirty(coord);
} }
private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices) 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); 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 try
{ {
ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version)); ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version, session));
lock (generationLock) lock (generationLock)
{ {
completedBuilds.Enqueue(result); completedBuilds.Enqueue(result);
@@ -893,8 +951,139 @@ namespace InfiniteWorld.VoxelWorld
RenderChunk(coord); 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) private bool ApplyBuildResult(ChunkBuildResult result)
{ {
if (result.Session != generationSession)
{
return false;
}
if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime)) if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime))
{ {
return false; return false;
@@ -999,6 +1188,19 @@ namespace InfiniteWorld.VoxelWorld
chunks.Clear(); 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() private int CountConfiguredBiomes()
{ {
int count = 0; int count = 0;
@@ -1015,18 +1217,20 @@ namespace InfiniteWorld.VoxelWorld
private readonly struct ChunkBuildResult 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; Coord = coord;
Heights = heights; Heights = heights;
BiomeIndices = biomeIndices; BiomeIndices = biomeIndices;
Version = version; Version = version;
Session = session;
} }
public Vector2Int Coord { get; } public Vector2Int Coord { get; }
public int[] Heights { get; } public int[] Heights { get; }
public byte[] BiomeIndices { get; } public byte[] BiomeIndices { get; }
public int Version { get; } public int Version { get; }
public int Session { get; }
} }
private readonly struct ChunkMeshBuild private readonly struct ChunkMeshBuild
@@ -1080,8 +1284,6 @@ namespace InfiniteWorld.VoxelWorld
private sealed class ChunkRuntime private sealed class ChunkRuntime
{ {
public Transform Root; public Transform Root;
public MeshFilter Filter;
public MeshRenderer Renderer;
public MeshCollider MountainCollider; public MeshCollider MountainCollider;
public BoxCollider GroundCollider; public BoxCollider GroundCollider;
public Mesh RenderMesh; public Mesh RenderMesh;
@@ -1094,21 +1296,18 @@ namespace InfiniteWorld.VoxelWorld
public bool HasData => Heights != null && BiomeIndices != null; public bool HasData => Heights != null && BiomeIndices != null;
public bool IsRendered => State == ChunkState.Rendered && Root != 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) if (Root == null)
{ {
GameObject chunkObject = new GameObject($"VoxelChunk_{coord.x}_{coord.y}"); GameObject chunkObject = new GameObject($"VoxelChunk_{coord.x}_{coord.y}");
chunkObject.transform.SetParent(parent, false); chunkObject.transform.SetParent(parent, false);
Root = chunkObject.transform; Root = chunkObject.transform;
Filter = chunkObject.AddComponent<MeshFilter>();
Renderer = chunkObject.AddComponent<MeshRenderer>();
MountainCollider = chunkObject.AddComponent<MeshCollider>(); MountainCollider = chunkObject.AddComponent<MeshCollider>();
GroundCollider = chunkObject.AddComponent<BoxCollider>(); GroundCollider = chunkObject.AddComponent<BoxCollider>();
} }
Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize); Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize);
Renderer.sharedMaterial = material;
GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize); GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize);
GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f); GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f);
} }
@@ -1127,7 +1326,6 @@ namespace InfiniteWorld.VoxelWorld
RenderMesh = build.RenderMesh; RenderMesh = build.RenderMesh;
ColliderMesh = build.ColliderMesh; ColliderMesh = build.ColliderMesh;
Filter.sharedMesh = RenderMesh;
MountainCollider.sharedMesh = null; MountainCollider.sharedMesh = null;
MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : 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 private enum ChunkState
{ {
None, None,