[Add] Voxel World Generator

This commit is contained in:
2026-03-31 09:02:06 +07:00
parent c22c08753a
commit 122d7e55c2
58 changed files with 3942 additions and 2037 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a6692637cf5eb1b41a9d619e7557b2f0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
{
"name": "VoxelWorld.Runtime",
"rootNamespace": "InfiniteWorld.VoxelWorld",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f0a085d6765d01448bb424934e24ed9c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
public enum VoxelSurfaceType : byte
{
CliffTop,
CliffSide,
Dirt,
WalkableSurface
}
[CreateAssetMenu(menuName = "Infinite World/Voxel Biome Profile", fileName = "VoxelBiomeProfile")]
public sealed class VoxelBiomeProfile : ScriptableObject
{
public Sprite cliffTopSprite;
public Sprite cliffSideSprite;
public Sprite dirtSprite;
public Sprite walkableSurfaceSprite;
public Sprite GetSprite(VoxelSurfaceType surfaceType)
{
return surfaceType switch
{
VoxelSurfaceType.CliffTop => cliffTopSprite,
VoxelSurfaceType.CliffSide => cliffSideSprite,
VoxelSurfaceType.Dirt => dirtSprite,
VoxelSurfaceType.WalkableSurface => walkableSurfaceSprite,
_ => walkableSurfaceSprite
};
}
public bool HasAnyAssignedSprites()
{
return cliffTopSprite != null || cliffSideSprite != null || dirtSprite != null || walkableSurfaceSprite != null;
}
}
internal sealed class VoxelWorldAtlas : IDisposable
{
private const int FallbackTileSize = 32;
private static readonly VoxelSurfaceType[] SurfaceOrder =
{
VoxelSurfaceType.CliffTop,
VoxelSurfaceType.CliffSide,
VoxelSurfaceType.Dirt,
VoxelSurfaceType.WalkableSurface
};
private readonly Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvLookup;
public VoxelWorldAtlas(Texture2D texture, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvRects)
{
Texture = texture;
Material = material;
uvLookup = uvRects;
}
public Texture2D Texture { get; }
public Material Material { get; }
public int BiomeCount => uvLookup.Count;
public Rect GetUvRect(int biomeIndex, VoxelSurfaceType surfaceType)
{
if (uvLookup.TryGetValue(biomeIndex, out Dictionary<VoxelSurfaceType, Rect> biomeRects) && biomeRects.TryGetValue(surfaceType, out Rect rect))
{
return rect;
}
return uvLookup[0][surfaceType];
}
public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
{
List<VoxelBiomeProfile> sourceBiomes = new List<VoxelBiomeProfile>();
if (biomeProfiles != null)
{
for (int i = 0; i < biomeProfiles.Count; i++)
{
if (biomeProfiles[i] != null)
{
sourceBiomes.Add(biomeProfiles[i]);
}
}
}
if (sourceBiomes.Count == 0)
{
sourceBiomes.Add(null);
}
int tileSize = DetermineTileSize(sourceBiomes);
Texture2D texture = new Texture2D(tileSize * SurfaceOrder.Length, tileSize * sourceBiomes.Count, TextureFormat.RGBA32, false)
{
filterMode = FilterMode.Point,
wrapMode = TextureWrapMode.Clamp,
name = "VoxelWorld_RuntimeAtlas",
hideFlags = HideFlags.HideAndDontSave
};
Fill(texture, new Color(1f, 0f, 1f, 1f));
Dictionary<int, Dictionary<VoxelSurfaceType, Rect>> uvRects = new Dictionary<int, Dictionary<VoxelSurfaceType, Rect>>(sourceBiomes.Count);
for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++)
{
Dictionary<VoxelSurfaceType, Rect> biomeRects = new Dictionary<VoxelSurfaceType, Rect>(SurfaceOrder.Length);
uvRects[biomeIndex] = biomeRects;
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
{
VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex];
RectInt rect = new RectInt(surfaceIndex * tileSize, biomeIndex * tileSize, tileSize, tileSize);
DrawSurfaceTile(texture, rect, sourceBiomes[biomeIndex], surfaceType, tileSize);
biomeRects[surfaceType] = BuildUvRect(rect, texture.width, texture.height);
}
}
texture.Apply(false, false);
Shader shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
Material material = new Material(shader)
{
name = "VoxelWorld_RuntimeMaterial",
hideFlags = HideFlags.HideAndDontSave
};
if (material.HasProperty("_BaseMap"))
{
material.SetTexture("_BaseMap", texture);
}
if (material.HasProperty("_MainTex"))
{
material.SetTexture("_MainTex", texture);
}
if (material.HasProperty("_BaseColor"))
{
material.SetColor("_BaseColor", Color.white);
}
if (material.HasProperty("_Color"))
{
material.SetColor("_Color", Color.white);
}
return new VoxelWorldAtlas(texture, material, uvRects);
}
public void Dispose()
{
if (Application.isPlaying)
{
UnityEngine.Object.Destroy(Texture);
UnityEngine.Object.Destroy(Material);
}
else
{
UnityEngine.Object.DestroyImmediate(Texture);
UnityEngine.Object.DestroyImmediate(Material);
}
}
private static int DetermineTileSize(IReadOnlyList<VoxelBiomeProfile> biomeProfiles)
{
int tileSize = FallbackTileSize;
for (int biomeIndex = 0; biomeIndex < biomeProfiles.Count; biomeIndex++)
{
VoxelBiomeProfile biome = biomeProfiles[biomeIndex];
for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++)
{
Sprite sprite = biome != null ? biome.GetSprite(SurfaceOrder[surfaceIndex]) : null;
if (sprite == null)
{
continue;
}
Rect rect = sprite.textureRect;
tileSize = Mathf.Max(tileSize, Mathf.CeilToInt(Mathf.Max(rect.width, rect.height)));
}
}
return Mathf.Max(FallbackTileSize, tileSize);
}
private static Rect BuildUvRect(RectInt rect, int textureWidth, int textureHeight)
{
float paddingX = 0.5f / textureWidth;
float paddingY = 0.5f / textureHeight;
return new Rect(
rect.xMin / (float)textureWidth + paddingX,
rect.yMin / (float)textureHeight + paddingY,
rect.width / (float)textureWidth - paddingX * 2f,
rect.height / (float)textureHeight - paddingY * 2f);
}
private static void DrawSurfaceTile(Texture2D atlas, RectInt targetRect, VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize)
{
Sprite sprite = biome != null ? biome.GetSprite(surfaceType) : null;
if (sprite != null && TryBlitSprite(atlas, targetRect, sprite))
{
return;
}
DrawFallbackSurface(atlas, targetRect, surfaceType);
}
private static bool TryBlitSprite(Texture2D atlas, RectInt targetRect, Sprite sprite)
{
try
{
Texture2D source = sprite.texture;
Rect spriteRect = sprite.textureRect;
for (int y = 0; y < targetRect.height; y++)
{
float sampleY = Mathf.Lerp(spriteRect.yMin, spriteRect.yMax - 1f, y / (float)Mathf.Max(1, targetRect.height - 1));
int sourceY = Mathf.Clamp(Mathf.RoundToInt(sampleY), 0, source.height - 1);
for (int x = 0; x < targetRect.width; x++)
{
float sampleX = Mathf.Lerp(spriteRect.xMin, spriteRect.xMax - 1f, x / (float)Mathf.Max(1, targetRect.width - 1));
int sourceX = Mathf.Clamp(Mathf.RoundToInt(sampleX), 0, source.width - 1);
atlas.SetPixel(targetRect.x + x, targetRect.y + y, source.GetPixel(sourceX, sourceY));
}
}
return true;
}
catch (UnityException)
{
Debug.LogWarning($"Sprite '{sprite.name}' texture is not readable. Falling back to generated tile for voxel biome atlas.");
return false;
}
}
private static void DrawFallbackSurface(Texture2D texture, RectInt rect, VoxelSurfaceType surfaceType)
{
Color baseColor;
Color accentColor;
switch (surfaceType)
{
case VoxelSurfaceType.CliffTop:
baseColor = new Color(0.40f, 0.72f, 0.28f, 1f);
accentColor = new Color(0.18f, 0.35f, 0.13f, 1f);
break;
case VoxelSurfaceType.CliffSide:
baseColor = new Color(0.47f, 0.34f, 0.22f, 1f);
accentColor = new Color(0.25f, 0.52f, 0.18f, 1f);
break;
case VoxelSurfaceType.Dirt:
baseColor = new Color(0.44f, 0.31f, 0.20f, 1f);
accentColor = new Color(0.31f, 0.20f, 0.12f, 1f);
break;
default:
baseColor = new Color(0.28f, 0.58f, 0.25f, 1f);
accentColor = new Color(0.18f, 0.40f, 0.16f, 1f);
break;
}
FillRect(texture, rect, baseColor);
ApplyNoise(texture, rect, 0.06f, 101 + (int)surfaceType * 43);
if (surfaceType == VoxelSurfaceType.CliffSide)
{
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 6), rect.width, Mathf.Max(4, rect.height / 6)), accentColor);
}
else if (surfaceType == VoxelSurfaceType.CliffTop)
{
FillRect(texture, new RectInt(rect.x, rect.yMax - Mathf.Max(4, rect.height / 5), rect.width, Mathf.Max(4, rect.height / 5)), accentColor);
}
else
{
for (int y = rect.y + 4; y < rect.yMax - 4; y += Mathf.Max(6, rect.height / 4))
{
for (int x = rect.x + 4; x < rect.xMax - 4; x += Mathf.Max(6, rect.width / 4))
{
DrawDisc(texture, x, y, 2, accentColor);
}
}
}
}
private static void Fill(Texture2D texture, Color color)
{
Color[] pixels = new Color[texture.width * texture.height];
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = color;
}
texture.SetPixels(pixels);
}
private static void FillRect(Texture2D texture, RectInt rect, Color color)
{
for (int y = rect.yMin; y < rect.yMax; y++)
{
for (int x = rect.xMin; x < rect.xMax; x++)
{
texture.SetPixel(x, y, color);
}
}
}
private static void DrawDisc(Texture2D texture, int centerX, int centerY, int radius, Color color)
{
int sqrRadius = radius * radius;
for (int y = -radius; y <= radius; y++)
{
for (int x = -radius; x <= radius; x++)
{
if (x * x + y * y > sqrRadius)
{
continue;
}
texture.SetPixel(centerX + x, centerY + y, color);
}
}
}
private static void ApplyNoise(Texture2D texture, RectInt rect, float strength, int seed)
{
for (int y = rect.yMin; y < rect.yMax; y++)
{
for (int x = rect.xMin; x < rect.xMax; x++)
{
Color pixel = texture.GetPixel(x, y);
float noise = Mathf.PerlinNoise((x + seed) * 0.18f, (y - seed) * 0.18f) - 0.5f;
texture.SetPixel(x, y, pixel * (1f + noise * strength * 2f));
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 981858f28439459429fa291e1f6cb935
@@ -0,0 +1,958 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace InfiniteWorld.VoxelWorld
{
public sealed class VoxelWorldGenerator : MonoBehaviour
{
[Header("Streaming")]
[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<VoxelBiomeProfile> biomeProfiles = new List<VoxelBiomeProfile>();
[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;
private readonly Dictionary<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
private readonly object generationLock = new object();
private Transform chunkRoot;
private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
private int activeGenerationJobs;
private VoxelWorldAtlas atlas;
private int atlasBiomeCount;
private void Awake()
{
EnsureRuntimeData();
EnsureChunkRoot();
TryResolveStreamTarget();
}
private void Update()
{
EnsureRuntimeData();
EnsureChunkRoot();
if (!TryResolveStreamTarget())
{
return;
}
Vector2Int centerChunk = WorldToChunk(streamTarget.position);
if (centerChunk != lastGeneratedCenter)
{
lastGeneratedCenter = centerChunk;
UnloadDistantChunks(centerChunk);
}
DrainCompletedBuilds(maxChunkBuildsPerFrame);
ScheduleChunkGeneration(centerChunk);
EnsureBlockingChunksLoaded(centerChunk);
}
private void OnDisable()
{
CleanupChunks();
atlas?.Dispose();
atlas = null;
}
private void EnsureRuntimeData()
{
maxMountainHeight = Mathf.Max(1, maxMountainHeight);
chunkSize = Mathf.Max(8, chunkSize);
int configuredBiomeCount = CountConfiguredBiomes();
if (atlas != null && atlasBiomeCount == configuredBiomeCount)
{
return;
}
atlas?.Dispose();
atlas = VoxelWorldAtlas.CreateRuntimeAtlas(biomeProfiles);
atlasBiomeCount = atlas.BiomeCount;
}
private void EnsureChunkRoot()
{
if (chunkRoot != null)
{
return;
}
Transform existing = transform.Find("VoxelChunks");
if (existing != null)
{
chunkRoot = existing;
return;
}
GameObject root = new GameObject("VoxelChunks");
root.transform.SetParent(transform, false);
chunkRoot = root.transform;
}
private bool TryResolveStreamTarget()
{
if (streamTarget != null)
{
return true;
}
Camera mainCamera = Camera.main;
if (mainCamera == null)
{
return false;
}
streamTarget = mainCamera.transform;
return true;
}
private void ScheduleChunkGeneration(Vector2Int centerChunk)
{
List<Vector2Int> coords = GetCoordsByPriority(centerChunk, generationRadius);
for (int i = 0; i < coords.Count; i++)
{
Vector2Int coord = coords[i];
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating)
{
continue;
}
if (IsWithinRadius(coord, centerChunk, blockingGenerationRadius))
{
continue;
}
if (!TryReserveGenerationSlot())
{
break;
}
runtime.State = ChunkState.Generating;
runtime.Version++;
GenerateChunkDataAsync(coord, runtime.Version).Forget();
}
}
private void EnsureBlockingChunksLoaded(Vector2Int centerChunk)
{
List<Vector2Int> requiredCoords = GetCoordsByPriority(centerChunk, blockingGenerationRadius);
for (int i = 0; i < requiredCoords.Count; i++)
{
Vector2Int coord = requiredCoords[i];
ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
if (runtime.IsRendered)
{
continue;
}
if (runtime.State == ChunkState.Generating)
{
continue;
}
runtime.Version++;
runtime.State = ChunkState.SyncBuilding;
ApplyBuildResult(GenerateChunkData(coord, runtime.Version));
RenderChunk(coord);
RefreshNeighborBorders(coord);
}
}
private void UnloadDistantChunks(Vector2Int centerChunk)
{
List<Vector2Int> coordsToRemove = new List<Vector2Int>();
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
{
if (IsWithinRadius(pair.Key, centerChunk, generationRadius))
{
continue;
}
coordsToRemove.Add(pair.Key);
}
for (int i = 0; i < coordsToRemove.Count; i++)
{
Vector2Int coord = coordsToRemove[i];
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
{
continue;
}
chunks.Remove(coord);
runtime.Dispose();
RefreshNeighborBorders(coord);
}
}
private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version)
{
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);
}
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, atlas.Material);
ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices);
runtime.ApplyMeshes(meshBuild, chunkSize);
runtime.State = ChunkState.Rendered;
}
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);
return new ChunkMeshBuild(renderBuffers.ToMesh($"Render_{coord.x}_{coord.y}"), colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"));
}
private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers)
{
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
if (heights[z * chunkSize + x] > 0)
{
continue;
}
int index = z * chunkSize + x;
AddTopQuad(renderBuffers, x, z, 1f, 1f, 0f, atlas.GetUvRect(biomeIndices[index], VoxelSurfaceType.WalkableSurface));
}
}
}
private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
{
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
int height = heights[z * chunkSize + x];
if (height <= 0)
{
continue;
}
int index = z * chunkSize + x;
VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
Rect uvRect = atlas.GetUvRect(biomeIndices[index], surfaceType);
AddTopQuad(renderBuffers, x, z, 1f, 1f, height, uvRect);
AddTopQuad(colliderBuffers, x, z, 1f, 1f, height, Rect.zero, false);
}
}
}
private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
{
for (int z = 0; z < chunkSize; z++)
{
for (int x = 0; x < chunkSize; x++)
{
int current = heights[z * chunkSize + x];
if (current <= 0)
{
continue;
}
int worldX = coord.x * chunkSize + x;
int worldZ = coord.y * chunkSize + z;
byte biomeIndex = biomeIndices[z * chunkSize + x];
AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ + 1)), biomeIndex, true);
AddNorthSouthFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX, worldZ - 1)), biomeIndex, false);
AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX + 1, worldZ)), biomeIndex, true);
AddEastWestFace(renderBuffers, colliderBuffers, x, z, current, GetHeightAtWorldCell(new Vector2Int(worldX - 1, worldZ)), biomeIndex, false);
}
}
}
private void AddNorthSouthFace(MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, int z, int current, int neighbor, byte biomeIndex, bool north)
{
if (current <= neighbor)
{
return;
}
int bottom = Mathf.Min(current, neighbor);
float faceZ = north ? z + 1f : z;
for (int y = bottom; y < current; y++)
{
VoxelSurfaceType surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
Rect uvRect = atlas.GetUvRect(biomeIndex, surfaceType);
Vector3 bl = new Vector3(x, y, faceZ);
Vector3 br = new Vector3(x + 1f, y, faceZ);
Vector3 tr = new Vector3(x + 1f, y + 1f, faceZ);
Vector3 tl = new Vector3(x, y + 1f, faceZ);
if (north)
{
AddVerticalQuad(renderBuffers, bl, br, tr, tl, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, Rect.zero, 1f, 1f, false);
}
else
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, Rect.zero, 1f, 1f, false);
}
}
}
private void AddEastWestFace(MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, int z, int current, int neighbor, byte biomeIndex, bool east)
{
if (current <= neighbor)
{
return;
}
int bottom = Mathf.Min(current, neighbor);
float faceX = east ? x + 1f : x;
for (int y = bottom; y < current; y++)
{
VoxelSurfaceType surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
Rect uvRect = atlas.GetUvRect(biomeIndex, surfaceType);
Vector3 bl = new Vector3(faceX, y, z);
Vector3 br = new Vector3(faceX, y, z + 1f);
Vector3 tr = new Vector3(faceX, y + 1f, z + 1f);
Vector3 tl = new Vector3(faceX, y + 1f, z);
if (east)
{
AddVerticalQuad(renderBuffers, br, bl, tl, tr, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, br, bl, tl, tr, Rect.zero, 1f, 1f, false);
}
else
{
AddVerticalQuad(renderBuffers, bl, br, tr, tl, uvRect, 1f, 1f);
AddVerticalQuad(colliderBuffers, bl, br, tr, tl, Rect.zero, 1f, 1f, false);
}
}
}
private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, Rect uvRect, 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));
if (withUv)
{
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMin));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax));
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax));
}
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, Rect uvRect, float width, float height, bool withUv = true)
{
int baseIndex = buffers.Vertices.Count;
buffers.Vertices.Add(bottomLeft);
buffers.Vertices.Add(bottomRight);
buffers.Vertices.Add(topRight);
buffers.Vertices.Add(topLeft);
if (withUv)
{
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMin));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMin));
buffers.Uvs.Add(new Vector2(uvRect.xMax, uvRect.yMax));
buffers.Uvs.Add(new Vector2(uvRect.xMin, uvRect.yMax));
}
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(
Mathf.FloorToInt(position.x / chunkSize),
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)
{
try
{
ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version));
lock (generationLock)
{
completedBuilds.Enqueue(result);
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
}
}
catch (Exception exception)
{
lock (generationLock)
{
activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
}
Debug.LogException(exception, this);
}
}
private void DrainCompletedBuilds(int maxBuilds)
{
int builds = 0;
while (builds < maxBuilds)
{
ChunkBuildResult result;
lock (generationLock)
{
if (completedBuilds.Count == 0)
{
break;
}
result = completedBuilds.Dequeue();
}
if (!ApplyBuildResult(result))
{
continue;
}
RenderChunk(result.Coord);
RefreshNeighborBorders(result.Coord);
builds++;
}
}
private void RefreshNeighborBorders(Vector2Int coord)
{
TryRenderNeighbor(coord + Vector2Int.up);
TryRenderNeighbor(coord + Vector2Int.right);
TryRenderNeighbor(coord + Vector2Int.down);
TryRenderNeighbor(coord + Vector2Int.left);
}
private void TryRenderNeighbor(Vector2Int coord)
{
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
{
return;
}
RenderChunk(coord);
}
private bool ApplyBuildResult(ChunkBuildResult result)
{
if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime))
{
return false;
}
if (runtime.Version != result.Version)
{
return false;
}
runtime.Heights = result.Heights;
runtime.BiomeIndices = result.BiomeIndices;
if (!runtime.IsRendered)
{
runtime.State = ChunkState.ReadyToRender;
}
return true;
}
private bool TryReserveGenerationSlot()
{
lock (generationLock)
{
if (activeGenerationJobs >= maxAsyncChunkJobs)
{
return false;
}
activeGenerationJobs++;
return true;
}
}
private List<Vector2Int> GetCoordsByPriority(Vector2Int centerChunk, int radius)
{
List<Vector2Int> coords = new List<Vector2Int>((radius * 2 + 1) * (radius * 2 + 1));
for (int z = -radius; z <= radius; z++)
{
for (int x = -radius; x <= radius; x++)
{
coords.Add(new Vector2Int(centerChunk.x + x, centerChunk.y + z));
}
}
coords.Sort((left, right) => CompareChunkPriority(centerChunk, left, right));
return coords;
}
private static int CompareChunkPriority(Vector2Int centerChunk, Vector2Int left, Vector2Int right)
{
int leftDx = Mathf.Abs(left.x - centerChunk.x);
int leftDz = Mathf.Abs(left.y - centerChunk.y);
int rightDx = Mathf.Abs(right.x - centerChunk.x);
int rightDz = Mathf.Abs(right.y - centerChunk.y);
int leftChebyshev = Mathf.Max(leftDx, leftDz);
int rightChebyshev = Mathf.Max(rightDx, rightDz);
int chebyshevCompare = leftChebyshev.CompareTo(rightChebyshev);
if (chebyshevCompare != 0)
{
return chebyshevCompare;
}
int leftDistance = leftDx * leftDx + leftDz * leftDz;
int rightDistance = rightDx * rightDx + rightDz * rightDz;
int distanceCompare = leftDistance.CompareTo(rightDistance);
if (distanceCompare != 0)
{
return distanceCompare;
}
int zCompare = left.y.CompareTo(right.y);
return zCompare != 0 ? zCompare : left.x.CompareTo(right.x);
}
private ChunkRuntime GetOrCreateChunkRuntime(Vector2Int coord)
{
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
{
runtime = new ChunkRuntime();
chunks.Add(coord, runtime);
}
return runtime;
}
private static bool IsWithinRadius(Vector2Int coord, Vector2Int centerChunk, int radius)
{
int dx = Mathf.Abs(coord.x - centerChunk.x);
int dz = Mathf.Abs(coord.y - centerChunk.y);
return dx <= radius && dz <= radius;
}
private void CleanupChunks()
{
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
{
pair.Value.Dispose();
}
chunks.Clear();
}
private int CountConfiguredBiomes()
{
int count = 0;
for (int i = 0; i < biomeProfiles.Count; i++)
{
if (biomeProfiles[i] != null)
{
count++;
}
}
return Mathf.Max(1, count);
}
private readonly struct ChunkBuildResult
{
public ChunkBuildResult(Vector2Int coord, int[] heights, byte[] biomeIndices, int version)
{
Coord = coord;
Heights = heights;
BiomeIndices = biomeIndices;
Version = version;
}
public Vector2Int Coord { get; }
public int[] Heights { get; }
public byte[] BiomeIndices { get; }
public int Version { get; }
}
private readonly struct ChunkMeshBuild
{
public ChunkMeshBuild(Mesh renderMesh, Mesh colliderMesh)
{
RenderMesh = renderMesh;
ColliderMesh = colliderMesh;
}
public Mesh RenderMesh { get; }
public Mesh ColliderMesh { get; }
}
private sealed class MeshBuffers
{
public readonly List<Vector3> Vertices = new List<Vector3>(512);
public readonly List<Vector2> Uvs = new List<Vector2>(512);
public readonly List<int> Triangles = new List<int>(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 (Uvs.Count == Vertices.Count)
{
mesh.SetUVs(0, Uvs);
}
mesh.SetTriangles(Triangles, 0, true);
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
}
private sealed class ChunkRuntime
{
public Transform Root;
public MeshFilter Filter;
public MeshRenderer Renderer;
public MeshCollider MountainCollider;
public BoxCollider GroundCollider;
public Mesh RenderMesh;
public Mesh ColliderMesh;
public int[] Heights;
public byte[] BiomeIndices;
public ChunkState State;
public int Version;
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)
{
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);
}
public void ApplyMeshes(ChunkMeshBuild build, int chunkSize)
{
if (RenderMesh != null)
{
DestroyMesh(RenderMesh);
}
if (ColliderMesh != null)
{
DestroyMesh(ColliderMesh);
}
RenderMesh = build.RenderMesh;
ColliderMesh = build.ColliderMesh;
Filter.sharedMesh = RenderMesh;
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(RenderMesh);
DestroyMesh(ColliderMesh);
}
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
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 62cd563780165844caddc098f92ff23f