diff --git a/Client/Scripts/ChunkRenderer.cs b/Client/Scripts/ChunkRenderer.cs
index f8b3811..ea3558b 100644
--- a/Client/Scripts/ChunkRenderer.cs
+++ b/Client/Scripts/ChunkRenderer.cs
@@ -8,8 +8,12 @@ namespace IslaApocalypse.Client
{
public void RenderChunk(ChunkData data)
{
- // 1. Pass the BlockIDs array to the generator!
- Mesh = MarchingCubes.GenerateMesh(data.Densities, data.BlockIDs, Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
+ // 1. Pass the BlockIDs array to the generator, plus the per-column
+ // surface/biome/road data it needs to fade colours smoothly.
+ Mesh = MarchingCubes.GenerateMesh(
+ data.Densities, data.BlockIDs,
+ data.SurfaceHeights, data.ColumnBiomes, data.ColumnIsRoad,
+ Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
StandardMaterial3D mat = new StandardMaterial3D();
diff --git a/Core/Scripts/BiomePalette.cs b/Core/Scripts/BiomePalette.cs
index 5f25e07..d1fb8d9 100644
--- a/Core/Scripts/BiomePalette.cs
+++ b/Core/Scripts/BiomePalette.cs
@@ -59,5 +59,111 @@ namespace IslaApocalypse.Core
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, 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);
+ }
+ }
+
+ ///
+ /// 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, 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;
+ }
+ }
+
+ /// 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;
+ }
+ }
}
}
diff --git a/Core/Scripts/ChunkData.cs b/Core/Scripts/ChunkData.cs
index 81927f6..4205eca 100644
--- a/Core/Scripts/ChunkData.cs
+++ b/Core/Scripts/ChunkData.cs
@@ -17,8 +17,23 @@ namespace IslaApocalypse.Core
public float[,,] Densities = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT + 1, Constants.CHUNK_SIZE_Z + 1];
public byte[,,] BlockIDs = new byte[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_HEIGHT + 1, Constants.CHUNK_SIZE_Z + 1];
+ // --- PER-COLUMN DATA, FOR VERTEX COLOURING ONLY ----------------------
+ // One entry per X/Z column of this chunk (same +1 padding as above).
+ //
+ // Why these exist: BlockIDs are classified against a ROUNDED surface
+ // height, which is fine for storage but makes the rendered colour jump
+ // in whole-metre steps (the "contour band" artefact). The renderer
+ // instead reads the TRUE, unrounded surface height from here, so the
+ // material fade follows the real ground.
+ //
+ // These are read by the mesher only. Nothing about geometry, collision
+ // or storage depends on them.
+ public float[,] SurfaceHeights = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+ public Biome[,] ColumnBiomes = new Biome[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+ public bool[,] ColumnIsRoad = new bool[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1];
+
// For our future delta-save system
- public bool IsPlayerProtected = false;
+ public bool IsPlayerProtected = false;
public bool NeedsSaving = false;
public ChunkData(Vector2I position)
diff --git a/Core/Scripts/Constants.cs b/Core/Scripts/Constants.cs
index 7de2266..30db259 100644
--- a/Core/Scripts/Constants.cs
+++ b/Core/Scripts/Constants.cs
@@ -10,5 +10,18 @@ namespace IslaApocalypse.Core
// World Generation
public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes
public const float VOXEL_SCALE = 1.0f; // 1 Pixel = 1 Meter
+
+ // --- APPEARANCE TUNING ---------------------------------------------
+ // How many metres it takes for one surface material to fade into the
+ // next (grass -> dirt -> stone) on the rendered mesh.
+ //
+ // TUNE THIS BY EYE: bigger = softer, more gradual fade.
+ // 0.0 = hard switch, exactly like the old banded look
+ // 2.0 = default, a gentle two-metre blend
+ // 4.0+ = very soft, materials smear into each other
+ //
+ // Colour only. This does NOT affect terrain shape, collision, or what
+ // block is actually stored in a voxel.
+ public const float BLEND_BAND_METERS = 2.0f;
}
}
\ No newline at end of file
diff --git a/Core/Scripts/MarchingCubes.cs b/Core/Scripts/MarchingCubes.cs
index 220b883..02ab820 100644
--- a/Core/Scripts/MarchingCubes.cs
+++ b/Core/Scripts/MarchingCubes.cs
@@ -403,7 +403,17 @@ namespace IslaApocalypse.Core
return new Vector3(gx, gy, gz).Normalized();
}
- public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs, float isolevel = 0.0f, float voxelScale = 1.0f)
+ ///
+ /// Builds the chunk mesh.
+ ///
+ /// Kept for non-visual use and as a colour fallback; the vertex
+ /// colour itself now comes from the per-column data below, which is not rounded.
+ /// True (unrounded) surface height per X/Z column.
+ /// Biome per X/Z column.
+ /// Whether each X/Z column is roadbed.
+ public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs,
+ float[,] surfaceHeights, Biome[,] columnBiomes, bool[,] columnIsRoad,
+ float isolevel = 0.0f, float voxelScale = 1.0f)
{
var st = new SurfaceTool();
st.Begin(Mesh.PrimitiveType.Triangles);
@@ -479,11 +489,32 @@ namespace IslaApocalypse.Core
float mu = (isolevel - valA) / (valB - valA);
Vector3 exactNormal = (gradA + mu * (gradB - gradA)).Normalized();
- // 4. THE COLOR FIX: Grab the BlockID from CornerA and paint the vertex!
- // (We just reuse the gridPosA we already declared above the if-statement!)
- byte blockID = blockIDs[gridPosA.X, gridPosA.Y, gridPosA.Z];
-
- st.SetColor(GetBlockColor(blockID));
+ // 4. THE COLOUR: fade between materials using the TRUE surface height.
+ //
+ // The old code read a BlockID here. That ID had been classified
+ // against a ROUNDED surface height, so the colour flipped between
+ // grass and dirt depending on which way the rounding fell — which
+ // is what drew those horizontal contour bands across slopes.
+ //
+ // Now we ask: how far below this column's real, unrounded surface
+ // does this exact vertex sit? Then we fade between the two
+ // materials either side of the nearest boundary.
+ int colX = gridPosA.X;
+ int colZ = gridPosA.Z;
+
+ // exactPos is scaled by voxelScale, so divide it back out to get
+ // the height in the same units the surface heights are stored in.
+ float vertexY = exactPos.Y / voxelScale;
+ float depthBelowSurface = surfaceHeights[colX, colZ] - vertexY;
+
+ BiomePalette.GetBlendedVoxelIDs(
+ columnBiomes[colX, colZ],
+ columnIsRoad[colX, colZ],
+ depthBelowSurface,
+ Constants.BLEND_BAND_METERS,
+ out byte idNear, out byte idFar, out float blendAmount);
+
+ st.SetColor(GetBlockColor(idNear).Lerp(GetBlockColor(idFar), blendAmount));
// 5. Feed the perfect normal AND the position to Godot!
// (SetColor and SetNormal MUST be called immediately before AddVertex)
diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs
index e29bb46..c9b8f37 100644
--- a/Server/Scripts/ServerChunkManager.cs
+++ b/Server/Scripts/ServerChunkManager.cs
@@ -174,6 +174,13 @@ namespace IslaApocalypse.Server
Biome columnBiome = _blueprint.BiomeMap[globalX, globalZ];
+ // Hand the renderer the TRUE surface height for this column, plus the
+ // biome and road flag that go with it. The BlockIDs below still use the
+ // rounded height (unchanged) — but the colour no longer has to.
+ newChunk.SurfaceHeights[x, z] = exactSurfaceY;
+ newChunk.ColumnBiomes[x, z] = columnBiome;
+ newChunk.ColumnIsRoad[x, z] = isMainRoad;
+
float hRight = surfaceRight - exactSurfaceY;
float hFwd = surfaceFwd - exactSurfaceY;
float slopeX = hRight / Constants.VOXEL_SCALE;