islaApocalypse/Core/Scripts/BiomePalette.cs
beezm ea11258871 F1: soft-fade biome color blending (BLEND_BAND_METERS, geometry untouched)
Vertex colour is now chosen from the true, unrounded surface height and faded
between adjacent materials across a tunable band, instead of switching at one
rounded integer depth. This removes the horizontal contour banding.

Colour path only: density field, Marching Cubes interpolation/welding, chunk
dimensions, the .dat contract and all road logic are untouched. BlockID
classification is unchanged and still drives non-visual use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:51:06 -04:00

169 lines
6.5 KiB
C#

using Godot;
namespace IslaApocalypse.Core
{
public static class BiomePalette
{
/// <summary>
/// Determines the exact Block ID for a specific voxel in the 3D world based on its depth and biome.
/// </summary>
/// <param name="biome">The 2D biome pixel from the MapData.</param>
/// <param name="surfaceHeight">The maximum height of the terrain at this X/Z coordinate.</param>
/// <param name="currentY">The Y coordinate of the specific voxel we are painting.</param>
/// <param name="isRoad">Is this coordinate part of the highway/branch road network?</param>
public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, bool isRoad)
{
// 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 asphalt.
if (isRoad && currentY == surfaceHeight) return BlockRegistry.ASPHALT;
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;
/// <summary>
/// Picks the two materials to fade between for a point sitting
/// <paramref name="depthBelowSurface"/> metres under the real surface.
/// </summary>
/// <param name="blendBand">Metres over which one material becomes the next. 0 = hard switch.</param>
/// <param name="idNear">The material on the shallower side of the boundary.</param>
/// <param name="idFar">The material on the deeper side.</param>
/// <param name="blend">0 = fully idNear, 1 = fully idFar.</param>
public static void GetBlendedVoxelIDs(Biome biome, bool isRoad, 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, isRoad);
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);
}
}
/// <summary>
/// 0 before the boundary, 1 after it, smoothly eased across a band of
/// <paramref name="band"/> metres centred on the boundary.
/// </summary>
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
}
/// <summary>The topmost material: what you actually walk on.</summary>
private static byte GetSkinMaterial(Biome biome, bool isRoad)
{
if (isRoad) return BlockRegistry.ASPHALT;
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;
}
}
/// <summary>The layer just underneath the skin, before stone takes over.</summary>
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;
}
}
}
}