sindicate 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/docs/plans/kiwi-road-recon.md +97 -0
- package/docs/plans/road-system.md +142 -0
- package/docs/plans/three-cities.md +43 -0
- package/package.json +3 -2
- package/src/systems/train.js +16 -6
- package/src/vehicle/car.js +321 -0
- package/src/vehicle/carAudio.js +124 -0
- package/src/world/design.js +72 -0
- package/src/world/heightfield.js +146 -0
- package/src/world/kit.js +924 -0
- package/src/world/permanentWay.js +161 -0
- package/src/world/placement.js +309 -0
- package/src/world/roadAssert.js +125 -0
- package/src/world/roadKit.js +110 -0
- package/src/world/roadLink.js +343 -0
- package/src/world/roadNetwork.js +158 -0
- package/src/world/roadTerrain.js +377 -0
- package/src/world/roadWorld.js +200 -0
- package/src/world/waterLevels.js +95 -0
- package/src/world/waterMeshes.js +168 -0
- package/src/world/worldBase.js +493 -0
- package/tools/ExportKiwiRoadMaterials.cs +145 -0
- package/tools/ExportKiwiRoadRefs.cs +94 -0
- package/tools/buildKiwiRoadsPack.mjs +36 -0
- package/tools/designer-mcp.mjs +103 -0
- package/tools/designer.js +316 -0
- package/tools/devPlugin.js +57 -0
- package/tools/fbxToGlb.mjs +115 -0
- package/tools/glbWrite.mjs +62 -0
- package/tools/rebuildSimpleRoundabouts.mjs +211 -0
- package/tools/roadKitAnalyze.mjs +366 -0
- package/tools/truthToAssemblies.mjs +160 -0
- package/tools/unityMeshToGlb.mjs +95 -0
- package/tools/unityPackageExtract.mjs +56 -0
- package/tools/unityPrefabToAssembly.mjs +276 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// ROAD TERRAIN — the ground has to agree with the roads.
|
|
2
|
+
//
|
|
3
|
+
// Two edits, both applied by wrapping a game's own heightAt:
|
|
4
|
+
// · PADS — a junction is a rigid prefab: it cannot follow a slope, so the ground under its
|
|
5
|
+
// footprint is levelled and blended back out to the natural terrain.
|
|
6
|
+
// · CORRIDORS — a link road carries its own solved profile; the ground is cut or filled to
|
|
7
|
+
// meet it across the carriageway, then blended out over the verge.
|
|
8
|
+
//
|
|
9
|
+
// const heightAt = makeRoadTerrain(naturalHeightAt, { pads, corridors })
|
|
10
|
+
//
|
|
11
|
+
// Wrapping rather than baking means the terrain cook, collision, grass occupancy and
|
|
12
|
+
// anything else that samples the ground all see the same edited surface for free.
|
|
13
|
+
import { smoothstep } from './geo.js';
|
|
14
|
+
|
|
15
|
+
// segment lookup on a uniform grid — a world's worth of road is far too many segments to
|
|
16
|
+
// walk per lattice vertex, and the lattice is ~1.7M vertices
|
|
17
|
+
function segmentIndex(corridors, cell) {
|
|
18
|
+
const buckets = new Map();
|
|
19
|
+
const key = (i, j) => `${i},${j}`;
|
|
20
|
+
const segs = [];
|
|
21
|
+
for (const c of corridors) {
|
|
22
|
+
const line = c.centreline;
|
|
23
|
+
// a batter can run a long way when the road crosses a deep valley, so the bucket reach
|
|
24
|
+
// has to cover the whole earthwork, not just the carriageway
|
|
25
|
+
const reach = (c.halfWidth ?? 5) + (c.maxEarthwork ?? 90);
|
|
26
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
27
|
+
const a = line[i], b = line[i + 1];
|
|
28
|
+
const s = {
|
|
29
|
+
ax: a[0], ay: a[1], az: a[2], bx: b[0], by: b[1], bz: b[2],
|
|
30
|
+
half: c.halfWidth ?? 5, drop: c.drop ?? 0, batter: c.batter ?? 0.5,
|
|
31
|
+
};
|
|
32
|
+
const id = segs.push(s) - 1;
|
|
33
|
+
const i0 = Math.floor((Math.min(a[0], b[0]) - reach) / cell), i1 = Math.floor((Math.max(a[0], b[0]) + reach) / cell);
|
|
34
|
+
const j0 = Math.floor((Math.min(a[2], b[2]) - reach) / cell), j1 = Math.floor((Math.max(a[2], b[2]) + reach) / cell);
|
|
35
|
+
for (let ii = i0; ii <= i1; ii++) for (let jj = j0; jj <= j1; jj++) {
|
|
36
|
+
const k = key(ii, jj);
|
|
37
|
+
if (!buckets.has(k)) buckets.set(k, []);
|
|
38
|
+
buckets.get(k).push(id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
near(x, z) { return buckets.get(key(Math.floor(x / cell), Math.floor(z / cell))) ?? null; },
|
|
44
|
+
segs,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// How far outside a pad's footprint a point is (0 inside). A simple junction is a circle,
|
|
49
|
+
// but an interchange is a long rectangle with slip roads and spurs down one axis — flatten a
|
|
50
|
+
// circle under one of those and the arms stick out over thin air.
|
|
51
|
+
function padDistance(p, x, z) {
|
|
52
|
+
const dx = x - p.x, dz = z - p.z;
|
|
53
|
+
if (p.halfX == null) return Math.max(Math.hypot(dx, dz) - (p.radius ?? 0), 0);
|
|
54
|
+
const c = Math.cos(p.yaw ?? 0), s = Math.sin(p.yaw ?? 0);
|
|
55
|
+
const lx = Math.abs(dx * c - dz * s) - p.halfX; // into the pad's own frame
|
|
56
|
+
const lz = Math.abs(dx * s + dz * c) - p.halfZ;
|
|
57
|
+
return Math.hypot(Math.max(lx, 0), Math.max(lz, 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// THE GROUND UNDER A PIECE, TAKEN FROM THE PIECE ITSELF.
|
|
61
|
+
//
|
|
62
|
+
// The law: no road may fly with nothing under it — except a deck, which flies over the road
|
|
63
|
+
// it crosses. Both follow from one rule: the ground tracks the LOWEST DRIVEABLE SURFACE at
|
|
64
|
+
// each spot. Under a bridge the lowest surface is the carriageway below, so the ground is cut
|
|
65
|
+
// to it and the deck spans the gap; everywhere else the lowest surface is the only surface,
|
|
66
|
+
// so the ground comes up to meet it — slip roads included, wherever they curve.
|
|
67
|
+
//
|
|
68
|
+
// `points` must be DECK vertices only. Pillars, supports and retaining walls hang far below
|
|
69
|
+
// the road they carry; let those in and the ground is dug out from under the whole junction.
|
|
70
|
+
export function surfaceRaster(points, { x = 0, z = 0, yaw = 0, halfX, halfZ, res = 3, drop = 0.3, batter = 0.5 }) {
|
|
71
|
+
const nx = Math.max(1, Math.ceil((halfX * 2) / res));
|
|
72
|
+
const nz = Math.max(1, Math.ceil((halfZ * 2) / res));
|
|
73
|
+
const h = new Float32Array(nx * nz).fill(Infinity);
|
|
74
|
+
const c = Math.cos(yaw), s = Math.sin(yaw);
|
|
75
|
+
for (const p of points) {
|
|
76
|
+
const dx = p[0] - x, dz = p[2] - z;
|
|
77
|
+
const lx = dx * c - dz * s, lz = dx * s + dz * c; // into the piece's own frame
|
|
78
|
+
const i = Math.floor((lx + halfX) / res), j = Math.floor((lz + halfZ) / res);
|
|
79
|
+
if (i < 0 || j < 0 || i >= nx || j >= nz) continue;
|
|
80
|
+
const k = j * nx + i;
|
|
81
|
+
if (p[1] < h[k]) h[k] = p[1]; // LOWEST deck wins
|
|
82
|
+
}
|
|
83
|
+
// chamfer distance transform: every empty cell learns its nearest deck and how far away,
|
|
84
|
+
// so the batter can slope out of it in O(1) per query
|
|
85
|
+
const d = new Float32Array(nx * nz).fill(Infinity);
|
|
86
|
+
for (let k = 0; k < h.length; k++) if (h[k] < Infinity) d[k] = 0;
|
|
87
|
+
const relax = (k, k2, cost) => { if (d[k2] + cost < d[k]) { d[k] = d[k2] + cost; h[k] = h[k2]; } };
|
|
88
|
+
for (let j = 0; j < nz; j++) for (let i = 0; i < nx; i++) {
|
|
89
|
+
const k = j * nx + i;
|
|
90
|
+
if (i > 0) relax(k, k - 1, res);
|
|
91
|
+
if (j > 0) relax(k, k - nx, res);
|
|
92
|
+
if (i > 0 && j > 0) relax(k, k - nx - 1, res * 1.414);
|
|
93
|
+
if (i < nx - 1 && j > 0) relax(k, k - nx + 1, res * 1.414);
|
|
94
|
+
}
|
|
95
|
+
for (let j = nz - 1; j >= 0; j--) for (let i = nx - 1; i >= 0; i--) {
|
|
96
|
+
const k = j * nx + i;
|
|
97
|
+
if (i < nx - 1) relax(k, k + 1, res);
|
|
98
|
+
if (j < nz - 1) relax(k, k + nx, res);
|
|
99
|
+
if (i < nx - 1 && j < nz - 1) relax(k, k + nx + 1, res * 1.414);
|
|
100
|
+
if (i > 0 && j < nz - 1) relax(k, k + nx - 1, res * 1.414);
|
|
101
|
+
}
|
|
102
|
+
return { nx, nz, res, x, z, yaw, halfX, halfZ, h, d, drop, batter };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function rasterAt(r, x, z) {
|
|
106
|
+
const c = Math.cos(r.yaw), s = Math.sin(r.yaw);
|
|
107
|
+
const dx = x - r.x, dz = z - r.z;
|
|
108
|
+
const lx = dx * c - dz * s, lz = dx * s + dz * c;
|
|
109
|
+
const outX = Math.max(Math.abs(lx) - r.halfX, 0), outZ = Math.max(Math.abs(lz) - r.halfZ, 0);
|
|
110
|
+
const i = Math.min(Math.max(Math.floor((lx + r.halfX) / r.res), 0), r.nx - 1);
|
|
111
|
+
const j = Math.min(Math.max(Math.floor((lz + r.halfZ) / r.res), 0), r.nz - 1);
|
|
112
|
+
const k = j * r.nx + i;
|
|
113
|
+
if (!(r.h[k] < Infinity)) return null;
|
|
114
|
+
return { y: r.h[k], d: r.d[k] + Math.hypot(outX, outZ) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Pads get a bucket grid too. A world has tens of junctions and the lattice asks this
|
|
118
|
+
// function ~1.7 million times for a cold cook: scanning every pad per call is 40 million
|
|
119
|
+
// distance tests nobody needs, and it showed up as a 25x cost over the bare terrain.
|
|
120
|
+
function padIndex(pads, cell) {
|
|
121
|
+
const buckets = new Map();
|
|
122
|
+
pads.forEach((p, id) => {
|
|
123
|
+
const reach = (p.halfX != null ? Math.hypot(p.halfX, p.halfZ) : (p.radius ?? 0))
|
|
124
|
+
+ Math.max(p.fade ?? 0, p.maxEarthwork ?? 120);
|
|
125
|
+
const i0 = Math.floor((p.x - reach) / cell), i1 = Math.floor((p.x + reach) / cell);
|
|
126
|
+
const j0 = Math.floor((p.z - reach) / cell), j1 = Math.floor((p.z + reach) / cell);
|
|
127
|
+
for (let i = i0; i <= i1; i++) for (let j = j0; j <= j1; j++) {
|
|
128
|
+
const k = `${i},${j}`;
|
|
129
|
+
if (!buckets.has(k)) buckets.set(k, []);
|
|
130
|
+
buckets.get(k).push(id);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return (x, z) => buckets.get(`${Math.floor(x / cell)},${Math.floor(z / cell)}`) ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function makeRoadTerrain(baseHeightAt, { pads = [], corridors = [], surfaces = [], cell = 32, clearance = 0.08 } = {}) {
|
|
137
|
+
const index = corridors.length ? segmentIndex(corridors, cell) : null;
|
|
138
|
+
const nearPads = pads.length ? padIndex(pads, cell) : null;
|
|
139
|
+
|
|
140
|
+
// WHERE THE DRIVEABLE SURFACE IS. The ground is deliberately cut a little below the road so
|
|
141
|
+
// the two do not fight for the same pixels, which means anything that drives — the player's
|
|
142
|
+
// car, traffic, a pedestrian on a bridge — must ask for the road, not the dirt.
|
|
143
|
+
// Returns null away from any road, so the caller falls back to the ground.
|
|
144
|
+
heightAt.road = function roadAt(x, z, preferY = null) {
|
|
145
|
+
const found = [];
|
|
146
|
+
// pads are driveable too: a junction's deck is where its own roads sit
|
|
147
|
+
for (const pid of nearPads?.(x, z) ?? []) {
|
|
148
|
+
const p = pads[pid];
|
|
149
|
+
if (padDistance(p, x, z) <= 0) found.push(p.y);
|
|
150
|
+
}
|
|
151
|
+
if (index) {
|
|
152
|
+
const ids = index.near(x, z);
|
|
153
|
+
for (const id of ids ?? []) {
|
|
154
|
+
const s = index.segs[id];
|
|
155
|
+
const dx = s.bx - s.ax, dz = s.bz - s.az;
|
|
156
|
+
const len2 = dx * dx + dz * dz || 1e-6;
|
|
157
|
+
const t = Math.min(1, Math.max(0, ((x - s.ax) * dx + (z - s.az) * dz) / len2));
|
|
158
|
+
if (Math.hypot(x - (s.ax + dx * t), z - (s.az + dz * t)) > s.half) continue;
|
|
159
|
+
found.push(s.ay + (s.by - s.ay) * t);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!found.length) return null;
|
|
163
|
+
// MULTI-LEVEL: where a bridge crosses a cutting, or a link overlaps an interchange's
|
|
164
|
+
// trench, several surfaces cover the same spot. Answer with the one nearest the height
|
|
165
|
+
// the caller is already at — you stay on the deck you are driving on, you do not fall
|
|
166
|
+
// through it onto the road below.
|
|
167
|
+
if (preferY == null) return Math.max(...found);
|
|
168
|
+
// 'low' / 'high' ask for a level outright. (A non-finite preference cannot do this by
|
|
169
|
+
// arithmetic: |y - -Infinity| is Infinity for EVERY candidate, so the nearest-wins
|
|
170
|
+
// comparison is always false and the first deck found is returned regardless.)
|
|
171
|
+
if (preferY === 'low' || preferY === -Infinity) return Math.min(...found);
|
|
172
|
+
if (preferY === 'high' || preferY === Infinity) return Math.max(...found);
|
|
173
|
+
return found.reduce((best, y) => (Math.abs(y - preferY) < Math.abs(best - preferY) ? y : best));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// JUST THE CARRIAGEWAYS — no pads. A pad is a levelled area a junction stands on, not a
|
|
177
|
+
// road deck: anything asking "is this ground hidden under tarmac?" must not be told yes
|
|
178
|
+
// for the whole footprint of an interchange, or it will remove ground you can plainly see.
|
|
179
|
+
// `shrink` narrows the corridor before answering. A corridor's bed is deliberately wider
|
|
180
|
+
// than its tarmac (the road needs level ground beside it), so anything using this to decide
|
|
181
|
+
// "is the ground hidden here?" must ask about the ROAD, not the bed — or it cuts away a
|
|
182
|
+
// strip of terrain along both edges that nothing is covering, leaving a gap at the kerb.
|
|
183
|
+
heightAt.deck = function deckAt(x, z, shrink = 0) {
|
|
184
|
+
if (!index) return null;
|
|
185
|
+
const ids = index.near(x, z);
|
|
186
|
+
if (!ids) return null;
|
|
187
|
+
let lowest = null;
|
|
188
|
+
for (const id of ids) {
|
|
189
|
+
const s = index.segs[id];
|
|
190
|
+
const dx = s.bx - s.ax, dz = s.bz - s.az;
|
|
191
|
+
const len2 = dx * dx + dz * dz || 1e-6;
|
|
192
|
+
const t = Math.min(1, Math.max(0, ((x - s.ax) * dx + (z - s.az) * dz) / len2));
|
|
193
|
+
if (Math.hypot(x - (s.ax + dx * t), z - (s.az + dz * t)) > s.half - shrink) continue;
|
|
194
|
+
const y = s.ay + (s.by - s.ay) * t;
|
|
195
|
+
if (lowest == null || y < lowest) lowest = y;
|
|
196
|
+
}
|
|
197
|
+
return lowest;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function heightAt(x, z) {
|
|
201
|
+
let h = baseHeightAt(x, z);
|
|
202
|
+
|
|
203
|
+
// ── junction pads: level, because a prefab junction cannot bend ──
|
|
204
|
+
for (const pid of nearPads?.(x, z) ?? []) {
|
|
205
|
+
const p = pads[pid];
|
|
206
|
+
const d = padDistance(p, x, z);
|
|
207
|
+
// sit the dirt a little under the deck, or the two surfaces fight for the same pixels
|
|
208
|
+
const y = p.y - (p.drop ?? 0);
|
|
209
|
+
if (d <= 0) { h = y; continue; } // inside the footprint: level
|
|
210
|
+
// A FADE, NOT A CUT. An embankment batter is right for a road crossing a valley and
|
|
211
|
+
// wrong for the ground around a junction: it leaves a visible rim, and the eye reads
|
|
212
|
+
// the whole pad as a platform someone dropped on the landscape. Kiwi's answer, and
|
|
213
|
+
// the right one, is to ease back to the ORIGINAL height over a generous distance —
|
|
214
|
+
// far enough that you cannot tell the ground was levelled at all.
|
|
215
|
+
const fade = p.fade ?? 0;
|
|
216
|
+
if (fade > 0) {
|
|
217
|
+
if (d >= fade) continue; // beyond the fade: natural ground
|
|
218
|
+
const t = smoothstep(0, 1, d / fade);
|
|
219
|
+
h = y + (h - y) * t;
|
|
220
|
+
} else {
|
|
221
|
+
const reach = d * (p.batter ?? 0.5); // the old earthwork law
|
|
222
|
+
h = Math.min(Math.max(h, y - reach), y + reach);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── piece decks: cut the plateau open wherever a deck runs BELOW it ──
|
|
227
|
+
// The pad above gives a junction the level ground it needs. This pass then digs out the
|
|
228
|
+
// parts that live under grade — the motorway through an overpass, the slip roads
|
|
229
|
+
// descending to it — so those are open to the sky and the deck above spans a real gap.
|
|
230
|
+
// It only ever lowers: raising here would make the ground follow every kerb and ramp,
|
|
231
|
+
// which turns a junction into a lumpy mess.
|
|
232
|
+
for (const r of surfaces) {
|
|
233
|
+
const hit = rasterAt(r, x, z);
|
|
234
|
+
if (!hit) continue;
|
|
235
|
+
const y = hit.y - r.drop;
|
|
236
|
+
// retaining-wall gradient out of the cut, so it stays tight to the road below
|
|
237
|
+
h = Math.min(h, y + hit.d * (r.wall ?? 3));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── link corridors: cut/fill the ground to the road's own profile ──
|
|
241
|
+
if (index) {
|
|
242
|
+
const ids = index.near(x, z);
|
|
243
|
+
if (ids) {
|
|
244
|
+
let best = null;
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
const s = index.segs[id];
|
|
247
|
+
const dx = s.bx - s.ax, dz = s.bz - s.az;
|
|
248
|
+
const len2 = dx * dx + dz * dz || 1e-6;
|
|
249
|
+
const t = Math.min(1, Math.max(0, ((x - s.ax) * dx + (z - s.az) * dz) / len2));
|
|
250
|
+
const px = s.ax + dx * t, pz = s.az + dz * t;
|
|
251
|
+
const d = Math.hypot(x - px, z - pz);
|
|
252
|
+
if (!best || d < best.d) best = { d, s, y: s.ay + (s.by - s.ay) * t };
|
|
253
|
+
}
|
|
254
|
+
if (best) {
|
|
255
|
+
const road = best.y - best.s.drop;
|
|
256
|
+
if (best.d <= best.s.half) h = road; // the bed itself
|
|
257
|
+
else {
|
|
258
|
+
// EMBANKMENT / CUTTING: outside the bed the ground may only climb away from the
|
|
259
|
+
// road at the batter slope (1:2 by default). Fill rises to meet a road above the
|
|
260
|
+
// valley, cutting falls away from a road below the hill, and natural ground
|
|
261
|
+
// further out is left alone — no fixed blend distance to get wrong.
|
|
262
|
+
const reach = (best.d - best.s.half) * best.s.batter;
|
|
263
|
+
h = Math.min(Math.max(h, road - reach), road + reach);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// THE GUARANTEE: ground never sits above a road. The clearance is a hair — just enough
|
|
270
|
+
// that the two surfaces do not fight for the same pixels. It was much larger while this
|
|
271
|
+
// clamp was silently broken, and that showed as a step along every road edge: the ground
|
|
272
|
+
// beside a kerb sitting half a metre below the tarmac it should meet. Corridor and pad bookkeeping alone
|
|
273
|
+
// cannot promise it — a terrain lattice is metres wide, so a vertex just outside a
|
|
274
|
+
// road's flat bed drags its triangle up across the carriageway and lays a wedge of dirt
|
|
275
|
+
// over the tarmac. Every consumer of this function (the mesh, the cook, collision,
|
|
276
|
+
// grass) gets the same promise, in every game, without having to know the rule exists.
|
|
277
|
+
const deck = heightAt.road(x, z, 'low');
|
|
278
|
+
if (deck != null && h > deck - clearance) h = deck - clearance;
|
|
279
|
+
return h;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return heightAt;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// The height a junction should sit at: the average ground across its footprint, so it is
|
|
286
|
+
// neither buried on one side nor stilted on the other. `spread` is how much the ground
|
|
287
|
+
// varies underneath — a site law: junctions go on the flats.
|
|
288
|
+
export function seatHeight(heightAt, x, z, radius, rings = 3, spokes = 8) {
|
|
289
|
+
// `radius` may be a footprint {halfX, halfZ, yaw}; sample its own extent in that case
|
|
290
|
+
if (typeof radius === 'object' && radius) {
|
|
291
|
+
const { halfX, halfZ, yaw = 0 } = radius;
|
|
292
|
+
const c = Math.cos(yaw), sn = Math.sin(yaw);
|
|
293
|
+
let sum = 0, n = 0, lo = Infinity, hi = -Infinity;
|
|
294
|
+
for (let i = -rings; i <= rings; i++) {
|
|
295
|
+
for (let j = -rings; j <= rings; j++) {
|
|
296
|
+
const lx = (i / rings) * halfX, lz = (j / rings) * halfZ;
|
|
297
|
+
const h = heightAt(x + lx * c + lz * sn, z - lx * sn + lz * c);
|
|
298
|
+
sum += h; n++;
|
|
299
|
+
if (h < lo) lo = h;
|
|
300
|
+
if (h > hi) hi = h;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { y: sum / n, spread: hi - lo };
|
|
304
|
+
}
|
|
305
|
+
let sum = heightAt(x, z), n = 1, lo = Infinity, hi = -Infinity;
|
|
306
|
+
for (let r = 1; r <= rings; r++) {
|
|
307
|
+
const rad = (radius * r) / rings;
|
|
308
|
+
for (let a = 0; a < spokes; a++) {
|
|
309
|
+
const th = (a / spokes) * Math.PI * 2;
|
|
310
|
+
const h = heightAt(x + Math.cos(th) * rad, z + Math.sin(th) * rad);
|
|
311
|
+
sum += h; n++;
|
|
312
|
+
if (h < lo) lo = h;
|
|
313
|
+
if (h > hi) hi = h;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { y: sum / n, spread: hi - lo };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Where should a junction go? Sample the map, keep the flat spots, and take the pair that is
|
|
320
|
+
// both well separated and interestingly different in height — the "sites go on the flats"
|
|
321
|
+
// law from the road plan, applied automatically.
|
|
322
|
+
// Several sites at once, for a whole map: flat enough to stand a junction on, spread out
|
|
323
|
+
// enough to be separate places. Greedy farthest-point selection keeps them from clumping.
|
|
324
|
+
export function pickManySites(heightAt, { count = 5, half = 600, step = 90, radius = 60, minGap = 300, maxGrade = 0 } = {}) {
|
|
325
|
+
const cands = [];
|
|
326
|
+
for (let x = -half; x <= half; x += step) {
|
|
327
|
+
for (let z = -half; z <= half; z += step) {
|
|
328
|
+
const seat = seatHeight(heightAt, x, z, radius);
|
|
329
|
+
if (seat.spread > radius * 0.3) continue;
|
|
330
|
+
cands.push({ x, z, y: seat.y, spread: seat.spread });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!cands.length) return [];
|
|
334
|
+
cands.sort((a, b) => a.spread - b.spread); // flattest first
|
|
335
|
+
const chosen = [cands[0]];
|
|
336
|
+
for (const c of cands) {
|
|
337
|
+
if (chosen.length >= count) break;
|
|
338
|
+
const ok = chosen.every((s) => {
|
|
339
|
+
const gap = Math.hypot(s.x - c.x, s.z - c.z);
|
|
340
|
+
if (gap < minGap) return false;
|
|
341
|
+
// AND A ROAD MUST BE ABLE TO REACH IT. Picking the flattest spots regardless of each
|
|
342
|
+
// other's height cheerfully pairs a hilltop with a valley floor, and no road between
|
|
343
|
+
// them can be built at a sane grade — the link then quietly exceeds it.
|
|
344
|
+
if (maxGrade > 0 && Math.abs(s.y - c.y) > gap * maxGrade * 0.55) return false;
|
|
345
|
+
return true;
|
|
346
|
+
});
|
|
347
|
+
if (ok) chosen.push(c);
|
|
348
|
+
}
|
|
349
|
+
return chosen;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function pickSites(heightAt, { half = 600, step = 90, radius = 60, minGap = 340, maxGap = 760, maxGrade = 0.07 } = {}) {
|
|
353
|
+
const cands = [];
|
|
354
|
+
for (let x = -half; x <= half; x += step) {
|
|
355
|
+
for (let z = -half; z <= half; z += step) {
|
|
356
|
+
const seat = seatHeight(heightAt, x, z, radius);
|
|
357
|
+
if (seat.spread > radius * 0.28) continue; // too lumpy to stand a junction on
|
|
358
|
+
cands.push({ x, z, y: seat.y, spread: seat.spread });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
let best = null;
|
|
362
|
+
for (let i = 0; i < cands.length; i++) {
|
|
363
|
+
for (let j = i + 1; j < cands.length; j++) {
|
|
364
|
+
const a = cands[i], b = cands[j];
|
|
365
|
+
const gap = Math.hypot(a.x - b.x, a.z - b.z);
|
|
366
|
+
if (gap < minGap || gap > maxGap) continue;
|
|
367
|
+
// interesting relief, but only as much as a road can actually climb between them:
|
|
368
|
+
// pairing the highest peak with the deepest valley just guarantees an unbuildable link
|
|
369
|
+
const dy = Math.abs(a.y - b.y);
|
|
370
|
+
const climbable = gap * maxGrade * 0.55;
|
|
371
|
+
const relief = dy <= climbable ? dy : climbable - (dy - climbable) * 3;
|
|
372
|
+
const score = relief * 2 - (a.spread + b.spread) + gap * 0.02;
|
|
373
|
+
if (!best || score > best.score) best = { a, b, gap, score };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return best;
|
|
377
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// ROADS AS A WORLD-BUILD STEP.
|
|
2
|
+
//
|
|
3
|
+
// The pieces exist — a kit to read, a network to solve, links to generate, earthworks to cut.
|
|
4
|
+
// This is the one call that runs them in the right order and hands back everything a world
|
|
5
|
+
// needs, so a game does not have to know that order (and cannot get it wrong):
|
|
6
|
+
//
|
|
7
|
+
// const roads = await buildRoadWorld({ sites, kit, candidates, ground, placePiece });
|
|
8
|
+
// const heightAt = roads.heightAt; // ← give THIS to buildHeightfield
|
|
9
|
+
// scene.add(...roads.meshes); // ← and stamp these BEFORE the BVH bake
|
|
10
|
+
// for (const m of roads.colliders) bvhSources.push(m);
|
|
11
|
+
//
|
|
12
|
+
// THE ORDER IS THE POINT, and it is not obvious:
|
|
13
|
+
// 1. site the junctions on flat ground — before anything is placed
|
|
14
|
+
// 2. seat each piece at ITS grade level — multi-level pieces are authored high
|
|
15
|
+
// 3. wrap the ground with the pads — so links can follow a padded surface
|
|
16
|
+
// 4. solve and generate the links — they read that padded ground
|
|
17
|
+
// 5. wrap again with pads AND corridors — the final heightAt the world uses
|
|
18
|
+
// Do 4 before 3 and links follow raw terrain into the sides of junctions; do 5 before 4 and
|
|
19
|
+
// there are no corridors to add yet.
|
|
20
|
+
//
|
|
21
|
+
// `placePiece(name, { x, y, z, yaw })` is the game's: it loads the assembly, adds it to the
|
|
22
|
+
// scene and returns `{ object, sockets, deckPoints }`. The engine never loads assets.
|
|
23
|
+
import { makeRoadTerrain, seatHeight, pickManySites } from './roadTerrain.js';
|
|
24
|
+
import { solveNetwork, planLoop, loopTangents } from './roadNetwork.js';
|
|
25
|
+
import { buildRoadLink, buildDeck } from './roadLink.js';
|
|
26
|
+
import { seatY, gradeLevel } from './roadKit.js';
|
|
27
|
+
|
|
28
|
+
const footprintOf = (piece, margin = 6) => ({
|
|
29
|
+
halfX: (piece.bbox.max[0] - piece.bbox.min[0]) / 2 + margin,
|
|
30
|
+
halfZ: (piece.bbox.max[2] - piece.bbox.min[2]) / 2 + margin,
|
|
31
|
+
cx: (piece.bbox.max[0] + piece.bbox.min[0]) / 2,
|
|
32
|
+
cz: (piece.bbox.max[2] + piece.bbox.min[2]) / 2,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export async function buildRoadWorld({
|
|
36
|
+
kit, // from makeRoadKit
|
|
37
|
+
ground, // the game's natural heightAt
|
|
38
|
+
placePiece, // (name, transform) => { object, sockets, deckPoints }
|
|
39
|
+
sites = null, // explicit sites, or let pickManySites choose
|
|
40
|
+
candidates = [], // junction piece names the solver may use
|
|
41
|
+
count = 6, // how many places, when choosing them
|
|
42
|
+
half = 600, minGap = 420, // where they may go
|
|
43
|
+
loop = false, // a closed circuit rather than a network
|
|
44
|
+
maxGrade = 0.07,
|
|
45
|
+
fade = 90, // how far a junction pad eases back into the landscape
|
|
46
|
+
drop = 0.12, // ground below a deck: a hair, so they do not z-fight
|
|
47
|
+
linkStep = 3,
|
|
48
|
+
handleScale = 0.4,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const pieceOf = (n) => (kit.piece ? kit.piece(n) : kit.pieces?.[n]);
|
|
51
|
+
const pad = Math.max(...candidates.map((n) => pieceOf(n)?.size?.[0] ?? 100)) * 0.55;
|
|
52
|
+
|
|
53
|
+
// 1. WHERE THE PLACES GO — flat enough to stand a junction on, far enough apart to be
|
|
54
|
+
// separate places
|
|
55
|
+
const chosen = sites ?? pickManySites(ground, { count, half, step: 70, radius: pad, minGap, maxGrade });
|
|
56
|
+
if (chosen.length < 2) return { heightAt: ground, meshes: [], colliders: [], network: null, sites: chosen };
|
|
57
|
+
|
|
58
|
+
// 2. THE NETWORK — who joins whom, and what junction each place needs
|
|
59
|
+
const order = loop ? planLoop(chosen) : null;
|
|
60
|
+
const facing = loop ? loopTangents(chosen, order) : null;
|
|
61
|
+
const net = loop ? null : solveNetwork({ sites: chosen, kit, candidates, kinds: ['road'] });
|
|
62
|
+
|
|
63
|
+
// 3. PLACE EACH JUNCTION, seated at its own grade level
|
|
64
|
+
const placed = new Map();
|
|
65
|
+
const pads = [];
|
|
66
|
+
const placeOne = async (index, name, yaw) => {
|
|
67
|
+
const piece = pieceOf(name);
|
|
68
|
+
if (!piece) return;
|
|
69
|
+
const st = chosen[index];
|
|
70
|
+
const p = await placePiece(name, { x: st.x, y: seatY(piece, st.y), z: st.z, yaw });
|
|
71
|
+
if (!p) return;
|
|
72
|
+
const foot = footprintOf(piece);
|
|
73
|
+
const c = Math.cos(yaw), s = Math.sin(yaw);
|
|
74
|
+
pads.push({
|
|
75
|
+
x: st.x + foot.cx * c + foot.cz * s, z: st.z - foot.cx * s + foot.cz * c,
|
|
76
|
+
y: st.y, yaw, halfX: foot.halfX, halfZ: foot.halfZ, drop, fade,
|
|
77
|
+
});
|
|
78
|
+
placed.set(index, { ...p, site: st, yaw, name, piece });
|
|
79
|
+
};
|
|
80
|
+
if (loop) for (const { index, yaw } of facing) await placeOne(index, candidates[0], yaw);
|
|
81
|
+
else for (const node of net.nodes) if (node.name) await placeOne(node.index, node.name, node.yaw);
|
|
82
|
+
|
|
83
|
+
// 4. THE LINKS — generated over ground that already knows about the pads, so a road
|
|
84
|
+
// approaches a junction over its own levelled apron rather than raw hillside
|
|
85
|
+
const padded = makeRoadTerrain(ground, { pads });
|
|
86
|
+
const kinds = loop ? ['motorway'] : ['road'];
|
|
87
|
+
const facingSocket = (p, tx, tz) => (p.sockets ?? [])
|
|
88
|
+
.filter((s) => kinds.includes(s.kind ?? 'road'))
|
|
89
|
+
.reduce((best, s) => {
|
|
90
|
+
const v = [tx - s.pos[0], tz - s.pos[2]];
|
|
91
|
+
const len = Math.hypot(...v) || 1;
|
|
92
|
+
const sc = (s.dir[0] * v[0] + s.dir[1] * v[1]) / len;
|
|
93
|
+
return !best || sc > best.sc ? { s, sc } : best;
|
|
94
|
+
}, null)?.s ?? null;
|
|
95
|
+
|
|
96
|
+
// IN A LOOP THE TWO ENDS ARE ASSIGNED, not chosen independently. A through-piece has
|
|
97
|
+
// exactly two motorway sockets and exactly two neighbours; picking the best-facing socket
|
|
98
|
+
// for each neighbour separately lets both pick the SAME end on a turn, and the second road
|
|
99
|
+
// is then dropped for want of an arm — a circuit with holes in it.
|
|
100
|
+
const assigned = new Map();
|
|
101
|
+
if (loop) {
|
|
102
|
+
for (let k = 0; k < order.length; k++) {
|
|
103
|
+
const me = placed.get(order[k]);
|
|
104
|
+
if (!me) continue;
|
|
105
|
+
const prev = placed.get(order[(k - 1 + order.length) % order.length]);
|
|
106
|
+
const next = placed.get(order[(k + 1) % order.length]);
|
|
107
|
+
const mw = (me.sockets ?? []).filter((sk) => sk.kind === 'motorway');
|
|
108
|
+
if (mw.length < 2 || !prev || !next) continue;
|
|
109
|
+
const toward = (sk, t) => {
|
|
110
|
+
const v = [t.site.x - sk.pos[0], t.site.z - sk.pos[2]];
|
|
111
|
+
const len = Math.hypot(...v) || 1;
|
|
112
|
+
return (sk.dir[0] * v[0] + sk.dir[1] * v[1]) / len;
|
|
113
|
+
};
|
|
114
|
+
// take whichever pairing serves both neighbours better overall
|
|
115
|
+
const straight = toward(mw[0], prev) + toward(mw[1], next);
|
|
116
|
+
const swapped = toward(mw[1], prev) + toward(mw[0], next);
|
|
117
|
+
assigned.set(order[k], straight >= swapped
|
|
118
|
+
? { prev: mw[0], next: mw[1] }
|
|
119
|
+
: { prev: mw[1], next: mw[0] });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const links = [];
|
|
124
|
+
const pairs = loop
|
|
125
|
+
? order.map((a, k) => [a, order[(k + 1) % order.length]])
|
|
126
|
+
: net.edges.map((e) => [e.a, e.b]);
|
|
127
|
+
const used = new Set();
|
|
128
|
+
for (const [ia, ib] of pairs) {
|
|
129
|
+
const A = placed.get(ia), B = placed.get(ib);
|
|
130
|
+
if (!A || !B) continue;
|
|
131
|
+
const from = assigned.get(ia)?.next ?? facingSocket(A, B.site.x, B.site.z);
|
|
132
|
+
const to = assigned.get(ib)?.prev ?? facingSocket(B, A.site.x, A.site.z);
|
|
133
|
+
if (!from || !to) continue;
|
|
134
|
+
const ka = `${ia}:${from.id}`, kb = `${ib}:${to.id}`;
|
|
135
|
+
if (used.has(ka) || used.has(kb)) continue; // one road per arm
|
|
136
|
+
used.add(ka); used.add(kb);
|
|
137
|
+
links.push({
|
|
138
|
+
a: ia, b: ib, from, to,
|
|
139
|
+
link: buildRoadLink({
|
|
140
|
+
from, to, groundAt: (x, z) => padded(x, z) + 0.15,
|
|
141
|
+
handleScale, step: linkStep, maxGrade,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5. THE EARTHWORKS the world actually uses: pads AND the corridors those links carry,
|
|
147
|
+
// plus a trench for anything a piece keeps below its own grade
|
|
148
|
+
const corridors = links.map(({ link, from }) => ({
|
|
149
|
+
centreline: link.centreline, halfWidth: from.width / 2 + 5, drop, batter: 0.5,
|
|
150
|
+
}));
|
|
151
|
+
for (const [, p] of placed) {
|
|
152
|
+
const mw = (p.sockets ?? []).filter((s) => s.kind === 'motorway');
|
|
153
|
+
if (mw.length < 2 || !p.deckPoints?.length) continue;
|
|
154
|
+
const a = mw[0].pos, b = mw[1].pos;
|
|
155
|
+
const ax = [b[0] - a[0], b[2] - a[2]];
|
|
156
|
+
const al = Math.hypot(...ax) || 1;
|
|
157
|
+
ax[0] /= al; ax[1] /= al;
|
|
158
|
+
const perp = [-ax[1], ax[0]];
|
|
159
|
+
const grade = p.site.y;
|
|
160
|
+
let wide = 0, lowest = Infinity;
|
|
161
|
+
for (const q of p.deckPoints) {
|
|
162
|
+
if (q[1] > grade - 1.5) continue;
|
|
163
|
+
const lat = Math.abs((q[0] - p.site.x) * perp[0] + (q[2] - p.site.z) * perp[1]);
|
|
164
|
+
if (lat > wide) wide = lat;
|
|
165
|
+
if (q[1] < lowest) lowest = q[1];
|
|
166
|
+
}
|
|
167
|
+
if (!(wide > 0)) continue;
|
|
168
|
+
// KEEP THE TRENCH TIGHT TO THE CARRIAGEWAY. Sizing it from everything below grade lets
|
|
169
|
+
// the slip roads — which sweep far out to either side as they climb — widen the cut until
|
|
170
|
+
// it swallows the whole junction, dropping the entire pad to motorway level and leaving
|
|
171
|
+
// every piece standing 10 m above its own ground.
|
|
172
|
+
const bed = Math.min(wide, Math.max(mw[0].width, mw[1].width) / 2 + 18) + 5;
|
|
173
|
+
corridors.push({
|
|
174
|
+
centreline: [
|
|
175
|
+
[a[0] - ax[0] * 30, Math.min(a[1], lowest), a[2] - ax[1] * 30],
|
|
176
|
+
[b[0] + ax[0] * 30, Math.min(b[1], lowest), b[2] + ax[1] * 30],
|
|
177
|
+
],
|
|
178
|
+
halfWidth: bed, drop, batter: 4,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
heightAt: makeRoadTerrain(ground, { pads, corridors }),
|
|
184
|
+
links,
|
|
185
|
+
pads,
|
|
186
|
+
corridors,
|
|
187
|
+
placed,
|
|
188
|
+
sites: chosen,
|
|
189
|
+
network: net,
|
|
190
|
+
order,
|
|
191
|
+
// the median strip through each junction: its two carriageways are separate meshes, and
|
|
192
|
+
// the ground would otherwise be visible dropping away between them
|
|
193
|
+
medianDecks: [...placed.values()].flatMap((p) => {
|
|
194
|
+
const mw = (p.sockets ?? []).filter((s) => s.kind === 'motorway');
|
|
195
|
+
if (mw.length < 2) return [];
|
|
196
|
+
const inner = (mw[0].section?.lines ?? []).map((b) => Math.abs(b.at)).sort((x, y) => x - y)[0] ?? 1.1;
|
|
197
|
+
return [buildDeck({ from: mw[0].pos, to: mw[1].pos, width: inner * 2 + 0.6 })];
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// THE WATER-LEVEL BAKE (world splits, step 3b) — R.wy, the per-vertex water surface every
|
|
2
|
+
// carve, sheet, swimmer and bridge reads. Moved verbatim from the games (identical bodies;
|
|
3
|
+
// western adds PASS 0 + the R.bd gates, which subsume fable's ungated loops — an unsolved
|
|
4
|
+
// course simply runs the live passes).
|
|
5
|
+
//
|
|
6
|
+
// PASS 0 — THE SURVEY GOVERNS (solved courses, R.bd present): wy = solved bed + baked
|
|
7
|
+
// reach depth, source/terminus lake pins, then strictly monotone — the uphill
|
|
8
|
+
// forgiveness below does not apply to a solved course.
|
|
9
|
+
// PASS 1 — raw level = lowest of the ±ww cross-section on the river-free ground, −1 m
|
|
10
|
+
// (cross-section reach capped at 10 m — the wide-trunk lesson: ±26 m banks
|
|
11
|
+
// reached distant low ground and PASS 2 dragged whole reaches to sea level).
|
|
12
|
+
// PASS 2 — terminus pin (end lake surface −0.8 / parent trunk's level at the join / the
|
|
13
|
+
// open sea), monotone-downhill, floor at sea level. Trunks must precede their
|
|
14
|
+
// tributaries in the array so a join reads a FINALIZED parent level.
|
|
15
|
+
// PASS 3 — riffle relief: water rides within riffleCut of the river-free ground (highest
|
|
16
|
+
// of centreline AND both banks at ±(ww+4)) so the monotone floor cannot gouge a
|
|
17
|
+
// slot-gorge across a rise ('rivers underground').
|
|
18
|
+
// PASS 4 — estuary descent: sea-reaching trunks ease to sea level anchored at the
|
|
19
|
+
// SHORELINE node (tails run out to sea), pin seaward, re-monotone the reach.
|
|
20
|
+
//
|
|
21
|
+
// groundFn(x, z) is the game's river-free pristine ground — rawHeight(x, z, true) in both
|
|
22
|
+
// counties. Call this at the game's bake site (BELOW its rawHeight, per the TDZ law both
|
|
23
|
+
// files document). Headless by law.
|
|
24
|
+
import { lerp, smoothstep } from './geo.js';
|
|
25
|
+
|
|
26
|
+
function riverWyAt(R, p) { // baked level of R nearest p — for confluences
|
|
27
|
+
let best = Infinity, wy = R.wy[0];
|
|
28
|
+
for (let i = 0; i < R.pts.length; i++) {
|
|
29
|
+
const d = Math.hypot(R.pts[i].x - p.x, R.pts[i].z - p.z);
|
|
30
|
+
if (d < best) { best = d; wy = R.wy[i]; }
|
|
31
|
+
}
|
|
32
|
+
return wy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function bakeWaterLevels(rivers, groundFn, { seaLevel, coastZ, riffleCut = 1.6 } = {}) {
|
|
36
|
+
// PASS 0
|
|
37
|
+
for (const R of rivers) {
|
|
38
|
+
if (!R.bd) continue;
|
|
39
|
+
const n = R.pts.length;
|
|
40
|
+
R.wy = R.bd.map((b, i) => b + R.wdep[i]);
|
|
41
|
+
if (R.srcLake) R.wy[0] = R.srcLake.level - 0.3;
|
|
42
|
+
if (R.endLake) R.wy[n - 1] = R.endLake.level - 0.8;
|
|
43
|
+
for (let i = 1; i < n; i++) R.wy[i] = Math.min(R.wy[i], R.wy[i - 1]);
|
|
44
|
+
}
|
|
45
|
+
// PASS 1
|
|
46
|
+
for (const R of rivers) {
|
|
47
|
+
if (R.bd) continue;
|
|
48
|
+
R.wy = R.pts.map((p, i, a) => {
|
|
49
|
+
const pa = a[Math.max(0, i - 1)], pb = a[Math.min(a.length - 1, i + 1)];
|
|
50
|
+
let tx = pb.x - pa.x, tz = pb.z - pa.z; const tl = Math.hypot(tx, tz) || 1; const nx = -tz / tl, nz = tx / tl;
|
|
51
|
+
const w = Math.min(R.ww[i], 10);
|
|
52
|
+
return Math.min(groundFn(p.x, p.z), groundFn(p.x + nx * w, p.z + nz * w), groundFn(p.x - nx * w, p.z - nz * w)) - 1.0;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// PASS 2
|
|
56
|
+
for (const R of rivers) {
|
|
57
|
+
if (R.bd) continue;
|
|
58
|
+
const n = R.wy.length;
|
|
59
|
+
R.wy[n - 1] = R.endLake ? R.endLake.level - 0.8
|
|
60
|
+
: R.parent != null ? riverWyAt(rivers[R.parent], R.pts[n - 1])
|
|
61
|
+
: seaLevel;
|
|
62
|
+
for (let i = 1; i < n; i++) R.wy[i] = Math.min(R.wy[i], R.wy[i - 1]);
|
|
63
|
+
for (let i = 0; i < n; i++) R.wy[i] = Math.max(R.wy[i], seaLevel);
|
|
64
|
+
}
|
|
65
|
+
// PASS 3
|
|
66
|
+
for (const R of rivers) {
|
|
67
|
+
if (R.bd) continue;
|
|
68
|
+
for (let i = 0; i < R.wy.length; i++) {
|
|
69
|
+
const p2 = R.pts[i], pa2 = R.pts[Math.max(0, i - 1)], pb2 = R.pts[Math.min(R.pts.length - 1, i + 1)];
|
|
70
|
+
let tx2 = pb2.x - pa2.x, tz2 = pb2.z - pa2.z; const tl2 = Math.hypot(tx2, tz2) || 1;
|
|
71
|
+
const nx2 = -tz2 / tl2, nz2 = tx2 / tl2, off2 = R.ww[i] + 4;
|
|
72
|
+
const gRef = Math.max(
|
|
73
|
+
groundFn(p2.x, p2.z),
|
|
74
|
+
groundFn(p2.x + nx2 * off2, p2.z + nz2 * off2),
|
|
75
|
+
groundFn(p2.x - nx2 * off2, p2.z - nz2 * off2),
|
|
76
|
+
);
|
|
77
|
+
R.wy[i] = Math.max(R.wy[i], gRef - riffleCut);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// PASS 4
|
|
81
|
+
for (const R of rivers) {
|
|
82
|
+
if (R.bd || R.parent != null || R.endLake) continue;
|
|
83
|
+
const n = R.wy.length;
|
|
84
|
+
if (R.pts[n - 1].z < coastZ) continue;
|
|
85
|
+
let iShore = n - 1;
|
|
86
|
+
while (iShore > 0 && groundFn(R.pts[iShore].x, R.pts[iShore].z) < seaLevel + 0.3) iShore--;
|
|
87
|
+
const K = Math.min(14, iShore);
|
|
88
|
+
for (let i = iShore - K; i <= iShore; i++) {
|
|
89
|
+
const t = smoothstep(iShore - K, iShore, i);
|
|
90
|
+
R.wy[i] = Math.max(seaLevel, lerp(R.wy[i], seaLevel, t));
|
|
91
|
+
}
|
|
92
|
+
for (let i = iShore + 1; i < n; i++) R.wy[i] = seaLevel;
|
|
93
|
+
for (let i = Math.max(1, iShore - Math.min(24, iShore)); i < n; i++) R.wy[i] = Math.min(R.wy[i], R.wy[i - 1]);
|
|
94
|
+
}
|
|
95
|
+
}
|