diff --git a/Assets/Data/VoxelWorldConfig.asset b/Assets/Data/VoxelWorldConfig.asset new file mode 100644 index 00000000..2ef2363b --- /dev/null +++ b/Assets/Data/VoxelWorldConfig.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cac3da4969c2f94985de9f5fb30a682, type: 3} + m_Name: VoxelWorldConfig + m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldConfig + chunkSize: 16 + generationRadius: 3 + blockingGenerationRadius: 0 + seed: 12345 + maxMountainHeight: 3 + macroNoiseScale: 0.05 + detailNoiseScale: 0.12 + ridgeNoiseScale: 0.18 + wallThreshold: 0.6 + rockBias: 0.04 + smoothingPasses: 2 + passNoiseScale: 0.018 + passDetailScale: 0.041 + passThreshold: 0.22 + passFeather: 0.12 + heightNoiseScale: 0.08 + terraceNoiseScale: 0.17 + heightBias: 0.05 + biomeProfiles: + - {fileID: 11400000, guid: ae25f699f7f7d4144bbd148907fda668, type: 2} + - {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2} + - {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2} + biomeNoiseScale: 0.02 + biomeSize: 48 + maxAsyncChunkJobs: 2 + maxChunkBuildsPerFrame: 1 + maxChunkMeshBuildsPerFrame: 1 + maxColliderAppliesPerFrame: 1 + maxNeighborRefreshesPerFrame: 2 + renderRegionSizeInChunks: 4 + maxRegionBuildsPerFrame: 1 diff --git a/Assets/Data/VoxelWorldConfig.asset.meta b/Assets/Data/VoxelWorldConfig.asset.meta new file mode 100644 index 00000000..16c8036c --- /dev/null +++ b/Assets/Data/VoxelWorldConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8cf28a5522134b479c23f017234070c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/VoxelWorldTestScene.unity b/Assets/Scenes/VoxelWorldTestScene.unity index 5c80f866..1d2b6978 100644 --- a/Assets/Scenes/VoxelWorldTestScene.unity +++ b/Assets/Scenes/VoxelWorldTestScene.unity @@ -413,32 +413,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: VoxelWorld.Runtime::InfiniteWorld.VoxelWorld.VoxelWorldGenerator streamTarget: {fileID: 1331065949} - chunkSize: 16 - generationRadius: 7 - blockingGenerationRadius: 1 - seed: 12345 - maxMountainHeight: 12 - macroNoiseScale: 0.05 - detailNoiseScale: 0.12 - ridgeNoiseScale: 0.18 - wallThreshold: 0.6 - rockBias: 0.04 - smoothingPasses: 2 - passNoiseScale: 0.018 - passDetailScale: 0.041 - passThreshold: 0.22 - passFeather: 0.12 - heightNoiseScale: 0.08 - terraceNoiseScale: 0.17 - heightBias: 0.05 - biomeProfiles: - - {fileID: 11400000, guid: ae25f699f7f7d4144bbd148907fda668, type: 2} - - {fileID: 11400000, guid: eac8d825dd62e1c439235d273a4ca613, type: 2} - - {fileID: 11400000, guid: 6d0dbe510ed048440a3925ef40aeeb5b, type: 2} - biomeNoiseScale: 0.02 - biomeSize: 4 - maxAsyncChunkJobs: 8 - maxChunkBuildsPerFrame: 2 + config: {fileID: 11400000, guid: b8cf28a5522134b479c23f017234070c, type: 2} --- !u!4 &1842209028 Transform: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs new file mode 100644 index 00000000..a9c4b866 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld +{ + [CreateAssetMenu(menuName = "Infinite World/Voxel World Config", fileName = "VoxelWorldConfig")] + public sealed class VoxelWorldConfig : ScriptableObject + { + [Header("Streaming")] + [Min(8)] public int chunkSize = 16; + [Min(1)] public int generationRadius = 3; + [Min(0)] public int blockingGenerationRadius; + public int seed = 12345; + [Min(1)] public int maxMountainHeight = 3; + + [Header("Shape Noise")] + public float macroNoiseScale = 0.05f; + public float detailNoiseScale = 0.12f; + public float ridgeNoiseScale = 0.18f; + public float wallThreshold = 0.6f; + public float rockBias = 0.04f; + [Min(0)] public int smoothingPasses = 2; + + [Header("Global Passes")] + public float passNoiseScale = 0.018f; + public float passDetailScale = 0.041f; + public float passThreshold = 0.22f; + public float passFeather = 0.12f; + + [Header("Height")] + public float heightNoiseScale = 0.08f; + public float terraceNoiseScale = 0.17f; + public float heightBias = 0.05f; + + [Header("Biomes")] + public List biomeProfiles = new List(); + public float biomeNoiseScale = 0.02f; + [Min(1f)] public float biomeSize = 48f; + + [Header("Runtime")] + [Min(1)] public int maxAsyncChunkJobs = 2; + [Min(1)] public int maxChunkBuildsPerFrame = 1; + [Min(1)] public int maxChunkMeshBuildsPerFrame = 1; + [Min(1)] public int maxColliderAppliesPerFrame = 1; + [Min(0)] public int maxNeighborRefreshesPerFrame = 2; + [Min(1)] public int renderRegionSizeInChunks = 4; + [Min(1)] public int maxRegionBuildsPerFrame = 1; + } + + internal readonly struct VoxelWorldResolvedSettings + { + private static readonly IReadOnlyList EmptyBiomes = System.Array.Empty(); + + public static readonly VoxelWorldResolvedSettings Default = Resolve(null); + + public VoxelWorldResolvedSettings( + int chunkSize, + int generationRadius, + int blockingGenerationRadius, + int seed, + int maxMountainHeight, + float macroNoiseScale, + float detailNoiseScale, + float ridgeNoiseScale, + float wallThreshold, + float rockBias, + int smoothingPasses, + float passNoiseScale, + float passDetailScale, + float passThreshold, + float passFeather, + float heightNoiseScale, + float terraceNoiseScale, + float heightBias, + IReadOnlyList biomeProfiles, + float biomeNoiseScale, + float biomeSize, + int maxAsyncChunkJobs, + int maxChunkBuildsPerFrame, + int maxChunkMeshBuildsPerFrame, + int maxColliderAppliesPerFrame, + int maxNeighborRefreshesPerFrame, + int renderRegionSizeInChunks, + int maxRegionBuildsPerFrame) + { + ChunkSize = chunkSize; + GenerationRadius = generationRadius; + BlockingGenerationRadius = blockingGenerationRadius; + Seed = seed; + MaxMountainHeight = maxMountainHeight; + MacroNoiseScale = macroNoiseScale; + DetailNoiseScale = detailNoiseScale; + RidgeNoiseScale = ridgeNoiseScale; + WallThreshold = wallThreshold; + RockBias = rockBias; + SmoothingPasses = smoothingPasses; + PassNoiseScale = passNoiseScale; + PassDetailScale = passDetailScale; + PassThreshold = passThreshold; + PassFeather = passFeather; + HeightNoiseScale = heightNoiseScale; + TerraceNoiseScale = terraceNoiseScale; + HeightBias = heightBias; + BiomeProfiles = biomeProfiles; + BiomeNoiseScale = biomeNoiseScale; + BiomeSize = biomeSize; + MaxAsyncChunkJobs = maxAsyncChunkJobs; + MaxChunkBuildsPerFrame = maxChunkBuildsPerFrame; + MaxChunkMeshBuildsPerFrame = maxChunkMeshBuildsPerFrame; + MaxColliderAppliesPerFrame = maxColliderAppliesPerFrame; + MaxNeighborRefreshesPerFrame = maxNeighborRefreshesPerFrame; + RenderRegionSizeInChunks = renderRegionSizeInChunks; + MaxRegionBuildsPerFrame = maxRegionBuildsPerFrame; + } + + public int ChunkSize { get; } + public int GenerationRadius { get; } + public int BlockingGenerationRadius { get; } + public int Seed { get; } + public int MaxMountainHeight { get; } + public float MacroNoiseScale { get; } + public float DetailNoiseScale { get; } + public float RidgeNoiseScale { get; } + public float WallThreshold { get; } + public float RockBias { get; } + public int SmoothingPasses { get; } + public float PassNoiseScale { get; } + public float PassDetailScale { get; } + public float PassThreshold { get; } + public float PassFeather { get; } + public float HeightNoiseScale { get; } + public float TerraceNoiseScale { get; } + public float HeightBias { get; } + public IReadOnlyList BiomeProfiles { get; } + public float BiomeNoiseScale { get; } + public float BiomeSize { get; } + public int MaxAsyncChunkJobs { get; } + public int MaxChunkBuildsPerFrame { get; } + public int MaxChunkMeshBuildsPerFrame { get; } + public int MaxColliderAppliesPerFrame { get; } + public int MaxNeighborRefreshesPerFrame { get; } + public int RenderRegionSizeInChunks { get; } + public int MaxRegionBuildsPerFrame { get; } + + public static VoxelWorldResolvedSettings Resolve(VoxelWorldConfig config) + { + IReadOnlyList biomes = config != null && config.biomeProfiles != null + ? config.biomeProfiles + : EmptyBiomes; + + return new VoxelWorldResolvedSettings( + Mathf.Max(8, config != null ? config.chunkSize : 16), + Mathf.Max(1, config != null ? config.generationRadius : 3), + Mathf.Max(0, config != null ? config.blockingGenerationRadius : 0), + config != null ? config.seed : 12345, + Mathf.Max(1, config != null ? config.maxMountainHeight : 3), + config != null ? config.macroNoiseScale : 0.05f, + config != null ? config.detailNoiseScale : 0.12f, + config != null ? config.ridgeNoiseScale : 0.18f, + config != null ? config.wallThreshold : 0.6f, + config != null ? config.rockBias : 0.04f, + Mathf.Max(0, config != null ? config.smoothingPasses : 2), + config != null ? config.passNoiseScale : 0.018f, + config != null ? config.passDetailScale : 0.041f, + config != null ? config.passThreshold : 0.22f, + config != null ? config.passFeather : 0.12f, + config != null ? config.heightNoiseScale : 0.08f, + config != null ? config.terraceNoiseScale : 0.17f, + config != null ? config.heightBias : 0.05f, + biomes, + config != null ? config.biomeNoiseScale : 0.02f, + Mathf.Max(1f, config != null ? config.biomeSize : 48f), + Mathf.Max(1, config != null ? config.maxAsyncChunkJobs : 2), + Mathf.Max(1, config != null ? config.maxChunkBuildsPerFrame : 1), + Mathf.Max(1, config != null ? config.maxChunkMeshBuildsPerFrame : 1), + Mathf.Max(1, config != null ? config.maxColliderAppliesPerFrame : 1), + Mathf.Max(0, config != null ? config.maxNeighborRefreshesPerFrame : 2), + Mathf.Max(1, config != null ? config.renderRegionSizeInChunks : 4), + Mathf.Max(1, config != null ? config.maxRegionBuildsPerFrame : 1)); + } + } +} diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs.meta b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs.meta new file mode 100644 index 00000000..ea97724f --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0cac3da4969c2f94985de9f5fb30a682 \ No newline at end of file diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs new file mode 100644 index 00000000..0e99dd99 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs @@ -0,0 +1,165 @@ +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld +{ + public sealed partial class VoxelWorldGenerator + { + private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version, int session, int runtimeId) + { + int margin = Mathf.Max(2, smoothingPasses + 1); + int sampleSize = chunkSize + margin * 2; + bool[,] sampled = new bool[sampleSize, sampleSize]; + + for (int z = 0; z < sampleSize; z++) + { + for (int x = 0; x < sampleSize; x++) + { + int localX = x - margin; + int localZ = z - margin; + Vector2Int worldCell = ChunkToWorldCell(coord, localX, localZ); + sampled[x, z] = SampleRock(worldCell); + } + } + + for (int pass = 0; pass < smoothingPasses; pass++) + { + sampled = SmoothSampledMask(sampled); + } + + int[] heights = new int[chunkSize * chunkSize]; + byte[] biomeIndices = new byte[chunkSize * chunkSize]; + for (int z = 0; z < chunkSize; z++) + { + for (int x = 0; x < chunkSize; x++) + { + Vector2Int worldCell = ChunkToWorldCell(coord, x, z); + biomeIndices[z * chunkSize + x] = SampleBiomeIndex(worldCell); + bool hasMountain = sampled[x + margin, z + margin]; + if (!hasMountain) + { + continue; + } + + heights[z * chunkSize + x] = SampleHeight(worldCell); + } + } + + return new ChunkBuildResult(coord, heights, biomeIndices, version, session, runtimeId); + } + + private bool[,] SmoothSampledMask(bool[,] source) + { + int width = source.GetLength(0); + int height = source.GetLength(1); + bool[,] result = new bool[width, height]; + + for (int z = 0; z < height; z++) + { + for (int x = 0; x < width; x++) + { + int solidNeighbors = CountSampledNeighbors(source, x, z); + if (solidNeighbors >= 5) + { + result[x, z] = true; + } + else if (solidNeighbors <= 2) + { + result[x, z] = false; + } + else + { + result[x, z] = source[x, z]; + } + } + } + + return result; + } + + private static int CountSampledNeighbors(bool[,] sampled, int x, int z) + { + int width = sampled.GetLength(0); + int height = sampled.GetLength(1); + int count = 0; + + for (int oz = -1; oz <= 1; oz++) + { + for (int ox = -1; ox <= 1; ox++) + { + if (ox == 0 && oz == 0) + { + continue; + } + + int nx = x + ox; + int nz = z + oz; + if (nx < 0 || nz < 0 || nx >= width || nz >= height) + { + count++; + continue; + } + + if (sampled[nx, nz]) + { + count++; + } + } + } + + return count; + } + + private bool SampleRock(Vector2Int worldCell) + { + float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale); + float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale); + float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f); + + float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias; + if (IsInsideGlobalPass(worldCell)) + { + rockValue -= 0.45f; + } + + return rockValue >= wallThreshold; + } + + private bool IsInsideGlobalPass(Vector2Int worldCell) + { + float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale); + float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale); + float ridged = Mathf.Abs(primary * 2f - 1f); + float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f); + return warped <= passThreshold + passFeather * detail; + } + + private int SampleHeight(Vector2Int worldCell) + { + float macro = Mathf.PerlinNoise((worldCell.x - seed * 0.47f) * heightNoiseScale, (worldCell.y + seed * 0.37f) * heightNoiseScale); + float terrace = Mathf.PerlinNoise((worldCell.x + seed * 0.67f) * terraceNoiseScale, (worldCell.y - seed * 0.59f) * terraceNoiseScale); + float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.71f) * ridgeNoiseScale, (worldCell.y - seed * 0.73f) * ridgeNoiseScale) * 2f - 1f); + float heightValue = macro * 0.55f + terrace * 0.2f + ridge * 0.25f + heightBias; + int height = 1 + Mathf.FloorToInt(Mathf.Clamp01(heightValue) * maxMountainHeight); + return Mathf.Clamp(height, 1, maxMountainHeight); + } + + private byte SampleBiomeIndex(Vector2Int worldCell) + { + int biomeCount = Mathf.Max(1, atlasBiomeCount > 0 ? atlasBiomeCount : CountConfiguredBiomes()); + if (biomeCount <= 1) + { + return 0; + } + + float effectiveScale = biomeNoiseScale / Mathf.Max(1f, biomeSize); + float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.83f) * effectiveScale, (worldCell.y - seed * 0.79f) * effectiveScale); + int biomeIndex = Mathf.FloorToInt(Mathf.Clamp01(noise) * biomeCount); + return (byte)Mathf.Clamp(biomeIndex, 0, biomeCount - 1); + } + + private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localZ) + { + return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localZ); + } + } +} diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs.meta b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs.meta new file mode 100644 index 00000000..6067b513 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Generation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9da608950686fb345b172db0a56bced5 \ No newline at end of file diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs new file mode 100644 index 00000000..7d458478 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs @@ -0,0 +1,451 @@ +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld +{ + public sealed partial class VoxelWorldGenerator + { + private void RenderChunk(Vector2Int coord) + { + if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData || atlas == null) + { + return; + } + + runtime.EnsureCreated(coord, chunkRoot, chunkSize); + ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices); + runtime.ApplyRenderData(meshBuild.RenderSnapshot); + EnqueueColliderApply(coord, runtime.Version, runtime.RuntimeId, meshBuild.ColliderMesh); + runtime.State = ChunkState.Rendered; + MarkRegionDirty(coord); + } + + private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices) + { + MeshBuffers renderBuffers = new MeshBuffers(); + MeshBuffers colliderBuffers = new MeshBuffers(); + + BuildGroundSurface(heights, biomeIndices, renderBuffers); + BuildMountainTops(coord, heights, biomeIndices, renderBuffers, colliderBuffers); + BuildMountainSides(coord, heights, biomeIndices, renderBuffers, colliderBuffers); + + ChunkRenderSnapshot renderSnapshot = renderBuffers.ToSnapshot(); + return new ChunkMeshBuild(colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"), renderSnapshot); + } + + private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers) + { + bool[,] visited = new bool[chunkSize, chunkSize]; + for (int z = 0; z < chunkSize; z++) + { + for (int x = 0; x < chunkSize; x++) + { + int index = z * chunkSize + x; + if (visited[x, z] || heights[index] > 0) + { + continue; + } + + byte biomeIndex = biomeIndices[index]; + int width = 1; + while (x + width < chunkSize && !visited[x + width, z] && heights[z * chunkSize + x + width] == 0 && biomeIndices[z * chunkSize + x + width] == biomeIndex) + { + width++; + } + + int depth = 1; + bool canGrow = true; + while (z + depth < chunkSize && canGrow) + { + for (int ix = 0; ix < width; ix++) + { + int candidateIndex = (z + depth) * chunkSize + x + ix; + if (visited[x + ix, z + depth] || heights[candidateIndex] > 0 || biomeIndices[candidateIndex] != biomeIndex) + { + canGrow = false; + break; + } + } + + if (canGrow) + { + depth++; + } + } + + for (int dz = 0; dz < depth; dz++) + { + for (int dx = 0; dx < width; dx++) + { + visited[x + dx, z + dz] = true; + } + } + + AddTopQuad(renderBuffers, x, z, width, depth, 0f, atlas.GetTextureLayer(biomeIndex, VoxelSurfaceType.WalkableSurface)); + } + } + } + + private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers) + { + bool[,] visited = new bool[chunkSize, chunkSize]; + for (int z = 0; z < chunkSize; z++) + { + for (int x = 0; x < chunkSize; x++) + { + int height = heights[z * chunkSize + x]; + if (height <= 0 || visited[x, z]) + { + continue; + } + + int index = z * chunkSize + x; + VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; + byte biomeIndex = biomeIndices[index]; + + int width = 1; + while (x + width < chunkSize && !visited[x + width, z]) + { + int candidateIndex = z * chunkSize + x + width; + if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex) + { + break; + } + + VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + width, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; + if (candidateSurface != surfaceType) + { + break; + } + + width++; + } + + int depth = 1; + bool canGrow = true; + while (z + depth < chunkSize && canGrow) + { + for (int ix = 0; ix < width; ix++) + { + if (visited[x + ix, z + depth]) + { + canGrow = false; + break; + } + + int candidateIndex = (z + depth) * chunkSize + x + ix; + if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex) + { + canGrow = false; + break; + } + + VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + ix, z + depth), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; + if (candidateSurface != surfaceType) + { + canGrow = false; + break; + } + } + + if (canGrow) + { + depth++; + } + } + + for (int dz = 0; dz < depth; dz++) + { + for (int dx = 0; dx < width; dx++) + { + visited[x + dx, z + dz] = true; + } + } + + int textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); + AddTopQuad(renderBuffers, x, z, width, depth, height, textureLayer); + AddTopQuad(colliderBuffers, x, z, width, depth, height, 0, false); + } + } + } + + private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers) + { + for (int z = 0; z < chunkSize; z++) + { + BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, true); + BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, false); + } + + for (int x = 0; x < chunkSize; x++) + { + BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, true); + BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, false); + } + } + + private void BuildNorthSouthFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int z, bool north) + { + bool[,] visited = new bool[chunkSize, maxMountainHeight]; + for (int x = 0; x < chunkSize; x++) + { + for (int y = 0; y < maxMountainHeight; y++) + { + if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)) + { + continue; + } + + int width = 1; + while (x + width < chunkSize && !visited[x + width, y] && TryGetNorthSouthFace(coord, heights, biomeIndices, x + width, z, y, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer) + { + width++; + } + + int height = 1; + bool canGrow = true; + while (y + height < maxMountainHeight && canGrow) + { + for (int dx = 0; dx < width; dx++) + { + if (visited[x + dx, y + height] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x + dx, z, y + height, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer) + { + canGrow = false; + break; + } + } + + if (canGrow) + { + height++; + } + } + + for (int dy = 0; dy < height; dy++) + { + for (int dx = 0; dx < width; dx++) + { + visited[x + dx, y + dy] = true; + } + } + + float faceZ = north ? z + 1f : z; + Vector3 bl = new Vector3(x, y, faceZ); + Vector3 br = new Vector3(x + width, y, faceZ); + Vector3 tr = new Vector3(x + width, y + height, faceZ); + Vector3 tl = new Vector3(x, y + height, faceZ); + + if (north) + { + AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.forward); + AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.forward, false); + } + else + { + AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.back); + AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.back, false); + } + } + } + } + + private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east) + { + bool[,] visited = new bool[chunkSize, maxMountainHeight]; + for (int z = 0; z < chunkSize; z++) + { + for (int y = 0; y < maxMountainHeight; y++) + { + if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)) + { + continue; + } + + int width = 1; + while (z + width < chunkSize && !visited[z + width, y] && TryGetEastWestFace(coord, heights, biomeIndices, x, z + width, y, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer) + { + width++; + } + + int height = 1; + bool canGrow = true; + while (y + height < maxMountainHeight && canGrow) + { + for (int dz = 0; dz < width; dz++) + { + if (visited[z + dz, y + height] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z + dz, y + height, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer) + { + canGrow = false; + break; + } + } + + if (canGrow) + { + height++; + } + } + + for (int dy = 0; dy < height; dy++) + { + for (int dz = 0; dz < width; dz++) + { + visited[z + dz, y + dy] = true; + } + } + + float faceX = east ? x + 1f : x; + Vector3 bl = new Vector3(faceX, y, z); + Vector3 br = new Vector3(faceX, y, z + width); + Vector3 tr = new Vector3(faceX, y + height, z + width); + Vector3 tl = new Vector3(faceX, y + height, z); + + if (east) + { + AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.right); + AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.right, false); + } + else + { + AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.left); + AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.left, false); + } + } + } + } + + private bool TryGetNorthSouthFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer) + { + int current = heights[z * chunkSize + x]; + int worldX = coord.x * chunkSize + x; + int worldZ = coord.y * chunkSize + z; + int neighbor = GetHeightAtWorldCell(new Vector2Int(worldX, north ? worldZ + 1 : worldZ - 1)); + if (y >= current || y < neighbor) + { + biomeIndex = 0; + surfaceType = VoxelSurfaceType.Dirt; + textureLayer = 0; + return false; + } + + biomeIndex = biomeIndices[z * chunkSize + x]; + surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt; + textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); + return true; + } + + private bool TryGetEastWestFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer) + { + int current = heights[z * chunkSize + x]; + int worldX = coord.x * chunkSize + x; + int worldZ = coord.y * chunkSize + z; + int neighbor = GetHeightAtWorldCell(new Vector2Int(east ? worldX + 1 : worldX - 1, worldZ)); + if (y >= current || y < neighbor) + { + biomeIndex = 0; + surfaceType = VoxelSurfaceType.Dirt; + textureLayer = 0; + return false; + } + + biomeIndex = biomeIndices[z * chunkSize + x]; + surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt; + textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); + return true; + } + + private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, int textureLayer, bool withUv = true) + { + int baseIndex = buffers.Vertices.Count; + buffers.Vertices.Add(new Vector3(x, height, z)); + buffers.Vertices.Add(new Vector3(x + width, height, z)); + buffers.Vertices.Add(new Vector3(x + width, height, z + depth)); + buffers.Vertices.Add(new Vector3(x, height, z + depth)); + buffers.Normals.Add(Vector3.up); + buffers.Normals.Add(Vector3.up); + buffers.Normals.Add(Vector3.up); + buffers.Normals.Add(Vector3.up); + + if (withUv) + { + buffers.Uvs.Add(new Vector2(0f, 0f)); + buffers.Uvs.Add(new Vector2(width, 0f)); + buffers.Uvs.Add(new Vector2(width, depth)); + buffers.Uvs.Add(new Vector2(0f, depth)); + Vector2 textureData = new Vector2(textureLayer, 0f); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + } + + buffers.Triangles.Add(baseIndex); + buffers.Triangles.Add(baseIndex + 2); + buffers.Triangles.Add(baseIndex + 1); + buffers.Triangles.Add(baseIndex); + buffers.Triangles.Add(baseIndex + 3); + buffers.Triangles.Add(baseIndex + 2); + } + + private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, float width, float height, int textureLayer, Vector3 normal, bool withUv = true) + { + int baseIndex = buffers.Vertices.Count; + buffers.Vertices.Add(bottomLeft); + buffers.Vertices.Add(bottomRight); + buffers.Vertices.Add(topRight); + buffers.Vertices.Add(topLeft); + buffers.Normals.Add(normal); + buffers.Normals.Add(normal); + buffers.Normals.Add(normal); + buffers.Normals.Add(normal); + + if (withUv) + { + buffers.Uvs.Add(new Vector2(0f, 0f)); + buffers.Uvs.Add(new Vector2(width, 0f)); + buffers.Uvs.Add(new Vector2(width, height)); + buffers.Uvs.Add(new Vector2(0f, height)); + Vector2 textureData = new Vector2(textureLayer, 0f); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + buffers.TextureData.Add(textureData); + } + + buffers.Triangles.Add(baseIndex); + buffers.Triangles.Add(baseIndex + 1); + buffers.Triangles.Add(baseIndex + 2); + buffers.Triangles.Add(baseIndex); + buffers.Triangles.Add(baseIndex + 2); + buffers.Triangles.Add(baseIndex + 3); + } + + private bool IsCliffTop(Vector2Int worldCell, int currentHeight) + { + return GetHeightAtWorldCell(worldCell + Vector2Int.up) < currentHeight || + GetHeightAtWorldCell(worldCell + Vector2Int.right) < currentHeight || + GetHeightAtWorldCell(worldCell + Vector2Int.down) < currentHeight || + GetHeightAtWorldCell(worldCell + Vector2Int.left) < currentHeight; + } + + private int GetHeightAtWorldCell(Vector2Int worldCell) + { + Vector2Int coord = new Vector2Int( + Mathf.FloorToInt(worldCell.x / (float)chunkSize), + Mathf.FloorToInt(worldCell.y / (float)chunkSize)); + + if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) + { + return SampleRock(worldCell) ? SampleHeight(worldCell) : 0; + } + + int localX = worldCell.x - coord.x * chunkSize; + int localZ = worldCell.y - coord.y * chunkSize; + if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize) + { + return SampleRock(worldCell) ? SampleHeight(worldCell) : 0; + } + + return runtime.Heights[localZ * chunkSize + localX]; + } + } +} diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs.meta b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs.meta new file mode 100644 index 00000000..a7dc60d2 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Mesh.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 811ff92b4e36193499cad8631c9443a7 \ No newline at end of file diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs new file mode 100644 index 00000000..0fc1a876 --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs @@ -0,0 +1,339 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld +{ + public sealed partial class VoxelWorldGenerator + { + private struct ChunkBuildResult + { + public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version, int session, int runtimeId) + { + Coord = coord; + Heights = heights; + BiomeIndices = biomeIndices; + Version = version; + Session = session; + RuntimeId = runtimeId; + } + + public Vector2Int Coord { get; } + public int[] Heights { get; } + public byte[] BiomeIndices { get; } + public int Version { get; } + public int Session { get; } + public int RuntimeId { get; } + } + + private struct ChunkMeshBuild + { + public ChunkMeshBuild(Mesh colliderMesh, ChunkRenderSnapshot renderSnapshot) + { + ColliderMesh = colliderMesh; + RenderSnapshot = renderSnapshot; + } + + public Mesh ColliderMesh { get; } + public ChunkRenderSnapshot RenderSnapshot { get; } + } + + private struct PendingColliderMeshApply + { + public PendingColliderMeshApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh) + { + Coord = coord; + Version = version; + RuntimeId = runtimeId; + ColliderMesh = colliderMesh; + } + + public Vector2Int Coord { get; } + public int Version { get; } + public int RuntimeId { get; } + public Mesh ColliderMesh { get; } + } + + private sealed class ChunkRenderSnapshot + { + public ChunkRenderSnapshot(Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles) + { + Vertices = vertices; + Normals = normals; + Uv0 = uv0; + Uv1 = uv1; + Triangles = triangles; + } + + public Vector3[] Vertices { get; } + public Vector3[] Normals { get; } + public Vector2[] Uv0 { get; } + public Vector2[] Uv1 { get; } + public int[] Triangles { get; } + public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0; + } + + private struct RegionChunkSnapshot + { + public RegionChunkSnapshot(ChunkRenderSnapshot snapshot, Vector3 localOffset) + { + Snapshot = snapshot; + LocalOffset = localOffset; + } + + public ChunkRenderSnapshot Snapshot { get; } + public Vector3 LocalOffset { get; } + } + + private struct RegionBuildRequest + { + public RegionBuildRequest(Vector2Int regionCoord, int version, int session, RegionChunkSnapshot[] chunks) + { + RegionCoord = regionCoord; + Version = version; + Session = session; + Chunks = chunks; + } + + public Vector2Int RegionCoord { get; } + public int Version { get; } + public int Session { get; } + public RegionChunkSnapshot[] Chunks { get; } + } + + private struct RegionBuildResult + { + public RegionBuildResult(Vector2Int regionCoord, int version, int session, Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles, Bounds bounds) + { + RegionCoord = regionCoord; + Version = version; + Session = session; + Vertices = vertices; + Normals = normals; + Uv0 = uv0; + Uv1 = uv1; + Triangles = triangles; + Bounds = bounds; + } + + public Vector2Int RegionCoord { get; } + public int Version { get; } + public int Session { get; } + public Vector3[] Vertices { get; } + public Vector3[] Normals { get; } + public Vector2[] Uv0 { get; } + public Vector2[] Uv1 { get; } + public int[] Triangles { get; } + public Bounds Bounds { get; } + public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0; + + public static RegionBuildResult CreateEmpty(Vector2Int regionCoord, int version, int session) + { + return new RegionBuildResult(regionCoord, version, session, null, null, null, null, null, new Bounds()); + } + } + + private sealed class MeshBuffers + { + public readonly List Vertices = new List(512); + public readonly List Normals = new List(512); + public readonly List Uvs = new List(512); + public readonly List TextureData = new List(512); + public readonly List Triangles = new List(1024); + + public Mesh ToMesh(string meshName) + { + Mesh mesh = new Mesh { name = meshName }; + if (Vertices.Count == 0) + { + return mesh; + } + + mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; + mesh.SetVertices(Vertices); + if (Normals.Count == Vertices.Count) + { + mesh.SetNormals(Normals); + } + if (Uvs.Count == Vertices.Count) + { + mesh.SetUVs(0, Uvs); + } + if (TextureData.Count == Vertices.Count) + { + mesh.SetUVs(1, TextureData); + } + mesh.SetTriangles(Triangles, 0, true); + mesh.RecalculateBounds(); + return mesh; + } + + public ChunkRenderSnapshot ToSnapshot() + { + return new ChunkRenderSnapshot( + Vertices.ToArray(), + Normals.ToArray(), + Uvs.ToArray(), + TextureData.ToArray(), + Triangles.ToArray()); + } + } + + private sealed class ChunkRuntime + { + public Transform Root; + public MeshCollider MountainCollider; + public BoxCollider GroundCollider; + public Mesh ColliderMesh; + public ChunkRenderSnapshot RenderSnapshot; + public int[] Heights; + public byte[] BiomeIndices; + public ChunkState State; + public int Version; + public int RuntimeId; + + public bool HasData => Heights != null && BiomeIndices != null; + public bool IsRendered => State == ChunkState.Rendered && Root != null; + + 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; + MountainCollider = chunkObject.AddComponent(); + GroundCollider = chunkObject.AddComponent(); + } + + Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize); + GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize); + GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f); + } + + public void ApplyRenderData(ChunkRenderSnapshot renderSnapshot) + { + RenderSnapshot = renderSnapshot; + } + + public void ApplyColliderMesh(Mesh colliderMesh) + { + if (ColliderMesh != null) + { + DestroyMesh(ColliderMesh); + } + + ColliderMesh = colliderMesh; + MountainCollider.sharedMesh = null; + MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : null; + } + + public void Dispose() + { + if (Root != null) + { + if (Application.isPlaying) + { + Object.Destroy(Root.gameObject); + } + else + { + Object.DestroyImmediate(Root.gameObject); + } + } + + DestroyMesh(ColliderMesh); + RenderSnapshot = null; + } + + private static void DestroyMesh(Mesh mesh) + { + if (mesh == null) + { + return; + } + + if (Application.isPlaying) + { + Object.Destroy(mesh); + } + else + { + Object.DestroyImmediate(mesh); + } + } + } + + 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) + { + Object.Destroy(Root.gameObject); + } + else + { + Object.DestroyImmediate(Root.gameObject); + } + } + + DestroyMesh(RenderMesh); + } + + private static void DestroyMesh(Mesh mesh) + { + if (mesh == null) + { + return; + } + + if (Application.isPlaying) + { + Object.Destroy(mesh); + } + else + { + Object.DestroyImmediate(mesh); + } + } + } + + private enum ChunkState + { + None, + Generating, + SyncBuilding, + ReadyToRender, + Rendered + } + } +} diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs.meta b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs.meta new file mode 100644 index 00000000..c07dce9d --- /dev/null +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.Types.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 56e0b6ddd79410b40bdda73c4e20515d \ No newline at end of file diff --git a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs index 2a770e19..467ed262 100644 --- a/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs +++ b/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldGenerator.cs @@ -5,48 +5,11 @@ using UnityEngine; namespace InfiniteWorld.VoxelWorld { - public sealed class VoxelWorldGenerator : MonoBehaviour + public sealed partial class VoxelWorldGenerator : MonoBehaviour { - [Header("Streaming")] + [Header("References")] [SerializeField] private Transform streamTarget; - [SerializeField, Min(8)] private int chunkSize = 16; - [SerializeField, Min(1)] private int generationRadius = 3; - [SerializeField, Min(0)] private int blockingGenerationRadius = 0; - [SerializeField] private int seed = 12345; - [SerializeField, Min(1)] private int maxMountainHeight = 3; - - [Header("Shape Noise")] - [SerializeField] private float macroNoiseScale = 0.05f; - [SerializeField] private float detailNoiseScale = 0.12f; - [SerializeField] private float ridgeNoiseScale = 0.18f; - [SerializeField] private float wallThreshold = 0.6f; - [SerializeField] private float rockBias = 0.04f; - [SerializeField, Min(0)] private int smoothingPasses = 2; - - [Header("Global Passes")] - [SerializeField] private float passNoiseScale = 0.018f; - [SerializeField] private float passDetailScale = 0.041f; - [SerializeField] private float passThreshold = 0.22f; - [SerializeField] private float passFeather = 0.12f; - - [Header("Height")] - [SerializeField] private float heightNoiseScale = 0.08f; - [SerializeField] private float terraceNoiseScale = 0.17f; - [SerializeField] private float heightBias = 0.05f; - - [Header("Biomes")] - [SerializeField] private List biomeProfiles = new List(); - [SerializeField] private float biomeNoiseScale = 0.02f; - [SerializeField, Min(1f)] private float biomeSize = 48f; - - [Header("Runtime")] - [SerializeField, Min(1)] private int maxAsyncChunkJobs = 2; - [SerializeField, Min(1)] private int maxChunkBuildsPerFrame = 1; - [SerializeField, Min(1)] private int maxChunkMeshBuildsPerFrame = 1; - [SerializeField, Min(1)] private int maxColliderAppliesPerFrame = 1; - [SerializeField, Min(0)] private int maxNeighborRefreshesPerFrame = 2; - [SerializeField, Min(1)] private int renderRegionSizeInChunks = 4; - [SerializeField, Min(1)] private int maxRegionBuildsPerFrame = 1; + [SerializeField] private VoxelWorldConfig config; private readonly Dictionary chunks = new Dictionary(); private readonly Dictionary regions = new Dictionary(); @@ -73,6 +36,36 @@ namespace InfiniteWorld.VoxelWorld private VoxelWorldAtlas atlas; private int atlasBiomeCount; private bool regionRebuildLoopRunning; + private VoxelWorldResolvedSettings settings = VoxelWorldResolvedSettings.Default; + + private int chunkSize => settings.ChunkSize; + private int generationRadius => settings.GenerationRadius; + private int blockingGenerationRadius => settings.BlockingGenerationRadius; + private int seed => settings.Seed; + private int maxMountainHeight => settings.MaxMountainHeight; + private float macroNoiseScale => settings.MacroNoiseScale; + private float detailNoiseScale => settings.DetailNoiseScale; + private float ridgeNoiseScale => settings.RidgeNoiseScale; + private float wallThreshold => settings.WallThreshold; + private float rockBias => settings.RockBias; + private int smoothingPasses => settings.SmoothingPasses; + private float passNoiseScale => settings.PassNoiseScale; + private float passDetailScale => settings.PassDetailScale; + private float passThreshold => settings.PassThreshold; + private float passFeather => settings.PassFeather; + private float heightNoiseScale => settings.HeightNoiseScale; + private float terraceNoiseScale => settings.TerraceNoiseScale; + private float heightBias => settings.HeightBias; + private IReadOnlyList biomeProfiles => settings.BiomeProfiles; + private float biomeNoiseScale => settings.BiomeNoiseScale; + private float biomeSize => settings.BiomeSize; + private int maxAsyncChunkJobs => settings.MaxAsyncChunkJobs; + private int maxChunkBuildsPerFrame => settings.MaxChunkBuildsPerFrame; + private int maxChunkMeshBuildsPerFrame => settings.MaxChunkMeshBuildsPerFrame; + private int maxColliderAppliesPerFrame => settings.MaxColliderAppliesPerFrame; + private int maxNeighborRefreshesPerFrame => settings.MaxNeighborRefreshesPerFrame; + private int renderRegionSizeInChunks => settings.RenderRegionSizeInChunks; + private int maxRegionBuildsPerFrame => settings.MaxRegionBuildsPerFrame; private void Awake() { @@ -132,13 +125,7 @@ namespace InfiniteWorld.VoxelWorld private void EnsureRuntimeData() { - maxMountainHeight = Mathf.Max(1, maxMountainHeight); - chunkSize = Mathf.Max(8, chunkSize); - maxChunkMeshBuildsPerFrame = Mathf.Max(1, maxChunkMeshBuildsPerFrame); - maxColliderAppliesPerFrame = Mathf.Max(1, maxColliderAppliesPerFrame); - maxNeighborRefreshesPerFrame = Mathf.Max(0, maxNeighborRefreshesPerFrame); - renderRegionSizeInChunks = Mathf.Max(1, renderRegionSizeInChunks); - maxRegionBuildsPerFrame = Mathf.Max(1, maxRegionBuildsPerFrame); + settings = VoxelWorldResolvedSettings.Resolve(config); int configuredBiomeCount = CountConfiguredBiomes(); if (atlas != null && atlasBiomeCount == configuredBiomeCount) @@ -301,603 +288,6 @@ namespace InfiniteWorld.VoxelWorld } } - private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version, int session, int runtimeId) - { - int margin = Mathf.Max(2, smoothingPasses + 1); - int sampleSize = chunkSize + margin * 2; - bool[,] sampled = new bool[sampleSize, sampleSize]; - - for (int z = 0; z < sampleSize; z++) - { - for (int x = 0; x < sampleSize; x++) - { - int localX = x - margin; - int localZ = z - margin; - Vector2Int worldCell = ChunkToWorldCell(coord, localX, localZ); - sampled[x, z] = SampleRock(worldCell); - } - } - - for (int pass = 0; pass < smoothingPasses; pass++) - { - sampled = SmoothSampledMask(sampled); - } - - int[] heights = new int[chunkSize * chunkSize]; - byte[] biomeIndices = new byte[chunkSize * chunkSize]; - for (int z = 0; z < chunkSize; z++) - { - for (int x = 0; x < chunkSize; x++) - { - Vector2Int worldCell = ChunkToWorldCell(coord, x, z); - biomeIndices[z * chunkSize + x] = SampleBiomeIndex(worldCell); - bool hasMountain = sampled[x + margin, z + margin]; - if (!hasMountain) - { - continue; - } - - heights[z * chunkSize + x] = SampleHeight(worldCell); - } - } - - return new ChunkBuildResult(coord, heights, biomeIndices, version, session, runtimeId); - } - - private bool[,] SmoothSampledMask(bool[,] source) - { - int width = source.GetLength(0); - int height = source.GetLength(1); - bool[,] result = new bool[width, height]; - - for (int z = 0; z < height; z++) - { - for (int x = 0; x < width; x++) - { - int solidNeighbors = CountSampledNeighbors(source, x, z); - if (solidNeighbors >= 5) - { - result[x, z] = true; - } - else if (solidNeighbors <= 2) - { - result[x, z] = false; - } - else - { - result[x, z] = source[x, z]; - } - } - } - - return result; - } - - private static int CountSampledNeighbors(bool[,] sampled, int x, int z) - { - int width = sampled.GetLength(0); - int height = sampled.GetLength(1); - int count = 0; - - for (int oz = -1; oz <= 1; oz++) - { - for (int ox = -1; ox <= 1; ox++) - { - if (ox == 0 && oz == 0) - { - continue; - } - - int nx = x + ox; - int nz = z + oz; - if (nx < 0 || nz < 0 || nx >= width || nz >= height) - { - count++; - continue; - } - - if (sampled[nx, nz]) - { - count++; - } - } - } - - return count; - } - - private bool SampleRock(Vector2Int worldCell) - { - float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale); - float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale); - float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f); - - float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias; - if (IsInsideGlobalPass(worldCell)) - { - rockValue -= 0.45f; - } - - return rockValue >= wallThreshold; - } - - private bool IsInsideGlobalPass(Vector2Int worldCell) - { - float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale); - float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale); - float ridged = Mathf.Abs(primary * 2f - 1f); - float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f); - return warped <= passThreshold + passFeather * detail; - } - - private int SampleHeight(Vector2Int worldCell) - { - float macro = Mathf.PerlinNoise((worldCell.x - seed * 0.47f) * heightNoiseScale, (worldCell.y + seed * 0.37f) * heightNoiseScale); - float terrace = Mathf.PerlinNoise((worldCell.x + seed * 0.67f) * terraceNoiseScale, (worldCell.y - seed * 0.59f) * terraceNoiseScale); - float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.71f) * ridgeNoiseScale, (worldCell.y - seed * 0.73f) * ridgeNoiseScale) * 2f - 1f); - float heightValue = macro * 0.55f + terrace * 0.2f + ridge * 0.25f + heightBias; - int height = 1 + Mathf.FloorToInt(Mathf.Clamp01(heightValue) * maxMountainHeight); - return Mathf.Clamp(height, 1, maxMountainHeight); - } - - private byte SampleBiomeIndex(Vector2Int worldCell) - { - int biomeCount = Mathf.Max(1, atlasBiomeCount > 0 ? atlasBiomeCount : CountConfiguredBiomes()); - if (biomeCount <= 1) - { - return 0; - } - - float effectiveScale = biomeNoiseScale / Mathf.Max(1f, biomeSize); - float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.83f) * effectiveScale, (worldCell.y - seed * 0.79f) * effectiveScale); - int biomeIndex = Mathf.FloorToInt(Mathf.Clamp01(noise) * biomeCount); - return (byte)Mathf.Clamp(biomeIndex, 0, biomeCount - 1); - } - - private void RenderChunk(Vector2Int coord) - { - if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData || atlas == null) - { - return; - } - - runtime.EnsureCreated(coord, chunkRoot, chunkSize); - ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices); - runtime.ApplyRenderData(meshBuild.RenderSnapshot); - EnqueueColliderApply(coord, runtime.Version, runtime.RuntimeId, meshBuild.ColliderMesh); - runtime.State = ChunkState.Rendered; - MarkRegionDirty(coord); - } - - private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices) - { - MeshBuffers renderBuffers = new MeshBuffers(); - MeshBuffers colliderBuffers = new MeshBuffers(); - - BuildGroundSurface(heights, biomeIndices, renderBuffers); - BuildMountainTops(coord, heights, biomeIndices, renderBuffers, colliderBuffers); - BuildMountainSides(coord, heights, biomeIndices, renderBuffers, colliderBuffers); - - ChunkRenderSnapshot renderSnapshot = renderBuffers.ToSnapshot(); - return new ChunkMeshBuild(colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"), renderSnapshot); - } - - private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers) - { - bool[,] visited = new bool[chunkSize, chunkSize]; - for (int z = 0; z < chunkSize; z++) - { - for (int x = 0; x < chunkSize; x++) - { - int index = z * chunkSize + x; - if (visited[x, z] || heights[index] > 0) - { - continue; - } - - byte biomeIndex = biomeIndices[index]; - int width = 1; - while (x + width < chunkSize && !visited[x + width, z] && heights[z * chunkSize + x + width] == 0 && biomeIndices[z * chunkSize + x + width] == biomeIndex) - { - width++; - } - - int depth = 1; - bool canGrow = true; - while (z + depth < chunkSize && canGrow) - { - for (int ix = 0; ix < width; ix++) - { - int candidateIndex = (z + depth) * chunkSize + x + ix; - if (visited[x + ix, z + depth] || heights[candidateIndex] > 0 || biomeIndices[candidateIndex] != biomeIndex) - { - canGrow = false; - break; - } - } - - if (canGrow) - { - depth++; - } - } - - for (int dz = 0; dz < depth; dz++) - { - for (int dx = 0; dx < width; dx++) - { - visited[x + dx, z + dz] = true; - } - } - - AddTopQuad(renderBuffers, x, z, width, depth, 0f, atlas.GetTextureLayer(biomeIndex, VoxelSurfaceType.WalkableSurface)); - } - } - } - - private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers) - { - bool[,] visited = new bool[chunkSize, chunkSize]; - for (int z = 0; z < chunkSize; z++) - { - for (int x = 0; x < chunkSize; x++) - { - int height = heights[z * chunkSize + x]; - if (height <= 0 || visited[x, z]) - { - continue; - } - - int index = z * chunkSize + x; - VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; - byte biomeIndex = biomeIndices[index]; - - int width = 1; - while (x + width < chunkSize && !visited[x + width, z]) - { - int candidateIndex = z * chunkSize + x + width; - if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex) - { - break; - } - - VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + width, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; - if (candidateSurface != surfaceType) - { - break; - } - - width++; - } - - int depth = 1; - bool canGrow = true; - while (z + depth < chunkSize && canGrow) - { - for (int ix = 0; ix < width; ix++) - { - if (visited[x + ix, z + depth]) - { - canGrow = false; - break; - } - - int candidateIndex = (z + depth) * chunkSize + x + ix; - if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex) - { - canGrow = false; - break; - } - - VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + ix, z + depth), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface; - if (candidateSurface != surfaceType) - { - canGrow = false; - break; - } - } - - if (canGrow) - { - depth++; - } - } - - for (int dz = 0; dz < depth; dz++) - { - for (int dx = 0; dx < width; dx++) - { - visited[x + dx, z + dz] = true; - } - } - - int textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); - AddTopQuad(renderBuffers, x, z, width, depth, height, textureLayer); - AddTopQuad(colliderBuffers, x, z, width, depth, height, 0, false); - } - } - } - - private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers) - { - for (int z = 0; z < chunkSize; z++) - { - BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, true); - BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, false); - } - - for (int x = 0; x < chunkSize; x++) - { - BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, true); - BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, false); - } - } - - private void BuildNorthSouthFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int z, bool north) - { - bool[,] visited = new bool[chunkSize, maxMountainHeight]; - for (int x = 0; x < chunkSize; x++) - { - for (int y = 0; y < maxMountainHeight; y++) - { - if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)) - { - continue; - } - - int width = 1; - while (x + width < chunkSize && !visited[x + width, y] && TryGetNorthSouthFace(coord, heights, biomeIndices, x + width, z, y, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer) - { - width++; - } - - int height = 1; - bool canGrow = true; - while (y + height < maxMountainHeight && canGrow) - { - for (int dx = 0; dx < width; dx++) - { - if (visited[x + dx, y + height] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x + dx, z, y + height, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer) - { - canGrow = false; - break; - } - } - - if (canGrow) - { - height++; - } - } - - for (int dy = 0; dy < height; dy++) - { - for (int dx = 0; dx < width; dx++) - { - visited[x + dx, y + dy] = true; - } - } - - float faceZ = north ? z + 1f : z; - Vector3 bl = new Vector3(x, y, faceZ); - Vector3 br = new Vector3(x + width, y, faceZ); - Vector3 tr = new Vector3(x + width, y + height, faceZ); - Vector3 tl = new Vector3(x, y + height, faceZ); - - if (north) - { - AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.forward); - AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.forward, false); - } - else - { - AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.back); - AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.back, false); - } - } - } - } - - private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east) - { - bool[,] visited = new bool[chunkSize, maxMountainHeight]; - for (int z = 0; z < chunkSize; z++) - { - for (int y = 0; y < maxMountainHeight; y++) - { - if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)) - { - continue; - } - - int width = 1; - while (z + width < chunkSize && !visited[z + width, y] && TryGetEastWestFace(coord, heights, biomeIndices, x, z + width, y, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer) - { - width++; - } - - int height = 1; - bool canGrow = true; - while (y + height < maxMountainHeight && canGrow) - { - for (int dz = 0; dz < width; dz++) - { - if (visited[z + dz, y + height] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z + dz, y + height, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer) - { - canGrow = false; - break; - } - } - - if (canGrow) - { - height++; - } - } - - for (int dy = 0; dy < height; dy++) - { - for (int dz = 0; dz < width; dz++) - { - visited[z + dz, y + dy] = true; - } - } - - float faceX = east ? x + 1f : x; - Vector3 bl = new Vector3(faceX, y, z); - Vector3 br = new Vector3(faceX, y, z + width); - Vector3 tr = new Vector3(faceX, y + height, z + width); - Vector3 tl = new Vector3(faceX, y + height, z); - - if (east) - { - AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.right); - AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.right, false); - } - else - { - AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.left); - AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.left, false); - } - } - } - } - - private bool TryGetNorthSouthFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer) - { - int current = heights[z * chunkSize + x]; - int worldX = coord.x * chunkSize + x; - int worldZ = coord.y * chunkSize + z; - int neighbor = GetHeightAtWorldCell(new Vector2Int(worldX, north ? worldZ + 1 : worldZ - 1)); - if (y >= current || y < neighbor) - { - biomeIndex = 0; - surfaceType = VoxelSurfaceType.Dirt; - textureLayer = 0; - return false; - } - - biomeIndex = biomeIndices[z * chunkSize + x]; - surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt; - textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); - return true; - } - - private bool TryGetEastWestFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer) - { - int current = heights[z * chunkSize + x]; - int worldX = coord.x * chunkSize + x; - int worldZ = coord.y * chunkSize + z; - int neighbor = GetHeightAtWorldCell(new Vector2Int(east ? worldX + 1 : worldX - 1, worldZ)); - if (y >= current || y < neighbor) - { - biomeIndex = 0; - surfaceType = VoxelSurfaceType.Dirt; - textureLayer = 0; - return false; - } - - biomeIndex = biomeIndices[z * chunkSize + x]; - surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt; - textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType); - return true; - } - - private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, int textureLayer, bool withUv = true) - { - int baseIndex = buffers.Vertices.Count; - buffers.Vertices.Add(new Vector3(x, height, z)); - buffers.Vertices.Add(new Vector3(x + width, height, z)); - buffers.Vertices.Add(new Vector3(x + width, height, z + depth)); - buffers.Vertices.Add(new Vector3(x, height, z + depth)); - buffers.Normals.Add(Vector3.up); - buffers.Normals.Add(Vector3.up); - buffers.Normals.Add(Vector3.up); - buffers.Normals.Add(Vector3.up); - - if (withUv) - { - buffers.Uvs.Add(new Vector2(0f, 0f)); - buffers.Uvs.Add(new Vector2(width, 0f)); - buffers.Uvs.Add(new Vector2(width, depth)); - buffers.Uvs.Add(new Vector2(0f, depth)); - Vector2 textureData = new Vector2(textureLayer, 0f); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - } - - buffers.Triangles.Add(baseIndex); - buffers.Triangles.Add(baseIndex + 2); - buffers.Triangles.Add(baseIndex + 1); - buffers.Triangles.Add(baseIndex); - buffers.Triangles.Add(baseIndex + 3); - buffers.Triangles.Add(baseIndex + 2); - } - - private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, float width, float height, int textureLayer, Vector3 normal, bool withUv = true) - { - int baseIndex = buffers.Vertices.Count; - buffers.Vertices.Add(bottomLeft); - buffers.Vertices.Add(bottomRight); - buffers.Vertices.Add(topRight); - buffers.Vertices.Add(topLeft); - buffers.Normals.Add(normal); - buffers.Normals.Add(normal); - buffers.Normals.Add(normal); - buffers.Normals.Add(normal); - - if (withUv) - { - buffers.Uvs.Add(new Vector2(0f, 0f)); - buffers.Uvs.Add(new Vector2(width, 0f)); - buffers.Uvs.Add(new Vector2(width, height)); - buffers.Uvs.Add(new Vector2(0f, height)); - Vector2 textureData = new Vector2(textureLayer, 0f); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - buffers.TextureData.Add(textureData); - } - - buffers.Triangles.Add(baseIndex); - buffers.Triangles.Add(baseIndex + 1); - buffers.Triangles.Add(baseIndex + 2); - buffers.Triangles.Add(baseIndex); - buffers.Triangles.Add(baseIndex + 2); - buffers.Triangles.Add(baseIndex + 3); - } - - private bool IsCliffTop(Vector2Int worldCell, int currentHeight) - { - return GetHeightAtWorldCell(worldCell + Vector2Int.up) < currentHeight || - GetHeightAtWorldCell(worldCell + Vector2Int.right) < currentHeight || - GetHeightAtWorldCell(worldCell + Vector2Int.down) < currentHeight || - GetHeightAtWorldCell(worldCell + Vector2Int.left) < currentHeight; - } - - private int GetHeightAtWorldCell(Vector2Int worldCell) - { - Vector2Int coord = new Vector2Int( - Mathf.FloorToInt(worldCell.x / (float)chunkSize), - Mathf.FloorToInt(worldCell.y / (float)chunkSize)); - - if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData) - { - return SampleRock(worldCell) ? SampleHeight(worldCell) : 0; - } - - int localX = worldCell.x - coord.x * chunkSize; - int localZ = worldCell.y - coord.y * chunkSize; - if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize) - { - return SampleRock(worldCell) ? SampleHeight(worldCell) : 0; - } - - return runtime.Heights[localZ * chunkSize + localX]; - } - private Vector2Int WorldToChunk(Vector3 position) { return new Vector2Int( @@ -905,11 +295,6 @@ namespace InfiniteWorld.VoxelWorld Mathf.FloorToInt(position.z / chunkSize)); } - private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localZ) - { - return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localZ); - } - private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version, int session, int runtimeId) { try @@ -1500,335 +885,5 @@ namespace InfiniteWorld.VoxelWorld return Mathf.Max(1, count); } - private struct ChunkBuildResult - { - public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version, int session, int runtimeId) - { - Coord = coord; - Heights = heights; - BiomeIndices = biomeIndices; - Version = version; - Session = session; - RuntimeId = runtimeId; - } - - public Vector2Int Coord { get; } - public int[] Heights { get; } - public byte[] BiomeIndices { get; } - public int Version { get; } - public int Session { get; } - public int RuntimeId { get; } - } - - private struct ChunkMeshBuild - { - public ChunkMeshBuild(Mesh colliderMesh, ChunkRenderSnapshot renderSnapshot) - { - ColliderMesh = colliderMesh; - RenderSnapshot = renderSnapshot; - } - - public Mesh ColliderMesh { get; } - public ChunkRenderSnapshot RenderSnapshot { get; } - } - - private struct PendingColliderMeshApply - { - public PendingColliderMeshApply(Vector2Int coord, int version, int runtimeId, Mesh colliderMesh) - { - Coord = coord; - Version = version; - RuntimeId = runtimeId; - ColliderMesh = colliderMesh; - } - - public Vector2Int Coord { get; } - public int Version { get; } - public int RuntimeId { get; } - public Mesh ColliderMesh { get; } - } - - private sealed class ChunkRenderSnapshot - { - public ChunkRenderSnapshot(Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles) - { - Vertices = vertices; - Normals = normals; - Uv0 = uv0; - Uv1 = uv1; - Triangles = triangles; - } - - public Vector3[] Vertices { get; } - public Vector3[] Normals { get; } - public Vector2[] Uv0 { get; } - public Vector2[] Uv1 { get; } - public int[] Triangles { get; } - public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0; - } - - private struct RegionChunkSnapshot - { - public RegionChunkSnapshot(ChunkRenderSnapshot snapshot, Vector3 localOffset) - { - Snapshot = snapshot; - LocalOffset = localOffset; - } - - public ChunkRenderSnapshot Snapshot { get; } - public Vector3 LocalOffset { get; } - } - - private struct RegionBuildRequest - { - public RegionBuildRequest(Vector2Int regionCoord, int version, int session, RegionChunkSnapshot[] chunks) - { - RegionCoord = regionCoord; - Version = version; - Session = session; - Chunks = chunks; - } - - public Vector2Int RegionCoord { get; } - public int Version { get; } - public int Session { get; } - public RegionChunkSnapshot[] Chunks { get; } - } - - private struct RegionBuildResult - { - public RegionBuildResult(Vector2Int regionCoord, int version, int session, Vector3[] vertices, Vector3[] normals, Vector2[] uv0, Vector2[] uv1, int[] triangles, Bounds bounds) - { - RegionCoord = regionCoord; - Version = version; - Session = session; - Vertices = vertices; - Normals = normals; - Uv0 = uv0; - Uv1 = uv1; - Triangles = triangles; - Bounds = bounds; - } - - public Vector2Int RegionCoord { get; } - public int Version { get; } - public int Session { get; } - public Vector3[] Vertices { get; } - public Vector3[] Normals { get; } - public Vector2[] Uv0 { get; } - public Vector2[] Uv1 { get; } - public int[] Triangles { get; } - public Bounds Bounds { get; } - public bool IsEmpty => Vertices == null || Vertices.Length == 0 || Triangles == null || Triangles.Length == 0; - - public static RegionBuildResult CreateEmpty(Vector2Int regionCoord, int version, int session) - { - return new RegionBuildResult(regionCoord, version, session, null, null, null, null, null, new Bounds()); - } - } - - private sealed class MeshBuffers - { - public readonly List Vertices = new List(512); - public readonly List Normals = new List(512); - public readonly List Uvs = new List(512); - public readonly List TextureData = new List(512); - public readonly List Triangles = new List(1024); - - public Mesh ToMesh(string meshName) - { - Mesh mesh = new Mesh { name = meshName }; - if (Vertices.Count == 0) - { - return mesh; - } - - mesh.indexFormat = Vertices.Count > 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; - mesh.SetVertices(Vertices); - if (Normals.Count == Vertices.Count) - { - mesh.SetNormals(Normals); - } - if (Uvs.Count == Vertices.Count) - { - mesh.SetUVs(0, Uvs); - } - if (TextureData.Count == Vertices.Count) - { - mesh.SetUVs(1, TextureData); - } - mesh.SetTriangles(Triangles, 0, true); - mesh.RecalculateBounds(); - return mesh; - } - - public ChunkRenderSnapshot ToSnapshot() - { - return new ChunkRenderSnapshot( - Vertices.ToArray(), - Normals.ToArray(), - Uvs.ToArray(), - TextureData.ToArray(), - Triangles.ToArray()); - } - } - - private sealed class ChunkRuntime - { - public Transform Root; - public MeshCollider MountainCollider; - public BoxCollider GroundCollider; - public Mesh ColliderMesh; - public ChunkRenderSnapshot RenderSnapshot; - public int[] Heights; - public byte[] BiomeIndices; - public ChunkState State; - public int Version; - public int RuntimeId; - - public bool HasData => Heights != null && BiomeIndices != null; - public bool IsRendered => State == ChunkState.Rendered && Root != null; - - 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; - MountainCollider = chunkObject.AddComponent(); - GroundCollider = chunkObject.AddComponent(); - } - - Root.localPosition = new Vector3(coord.x * chunkSize, 0f, coord.y * chunkSize); - GroundCollider.size = new Vector3(chunkSize, 0.2f, chunkSize); - GroundCollider.center = new Vector3(chunkSize * 0.5f, -0.1f, chunkSize * 0.5f); - } - - public void ApplyRenderData(ChunkRenderSnapshot renderSnapshot) - { - RenderSnapshot = renderSnapshot; - } - - public void ApplyColliderMesh(Mesh colliderMesh) - { - if (ColliderMesh != null) - { - DestroyMesh(ColliderMesh); - } - - ColliderMesh = colliderMesh; - MountainCollider.sharedMesh = null; - MountainCollider.sharedMesh = ColliderMesh != null && ColliderMesh.vertexCount > 0 ? ColliderMesh : null; - } - - public void Dispose() - { - if (Root != null) - { - if (Application.isPlaying) - { - UnityEngine.Object.Destroy(Root.gameObject); - } - else - { - UnityEngine.Object.DestroyImmediate(Root.gameObject); - } - } - - DestroyMesh(ColliderMesh); - RenderSnapshot = null; - } - - private static void DestroyMesh(Mesh mesh) - { - if (mesh == null) - { - return; - } - - if (Application.isPlaying) - { - UnityEngine.Object.Destroy(mesh); - } - else - { - UnityEngine.Object.DestroyImmediate(mesh); - } - } - } - - 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, - Generating, - SyncBuilding, - ReadyToRender, - Rendered - } } }