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