Closes the C1-polish carryover. The cause was confirmed by a read-only dump BEFORE any code was written, per the filed note's discipline; the chat had guessed twice from screenshots. DIAGNOSIS. WBID is classified from the UNCURVED heightmap — the classify path that keeps the biome/water oracle byte-identical through all of Phase B — while the mesh renders the CURVED one. Outside the crater those two agree EXACTLY, and provably so: the curve is identity at sea and monotonic, so Apply(raw) < sea iff raw < sea. Inside the crater they do not. The carve lerps two different bases toward one target -- classify from raw, rendered from Apply(raw) -- and Apply(raw) < raw throughout the lowland band (the toe compresses raw 0.15..0.516 into 0.15..0.206). So the rendered surface sinks FASTER than the classify surface, leaving an annulus that renders below the waterline while WBID still calls it dry. MEASURED, seed 1825907253, map-wide: wet columns 32,799,866 WATER-OVER-DRY (wet, mesh above Y) 1,101 scattered, not the story DRY-UNDER-WATER (dry, mesh below sea) 375,824 <- the seam ...of which inside the crater carve: 100 % Dump across the gradient at z=905: at x=3800 the classify surface sits at 47.55 m while the mesh renders 22.38 m -- 25 m apart, no water drawn. The ring is west of the Capitol, which is exactly the developer's "flush on the right, lifted toward the left". Neither of the task's two candidate causes, and worth saying so: the water LEVEL is flat and correct at 37.65 everywhere it is drawn (rules out B), and the per-cell flat sheet is a rounding error next to this (1,101 px vs 375,824). It is the filed note's classify-vs-render mismatch, with the crater carve as the mechanism. THE FIX. Presence now takes two clauses: the blueprint's classification, OR the rendered ground actually being under the ocean's surface. The second clause tests exactly what the mesh draws (exactSurfaceY) against the OCEAN BODY'S OWN level from WBTB -- the runtime still derives nothing, it only notices that the ground it is drawing is under a surface the blueprint gave it. By the identity above that clause can only ever fire inside the carve, which is precisely the flooded bay it exists for. MEASURED after, same seed: 241,415 -> 358,576 water columns, 454 -> 648 chunks, 117,161 seam columns recovered in the loaded region. Each run now reports its own seam count. 2D PIPELINE UNTOUCHED, verified rather than asserted: the only changed file is the runtime chunk manager, and a regeneration is byte-identical to task 13's in every section but PRMS (timestamp + git hash). All four snapshot PNGs md5-identical. Static flat sheet, no animation, no water DATA change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
469 lines
20 KiB
C#
469 lines
20 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;
|
|
|
|
// Shoreline seam (task 15). The OCEAN body's own surface level, from the
|
|
// blueprint's WBTB table; -1 when the blueprint carries no ocean. Columns the
|
|
// mesh renders below this but WBID calls dry are the seam, and are counted.
|
|
private float _oceanLevelRaw = -1f;
|
|
private long _seamColumnsRecovered = 0;
|
|
|
|
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}.");
|
|
}
|
|
|
|
// The ocean's surface level, for the shoreline-seam rule below. Taken
|
|
// from the blueprint's body table (type OCEAN), never derived here —
|
|
// the runtime does not compute sea level (D-033).
|
|
if (_blueprint.WaterBodies != null)
|
|
foreach (var body in _blueprint.WaterBodies)
|
|
if (body.Type == WaterBodyInfo.TYPE_OCEAN) { _oceanLevelRaw = body.SurfaceLevel; break; }
|
|
|
|
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; " +
|
|
$"{_seamColumnsRecovered} shoreline-seam columns recovered (mesh below the ocean surface, WBID dry)."
|
|
: " — 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) + SHORELINE SEAM (task 15) ------
|
|
// The blueprint is the AUTHORITY on where water is and at what
|
|
// level; the runtime only draws it. 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.
|
|
//
|
|
// PRESENCE, though, takes two clauses. WBID was classified from the UNCURVED
|
|
// heightmap — the classify path that keeps the biome/water oracle
|
|
// byte-identical — while the mesh renders the CURVED one. Outside
|
|
// the crater those two agree exactly, because the curve is identity
|
|
// at sea and monotonic, so Apply(raw) < sea iff raw < sea. INSIDE
|
|
// the crater they do not: the carve lerps two different bases toward
|
|
// one target (classify from raw, rendered from Apply(raw), and
|
|
// Apply(raw) < raw throughout the lowland band), so the rendered
|
|
// surface sinks faster and leaves a ring that renders below the
|
|
// waterline while WBID still calls it dry. Measured on seed
|
|
// 1825907253: 375,824 such columns, 100 % of them inside the carve.
|
|
//
|
|
// So presence is decided by BOTH: the blueprint's classification,
|
|
// OR the rendered ground actually being under the ocean's surface.
|
|
// The second clause can only ever fire inside the carve (see the
|
|
// identity above), which is precisely the flooded bay it exists for.
|
|
if (_blueprint.WaterBodyIds != null)
|
|
{
|
|
ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ];
|
|
float levelRaw = -1f;
|
|
|
|
if (bodyId != 0)
|
|
{
|
|
if (_blueprint.WaterSurfaceQ != null)
|
|
{
|
|
ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ];
|
|
if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q);
|
|
}
|
|
if (levelRaw < 0f) levelRaw = BodyLevel(bodyId);
|
|
}
|
|
else if (_oceanLevelRaw >= 0f
|
|
&& exactSurfaceY < _oceanLevelRaw * Constants.HEIGHT_SCALE)
|
|
{
|
|
// Rendered ground below the ocean's own surface level. The
|
|
// level still comes from the blueprint (the ocean body's
|
|
// WBTB entry) — the runtime derives nothing, it only notices
|
|
// that the ground the MESH draws is under that surface.
|
|
levelRaw = _oceanLevelRaw;
|
|
_seamColumnsRecovered++;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
}
|