From ea11258871981c33284cd43888345acfd0f58b75 Mon Sep 17 00:00:00 2001 From: beezm Date: Wed, 5 Aug 2026 01:51:06 -0400 Subject: [PATCH] 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) --- Client/Scripts/ChunkRenderer.cs | 8 +- Core/Scripts/BiomePalette.cs | 106 +++++++++++++++++++++++++++ Core/Scripts/ChunkData.cs | 17 ++++- Core/Scripts/Constants.cs | 13 ++++ Core/Scripts/MarchingCubes.cs | 43 +++++++++-- Server/Scripts/ServerChunkManager.cs | 7 ++ 6 files changed, 185 insertions(+), 9 deletions(-) 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;