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 const string ShaderName = "Infinite World/VoxelWorld/TextureArrayUnlit"; private static readonly VoxelSurfaceType[] SurfaceOrder = { VoxelSurfaceType.CliffTop, VoxelSurfaceType.CliffSide, VoxelSurfaceType.Dirt, VoxelSurfaceType.WalkableSurface }; private readonly Dictionary> layerLookup; private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary> layers) { TextureArray = textureArray; Material = material; layerLookup = layers; } public Texture2DArray TextureArray { get; } public Material Material { get; } public int BiomeCount => layerLookup.Count; public int GetTextureLayer(int biomeIndex, VoxelSurfaceType surfaceType) { if (layerLookup.TryGetValue(biomeIndex, out Dictionary biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer)) { return layer; } return layerLookup[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); int layerCount = sourceBiomes.Count * SurfaceOrder.Length; Texture2DArray textureArray = new Texture2DArray(tileSize, tileSize, layerCount, TextureFormat.RGBA32, false, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Repeat, anisoLevel = 1, name = "VoxelWorld_RuntimeTextureArray", hideFlags = HideFlags.HideAndDontSave }; Dictionary> layers = new Dictionary>(sourceBiomes.Count); int layerIndex = 0; for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++) { Dictionary biomeLayers = new Dictionary(SurfaceOrder.Length); layers[biomeIndex] = biomeLayers; for (int surfaceIndex = 0; surfaceIndex < SurfaceOrder.Length; surfaceIndex++) { VoxelSurfaceType surfaceType = SurfaceOrder[surfaceIndex]; Texture2D tileTexture = BuildSurfaceTexture(sourceBiomes[biomeIndex], surfaceType, tileSize); CopyTextureToArrayLayer(tileTexture, textureArray, layerIndex); biomeLayers[surfaceType] = layerIndex; layerIndex++; if (Application.isPlaying) { UnityEngine.Object.Destroy(tileTexture); } else { UnityEngine.Object.DestroyImmediate(tileTexture); } } } textureArray.Apply(false, false); Shader shader = Shader.Find(ShaderName); if (shader == null) { shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Standard"); Debug.LogError($"Shader '{ShaderName}' was not found. Falling back to '{shader?.name ?? ""}', but voxel texture array sampling will not work until the shader asset is imported correctly."); } Material material = new Material(shader) { name = "VoxelWorld_RuntimeMaterial", hideFlags = HideFlags.HideAndDontSave }; if (material.HasProperty("_TextureArray")) { material.SetTexture("_TextureArray", textureArray); } if (material.HasProperty("_BaseColor")) { material.SetColor("_BaseColor", Color.white); } if (material.HasProperty("_Color")) { material.SetColor("_Color", Color.white); } return new VoxelWorldAtlas(textureArray, material, layers); } public void Dispose() { if (Application.isPlaying) { UnityEngine.Object.Destroy(TextureArray); UnityEngine.Object.Destroy(Material); } else { UnityEngine.Object.DestroyImmediate(TextureArray); 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 Texture2D BuildSurfaceTexture(VoxelBiomeProfile biome, VoxelSurfaceType surfaceType, int tileSize) { Texture2D texture = new Texture2D(tileSize, tileSize, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Repeat, hideFlags = HideFlags.HideAndDontSave }; Fill(texture, new Color(1f, 0f, 1f, 1f)); Sprite sprite = biome != null ? biome.GetSprite(surfaceType) : null; if (sprite == null || !TryBlitSprite(texture, sprite)) { DrawFallbackSurface(texture, new RectInt(0, 0, tileSize, tileSize), surfaceType); } texture.Apply(false, false); return texture; } private static void CopyTextureToArrayLayer(Texture2D source, Texture2DArray destination, int layerIndex) { bool canUseCopyTexture = SystemInfo.supports2DArrayTextures && SystemInfo.copyTextureSupport != UnityEngine.Rendering.CopyTextureSupport.None && source.width == destination.width && source.height == destination.height; if (canUseCopyTexture) { try { Graphics.CopyTexture(source, 0, 0, destination, layerIndex, 0); return; } catch (Exception) { // Fall back to CPU copy when runtime copy is unavailable for this platform/import setup. } } destination.SetPixels(source.GetPixels(), layerIndex, 0); } private static bool TryBlitSprite(Texture2D target, Sprite sprite) { try { Texture2D source = sprite.texture; Rect spriteRect = sprite.textureRect; for (int y = 0; y < target.height; y++) { float sampleY = Mathf.Lerp(spriteRect.yMin, spriteRect.yMax - 1f, y / (float)Mathf.Max(1, target.height - 1)); int sourceY = Mathf.Clamp(Mathf.RoundToInt(sampleY), 0, source.height - 1); for (int x = 0; x < target.width; x++) { float sampleX = Mathf.Lerp(spriteRect.xMin, spriteRect.xMax - 1f, x / (float)Mathf.Max(1, target.width - 1)); int sourceX = Mathf.Clamp(Mathf.RoundToInt(sampleX), 0, source.width - 1); target.SetPixel(x, 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 texture array."); 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)); } } } } }