[Add] Batch optimization
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user