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> uvLookup; public VoxelWorldAtlas(Texture2D texture, Material material, Dictionary> 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 biomeRects) && biomeRects.TryGetValue(surfaceType, out Rect rect)) { return rect; } return uvLookup[0][surfaceType]; } public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList biomeProfiles) { List sourceBiomes = new List(); 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> uvRects = new Dictionary>(sourceBiomes.Count); for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++) { Dictionary biomeRects = new Dictionary(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 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)); } } } } }