Files
TheDeclineOfWarriors/Assets/Scripts/VoxelWorld/Runtime/VoxelWorldAtlas.cs
T
2026-03-31 09:02:06 +07:00

341 lines
12 KiB
C#

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));
}
}
}
}
}