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>
169 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|