using Godot; namespace IslaApocalypse.Core { public static class BiomePalette { /// /// Determines the exact Block ID for a specific voxel in the 3D world based on its depth and biome. /// /// The 2D biome pixel from the MapData. /// The maximum height of the terrain at this X/Z coordinate. /// The Y coordinate of the specific voxel we are painting. /// What this column's road is paved with, or 0 if it is not a road. public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, byte roadMaterial) { // 1. Air Check: If we are above the terrain, it's Air. if (currentY > surfaceHeight) return BlockRegistry.AIR; // 2. Bedrock Check: The very bottom of the world is indestructible. if (currentY == 0) return BlockRegistry.BEDROCK; // 3. Infrastructure Check: If this is a road, and we are exactly on the surface, // paint whatever that road tier is surfaced with (asphalt, dirt, ...). if (roadMaterial != BlockRegistry.AIR && currentY == surfaceHeight) return roadMaterial; int depthBelowSurface = surfaceHeight - currentY; // 4. Subsurface Logic (Dirt layer vs Stone layer) // The top 4 blocks are usually dirt/sand, everything below that is solid stone. if (depthBelowSurface > 4) return BlockRegistry.STONE; // 5. Surface & Shallow Subsurface Logic (Based on Biome) switch (biome) { case Biome.Beach: case Biome.Crater: return BlockRegistry.SAND; // Craters and Beaches are deep sand case Biome.Snow: case Biome.Mountain: if (depthBelowSurface == 0) return BlockRegistry.SNOW; // Only the top layer is snow return BlockRegistry.STONE; // Mountains have shallow topsoil, mostly stone case Biome.Wasteland: return BlockRegistry.WASTELAND_DIRT; case Biome.Jungle: if (depthBelowSurface == 0) return BlockRegistry.GRASS_JUNGLE; return BlockRegistry.DIRT; case Biome.Paradise: if (depthBelowSurface == 0) return BlockRegistry.GRASS_PARADISE; return BlockRegistry.DIRT; case Biome.Tropical: if (depthBelowSurface == 0) return BlockRegistry.GRASS_TROPICAL; return BlockRegistry.DIRT; default: return BlockRegistry.DIRT; // Fallback } } // =================================================================== // SOFT-FADE COLOURING (rendering only) // =================================================================== // GetVoxelID above is the authoritative, INTEGER classification. It // still decides what block is actually stored in each voxel, and it is // deliberately left exactly as it was. // // What follows answers the same question — "what material is here?" — // but for the RENDERER, and it does it differently in two ways: // 1. It measures depth as a real number from the true surface height, // instead of counting whole blocks down from a rounded one. // 2. Instead of returning one material, it returns the two materials // either side of the nearest boundary plus how far between them we // are, so the renderer can fade rather than snap. // That is the whole fix for the horizontal colour banding. // Thickness of the top "skin" layer. Mirrors GetVoxelID's rule that // only the single topmost block gets grass/snow/asphalt. private const float SKIN_DEPTH_METERS = 1.0f; // Depth at which we are into solid stone. Mirrors "depthBelowSurface > 4". private const float STONE_DEPTH_METERS = 4.0f; /// /// Picks the two materials to fade between for a point sitting /// metres under the real surface. /// /// Metres over which one material becomes the next. 0 = hard switch. /// The material on the shallower side of the boundary. /// The material on the deeper side. /// 0 = fully idNear, 1 = fully idFar. public static void GetBlendedVoxelIDs(Biome biome, byte roadMaterial, float depthBelowSurface, float blendBand, out byte idNear, out byte idFar, out float blend) { // Above the surface can happen by a hair on interpolated vertices; treat as surface. float depth = Mathf.Max(depthBelowSurface, 0.0f); byte skinID = GetSkinMaterial(biome, roadMaterial); byte shallowID = GetShallowMaterial(biome); // Two boundaries exist: skin->shallow and shallow->stone. Work out // which one we are closest to and fade across that one. if (depth < (SKIN_DEPTH_METERS + STONE_DEPTH_METERS) * 0.5f) { idNear = skinID; idFar = shallowID; blend = Fade(depth, SKIN_DEPTH_METERS, blendBand); } else { idNear = shallowID; idFar = BlockRegistry.STONE; blend = Fade(depth, STONE_DEPTH_METERS, blendBand); } } /// /// 0 before the boundary, 1 after it, smoothly eased across a band of /// metres centred on the boundary. /// private static float Fade(float depth, float boundary, float band) { // A band of zero means "behave exactly like the old hard switch". if (band <= 0.0f) return depth < boundary ? 0.0f : 1.0f; float t = Mathf.Clamp((depth - (boundary - band * 0.5f)) / band, 0.0f, 1.0f); return t * t * (3.0f - 2.0f * t); // smoothstep — eases in and out } /// The topmost material: what you actually walk on. private static byte GetSkinMaterial(Biome biome, byte roadMaterial) { // A road paves over whatever biome it crosses, with its own tier's surface. if (roadMaterial != BlockRegistry.AIR) return roadMaterial; switch (biome) { case Biome.Beach: case Biome.Crater: return BlockRegistry.SAND; case Biome.Snow: case Biome.Mountain: return BlockRegistry.SNOW; case Biome.Wasteland: return BlockRegistry.WASTELAND_DIRT; case Biome.Jungle: return BlockRegistry.GRASS_JUNGLE; case Biome.Paradise: return BlockRegistry.GRASS_PARADISE; case Biome.Tropical: return BlockRegistry.GRASS_TROPICAL; default: return BlockRegistry.DIRT; } } /// The layer just underneath the skin, before stone takes over. private static byte GetShallowMaterial(Biome biome) { switch (biome) { // Sand goes all the way down on beaches and in the crater. case Biome.Beach: case Biome.Crater: return BlockRegistry.SAND; // Mountains have essentially no topsoil — straight to rock. case Biome.Snow: case Biome.Mountain: return BlockRegistry.STONE; case Biome.Wasteland: return BlockRegistry.WASTELAND_DIRT; default: return BlockRegistry.DIRT; } } } }