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:
parent
a827595d61
commit
f9ea2da3f4
7 changed files with 255 additions and 130 deletions
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue