396 lines
14 KiB
C#
396 lines
14 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 const string ShaderName = "Infinite World/VoxelWorld/TextureArrayUnlit";
|
|
|
|
private static readonly VoxelSurfaceType[] SurfaceOrder =
|
|
{
|
|
VoxelSurfaceType.CliffTop,
|
|
VoxelSurfaceType.CliffSide,
|
|
VoxelSurfaceType.Dirt,
|
|
VoxelSurfaceType.WalkableSurface
|
|
};
|
|
|
|
private readonly Dictionary<int, Dictionary<VoxelSurfaceType, int>> layerLookup;
|
|
|
|
private VoxelWorldAtlas(Texture2DArray textureArray, Material material, Dictionary<int, Dictionary<VoxelSurfaceType, int>> 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<VoxelSurfaceType, int> biomeLayers) && biomeLayers.TryGetValue(surfaceType, out int layer))
|
|
{
|
|
return layer;
|
|
}
|
|
|
|
return layerLookup[0][surfaceType];
|
|
}
|
|
|
|
public static VoxelWorldAtlas CreateRuntimeAtlas(IReadOnlyList<VoxelBiomeProfile> biomeProfiles, Shader terrainShader)
|
|
{
|
|
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);
|
|
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<int, Dictionary<VoxelSurfaceType, int>> layers = new Dictionary<int, Dictionary<VoxelSurfaceType, int>>(sourceBiomes.Count);
|
|
int layerIndex = 0;
|
|
for (int biomeIndex = 0; biomeIndex < sourceBiomes.Count; biomeIndex++)
|
|
{
|
|
Dictionary<VoxelSurfaceType, int> biomeLayers = new Dictionary<VoxelSurfaceType, int>(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 = terrainShader;
|
|
if (shader == null)
|
|
{
|
|
shader = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Standard");
|
|
Debug.LogError($"Shader was not found. Falling back to '{shader?.name ?? "<missing>"}', 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("_UseTextureArray"))
|
|
{
|
|
material.SetFloat("_UseTextureArray", 1f);
|
|
}
|
|
|
|
if (material.HasProperty("_UseBaseMap"))
|
|
{
|
|
material.SetFloat("_UseBaseMap", 0f);
|
|
}
|
|
|
|
if (material.HasProperty("_UseLightMap"))
|
|
{
|
|
material.SetFloat("_UseLightMap", 0f);
|
|
}
|
|
|
|
if (material.HasProperty("_UseFog"))
|
|
{
|
|
material.SetFloat("_UseFog", 1f);
|
|
}
|
|
|
|
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<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 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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|