The blueprint has carried water since task 03 -- WBID per-pixel body id, WBTB body table, WSRF per-pixel surface level. Nothing ever drew it. Now the runtime does. No new water data: the blueprint is the authority on where water is and at what level, and this only reads it. WHY WATER IS ITS OWN MESH, not a block in the terrain field. The terrain is a Marching-Cubes iso-surface over ChunkData.Densities. Writing WATER into that field would not lay a sheet on top of the seabed -- it would move the iso-surface, fusing the sea into the terrain as if it were solid ground. A second surface is the only way water can sit at ITS level independent of the ground under it. So ChunkRenderer builds a water MeshInstance3D as a child of the chunk's terrain mesh. TRANSPARENCY IS SHIPPED, NOT DEFERRED (Step 2.4's cheap branch). Because water is a distinct MeshInstance3D it carries its own StandardMaterial3D, so alpha is one flag and Godot sorts transparent surfaces after opaque ones by itself. Zero mesher changes. Alpha runs 0.62 shallow -> 0.97 deep, so the shelved coast stays readable and open ocean closes up. WHICH DATA DROVE IT: WSRF for the level (per-pixel, quantised to 1/32768 raw ~ 7.7 mm, far under the 1 m voxel, and no table lookup), WBID for presence (it is the water stage's own classification output -- the same set the biome grid's Ocean/Lake pixels form by construction), WBTB as the fallback when a column is flagged wet but carries the WSRF no-water sentinel. A body the table does not know leaves the column DRY rather than guessing a level. Details worth keeping: - Water Y uses Constants.HEIGHT_SCALE, the SAME mapping as the terrain surface, named so the sheet and the seabed cannot drift apart if the vertical band is ever retuned. - Depth for shading comes from BLUEPRINT heights, not rendered geometry: the terrain render clamps its floor at Y=2, so every deep-ocean column would otherwise read as one flat ~36 m and the gradient would die exactly where the ocean gets interesting. - A cell is drawn if ANY corner is wet, flat at the highest wet level. Drawing onto a partly-dry cell is deliberate -- it carries the sheet under the shoreline where the opaque terrain hides it. Only-fully-wet cells retreat the waterline a metre and leave a dry gap around every coast and lake. - Non-metallic, roughness 0.35: the scene environment is minimal, and a metallic surface reads near-black when there is nothing to reflect. BlockRegistry gains WATER (id 11). Wire-safe: block IDs are never serialized -- the blueprint's section table stores heights, biome ordinals and water data, never block IDs, and chunks are not persisted yet. MEASURED on seed 1825907253: 241,415 water columns across 454 of 4096 chunks; surface Y 37.6..37.6 m (flat, = 0.15 x 251 -- the flat sea model, rendered); depth to 32.3 m, matching an independent read of the blueprint exactly. Both an ocean and a lake fall in the default view. NO REGRESSION to the 2D pipeline, verified section by section: a regeneration of the working seed is byte-identical to task 12's run in TCRV, TDTL, HGTS, BIOM, WBID, WBTB, WSRF, TOWN and all four road sections; only PRMS differs, and only in its write timestamp. All four snapshot PNGs md5-identical. No storms, no waves, no animation, no flow -- water at rest. Those are C2+. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
429 lines
18 KiB
C#
429 lines
18 KiB
C#
using Godot;
|
|
using System.Collections.Generic;
|
|
using IslaApocalypse.Core;
|
|
|
|
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
|
|
{
|
|
private WorldBlueprint _blueprint;
|
|
private Dictionary<Vector2I, ChunkData> _activeChunks = new Dictionary<Vector2I, ChunkData>();
|
|
public int chunkSize = 24;
|
|
|
|
// Water-at-rest diagnostics (task 13), summed across the chunks generated.
|
|
private long _waterColumnsRendered = 0;
|
|
private int _chunksWithWater = 0;
|
|
private float _waterMinY = float.MaxValue, _waterMaxY = float.MinValue, _waterMaxDepth = 0f;
|
|
|
|
public override void _Ready()
|
|
{
|
|
|
|
// 1. LOAD CONFIGURATION and the seed-based blueprint data from the MapDataParser!
|
|
ConfigManager.LoadConfig();
|
|
chunkSize = ConfigManager.ChunkRadius; // Update chunk radius from config file
|
|
_blueprint = MapDataParser.LoadMapData(ConfigManager.WorldSeed.ToString());
|
|
|
|
if (_blueprint != null)
|
|
{
|
|
// Params cross-check (v2 blueprints only): the file carries its resolved
|
|
// generation inputs, so a config edited after generation is detectable —
|
|
// canon H7's silent desync becomes loud. Warning, not an abort: the world
|
|
// still loads; the developer decides what to do about the mismatch.
|
|
if (_blueprint.Params != null)
|
|
{
|
|
var p = _blueprint.Params;
|
|
if (p.WorldSeed != BlueprintFormat.SENTINEL_WORLD_SEED && p.WorldSeed != ConfigManager.WorldSeed)
|
|
GD.PrintErr($"[Server] ⚠⚠ BLUEPRINT/CONFIG DESYNC: blueprint was generated with seed " +
|
|
$"{p.WorldSeed} but ServerConfig.json says {ConfigManager.WorldSeed}. " +
|
|
"The world you load is not the world this config describes.");
|
|
if (p.MapSize != ConfigManager.MapSize)
|
|
GD.PrintErr($"[Server] ⚠⚠ BLUEPRINT/CONFIG DESYNC: blueprint MapSize {p.MapSize} " +
|
|
$"vs config MapSize {ConfigManager.MapSize}.");
|
|
}
|
|
|
|
GD.Print("[Server] Blueprint loaded. Locating Capitol City...");
|
|
|
|
// 2. Find the Capitol in the parsed data
|
|
|
|
Vector2 capitolPos = new Vector2(2048, 2048); // Safe fallback center
|
|
foreach (var town in _blueprint.Towns) {
|
|
if (town.Tier == TownTier.Capitol) {
|
|
capitolPos = town.Position;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Old original png map coords
|
|
// Vector2 capitolPos = new Vector2(2405, 3296); // hardcoded for now since we know exactly where it is in this seed, which is the "paradise" biome hub
|
|
// New Manual Test point
|
|
// Vector2 capitolPos = new Vector2(4487, 4424);
|
|
|
|
GD.Print($"[Server] Capitol found at {capitolPos}. Generating chunks...");
|
|
|
|
// 3. Convert pixel coordinates to Chunk coordinates
|
|
int capitolChunkX = (int)(capitolPos.X / Constants.CHUNK_SIZE_X);
|
|
int capitolChunkZ = (int)(capitolPos.Y / Constants.CHUNK_SIZE_Z);
|
|
|
|
int radius = ConfigManager.ChunkRadius;
|
|
|
|
for (int x = capitolChunkX - radius; x < capitolChunkX + radius; x++)
|
|
{
|
|
for (int z = capitolChunkZ - radius; z < capitolChunkZ + radius; z++)
|
|
{
|
|
GenerateChunk(new Vector2I(x, z));
|
|
}
|
|
}
|
|
|
|
// Say what was actually drawn. The water is the blueprint's, not the
|
|
// runtime's, so if this reads zero the question is which of the two
|
|
// sides went quiet — and this line answers it without a debugger.
|
|
GD.Print($"[Server] Water at rest: {_waterColumnsRendered} water columns across " +
|
|
$"{_chunksWithWater} of {_activeChunks.Count} chunks" +
|
|
(_waterColumnsRendered > 0
|
|
? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m."
|
|
: " — nothing to draw here (check the blueprint carries WBID/WSRF)."));
|
|
|
|
// 5. Teleport the Camera to look down at our creation!
|
|
Camera3D cam = GetNodeOrNull<Camera3D>("Camera3D");
|
|
if (cam != null)
|
|
{
|
|
// Put the camera 120 meters in the air above the Capitol
|
|
cam.GlobalPosition = new Vector3(capitolPos.X, 120f, capitolPos.Y);
|
|
// Point it straight down at the ground
|
|
cam.LookAt(new Vector3(capitolPos.X, 0, capitolPos.Y));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void GenerateChunk(Vector2I chunkCoord)
|
|
{
|
|
if (_activeChunks.ContainsKey(chunkCoord)) return;
|
|
|
|
ChunkData newChunk = new ChunkData(chunkCoord);
|
|
|
|
int startX = chunkCoord.X * Constants.CHUNK_SIZE_X;
|
|
int startZ = chunkCoord.Y * Constants.CHUNK_SIZE_Z;
|
|
|
|
// --- SPATIAL CULLING FOR ROADS ---
|
|
// Bounding box for this chunk, padded so a road running just outside it still
|
|
// carves its shoulder in. The pad comes from the widest shoulder any tier has
|
|
// (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);
|
|
|
|
List<RoadSegment> localRoadSegments = new List<RoadSegment>();
|
|
|
|
// Keep only the segments that actually reach this chunk, and remember which
|
|
// TIER each came from — that is what decides how wide and smooth it carves.
|
|
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));
|
|
if (chunkBounds.Intersects(segBounds.Grow(1.0f))) {
|
|
localRoadSegments.Add(new RoadSegment(a, b, tier));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 z = 0; z <= Constants.CHUNK_SIZE_Z; z++)
|
|
{
|
|
int globalX = startX + x;
|
|
int globalZ = startZ + z;
|
|
|
|
if (globalX >= _blueprint.MapSize || globalZ >= _blueprint.MapSize || globalX < 0 || globalZ < 0)
|
|
continue;
|
|
|
|
float GetExactSurface(int gX, int gZ, out byte roadMaterial)
|
|
{
|
|
roadMaterial = BlockRegistry.AIR; // 0 == "not a road"
|
|
if (gX >= _blueprint.MapSize) gX = _blueprint.MapSize - 1;
|
|
if (gZ >= _blueprint.MapSize) gZ = _blueprint.MapSize - 1;
|
|
|
|
float raw = _blueprint.HeightMap[gX, gZ];
|
|
float baseHeight = Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
|
|
|
// If no roads are in this chunk, skip the heavy math entirely!
|
|
if (localRoadSegments.Count == 0) return baseHeight;
|
|
|
|
float finalHeight = baseHeight;
|
|
Vector2 currentPos = new Vector2(gX, gZ);
|
|
|
|
// Details of the nearest road that actually REACHES us. Each tier has
|
|
// its own reach, so "nearest" alone isn't enough — a footpath two
|
|
// metres away must not shadow a highway whose shoulder still covers
|
|
// 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)
|
|
{
|
|
RoadProfile profile = Constants.GetRoadProfile(seg.Tier);
|
|
|
|
// '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.A, seg.B, out float alongT);
|
|
|
|
if (dist > profile.ShoulderRadius) continue; // this road doesn't reach us
|
|
if (dist >= bestDist) continue; // a closer one already won
|
|
|
|
// F2 FIX — the road elevation is 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.
|
|
// 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,
|
|
// 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.A), HeightAtPixel(seg.B), alongT);
|
|
float landElevation = HeightAtPixel(seg.A.Lerp(seg.B, alongT));
|
|
|
|
foundRoad = true;
|
|
bestDist = dist;
|
|
bestProfile = profile;
|
|
bestRoadElevation = Mathf.Lerp(landElevation, rampElevation, profile.GradeSmoothing);
|
|
}
|
|
|
|
// THE BULLDOZER: Carve the terrain, using the winning road's own dimensions.
|
|
if (foundRoad)
|
|
{
|
|
if (bestDist <= bestProfile.RoadRadius) {
|
|
finalHeight = bestRoadElevation; // Flatten it completely!
|
|
roadMaterial = bestProfile.SurfaceMaterial;
|
|
} else {
|
|
// Smoothly interpolate from the flat road up/down to the natural mountain height
|
|
float t = (bestDist - bestProfile.RoadRadius) / (bestProfile.ShoulderRadius - bestProfile.RoadRadius);
|
|
t = t * t * (3f - 2f * t); // SmoothStep equation for a beautiful curved slope
|
|
finalHeight = Mathf.Lerp(bestRoadElevation, baseHeight, t);
|
|
}
|
|
}
|
|
|
|
return finalHeight;
|
|
}
|
|
|
|
// Pass the road surface out for the main voxel so we can paint it!
|
|
// 0 (AIR) means this column is not part of any road.
|
|
byte roadSurface = BlockRegistry.AIR;
|
|
float exactSurfaceY = GetExactSurface(globalX, globalZ, out roadSurface);
|
|
|
|
// We discard the 'out' variable for the normals using an underscore '_'
|
|
float surfaceRight = GetExactSurface(globalX + 1, globalZ, out _);
|
|
float surfaceFwd = GetExactSurface(globalX, globalZ + 1, out _);
|
|
|
|
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.ColumnRoadMaterial[x, z] = roadSurface;
|
|
|
|
// --- WATER AT REST (task 13) ---------------------------------
|
|
// The blueprint is the AUTHORITY on where water is; the runtime
|
|
// only draws it. WBID decides presence (it is the water stage's
|
|
// own classification output, the same set the biome grid's
|
|
// Ocean/Lake pixels form by construction), WSRF gives the level
|
|
// per pixel, and the WBTB body table is the fallback if a column
|
|
// is flagged wet but carries the WSRF no-water sentinel.
|
|
if (_blueprint.WaterBodyIds != null)
|
|
{
|
|
ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ];
|
|
if (bodyId != 0)
|
|
{
|
|
float levelRaw = -1f;
|
|
if (_blueprint.WaterSurfaceQ != null)
|
|
{
|
|
ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ];
|
|
if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q);
|
|
}
|
|
if (levelRaw < 0f) levelRaw = BodyLevel(bodyId);
|
|
|
|
if (levelRaw >= 0f)
|
|
{
|
|
// Same mapping as the terrain, so the sheet and the
|
|
// seabed cannot drift apart.
|
|
newChunk.WaterSurfaceY[x, z] = Mathf.Clamp(
|
|
levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
|
// TRUE depth, from blueprint units — see ChunkData.
|
|
newChunk.WaterDepthM[x, z] =
|
|
Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE);
|
|
newChunk.HasAnyWater = true;
|
|
|
|
_waterColumnsRendered++;
|
|
float wy = newChunk.WaterSurfaceY[x, z];
|
|
if (wy < _waterMinY) _waterMinY = wy;
|
|
if (wy > _waterMaxY) _waterMaxY = wy;
|
|
if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z];
|
|
}
|
|
}
|
|
}
|
|
|
|
float hRight = surfaceRight - exactSurfaceY;
|
|
float hFwd = surfaceFwd - exactSurfaceY;
|
|
float slopeX = hRight / Constants.VOXEL_SCALE;
|
|
float slopeZ = hFwd / Constants.VOXEL_SCALE;
|
|
|
|
float len = Mathf.Sqrt(slopeX * slopeX + 1.0f + slopeZ * slopeZ);
|
|
|
|
for (int y = 0; y <= Constants.CHUNK_HEIGHT; y++)
|
|
{
|
|
float verticalDist = y - exactSurfaceY;
|
|
float density = verticalDist / len;
|
|
|
|
newChunk.Densities[x, y, z] = density;
|
|
|
|
int blockY = Mathf.RoundToInt(exactSurfaceY);
|
|
|
|
// THE PAINT: Pass the road surface into the Biome Palette!
|
|
newChunk.BlockIDs[x, y, z] = BiomePalette.GetVoxelID(columnBiome, blockY, y, roadSurface);
|
|
}
|
|
} // End of Z loop
|
|
} // End of X loop
|
|
|
|
if (newChunk.HasAnyWater) _chunksWithWater++;
|
|
_activeChunks.Add(chunkCoord, newChunk);
|
|
|
|
var renderer = new IslaApocalypse.Client.ChunkRenderer();
|
|
AddChild(renderer);
|
|
renderer.RenderChunk(newChunk);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A water body's flat surface level, from the blueprint's WBTB table. Only
|
|
/// used as the fallback when a column is flagged wet by WBID but its WSRF
|
|
/// entry is the no-water sentinel — WSRF is the per-pixel authority, this is
|
|
/// the per-body one. Returns -1 if the body is unknown, which the caller
|
|
/// reads as "leave this column dry" rather than guessing a level.
|
|
/// </summary>
|
|
private float BodyLevel(ushort bodyId)
|
|
{
|
|
if (_blueprint.WaterBodies == null) return -1f;
|
|
foreach (var body in _blueprint.WaterBodies)
|
|
if (body.Id == bodyId) return body.SurfaceLevel;
|
|
return -1f;
|
|
}
|
|
|
|
/// <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.
|
|
///
|
|
/// The position handed in is usually FRACTIONAL (road path points land between
|
|
/// heightmap cells), so we read the four cells around it and blend — bilinear
|
|
/// interpolation — rather than snapping to whichever cell we happen to land in.
|
|
///
|
|
/// Why that matters: snapping made the height jump by a whole cell's worth the
|
|
/// instant a point crossed a cell boundary, and hold perfectly flat until it did.
|
|
/// Along a road whose points sit less than a metre apart, that produced a fine
|
|
/// metre-scale staircase — the "washboard". Blending makes the height vary
|
|
/// continuously as the position moves, so the staircase has nothing to stand on.
|
|
/// </summary>
|
|
private float HeightAtPixel(Vector2 pixel)
|
|
{
|
|
int max = _blueprint.MapSize - 1;
|
|
|
|
// The four heightmap cells surrounding this position.
|
|
// At the very edge of the map x1/y1 clamp back onto x0/y0, which makes the
|
|
// blend a harmless no-op instead of reading out of bounds.
|
|
int x0 = Mathf.Clamp(Mathf.FloorToInt(pixel.X), 0, max);
|
|
int y0 = Mathf.Clamp(Mathf.FloorToInt(pixel.Y), 0, max);
|
|
int x1 = Mathf.Clamp(x0 + 1, 0, max);
|
|
int y1 = Mathf.Clamp(y0 + 1, 0, max);
|
|
|
|
// How far between those cells we actually are, 0 to 1 on each axis.
|
|
// Clamped so a position outside the map can't push the blend past its corners.
|
|
float fx = Mathf.Clamp(pixel.X - x0, 0.0f, 1.0f);
|
|
float fy = Mathf.Clamp(pixel.Y - y0, 0.0f, 1.0f);
|
|
|
|
// Blend across X on the near row and the far row, then blend those two down Y.
|
|
float nearRow = Mathf.Lerp(_blueprint.HeightMap[x0, y0], _blueprint.HeightMap[x1, y0], fx);
|
|
float farRow = Mathf.Lerp(_blueprint.HeightMap[x0, y1], _blueprint.HeightMap[x1, y1], fx);
|
|
float raw = Mathf.Lerp(nearRow, farRow, fy);
|
|
|
|
return Mathf.Clamp(raw * (Constants.CHUNK_HEIGHT - 5), 2.0f, Constants.CHUNK_HEIGHT - 2.0f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the shortest distance from a point to a line segment defined by v and w.
|
|
/// </summary>
|
|
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);
|
|
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.
|
|
// It falls where t = [(p-v) . (w-v)] / |w-v|^2
|
|
t = Mathf.Max(0, Mathf.Min(1, (point - v).Dot(w - v) / l2));
|
|
|
|
// Projection falls on the segment
|
|
Vector2 projection = v + t * (w - v);
|
|
return point.DistanceTo(projection);
|
|
}
|
|
|
|
}
|
|
}
|