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>
This commit is contained in:
parent
7f0f43b5fb
commit
ea11258871
6 changed files with 185 additions and 9 deletions
|
|
@ -8,8 +8,12 @@ namespace IslaApocalypse.Client
|
||||||
{
|
{
|
||||||
public void RenderChunk(ChunkData data)
|
public void RenderChunk(ChunkData data)
|
||||||
{
|
{
|
||||||
// 1. Pass the BlockIDs array to the generator!
|
// 1. Pass the BlockIDs array to the generator, plus the per-column
|
||||||
Mesh = MarchingCubes.GenerateMesh(data.Densities, data.BlockIDs, Constants.ISO_LEVEL, Constants.VOXEL_SCALE);
|
// 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();
|
StandardMaterial3D mat = new StandardMaterial3D();
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,111 @@ namespace IslaApocalypse.Core
|
||||||
return BlockRegistry.DIRT; // Fallback
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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];
|
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
|
// For our future delta-save system
|
||||||
public bool IsPlayerProtected = false;
|
public bool IsPlayerProtected = false;
|
||||||
public bool NeedsSaving = false;
|
public bool NeedsSaving = false;
|
||||||
|
|
||||||
public ChunkData(Vector2I position)
|
public ChunkData(Vector2I position)
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,18 @@ namespace IslaApocalypse.Core
|
||||||
// World Generation
|
// World Generation
|
||||||
public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes
|
public const float ISO_LEVEL = 0.0f; // The threshold for Marching Cubes
|
||||||
public const float VOXEL_SCALE = 1.0f; // 1 Pixel = 1 Meter
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +403,17 @@ namespace IslaApocalypse.Core
|
||||||
return new Vector3(gx, gy, gz).Normalized();
|
return new Vector3(gx, gy, gz).Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs, float isolevel = 0.0f, float voxelScale = 1.0f)
|
/// <summary>
|
||||||
|
/// Builds the chunk mesh.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="blockIDs">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.</param>
|
||||||
|
/// <param name="surfaceHeights">True (unrounded) surface height per X/Z column.</param>
|
||||||
|
/// <param name="columnBiomes">Biome per X/Z column.</param>
|
||||||
|
/// <param name="columnIsRoad">Whether each X/Z column is roadbed.</param>
|
||||||
|
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();
|
var st = new SurfaceTool();
|
||||||
st.Begin(Mesh.PrimitiveType.Triangles);
|
st.Begin(Mesh.PrimitiveType.Triangles);
|
||||||
|
|
@ -479,11 +489,32 @@ namespace IslaApocalypse.Core
|
||||||
float mu = (isolevel - valA) / (valB - valA);
|
float mu = (isolevel - valA) / (valB - valA);
|
||||||
Vector3 exactNormal = (gradA + mu * (gradB - gradA)).Normalized();
|
Vector3 exactNormal = (gradA + mu * (gradB - gradA)).Normalized();
|
||||||
|
|
||||||
// 4. THE COLOR FIX: Grab the BlockID from CornerA and paint the vertex!
|
// 4. THE COLOUR: fade between materials using the TRUE surface height.
|
||||||
// (We just reuse the gridPosA we already declared above the if-statement!)
|
//
|
||||||
byte blockID = blockIDs[gridPosA.X, gridPosA.Y, gridPosA.Z];
|
// The old code read a BlockID here. That ID had been classified
|
||||||
|
// against a ROUNDED surface height, so the colour flipped between
|
||||||
st.SetColor(GetBlockColor(blockID));
|
// 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!
|
// 5. Feed the perfect normal AND the position to Godot!
|
||||||
// (SetColor and SetNormal MUST be called immediately before AddVertex)
|
// (SetColor and SetNormal MUST be called immediately before AddVertex)
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,13 @@ namespace IslaApocalypse.Server
|
||||||
|
|
||||||
Biome columnBiome = _blueprint.BiomeMap[globalX, globalZ];
|
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 hRight = surfaceRight - exactSurfaceY;
|
||||||
float hFwd = surfaceFwd - exactSurfaceY;
|
float hFwd = surfaceFwd - exactSurfaceY;
|
||||||
float slopeX = hRight / Constants.VOXEL_SCALE;
|
float slopeX = hRight / Constants.VOXEL_SCALE;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue