@threenative/core 0.2.0 → 0.3.1
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/LICENSE +21 -0
- package/README.md +55 -0
- package/capabilities.json +5292 -0
- package/dist/assets-kyoF7JlJ.d.ts +103 -0
- package/dist/audio-BFiGneTL.d.ts +156 -0
- package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
- package/dist/game-XGrTzapq.d.ts +1166 -0
- package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
- package/dist/hot.d.ts +20 -2
- package/dist/hot.js +14 -2
- package/dist/index.d.ts +2108 -55
- package/dist/index.js +15645 -2222
- package/dist/net.d.ts +65 -0
- package/dist/net.js +643 -0
- package/dist/playtest.d.ts +37 -4
- package/dist/playtest.js +246 -546
- package/dist/react.d.ts +177 -0
- package/dist/react.js +635 -0
- package/dist/renderer-C6hqZpoG.d.ts +770 -0
- package/dist/ui-layer.d.ts +306 -0
- package/dist/ui-layer.js +425 -0
- package/dist/world.d.ts +254 -0
- package/dist/world.js +2686 -0
- package/gpl/LICENSE.GPL +117 -0
- package/gpl/convert.py +192 -0
- package/gpl/recipes/_common.py +169 -0
- package/gpl/recipes/bake_ao.py +111 -0
- package/gpl/recipes/decimate.py +64 -0
- package/gpl/recipes/retarget.py +131 -0
- package/gpl/recipes/unwrap.py +71 -0
- package/mcp/assets.mjs +5 -0
- package/mcp/blender-server.mjs +632 -0
- package/mcp/blender.mjs +27 -0
- package/mcp/engine-server.mjs +501 -0
- package/mcp/engine.mjs +31 -0
- package/mcp/install.d.mts +37 -0
- package/mcp/install.mjs +145 -0
- package/mcp/launch.mjs +72 -0
- package/mcp/sculpt.mjs +5 -0
- package/mcp/servers.d.mts +34 -0
- package/mcp/servers.mjs +160 -0
- package/package.json +76 -6
- package/patches/three@0.185.1.patch +522 -0
- package/scripts/apply-three-patch.mjs +297 -0
- package/scripts/ensure-mcp.mjs +43 -0
- package/scripts/postinstall.mjs +6 -0
- package/dist/audio-CEAw0w5y.d.ts +0 -35
- package/dist/game-DRt1Qhq3.d.ts +0 -429
package/dist/world.js
ADDED
|
@@ -0,0 +1,2686 @@
|
|
|
1
|
+
import { Object3D, Vector3, LOD, Group, BufferGeometry, BufferAttribute, Mesh } from 'three';
|
|
2
|
+
import { instancedArray, Fn, If, instanceIndex, Return, vec4, min, max, float, clamp, uint, atomicLoad, atomicAdd, log } from 'three/tsl';
|
|
3
|
+
|
|
4
|
+
// src/world.ts
|
|
5
|
+
|
|
6
|
+
// src/gpu-readback.ts
|
|
7
|
+
var GPUReadback = class {
|
|
8
|
+
everyFrames;
|
|
9
|
+
#attribute;
|
|
10
|
+
/** Frames observed through `request()`. The clock every other number here is measured against. */
|
|
11
|
+
#frame = 0;
|
|
12
|
+
/** The frame whose GPU state the landed bytes hold, or `-1` when nothing has landed. */
|
|
13
|
+
#sampleFrame = -1;
|
|
14
|
+
/** The frame the in-flight request was issued on, or `-1` when none is in flight. */
|
|
15
|
+
#requestedFrame = -1;
|
|
16
|
+
#pending = false;
|
|
17
|
+
#data;
|
|
18
|
+
#requests = 0;
|
|
19
|
+
#lands = 0;
|
|
20
|
+
#failures = 0;
|
|
21
|
+
#released = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (options.attribute === void 0 || options.attribute === null)
|
|
24
|
+
throw new Error("GPUReadback.attribute is required.");
|
|
25
|
+
if (!Number.isInteger(options.everyFrames) || options.everyFrames <= 0)
|
|
26
|
+
throw new Error("GPUReadback.everyFrames must be a positive integer.");
|
|
27
|
+
this.#attribute = options.attribute;
|
|
28
|
+
this.everyFrames = options.everyFrames;
|
|
29
|
+
}
|
|
30
|
+
get released() {
|
|
31
|
+
return this.#released;
|
|
32
|
+
}
|
|
33
|
+
/** True while a copy is in flight. A game that wants to pace its own work can read it. */
|
|
34
|
+
get pending() {
|
|
35
|
+
return this.#pending;
|
|
36
|
+
}
|
|
37
|
+
/** The newest landed bytes, or `undefined` before the first copy lands. */
|
|
38
|
+
get data() {
|
|
39
|
+
return this.#data;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How many frames old the landed bytes are.
|
|
43
|
+
*
|
|
44
|
+
* Before anything has landed this is the number of frames since construction, which grows
|
|
45
|
+
* without bound on purpose: "no data yet" and "data from frame zero" must not read the same.
|
|
46
|
+
*/
|
|
47
|
+
get staleFrames() {
|
|
48
|
+
if (this.#sampleFrame < 0) return this.#frame;
|
|
49
|
+
return this.#frame - this.#sampleFrame;
|
|
50
|
+
}
|
|
51
|
+
/** The newest bytes with their age attached, or `undefined` before the first copy lands. */
|
|
52
|
+
get sample() {
|
|
53
|
+
if (this.#data === void 0) return void 0;
|
|
54
|
+
return { data: this.#data, staleFrames: this.staleFrames };
|
|
55
|
+
}
|
|
56
|
+
/** Requests, landings and failures, for a report that has to say why a sample is old. */
|
|
57
|
+
get stats() {
|
|
58
|
+
return { requests: this.#requests, lands: this.#lands, failures: this.#failures };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Advances the frame clock and starts a copy when the throttle allows one.
|
|
62
|
+
*
|
|
63
|
+
* Safe to call every frame. It returns before the GPU has answered — awaiting it is the one
|
|
64
|
+
* thing that would turn this class into the stall it exists to avoid.
|
|
65
|
+
*/
|
|
66
|
+
request(renderer) {
|
|
67
|
+
if (this.#released) throw new Error("GPUReadback cannot request after release.");
|
|
68
|
+
this.#frame += 1;
|
|
69
|
+
if (this.#pending) return;
|
|
70
|
+
if (this.#requestedFrame >= 0 && this.#frame - this.#requestedFrame < this.everyFrames) return;
|
|
71
|
+
const issuedFrame = this.#frame;
|
|
72
|
+
this.#requestedFrame = issuedFrame;
|
|
73
|
+
this.#pending = true;
|
|
74
|
+
this.#requests += 1;
|
|
75
|
+
renderer.readback(this.#attribute).then((bytes) => this.#land(bytes, issuedFrame)).catch(() => this.#fail());
|
|
76
|
+
}
|
|
77
|
+
#land(bytes, issuedFrame) {
|
|
78
|
+
this.#pending = false;
|
|
79
|
+
if (this.#released) return;
|
|
80
|
+
this.#data = new Float32Array(bytes);
|
|
81
|
+
this.#sampleFrame = issuedFrame;
|
|
82
|
+
this.#lands += 1;
|
|
83
|
+
}
|
|
84
|
+
#fail() {
|
|
85
|
+
this.#pending = false;
|
|
86
|
+
if (this.#released) return;
|
|
87
|
+
this.#failures += 1;
|
|
88
|
+
}
|
|
89
|
+
/** Drops the attribute reference and the landed bytes. Further requests throw. */
|
|
90
|
+
dispose() {
|
|
91
|
+
if (this.#released) return;
|
|
92
|
+
this.#released = true;
|
|
93
|
+
this.#pending = false;
|
|
94
|
+
this.#attribute = void 0;
|
|
95
|
+
this.#data = void 0;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function finite(name, value, minimum = 0) {
|
|
99
|
+
if (!Number.isFinite(value) || value < minimum)
|
|
100
|
+
throw new Error(`World passes ${name} must be finite and at least ${minimum}.`);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function gridCount(name, value) {
|
|
104
|
+
if (!Number.isInteger(value) || value < 2)
|
|
105
|
+
throw new Error(`World passes ${name} must be an integer of at least 2.`);
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
var CARDINAL = [
|
|
109
|
+
[-1, 0],
|
|
110
|
+
[1, 0],
|
|
111
|
+
[0, -1],
|
|
112
|
+
[0, 1]
|
|
113
|
+
];
|
|
114
|
+
function neighbourIndex(index, direction, rows, columns) {
|
|
115
|
+
const pair = CARDINAL[direction];
|
|
116
|
+
if (pair === void 0) return void 0;
|
|
117
|
+
const column = index % columns + pair[0];
|
|
118
|
+
const row = Math.floor(index / columns) + pair[1];
|
|
119
|
+
if (column < 0 || column >= columns || row < 0 || row >= rows) return void 0;
|
|
120
|
+
return row * columns + column;
|
|
121
|
+
}
|
|
122
|
+
function simulateWorldPassesCpu(options) {
|
|
123
|
+
const rows = gridCount("rows", options.rows);
|
|
124
|
+
const columns = gridCount("columns", options.columns);
|
|
125
|
+
const count2 = rows * columns;
|
|
126
|
+
if (options.heights.length !== count2)
|
|
127
|
+
throw new Error(`World passes expected ${count2} heights, received ${options.heights.length}.`);
|
|
128
|
+
const cellWidth = finite("cellWidth", options.cellWidth, Number.EPSILON);
|
|
129
|
+
const cellDepth = finite("cellDepth", options.cellDepth, Number.EPSILON);
|
|
130
|
+
const tuning = options.erosion;
|
|
131
|
+
if (!Number.isInteger(tuning.iterations) || tuning.iterations < 0)
|
|
132
|
+
throw new Error("World passes iterations must be a non-negative integer.");
|
|
133
|
+
const rainfall = finite("rainfall", tuning.rainfall);
|
|
134
|
+
const evaporation = finite("evaporation", tuning.evaporation);
|
|
135
|
+
if (evaporation > 1) throw new Error("World passes evaporation must not exceed 1.");
|
|
136
|
+
const timeStep = finite("timeStep", tuning.timeStep, Number.EPSILON);
|
|
137
|
+
const sedimentCapacity = finite("sedimentCapacity", tuning.sedimentCapacity);
|
|
138
|
+
const erosionRate = finite("erosionRate", tuning.erosionRate);
|
|
139
|
+
const depositionRate = finite("depositionRate", tuning.depositionRate);
|
|
140
|
+
let heights = options.heights.slice();
|
|
141
|
+
for (const height2 of heights) finite("height sample", height2, Number.NEGATIVE_INFINITY);
|
|
142
|
+
let water = new Float32Array(count2);
|
|
143
|
+
let sediment = new Float32Array(count2);
|
|
144
|
+
const outflow = new Float32Array(count2 * CARDINAL.length);
|
|
145
|
+
for (let iteration = 0; iteration < tuning.iterations; iteration += 1) {
|
|
146
|
+
outflow.fill(0);
|
|
147
|
+
for (let index = 0; index < count2; index += 1) {
|
|
148
|
+
water[index] = water[index] + rainfall * timeStep;
|
|
149
|
+
const head = heights[index] + water[index];
|
|
150
|
+
let totalDrop = 0;
|
|
151
|
+
const drops = [0, 0, 0, 0];
|
|
152
|
+
for (let direction = 0; direction < CARDINAL.length; direction += 1) {
|
|
153
|
+
const neighbour = neighbourIndex(index, direction, rows, columns);
|
|
154
|
+
if (neighbour === void 0) continue;
|
|
155
|
+
const drop = Math.max(
|
|
156
|
+
0,
|
|
157
|
+
head - (heights[neighbour] + water[neighbour])
|
|
158
|
+
);
|
|
159
|
+
drops[direction] = drop;
|
|
160
|
+
totalDrop += drop;
|
|
161
|
+
}
|
|
162
|
+
if (totalDrop === 0) continue;
|
|
163
|
+
const available = Math.min(water[index], totalDrop * timeStep);
|
|
164
|
+
for (let direction = 0; direction < CARDINAL.length; direction += 1)
|
|
165
|
+
outflow[index * CARDINAL.length + direction] = available * (drops[direction] / totalDrop);
|
|
166
|
+
}
|
|
167
|
+
const nextWater = new Float32Array(count2);
|
|
168
|
+
const nextSediment = new Float32Array(count2);
|
|
169
|
+
const nextHeights = heights.slice();
|
|
170
|
+
for (let index = 0; index < count2; index += 1) {
|
|
171
|
+
let incomingWater = 0;
|
|
172
|
+
let outgoingWater = 0;
|
|
173
|
+
for (let direction = 0; direction < CARDINAL.length; direction += 1) {
|
|
174
|
+
outgoingWater += outflow[index * CARDINAL.length + direction];
|
|
175
|
+
const neighbour = neighbourIndex(index, direction, rows, columns);
|
|
176
|
+
if (neighbour === void 0) continue;
|
|
177
|
+
incomingWater += outflow[neighbour * CARDINAL.length + (direction ^ 1)];
|
|
178
|
+
}
|
|
179
|
+
const remainingWater = Math.max(
|
|
180
|
+
0,
|
|
181
|
+
(water[index] + incomingWater - outgoingWater) * (1 - evaporation * timeStep)
|
|
182
|
+
);
|
|
183
|
+
nextWater[index] = remainingWater;
|
|
184
|
+
const speed = outgoingWater / (timeStep * Math.min(cellWidth, cellDepth));
|
|
185
|
+
const capacity = sedimentCapacity * speed * outgoingWater;
|
|
186
|
+
let localSediment = sediment[index];
|
|
187
|
+
if (localSediment < capacity) {
|
|
188
|
+
const eroded = Math.min(
|
|
189
|
+
capacity - localSediment,
|
|
190
|
+
(capacity - localSediment) * erosionRate * timeStep
|
|
191
|
+
);
|
|
192
|
+
nextHeights[index] = nextHeights[index] - eroded;
|
|
193
|
+
localSediment += eroded;
|
|
194
|
+
} else {
|
|
195
|
+
const deposited = (localSediment - capacity) * depositionRate * timeStep;
|
|
196
|
+
nextHeights[index] = nextHeights[index] + deposited;
|
|
197
|
+
localSediment -= deposited;
|
|
198
|
+
}
|
|
199
|
+
const transported = Math.min(localSediment, localSediment * outgoingWater);
|
|
200
|
+
nextSediment[index] = nextSediment[index] + localSediment - transported;
|
|
201
|
+
if (outgoingWater === 0) continue;
|
|
202
|
+
for (let direction = 0; direction < CARDINAL.length; direction += 1) {
|
|
203
|
+
const neighbour = neighbourIndex(index, direction, rows, columns);
|
|
204
|
+
const amount = outflow[index * CARDINAL.length + direction];
|
|
205
|
+
if (neighbour !== void 0)
|
|
206
|
+
nextSediment[neighbour] = nextSediment[neighbour] + transported * (amount / outgoingWater);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
heights = nextHeights;
|
|
210
|
+
water = nextWater;
|
|
211
|
+
sediment = nextSediment;
|
|
212
|
+
}
|
|
213
|
+
const flow = accumulateFlow(heights, rows, columns);
|
|
214
|
+
const moisture = new Float32Array(count2);
|
|
215
|
+
for (let index = 0; index < count2; index += 1)
|
|
216
|
+
moisture[index] = Math.min(
|
|
217
|
+
1,
|
|
218
|
+
flow[index] * 0.7 + Math.min(1, Math.max(0, water[index])) * 0.3
|
|
219
|
+
);
|
|
220
|
+
return { flow, heights, moisture };
|
|
221
|
+
}
|
|
222
|
+
function accumulateFlow(heights, rows, columns) {
|
|
223
|
+
const targets = new Int32Array(heights.length);
|
|
224
|
+
targets.fill(-1);
|
|
225
|
+
const order = Array.from({ length: heights.length }, (_, index) => index).sort(
|
|
226
|
+
(a, b) => heights[b] - heights[a] || a - b
|
|
227
|
+
);
|
|
228
|
+
for (let source = 0; source < heights.length; source += 1) {
|
|
229
|
+
let destination = source;
|
|
230
|
+
let lowest = heights[source];
|
|
231
|
+
for (let direction = 0; direction < CARDINAL.length; direction += 1) {
|
|
232
|
+
const neighbour = neighbourIndex(source, direction, rows, columns);
|
|
233
|
+
if (neighbour === void 0) continue;
|
|
234
|
+
const candidate = heights[neighbour];
|
|
235
|
+
if (candidate < lowest) {
|
|
236
|
+
lowest = candidate;
|
|
237
|
+
destination = neighbour;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (destination !== source) targets[source] = destination;
|
|
241
|
+
}
|
|
242
|
+
const accumulated = new Float64Array(heights.length);
|
|
243
|
+
accumulated.fill(1);
|
|
244
|
+
for (const source of order) {
|
|
245
|
+
const destination = targets[source];
|
|
246
|
+
if (destination >= 0)
|
|
247
|
+
accumulated[destination] = accumulated[destination] + accumulated[source];
|
|
248
|
+
}
|
|
249
|
+
const scale = Math.log1p(heights.length);
|
|
250
|
+
return Float32Array.from(accumulated, (value) => Math.log1p(value) / scale);
|
|
251
|
+
}
|
|
252
|
+
function computeNode(name, count2, body) {
|
|
253
|
+
const node = Fn(body)().compute(count2);
|
|
254
|
+
node.setName(name);
|
|
255
|
+
return node;
|
|
256
|
+
}
|
|
257
|
+
function createWorldGpuPasses(options) {
|
|
258
|
+
const rows = gridCount("rows", options.rows);
|
|
259
|
+
const columns = gridCount("columns", options.columns);
|
|
260
|
+
const count2 = rows * columns;
|
|
261
|
+
if (options.heights.length !== count2)
|
|
262
|
+
throw new Error(`World passes expected ${count2} heights, received ${options.heights.length}.`);
|
|
263
|
+
finite("cellWidth", options.cellWidth, Number.EPSILON);
|
|
264
|
+
finite("cellDepth", options.cellDepth, Number.EPSILON);
|
|
265
|
+
simulateWorldPassesCpu({ ...options, erosion: { ...options.erosion, iterations: 0 } });
|
|
266
|
+
const initial = instancedArray(options.heights, "float").toReadOnly();
|
|
267
|
+
const heightA = instancedArray(count2, "float");
|
|
268
|
+
const heightB = instancedArray(count2, "float");
|
|
269
|
+
const waterA = instancedArray(count2, "float");
|
|
270
|
+
const waterB = instancedArray(count2, "float");
|
|
271
|
+
const sedimentA = instancedArray(count2, "float");
|
|
272
|
+
const sedimentB = instancedArray(count2, "float");
|
|
273
|
+
const sedimentTemporary = instancedArray(count2, "float");
|
|
274
|
+
const outflow = instancedArray(count2, "vec4");
|
|
275
|
+
const flowAtomic = instancedArray(count2, "uint");
|
|
276
|
+
const flowFrontierA = instancedArray(count2, "uint");
|
|
277
|
+
const flowFrontierB = instancedArray(count2, "uint");
|
|
278
|
+
const flow = instancedArray(count2, "float");
|
|
279
|
+
const moisture = instancedArray(count2, "float");
|
|
280
|
+
const allStorage = [
|
|
281
|
+
initial,
|
|
282
|
+
heightA,
|
|
283
|
+
heightB,
|
|
284
|
+
waterA,
|
|
285
|
+
waterB,
|
|
286
|
+
sedimentA,
|
|
287
|
+
sedimentB,
|
|
288
|
+
sedimentTemporary,
|
|
289
|
+
outflow,
|
|
290
|
+
flowAtomic,
|
|
291
|
+
flowFrontierA,
|
|
292
|
+
flowFrontierB,
|
|
293
|
+
flow,
|
|
294
|
+
moisture
|
|
295
|
+
];
|
|
296
|
+
const guard = (body) => Fn(() => {
|
|
297
|
+
If(instanceIndex.greaterThanEqual(count2), () => Return());
|
|
298
|
+
body();
|
|
299
|
+
})();
|
|
300
|
+
const at = (index, x, z) => {
|
|
301
|
+
const column = clamp(float(index.mod(columns)).add(x), 0, columns - 1).toInt();
|
|
302
|
+
const row = clamp(float(index.div(columns)).add(z), 0, rows - 1).toInt();
|
|
303
|
+
return row.mul(columns).add(column);
|
|
304
|
+
};
|
|
305
|
+
const synthesis = computeNode(
|
|
306
|
+
"world.synthesis",
|
|
307
|
+
count2,
|
|
308
|
+
() => guard(() => {
|
|
309
|
+
const index = instanceIndex.toInt();
|
|
310
|
+
heightA.element(index).assign(initial.element(index));
|
|
311
|
+
waterA.element(index).assign(0);
|
|
312
|
+
sedimentA.element(index).assign(0);
|
|
313
|
+
flowAtomic.element(index).assign(uint(1));
|
|
314
|
+
flowFrontierA.element(index).assign(uint(1));
|
|
315
|
+
flowFrontierB.element(index).assign(uint(0));
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
const erosionNodes = [];
|
|
319
|
+
const tuning = options.erosion;
|
|
320
|
+
const rain = tuning.rainfall * tuning.timeStep;
|
|
321
|
+
const makeIteration = (roles, iteration) => {
|
|
322
|
+
const flux = computeNode(
|
|
323
|
+
`world.erosion.${iteration}.flux`,
|
|
324
|
+
count2,
|
|
325
|
+
() => guard(() => {
|
|
326
|
+
const index = instanceIndex.toInt();
|
|
327
|
+
const water2 = roles.waterSource.element(index).add(rain);
|
|
328
|
+
const head = roles.heightSource.element(index).add(water2).toVar();
|
|
329
|
+
const drop = (x, z) => {
|
|
330
|
+
const neighbour = at(index, x, z);
|
|
331
|
+
return max(
|
|
332
|
+
0,
|
|
333
|
+
head.sub(
|
|
334
|
+
roles.heightSource.element(neighbour).add(roles.waterSource.element(neighbour)).add(rain)
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
const drops = vec4(drop(-1, 0), drop(1, 0), drop(0, -1), drop(0, 1)).toVar();
|
|
339
|
+
const totalDrop = drops.x.add(drops.y).add(drops.z).add(drops.w);
|
|
340
|
+
const available = min(water2, totalDrop.mul(tuning.timeStep));
|
|
341
|
+
const scale = available.div(max(totalDrop, 1e-6));
|
|
342
|
+
outflow.element(index).assign(drops.mul(scale));
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
const erode = computeNode(
|
|
346
|
+
`world.erosion.${iteration}.erode`,
|
|
347
|
+
count2,
|
|
348
|
+
() => guard(() => {
|
|
349
|
+
const index = instanceIndex.toInt();
|
|
350
|
+
const fluxValue = outflow.element(index).toVar();
|
|
351
|
+
const outgoing = fluxValue.x.add(fluxValue.y).add(fluxValue.z).add(fluxValue.w).toVar();
|
|
352
|
+
const incoming = outflow.element(at(index, -1, 0)).y.add(outflow.element(at(index, 1, 0)).x).add(outflow.element(at(index, 0, -1)).w).add(outflow.element(at(index, 0, 1)).z);
|
|
353
|
+
const water2 = max(
|
|
354
|
+
0,
|
|
355
|
+
roles.waterSource.element(index).add(rain).add(incoming).sub(outgoing).mul(1 - tuning.evaporation * tuning.timeStep)
|
|
356
|
+
);
|
|
357
|
+
roles.waterTarget.element(index).assign(water2);
|
|
358
|
+
const speed = outgoing.div(
|
|
359
|
+
tuning.timeStep * Math.min(options.cellWidth, options.cellDepth)
|
|
360
|
+
);
|
|
361
|
+
const capacity = float(tuning.sedimentCapacity).mul(speed).mul(outgoing);
|
|
362
|
+
const sediment = roles.sedimentSource.element(index).toVar();
|
|
363
|
+
const height3 = roles.heightSource.element(index).toVar();
|
|
364
|
+
If(sediment.lessThan(capacity), () => {
|
|
365
|
+
const amount = capacity.sub(sediment).mul(tuning.erosionRate * tuning.timeStep).min(capacity.sub(sediment));
|
|
366
|
+
height3.subAssign(amount);
|
|
367
|
+
sediment.addAssign(amount);
|
|
368
|
+
}).Else(() => {
|
|
369
|
+
const amount = sediment.sub(capacity).mul(tuning.depositionRate * tuning.timeStep);
|
|
370
|
+
height3.addAssign(amount);
|
|
371
|
+
sediment.subAssign(amount);
|
|
372
|
+
});
|
|
373
|
+
roles.heightTarget.element(index).assign(height3);
|
|
374
|
+
sedimentTemporary.element(index).assign(sediment);
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
const transport = computeNode(
|
|
378
|
+
`world.erosion.${iteration}.transport`,
|
|
379
|
+
count2,
|
|
380
|
+
() => guard(() => {
|
|
381
|
+
const index = instanceIndex.toInt();
|
|
382
|
+
const ownFlux = outflow.element(index).toVar();
|
|
383
|
+
const ownOut = ownFlux.x.add(ownFlux.y).add(ownFlux.z).add(ownFlux.w);
|
|
384
|
+
const retained = sedimentTemporary.element(index).mul(min(1, ownOut).oneMinus()).toVar();
|
|
385
|
+
const gather = (x, z, component) => {
|
|
386
|
+
const neighbour = at(index, x, z);
|
|
387
|
+
const neighbourFlux = outflow.element(neighbour).toVar();
|
|
388
|
+
const total = neighbourFlux.x.add(neighbourFlux.y).add(neighbourFlux.z).add(neighbourFlux.w);
|
|
389
|
+
return sedimentTemporary.element(neighbour).mul(min(1, total)).mul(neighbourFlux[component].div(max(total, 1e-6)));
|
|
390
|
+
};
|
|
391
|
+
retained.addAssign(gather(-1, 0, "y"));
|
|
392
|
+
retained.addAssign(gather(1, 0, "x"));
|
|
393
|
+
retained.addAssign(gather(0, -1, "w"));
|
|
394
|
+
retained.addAssign(gather(0, 1, "z"));
|
|
395
|
+
roles.sedimentTarget.element(index).assign(retained);
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
erosionNodes.push(flux, erode, transport);
|
|
399
|
+
};
|
|
400
|
+
for (let iteration = 0; iteration < tuning.iterations; iteration += 1)
|
|
401
|
+
makeIteration(
|
|
402
|
+
iteration % 2 === 0 ? {
|
|
403
|
+
heightSource: heightA,
|
|
404
|
+
heightTarget: heightB,
|
|
405
|
+
sedimentSource: sedimentA,
|
|
406
|
+
sedimentTarget: sedimentB,
|
|
407
|
+
waterSource: waterA,
|
|
408
|
+
waterTarget: waterB
|
|
409
|
+
} : {
|
|
410
|
+
heightSource: heightB,
|
|
411
|
+
heightTarget: heightA,
|
|
412
|
+
sedimentSource: sedimentB,
|
|
413
|
+
sedimentTarget: sedimentA,
|
|
414
|
+
waterSource: waterB,
|
|
415
|
+
waterTarget: waterA
|
|
416
|
+
},
|
|
417
|
+
iteration
|
|
418
|
+
);
|
|
419
|
+
if (erosionNodes.length === 0)
|
|
420
|
+
erosionNodes.push(
|
|
421
|
+
computeNode(
|
|
422
|
+
"world.erosion.bypass",
|
|
423
|
+
count2,
|
|
424
|
+
() => guard(
|
|
425
|
+
() => heightA.element(instanceIndex.toInt()).assign(heightA.element(instanceIndex.toInt()))
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
const height2 = tuning.iterations % 2 === 0 ? heightA : heightB;
|
|
430
|
+
const water = tuning.iterations % 2 === 0 ? waterA : waterB;
|
|
431
|
+
const flowNodes = [];
|
|
432
|
+
const makeFlowStep = (source, target, step) => {
|
|
433
|
+
flowNodes.push(
|
|
434
|
+
computeNode(
|
|
435
|
+
`world.flow.${step}.clear`,
|
|
436
|
+
count2,
|
|
437
|
+
() => guard(() => target.element(instanceIndex.toInt()).assign(uint(0)))
|
|
438
|
+
),
|
|
439
|
+
computeNode(
|
|
440
|
+
`world.flow.${step}.route`,
|
|
441
|
+
count2,
|
|
442
|
+
() => guard(() => {
|
|
443
|
+
const current = instanceIndex.toInt();
|
|
444
|
+
const amount = atomicLoad(source.element(current));
|
|
445
|
+
const best = height2.element(current).toVar();
|
|
446
|
+
const destination = current.toVar();
|
|
447
|
+
const consider = (x, z) => {
|
|
448
|
+
const neighbour = at(current, x, z);
|
|
449
|
+
const candidate = height2.element(neighbour);
|
|
450
|
+
If(candidate.lessThan(best), () => {
|
|
451
|
+
best.assign(candidate);
|
|
452
|
+
destination.assign(neighbour);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
consider(-1, 0);
|
|
456
|
+
consider(1, 0);
|
|
457
|
+
consider(0, -1);
|
|
458
|
+
consider(0, 1);
|
|
459
|
+
If(destination.notEqual(current).and(amount.greaterThan(0)), () => {
|
|
460
|
+
atomicAdd(target.element(destination), amount);
|
|
461
|
+
atomicAdd(flowAtomic.element(destination), amount);
|
|
462
|
+
});
|
|
463
|
+
})
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
for (let step = 0; step < rows + columns; step += 1)
|
|
468
|
+
makeFlowStep(
|
|
469
|
+
step % 2 === 0 ? flowFrontierA : flowFrontierB,
|
|
470
|
+
step % 2 === 0 ? flowFrontierB : flowFrontierA,
|
|
471
|
+
step
|
|
472
|
+
);
|
|
473
|
+
const normalizeFlow = computeNode(
|
|
474
|
+
"world.flow.normalize",
|
|
475
|
+
count2,
|
|
476
|
+
() => guard(() => {
|
|
477
|
+
const index = instanceIndex.toInt();
|
|
478
|
+
flow.element(index).assign(log(float(atomicLoad(flowAtomic.element(index))).add(1)).div(Math.log1p(count2)));
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
const deriveMoisture = computeNode(
|
|
482
|
+
"world.moisture",
|
|
483
|
+
count2,
|
|
484
|
+
() => guard(() => {
|
|
485
|
+
const index = instanceIndex.toInt();
|
|
486
|
+
moisture.element(index).assign(
|
|
487
|
+
clamp(
|
|
488
|
+
flow.element(index).mul(0.7).add(clamp(water.element(index), 0, 1).mul(0.3)),
|
|
489
|
+
0,
|
|
490
|
+
1
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
const stages = [
|
|
496
|
+
{ name: "synthesis", nodes: [synthesis] },
|
|
497
|
+
{ name: "erosion", nodes: erosionNodes },
|
|
498
|
+
{ name: "flow", nodes: [...flowNodes, normalizeFlow] },
|
|
499
|
+
{ name: "moisture", nodes: [deriveMoisture] }
|
|
500
|
+
];
|
|
501
|
+
return {
|
|
502
|
+
flow,
|
|
503
|
+
height: height2,
|
|
504
|
+
moisture,
|
|
505
|
+
queue: new BoundedWorldPassQueue({ dispatchBudget: options.dispatchBudget, stages }),
|
|
506
|
+
stages,
|
|
507
|
+
dispose: () => {
|
|
508
|
+
for (const node of stages.flatMap((stage) => stage.nodes))
|
|
509
|
+
node.dispose?.();
|
|
510
|
+
for (const buffer of allStorage) buffer.value.dispose();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
var STAGE_ORDER = ["synthesis", "erosion", "flow", "moisture"];
|
|
515
|
+
var BoundedWorldPassQueue = class {
|
|
516
|
+
dispatchBudget;
|
|
517
|
+
#nodes;
|
|
518
|
+
#cursor = 0;
|
|
519
|
+
constructor(options) {
|
|
520
|
+
if (!Number.isInteger(options.dispatchBudget) || options.dispatchBudget <= 0)
|
|
521
|
+
throw new Error("World passes dispatchBudget must be a positive integer.");
|
|
522
|
+
if (options.stages.length !== STAGE_ORDER.length)
|
|
523
|
+
throw new Error("World passes require synthesis, erosion, flow, and moisture stages.");
|
|
524
|
+
options.stages.forEach((stage, index) => {
|
|
525
|
+
const expected = STAGE_ORDER[index];
|
|
526
|
+
if (stage.name !== expected)
|
|
527
|
+
throw new Error(`World passes expected '${expected}' at stage ${index}.`);
|
|
528
|
+
if (stage.nodes.length === 0)
|
|
529
|
+
throw new Error(`World passes ${stage.name} stage requires at least one node.`);
|
|
530
|
+
});
|
|
531
|
+
this.dispatchBudget = options.dispatchBudget;
|
|
532
|
+
this.#nodes = options.stages.flatMap((stage) => stage.nodes);
|
|
533
|
+
}
|
|
534
|
+
get complete() {
|
|
535
|
+
return this.#cursor === this.#nodes.length;
|
|
536
|
+
}
|
|
537
|
+
get dispatched() {
|
|
538
|
+
return this.#cursor;
|
|
539
|
+
}
|
|
540
|
+
/** Submit at most one fixed step's budget, preserving the declared physical pass order. */
|
|
541
|
+
process(renderer) {
|
|
542
|
+
const end = Math.min(this.#nodes.length, this.#cursor + this.dispatchBudget);
|
|
543
|
+
const start = this.#cursor;
|
|
544
|
+
while (this.#cursor < end) {
|
|
545
|
+
renderer.compute(this.#nodes[this.#cursor]);
|
|
546
|
+
this.#cursor += 1;
|
|
547
|
+
}
|
|
548
|
+
return this.#cursor - start;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/world-topology.ts
|
|
553
|
+
function height(field, row, column) {
|
|
554
|
+
const clampedRow = Math.max(0, Math.min(field.rows - 1, row));
|
|
555
|
+
const clampedColumn = Math.max(0, Math.min(field.columns - 1, column));
|
|
556
|
+
const value = field.heights[clampedRow * field.columns + clampedColumn];
|
|
557
|
+
if (value === void 0 || !Number.isFinite(value))
|
|
558
|
+
throw new Error("World topology height sample is missing or not finite.");
|
|
559
|
+
return value;
|
|
560
|
+
}
|
|
561
|
+
function gradient(field, row, column) {
|
|
562
|
+
const dx = field.width / (field.columns - 1);
|
|
563
|
+
const dz = field.depth / (field.rows - 1);
|
|
564
|
+
return [
|
|
565
|
+
(height(field, row, column + 1) - height(field, row, column - 1)) / (2 * dx),
|
|
566
|
+
(height(field, row + 1, column) - height(field, row - 1, column)) / (2 * dz)
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
function variance(values) {
|
|
570
|
+
if (values.length === 0) return 0;
|
|
571
|
+
const mean = values.reduce((total, value) => total + value, 0) / values.length;
|
|
572
|
+
return values.reduce((total, value) => total + (value - mean) ** 2, 0) / values.length;
|
|
573
|
+
}
|
|
574
|
+
function directionalAnisotropy(field) {
|
|
575
|
+
const x = [];
|
|
576
|
+
const z = [];
|
|
577
|
+
for (let row = 0; row < field.rows; row += 1) {
|
|
578
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
579
|
+
const [gradientX, gradientZ] = gradient(field, row, column);
|
|
580
|
+
x.push(gradientX);
|
|
581
|
+
z.push(gradientZ);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const xVariance = variance(x);
|
|
585
|
+
const zVariance = variance(z);
|
|
586
|
+
if (xVariance === 0 || zVariance === 0) return Number.POSITIVE_INFINITY;
|
|
587
|
+
return Math.abs(Math.log(xVariance / zVariance));
|
|
588
|
+
}
|
|
589
|
+
function powerSpectrumSlope(field) {
|
|
590
|
+
const average = [];
|
|
591
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
592
|
+
let total = 0;
|
|
593
|
+
for (let row = 0; row < field.rows; row += 1) total += height(field, row, column);
|
|
594
|
+
average.push(total / field.rows);
|
|
595
|
+
}
|
|
596
|
+
const wavelengths = [];
|
|
597
|
+
const powers = [];
|
|
598
|
+
for (let frequency = 1; frequency <= Math.floor(field.columns / 2); frequency += 1) {
|
|
599
|
+
const wavelength = field.width / frequency;
|
|
600
|
+
if (wavelength < 8 || wavelength > 256) continue;
|
|
601
|
+
let real = 0;
|
|
602
|
+
let imaginary = 0;
|
|
603
|
+
for (let index = 0; index < field.columns; index += 1) {
|
|
604
|
+
const angle = 2 * Math.PI * frequency * index / field.columns;
|
|
605
|
+
real += average[index] * Math.cos(angle);
|
|
606
|
+
imaginary -= average[index] * Math.sin(angle);
|
|
607
|
+
}
|
|
608
|
+
const power = real * real + imaginary * imaginary;
|
|
609
|
+
if (power > 0) {
|
|
610
|
+
wavelengths.push(Math.log(wavelength));
|
|
611
|
+
powers.push(Math.log(power));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (wavelengths.length < 2) return Number.POSITIVE_INFINITY;
|
|
615
|
+
const meanWavelength = wavelengths.reduce((total, value) => total + value, 0) / wavelengths.length;
|
|
616
|
+
const meanPower = powers.reduce((total, value) => total + value, 0) / powers.length;
|
|
617
|
+
let numerator = 0;
|
|
618
|
+
let denominator = 0;
|
|
619
|
+
for (let index = 0; index < wavelengths.length; index += 1) {
|
|
620
|
+
numerator += (wavelengths[index] - meanWavelength) * (powers[index] - meanPower);
|
|
621
|
+
denominator += (wavelengths[index] - meanWavelength) ** 2;
|
|
622
|
+
}
|
|
623
|
+
return denominator === 0 ? Number.POSITIVE_INFINITY : numerator / denominator;
|
|
624
|
+
}
|
|
625
|
+
function reliefFieldEdge(field) {
|
|
626
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
627
|
+
let maximum = Number.NEGATIVE_INFINITY;
|
|
628
|
+
let edgeTotal = 0;
|
|
629
|
+
let edgeCount = 0;
|
|
630
|
+
for (let row = 0; row < field.rows; row += 1) {
|
|
631
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
632
|
+
const value = height(field, row, column);
|
|
633
|
+
minimum = Math.min(minimum, value);
|
|
634
|
+
maximum = Math.max(maximum, value);
|
|
635
|
+
if (row === 0 || column === 0 || row === field.rows - 1 || column === field.columns - 1) {
|
|
636
|
+
edgeTotal += value;
|
|
637
|
+
edgeCount += 1;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const relief = maximum - minimum;
|
|
642
|
+
if (relief === 0) return 0;
|
|
643
|
+
const edgeMean = edgeTotal / edgeCount;
|
|
644
|
+
const center = height(field, Math.floor(field.rows / 2), Math.floor(field.columns / 2));
|
|
645
|
+
return Math.abs(center - edgeMean) / relief;
|
|
646
|
+
}
|
|
647
|
+
function slidingExtrema(read, length, radius, write) {
|
|
648
|
+
const minimumDeque = [];
|
|
649
|
+
const maximumDeque = [];
|
|
650
|
+
let minimumHead = 0;
|
|
651
|
+
let maximumHead = 0;
|
|
652
|
+
let right = -1;
|
|
653
|
+
for (let index = 0; index < length; index += 1) {
|
|
654
|
+
const desiredRight = Math.min(length - 1, index + radius);
|
|
655
|
+
while (right < desiredRight) {
|
|
656
|
+
right += 1;
|
|
657
|
+
const value = read(right);
|
|
658
|
+
while (minimumDeque.length > minimumHead && read(minimumDeque[minimumDeque.length - 1]) >= value)
|
|
659
|
+
minimumDeque.pop();
|
|
660
|
+
minimumDeque.push(right);
|
|
661
|
+
while (maximumDeque.length > maximumHead && read(maximumDeque[maximumDeque.length - 1]) <= value)
|
|
662
|
+
maximumDeque.pop();
|
|
663
|
+
maximumDeque.push(right);
|
|
664
|
+
}
|
|
665
|
+
const left = Math.max(0, index - radius);
|
|
666
|
+
while (minimumHead < minimumDeque.length && minimumDeque[minimumHead] < left)
|
|
667
|
+
minimumHead += 1;
|
|
668
|
+
while (maximumHead < maximumDeque.length && maximumDeque[maximumHead] < left)
|
|
669
|
+
maximumHead += 1;
|
|
670
|
+
const minimumIndex = minimumDeque[minimumHead];
|
|
671
|
+
const maximumIndex = maximumDeque[maximumHead];
|
|
672
|
+
if (minimumIndex === void 0 || maximumIndex === void 0)
|
|
673
|
+
throw new Error("World topology sliding window is empty.");
|
|
674
|
+
write(index, read(minimumIndex), read(maximumIndex));
|
|
675
|
+
if (minimumHead > 64 && minimumHead * 2 > minimumDeque.length) {
|
|
676
|
+
minimumDeque.splice(0, minimumHead);
|
|
677
|
+
minimumHead = 0;
|
|
678
|
+
}
|
|
679
|
+
if (maximumHead > 64 && maximumHead * 2 > maximumDeque.length) {
|
|
680
|
+
maximumDeque.splice(0, maximumHead);
|
|
681
|
+
maximumHead = 0;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function median64mRelief(field) {
|
|
686
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
687
|
+
let maximum = Number.NEGATIVE_INFINITY;
|
|
688
|
+
for (let index = 0; index < field.heights.length; index += 1) {
|
|
689
|
+
const value = field.heights[index];
|
|
690
|
+
if (value === void 0 || !Number.isFinite(value))
|
|
691
|
+
throw new Error("World topology height sample is missing or not finite.");
|
|
692
|
+
minimum = Math.min(minimum, value);
|
|
693
|
+
maximum = Math.max(maximum, value);
|
|
694
|
+
}
|
|
695
|
+
const globalRelief = maximum - minimum;
|
|
696
|
+
if (globalRelief === 0) return 0;
|
|
697
|
+
const radiusX = Math.max(1, Math.round(64 / (field.width / (field.columns - 1))));
|
|
698
|
+
const radiusZ = Math.max(1, Math.round(64 / (field.depth / (field.rows - 1))));
|
|
699
|
+
const local = [];
|
|
700
|
+
const horizontalMinimum = new Float64Array(field.rows * field.columns);
|
|
701
|
+
const horizontalMaximum = new Float64Array(field.rows * field.columns);
|
|
702
|
+
for (let row = 0; row < field.rows; row += 1)
|
|
703
|
+
slidingExtrema(
|
|
704
|
+
(column) => height(field, row, column),
|
|
705
|
+
field.columns,
|
|
706
|
+
radiusX,
|
|
707
|
+
(column, lineMinimum, lineMaximum) => {
|
|
708
|
+
horizontalMinimum[row * field.columns + column] = lineMinimum;
|
|
709
|
+
horizontalMaximum[row * field.columns + column] = lineMaximum;
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
713
|
+
const localMinimum = new Float64Array(field.rows);
|
|
714
|
+
const localMaximum = new Float64Array(field.rows);
|
|
715
|
+
slidingExtrema(
|
|
716
|
+
(row) => horizontalMinimum[row * field.columns + column],
|
|
717
|
+
field.rows,
|
|
718
|
+
radiusZ,
|
|
719
|
+
(row, lineMinimum) => {
|
|
720
|
+
localMinimum[row] = lineMinimum;
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
slidingExtrema(
|
|
724
|
+
(row) => horizontalMaximum[row * field.columns + column],
|
|
725
|
+
field.rows,
|
|
726
|
+
radiusZ,
|
|
727
|
+
(row, _lineMinimum, lineMaximum) => {
|
|
728
|
+
localMaximum[row] = lineMaximum;
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
for (let row = 0; row < field.rows; row += 1)
|
|
732
|
+
local.push((localMaximum[row] - localMinimum[row]) / globalRelief);
|
|
733
|
+
}
|
|
734
|
+
local.sort((a, b) => a - b);
|
|
735
|
+
return local[Math.floor(local.length / 2)];
|
|
736
|
+
}
|
|
737
|
+
function downhill(field, index) {
|
|
738
|
+
const row = Math.floor(index / field.columns);
|
|
739
|
+
const column = index % field.columns;
|
|
740
|
+
const current = height(field, row, column);
|
|
741
|
+
let destination = index;
|
|
742
|
+
let lowest = current;
|
|
743
|
+
for (const [rowOffset, columnOffset] of [
|
|
744
|
+
[-1, 0],
|
|
745
|
+
[1, 0],
|
|
746
|
+
[0, -1],
|
|
747
|
+
[0, 1]
|
|
748
|
+
]) {
|
|
749
|
+
const nextRow = row + rowOffset;
|
|
750
|
+
const nextColumn = column + columnOffset;
|
|
751
|
+
if (nextRow < 0 || nextRow >= field.rows || nextColumn < 0 || nextColumn >= field.columns)
|
|
752
|
+
continue;
|
|
753
|
+
const candidate = height(field, nextRow, nextColumn);
|
|
754
|
+
if (candidate < lowest) {
|
|
755
|
+
lowest = candidate;
|
|
756
|
+
destination = nextRow * field.columns + nextColumn;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return destination;
|
|
760
|
+
}
|
|
761
|
+
function streamOrder(field) {
|
|
762
|
+
const count2 = field.rows * field.columns;
|
|
763
|
+
const targets = new Int32Array(count2);
|
|
764
|
+
targets.fill(-1);
|
|
765
|
+
const order = Array.from({ length: count2 }, (_, index) => index).sort(
|
|
766
|
+
(a, b) => height(field, Math.floor(b / field.columns), b % field.columns) - height(field, Math.floor(a / field.columns), a % field.columns) || a - b
|
|
767
|
+
);
|
|
768
|
+
for (let source = 0; source < count2; source += 1) {
|
|
769
|
+
const destination = downhill(field, source);
|
|
770
|
+
targets[source] = destination === source ? -1 : destination;
|
|
771
|
+
}
|
|
772
|
+
const accumulation = new Float64Array(count2);
|
|
773
|
+
for (const source of order) {
|
|
774
|
+
accumulation[source] = accumulation[source] + 1;
|
|
775
|
+
const destination = targets[source];
|
|
776
|
+
if (destination !== void 0 && destination >= 0)
|
|
777
|
+
accumulation[destination] = accumulation[destination] + accumulation[source];
|
|
778
|
+
}
|
|
779
|
+
const cellArea = field.width / (field.columns - 1) * (field.depth / (field.rows - 1));
|
|
780
|
+
const minimumFlow = Math.log1p(2048 / cellArea) / Math.log1p(count2);
|
|
781
|
+
const orders = new Int32Array(count2);
|
|
782
|
+
let maximum = 0;
|
|
783
|
+
for (const index of order) {
|
|
784
|
+
if (field.flow[index] < minimumFlow) continue;
|
|
785
|
+
const row = Math.floor(index / field.columns);
|
|
786
|
+
const column = index % field.columns;
|
|
787
|
+
const upstream = [];
|
|
788
|
+
for (const [rowOffset, columnOffset] of [
|
|
789
|
+
[-1, 0],
|
|
790
|
+
[1, 0],
|
|
791
|
+
[0, -1],
|
|
792
|
+
[0, 1]
|
|
793
|
+
]) {
|
|
794
|
+
const upstreamRow = row + rowOffset;
|
|
795
|
+
const upstreamColumn = column + columnOffset;
|
|
796
|
+
if (upstreamRow < 0 || upstreamRow >= field.rows || upstreamColumn < 0 || upstreamColumn >= field.columns)
|
|
797
|
+
continue;
|
|
798
|
+
const candidate = upstreamRow * field.columns + upstreamColumn;
|
|
799
|
+
if (targets[candidate] === index && field.flow[candidate] >= minimumFlow)
|
|
800
|
+
upstream.push(orders[candidate]);
|
|
801
|
+
}
|
|
802
|
+
const highest = Math.max(1, ...upstream);
|
|
803
|
+
const ties = upstream.filter((value) => value === highest).length;
|
|
804
|
+
orders[index] = ties > 1 ? highest + 1 : highest;
|
|
805
|
+
maximum = Math.max(maximum, orders[index]);
|
|
806
|
+
}
|
|
807
|
+
return maximum;
|
|
808
|
+
}
|
|
809
|
+
function curvatureExcessKurtosis(field) {
|
|
810
|
+
const values = [];
|
|
811
|
+
for (let row = 0; row < field.rows; row += 1) {
|
|
812
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
813
|
+
const center = height(field, row, column);
|
|
814
|
+
values.push(
|
|
815
|
+
height(field, row, column - 1) + height(field, row, column + 1) + height(field, row - 1, column) + height(field, row + 1, column) - 4 * center
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const mean = values.reduce((total, value) => total + value, 0) / values.length;
|
|
820
|
+
const second = values.reduce((total, value) => total + (value - mean) ** 2, 0) / values.length;
|
|
821
|
+
if (second === 0) return Number.NEGATIVE_INFINITY;
|
|
822
|
+
const fourth = values.reduce((total, value) => total + (value - mean) ** 4, 0) / values.length;
|
|
823
|
+
return fourth / (second * second) - 3;
|
|
824
|
+
}
|
|
825
|
+
function slopeTail(field) {
|
|
826
|
+
let steep = 0;
|
|
827
|
+
for (let row = 0; row < field.rows; row += 1) {
|
|
828
|
+
for (let column = 0; column < field.columns; column += 1) {
|
|
829
|
+
const [x, z] = gradient(field, row, column);
|
|
830
|
+
if (Math.atan(Math.hypot(x, z)) > Math.PI / 6) steep += 1;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return steep / (field.rows * field.columns);
|
|
834
|
+
}
|
|
835
|
+
function summarizeWorldTopology(field) {
|
|
836
|
+
if (!Number.isInteger(field.rows) || field.rows < 2)
|
|
837
|
+
throw new Error("World topology rows must be integers of at least 2.");
|
|
838
|
+
if (!Number.isInteger(field.columns) || field.columns < 2)
|
|
839
|
+
throw new Error("World topology columns must be integers of at least 2.");
|
|
840
|
+
if (field.heights.length !== field.rows * field.columns)
|
|
841
|
+
throw new Error("World topology heights must contain rows times columns samples.");
|
|
842
|
+
if (field.flow.length !== field.rows * field.columns)
|
|
843
|
+
throw new Error("World topology flow must contain rows times columns samples.");
|
|
844
|
+
for (let index = 0; index < field.flow.length; index += 1) {
|
|
845
|
+
const value = field.flow[index];
|
|
846
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0 || value > 1)
|
|
847
|
+
throw new Error("World topology flow samples must be finite values from 0 through 1.");
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
directionalAnisotropy: directionalAnisotropy(field),
|
|
851
|
+
effectiveVertexDensityPerKm2: field.rows * field.columns * 1e6 / (field.width * field.depth),
|
|
852
|
+
maxHortonStrahlerOrder: streamOrder(field),
|
|
853
|
+
median64mRelief: median64mRelief(field),
|
|
854
|
+
powerSpectrumSlope: powerSpectrumSlope(field),
|
|
855
|
+
profileCurvatureExcessKurtosis: curvatureExcessKurtosis(field),
|
|
856
|
+
reliefFieldEdge: reliefFieldEdge(field),
|
|
857
|
+
slopeTailAbove30Degrees: slopeTail(field)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/world-tiles.ts
|
|
862
|
+
var MAX_RAW_TOPOLOGY_SAMPLES = 1e4;
|
|
863
|
+
var LOD_POP_THRESHOLD = 16;
|
|
864
|
+
var LOD_TRANSITION_FRAMES = 3;
|
|
865
|
+
var BRIDGE_COORDINATE_EPSILON = 1e-4;
|
|
866
|
+
var BRIDGE_COORDINATE_RELATIVE_EPSILON = 2 ** -22;
|
|
867
|
+
var BRIDGE_STRIP_INDEX_PATTERN = [0, 1, 2, 2, 1, 3, 2, 1, 0, 3, 1, 2];
|
|
868
|
+
function bridgeCoordinateMatches(actual, expected) {
|
|
869
|
+
return Math.abs(actual - expected) <= Math.max(
|
|
870
|
+
BRIDGE_COORDINATE_EPSILON,
|
|
871
|
+
Math.max(1, Math.abs(actual), Math.abs(expected)) * BRIDGE_COORDINATE_RELATIVE_EPSILON
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
var EmptyCollider = class {
|
|
875
|
+
#disposed = false;
|
|
876
|
+
get disposed() {
|
|
877
|
+
return this.#disposed;
|
|
878
|
+
}
|
|
879
|
+
dispose() {
|
|
880
|
+
this.#disposed = true;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
function finite2(value, name) {
|
|
884
|
+
if (!Number.isFinite(value)) throw new Error(`TerrainTiles ${name} must be finite.`);
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
function positive(value, name) {
|
|
888
|
+
finite2(value, name);
|
|
889
|
+
if (value <= 0) throw new Error(`TerrainTiles ${name} must be greater than zero.`);
|
|
890
|
+
return value;
|
|
891
|
+
}
|
|
892
|
+
function integerAtLeast(value, minimum, name) {
|
|
893
|
+
if (!Number.isInteger(value) || value < minimum)
|
|
894
|
+
throw new Error(`TerrainTiles ${name} must be an integer of at least ${minimum}.`);
|
|
895
|
+
return value;
|
|
896
|
+
}
|
|
897
|
+
function keyFor(tileX, tileZ) {
|
|
898
|
+
return `${String(tileX)}:${String(tileZ)}`;
|
|
899
|
+
}
|
|
900
|
+
function lodLevelForDistance(distance, thresholds) {
|
|
901
|
+
let level = 0;
|
|
902
|
+
for (const threshold of thresholds) {
|
|
903
|
+
if (distance < threshold) break;
|
|
904
|
+
level += 1;
|
|
905
|
+
}
|
|
906
|
+
return level;
|
|
907
|
+
}
|
|
908
|
+
function resolutionFor(tileResolution, factor) {
|
|
909
|
+
const cells = (tileResolution - 1) / factor;
|
|
910
|
+
if (!Number.isInteger(cells))
|
|
911
|
+
throw new Error("TerrainTiles tileResolution minus one must divide every lod factor.");
|
|
912
|
+
return cells + 1;
|
|
913
|
+
}
|
|
914
|
+
function estimatedLevelBytes(resolution) {
|
|
915
|
+
const vertices = resolution * resolution + resolution * 4;
|
|
916
|
+
const triangles = (resolution - 1) * (resolution - 1) + (resolution - 1) * 4;
|
|
917
|
+
const edgeSampleBytes = resolution * 4 * Float32Array.BYTES_PER_ELEMENT;
|
|
918
|
+
return vertices * 3 * Float32Array.BYTES_PER_ELEMENT * 2 + triangles * 6 * Uint32Array.BYTES_PER_ELEMENT + edgeSampleBytes;
|
|
919
|
+
}
|
|
920
|
+
function estimatedFieldBytes(tileResolution, worldPasses) {
|
|
921
|
+
const sampleBytes = tileResolution * tileResolution * Float32Array.BYTES_PER_ELEMENT;
|
|
922
|
+
if (worldPasses === void 0) return sampleBytes * 2;
|
|
923
|
+
return sampleBytes * (worldPasses.gpu === false ? 4 : 24);
|
|
924
|
+
}
|
|
925
|
+
function estimatedTileBytes(tileResolution, factors, worldPasses) {
|
|
926
|
+
return factors.reduce(
|
|
927
|
+
(total, factor) => total + estimatedLevelBytes(resolutionFor(tileResolution, factor)),
|
|
928
|
+
estimatedFieldBytes(tileResolution, worldPasses)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
function tiledObservationResolution(extent, tileSize, tileResolution, axis) {
|
|
932
|
+
const tileCount = extent / tileSize;
|
|
933
|
+
if (!Number.isInteger(tileCount) || tileCount < 1)
|
|
934
|
+
throw new Error(
|
|
935
|
+
`TerrainTiles topologyObservation ${axis} must cover a positive whole number of rendered tiles.`
|
|
936
|
+
);
|
|
937
|
+
return tileCount * (tileResolution - 1) + 1;
|
|
938
|
+
}
|
|
939
|
+
function validateTopologyObservation(observation, tileSize, tileResolution) {
|
|
940
|
+
const expectedColumns = tiledObservationResolution(
|
|
941
|
+
observation.width,
|
|
942
|
+
tileSize,
|
|
943
|
+
tileResolution,
|
|
944
|
+
"width"
|
|
945
|
+
);
|
|
946
|
+
const expectedRows = tiledObservationResolution(
|
|
947
|
+
observation.depth,
|
|
948
|
+
tileSize,
|
|
949
|
+
tileResolution,
|
|
950
|
+
"depth"
|
|
951
|
+
);
|
|
952
|
+
if (observation.columns !== expectedColumns)
|
|
953
|
+
throw new Error(
|
|
954
|
+
`TerrainTiles topologyObservation columns must match the rendered tile grid (expected ${String(expectedColumns)}, received ${String(observation.columns)}).`
|
|
955
|
+
);
|
|
956
|
+
if (observation.rows !== expectedRows)
|
|
957
|
+
throw new Error(
|
|
958
|
+
`TerrainTiles topologyObservation rows must match the rendered tile grid (expected ${String(expectedRows)}, received ${String(observation.rows)}).`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
function edgeSamplesFor(values, resolution) {
|
|
962
|
+
const north = new Float32Array(resolution);
|
|
963
|
+
const south = new Float32Array(resolution);
|
|
964
|
+
const west = new Float32Array(resolution);
|
|
965
|
+
const east = new Float32Array(resolution);
|
|
966
|
+
for (let index = 0; index < resolution; index += 1) {
|
|
967
|
+
north[index] = values[index];
|
|
968
|
+
south[index] = values[(resolution - 1) * resolution + index];
|
|
969
|
+
west[index] = values[index * resolution];
|
|
970
|
+
east[index] = values[index * resolution + resolution - 1];
|
|
971
|
+
}
|
|
972
|
+
return { east, north, south, west };
|
|
973
|
+
}
|
|
974
|
+
function edgeVertexIndex(level, side, index) {
|
|
975
|
+
const row = side === "north" ? 0 : side === "south" ? level.resolution - 1 : index;
|
|
976
|
+
const column = side === "west" ? 0 : side === "east" ? level.resolution - 1 : index;
|
|
977
|
+
return row * level.resolution + column;
|
|
978
|
+
}
|
|
979
|
+
function appendQuad(indices, topLeft, bottomLeft, topRight, bottomRight) {
|
|
980
|
+
indices.push(topLeft, bottomLeft, topRight, topRight, bottomLeft, bottomRight);
|
|
981
|
+
}
|
|
982
|
+
function buildLevel(field, resolution, skirtDepth, surface) {
|
|
983
|
+
const positions = [];
|
|
984
|
+
const normals = [];
|
|
985
|
+
const heights = [];
|
|
986
|
+
const minimumX = field.origin.x - field.width / 2;
|
|
987
|
+
const minimumZ = field.origin.z - field.depth / 2;
|
|
988
|
+
const cellWidth = field.width / (resolution - 1);
|
|
989
|
+
const cellDepth = field.depth / (resolution - 1);
|
|
990
|
+
const normal = new Vector3();
|
|
991
|
+
for (let row = 0; row < resolution; row += 1) {
|
|
992
|
+
const z = minimumZ + row * cellDepth;
|
|
993
|
+
for (let column = 0; column < resolution; column += 1) {
|
|
994
|
+
const x = minimumX + column * cellWidth;
|
|
995
|
+
const height2 = field.heightAt(x, z);
|
|
996
|
+
heights.push(height2);
|
|
997
|
+
positions.push(x - field.origin.x, height2, z - field.origin.z);
|
|
998
|
+
field.normalAt(x, z, normal);
|
|
999
|
+
normals.push(normal.x, normal.y, normal.z);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const indices = [];
|
|
1003
|
+
for (let row = 0; row < resolution - 1; row += 1) {
|
|
1004
|
+
for (let column = 0; column < resolution - 1; column += 1) {
|
|
1005
|
+
const topLeft = row * resolution + column;
|
|
1006
|
+
appendQuad(indices, topLeft, topLeft + resolution, topLeft + 1, topLeft + resolution + 1);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const edges = [
|
|
1010
|
+
(index) => index,
|
|
1011
|
+
(index) => (resolution - 1) * resolution + index,
|
|
1012
|
+
(index) => index * resolution,
|
|
1013
|
+
(index) => index * resolution + resolution - 1
|
|
1014
|
+
];
|
|
1015
|
+
for (const edge of edges) {
|
|
1016
|
+
const bottom = positions.length / 3;
|
|
1017
|
+
for (let index = 0; index < resolution; index += 1) {
|
|
1018
|
+
const top = edge(index);
|
|
1019
|
+
positions.push(
|
|
1020
|
+
positions[top * 3],
|
|
1021
|
+
positions[top * 3 + 1] - skirtDepth,
|
|
1022
|
+
positions[top * 3 + 2]
|
|
1023
|
+
);
|
|
1024
|
+
normals.push(0, 1, 0);
|
|
1025
|
+
}
|
|
1026
|
+
for (let index = 0; index < resolution - 1; index += 1) {
|
|
1027
|
+
const topLeft = edge(index);
|
|
1028
|
+
const topRight = edge(index + 1);
|
|
1029
|
+
appendQuad(indices, topLeft, bottom + index, topRight, bottom + index + 1);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const geometry = new BufferGeometry();
|
|
1033
|
+
geometry.setAttribute("position", new BufferAttribute(Float32Array.from(positions), 3));
|
|
1034
|
+
geometry.setAttribute("normal", new BufferAttribute(Float32Array.from(normals), 3));
|
|
1035
|
+
geometry.setIndex(new BufferAttribute(Uint32Array.from(indices), 1));
|
|
1036
|
+
geometry.computeBoundingBox();
|
|
1037
|
+
geometry.computeBoundingSphere();
|
|
1038
|
+
const skirt = inspectSkirtGeometry(geometry, resolution, edges);
|
|
1039
|
+
return {
|
|
1040
|
+
edgeSamples: edgeSamplesFor(heights, resolution),
|
|
1041
|
+
geometry,
|
|
1042
|
+
mesh: new Mesh(geometry, surface),
|
|
1043
|
+
resolution,
|
|
1044
|
+
skirtDepth: skirt.depth,
|
|
1045
|
+
skirtVertexCount: skirt.vertexCount
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function inspectSkirtGeometry(geometry, resolution, edges) {
|
|
1049
|
+
const position = geometry.getAttribute("position");
|
|
1050
|
+
const surfaceVertexCount = resolution * resolution;
|
|
1051
|
+
const vertexCount = position.count - surfaceVertexCount;
|
|
1052
|
+
if (vertexCount < 0 || vertexCount % edges.length !== 0)
|
|
1053
|
+
throw new Error("TerrainTiles generated skirt geometry has an invalid vertex count.");
|
|
1054
|
+
if (vertexCount === 0) return { depth: 0, vertexCount: 0 };
|
|
1055
|
+
const edgeVertexCount = vertexCount / edges.length;
|
|
1056
|
+
let depth = Number.POSITIVE_INFINITY;
|
|
1057
|
+
for (const [edgeIndex, edge] of edges.entries()) {
|
|
1058
|
+
for (let index = 0; index < edgeVertexCount; index += 1) {
|
|
1059
|
+
const top = edge(index);
|
|
1060
|
+
const bottom = surfaceVertexCount + edgeIndex * edgeVertexCount + index;
|
|
1061
|
+
depth = Math.min(depth, position.getY(top) - position.getY(bottom));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (!Number.isFinite(depth) || depth < 0)
|
|
1065
|
+
throw new Error("TerrainTiles generated skirt geometry has an invalid depth.");
|
|
1066
|
+
return { depth, vertexCount };
|
|
1067
|
+
}
|
|
1068
|
+
function opposingEdge(a, b) {
|
|
1069
|
+
if (a.tileX < b.tileX) return ["east", "west"];
|
|
1070
|
+
if (a.tileX > b.tileX) return ["west", "east"];
|
|
1071
|
+
if (a.tileZ < b.tileZ) return ["south", "north"];
|
|
1072
|
+
return ["north", "south"];
|
|
1073
|
+
}
|
|
1074
|
+
function renderedLevel(tile) {
|
|
1075
|
+
return tile.levels.find(({ mesh }) => mesh.visible) ?? tile.levels[tile.lodLevel];
|
|
1076
|
+
}
|
|
1077
|
+
function edgeVertexHeight(level, side, index) {
|
|
1078
|
+
const value = level.geometry.getAttribute("position").getY(edgeVertexIndex(level, side, index));
|
|
1079
|
+
if (!Number.isFinite(value))
|
|
1080
|
+
throw new Error("TerrainTiles seam diagnostic edge height must be finite.");
|
|
1081
|
+
return value;
|
|
1082
|
+
}
|
|
1083
|
+
function edgeHeight(level, side, normalized) {
|
|
1084
|
+
const position = Math.max(0, Math.min(1, normalized)) * (level.resolution - 1);
|
|
1085
|
+
const lower = Math.floor(position);
|
|
1086
|
+
const upper = Math.min(level.resolution - 1, lower + 1);
|
|
1087
|
+
const mix = position - lower;
|
|
1088
|
+
return edgeVertexHeight(level, side, lower) * (1 - mix) + edgeVertexHeight(level, side, upper) * mix;
|
|
1089
|
+
}
|
|
1090
|
+
function edgeWorldVertexHeight(level, side, index, target) {
|
|
1091
|
+
const position = level.geometry.getAttribute("position");
|
|
1092
|
+
const vertex = edgeVertexIndex(level, side, index);
|
|
1093
|
+
const localX = position.getX(vertex);
|
|
1094
|
+
const localY = position.getY(vertex);
|
|
1095
|
+
const localZ = position.getZ(vertex);
|
|
1096
|
+
if (!Number.isFinite(localX) || !Number.isFinite(localY) || !Number.isFinite(localZ))
|
|
1097
|
+
throw new Error("TerrainTiles seam diagnostic bridge coordinates must be finite.");
|
|
1098
|
+
target.set(localX, localY, localZ).applyMatrix4(level.mesh.matrixWorld);
|
|
1099
|
+
if (!Number.isFinite(target.x) || !Number.isFinite(target.y) || !Number.isFinite(target.z))
|
|
1100
|
+
throw new Error("TerrainTiles seam diagnostic bridge world coordinates must be finite.");
|
|
1101
|
+
return target.y;
|
|
1102
|
+
}
|
|
1103
|
+
function edgeWorldHeight(level, side, normalized, target) {
|
|
1104
|
+
const position = Math.max(0, Math.min(1, normalized)) * (level.resolution - 1);
|
|
1105
|
+
const lower = Math.floor(position);
|
|
1106
|
+
const upper = Math.min(level.resolution - 1, lower + 1);
|
|
1107
|
+
const mix = position - lower;
|
|
1108
|
+
return edgeWorldVertexHeight(level, side, lower, target) * (1 - mix) + edgeWorldVertexHeight(level, side, upper, target) * mix;
|
|
1109
|
+
}
|
|
1110
|
+
function bridgeEndpointWorldHeight(position, matrixWorld, vertex, expectedX, expectedY, expectedZ, worldPosition) {
|
|
1111
|
+
const localX = position.getX(vertex);
|
|
1112
|
+
const localY = position.getY(vertex);
|
|
1113
|
+
const localZ = position.getZ(vertex);
|
|
1114
|
+
if (!Number.isFinite(localX) || !Number.isFinite(localY) || !Number.isFinite(localZ))
|
|
1115
|
+
throw new Error("TerrainTiles seam diagnostic bridge coordinates must be finite.");
|
|
1116
|
+
worldPosition.set(localX, localY, localZ).applyMatrix4(matrixWorld);
|
|
1117
|
+
if (!Number.isFinite(worldPosition.x) || !Number.isFinite(worldPosition.y) || !Number.isFinite(worldPosition.z))
|
|
1118
|
+
throw new Error("TerrainTiles seam diagnostic bridge world coordinates must be finite.");
|
|
1119
|
+
if (!Number.isFinite(expectedX) || !Number.isFinite(expectedY) || !Number.isFinite(expectedZ) || !bridgeCoordinateMatches(worldPosition.x, expectedX) || !bridgeCoordinateMatches(worldPosition.y, expectedY) || !bridgeCoordinateMatches(worldPosition.z, expectedZ))
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
"TerrainTiles seam diagnostic bridge topology does not match current neighboring edges."
|
|
1122
|
+
);
|
|
1123
|
+
return worldPosition.y;
|
|
1124
|
+
}
|
|
1125
|
+
function bridgeEndpointHeight(bridge, position, sample, endpoint, tile, side, level, worldPosition, expectedWorldPosition) {
|
|
1126
|
+
const sampleNormalized = sample / (bridge.resolution - 1);
|
|
1127
|
+
const [expectedX, , expectedZ] = edgeWorldPoint(tile.field, side, sampleNormalized, 0);
|
|
1128
|
+
const expectedY = edgeWorldHeight(level, side, sampleNormalized, expectedWorldPosition);
|
|
1129
|
+
return bridgeEndpointWorldHeight(
|
|
1130
|
+
position,
|
|
1131
|
+
bridge.mesh.matrixWorld,
|
|
1132
|
+
sample * 2 + endpoint,
|
|
1133
|
+
expectedX,
|
|
1134
|
+
expectedY,
|
|
1135
|
+
expectedZ,
|
|
1136
|
+
worldPosition
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
function bridgeEndpointPair(a, aSide, b, bSide) {
|
|
1140
|
+
const aLevel = renderedLevel(a);
|
|
1141
|
+
const bLevel = renderedLevel(b);
|
|
1142
|
+
if (aLevel === void 0 || bLevel === void 0)
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
"TerrainTiles seam diagnostic bridge topology has no rendered neighboring edge."
|
|
1145
|
+
);
|
|
1146
|
+
const aEndpoint = { level: aLevel, side: aSide, tile: a };
|
|
1147
|
+
const bEndpoint = { level: bLevel, side: bSide, tile: b };
|
|
1148
|
+
return aLevel.resolution > bLevel.resolution ? [aEndpoint, bEndpoint] : [bEndpoint, aEndpoint];
|
|
1149
|
+
}
|
|
1150
|
+
function interpolatedEdgeSample(samples, normalized, name) {
|
|
1151
|
+
const position = Math.max(0, Math.min(1, normalized)) * (samples.length - 1);
|
|
1152
|
+
const lower = Math.floor(position);
|
|
1153
|
+
const upper = Math.min(samples.length - 1, lower + 1);
|
|
1154
|
+
const mix = position - lower;
|
|
1155
|
+
const lowerValue = samples[lower];
|
|
1156
|
+
const upperValue = samples[upper];
|
|
1157
|
+
if (!Number.isFinite(lowerValue) || !Number.isFinite(upperValue))
|
|
1158
|
+
throw new Error(`TerrainTiles retained ${name} edge sample must be finite.`);
|
|
1159
|
+
return lowerValue * (1 - mix) + upperValue * mix;
|
|
1160
|
+
}
|
|
1161
|
+
function refreshEdgeSamples(level) {
|
|
1162
|
+
for (const side of ["east", "north", "south", "west"]) {
|
|
1163
|
+
const samples = level.edgeSamples[side];
|
|
1164
|
+
for (let index = 0; index < level.resolution; index += 1)
|
|
1165
|
+
samples[index] = edgeVertexHeight(level, side, index);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function edgeWorldPoint(field, side, normalized, height2) {
|
|
1169
|
+
const minimumX = field.origin.x - field.width / 2;
|
|
1170
|
+
const minimumZ = field.origin.z - field.depth / 2;
|
|
1171
|
+
const x = side === "west" ? minimumX : side === "east" ? minimumX + field.width : minimumX + normalized * field.width;
|
|
1172
|
+
const z = side === "north" ? minimumZ : side === "south" ? minimumZ + field.depth : minimumZ + normalized * field.depth;
|
|
1173
|
+
return [x, height2, z];
|
|
1174
|
+
}
|
|
1175
|
+
function restoreLevelEdge(field, level, side) {
|
|
1176
|
+
const position = level.geometry.getAttribute("position");
|
|
1177
|
+
const normalAttribute = level.geometry.getAttribute("normal");
|
|
1178
|
+
const normal = new Vector3();
|
|
1179
|
+
let changed = false;
|
|
1180
|
+
for (let index = 0; index < level.resolution; index += 1) {
|
|
1181
|
+
const normalized = index / (level.resolution - 1);
|
|
1182
|
+
const [x, , z] = edgeWorldPoint(field, side, normalized, 0);
|
|
1183
|
+
const vertex = edgeVertexIndex(level, side, index);
|
|
1184
|
+
const height2 = field.heightAt(x, z);
|
|
1185
|
+
field.normalAt(x, z, normal);
|
|
1186
|
+
if (position.getY(vertex) !== height2) {
|
|
1187
|
+
position.setY(vertex, height2);
|
|
1188
|
+
changed = true;
|
|
1189
|
+
}
|
|
1190
|
+
if (normalAttribute.getX(vertex) !== normal.x || normalAttribute.getY(vertex) !== normal.y || normalAttribute.getZ(vertex) !== normal.z) {
|
|
1191
|
+
normalAttribute.setXYZ(vertex, normal.x, normal.y, normal.z);
|
|
1192
|
+
changed = true;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!changed) return false;
|
|
1196
|
+
updateLevelSkirts(level);
|
|
1197
|
+
refreshEdgeSamples(level);
|
|
1198
|
+
position.needsUpdate = true;
|
|
1199
|
+
normalAttribute.needsUpdate = true;
|
|
1200
|
+
level.geometry.computeBoundingBox();
|
|
1201
|
+
level.geometry.computeBoundingSphere();
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
function stitchGeometryData(finer, finerLevel, finerSide, coarser, coarserLevel, coarserSide) {
|
|
1205
|
+
const positions = [];
|
|
1206
|
+
const normals = [];
|
|
1207
|
+
const indices = [];
|
|
1208
|
+
let coverageDepth = 0;
|
|
1209
|
+
for (let index = 0; index < finerLevel.resolution; index += 1) {
|
|
1210
|
+
const normalized = index / (finerLevel.resolution - 1);
|
|
1211
|
+
const fineHeight = interpolatedEdgeSample(
|
|
1212
|
+
finerLevel.edgeSamples[finerSide],
|
|
1213
|
+
normalized,
|
|
1214
|
+
`${finerSide} of finer LOD`
|
|
1215
|
+
);
|
|
1216
|
+
const coarseHeight = interpolatedEdgeSample(
|
|
1217
|
+
coarserLevel.edgeSamples[coarserSide],
|
|
1218
|
+
normalized,
|
|
1219
|
+
`${coarserSide} of coarser LOD`
|
|
1220
|
+
);
|
|
1221
|
+
coverageDepth = Math.max(coverageDepth, Math.abs(fineHeight - coarseHeight));
|
|
1222
|
+
positions.push(...edgeWorldPoint(finer.field, finerSide, normalized, fineHeight));
|
|
1223
|
+
positions.push(...edgeWorldPoint(coarser.field, coarserSide, normalized, coarseHeight));
|
|
1224
|
+
normals.push(0, 1, 0, 0, 1, 0);
|
|
1225
|
+
}
|
|
1226
|
+
for (let index = 0; index < finerLevel.resolution - 1; index += 1) {
|
|
1227
|
+
const fine = index * 2;
|
|
1228
|
+
const coarse = fine + 1;
|
|
1229
|
+
const nextFine = fine + 2;
|
|
1230
|
+
const nextCoarse = coarse + 2;
|
|
1231
|
+
indices.push(
|
|
1232
|
+
fine,
|
|
1233
|
+
coarse,
|
|
1234
|
+
nextFine,
|
|
1235
|
+
nextFine,
|
|
1236
|
+
coarse,
|
|
1237
|
+
nextCoarse,
|
|
1238
|
+
nextFine,
|
|
1239
|
+
coarse,
|
|
1240
|
+
fine,
|
|
1241
|
+
nextCoarse,
|
|
1242
|
+
coarse,
|
|
1243
|
+
nextFine
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
coverageDepth,
|
|
1248
|
+
indices: Uint32Array.from(indices),
|
|
1249
|
+
normals: Float32Array.from(normals),
|
|
1250
|
+
positions: Float32Array.from(positions)
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function stitchGeometry(data) {
|
|
1254
|
+
const geometry = new BufferGeometry();
|
|
1255
|
+
geometry.setAttribute("position", new BufferAttribute(data.positions, 3));
|
|
1256
|
+
geometry.setAttribute("normal", new BufferAttribute(data.normals, 3));
|
|
1257
|
+
geometry.setIndex(new BufferAttribute(data.indices, 1));
|
|
1258
|
+
geometry.computeBoundingBox();
|
|
1259
|
+
geometry.computeBoundingSphere();
|
|
1260
|
+
return geometry;
|
|
1261
|
+
}
|
|
1262
|
+
function geometryBytes(geometry) {
|
|
1263
|
+
const index = geometry.getIndex();
|
|
1264
|
+
return geometry.getAttribute("position").array.byteLength + geometry.getAttribute("normal").array.byteLength + (index?.array.byteLength ?? 0);
|
|
1265
|
+
}
|
|
1266
|
+
function validateBridgeTriangleTopology(geometry, expectedVertexCount) {
|
|
1267
|
+
const position = geometry.getAttribute("position");
|
|
1268
|
+
const index = geometry.getIndex();
|
|
1269
|
+
const drawRange = geometry.drawRange;
|
|
1270
|
+
const positionArray = position?.array;
|
|
1271
|
+
if (position === void 0 || position.itemSize !== 3 || !Number.isInteger(position.count) || position.count < 6 || position.count % 2 !== 0 || positionArray === void 0 || positionArray.length < position.count * position.itemSize || expectedVertexCount !== void 0 && position.count !== expectedVertexCount || index === null || index.itemSize !== 1 || !Number.isInteger(index.count) || index.array.length !== index.count || !(index.array instanceof Uint16Array || index.array instanceof Uint32Array) || !Number.isInteger(drawRange.start) || drawRange.start !== 0 || drawRange.count !== Number.POSITIVE_INFINITY && (!Number.isInteger(drawRange.count) || drawRange.count < index.count || drawRange.count % 3 !== 0))
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
"TerrainTiles seam diagnostic bridge topology has invalid rendered triangle data."
|
|
1274
|
+
);
|
|
1275
|
+
const vertexCount = position.count;
|
|
1276
|
+
const stripResolution = vertexCount / 2;
|
|
1277
|
+
const expectedIndexCount = (stripResolution - 1) * BRIDGE_STRIP_INDEX_PATTERN.length;
|
|
1278
|
+
if (index.count !== expectedIndexCount)
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
"TerrainTiles seam diagnostic bridge topology has invalid rendered triangle data."
|
|
1281
|
+
);
|
|
1282
|
+
for (let vertex = 0; vertex < vertexCount; vertex += 1) {
|
|
1283
|
+
if (!Number.isFinite(position.getX(vertex)) || !Number.isFinite(position.getY(vertex)) || !Number.isFinite(position.getZ(vertex)))
|
|
1284
|
+
throw new Error("TerrainTiles seam diagnostic bridge coordinates must be finite.");
|
|
1285
|
+
}
|
|
1286
|
+
for (let offset = 0; offset < index.count; offset += 1) {
|
|
1287
|
+
const segment = Math.floor(offset / BRIDGE_STRIP_INDEX_PATTERN.length);
|
|
1288
|
+
const pair = segment * 2;
|
|
1289
|
+
const expected = pair + BRIDGE_STRIP_INDEX_PATTERN[offset % BRIDGE_STRIP_INDEX_PATTERN.length];
|
|
1290
|
+
const vertex = index.getX(offset);
|
|
1291
|
+
if (vertex !== expected)
|
|
1292
|
+
throw new Error("TerrainTiles seam diagnostic bridge topology has invalid index data.");
|
|
1293
|
+
}
|
|
1294
|
+
return position;
|
|
1295
|
+
}
|
|
1296
|
+
function updateStitchBridge(bridge, data) {
|
|
1297
|
+
const position = validateBridgeTriangleTopology(bridge.geometry, bridge.resolution * 2);
|
|
1298
|
+
if (bridge.resolution !== data.positions.length / 6) {
|
|
1299
|
+
const previous = bridge.geometry;
|
|
1300
|
+
bridge.geometry = stitchGeometry(data);
|
|
1301
|
+
bridge.mesh.geometry = bridge.geometry;
|
|
1302
|
+
bridge.resolution = data.positions.length / 6;
|
|
1303
|
+
bridge.bytes = geometryBytes(bridge.geometry);
|
|
1304
|
+
previous.dispose();
|
|
1305
|
+
} else {
|
|
1306
|
+
const normal = bridge.geometry.getAttribute("normal");
|
|
1307
|
+
const index = bridge.geometry.getIndex();
|
|
1308
|
+
if (normal === void 0 || normal.itemSize !== 3 || normal.count !== position.count || index === null)
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
"TerrainTiles seam diagnostic bridge topology has invalid rendered triangle data."
|
|
1311
|
+
);
|
|
1312
|
+
position.array.set(data.positions);
|
|
1313
|
+
normal.array.set(data.normals);
|
|
1314
|
+
index.array.set(data.indices);
|
|
1315
|
+
position.needsUpdate = true;
|
|
1316
|
+
normal.needsUpdate = true;
|
|
1317
|
+
index.needsUpdate = true;
|
|
1318
|
+
bridge.geometry.computeBoundingBox();
|
|
1319
|
+
bridge.geometry.computeBoundingSphere();
|
|
1320
|
+
}
|
|
1321
|
+
bridge.coverageDepth = data.coverageDepth;
|
|
1322
|
+
}
|
|
1323
|
+
function levelHeight(level, row, column) {
|
|
1324
|
+
const position = level.geometry.getAttribute("position");
|
|
1325
|
+
const value = position.getY(row * level.resolution + column);
|
|
1326
|
+
if (!Number.isFinite(value)) throw new Error("TerrainTiles LOD geometry has an invalid height.");
|
|
1327
|
+
return value;
|
|
1328
|
+
}
|
|
1329
|
+
function interpolatedLevelHeight(level, x, z) {
|
|
1330
|
+
const column = Math.max(0, Math.min(1, x)) * (level.resolution - 1);
|
|
1331
|
+
const row = Math.max(0, Math.min(1, z)) * (level.resolution - 1);
|
|
1332
|
+
const column0 = Math.floor(column);
|
|
1333
|
+
const row0 = Math.floor(row);
|
|
1334
|
+
const column1 = Math.min(level.resolution - 1, column0 + 1);
|
|
1335
|
+
const row1 = Math.min(level.resolution - 1, row0 + 1);
|
|
1336
|
+
const columnMix = column - column0;
|
|
1337
|
+
const rowMix = row - row0;
|
|
1338
|
+
const upperLeft = levelHeight(level, row0, column0);
|
|
1339
|
+
const upperRight = levelHeight(level, row0, column1);
|
|
1340
|
+
const lowerLeft = levelHeight(level, row1, column0);
|
|
1341
|
+
const lowerRight = levelHeight(level, row1, column1);
|
|
1342
|
+
const upper = upperLeft + (upperRight - upperLeft) * columnMix;
|
|
1343
|
+
const lower = lowerLeft + (lowerRight - lowerLeft) * columnMix;
|
|
1344
|
+
return upper + (lower - upper) * rowMix;
|
|
1345
|
+
}
|
|
1346
|
+
function interpolatedLevelNormal(level, x, z, target) {
|
|
1347
|
+
const column = Math.max(0, Math.min(1, x)) * (level.resolution - 1);
|
|
1348
|
+
const row = Math.max(0, Math.min(1, z)) * (level.resolution - 1);
|
|
1349
|
+
const column0 = Math.floor(column);
|
|
1350
|
+
const row0 = Math.floor(row);
|
|
1351
|
+
const column1 = Math.min(level.resolution - 1, column0 + 1);
|
|
1352
|
+
const row1 = Math.min(level.resolution - 1, row0 + 1);
|
|
1353
|
+
const columnMix = column - column0;
|
|
1354
|
+
const rowMix = row - row0;
|
|
1355
|
+
const normal = level.geometry.getAttribute("normal");
|
|
1356
|
+
const upperLeft = row0 * level.resolution + column0;
|
|
1357
|
+
const upperRight = row0 * level.resolution + column1;
|
|
1358
|
+
const lowerLeft = row1 * level.resolution + column0;
|
|
1359
|
+
const lowerRight = row1 * level.resolution + column1;
|
|
1360
|
+
const blend = (upper, lower) => upper + (lower - upper) * rowMix;
|
|
1361
|
+
const interpolate = (topLeft, topRight, bottomLeft, bottomRight) => blend(
|
|
1362
|
+
topLeft + (topRight - topLeft) * columnMix,
|
|
1363
|
+
bottomLeft + (bottomRight - bottomLeft) * columnMix
|
|
1364
|
+
);
|
|
1365
|
+
return target.set(
|
|
1366
|
+
interpolate(
|
|
1367
|
+
normal.getX(upperLeft),
|
|
1368
|
+
normal.getX(upperRight),
|
|
1369
|
+
normal.getX(lowerLeft),
|
|
1370
|
+
normal.getX(lowerRight)
|
|
1371
|
+
),
|
|
1372
|
+
interpolate(
|
|
1373
|
+
normal.getY(upperLeft),
|
|
1374
|
+
normal.getY(upperRight),
|
|
1375
|
+
normal.getY(lowerLeft),
|
|
1376
|
+
normal.getY(lowerRight)
|
|
1377
|
+
),
|
|
1378
|
+
interpolate(
|
|
1379
|
+
normal.getZ(upperLeft),
|
|
1380
|
+
normal.getZ(upperRight),
|
|
1381
|
+
normal.getZ(lowerLeft),
|
|
1382
|
+
normal.getZ(lowerRight)
|
|
1383
|
+
)
|
|
1384
|
+
).normalize();
|
|
1385
|
+
}
|
|
1386
|
+
function updateLevelSkirts(level) {
|
|
1387
|
+
const position = level.geometry.getAttribute("position");
|
|
1388
|
+
const normal = level.geometry.getAttribute("normal");
|
|
1389
|
+
const surfaceVertexCount = level.resolution * level.resolution;
|
|
1390
|
+
const edges = [
|
|
1391
|
+
(index) => index,
|
|
1392
|
+
(index) => (level.resolution - 1) * level.resolution + index,
|
|
1393
|
+
(index) => index * level.resolution,
|
|
1394
|
+
(index) => index * level.resolution + level.resolution - 1
|
|
1395
|
+
];
|
|
1396
|
+
for (const [edgeIndex, edge] of edges.entries()) {
|
|
1397
|
+
for (let index = 0; index < level.resolution; index += 1) {
|
|
1398
|
+
const top = edge(index);
|
|
1399
|
+
const bottom = surfaceVertexCount + edgeIndex * level.resolution + index;
|
|
1400
|
+
position.setY(bottom, position.getY(top) - level.skirtDepth);
|
|
1401
|
+
normal.setXYZ(bottom, 0, 1, 0);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function restoreLevelSurface(field, level) {
|
|
1406
|
+
const position = level.geometry.getAttribute("position");
|
|
1407
|
+
const normalAttribute = level.geometry.getAttribute("normal");
|
|
1408
|
+
const minimumX = field.origin.x - field.width / 2;
|
|
1409
|
+
const minimumZ = field.origin.z - field.depth / 2;
|
|
1410
|
+
const cellWidth = field.width / (level.resolution - 1);
|
|
1411
|
+
const cellDepth = field.depth / (level.resolution - 1);
|
|
1412
|
+
const normal = new Vector3();
|
|
1413
|
+
for (let row = 0; row < level.resolution; row += 1) {
|
|
1414
|
+
const z = minimumZ + row * cellDepth;
|
|
1415
|
+
for (let column = 0; column < level.resolution; column += 1) {
|
|
1416
|
+
const x = minimumX + column * cellWidth;
|
|
1417
|
+
const index = row * level.resolution + column;
|
|
1418
|
+
position.setY(index, field.heightAt(x, z));
|
|
1419
|
+
field.normalAt(x, z, normal);
|
|
1420
|
+
normalAttribute.setXYZ(index, normal.x, normal.y, normal.z);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
updateLevelSkirts(level);
|
|
1424
|
+
refreshEdgeSamples(level);
|
|
1425
|
+
position.needsUpdate = true;
|
|
1426
|
+
normalAttribute.needsUpdate = true;
|
|
1427
|
+
level.geometry.computeBoundingBox();
|
|
1428
|
+
level.geometry.computeBoundingSphere();
|
|
1429
|
+
}
|
|
1430
|
+
function updateLodTransitionGeometry(tile, transition, progress) {
|
|
1431
|
+
const from = tile.levels[transition.from];
|
|
1432
|
+
const to = tile.levels[transition.to];
|
|
1433
|
+
if (from === void 0 || to === void 0)
|
|
1434
|
+
throw new Error("TerrainTiles LOD transition references a missing level.");
|
|
1435
|
+
const finer = from.resolution >= to.resolution ? from : to;
|
|
1436
|
+
const coarser = finer === from ? to : from;
|
|
1437
|
+
const fromIsFiner = finer === from;
|
|
1438
|
+
const position = finer.geometry.getAttribute("position");
|
|
1439
|
+
const normalAttribute = finer.geometry.getAttribute("normal");
|
|
1440
|
+
const minimumX = tile.field.origin.x - tile.field.width / 2;
|
|
1441
|
+
const minimumZ = tile.field.origin.z - tile.field.depth / 2;
|
|
1442
|
+
const cellWidth = tile.field.width / (finer.resolution - 1);
|
|
1443
|
+
const cellDepth = tile.field.depth / (finer.resolution - 1);
|
|
1444
|
+
const fineNormal = new Vector3();
|
|
1445
|
+
const coarseNormal = new Vector3();
|
|
1446
|
+
const blendedNormal = new Vector3();
|
|
1447
|
+
for (let row = 0; row < finer.resolution; row += 1) {
|
|
1448
|
+
const z = minimumZ + row * cellDepth;
|
|
1449
|
+
const normalizedZ = row / (finer.resolution - 1);
|
|
1450
|
+
for (let column = 0; column < finer.resolution; column += 1) {
|
|
1451
|
+
const x = minimumX + column * cellWidth;
|
|
1452
|
+
const normalizedX = column / (finer.resolution - 1);
|
|
1453
|
+
const index = row * finer.resolution + column;
|
|
1454
|
+
const fineHeight = tile.field.heightAt(x, z);
|
|
1455
|
+
const coarseHeight = interpolatedLevelHeight(coarser, normalizedX, normalizedZ);
|
|
1456
|
+
const startHeight = fromIsFiner ? fineHeight : coarseHeight;
|
|
1457
|
+
const endHeight = fromIsFiner ? coarseHeight : fineHeight;
|
|
1458
|
+
position.setY(index, startHeight + (endHeight - startHeight) * progress);
|
|
1459
|
+
tile.field.normalAt(x, z, fineNormal);
|
|
1460
|
+
interpolatedLevelNormal(coarser, normalizedX, normalizedZ, coarseNormal);
|
|
1461
|
+
const startNormal = fromIsFiner ? fineNormal : coarseNormal;
|
|
1462
|
+
const endNormal = fromIsFiner ? coarseNormal : fineNormal;
|
|
1463
|
+
blendedNormal.set(
|
|
1464
|
+
startNormal.x + (endNormal.x - startNormal.x) * progress,
|
|
1465
|
+
startNormal.y + (endNormal.y - startNormal.y) * progress,
|
|
1466
|
+
startNormal.z + (endNormal.z - startNormal.z) * progress
|
|
1467
|
+
).normalize();
|
|
1468
|
+
normalAttribute.setXYZ(index, blendedNormal.x, blendedNormal.y, blendedNormal.z);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
updateLevelSkirts(finer);
|
|
1472
|
+
refreshEdgeSamples(finer);
|
|
1473
|
+
position.needsUpdate = true;
|
|
1474
|
+
normalAttribute.needsUpdate = true;
|
|
1475
|
+
finer.geometry.computeBoundingBox();
|
|
1476
|
+
finer.geometry.computeBoundingSphere();
|
|
1477
|
+
}
|
|
1478
|
+
function surfaceHeights(level) {
|
|
1479
|
+
const position = level.geometry.getAttribute("position");
|
|
1480
|
+
const heights = new Float32Array(level.resolution * level.resolution);
|
|
1481
|
+
for (let index = 0; index < heights.length; index += 1) heights[index] = position.getY(index);
|
|
1482
|
+
return heights;
|
|
1483
|
+
}
|
|
1484
|
+
function interpolatedSamplesHeight(samples, resolution, x, z) {
|
|
1485
|
+
const column = Math.max(0, Math.min(1, x)) * (resolution - 1);
|
|
1486
|
+
const row = Math.max(0, Math.min(1, z)) * (resolution - 1);
|
|
1487
|
+
const column0 = Math.floor(column);
|
|
1488
|
+
const row0 = Math.floor(row);
|
|
1489
|
+
const column1 = Math.min(resolution - 1, column0 + 1);
|
|
1490
|
+
const row1 = Math.min(resolution - 1, row0 + 1);
|
|
1491
|
+
const columnMix = column - column0;
|
|
1492
|
+
const rowMix = row - row0;
|
|
1493
|
+
const upperLeft = samples[row0 * resolution + column0];
|
|
1494
|
+
const upperRight = samples[row0 * resolution + column1];
|
|
1495
|
+
const lowerLeft = samples[row1 * resolution + column0];
|
|
1496
|
+
const lowerRight = samples[row1 * resolution + column1];
|
|
1497
|
+
const upper = upperLeft + (upperRight - upperLeft) * columnMix;
|
|
1498
|
+
const lower = lowerLeft + (lowerRight - lowerLeft) * columnMix;
|
|
1499
|
+
return upper + (lower - upper) * rowMix;
|
|
1500
|
+
}
|
|
1501
|
+
function surfaceDeltaFromSamples(samples, resolution, level) {
|
|
1502
|
+
const sampleCount = Math.max(resolution, level.resolution);
|
|
1503
|
+
let maximum = 0;
|
|
1504
|
+
for (let row = 0; row < sampleCount; row += 1) {
|
|
1505
|
+
for (let column = 0; column < sampleCount; column += 1) {
|
|
1506
|
+
const x = column / (sampleCount - 1);
|
|
1507
|
+
const z = row / (sampleCount - 1);
|
|
1508
|
+
maximum = Math.max(
|
|
1509
|
+
maximum,
|
|
1510
|
+
Math.abs(
|
|
1511
|
+
interpolatedSamplesHeight(samples, resolution, x, z) - interpolatedLevelHeight(level, x, z)
|
|
1512
|
+
)
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return maximum;
|
|
1517
|
+
}
|
|
1518
|
+
function seamCoverageDepth(a, b) {
|
|
1519
|
+
const aLevel = a.levels[a.lodLevel];
|
|
1520
|
+
const bLevel = b.levels[b.lodLevel];
|
|
1521
|
+
if (aLevel === void 0 || bLevel === void 0) return 0;
|
|
1522
|
+
return Math.min(aLevel.skirtDepth, bLevel.skirtDepth);
|
|
1523
|
+
}
|
|
1524
|
+
function bridgeCoverageAt(bridge, a, aSide, b, bSide, normalized) {
|
|
1525
|
+
if (bridge.mesh.geometry !== bridge.geometry)
|
|
1526
|
+
throw new Error("TerrainTiles seam diagnostic bridge geometry is not attached to its mesh.");
|
|
1527
|
+
if (!Number.isInteger(bridge.resolution) || bridge.resolution < 2)
|
|
1528
|
+
throw new Error("TerrainTiles seam diagnostic bridge resolution is invalid.");
|
|
1529
|
+
if (bridge.keys[0] !== (a.key < b.key ? a.key : b.key) || bridge.keys[1] !== (a.key < b.key ? b.key : a.key))
|
|
1530
|
+
throw new Error("TerrainTiles seam diagnostic bridge topology does not match its tile pair.");
|
|
1531
|
+
if (!Number.isFinite(normalized))
|
|
1532
|
+
throw new Error("TerrainTiles seam diagnostic bridge sample coordinate must be finite.");
|
|
1533
|
+
const position = validateBridgeTriangleTopology(bridge.mesh.geometry, bridge.resolution * 2);
|
|
1534
|
+
if (position.count !== bridge.resolution * 2)
|
|
1535
|
+
throw new Error("TerrainTiles seam diagnostic bridge topology has invalid coverage.");
|
|
1536
|
+
const [finer, coarser] = bridgeEndpointPair(a, aSide, b, bSide);
|
|
1537
|
+
bridge.mesh.updateWorldMatrix(true, false);
|
|
1538
|
+
finer.level.mesh.updateWorldMatrix(true, false);
|
|
1539
|
+
coarser.level.mesh.updateWorldMatrix(true, false);
|
|
1540
|
+
const worldPosition = new Vector3();
|
|
1541
|
+
const expectedWorldPosition = new Vector3();
|
|
1542
|
+
const samplePosition = Math.max(0, Math.min(1, normalized)) * (bridge.resolution - 1);
|
|
1543
|
+
const lower = Math.floor(samplePosition);
|
|
1544
|
+
const upper = Math.min(bridge.resolution - 1, lower + 1);
|
|
1545
|
+
const mix = samplePosition - lower;
|
|
1546
|
+
const fine = bridgeEndpointHeight(
|
|
1547
|
+
bridge,
|
|
1548
|
+
position,
|
|
1549
|
+
lower,
|
|
1550
|
+
0,
|
|
1551
|
+
finer.tile,
|
|
1552
|
+
finer.side,
|
|
1553
|
+
finer.level,
|
|
1554
|
+
worldPosition,
|
|
1555
|
+
expectedWorldPosition
|
|
1556
|
+
) * (1 - mix) + bridgeEndpointHeight(
|
|
1557
|
+
bridge,
|
|
1558
|
+
position,
|
|
1559
|
+
upper,
|
|
1560
|
+
0,
|
|
1561
|
+
finer.tile,
|
|
1562
|
+
finer.side,
|
|
1563
|
+
finer.level,
|
|
1564
|
+
worldPosition,
|
|
1565
|
+
expectedWorldPosition
|
|
1566
|
+
) * mix;
|
|
1567
|
+
const coarse = bridgeEndpointHeight(
|
|
1568
|
+
bridge,
|
|
1569
|
+
position,
|
|
1570
|
+
lower,
|
|
1571
|
+
1,
|
|
1572
|
+
coarser.tile,
|
|
1573
|
+
coarser.side,
|
|
1574
|
+
coarser.level,
|
|
1575
|
+
worldPosition,
|
|
1576
|
+
expectedWorldPosition
|
|
1577
|
+
) * (1 - mix) + bridgeEndpointHeight(
|
|
1578
|
+
bridge,
|
|
1579
|
+
position,
|
|
1580
|
+
upper,
|
|
1581
|
+
1,
|
|
1582
|
+
coarser.tile,
|
|
1583
|
+
coarser.side,
|
|
1584
|
+
coarser.level,
|
|
1585
|
+
worldPosition,
|
|
1586
|
+
expectedWorldPosition
|
|
1587
|
+
) * mix;
|
|
1588
|
+
const coverage = Math.abs(fine - coarse);
|
|
1589
|
+
if (!Number.isFinite(coverage))
|
|
1590
|
+
throw new Error("TerrainTiles seam diagnostic bridge coverage must be finite.");
|
|
1591
|
+
return coverage;
|
|
1592
|
+
}
|
|
1593
|
+
function seamObservation(a, b, bridge, owner, includeBridgeCoverage) {
|
|
1594
|
+
const [aSide, bSide] = opposingEdge(a, b);
|
|
1595
|
+
const aLevel = renderedLevel(a);
|
|
1596
|
+
const bLevel = renderedLevel(b);
|
|
1597
|
+
if (aLevel === void 0 || bLevel === void 0)
|
|
1598
|
+
throw new Error("TerrainTiles seam diagnostic observation has no rendered level.");
|
|
1599
|
+
const samples = Math.max(aLevel.resolution, bLevel.resolution);
|
|
1600
|
+
const bridgeAttached = includeBridgeCoverage && bridge !== void 0 && bridge.mesh.parent === owner && bridge.mesh.visible === true;
|
|
1601
|
+
let gap = 0;
|
|
1602
|
+
let visualGap = 0;
|
|
1603
|
+
for (let index = 0; index < samples; index += 1) {
|
|
1604
|
+
const normalized = samples === 1 ? 0 : index / (samples - 1);
|
|
1605
|
+
const currentGap = Math.abs(
|
|
1606
|
+
edgeHeight(aLevel, aSide, normalized) - edgeHeight(bLevel, bSide, normalized)
|
|
1607
|
+
);
|
|
1608
|
+
if (!Number.isFinite(currentGap))
|
|
1609
|
+
throw new Error("TerrainTiles seam diagnostic observation must be finite.");
|
|
1610
|
+
gap = Math.max(gap, currentGap);
|
|
1611
|
+
const bridgeCoverage = bridgeAttached ? bridgeCoverageAt(bridge, a, aSide, b, bSide, normalized) : 0;
|
|
1612
|
+
const coverage = Math.max(seamCoverageDepth(a, b), bridgeCoverage);
|
|
1613
|
+
visualGap = Math.max(visualGap, Math.max(0, currentGap - coverage));
|
|
1614
|
+
}
|
|
1615
|
+
if (!Number.isFinite(visualGap))
|
|
1616
|
+
throw new Error("TerrainTiles visual seam diagnostic observation must be finite.");
|
|
1617
|
+
return { gap, visualGap };
|
|
1618
|
+
}
|
|
1619
|
+
function areNeighbors(a, b) {
|
|
1620
|
+
return Math.abs(a.tileX - b.tileX) + Math.abs(a.tileZ - b.tileZ) === 1;
|
|
1621
|
+
}
|
|
1622
|
+
function neighborPairKey(pair) {
|
|
1623
|
+
const [a, b] = pair;
|
|
1624
|
+
return a.key < b.key ? `${a.key}|${b.key}` : `${b.key}|${a.key}`;
|
|
1625
|
+
}
|
|
1626
|
+
function neighborPairs(tiles) {
|
|
1627
|
+
const pairs = [];
|
|
1628
|
+
for (let index = 0; index < tiles.length; index += 1) {
|
|
1629
|
+
const tile = tiles[index];
|
|
1630
|
+
for (let neighborIndex = index + 1; neighborIndex < tiles.length; neighborIndex += 1) {
|
|
1631
|
+
const neighbor = tiles[neighborIndex];
|
|
1632
|
+
if (areNeighbors(tile, neighbor)) pairs.push([tile, neighbor]);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return pairs;
|
|
1636
|
+
}
|
|
1637
|
+
function neighborLodCorrection(pair) {
|
|
1638
|
+
const [a, b] = pair;
|
|
1639
|
+
if (Math.abs(a.lodLevel - b.lodLevel) <= 1) return void 0;
|
|
1640
|
+
const finer = a.lodLevel < b.lodLevel ? a : b;
|
|
1641
|
+
const coarser = finer === a ? b : a;
|
|
1642
|
+
return { coarser, level: finer.lodLevel + 1 };
|
|
1643
|
+
}
|
|
1644
|
+
function reconcileNeighborPair(pair, surface, existingBridge) {
|
|
1645
|
+
const [a, b] = pair;
|
|
1646
|
+
const [aSide, bSide] = opposingEdge(a, b);
|
|
1647
|
+
const aLevel = renderedLevel(a);
|
|
1648
|
+
const bLevel = renderedLevel(b);
|
|
1649
|
+
if (aLevel === void 0 || bLevel === void 0)
|
|
1650
|
+
throw new Error("TerrainTiles cannot reconcile a neighbor with no rendered LOD level.");
|
|
1651
|
+
restoreLevelEdge(a.field, aLevel, aSide);
|
|
1652
|
+
restoreLevelEdge(b.field, bLevel, bSide);
|
|
1653
|
+
if (aLevel.resolution === bLevel.resolution) return void 0;
|
|
1654
|
+
const finer = aLevel.resolution > bLevel.resolution ? a : b;
|
|
1655
|
+
const finerLevel = aLevel.resolution > bLevel.resolution ? aLevel : bLevel;
|
|
1656
|
+
const finerSide = aLevel.resolution > bLevel.resolution ? aSide : bSide;
|
|
1657
|
+
const coarser = finer === a ? b : a;
|
|
1658
|
+
const coarserLevel = finer === a ? bLevel : aLevel;
|
|
1659
|
+
const coarserSide = finer === a ? bSide : aSide;
|
|
1660
|
+
const data = stitchGeometryData(finer, finerLevel, finerSide, coarser, coarserLevel, coarserSide);
|
|
1661
|
+
if (existingBridge === void 0) {
|
|
1662
|
+
const geometry = stitchGeometry(data);
|
|
1663
|
+
const mesh = new Mesh(geometry, surface);
|
|
1664
|
+
mesh.frustumCulled = true;
|
|
1665
|
+
return {
|
|
1666
|
+
bytes: geometryBytes(geometry),
|
|
1667
|
+
coverageDepth: data.coverageDepth,
|
|
1668
|
+
geometry,
|
|
1669
|
+
keys: [a.key, b.key].sort(),
|
|
1670
|
+
mesh,
|
|
1671
|
+
resolution: finerLevel.resolution
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
updateStitchBridge(existingBridge, data);
|
|
1675
|
+
return existingBridge;
|
|
1676
|
+
}
|
|
1677
|
+
function setManualLodLevel(lod, level) {
|
|
1678
|
+
lod._currentLevel = level;
|
|
1679
|
+
}
|
|
1680
|
+
var TerrainTiles = class extends Object3D {
|
|
1681
|
+
residentTileBudget;
|
|
1682
|
+
residentByteBudget;
|
|
1683
|
+
skirtDepth;
|
|
1684
|
+
tileResolution;
|
|
1685
|
+
tileSize;
|
|
1686
|
+
processCadence = "render";
|
|
1687
|
+
#assets;
|
|
1688
|
+
#assetKey;
|
|
1689
|
+
#createCollider;
|
|
1690
|
+
#factors;
|
|
1691
|
+
#lodDistances;
|
|
1692
|
+
#surface;
|
|
1693
|
+
#sampleHeight;
|
|
1694
|
+
#streamRadius;
|
|
1695
|
+
/** Canonical topology samples retained only for the declared diagnostics region. */
|
|
1696
|
+
#topologyField;
|
|
1697
|
+
#topologyBytes;
|
|
1698
|
+
#worldPasses;
|
|
1699
|
+
#resident = /* @__PURE__ */ new Map();
|
|
1700
|
+
#topologyMetrics;
|
|
1701
|
+
#focus;
|
|
1702
|
+
#peakBytes = 0;
|
|
1703
|
+
#peakTiles = 0;
|
|
1704
|
+
#lodTransitions = 0;
|
|
1705
|
+
#maxLodPop = 0;
|
|
1706
|
+
#maxLodTransitionFrames = 0;
|
|
1707
|
+
// These diagnostics are lifetime maxima for this residency owner. Zero is the deliberate
|
|
1708
|
+
// empty/evicted value; each finite live-geometry observation can only increase it.
|
|
1709
|
+
#maxSeamGap = 0;
|
|
1710
|
+
#maxVisualSeamGap = 0;
|
|
1711
|
+
#stitchedEdges = 0;
|
|
1712
|
+
#stitchBytes = 0;
|
|
1713
|
+
#stitches = /* @__PURE__ */ new Map();
|
|
1714
|
+
#released = false;
|
|
1715
|
+
#renderer;
|
|
1716
|
+
constructor(options) {
|
|
1717
|
+
super();
|
|
1718
|
+
this.tileSize = positive(options.tileSize, "tileSize");
|
|
1719
|
+
this.tileResolution = integerAtLeast(options.tileResolution, 3, "tileResolution");
|
|
1720
|
+
this.residentTileBudget = integerAtLeast(options.residentTileBudget, 1, "residentTileBudget");
|
|
1721
|
+
this.residentByteBudget = integerAtLeast(options.residentByteBudget, 1, "residentByteBudget");
|
|
1722
|
+
this.skirtDepth = positive(options.skirtDepth ?? this.tileSize, "skirtDepth");
|
|
1723
|
+
this.#streamRadius = integerAtLeast(options.streamRadius ?? 1, 0, "streamRadius");
|
|
1724
|
+
this.#surface = options.surface;
|
|
1725
|
+
if (options.surface === void 0 || options.surface === null)
|
|
1726
|
+
throw new Error("TerrainTiles surface is required and must be game-owned.");
|
|
1727
|
+
this.#sampleHeight = options.sampleHeight;
|
|
1728
|
+
if (typeof options.sampleHeight !== "function")
|
|
1729
|
+
throw new Error("TerrainTiles sampleHeight is required.");
|
|
1730
|
+
this.#factors = [...options.lodFactors ?? [1, 2, 4]];
|
|
1731
|
+
if (this.#factors.length < 1) throw new Error("TerrainTiles lodFactors must not be empty.");
|
|
1732
|
+
for (const factor of this.#factors) integerAtLeast(factor, 1, "lod factor");
|
|
1733
|
+
for (const factor of this.#factors) resolutionFor(this.tileResolution, factor);
|
|
1734
|
+
this.#lodDistances = [...options.lodDistances ?? [this.tileSize * 2, this.tileSize * 4]];
|
|
1735
|
+
if (this.#lodDistances.length !== this.#factors.length - 1)
|
|
1736
|
+
throw new Error("TerrainTiles lodDistances must contain one threshold per LOD transition.");
|
|
1737
|
+
this.#lodDistances.forEach((distance, index) => {
|
|
1738
|
+
positive(distance, `lodDistances[${String(index)}]`);
|
|
1739
|
+
if (index > 0 && distance <= this.#lodDistances[index - 1])
|
|
1740
|
+
throw new Error("TerrainTiles lodDistances must be strictly increasing.");
|
|
1741
|
+
});
|
|
1742
|
+
if (options.assetKey !== void 0 && options.assets === void 0)
|
|
1743
|
+
throw new Error("TerrainTiles assetKey requires an assets.release consumer.");
|
|
1744
|
+
this.#assets = options.assets;
|
|
1745
|
+
this.#assetKey = options.assetKey;
|
|
1746
|
+
this.#createCollider = options.createCollider;
|
|
1747
|
+
this.#worldPasses = options.worldPasses;
|
|
1748
|
+
if (options.topologyObservation !== void 0)
|
|
1749
|
+
validateTopologyObservation(options.topologyObservation, this.tileSize, this.tileResolution);
|
|
1750
|
+
this.#topologyField = options.topologyObservation === void 0 ? void 0 : Heightfield.fromSampler({
|
|
1751
|
+
...options.topologyObservation,
|
|
1752
|
+
sampleHeight: this.#sampleHeight,
|
|
1753
|
+
...this.#worldPasses === void 0 ? {} : { worldPasses: this.#worldPasses }
|
|
1754
|
+
});
|
|
1755
|
+
this.#topologyBytes = this.#topologyField?.memoryBytes ?? 0;
|
|
1756
|
+
if (this.#topologyBytes > this.residentByteBudget)
|
|
1757
|
+
throw new Error("TerrainTiles residentByteBudget cannot fit the topology observation.");
|
|
1758
|
+
this.#recordPeaks();
|
|
1759
|
+
this.#recordSeamDiagnostics();
|
|
1760
|
+
this.frustumCulled = true;
|
|
1761
|
+
}
|
|
1762
|
+
get released() {
|
|
1763
|
+
return this.#released;
|
|
1764
|
+
}
|
|
1765
|
+
get residentTileCount() {
|
|
1766
|
+
return this.#resident.size;
|
|
1767
|
+
}
|
|
1768
|
+
get residentBytes() {
|
|
1769
|
+
return this.#topologyBytes + this.#stitchBytes + [...this.#resident.values()].reduce((total, tile) => total + tile.bytes, 0);
|
|
1770
|
+
}
|
|
1771
|
+
get peakResidentTileCount() {
|
|
1772
|
+
return this.#peakTiles;
|
|
1773
|
+
}
|
|
1774
|
+
get peakResidentBytes() {
|
|
1775
|
+
return this.#peakBytes;
|
|
1776
|
+
}
|
|
1777
|
+
get residentKeys() {
|
|
1778
|
+
return [...this.#resident.keys()].sort();
|
|
1779
|
+
}
|
|
1780
|
+
get residentColliderKeys() {
|
|
1781
|
+
return [...this.#resident.values()].filter((tile) => tile.collider !== void 0).map((tile) => tile.key).sort();
|
|
1782
|
+
}
|
|
1783
|
+
get lodLevelCount() {
|
|
1784
|
+
return this.#factors.length;
|
|
1785
|
+
}
|
|
1786
|
+
get lodTransitions() {
|
|
1787
|
+
return this.#lodTransitions;
|
|
1788
|
+
}
|
|
1789
|
+
/** Maximum per-render-frame displacement of the visible LOD surface during transitions. */
|
|
1790
|
+
get maxLodPop() {
|
|
1791
|
+
return this.#maxLodPop;
|
|
1792
|
+
}
|
|
1793
|
+
/** Maximum number of rendered frames during which an LOD transition remained observable. */
|
|
1794
|
+
get maxLodTransitionFrames() {
|
|
1795
|
+
return this.#maxLodTransitionFrames;
|
|
1796
|
+
}
|
|
1797
|
+
/** Maximum visible edge gap observed across follow/process calls for this residency owner. */
|
|
1798
|
+
get maxSeamGap() {
|
|
1799
|
+
return this.#maxSeamGap;
|
|
1800
|
+
}
|
|
1801
|
+
/** Maximum remaining visible gap after skirt or bridge coverage observed across follow/process calls. */
|
|
1802
|
+
get maxVisualSeamGap() {
|
|
1803
|
+
return this.#maxVisualSeamGap;
|
|
1804
|
+
}
|
|
1805
|
+
/** Number of mixed-LOD edge reconciliations observed during this residency lifetime. */
|
|
1806
|
+
get stitchedEdgeCount() {
|
|
1807
|
+
return this.#stitchedEdges;
|
|
1808
|
+
}
|
|
1809
|
+
get warmupNodes() {
|
|
1810
|
+
return [
|
|
1811
|
+
...this.#topologyField?.warmupNodes ?? [],
|
|
1812
|
+
...[...this.#resident.values()].flatMap((tile) => tile.field.warmupNodes)
|
|
1813
|
+
];
|
|
1814
|
+
}
|
|
1815
|
+
getTile(key) {
|
|
1816
|
+
return this.#resident.get(key);
|
|
1817
|
+
}
|
|
1818
|
+
follow(position) {
|
|
1819
|
+
if (this.#released) throw new Error("TerrainTiles cannot follow after release.");
|
|
1820
|
+
const x = finite2(position.x, "follow x");
|
|
1821
|
+
const z = finite2(position.z, "follow z");
|
|
1822
|
+
const hadFocus = this.#focus !== void 0;
|
|
1823
|
+
this.#focus = { x, z };
|
|
1824
|
+
const centerX = Math.floor((x + this.tileSize / 2) / this.tileSize);
|
|
1825
|
+
const centerZ = Math.floor((z + this.tileSize / 2) / this.tileSize);
|
|
1826
|
+
const wanted = [];
|
|
1827
|
+
for (let tileZ = centerZ - this.#streamRadius; tileZ <= centerZ + this.#streamRadius; tileZ += 1) {
|
|
1828
|
+
for (let tileX = centerX - this.#streamRadius; tileX <= centerX + this.#streamRadius; tileX += 1) {
|
|
1829
|
+
const tileCenterX = tileX * this.tileSize;
|
|
1830
|
+
const tileCenterZ = tileZ * this.tileSize;
|
|
1831
|
+
wanted.push({
|
|
1832
|
+
distance: Math.hypot(x - tileCenterX, z - tileCenterZ),
|
|
1833
|
+
tileX,
|
|
1834
|
+
tileZ
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
wanted.sort((a, b) => a.distance - b.distance || a.tileZ - b.tileZ || a.tileX - b.tileX);
|
|
1839
|
+
const selected = wanted.slice(0, this.residentTileBudget);
|
|
1840
|
+
const selectedKeys = new Set(selected.map(({ tileX, tileZ }) => keyFor(tileX, tileZ)));
|
|
1841
|
+
for (const tile of [...this.#resident.values()]) {
|
|
1842
|
+
if (!selectedKeys.has(tile.key)) this.#evict(tile);
|
|
1843
|
+
}
|
|
1844
|
+
for (const candidate of selected) {
|
|
1845
|
+
const key = keyFor(candidate.tileX, candidate.tileZ);
|
|
1846
|
+
if (this.#resident.has(key)) {
|
|
1847
|
+
this.#selectLod(this.#resident.get(key), candidate.distance);
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const estimate = estimatedTileBytes(this.tileResolution, this.#factors, this.#worldPasses);
|
|
1851
|
+
if (this.residentBytes + estimate > this.residentByteBudget) {
|
|
1852
|
+
if (candidate.tileX === centerX && candidate.tileZ === centerZ)
|
|
1853
|
+
throw new Error("TerrainTiles residentByteBudget cannot fit the followed tile.");
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const tile = this.#createTile(candidate.tileX, candidate.tileZ, candidate.distance);
|
|
1857
|
+
if (this.residentBytes + tile.bytes > this.residentByteBudget) {
|
|
1858
|
+
this.#disposeTile(tile);
|
|
1859
|
+
if (candidate.tileX === centerX && candidate.tileZ === centerZ)
|
|
1860
|
+
throw new Error("TerrainTiles residentByteBudget cannot fit the followed tile.");
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
this.#resident.set(tile.key, tile);
|
|
1864
|
+
this.add(tile.lod);
|
|
1865
|
+
this.#recordPeaks();
|
|
1866
|
+
}
|
|
1867
|
+
this.#recordPeaks();
|
|
1868
|
+
this.#coordinateNeighborLods(hadFocus);
|
|
1869
|
+
this.#recordSeamDiagnostics(false);
|
|
1870
|
+
this.#reconcileNeighbors();
|
|
1871
|
+
this.#recordSeamDiagnostics();
|
|
1872
|
+
this.#recordPeaks();
|
|
1873
|
+
}
|
|
1874
|
+
heightAt(x, z) {
|
|
1875
|
+
return this.#fieldAt(x, z).heightAt(x, z);
|
|
1876
|
+
}
|
|
1877
|
+
normalAt(x, z, target = new Vector3()) {
|
|
1878
|
+
return this.#fieldAt(x, z).normalAt(x, z, target);
|
|
1879
|
+
}
|
|
1880
|
+
sample(channel, x, z) {
|
|
1881
|
+
if (channel === "slope") return 1 - this.normalAt(x, z).y;
|
|
1882
|
+
return this.#fieldAt(x, z).sample(channel, x, z);
|
|
1883
|
+
}
|
|
1884
|
+
attachRenderer(renderer) {
|
|
1885
|
+
if (this.#released) throw new Error("TerrainTiles cannot attach after release.");
|
|
1886
|
+
this.#renderer = renderer;
|
|
1887
|
+
this.#topologyField?.attachRenderer(renderer);
|
|
1888
|
+
for (const tile of this.#resident.values()) tile.field.attachRenderer(renderer);
|
|
1889
|
+
}
|
|
1890
|
+
process(renderer = this.#renderer) {
|
|
1891
|
+
if (this.#released) return;
|
|
1892
|
+
const lodFrame = this.#captureLodFrame();
|
|
1893
|
+
this.#advanceLodTransitions();
|
|
1894
|
+
if (renderer !== void 0) {
|
|
1895
|
+
this.#topologyField?.process(renderer);
|
|
1896
|
+
for (const tile of this.#resident.values()) {
|
|
1897
|
+
tile.field.attachRenderer(renderer);
|
|
1898
|
+
tile.field.process(renderer);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
this.#recordSeamDiagnostics(false);
|
|
1902
|
+
this.#reconcileNeighbors();
|
|
1903
|
+
this.#recordSeamDiagnostics();
|
|
1904
|
+
this.#recordLodPopAfterReconciliation(lodFrame);
|
|
1905
|
+
this.#recordPeaks();
|
|
1906
|
+
}
|
|
1907
|
+
debug() {
|
|
1908
|
+
const topologyField = this.#topologyField;
|
|
1909
|
+
const topology = topologyField === void 0 ? void 0 : (() => {
|
|
1910
|
+
const heights = topologyField.heights;
|
|
1911
|
+
const flow = topologyField.flow;
|
|
1912
|
+
const description = {
|
|
1913
|
+
columns: topologyField.columns,
|
|
1914
|
+
depth: topologyField.depth,
|
|
1915
|
+
origin: topologyField.origin,
|
|
1916
|
+
rows: topologyField.rows,
|
|
1917
|
+
width: topologyField.width
|
|
1918
|
+
};
|
|
1919
|
+
if (heights.length <= MAX_RAW_TOPOLOGY_SAMPLES)
|
|
1920
|
+
return {
|
|
1921
|
+
...description,
|
|
1922
|
+
heights: Array.from(heights),
|
|
1923
|
+
...flow === void 0 ? {} : { flow: Array.from(flow) }
|
|
1924
|
+
};
|
|
1925
|
+
if (flow === void 0) return description;
|
|
1926
|
+
if (this.#topologyMetrics === void 0)
|
|
1927
|
+
this.#topologyMetrics = summarizeWorldTopology({
|
|
1928
|
+
columns: topologyField.columns,
|
|
1929
|
+
depth: topologyField.depth,
|
|
1930
|
+
flow,
|
|
1931
|
+
heights,
|
|
1932
|
+
rows: topologyField.rows,
|
|
1933
|
+
width: topologyField.width
|
|
1934
|
+
});
|
|
1935
|
+
return {
|
|
1936
|
+
...description,
|
|
1937
|
+
metrics: this.#topologyMetrics
|
|
1938
|
+
};
|
|
1939
|
+
})();
|
|
1940
|
+
return {
|
|
1941
|
+
maxSeamGap: this.maxSeamGap,
|
|
1942
|
+
maxVisualSeamGap: this.maxVisualSeamGap,
|
|
1943
|
+
maxLodTransitionFrames: this.#maxLodTransitionFrames,
|
|
1944
|
+
peakResidentBytes: this.#peakBytes,
|
|
1945
|
+
peakResidentTiles: this.#peakTiles,
|
|
1946
|
+
residentBytes: this.residentBytes,
|
|
1947
|
+
residentByteBudget: this.residentByteBudget,
|
|
1948
|
+
residentKeys: this.residentKeys,
|
|
1949
|
+
residentTiles: this.residentTileCount,
|
|
1950
|
+
residentTileBudget: this.residentTileBudget,
|
|
1951
|
+
topologyBytes: this.#topologyBytes,
|
|
1952
|
+
lodTransitions: this.#lodTransitions,
|
|
1953
|
+
maxLodPop: this.#maxLodPop,
|
|
1954
|
+
stitchedEdges: this.#stitchedEdges,
|
|
1955
|
+
skirtVertexCount: [...this.#resident.values()].reduce(
|
|
1956
|
+
(total, tile) => total + tile.skirtVertexCount,
|
|
1957
|
+
0
|
|
1958
|
+
),
|
|
1959
|
+
released: this.#released,
|
|
1960
|
+
...topology === void 0 ? {} : { topology }
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
detach() {
|
|
1964
|
+
this.dispose();
|
|
1965
|
+
}
|
|
1966
|
+
dispose() {
|
|
1967
|
+
if (this.#released) return;
|
|
1968
|
+
this.#released = true;
|
|
1969
|
+
this.#renderer = void 0;
|
|
1970
|
+
this.#topologyField?.detach();
|
|
1971
|
+
for (const tile of [...this.#resident.values()]) this.#evict(tile);
|
|
1972
|
+
this.#resident.clear();
|
|
1973
|
+
this.removeFromParent();
|
|
1974
|
+
}
|
|
1975
|
+
#createField(origin) {
|
|
1976
|
+
const sampler = {
|
|
1977
|
+
columns: this.tileResolution,
|
|
1978
|
+
depth: this.tileSize,
|
|
1979
|
+
origin,
|
|
1980
|
+
rows: this.tileResolution,
|
|
1981
|
+
sampleHeight: this.#sampleHeight,
|
|
1982
|
+
width: this.tileSize,
|
|
1983
|
+
...this.#worldPasses === void 0 ? {} : { worldPasses: this.#worldPasses }
|
|
1984
|
+
};
|
|
1985
|
+
return Heightfield.fromSampler(sampler);
|
|
1986
|
+
}
|
|
1987
|
+
#createTile(tileX, tileZ, distance) {
|
|
1988
|
+
const origin = { x: tileX * this.tileSize, z: tileZ * this.tileSize };
|
|
1989
|
+
const assetKey = this.#assetKey === void 0 ? void 0 : typeof this.#assetKey === "function" ? this.#assetKey(tileX, tileZ) : this.#assetKey;
|
|
1990
|
+
if (assetKey !== void 0 && (typeof assetKey !== "string" || assetKey.trim().length === 0))
|
|
1991
|
+
throw new Error("TerrainTiles assetKey must resolve to a non-empty string.");
|
|
1992
|
+
const field = this.#createField(origin);
|
|
1993
|
+
const levels = [];
|
|
1994
|
+
let lod;
|
|
1995
|
+
let collider;
|
|
1996
|
+
try {
|
|
1997
|
+
for (const factor of this.#factors)
|
|
1998
|
+
levels.push(
|
|
1999
|
+
buildLevel(
|
|
2000
|
+
field,
|
|
2001
|
+
resolutionFor(this.tileResolution, factor),
|
|
2002
|
+
this.skirtDepth,
|
|
2003
|
+
this.#surface
|
|
2004
|
+
)
|
|
2005
|
+
);
|
|
2006
|
+
lod = new LOD();
|
|
2007
|
+
lod.autoUpdate = false;
|
|
2008
|
+
lod.position.set(origin.x, 0, origin.z);
|
|
2009
|
+
levels.forEach(({ mesh }, index) => {
|
|
2010
|
+
mesh.visible = index === 0;
|
|
2011
|
+
lod?.addLevel(mesh, index === 0 ? 0 : this.#lodDistances[index - 1]);
|
|
2012
|
+
mesh.frustumCulled = true;
|
|
2013
|
+
});
|
|
2014
|
+
collider = this.#createCollider?.({ field, key: keyFor(tileX, tileZ), object: lod, tileX, tileZ }) ?? new EmptyCollider();
|
|
2015
|
+
const bytes = levels.reduce((total, level) => total + estimatedLevelBytes(level.resolution), 0) + field.memoryBytes;
|
|
2016
|
+
const tile = {
|
|
2017
|
+
...assetKey === void 0 ? {} : { assetKey },
|
|
2018
|
+
bytes,
|
|
2019
|
+
collider,
|
|
2020
|
+
field,
|
|
2021
|
+
key: keyFor(tileX, tileZ),
|
|
2022
|
+
lod,
|
|
2023
|
+
lodLevel: 0,
|
|
2024
|
+
levels,
|
|
2025
|
+
object: lod,
|
|
2026
|
+
origin,
|
|
2027
|
+
skirtVertexCount: levels.reduce((total, level) => total + level.skirtVertexCount, 0),
|
|
2028
|
+
skirts: this.skirtDepth,
|
|
2029
|
+
tileX,
|
|
2030
|
+
tileZ
|
|
2031
|
+
};
|
|
2032
|
+
this.#selectLod(tile, distance, false);
|
|
2033
|
+
return tile;
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
collider?.dispose();
|
|
2036
|
+
lod?.removeFromParent();
|
|
2037
|
+
for (const level of levels) level.geometry.dispose();
|
|
2038
|
+
field.detach();
|
|
2039
|
+
throw error;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
#selectLod(tile, distance, countTransition = true) {
|
|
2043
|
+
this.#setLodLevel(tile, lodLevelForDistance(distance, this.#lodDistances), countTransition);
|
|
2044
|
+
}
|
|
2045
|
+
#setLodLevel(tile, level, countTransition = true) {
|
|
2046
|
+
if (level === tile.lodLevel) {
|
|
2047
|
+
setManualLodLevel(tile.lod, level);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const interruptedFrame = tile.lodTransition === void 0 ? void 0 : this.#captureLodFrameForTile(tile);
|
|
2051
|
+
if (tile.lodTransition !== void 0) this.#finishLodTransition(tile);
|
|
2052
|
+
const previousLevel = tile.lodLevel;
|
|
2053
|
+
const previous = tile.levels[previousLevel];
|
|
2054
|
+
const next = tile.levels[level];
|
|
2055
|
+
if (previous === void 0 || next === void 0)
|
|
2056
|
+
throw new Error("TerrainTiles LOD transition references a missing level.");
|
|
2057
|
+
tile.lodLevel = level;
|
|
2058
|
+
if (!countTransition) {
|
|
2059
|
+
setManualLodLevel(tile.lod, level);
|
|
2060
|
+
this.#setLodVisibility(tile);
|
|
2061
|
+
this.#recordLodPopAfterRetarget(interruptedFrame);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
this.#lodTransitions += 1;
|
|
2065
|
+
tile.lodTransition = {
|
|
2066
|
+
elapsedFrames: 0,
|
|
2067
|
+
from: previousLevel,
|
|
2068
|
+
remainingFrames: LOD_TRANSITION_FRAMES,
|
|
2069
|
+
to: level
|
|
2070
|
+
};
|
|
2071
|
+
const finerLevel = previous.resolution >= next.resolution ? previousLevel : level;
|
|
2072
|
+
previous.resolution >= next.resolution ? previous : next;
|
|
2073
|
+
setManualLodLevel(tile.lod, finerLevel);
|
|
2074
|
+
updateLodTransitionGeometry(tile, tile.lodTransition, 0);
|
|
2075
|
+
this.#setLodVisibility(tile, [finerLevel]);
|
|
2076
|
+
this.#recordLodPopAfterRetarget(interruptedFrame);
|
|
2077
|
+
}
|
|
2078
|
+
#setLodVisibility(tile, visibleLevels = [tile.lodLevel]) {
|
|
2079
|
+
const visible = new Set(visibleLevels);
|
|
2080
|
+
tile.levels.forEach(({ mesh }, index) => {
|
|
2081
|
+
mesh.visible = visible.has(index);
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
#captureLodFrame() {
|
|
2085
|
+
const snapshots = [];
|
|
2086
|
+
for (const tile of this.#resident.values()) {
|
|
2087
|
+
if (tile.lodTransition === void 0) continue;
|
|
2088
|
+
snapshots.push(this.#captureLodFrameForTile(tile));
|
|
2089
|
+
}
|
|
2090
|
+
return snapshots;
|
|
2091
|
+
}
|
|
2092
|
+
#captureLodFrameForTile(tile) {
|
|
2093
|
+
const level = renderedLevel(tile);
|
|
2094
|
+
if (level === void 0)
|
|
2095
|
+
throw new Error("TerrainTiles cannot measure an LOD transition without a visible level.");
|
|
2096
|
+
return { heights: surfaceHeights(level), resolution: level.resolution, tile };
|
|
2097
|
+
}
|
|
2098
|
+
#recordLodPopAfterReconciliation(snapshots) {
|
|
2099
|
+
for (const snapshot of snapshots) {
|
|
2100
|
+
const level = renderedLevel(snapshot.tile);
|
|
2101
|
+
if (level === void 0)
|
|
2102
|
+
throw new Error("TerrainTiles cannot measure an LOD frame without a visible level.");
|
|
2103
|
+
this.#recordLodPop(surfaceDeltaFromSamples(snapshot.heights, snapshot.resolution, level));
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
#recordLodPopAfterRetarget(snapshot) {
|
|
2107
|
+
if (snapshot === void 0) return;
|
|
2108
|
+
const level = renderedLevel(snapshot.tile);
|
|
2109
|
+
if (level === void 0)
|
|
2110
|
+
throw new Error(
|
|
2111
|
+
"TerrainTiles cannot measure a retargeted LOD frame without a visible level."
|
|
2112
|
+
);
|
|
2113
|
+
this.#recordLodPop(surfaceDeltaFromSamples(snapshot.heights, snapshot.resolution, level));
|
|
2114
|
+
}
|
|
2115
|
+
#advanceLodTransitions() {
|
|
2116
|
+
for (const tile of this.#resident.values()) {
|
|
2117
|
+
const transition = tile.lodTransition;
|
|
2118
|
+
if (transition === void 0) continue;
|
|
2119
|
+
const from = tile.levels[transition.from];
|
|
2120
|
+
if (from === void 0 || tile.levels[transition.to] === void 0)
|
|
2121
|
+
throw new Error("TerrainTiles LOD transition references a missing level.");
|
|
2122
|
+
transition.elapsedFrames += 1;
|
|
2123
|
+
transition.remainingFrames -= 1;
|
|
2124
|
+
updateLodTransitionGeometry(
|
|
2125
|
+
tile,
|
|
2126
|
+
transition,
|
|
2127
|
+
Math.min(1, transition.elapsedFrames / LOD_TRANSITION_FRAMES)
|
|
2128
|
+
);
|
|
2129
|
+
if (transition.remainingFrames > 0) continue;
|
|
2130
|
+
this.#maxLodTransitionFrames = Math.max(
|
|
2131
|
+
this.#maxLodTransitionFrames,
|
|
2132
|
+
transition.elapsedFrames
|
|
2133
|
+
);
|
|
2134
|
+
this.#restoreLodTransition(tile, transition);
|
|
2135
|
+
tile.lodTransition = void 0;
|
|
2136
|
+
setManualLodLevel(tile.lod, transition.to);
|
|
2137
|
+
this.#setLodVisibility(tile);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
#coordinateNeighborLods(countTransitions) {
|
|
2141
|
+
const pairs = neighborPairs([...this.#resident.values()]);
|
|
2142
|
+
let changed = true;
|
|
2143
|
+
while (changed) {
|
|
2144
|
+
changed = false;
|
|
2145
|
+
for (const pair of pairs) {
|
|
2146
|
+
const correction = neighborLodCorrection(pair);
|
|
2147
|
+
if (correction === void 0) continue;
|
|
2148
|
+
this.#setLodLevel(correction.coarser, correction.level, countTransitions);
|
|
2149
|
+
changed = true;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
#reconcileNeighbors() {
|
|
2154
|
+
const pairs = neighborPairs([...this.#resident.values()]);
|
|
2155
|
+
const active = /* @__PURE__ */ new Set();
|
|
2156
|
+
for (const pair of pairs) {
|
|
2157
|
+
const key = neighborPairKey(pair);
|
|
2158
|
+
active.add(key);
|
|
2159
|
+
const previousBytes = this.#stitches.get(key)?.bytes ?? 0;
|
|
2160
|
+
const bridge = reconcileNeighborPair(pair, this.#surface, this.#stitches.get(key));
|
|
2161
|
+
if (bridge === void 0) {
|
|
2162
|
+
this.#removeStitch(key);
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
if (this.#stitches.has(key)) {
|
|
2166
|
+
this.#stitchBytes += bridge.bytes - previousBytes;
|
|
2167
|
+
} else {
|
|
2168
|
+
this.#stitches.set(key, bridge);
|
|
2169
|
+
this.add(bridge.mesh);
|
|
2170
|
+
this.#stitchBytes += bridge.bytes;
|
|
2171
|
+
}
|
|
2172
|
+
this.#stitchedEdges += 1;
|
|
2173
|
+
}
|
|
2174
|
+
for (const key of this.#stitches.keys()) {
|
|
2175
|
+
if (!active.has(key)) this.#removeStitch(key);
|
|
2176
|
+
}
|
|
2177
|
+
if (this.residentBytes > this.residentByteBudget)
|
|
2178
|
+
throw new Error("TerrainTiles residentByteBudget cannot fit stitched neighbor geometry.");
|
|
2179
|
+
}
|
|
2180
|
+
#finishLodTransition(tile) {
|
|
2181
|
+
const transition = tile.lodTransition;
|
|
2182
|
+
if (transition === void 0) return;
|
|
2183
|
+
this.#maxLodTransitionFrames = Math.max(this.#maxLodTransitionFrames, transition.elapsedFrames);
|
|
2184
|
+
this.#restoreLodTransition(tile, transition);
|
|
2185
|
+
tile.lodTransition = void 0;
|
|
2186
|
+
setManualLodLevel(tile.lod, transition.to);
|
|
2187
|
+
this.#setLodVisibility(tile);
|
|
2188
|
+
}
|
|
2189
|
+
#restoreLodTransition(tile, transition) {
|
|
2190
|
+
const from = tile.levels[transition.from];
|
|
2191
|
+
const to = tile.levels[transition.to];
|
|
2192
|
+
if (from === void 0 || to === void 0) return;
|
|
2193
|
+
restoreLevelSurface(tile.field, from.resolution >= to.resolution ? from : to);
|
|
2194
|
+
}
|
|
2195
|
+
#recordLodPop(pop) {
|
|
2196
|
+
if (!Number.isFinite(pop)) throw new Error("TerrainTiles LOD pop observation must be finite.");
|
|
2197
|
+
this.#maxLodPop = Math.max(this.#maxLodPop, pop);
|
|
2198
|
+
if (pop > LOD_POP_THRESHOLD)
|
|
2199
|
+
throw new Error(
|
|
2200
|
+
`TerrainTiles LOD pop threshold ${String(LOD_POP_THRESHOLD)} exceeded by ${String(pop)}.`
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
#removeStitch(key) {
|
|
2204
|
+
const bridge = this.#stitches.get(key);
|
|
2205
|
+
if (bridge === void 0) return;
|
|
2206
|
+
this.#stitches.delete(key);
|
|
2207
|
+
this.remove(bridge.mesh);
|
|
2208
|
+
this.#stitchBytes -= bridge.bytes;
|
|
2209
|
+
bridge.geometry.dispose();
|
|
2210
|
+
}
|
|
2211
|
+
#removeStitchesForTile(tileKey) {
|
|
2212
|
+
for (const [key, bridge] of this.#stitches) {
|
|
2213
|
+
if (bridge.keys.includes(tileKey)) this.#removeStitch(key);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
#recordSeamDiagnostics(includeBridgeCoverage = true) {
|
|
2217
|
+
const tiles = [...this.#resident.values()];
|
|
2218
|
+
for (const tile of tiles) {
|
|
2219
|
+
for (const neighbor of tiles) {
|
|
2220
|
+
if (tile.key >= neighbor.key || !areNeighbors(tile, neighbor)) continue;
|
|
2221
|
+
const { gap, visualGap } = seamObservation(
|
|
2222
|
+
tile,
|
|
2223
|
+
neighbor,
|
|
2224
|
+
this.#stitches.get(neighborPairKey([tile, neighbor])),
|
|
2225
|
+
this,
|
|
2226
|
+
includeBridgeCoverage
|
|
2227
|
+
);
|
|
2228
|
+
this.#maxSeamGap = Math.max(this.#maxSeamGap, gap);
|
|
2229
|
+
this.#maxVisualSeamGap = Math.max(this.#maxVisualSeamGap, visualGap);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
#fieldAt(x, z) {
|
|
2234
|
+
finite2(x, "query x");
|
|
2235
|
+
finite2(z, "query z");
|
|
2236
|
+
const tileX = Math.floor((x + this.tileSize / 2) / this.tileSize);
|
|
2237
|
+
const tileZ = Math.floor((z + this.tileSize / 2) / this.tileSize);
|
|
2238
|
+
const tile = this.#resident.get(keyFor(tileX, tileZ));
|
|
2239
|
+
if (tile === void 0)
|
|
2240
|
+
throw new Error(`TerrainTiles query (${x}, ${z}) is outside its resident region.`);
|
|
2241
|
+
return tile.field;
|
|
2242
|
+
}
|
|
2243
|
+
#evict(tile) {
|
|
2244
|
+
if (this.#resident.get(tile.key) === tile) this.#resident.delete(tile.key);
|
|
2245
|
+
this.#removeStitchesForTile(tile.key);
|
|
2246
|
+
this.remove(tile.lod);
|
|
2247
|
+
tile.collider.dispose();
|
|
2248
|
+
tile.field.detach();
|
|
2249
|
+
for (const level of tile.levels) level.geometry.dispose();
|
|
2250
|
+
this.#releaseAsset(tile);
|
|
2251
|
+
}
|
|
2252
|
+
#disposeTile(tile) {
|
|
2253
|
+
tile.collider.dispose();
|
|
2254
|
+
tile.field.detach();
|
|
2255
|
+
for (const level of tile.levels) level.geometry.dispose();
|
|
2256
|
+
tile.lod.removeFromParent();
|
|
2257
|
+
this.#releaseAsset(tile);
|
|
2258
|
+
}
|
|
2259
|
+
#releaseAsset(tile) {
|
|
2260
|
+
if (this.#assets === void 0 || tile.assetKey === void 0) return;
|
|
2261
|
+
const stillReferenced = [...this.#resident.values()].some(
|
|
2262
|
+
(resident) => resident !== tile && resident.assetKey === tile.assetKey
|
|
2263
|
+
);
|
|
2264
|
+
if (!stillReferenced) this.#assets.release("model", tile.assetKey);
|
|
2265
|
+
}
|
|
2266
|
+
#recordPeaks() {
|
|
2267
|
+
this.#peakTiles = Math.max(this.#peakTiles, this.residentTileCount);
|
|
2268
|
+
this.#peakBytes = Math.max(this.#peakBytes, this.residentBytes);
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// src/world-capabilities.ts
|
|
2273
|
+
var DEFAULT_LIMITS = {
|
|
2274
|
+
maxComputeInvocationsPerWorkgroup: 0,
|
|
2275
|
+
maxComputeWorkgroupsPerDimension: 0,
|
|
2276
|
+
maxStorageBufferBindingSize: 0
|
|
2277
|
+
};
|
|
2278
|
+
function finite3(value, name, minimum = 0) {
|
|
2279
|
+
if (!Number.isFinite(value) || value < minimum)
|
|
2280
|
+
throw new Error(`World capabilities ${name} must be finite and at least ${String(minimum)}.`);
|
|
2281
|
+
return value;
|
|
2282
|
+
}
|
|
2283
|
+
function integer(value, name, minimum = 0) {
|
|
2284
|
+
if (!Number.isInteger(value) || value < minimum)
|
|
2285
|
+
throw new Error(
|
|
2286
|
+
`World capabilities ${name} must be an integer of at least ${String(minimum)}.`
|
|
2287
|
+
);
|
|
2288
|
+
return value;
|
|
2289
|
+
}
|
|
2290
|
+
function resolveLimits(limits) {
|
|
2291
|
+
return {
|
|
2292
|
+
maxComputeInvocationsPerWorkgroup: finite3(
|
|
2293
|
+
limits?.maxComputeInvocationsPerWorkgroup ?? DEFAULT_LIMITS.maxComputeInvocationsPerWorkgroup,
|
|
2294
|
+
"maxComputeInvocationsPerWorkgroup"
|
|
2295
|
+
),
|
|
2296
|
+
maxComputeWorkgroupsPerDimension: finite3(
|
|
2297
|
+
limits?.maxComputeWorkgroupsPerDimension ?? DEFAULT_LIMITS.maxComputeWorkgroupsPerDimension,
|
|
2298
|
+
"maxComputeWorkgroupsPerDimension"
|
|
2299
|
+
),
|
|
2300
|
+
maxStorageBufferBindingSize: finite3(
|
|
2301
|
+
limits?.maxStorageBufferBindingSize ?? DEFAULT_LIMITS.maxStorageBufferBindingSize,
|
|
2302
|
+
"maxStorageBufferBindingSize"
|
|
2303
|
+
)
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
function getWorldCapabilities(options = {}) {
|
|
2307
|
+
const minimumWorkgroups = integer(
|
|
2308
|
+
options.minimumWorkgroupsPerDimension ?? 1,
|
|
2309
|
+
"minimumWorkgroupsPerDimension",
|
|
2310
|
+
1
|
|
2311
|
+
);
|
|
2312
|
+
const minimumStorage = finite3(
|
|
2313
|
+
options.minimumStorageBufferBindingSize ?? 1,
|
|
2314
|
+
"minimumStorageBufferBindingSize",
|
|
2315
|
+
1
|
|
2316
|
+
);
|
|
2317
|
+
const fallbackIterations = integer(options.cpuFallbackIterations ?? 0, "cpuFallbackIterations");
|
|
2318
|
+
const limits = resolveLimits(options.limits);
|
|
2319
|
+
const hasReportedLimits = options.limits?.maxComputeInvocationsPerWorkgroup !== void 0 && options.limits?.maxComputeWorkgroupsPerDimension !== void 0 && options.limits?.maxStorageBufferBindingSize !== void 0;
|
|
2320
|
+
const gpuAvailable = options.gpuAvailable ?? false;
|
|
2321
|
+
const meetsRequirements = hasReportedLimits && limits.maxComputeInvocationsPerWorkgroup > 0 && limits.maxComputeWorkgroupsPerDimension >= minimumWorkgroups && limits.maxStorageBufferBindingSize >= minimumStorage;
|
|
2322
|
+
const reason = !gpuAvailable ? "GPU compute is unavailable on this host." : !hasReportedLimits ? "GPU compute limits were not reported by the host." : !meetsRequirements ? `GPU limits are below the required workgroup (${String(minimumWorkgroups)}) or storage-buffer (${String(minimumStorage)}) floor.` : "GPU compute is available, but canonical GPU world-field readback is unsupported.";
|
|
2323
|
+
if (fallbackIterations > 0)
|
|
2324
|
+
return {
|
|
2325
|
+
compute: false,
|
|
2326
|
+
cpuFallbackIterations: fallbackIterations,
|
|
2327
|
+
generation: "cpu-fallback",
|
|
2328
|
+
gpu: gpuAvailable,
|
|
2329
|
+
limits,
|
|
2330
|
+
reason: `${reason} Using ${String(fallbackIterations)} reduced CPU erosion iterations.`
|
|
2331
|
+
};
|
|
2332
|
+
return {
|
|
2333
|
+
compute: false,
|
|
2334
|
+
cpuFallbackIterations: 0,
|
|
2335
|
+
generation: "unsupported",
|
|
2336
|
+
gpu: gpuAvailable,
|
|
2337
|
+
limits,
|
|
2338
|
+
reason
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/world.ts
|
|
2343
|
+
function finite4(value, name) {
|
|
2344
|
+
if (!Number.isFinite(value)) throw new Error(`Heightfield ${name} must be finite.`);
|
|
2345
|
+
return value;
|
|
2346
|
+
}
|
|
2347
|
+
function positive2(value, name) {
|
|
2348
|
+
finite4(value, name);
|
|
2349
|
+
if (value <= 0) throw new Error(`Heightfield ${name} must be greater than zero.`);
|
|
2350
|
+
return value;
|
|
2351
|
+
}
|
|
2352
|
+
function count(value, name) {
|
|
2353
|
+
if (!Number.isInteger(value) || value < 2)
|
|
2354
|
+
throw new Error(`Heightfield ${name} must be an integer of at least 2.`);
|
|
2355
|
+
return value;
|
|
2356
|
+
}
|
|
2357
|
+
function shouldBuildGpuPasses(options) {
|
|
2358
|
+
return options?.gpu === true;
|
|
2359
|
+
}
|
|
2360
|
+
function validateWorldPassBudget(options) {
|
|
2361
|
+
if (!Number.isInteger(options.dispatchBudget) || options.dispatchBudget <= 0)
|
|
2362
|
+
throw new Error("Heightfield world passes dispatchBudget must be a positive integer.");
|
|
2363
|
+
if (options.gpu === true) return;
|
|
2364
|
+
if (!Number.isInteger(options.erosion.iterations) || options.erosion.iterations < 0) return;
|
|
2365
|
+
if (options.erosion.iterations > options.dispatchBudget)
|
|
2366
|
+
throw new Error("Heightfield dispatchBudget cannot cover synchronous CPU erosion iterations.");
|
|
2367
|
+
}
|
|
2368
|
+
var Heightfield = class _Heightfield extends Group {
|
|
2369
|
+
columns;
|
|
2370
|
+
depth;
|
|
2371
|
+
origin;
|
|
2372
|
+
rows;
|
|
2373
|
+
width;
|
|
2374
|
+
processCadence = "render";
|
|
2375
|
+
warmupNodes;
|
|
2376
|
+
#cellDepth;
|
|
2377
|
+
#cellWidth;
|
|
2378
|
+
#colliderHeights;
|
|
2379
|
+
#heights;
|
|
2380
|
+
#minimumX;
|
|
2381
|
+
#minimumZ;
|
|
2382
|
+
#flow;
|
|
2383
|
+
#moisture;
|
|
2384
|
+
#gpu;
|
|
2385
|
+
#heightReadback;
|
|
2386
|
+
#flowReadback;
|
|
2387
|
+
#moistureReadback;
|
|
2388
|
+
#renderer;
|
|
2389
|
+
#gpuCompletionObserved = false;
|
|
2390
|
+
#released = false;
|
|
2391
|
+
constructor(options) {
|
|
2392
|
+
super();
|
|
2393
|
+
this.columns = count(options.columns, "columns");
|
|
2394
|
+
this.rows = count(options.rows, "rows");
|
|
2395
|
+
this.width = positive2(options.width, "width");
|
|
2396
|
+
this.depth = positive2(options.depth, "depth");
|
|
2397
|
+
this.origin = {
|
|
2398
|
+
x: finite4(options.origin.x, "origin.x"),
|
|
2399
|
+
z: finite4(options.origin.z, "origin.z")
|
|
2400
|
+
};
|
|
2401
|
+
const expected = this.rows * this.columns;
|
|
2402
|
+
if (options.heights.length !== expected)
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`Heightfield expected ${expected} heights, received ${options.heights.length}.`
|
|
2405
|
+
);
|
|
2406
|
+
if (options.flow !== void 0 && options.flow.length !== expected)
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
`Heightfield expected ${expected} flow samples, received ${options.flow.length}.`
|
|
2409
|
+
);
|
|
2410
|
+
if (options.moisture !== void 0 && options.moisture.length !== expected)
|
|
2411
|
+
throw new Error(
|
|
2412
|
+
`Heightfield expected ${expected} moisture samples, received ${options.moisture.length}.`
|
|
2413
|
+
);
|
|
2414
|
+
for (const height2 of options.heights) finite4(height2, "height sample");
|
|
2415
|
+
for (const value of options.flow ?? []) finite4(value, "flow sample");
|
|
2416
|
+
for (const value of options.moisture ?? []) finite4(value, "moisture sample");
|
|
2417
|
+
const baseHeights = options.heights.slice();
|
|
2418
|
+
const storedFlow = options.flow?.slice();
|
|
2419
|
+
const storedMoisture = options.moisture?.slice();
|
|
2420
|
+
const passOptions = options.worldPasses;
|
|
2421
|
+
if (passOptions !== void 0) validateWorldPassBudget(passOptions);
|
|
2422
|
+
if (passOptions?.gpu === true)
|
|
2423
|
+
throw new Error(
|
|
2424
|
+
"Heightfield GPU generation cannot be canonical; use gpu: false until GPU readback owns the field."
|
|
2425
|
+
);
|
|
2426
|
+
const cpu = passOptions === void 0 ? void 0 : simulateWorldPassesCpu({
|
|
2427
|
+
cellDepth: this.depth / (this.rows - 1),
|
|
2428
|
+
cellWidth: this.width / (this.columns - 1),
|
|
2429
|
+
columns: this.columns,
|
|
2430
|
+
erosion: passOptions.erosion,
|
|
2431
|
+
heights: baseHeights,
|
|
2432
|
+
rows: this.rows
|
|
2433
|
+
});
|
|
2434
|
+
this.#heights = cpu?.heights ?? baseHeights;
|
|
2435
|
+
this.#flow = cpu?.flow ?? storedFlow;
|
|
2436
|
+
this.#moisture = cpu?.moisture ?? storedMoisture;
|
|
2437
|
+
this.#gpu = passOptions === void 0 || !shouldBuildGpuPasses(passOptions) ? void 0 : createWorldGpuPasses({
|
|
2438
|
+
cellDepth: this.depth / (this.rows - 1),
|
|
2439
|
+
cellWidth: this.width / (this.columns - 1),
|
|
2440
|
+
columns: this.columns,
|
|
2441
|
+
dispatchBudget: passOptions.dispatchBudget,
|
|
2442
|
+
erosion: passOptions.erosion,
|
|
2443
|
+
heights: baseHeights,
|
|
2444
|
+
rows: this.rows
|
|
2445
|
+
});
|
|
2446
|
+
this.warmupNodes = this.#gpu?.stages.flatMap((stage) => stage.nodes) ?? [];
|
|
2447
|
+
this.#heightReadback = this.#gpu === void 0 ? void 0 : new GPUReadback({ attribute: this.#gpu.height.value, everyFrames: 1 });
|
|
2448
|
+
this.#flowReadback = this.#gpu === void 0 ? void 0 : new GPUReadback({ attribute: this.#gpu.flow.value, everyFrames: 1 });
|
|
2449
|
+
this.#moistureReadback = this.#gpu === void 0 ? void 0 : new GPUReadback({ attribute: this.#gpu.moisture.value, everyFrames: 1 });
|
|
2450
|
+
this.#colliderHeights = new Float32Array(expected);
|
|
2451
|
+
for (let column = 0; column < this.columns; column += 1) {
|
|
2452
|
+
for (let row = 0; row < this.rows; row += 1) {
|
|
2453
|
+
this.#colliderHeights[column * this.rows + row] = this.#height(row * this.columns + column);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
this.#cellWidth = this.width / (this.columns - 1);
|
|
2457
|
+
this.#cellDepth = this.depth / (this.rows - 1);
|
|
2458
|
+
this.#minimumX = this.origin.x - this.width / 2;
|
|
2459
|
+
this.#minimumZ = this.origin.z - this.depth / 2;
|
|
2460
|
+
}
|
|
2461
|
+
static fromSampler(options) {
|
|
2462
|
+
const columns = count(options.columns, "columns");
|
|
2463
|
+
const rows = count(options.rows, "rows");
|
|
2464
|
+
const width = positive2(options.width, "width");
|
|
2465
|
+
const depth = positive2(options.depth, "depth");
|
|
2466
|
+
const originX = finite4(options.origin.x, "origin.x");
|
|
2467
|
+
const originZ = finite4(options.origin.z, "origin.z");
|
|
2468
|
+
const minimumX = originX - width / 2;
|
|
2469
|
+
const minimumZ = originZ - depth / 2;
|
|
2470
|
+
const cellWidth = width / (columns - 1);
|
|
2471
|
+
const cellDepth = depth / (rows - 1);
|
|
2472
|
+
const heights = new Float32Array(rows * columns);
|
|
2473
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2474
|
+
const z = minimumZ + row * cellDepth;
|
|
2475
|
+
for (let column = 0; column < columns; column += 1) {
|
|
2476
|
+
const x = minimumX + column * cellWidth;
|
|
2477
|
+
heights[row * columns + column] = finite4(options.sampleHeight(x, z), "sampleHeight result");
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
return new _Heightfield({
|
|
2481
|
+
columns,
|
|
2482
|
+
depth,
|
|
2483
|
+
heights,
|
|
2484
|
+
origin: options.origin,
|
|
2485
|
+
rows,
|
|
2486
|
+
width,
|
|
2487
|
+
...options.worldPasses === void 0 ? {} : { worldPasses: options.worldPasses }
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
sample(channel, x, z) {
|
|
2491
|
+
if (channel === "height") return this.heightAt(x, z);
|
|
2492
|
+
if (channel === "slope") return 1 - this.normalAt(x, z).y;
|
|
2493
|
+
const values = channel === "flow" ? this.#flow : channel === "moisture" ? this.#moisture : void 0;
|
|
2494
|
+
if (values === void 0) throw new Error(`Heightfield unknown channel '${channel}'.`);
|
|
2495
|
+
return this.#interpolateValues(values, x, z);
|
|
2496
|
+
}
|
|
2497
|
+
get released() {
|
|
2498
|
+
return this.#released;
|
|
2499
|
+
}
|
|
2500
|
+
get generationComplete() {
|
|
2501
|
+
return this.#gpu?.queue.complete ?? true;
|
|
2502
|
+
}
|
|
2503
|
+
get gpuHeightSample() {
|
|
2504
|
+
return this.#heightReadback?.sample;
|
|
2505
|
+
}
|
|
2506
|
+
attachRenderer(renderer) {
|
|
2507
|
+
if (this.#released) throw new Error("Heightfield cannot be attached after release.");
|
|
2508
|
+
if (this.#gpu === void 0) return;
|
|
2509
|
+
if (renderer.kind !== "webgpu") throw new Error("Heightfield world passes require WebGPU.");
|
|
2510
|
+
if (this.#renderer !== void 0 && this.#renderer !== renderer)
|
|
2511
|
+
throw new Error("Heightfield is already attached to a renderer.");
|
|
2512
|
+
this.#renderer = renderer;
|
|
2513
|
+
}
|
|
2514
|
+
process(renderer = this.#renderer) {
|
|
2515
|
+
if (this.#released || this.#gpu === void 0) return;
|
|
2516
|
+
if (renderer === void 0) throw new Error("Heightfield is not attached to a renderer.");
|
|
2517
|
+
const submitted = this.#gpu.queue.process(renderer);
|
|
2518
|
+
if (submitted > 0 || !this.#gpu.queue.complete) {
|
|
2519
|
+
this.#gpuCompletionObserved = false;
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (!this.#gpuCompletionObserved) {
|
|
2523
|
+
this.#gpuCompletionObserved = true;
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (this.#heightReadback?.sample === void 0) {
|
|
2527
|
+
this.#heightReadback?.request(renderer);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (this.#flowReadback?.sample === void 0) {
|
|
2531
|
+
this.#flowReadback?.request(renderer);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
if (this.#moistureReadback?.sample === void 0) this.#moistureReadback?.request(renderer);
|
|
2535
|
+
}
|
|
2536
|
+
debug() {
|
|
2537
|
+
return {
|
|
2538
|
+
complete: this.generationComplete,
|
|
2539
|
+
dispatched: this.#gpu?.queue.dispatched ?? 0,
|
|
2540
|
+
gpuHeightMaxError: this.#sampleError(this.#heightReadback?.sample, this.#heights),
|
|
2541
|
+
gpuFlowMaxError: this.#sampleError(this.#flowReadback?.sample, this.#flow),
|
|
2542
|
+
gpuMoistureMaxError: this.#sampleError(this.#moistureReadback?.sample, this.#moisture),
|
|
2543
|
+
released: this.#released
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
detach() {
|
|
2547
|
+
if (this.#released) return;
|
|
2548
|
+
this.#renderer = void 0;
|
|
2549
|
+
this.#heightReadback?.dispose();
|
|
2550
|
+
this.#flowReadback?.dispose();
|
|
2551
|
+
this.#moistureReadback?.dispose();
|
|
2552
|
+
this.#gpu?.dispose();
|
|
2553
|
+
this.#released = true;
|
|
2554
|
+
}
|
|
2555
|
+
/** A copy of the canonical row-major samples, safe for game-side analysis. */
|
|
2556
|
+
get heights() {
|
|
2557
|
+
return this.#heights.slice();
|
|
2558
|
+
}
|
|
2559
|
+
/** A copy of the normalized routed-flow channel, when world passes were requested. */
|
|
2560
|
+
get flow() {
|
|
2561
|
+
return this.#flow?.slice();
|
|
2562
|
+
}
|
|
2563
|
+
/** Bytes retained by this field's CPU channels and its conservative GPU allowance. */
|
|
2564
|
+
get memoryBytes() {
|
|
2565
|
+
const sampleBytes = this.rows * this.columns * Float32Array.BYTES_PER_ELEMENT;
|
|
2566
|
+
const cpuBytes = sampleBytes * 2 + (this.#flow === void 0 ? 0 : sampleBytes) + (this.#moisture === void 0 ? 0 : sampleBytes);
|
|
2567
|
+
return this.#gpu === void 0 ? cpuBytes : Math.max(cpuBytes, sampleBytes * 24);
|
|
2568
|
+
}
|
|
2569
|
+
heightAt(x, z) {
|
|
2570
|
+
finite4(x, "query x");
|
|
2571
|
+
finite4(z, "query z");
|
|
2572
|
+
const column = (x - this.#minimumX) / this.#cellWidth;
|
|
2573
|
+
const row = (z - this.#minimumZ) / this.#cellDepth;
|
|
2574
|
+
const epsilon = 1e-9;
|
|
2575
|
+
if (column < -epsilon || row < -epsilon || column > this.columns - 1 + epsilon || row > this.rows - 1 + epsilon)
|
|
2576
|
+
throw new Error(`Heightfield query (${x}, ${z}) is outside its resident region.`);
|
|
2577
|
+
return this.#interpolate(
|
|
2578
|
+
Math.min(this.columns - 1, Math.max(0, column)),
|
|
2579
|
+
Math.min(this.rows - 1, Math.max(0, row))
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
normalAt(x, z, target = new Vector3()) {
|
|
2583
|
+
this.heightAt(x, z);
|
|
2584
|
+
const leftX = Math.max(this.#minimumX, x - this.#cellWidth);
|
|
2585
|
+
const rightX = Math.min(this.#minimumX + this.width, x + this.#cellWidth);
|
|
2586
|
+
const nearZ = Math.max(this.#minimumZ, z - this.#cellDepth);
|
|
2587
|
+
const farZ = Math.min(this.#minimumZ + this.depth, z + this.#cellDepth);
|
|
2588
|
+
const slopeX = (this.heightAt(rightX, z) - this.heightAt(leftX, z)) / (rightX - leftX);
|
|
2589
|
+
const slopeZ = (this.heightAt(x, farZ) - this.heightAt(x, nearZ)) / (farZ - nearZ);
|
|
2590
|
+
return target.set(-slopeX, 1, -slopeZ).normalize();
|
|
2591
|
+
}
|
|
2592
|
+
/** The same values transposed once into Rapier's column-major height-matrix order. */
|
|
2593
|
+
toColliderHeights() {
|
|
2594
|
+
return this.#colliderHeights.slice();
|
|
2595
|
+
}
|
|
2596
|
+
/** Builds an ordinary Three.js geometry without choosing its surface. */
|
|
2597
|
+
toGeometry() {
|
|
2598
|
+
const count2 = this.rows * this.columns;
|
|
2599
|
+
const positions = new Float32Array(count2 * 3);
|
|
2600
|
+
const normals = new Float32Array(count2 * 3);
|
|
2601
|
+
const normal = new Vector3();
|
|
2602
|
+
for (let row = 0; row < this.rows; row += 1) {
|
|
2603
|
+
const localZ = -this.depth / 2 + row * this.#cellDepth;
|
|
2604
|
+
const worldZ = this.origin.z + localZ;
|
|
2605
|
+
for (let column = 0; column < this.columns; column += 1) {
|
|
2606
|
+
const index = row * this.columns + column;
|
|
2607
|
+
const localX = -this.width / 2 + column * this.#cellWidth;
|
|
2608
|
+
const worldX = this.origin.x + localX;
|
|
2609
|
+
positions[index * 3] = localX;
|
|
2610
|
+
positions[index * 3 + 1] = this.#height(index);
|
|
2611
|
+
positions[index * 3 + 2] = localZ;
|
|
2612
|
+
this.normalAt(worldX, worldZ, normal);
|
|
2613
|
+
normals[index * 3] = normal.x;
|
|
2614
|
+
normals[index * 3 + 1] = normal.y;
|
|
2615
|
+
normals[index * 3 + 2] = normal.z;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
const indices = new Uint32Array((this.rows - 1) * (this.columns - 1) * 6);
|
|
2619
|
+
let offset = 0;
|
|
2620
|
+
for (let row = 0; row < this.rows - 1; row += 1) {
|
|
2621
|
+
for (let column = 0; column < this.columns - 1; column += 1) {
|
|
2622
|
+
const upperLeft = row * this.columns + column;
|
|
2623
|
+
const upperRight = upperLeft + 1;
|
|
2624
|
+
const lowerLeft = upperLeft + this.columns;
|
|
2625
|
+
const lowerRight = lowerLeft + 1;
|
|
2626
|
+
indices.set([upperLeft, lowerLeft, upperRight, upperRight, lowerLeft, lowerRight], offset);
|
|
2627
|
+
offset += 6;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
const geometry = new BufferGeometry();
|
|
2631
|
+
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
2632
|
+
geometry.setAttribute("normal", new BufferAttribute(normals, 3));
|
|
2633
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
2634
|
+
return geometry;
|
|
2635
|
+
}
|
|
2636
|
+
#interpolate(column, row) {
|
|
2637
|
+
const column0 = Math.floor(column);
|
|
2638
|
+
const row0 = Math.floor(row);
|
|
2639
|
+
const column1 = Math.min(this.columns - 1, column0 + 1);
|
|
2640
|
+
const row1 = Math.min(this.rows - 1, row0 + 1);
|
|
2641
|
+
const columnMix = column - column0;
|
|
2642
|
+
const rowMix = row - row0;
|
|
2643
|
+
const upperLeft = this.#height(row0 * this.columns + column0);
|
|
2644
|
+
const upperRight = this.#height(row0 * this.columns + column1);
|
|
2645
|
+
const lowerLeft = this.#height(row1 * this.columns + column0);
|
|
2646
|
+
const lowerRight = this.#height(row1 * this.columns + column1);
|
|
2647
|
+
const upper = upperLeft + (upperRight - upperLeft) * columnMix;
|
|
2648
|
+
const lower = lowerLeft + (lowerRight - lowerLeft) * columnMix;
|
|
2649
|
+
return upper + (lower - upper) * rowMix;
|
|
2650
|
+
}
|
|
2651
|
+
#interpolateValues(values, x, z) {
|
|
2652
|
+
finite4(x, "query x");
|
|
2653
|
+
finite4(z, "query z");
|
|
2654
|
+
const column = (x - this.#minimumX) / this.#cellWidth;
|
|
2655
|
+
const row = (z - this.#minimumZ) / this.#cellDepth;
|
|
2656
|
+
if (column < 0 || row < 0 || column > this.columns - 1 || row > this.rows - 1)
|
|
2657
|
+
throw new Error(`Heightfield query (${x}, ${z}) is outside its resident region.`);
|
|
2658
|
+
const column0 = Math.floor(column);
|
|
2659
|
+
const row0 = Math.floor(row);
|
|
2660
|
+
const column1 = Math.min(this.columns - 1, column0 + 1);
|
|
2661
|
+
const row1 = Math.min(this.rows - 1, row0 + 1);
|
|
2662
|
+
const columnMix = column - column0;
|
|
2663
|
+
const rowMix = row - row0;
|
|
2664
|
+
const upper = values[row0 * this.columns + column0] * (1 - columnMix) + values[row0 * this.columns + column1] * columnMix;
|
|
2665
|
+
const lower = values[row1 * this.columns + column0] * (1 - columnMix) + values[row1 * this.columns + column1] * columnMix;
|
|
2666
|
+
return upper * (1 - rowMix) + lower * rowMix;
|
|
2667
|
+
}
|
|
2668
|
+
#sampleError(sample, expected) {
|
|
2669
|
+
if (sample === void 0 || expected === void 0) return void 0;
|
|
2670
|
+
if (sample.data.length !== expected.length) return Number.POSITIVE_INFINITY;
|
|
2671
|
+
let maximum = 0;
|
|
2672
|
+
for (let index = 0; index < expected.length; index += 1)
|
|
2673
|
+
maximum = Math.max(
|
|
2674
|
+
maximum,
|
|
2675
|
+
Math.abs(sample.data[index] - expected[index])
|
|
2676
|
+
);
|
|
2677
|
+
return maximum;
|
|
2678
|
+
}
|
|
2679
|
+
#height(index) {
|
|
2680
|
+
const height2 = this.#heights[index];
|
|
2681
|
+
if (height2 === void 0) throw new Error(`Heightfield internal sample ${index} is missing.`);
|
|
2682
|
+
return height2;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
export { Heightfield, TerrainTiles, getWorldCapabilities };
|