F2: continuous road grade — along-segment elevation + joint smoothing (D-021)
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) <noreply@anthropic.com>
This commit is contained in:
parent
ea11258871
commit
0910338a2a
2 changed files with 76 additions and 11 deletions
|
|
@ -23,5 +23,23 @@ namespace IslaApocalypse.Core
|
||||||
// Colour only. This does NOT affect terrain shape, collision, or what
|
// Colour only. This does NOT affect terrain shape, collision, or what
|
||||||
// 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 ---------------------------------------------
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,17 +135,36 @@ namespace IslaApocalypse.Server
|
||||||
|
|
||||||
foreach (var seg in localRoadSegments)
|
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)
|
if (dist < minDist)
|
||||||
{
|
{
|
||||||
minDist = dist;
|
minDist = dist;
|
||||||
if (dist <= shoulderRadius)
|
if (dist <= shoulderRadius)
|
||||||
{
|
{
|
||||||
// Sample the heightmap precisely at the center of the road segment
|
// F2 FIX — the road elevation is now taken at the point on the
|
||||||
// so the whole road stays at a uniform elevation, ignoring the sloped mountain under it.
|
// segment CLOSEST TO US, not at the segment's midpoint.
|
||||||
Vector2 midPoint = (seg[0] + seg[1]) / 2.0f;
|
//
|
||||||
float roadRaw = _blueprint.HeightMap[(int)midPoint.X, (int)midPoint.Y];
|
// The old code gave every column near a segment that segment's
|
||||||
closestRoadElevation = Mathf.Clamp(roadRaw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
// 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,18 +229,46 @@ namespace IslaApocalypse.Server
|
||||||
renderer.RenderChunk(newChunk);
|
renderer.RenderChunk(newChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the shortest distance from a point to a line segment defined by v and w.
|
/// Calculates the shortest distance from a point to a line segment defined by v and w.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w)
|
private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w)
|
||||||
|
{
|
||||||
|
return DistanceToLineSegment(point, v, w, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Same as above, but also reports WHERE along the segment the nearest point falls:
|
||||||
|
/// <paramref name="t"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
private float DistanceToLineSegment(Vector2 point, Vector2 v, Vector2 w, out float t)
|
||||||
{
|
{
|
||||||
float l2 = v.DistanceSquaredTo(w);
|
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).
|
// 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
|
// 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
|
// Projection falls on the segment
|
||||||
Vector2 projection = v + t * (w - v);
|
Vector2 projection = v + t * (w - v);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue