roads: enable Rugged+Trail carving + per-tier character (D-022)

All four road tiers now carve into the 3D world. Rugged and Trail were
generated, exported and parsed all along, but nothing ever consumed them —
which is why county roads visible on the 2D map did not exist in 3D.

Tier identity is now carried into the carve via a RoadSegment struct, so each
tier gets its own width, shoulder and grade-smoothing from one tunable block in
Constants: highways widest and holding a grade, trails narrow and hugging the
land. Rugged and Trail surface as dirt rather than asphalt.

Because tiers now have different reach, nearest-by-distance was no longer a
sound way to pick the governing road — a nearby footpath could shadow a highway
whose shoulder still covered the column. The carve now considers only roads
whose shoulder actually reaches, and takes the closest of those.

The per-chunk cull pad is derived from the widest shoulder plus a margin, so
widening a road cannot silently truncate it at chunk edges.

Untouched: density field, Marching Cubes interpolation/welding, chunk dims,
the .dat contract, the 2D A* generation, and mesher normals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-05 03:53:32 -04:00
parent a827595d61
commit f9ea2da3f4
7 changed files with 255 additions and 130 deletions

View file

@ -12,7 +12,7 @@ namespace IslaApocalypse.Client
// surface/biome/road data it needs to fade colours smoothly. // surface/biome/road data it needs to fade colours smoothly.
Mesh = MarchingCubes.GenerateMesh( Mesh = MarchingCubes.GenerateMesh(
data.Densities, data.BlockIDs, data.Densities, data.BlockIDs,
data.SurfaceHeights, data.ColumnBiomes, data.ColumnIsRoad, data.SurfaceHeights, data.ColumnBiomes, data.ColumnRoadMaterial,
Constants.ISO_LEVEL, Constants.VOXEL_SCALE); Constants.ISO_LEVEL, Constants.VOXEL_SCALE);

View file

@ -10,8 +10,8 @@ namespace IslaApocalypse.Core
/// <param name="biome">The 2D biome pixel from the MapData.</param> /// <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="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="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> /// <param name="roadMaterial">What this column's road is paved with, or 0 if it is not a road.</param>
public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, bool isRoad) public static byte GetVoxelID(Biome biome, int surfaceHeight, int currentY, byte roadMaterial)
{ {
// 1. Air Check: If we are above the terrain, it's Air. // 1. Air Check: If we are above the terrain, it's Air.
if (currentY > surfaceHeight) return BlockRegistry.AIR; if (currentY > surfaceHeight) return BlockRegistry.AIR;
@ -19,8 +19,9 @@ namespace IslaApocalypse.Core
// 2. Bedrock Check: The very bottom of the world is indestructible. // 2. Bedrock Check: The very bottom of the world is indestructible.
if (currentY == 0) return BlockRegistry.BEDROCK; if (currentY == 0) return BlockRegistry.BEDROCK;
// 3. Infrastructure Check: If this is a road, and we are exactly on the surface, paint asphalt. // 3. Infrastructure Check: If this is a road, and we are exactly on the surface,
if (isRoad && currentY == surfaceHeight) return BlockRegistry.ASPHALT; // paint whatever that road tier is surfaced with (asphalt, dirt, ...).
if (roadMaterial != BlockRegistry.AIR && currentY == surfaceHeight) return roadMaterial;
int depthBelowSurface = surfaceHeight - currentY; int depthBelowSurface = surfaceHeight - currentY;
@ -91,13 +92,13 @@ namespace IslaApocalypse.Core
/// <param name="idNear">The material on the shallower side of the boundary.</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="idFar">The material on the deeper side.</param>
/// <param name="blend">0 = fully idNear, 1 = fully idFar.</param> /// <param name="blend">0 = fully idNear, 1 = fully idFar.</param>
public static void GetBlendedVoxelIDs(Biome biome, bool isRoad, float depthBelowSurface, public static void GetBlendedVoxelIDs(Biome biome, byte roadMaterial, float depthBelowSurface,
float blendBand, out byte idNear, out byte idFar, out float blend) 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. // Above the surface can happen by a hair on interpolated vertices; treat as surface.
float depth = Mathf.Max(depthBelowSurface, 0.0f); float depth = Mathf.Max(depthBelowSurface, 0.0f);
byte skinID = GetSkinMaterial(biome, isRoad); byte skinID = GetSkinMaterial(biome, roadMaterial);
byte shallowID = GetShallowMaterial(biome); byte shallowID = GetShallowMaterial(biome);
// Two boundaries exist: skin->shallow and shallow->stone. Work out // Two boundaries exist: skin->shallow and shallow->stone. Work out
@ -130,9 +131,10 @@ namespace IslaApocalypse.Core
} }
/// <summary>The topmost material: what you actually walk on.</summary> /// <summary>The topmost material: what you actually walk on.</summary>
private static byte GetSkinMaterial(Biome biome, bool isRoad) private static byte GetSkinMaterial(Biome biome, byte roadMaterial)
{ {
if (isRoad) return BlockRegistry.ASPHALT; // A road paves over whatever biome it crosses, with its own tier's surface.
if (roadMaterial != BlockRegistry.AIR) return roadMaterial;
switch (biome) switch (biome)
{ {

View file

@ -30,7 +30,12 @@ namespace IslaApocalypse.Core
// or storage depends on them. // or storage depends on them.
public float[,] SurfaceHeights = new float[Constants.CHUNK_SIZE_X + 1, Constants.CHUNK_SIZE_Z + 1]; 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 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];
// What this column's road surface is paved with — asphalt for highways,
// dirt for trails, and 0 (BlockRegistry.AIR) for "not a road at all".
// Carries the tier's material rather than a plain yes/no, so a footpath
// doesn't get painted like a motorway.
public byte[,] ColumnRoadMaterial = new byte[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;

View file

@ -1,3 +1,5 @@
using Godot;
namespace IslaApocalypse.Core namespace IslaApocalypse.Core
{ {
public static class Constants public static class Constants
@ -24,22 +26,94 @@ namespace IslaApocalypse.Core
// block is actually stored in a voxel. // block is actually stored in a voxel.
public const float BLEND_BAND_METERS = 2.0f; public const float BLEND_BAND_METERS = 2.0f;
// --- ROAD GRADE TUNING --------------------------------------------- // --- ROAD CHARACTER BY TIER (D-022) ---------------------------------
// How much a road "holds a grade" instead of following every bump in // A road is carved to match how BUILT it is. One gradient, four steps:
// the ground underneath it. Roads exist to be DRIVEN on, so this is a // a highway is engineered, a trail is a footpath worn into the ground.
// driveability setting, not a cosmetic one.
// //
// 0.0 = hugs the land: the roadbed follows the terrain exactly. // Three knobs per tier:
// Never steps, but inherits every lump the ground has. // RoadRadius - half-width of the flat carriageway, in metres.
// 1.0 = holds a grade (default): each stretch of road is a straight // ShoulderRadius - how far out the carve eases back to natural
// ramp between its two path points, cutting through small // ground. Must always be LARGER than RoadRadius.
// bumps and filling small dips. Smoothest to drive. // GradeSmoothing - 0.0 hugs the land (follows every bump)
// 1.0 holds a grade (straight ramp between path
// points, cutting bumps and filling dips).
// SurfaceMaterial- what the carriageway is paved with.
// //
// Values in between mix the two. Raise toward 1.0 for a more // TUNE THESE BY EYE. They are starting values chosen to make the
// engineered road, lower toward 0.0 for a more rustic one. // hierarchy visible, not final ones. A trail being bumpy is CORRECT —
// nobody drives a truck down a footpath.
// //
// NOTE: this affects road ELEVATION (terrain shape under the road), // NOTE: these affect road ELEVATION (terrain shape) and paint, not the
// not colour. Tune it properly once vehicles exist to drive on it. // terrain noise. Tune the highway properly once vehicles can drive it.
public const float ROAD_GRADE_SMOOTHING = 1.0f;
public const float HIGHWAY_ROAD_RADIUS = 4.0f; // 8 m carriageway
public const float HIGHWAY_SHOULDER_RADIUS = 12.0f;
public const float HIGHWAY_GRADE_SMOOTHING = 1.0f; // fully engineered
public const float BRANCH_ROAD_RADIUS = 3.0f;
public const float BRANCH_SHOULDER_RADIUS = 9.0f;
public const float BRANCH_GRADE_SMOOTHING = 0.7f;
public const float RUGGED_ROAD_RADIUS = 2.0f;
public const float RUGGED_SHOULDER_RADIUS = 6.0f;
public const float RUGGED_GRADE_SMOOTHING = 0.4f;
public const float TRAIL_ROAD_RADIUS = 1.5f;
public const float TRAIL_SHOULDER_RADIUS = 4.0f;
public const float TRAIL_GRADE_SMOOTHING = 0.1f; // barely built at all
/// <summary>
/// The widest reach any road has. The per-chunk road cull pads by this
/// (plus a margin) so a road just outside a chunk still carves into it.
/// Computed from the values above, so raising a shoulder can never
/// silently outgrow the cull and truncate roads at chunk edges.
/// </summary>
public static readonly float MAX_SHOULDER_RADIUS = Mathf.Max(
Mathf.Max(HIGHWAY_SHOULDER_RADIUS, BRANCH_SHOULDER_RADIUS),
Mathf.Max(RUGGED_SHOULDER_RADIUS, TRAIL_SHOULDER_RADIUS));
/// <summary>Safety margin added to the road cull, in metres.</summary>
public const float ROAD_CULL_MARGIN = 3.0f;
/// <summary>
/// Looks up the carve settings for a road tier. One place to tune.
/// </summary>
public static RoadProfile GetRoadProfile(RoadTier tier)
{
switch (tier)
{
case RoadTier.Highway:
return new RoadProfile(HIGHWAY_ROAD_RADIUS, HIGHWAY_SHOULDER_RADIUS,
HIGHWAY_GRADE_SMOOTHING, BlockRegistry.ASPHALT);
case RoadTier.Branch:
return new RoadProfile(BRANCH_ROAD_RADIUS, BRANCH_SHOULDER_RADIUS,
BRANCH_GRADE_SMOOTHING, BlockRegistry.ASPHALT);
case RoadTier.Rugged:
return new RoadProfile(RUGGED_ROAD_RADIUS, RUGGED_SHOULDER_RADIUS,
RUGGED_GRADE_SMOOTHING, BlockRegistry.DIRT);
default: // Trail
return new RoadProfile(TRAIL_ROAD_RADIUS, TRAIL_SHOULDER_RADIUS,
TRAIL_GRADE_SMOOTHING, BlockRegistry.DIRT);
}
}
}
/// <summary>
/// How one tier of road is carved. Values come from Constants above.
/// </summary>
public struct RoadProfile
{
public float RoadRadius;
public float ShoulderRadius;
public float GradeSmoothing;
public byte SurfaceMaterial;
public RoadProfile(float roadRadius, float shoulderRadius, float gradeSmoothing, byte surfaceMaterial)
{
RoadRadius = roadRadius;
ShoulderRadius = shoulderRadius;
GradeSmoothing = gradeSmoothing;
SurfaceMaterial = surfaceMaterial;
}
} }
} }

View file

@ -5,4 +5,11 @@ namespace IslaApocalypse.Core
public enum TownTier { Village, Hub, Capitol, Outpost, IslandLoot, MiniPOI } public enum TownTier { Village, Hub, Capitol, Outpost, IslandLoot, MiniPOI }
public enum MapHalf { Any, Left, Right } public enum MapHalf { Any, Left, Right }
// How "built" a road is, most engineered first. Drives how wide and how
// smooth it gets carved into the 3D world (see D-022).
//
// NOTE: unlike Biome and TownTier above, this one is NOT written into the
// .dat file — the blueprint stores each tier in its own section, so this
// enum never becomes a serialized number. It is safe to reorder or extend.
public enum RoadTier { Highway, Branch, Rugged, Trail }
} }

View file

@ -410,9 +410,9 @@ namespace IslaApocalypse.Core
/// colour itself now comes from the per-column data below, which is not rounded.</param> /// 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="surfaceHeights">True (unrounded) surface height per X/Z column.</param>
/// <param name="columnBiomes">Biome 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> /// <param name="columnRoadMaterial">Road surface material per X/Z column, or 0 if not a road.</param>
public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs, public static ArrayMesh GenerateMesh(float[,,] scalarField, byte[,,] blockIDs,
float[,] surfaceHeights, Biome[,] columnBiomes, bool[,] columnIsRoad, float[,] surfaceHeights, Biome[,] columnBiomes, byte[,] columnRoadMaterial,
float isolevel = 0.0f, float voxelScale = 1.0f) float isolevel = 0.0f, float voxelScale = 1.0f)
{ {
var st = new SurfaceTool(); var st = new SurfaceTool();
@ -509,7 +509,7 @@ namespace IslaApocalypse.Core
BiomePalette.GetBlendedVoxelIDs( BiomePalette.GetBlendedVoxelIDs(
columnBiomes[colX, colZ], columnBiomes[colX, colZ],
columnIsRoad[colX, colZ], columnRoadMaterial[colX, colZ],
depthBelowSurface, depthBelowSurface,
Constants.BLEND_BAND_METERS, Constants.BLEND_BAND_METERS,
out byte idNear, out byte idFar, out float blendAmount); out byte idNear, out byte idFar, out float blendAmount);

View file

@ -4,6 +4,25 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Server namespace IslaApocalypse.Server
{ {
/// <summary>
/// One straight stretch of road, plus which tier it belongs to. The tier is what
/// decides how wide and how smoothly it gets carved (D-022) — before this existed,
/// every road was bulldozed to the same dimensions regardless of what it was.
/// </summary>
public struct RoadSegment
{
public Vector2 A;
public Vector2 B;
public RoadTier Tier;
public RoadSegment(Vector2 a, Vector2 b, RoadTier tier)
{
A = a;
B = b;
Tier = tier;
}
}
public partial class ServerChunkManager : Node public partial class ServerChunkManager : Node
{ {
private WorldBlueprint _blueprint; private WorldBlueprint _blueprint;
@ -73,33 +92,40 @@ namespace IslaApocalypse.Server
int startX = chunkCoord.X * Constants.CHUNK_SIZE_X; int startX = chunkCoord.X * Constants.CHUNK_SIZE_X;
int startZ = chunkCoord.Y * Constants.CHUNK_SIZE_Z; int startZ = chunkCoord.Y * Constants.CHUNK_SIZE_Z;
// --- NEW: SPATIAL CULLING FOR ROADS --- // --- SPATIAL CULLING FOR ROADS ---
// Create a bounding box for this chunk, plus a 15-meter padding to account for the road's dirt shoulders // Bounding box for this chunk, padded so a road running just outside it still
Rect2 chunkBounds = new Rect2(startX - 15, startZ - 15, Constants.CHUNK_SIZE_X + 30, Constants.CHUNK_SIZE_Z + 30); // carves its shoulder in. The pad comes from the widest shoulder any tier has
List<Vector2[]> localRoadSegments = new List<Vector2[]>(); // (plus a margin), so widening a road can't silently outgrow this and clip
// roads off at chunk edges.
float roadPad = Constants.MAX_SHOULDER_RADIUS + Constants.ROAD_CULL_MARGIN;
Rect2 chunkBounds = new Rect2(
startX - roadPad, startZ - roadPad,
Constants.CHUNK_SIZE_X + roadPad * 2, Constants.CHUNK_SIZE_Z + roadPad * 2);
// Filter Highways: Only save the segments that actually cross this specific chunk! List<RoadSegment> localRoadSegments = new List<RoadSegment>();
foreach (var highway in _blueprint.Highways) {
for (int i = 0; i < highway.Length - 1; i++) { // Keep only the segments that actually reach this chunk, and remember which
Vector2 a = highway[i]; // TIER each came from — that is what decides how wide and smooth it carves.
Vector2 b = highway[i+1]; void CollectSegments(List<Vector2[]> paths, RoadTier tier)
{
foreach (var path in paths) {
for (int i = 0; i < path.Length - 1; i++) {
Vector2 a = path[i];
Vector2 b = path[i+1];
Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y)); Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y));
if (chunkBounds.Intersects(segBounds.Grow(1.0f))) { if (chunkBounds.Intersects(segBounds.Grow(1.0f))) {
localRoadSegments.Add(new Vector2[] { a, b }); localRoadSegments.Add(new RoadSegment(a, b, tier));
} }
} }
} }
// Filter Branch Roads
foreach (var branch in _blueprint.BranchRoads) {
for (int i = 0; i < branch.Length - 1; i++) {
Vector2 a = branch[i];
Vector2 b = branch[i+1];
Rect2 segBounds = new Rect2(Mathf.Min(a.X, b.X), Mathf.Min(a.Y, b.Y), Mathf.Abs(b.X - a.X), Mathf.Abs(b.Y - a.Y));
if (chunkBounds.Intersects(segBounds.Grow(1.0f))) {
localRoadSegments.Add(new Vector2[] { a, b });
}
}
} }
// All four tiers now carve. Rugged and Trail are the county roads — they were
// generated and saved all along, but nothing ever consumed them.
CollectSegments(_blueprint.Highways, RoadTier.Highway);
CollectSegments(_blueprint.BranchRoads, RoadTier.Branch);
CollectSegments(_blueprint.RuggedRoads, RoadTier.Rugged);
CollectSegments(_blueprint.TrailRoads, RoadTier.Trail);
// --------------------------------------- // ---------------------------------------
for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++) for (int x = 0; x <= Constants.CHUNK_SIZE_X; x++)
@ -112,9 +138,9 @@ namespace IslaApocalypse.Server
if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0) if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0)
continue; continue;
float GetExactSurface(int gX, int gZ, out bool isRoad) float GetExactSurface(int gX, int gZ, out byte roadMaterial)
{ {
isRoad = false; roadMaterial = BlockRegistry.AIR; // 0 == "not a road"
if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1; if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1;
if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1; if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1;
@ -125,26 +151,31 @@ namespace IslaApocalypse.Server
if (localRoadSegments.Count == 0) return baseHeight; if (localRoadSegments.Count == 0) return baseHeight;
float finalHeight = baseHeight; float finalHeight = baseHeight;
float minDist = 9999f;
Vector2 currentPos = new Vector2(gX, gZ); Vector2 currentPos = new Vector2(gX, gZ);
float roadRadius = 4.0f; // The flat asphalt part (8 meters total width) // Details of the nearest road that actually REACHES us. Each tier has
float shoulderRadius = 12.0f; // The sloped dirt/rock carving into the mountain // its own reach, so "nearest" alone isn't enough — a footpath two
// metres away must not shadow a highway whose shoulder still covers
float closestRoadElevation = baseHeight; // us. We therefore ignore any road we're outside the shoulder of, and
// take the closest of what's left.
bool foundRoad = false;
float bestDist = float.MaxValue;
float bestRoadElevation = baseHeight;
RoadProfile bestProfile = default;
foreach (var seg in localRoadSegments) foreach (var seg in localRoadSegments)
{ {
RoadProfile profile = Constants.GetRoadProfile(seg.Tier);
// 'alongT' tells us HOW FAR ALONG this segment the nearest point is // 'alongT' tells us HOW FAR ALONG this segment the nearest point is
// (0 = at the start point, 1 = at the end point). // (0 = at the start point, 1 = at the end point).
float dist = DistanceToLineSegment(currentPos, seg[0], seg[1], out float alongT); float dist = DistanceToLineSegment(currentPos, seg.A, seg.B, out float alongT);
if (dist < minDist)
{ if (dist > profile.ShoulderRadius) continue; // this road doesn't reach us
minDist = dist; if (dist >= bestDist) continue; // a closer one already won
if (dist <= shoulderRadius)
{ // F2 FIX — the road elevation is taken at the point on the segment
// F2 FIX — the road elevation is now taken at the point on the // CLOSEST TO US, not at the segment's midpoint.
// segment CLOSEST TO US, not at the segment's midpoint.
// //
// The old code gave every column near a segment that segment's // The old code gave every column near a segment that segment's
// single midpoint height, so each stretch of road was one flat // single midpoint height, so each stretch of road was one flat
@ -155,37 +186,43 @@ namespace IslaApocalypse.Server
// end points. Holds a grade; cuts and fills. // end points. Holds a grade; cuts and fills.
// landElevation - the actual terrain under that point. // landElevation - the actual terrain under that point.
// Hugs the land; inherits its bumps. // Hugs the land; inherits its bumps.
// ROAD_GRADE_SMOOTHING dials between them (see D-021). // The tier's GradeSmoothing dials between them (D-021 / D-022):
// a highway holds its grade, a trail follows the ground.
// //
// Either way it varies CONTINUOUSLY as we move along the road, // Either way it varies CONTINUOUSLY as we move along the road,
// which is what kills the steps. And because neighbouring // which is what kills the steps. And because neighbouring
// segments share an end point, the height matches exactly where // segments share an end point, the height matches exactly where
// one segment hands over to the next — no seam at the joins. // one segment hands over to the next — no seam at the joins.
float rampElevation = Mathf.Lerp(HeightAtPixel(seg[0]), HeightAtPixel(seg[1]), alongT); float rampElevation = Mathf.Lerp(HeightAtPixel(seg.A), HeightAtPixel(seg.B), alongT);
float landElevation = HeightAtPixel(seg[0].Lerp(seg[1], alongT)); float landElevation = HeightAtPixel(seg.A.Lerp(seg.B, alongT));
closestRoadElevation = Mathf.Lerp(landElevation, rampElevation, Constants.ROAD_GRADE_SMOOTHING); foundRoad = true;
} bestDist = dist;
} bestProfile = profile;
bestRoadElevation = Mathf.Lerp(landElevation, rampElevation, profile.GradeSmoothing);
} }
// THE BULLDOZER: Carve the terrain // THE BULLDOZER: Carve the terrain, using the winning road's own dimensions.
if (minDist <= roadRadius) { if (foundRoad)
finalHeight = closestRoadElevation; // Flatten it completely! {
isRoad = true; if (bestDist <= bestProfile.RoadRadius) {
} else if (minDist <= shoulderRadius) { finalHeight = bestRoadElevation; // Flatten it completely!
roadMaterial = bestProfile.SurfaceMaterial;
} else {
// Smoothly interpolate from the flat road up/down to the natural mountain height // Smoothly interpolate from the flat road up/down to the natural mountain height
float t = (minDist - roadRadius) / (shoulderRadius - roadRadius); float t = (bestDist - bestProfile.RoadRadius) / (bestProfile.ShoulderRadius - bestProfile.RoadRadius);
t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope
finalHeight = Mathf.Lerp(closestRoadElevation, baseHeight, t); finalHeight = Mathf.Lerp(bestRoadElevation, baseHeight, t);
}
} }
return finalHeight; return finalHeight;
} }
// Pass the boolean out for the main voxel so we can paint it! // Pass the road surface out for the main voxel so we can paint it!
bool isMainRoad = false; // 0 (AIR) means this column is not part of any road.
float exactSurfaceY = GetExactSurface(globalX, globalZ, out isMainRoad); byte roadSurface = BlockRegistry.AIR;
float exactSurfaceY = GetExactSurface(globalX, globalZ, out roadSurface);
// We discard the 'out' variable for the normals using an underscore '_' // We discard the 'out' variable for the normals using an underscore '_'
float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _); float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _);
@ -198,7 +235,7 @@ namespace IslaApocalypse.Server
// rounded height (unchanged) — but the colour no longer has to. // rounded height (unchanged) — but the colour no longer has to.
newChunk.SurfaceHeights[x, z] = exactSurfaceY; newChunk.SurfaceHeights[x, z] = exactSurfaceY;
newChunk.ColumnBiomes[x, z] = columnBiome; newChunk.ColumnBiomes[x, z] = columnBiome;
newChunk.ColumnIsRoad[x, z] = isMainRoad; newChunk.ColumnRoadMaterial[x, z] = roadSurface;
float hRight = surfaceRight - exactSurfaceY; float hRight = surfaceRight - exactSurfaceY;
float hFwd = surfaceFwd - exactSurfaceY; float hFwd = surfaceFwd - exactSurfaceY;
@ -216,8 +253,8 @@ namespace IslaApocalypse.Server
int blockY = Mathf.RoundToInt(exactSurfaceY); int blockY = Mathf.RoundToInt(exactSurfaceY);
// THE PAINT: Pass isMainRoad into the Biome Palette! // THE PAINT: Pass the road surface into the Biome Palette!
newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, isMainRoad); newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, roadSurface);
} }
} // End of Z loop } // End of Z loop
} // End of X loop } // End of X loop