From 0910338a2a69b501c2065606c10924c71bc1f451 Mon Sep 17 00:00:00 2001 From: beezm Date: Wed, 5 Aug 2026 02:20:50 -0400 Subject: [PATCH] =?UTF-8?q?F2:=20continuous=20road=20grade=20=E2=80=94=20a?= =?UTF-8?q?long-segment=20elevation=20+=20joint=20smoothing=20(D-021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Road elevation is now sampled at the point on the road segment nearest the column being carved, instead of at the segment's midpoint. The old behaviour gave every column near a segment that segment's single midpoint height, so each stretch of road was one flat plank and consecutive planks stepped like a staircase wherever the road crossed a gradient. Elevation now varies continuously along the segment, and because neighbouring segments share an end point the height matches exactly at the joins. A new ROAD_GRADE_SMOOTHING constant dials between holding a straight grade (cut-and-fill) and hugging the land, per D-021. Untouched: density field, Marching Cubes, chunk dimensions, the .dat contract, and the 2D A* road network itself. Only how existing paths are carved into 3D. Co-Authored-By: Claude Opus 5 (1M context) --- Core/Scripts/Constants.cs | 18 ++++++++ Server/Scripts/ServerChunkManager.cs | 69 +++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/Core/Scripts/Constants.cs b/Core/Scripts/Constants.cs index 30db259..642e0a6 100644 --- a/Core/Scripts/Constants.cs +++ b/Core/Scripts/Constants.cs @@ -23,5 +23,23 @@ namespace IslaApocalypse.Core // 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; + + // --- ROAD GRADE TUNING --------------------------------------------- + // How much a road "holds a grade" instead of following every bump in + // the ground underneath it. Roads exist to be DRIVEN on, so this is a + // driveability setting, not a cosmetic one. + // + // 0.0 = hugs the land: the roadbed follows the terrain exactly. + // Never steps, but inherits every lump the ground has. + // 1.0 = holds a grade (default): each stretch of road is a straight + // ramp between its two path points, cutting through small + // bumps and filling small dips. Smoothest to drive. + // + // Values in between mix the two. Raise toward 1.0 for a more + // engineered road, lower toward 0.0 for a more rustic one. + // + // NOTE: this affects road ELEVATION (terrain shape under the road), + // not colour. Tune it properly once vehicles exist to drive on it. + public const float ROAD_GRADE_SMOOTHING = 1.0f; } } \ No newline at end of file diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs index c9b8f37..5170f0e 100644 --- a/Server/Scripts/ServerChunkManager.cs +++ b/Server/Scripts/ServerChunkManager.cs @@ -135,17 +135,36 @@ namespace IslaApocalypse.Server foreach (var seg in localRoadSegments) { - float dist = DistanceToLineSegment(currentPos, seg[0], seg[1]); + // 'alongT' tells us HOW FAR ALONG this segment the nearest point is + // (0 = at the start point, 1 = at the end point). + float dist = DistanceToLineSegment(currentPos, seg[0], seg[1], out float alongT); if (dist < minDist) { minDist = dist; if (dist <= shoulderRadius) { - // Sample the heightmap precisely at the center of the road segment - // so the whole road stays at a uniform elevation, ignoring the sloped mountain under it. - Vector2 midPoint = (seg[0] + seg[1]) / 2.0f; - float roadRaw = _blueprint.HeightMap[(int)midPoint.X, (int)midPoint.Y]; - closestRoadElevation = Mathf.Clamp(roadRaw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f); + // F2 FIX — the road elevation is now taken at the point on the + // segment CLOSEST TO US, not at the segment's midpoint. + // + // The old code gave every column near a segment that segment's + // single midpoint height, so each stretch of road was one flat + // plank and consecutive planks stepped up/down like a staircase. + // + // Two honest ways to read the height at our closest point: + // rampElevation - a straight line between this segment's two + // end points. Holds a grade; cuts and fills. + // landElevation - the actual terrain under that point. + // Hugs the land; inherits its bumps. + // ROAD_GRADE_SMOOTHING dials between them (see D-021). + // + // Either way it varies CONTINUOUSLY as we move along the road, + // which is what kills the steps. And because neighbouring + // segments share an end point, the height matches exactly where + // one segment hands over to the next — no seam at the joins. + float rampElevation = Mathf.Lerp(HeightAtPixel(seg[0]), HeightAtPixel(seg[1]), alongT); + float landElevation = HeightAtPixel(seg[0].Lerp(seg[1], alongT)); + + closestRoadElevation = Mathf.Lerp(landElevation, rampElevation, Constants.ROAD_GRADE_SMOOTHING); } } } @@ -210,21 +229,49 @@ namespace IslaApocalypse.Server renderer.RenderChunk(newChunk); } + /// + /// Turns a map-pixel position into a world surface height, with bounds clamping. + /// Same mapping used everywhere else: raw 0-1 heightmap value scaled into the + /// usable vertical band of the chunk. + /// + private float HeightAtPixel(Vector2 pixel) + { + int px = Mathf.Clamp((int)pixel.X, 0, _blueprint.MapSize - 1); + int py = Mathf.Clamp((int)pixel.Y, 0, _blueprint.MapSize - 1); + + float raw = _blueprint.HeightMap[px, py]; + return Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f); + } + /// /// Calculates the shortest distance from a point to a line segment defined by v and w. /// private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w) + { + return DistanceToLineSegment(point, v, w, out _); + } + + /// + /// Same as above, but also reports WHERE along the segment the nearest point falls: + /// is 0 at v, 1 at w. The road carving needs this so it can read + /// the height at the spot next to us instead of at the segment's midpoint. + /// + private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w, out float t) { float l2 = v.DistanceSquaredTo(w); - if (l2 == 0) return point.DistanceTo(v); // v == w case + if (l2 == 0) // v == w case + { + t = 0f; + return point.DistanceTo(v); + } // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. + // We find projection of point p onto the line. // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - float t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2)); - + t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2)); + // Projection falls on the segment - Vector2 projection = v + t * (w - v); + Vector2 projection = v + t * (w - v); return point.DistanceTo(projection); }