@hello-terrain/three 0.0.0-alpha.2 → 0.0.0-alpha.4
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/dist/index.cjs +766 -467
- package/dist/index.d.cts +374 -233
- package/dist/index.d.mts +374 -233
- package/dist/index.d.ts +374 -233
- package/dist/index.mjs +726 -449
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const three = require('three');
|
|
4
|
+
const webgpu = require('three/webgpu');
|
|
4
5
|
const tsl = require('three/tsl');
|
|
6
|
+
const work = require('@hello-terrain/work');
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
8
|
-
const n = Object.create(null);
|
|
9
|
-
if (e) {
|
|
10
|
-
for (const k in e) {
|
|
11
|
-
n[k] = e[k];
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
n.default = e;
|
|
15
|
-
return n;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const THREE__namespace = /*#__PURE__*/_interopNamespaceCompat(THREE);
|
|
19
|
-
|
|
20
|
-
class TerrainGeometry extends THREE.BufferGeometry {
|
|
8
|
+
class TerrainGeometry extends three.BufferGeometry {
|
|
21
9
|
constructor(innerSegments = 14, extendUV = false) {
|
|
22
10
|
super();
|
|
23
11
|
if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
|
|
@@ -29,21 +17,21 @@ class TerrainGeometry extends THREE.BufferGeometry {
|
|
|
29
17
|
this.setIndex(this.generateIndices(innerSegments));
|
|
30
18
|
this.setAttribute(
|
|
31
19
|
"position",
|
|
32
|
-
new
|
|
20
|
+
new three.BufferAttribute(
|
|
33
21
|
new Float32Array(this.generatePositions(innerSegments)),
|
|
34
22
|
3
|
|
35
23
|
)
|
|
36
24
|
);
|
|
37
25
|
this.setAttribute(
|
|
38
26
|
"normal",
|
|
39
|
-
new
|
|
27
|
+
new three.BufferAttribute(
|
|
40
28
|
new Float32Array(this.generateNormals(innerSegments)),
|
|
41
29
|
3
|
|
42
30
|
)
|
|
43
31
|
);
|
|
44
32
|
this.setAttribute(
|
|
45
33
|
"uv",
|
|
46
|
-
new
|
|
34
|
+
new three.BufferAttribute(
|
|
47
35
|
new Float32Array(
|
|
48
36
|
extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
|
|
49
37
|
),
|
|
@@ -229,6 +217,57 @@ class TerrainGeometry extends THREE.BufferGeometry {
|
|
|
229
217
|
}
|
|
230
218
|
}
|
|
231
219
|
|
|
220
|
+
const defaultTerrainMeshParams = {
|
|
221
|
+
innerTileSegments: 14,
|
|
222
|
+
maxNodes: 2048,
|
|
223
|
+
material: new webgpu.MeshStandardNodeMaterial()
|
|
224
|
+
};
|
|
225
|
+
class TerrainMesh extends webgpu.InstancedMesh {
|
|
226
|
+
_innerTileSegments;
|
|
227
|
+
_maxNodes;
|
|
228
|
+
constructor(params = defaultTerrainMeshParams) {
|
|
229
|
+
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
230
|
+
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
231
|
+
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
232
|
+
super(geometry, material, maxNodes);
|
|
233
|
+
this._innerTileSegments = innerTileSegments;
|
|
234
|
+
this._maxNodes = maxNodes;
|
|
235
|
+
}
|
|
236
|
+
get innerTileSegments() {
|
|
237
|
+
return this._innerTileSegments;
|
|
238
|
+
}
|
|
239
|
+
set innerTileSegments(tileSegments) {
|
|
240
|
+
const oldGeometry = this.geometry;
|
|
241
|
+
this.geometry = new TerrainGeometry(tileSegments, true);
|
|
242
|
+
this._innerTileSegments = tileSegments;
|
|
243
|
+
setTimeout(oldGeometry.dispose);
|
|
244
|
+
}
|
|
245
|
+
get maxNodes() {
|
|
246
|
+
return this._maxNodes;
|
|
247
|
+
}
|
|
248
|
+
set maxNodes(maxNodes) {
|
|
249
|
+
this._maxNodes = maxNodes;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
|
|
254
|
+
return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
|
|
255
|
+
});
|
|
256
|
+
const vectorSpaceToTextureSpace = tsl.Fn(([value]) => {
|
|
257
|
+
return tsl.remap(value, tsl.float(-1), tsl.float(1), tsl.float(0), tsl.float(1));
|
|
258
|
+
});
|
|
259
|
+
const blendAngleCorrectedNormals = tsl.Fn(([n1, n2]) => {
|
|
260
|
+
const t = tsl.vec3(n1.x, n1.y, n1.z.add(1));
|
|
261
|
+
const u = tsl.vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
262
|
+
const r = t.mul(tsl.dot(t, u)).sub(u.mul(t.z)).normalize();
|
|
263
|
+
return r;
|
|
264
|
+
});
|
|
265
|
+
const deriveNormalZ = tsl.Fn(([normalXY]) => {
|
|
266
|
+
const xy = normalXY.toVar();
|
|
267
|
+
const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
|
|
268
|
+
return tsl.vec3(xy.x, xy.y, z);
|
|
269
|
+
});
|
|
270
|
+
|
|
232
271
|
const isSkirtVertex = tsl.Fn(([segments]) => {
|
|
233
272
|
const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
|
|
234
273
|
const vIndex = tsl.int(tsl.vertexIndex);
|
|
@@ -249,487 +288,747 @@ const isSkirtUV = tsl.Fn(([segments]) => {
|
|
|
249
288
|
return innerX.and(innerY).not();
|
|
250
289
|
});
|
|
251
290
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return [
|
|
332
|
-
this.neighborsIndicesBuffer[offset],
|
|
333
|
-
this.neighborsIndicesBuffer[offset + 1],
|
|
334
|
-
this.neighborsIndicesBuffer[offset + 2],
|
|
335
|
-
this.neighborsIndicesBuffer[offset + 3]
|
|
336
|
-
];
|
|
337
|
-
}
|
|
338
|
-
// Setters for individual buffer values
|
|
339
|
-
setLevel(index, level) {
|
|
340
|
-
this.nodeBuffer[index * NODE_STRIDE] = level;
|
|
341
|
-
}
|
|
342
|
-
setX(index, x) {
|
|
343
|
-
this.nodeBuffer[index * NODE_STRIDE + 1] = x;
|
|
344
|
-
}
|
|
345
|
-
setY(index, y) {
|
|
346
|
-
this.nodeBuffer[index * NODE_STRIDE + 2] = y;
|
|
347
|
-
}
|
|
348
|
-
setLeaf(index, leaf) {
|
|
349
|
-
const wasLeaf = this.leafNodeMask[index] === 1;
|
|
350
|
-
const newValue = leaf ? 1 : 0;
|
|
351
|
-
if (leaf && !wasLeaf) {
|
|
352
|
-
this.leafNodeCountBuffer[0]++;
|
|
353
|
-
this.leafNodeMask[index] = 1;
|
|
354
|
-
this.activeLeafIndices[this.activeLeafCount] = index;
|
|
355
|
-
this.activeLeafCount++;
|
|
356
|
-
this.setChildren(index, [
|
|
357
|
-
EMPTY_SENTINEL_VALUE,
|
|
358
|
-
EMPTY_SENTINEL_VALUE,
|
|
359
|
-
EMPTY_SENTINEL_VALUE,
|
|
360
|
-
EMPTY_SENTINEL_VALUE
|
|
361
|
-
]);
|
|
362
|
-
} else if (!leaf && wasLeaf) {
|
|
363
|
-
this.leafNodeCountBuffer[0]--;
|
|
364
|
-
this.leafNodeMask[index] = 0;
|
|
365
|
-
}
|
|
366
|
-
this.nodeBuffer[index * NODE_STRIDE + 3] = newValue;
|
|
367
|
-
}
|
|
368
|
-
setChildren(index, children) {
|
|
369
|
-
const offset = index * CHILDREN_STRIDE;
|
|
370
|
-
this.childrenIndicesBuffer[offset] = children[0];
|
|
371
|
-
this.childrenIndicesBuffer[offset + 1] = children[1];
|
|
372
|
-
this.childrenIndicesBuffer[offset + 2] = children[2];
|
|
373
|
-
this.childrenIndicesBuffer[offset + 3] = children[3];
|
|
374
|
-
}
|
|
375
|
-
setNeighbors(index, neighbors) {
|
|
376
|
-
const offset = index * NEIGHBORS_STRIDE;
|
|
377
|
-
this.neighborsIndicesBuffer[offset] = neighbors[0];
|
|
378
|
-
this.neighborsIndicesBuffer[offset + 1] = neighbors[1];
|
|
379
|
-
this.neighborsIndicesBuffer[offset + 2] = neighbors[2];
|
|
380
|
-
this.neighborsIndicesBuffer[offset + 3] = neighbors[3];
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Get array of active leaf node indices with count (zero-copy, no allocation)
|
|
384
|
-
*/
|
|
385
|
-
getActiveLeafNodeIndices() {
|
|
386
|
-
return {
|
|
387
|
-
indices: this.activeLeafIndices,
|
|
388
|
-
count: this.activeLeafCount
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Release internal buffers and mark this view as destroyed
|
|
393
|
-
*/
|
|
394
|
-
destroy() {
|
|
395
|
-
this.childrenIndicesBuffer = new Uint16Array(0);
|
|
396
|
-
this.neighborsIndicesBuffer = new Uint16Array(0);
|
|
397
|
-
this.nodeBuffer = new Int32Array(0);
|
|
398
|
-
this.leafNodeMask = new Uint8Array(0);
|
|
399
|
-
this.leafNodeCountBuffer = new Uint16Array(0);
|
|
400
|
-
this.maxNodeCount = 0;
|
|
401
|
-
}
|
|
402
|
-
clone() {
|
|
403
|
-
return new QuadtreeNodeView(
|
|
404
|
-
this.maxNodeCount,
|
|
405
|
-
this.childrenIndicesBuffer,
|
|
406
|
-
this.neighborsIndicesBuffer,
|
|
407
|
-
this.nodeBuffer,
|
|
408
|
-
this.leafNodeMask,
|
|
409
|
-
this.leafNodeCountBuffer
|
|
410
|
-
);
|
|
291
|
+
function createTileWorldPosition(leafStorage, terrainUniforms) {
|
|
292
|
+
return tsl.Fn(() => {
|
|
293
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
294
|
+
const nodeIndex = tsl.int(tsl.instanceIndex);
|
|
295
|
+
const nodeOffset = nodeIndex.mul(tsl.int(4));
|
|
296
|
+
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
297
|
+
const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
|
|
298
|
+
const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
|
|
299
|
+
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
300
|
+
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
301
|
+
const half = tsl.float(0.5);
|
|
302
|
+
const size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
|
|
303
|
+
const halfRoot = rootSize.mul(half);
|
|
304
|
+
const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
|
|
305
|
+
const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
|
|
306
|
+
const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
|
|
307
|
+
const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
|
|
308
|
+
const worldX = centerX.add(clampedX.mul(size));
|
|
309
|
+
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
310
|
+
const baseY = rootOrigin.y;
|
|
311
|
+
const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
|
|
312
|
+
const worldY = tsl.select(skirtVertex, skirtY, baseY);
|
|
313
|
+
return tsl.vec3(worldX, worldY, worldZ);
|
|
314
|
+
})();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const Dir = {
|
|
318
|
+
LEFT: 0,
|
|
319
|
+
RIGHT: 1,
|
|
320
|
+
TOP: 2,
|
|
321
|
+
BOTTOM: 3
|
|
322
|
+
};
|
|
323
|
+
const U32_EMPTY = 4294967295;
|
|
324
|
+
function allocLeafSet(capacity) {
|
|
325
|
+
return {
|
|
326
|
+
capacity,
|
|
327
|
+
count: 0,
|
|
328
|
+
space: new Uint8Array(capacity),
|
|
329
|
+
level: new Uint8Array(capacity),
|
|
330
|
+
x: new Uint32Array(capacity),
|
|
331
|
+
y: new Uint32Array(capacity)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function resetLeafSet(leaves) {
|
|
335
|
+
leaves.count = 0;
|
|
336
|
+
}
|
|
337
|
+
function allocSeamTable(capacity) {
|
|
338
|
+
return {
|
|
339
|
+
capacity,
|
|
340
|
+
count: 0,
|
|
341
|
+
stride: 8,
|
|
342
|
+
neighbors: new Uint32Array(capacity * 8)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function resetSeamTable(seams) {
|
|
346
|
+
seams.count = 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function createNodeStore(maxNodes, spaceCount) {
|
|
350
|
+
return {
|
|
351
|
+
maxNodes,
|
|
352
|
+
nodesUsed: 0,
|
|
353
|
+
currentGen: 1,
|
|
354
|
+
gen: new Uint16Array(maxNodes),
|
|
355
|
+
space: new Uint8Array(maxNodes),
|
|
356
|
+
level: new Uint8Array(maxNodes),
|
|
357
|
+
x: new Uint32Array(maxNodes),
|
|
358
|
+
y: new Uint32Array(maxNodes),
|
|
359
|
+
firstChild: new Uint32Array(maxNodes),
|
|
360
|
+
flags: new Uint8Array(maxNodes),
|
|
361
|
+
roots: new Uint32Array(spaceCount)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function beginFrame(store) {
|
|
365
|
+
store.nodesUsed = 0;
|
|
366
|
+
store.currentGen = store.currentGen + 1 & 65535;
|
|
367
|
+
if (store.currentGen === 0) {
|
|
368
|
+
store.gen.fill(0);
|
|
369
|
+
store.currentGen = 1;
|
|
411
370
|
}
|
|
412
371
|
}
|
|
372
|
+
function allocNode(store, tile) {
|
|
373
|
+
const id = store.nodesUsed;
|
|
374
|
+
if (id >= store.maxNodes) return U32_EMPTY;
|
|
375
|
+
store.nodesUsed = id + 1;
|
|
376
|
+
store.gen[id] = store.currentGen;
|
|
377
|
+
store.space[id] = tile.space;
|
|
378
|
+
store.level[id] = tile.level;
|
|
379
|
+
store.x[id] = tile.x >>> 0;
|
|
380
|
+
store.y[id] = tile.y >>> 0;
|
|
381
|
+
store.firstChild[id] = U32_EMPTY;
|
|
382
|
+
store.flags[id] = 0;
|
|
383
|
+
return id;
|
|
384
|
+
}
|
|
385
|
+
function hasChildren(store, nodeId) {
|
|
386
|
+
return store.firstChild[nodeId] !== U32_EMPTY;
|
|
387
|
+
}
|
|
388
|
+
function ensureChildren(store, parentId) {
|
|
389
|
+
const existing = store.firstChild[parentId];
|
|
390
|
+
if (existing !== U32_EMPTY) return existing;
|
|
391
|
+
const childBase = store.nodesUsed;
|
|
392
|
+
if (childBase + 4 > store.maxNodes) return U32_EMPTY;
|
|
393
|
+
const space = store.space[parentId];
|
|
394
|
+
const level = store.level[parentId] + 1;
|
|
395
|
+
const px = store.x[parentId] << 1;
|
|
396
|
+
const py = store.y[parentId] << 1;
|
|
397
|
+
allocNode(store, { space, level, x: px, y: py });
|
|
398
|
+
allocNode(store, { space, level, x: px + 1, y: py });
|
|
399
|
+
allocNode(store, { space, level, x: px, y: py + 1 });
|
|
400
|
+
allocNode(store, { space, level, x: px + 1, y: py + 1 });
|
|
401
|
+
store.firstChild[parentId] = childBase;
|
|
402
|
+
return childBase;
|
|
403
|
+
}
|
|
413
404
|
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
405
|
+
function nextPow2(n) {
|
|
406
|
+
let x = 1;
|
|
407
|
+
while (x < n) x <<= 1;
|
|
408
|
+
return x;
|
|
409
|
+
}
|
|
410
|
+
function mix32(x) {
|
|
411
|
+
x >>>= 0;
|
|
412
|
+
x ^= x >>> 16;
|
|
413
|
+
x = Math.imul(x, 2146121005) >>> 0;
|
|
414
|
+
x ^= x >>> 15;
|
|
415
|
+
x = Math.imul(x, 2221713035) >>> 0;
|
|
416
|
+
x ^= x >>> 16;
|
|
417
|
+
return x >>> 0;
|
|
418
|
+
}
|
|
419
|
+
function hashKey(space, level, x, y) {
|
|
420
|
+
const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
|
|
421
|
+
return mix32(h);
|
|
422
|
+
}
|
|
423
|
+
function createSpatialIndex(maxEntries) {
|
|
424
|
+
const size = nextPow2(Math.max(2, maxEntries * 2));
|
|
425
|
+
return {
|
|
426
|
+
size,
|
|
427
|
+
mask: size - 1,
|
|
428
|
+
stampGen: 1,
|
|
429
|
+
stamp: new Uint16Array(size),
|
|
430
|
+
keysSpace: new Uint8Array(size),
|
|
431
|
+
keysLevel: new Uint8Array(size),
|
|
432
|
+
keysX: new Uint32Array(size),
|
|
433
|
+
keysY: new Uint32Array(size),
|
|
434
|
+
values: new Uint32Array(size)
|
|
420
435
|
};
|
|
421
436
|
}
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
437
|
+
function resetSpatialIndex(index) {
|
|
438
|
+
index.stampGen = index.stampGen + 1 & 65535;
|
|
439
|
+
if (index.stampGen === 0) {
|
|
440
|
+
index.stamp.fill(0);
|
|
441
|
+
index.stampGen = 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function insertSpatialIndexRaw(index, space, level, x, y, value) {
|
|
445
|
+
const s = space & 255;
|
|
446
|
+
const l = level & 255;
|
|
447
|
+
const xx = x >>> 0;
|
|
448
|
+
const yy = y >>> 0;
|
|
449
|
+
let slot = hashKey(s, l, xx, yy) & index.mask;
|
|
450
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
451
|
+
if (index.stamp[slot] !== index.stampGen) {
|
|
452
|
+
index.stamp[slot] = index.stampGen;
|
|
453
|
+
index.keysSpace[slot] = s;
|
|
454
|
+
index.keysLevel[slot] = l;
|
|
455
|
+
index.keysX[slot] = xx;
|
|
456
|
+
index.keysY[slot] = yy;
|
|
457
|
+
index.values[slot] = value >>> 0;
|
|
458
|
+
return;
|
|
428
459
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return
|
|
460
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
461
|
+
index.values[slot] = value >>> 0;
|
|
462
|
+
return;
|
|
432
463
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return triangleScreenSize > targetTrianglePixels;
|
|
437
|
-
};
|
|
464
|
+
slot = slot + 1 & index.mask;
|
|
465
|
+
}
|
|
466
|
+
throw new Error("SpatialIndex is full (no empty slot found).");
|
|
438
467
|
}
|
|
439
|
-
function
|
|
440
|
-
const
|
|
441
|
-
|
|
468
|
+
function lookupSpatialIndexRaw(index, space, level, x, y) {
|
|
469
|
+
const s = space & 255;
|
|
470
|
+
const l = level & 255;
|
|
471
|
+
const xx = x >>> 0;
|
|
472
|
+
const yy = y >>> 0;
|
|
473
|
+
let slot = hashKey(s, l, xx, yy) & index.mask;
|
|
474
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
475
|
+
if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
|
|
476
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
477
|
+
return index.values[slot];
|
|
478
|
+
}
|
|
479
|
+
slot = slot + 1 & index.mask;
|
|
480
|
+
}
|
|
481
|
+
return U32_EMPTY;
|
|
442
482
|
}
|
|
443
483
|
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
nodeCount = 0;
|
|
450
|
-
deepestLevel = 0;
|
|
451
|
-
config;
|
|
452
|
-
nodeView;
|
|
453
|
-
subdivisionStrategy;
|
|
454
|
-
// Pre-allocated buffers to avoid object creation
|
|
455
|
-
tempChildIndices = [-1, -1, -1, -1];
|
|
456
|
-
tempNeighborIndices = [-1, -1, -1, -1];
|
|
457
|
-
/**
|
|
458
|
-
* Create a new Quadtree.
|
|
459
|
-
*
|
|
460
|
-
* @param config Quadtree configuration parameters
|
|
461
|
-
* @param subdivisionStrategy Strategy function for subdivision decisions.
|
|
462
|
-
* Defaults to distanceBasedSubdivision(2).
|
|
463
|
-
* @param nodeView Optional pre-allocated NodeView for buffer reuse
|
|
464
|
-
*/
|
|
465
|
-
constructor(config, subdivisionStrategy, nodeView) {
|
|
466
|
-
this.config = config;
|
|
467
|
-
this.subdivisionStrategy = subdivisionStrategy ?? distanceBasedSubdivision(2);
|
|
468
|
-
this.nodeView = nodeView ?? new QuadtreeNodeView(config.maxNodes);
|
|
469
|
-
this.initialize();
|
|
484
|
+
function buildLeafIndex(leaves, out) {
|
|
485
|
+
const index = out ?? createSpatialIndex(leaves.count);
|
|
486
|
+
resetSpatialIndex(index);
|
|
487
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
488
|
+
insertSpatialIndexRaw(index, leaves.space[i], leaves.level[i], leaves.x[i], leaves.y[i], i);
|
|
470
489
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
490
|
+
return index;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function createState(cfg, surface) {
|
|
494
|
+
const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
|
|
495
|
+
return {
|
|
496
|
+
cfg,
|
|
497
|
+
store,
|
|
498
|
+
leaves: allocLeafSet(cfg.maxNodes),
|
|
499
|
+
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
500
|
+
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
501
|
+
stack: new Uint32Array(cfg.maxNodes),
|
|
502
|
+
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
503
|
+
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
504
|
+
splitGen: 1,
|
|
505
|
+
scratchTile: { space: 0, level: 0, x: 0, y: 0 },
|
|
506
|
+
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
507
|
+
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
508
|
+
spaceCount: surface.spaceCount
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function beginUpdate(state, surface) {
|
|
512
|
+
if (surface.spaceCount !== state.spaceCount) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
|
|
515
|
+
);
|
|
479
516
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
beginFrame(state.store);
|
|
518
|
+
for (let s = 0; s < surface.spaceCount; s++) {
|
|
519
|
+
const rootId = allocNode(state.store, { space: s, level: 0, x: 0, y: 0 });
|
|
520
|
+
if (rootId === U32_EMPTY) {
|
|
521
|
+
throw new Error("Failed to allocate root node (maxNodes too small).");
|
|
522
|
+
}
|
|
523
|
+
state.store.roots[s] = rootId;
|
|
485
524
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function shouldSplit(bounds, level, maxLevel, params) {
|
|
528
|
+
if (level >= maxLevel) return false;
|
|
529
|
+
const mode = params.mode ?? "distance";
|
|
530
|
+
const cx = bounds.cx;
|
|
531
|
+
const cy = bounds.cy;
|
|
532
|
+
const cz = bounds.cz;
|
|
533
|
+
const distSq = cx * cx + cy * cy + cz * cz;
|
|
534
|
+
const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
|
|
535
|
+
if (mode === "screen") {
|
|
536
|
+
const proj = params.projectionFactor ?? 0;
|
|
537
|
+
const target = params.targetPixels ?? 0;
|
|
538
|
+
if (proj <= 0 || target <= 0) {
|
|
539
|
+
const f2 = params.distanceFactor ?? 2;
|
|
540
|
+
const threshold2 = bounds.r * f2;
|
|
541
|
+
return safeDistSq < threshold2 * threshold2;
|
|
542
|
+
}
|
|
543
|
+
const left = bounds.r * bounds.r * proj * proj;
|
|
544
|
+
const right = safeDistSq * target * target;
|
|
545
|
+
return left > right;
|
|
491
546
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
547
|
+
const f = params.distanceFactor ?? 2;
|
|
548
|
+
const threshold = bounds.r * f;
|
|
549
|
+
return safeDistSq < threshold * threshold;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function refineLeaves(state, surface, params, outLeaves) {
|
|
553
|
+
const leaves = outLeaves ?? state.leaves;
|
|
554
|
+
resetLeafSet(leaves);
|
|
555
|
+
const store = state.store;
|
|
556
|
+
const stack = state.stack;
|
|
557
|
+
let sp = 0;
|
|
558
|
+
for (let s = 0; s < surface.spaceCount; s++) {
|
|
559
|
+
stack[sp++] = store.roots[s];
|
|
500
560
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
if (
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
tempBox3.set(tempMin, tempMax);
|
|
522
|
-
if (!frustum.intersectsBox(tempBox3)) {
|
|
523
|
-
this.nodeView.setLeaf(nodeIndex, false);
|
|
524
|
-
return -1;
|
|
525
|
-
}
|
|
561
|
+
while (sp > 0) {
|
|
562
|
+
const nodeId = stack[--sp];
|
|
563
|
+
const level = store.level[nodeId];
|
|
564
|
+
const space = store.space[nodeId];
|
|
565
|
+
const x = store.x[nodeId];
|
|
566
|
+
const y = store.y[nodeId];
|
|
567
|
+
const tile = state.scratchTile;
|
|
568
|
+
tile.space = space;
|
|
569
|
+
tile.level = level;
|
|
570
|
+
tile.x = x;
|
|
571
|
+
tile.y = y;
|
|
572
|
+
const bounds = state.scratchBounds;
|
|
573
|
+
surface.tileBounds(tile, params.cameraOrigin, bounds);
|
|
574
|
+
if (hasChildren(store, nodeId)) {
|
|
575
|
+
const base = store.firstChild[nodeId];
|
|
576
|
+
stack[sp++] = base + 3;
|
|
577
|
+
stack[sp++] = base + 2;
|
|
578
|
+
stack[sp++] = base + 1;
|
|
579
|
+
stack[sp++] = base + 0;
|
|
580
|
+
continue;
|
|
526
581
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const children = this.nodeView.getChildren(nodeIndex);
|
|
537
|
-
let bestLeafIndex = -1;
|
|
538
|
-
let bestDistSq = Number.POSITIVE_INFINITY;
|
|
539
|
-
for (let i = 0; i < 4; i++) {
|
|
540
|
-
if (children[i] !== -1) {
|
|
541
|
-
const leafIdx = this.updateNode(children[i], position, frustum);
|
|
542
|
-
if (leafIdx !== -1) {
|
|
543
|
-
const leafLevel = this.nodeView.getLevel(leafIdx);
|
|
544
|
-
const size = this.config.rootSize / (1 << leafLevel);
|
|
545
|
-
const x = this.nodeView.getX(leafIdx);
|
|
546
|
-
const y = this.nodeView.getY(leafIdx);
|
|
547
|
-
const cx = this.config.origin.x + ((x + 0.5) * size - 0.5 * this.config.rootSize);
|
|
548
|
-
const cz = this.config.origin.z + ((y + 0.5) * size - 0.5 * this.config.rootSize);
|
|
549
|
-
const dx = position.x - cx;
|
|
550
|
-
const dz = position.z - cz;
|
|
551
|
-
const d2 = dx * dx + dz * dz;
|
|
552
|
-
if (d2 < bestDistSq) {
|
|
553
|
-
bestDistSq = d2;
|
|
554
|
-
bestLeafIndex = leafIdx;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
582
|
+
const split = shouldSplit(bounds, level, state.cfg.maxLevel, params);
|
|
583
|
+
if (split) {
|
|
584
|
+
const base = ensureChildren(store, nodeId);
|
|
585
|
+
if (base !== U32_EMPTY) {
|
|
586
|
+
stack[sp++] = base + 3;
|
|
587
|
+
stack[sp++] = base + 2;
|
|
588
|
+
stack[sp++] = base + 1;
|
|
589
|
+
stack[sp++] = base + 0;
|
|
590
|
+
continue;
|
|
558
591
|
}
|
|
559
|
-
this.nodeView.setLeaf(nodeIndex, false);
|
|
560
|
-
return bestLeafIndex;
|
|
561
592
|
}
|
|
562
|
-
|
|
563
|
-
|
|
593
|
+
const i = leaves.count;
|
|
594
|
+
if (i >= leaves.capacity) {
|
|
595
|
+
throw new Error("LeafSet capacity exceeded.");
|
|
596
|
+
}
|
|
597
|
+
leaves.space[i] = space;
|
|
598
|
+
leaves.level[i] = level;
|
|
599
|
+
leaves.x[i] = x;
|
|
600
|
+
leaves.y[i] = y;
|
|
601
|
+
state.leafNodeIds[i] = nodeId;
|
|
602
|
+
leaves.count = i + 1;
|
|
564
603
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
minNodeSize: this.config.minNodeSize,
|
|
574
|
-
rootSize: this.config.rootSize
|
|
575
|
-
};
|
|
576
|
-
return this.subdivisionStrategy(context);
|
|
604
|
+
return leaves;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function resetSplitMarks(state) {
|
|
608
|
+
state.splitGen = state.splitGen + 1 & 65535;
|
|
609
|
+
if (state.splitGen === 0) {
|
|
610
|
+
state.splitStamp.fill(0);
|
|
611
|
+
state.splitGen = 1;
|
|
577
612
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
613
|
+
}
|
|
614
|
+
function scheduleSplit(state, nodeId, count) {
|
|
615
|
+
if (nodeId === U32_EMPTY) return count;
|
|
616
|
+
if (state.splitStamp[nodeId] === state.splitGen) return count;
|
|
617
|
+
state.splitStamp[nodeId] = state.splitGen;
|
|
618
|
+
state.splitQueue[count] = nodeId;
|
|
619
|
+
return count + 1;
|
|
620
|
+
}
|
|
621
|
+
function balance2to1(state, surface, params, leaves) {
|
|
622
|
+
const maxIters = state.cfg.maxLevel + 1;
|
|
623
|
+
for (let iter = 0; iter < maxIters; iter++) {
|
|
624
|
+
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
625
|
+
resetSplitMarks(state);
|
|
626
|
+
let splitCount = 0;
|
|
627
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
628
|
+
const leafLevel = leaves.level[i];
|
|
629
|
+
if (leafLevel < 2) continue;
|
|
630
|
+
const leafSpace = leaves.space[i];
|
|
631
|
+
const leafX = leaves.x[i];
|
|
632
|
+
const leafY = leaves.y[i];
|
|
633
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
634
|
+
for (let candidateLevel = leafLevel - 2; candidateLevel >= 0; candidateLevel--) {
|
|
635
|
+
const shift = leafLevel - candidateLevel;
|
|
636
|
+
const tile = state.scratchTile;
|
|
637
|
+
tile.space = leafSpace;
|
|
638
|
+
tile.level = candidateLevel;
|
|
639
|
+
tile.x = leafX >>> shift;
|
|
640
|
+
tile.y = leafY >>> shift;
|
|
641
|
+
const neighbor = state.scratchNeighbor;
|
|
642
|
+
if (!surface.neighborSameLevel(tile, dir, neighbor)) break;
|
|
643
|
+
const j = lookupSpatialIndexRaw(
|
|
644
|
+
index,
|
|
645
|
+
neighbor.space,
|
|
646
|
+
neighbor.level,
|
|
647
|
+
neighbor.x,
|
|
648
|
+
neighbor.y
|
|
649
|
+
);
|
|
650
|
+
if (j !== U32_EMPTY) {
|
|
651
|
+
splitCount = scheduleSplit(state, state.leafNodeIds[j], splitCount);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
585
656
|
}
|
|
586
|
-
if (
|
|
587
|
-
|
|
657
|
+
if (splitCount === 0) return leaves;
|
|
658
|
+
let anySplit = false;
|
|
659
|
+
for (let k = 0; k < splitCount; k++) {
|
|
660
|
+
const nodeId = state.splitQueue[k];
|
|
661
|
+
if (state.store.level[nodeId] >= state.cfg.maxLevel) continue;
|
|
662
|
+
const base = ensureChildren(state.store, nodeId);
|
|
663
|
+
if (base !== U32_EMPTY) anySplit = true;
|
|
588
664
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this.tempChildIndices[2] = EMPTY_SENTINEL_VALUE;
|
|
592
|
-
this.tempChildIndices[3] = EMPTY_SENTINEL_VALUE;
|
|
593
|
-
this.tempNeighborIndices[0] = EMPTY_SENTINEL_VALUE;
|
|
594
|
-
this.tempNeighborIndices[1] = EMPTY_SENTINEL_VALUE;
|
|
595
|
-
this.tempNeighborIndices[2] = EMPTY_SENTINEL_VALUE;
|
|
596
|
-
this.tempNeighborIndices[3] = EMPTY_SENTINEL_VALUE;
|
|
597
|
-
const nodeIndex = this.nodeCount++;
|
|
598
|
-
this.nodeView.setLevel(nodeIndex, level);
|
|
599
|
-
this.nodeView.setX(nodeIndex, x);
|
|
600
|
-
this.nodeView.setY(nodeIndex, y);
|
|
601
|
-
this.nodeView.setChildren(nodeIndex, this.tempChildIndices);
|
|
602
|
-
this.nodeView.setNeighbors(nodeIndex, this.tempNeighborIndices);
|
|
603
|
-
this.nodeView.setLeaf(nodeIndex, false);
|
|
604
|
-
return nodeIndex;
|
|
665
|
+
if (!anySplit) return leaves;
|
|
666
|
+
refineLeaves(state, surface, params, leaves);
|
|
605
667
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
];
|
|
623
|
-
if (childIndices.some((index) => index === -1)) {
|
|
624
|
-
console.warn("Failed to create all children, skipping subdivision");
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
this.nodeView.setChildren(nodeIndex, childIndices);
|
|
628
|
-
this.updateChildNeighbors(nodeIndex, childIndices);
|
|
668
|
+
return leaves;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function update(state, surface, params, outLeaves) {
|
|
672
|
+
beginUpdate(state, surface);
|
|
673
|
+
const leaves = refineLeaves(state, surface, params, outLeaves);
|
|
674
|
+
return balance2to1(state, surface, params, leaves);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
678
|
+
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
679
|
+
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
680
|
+
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
681
|
+
function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
682
|
+
if (outSeams.capacity < leaves.count) {
|
|
683
|
+
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
629
684
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
685
|
+
const index = buildLeafIndex(leaves, outIndex);
|
|
686
|
+
outSeams.count = leaves.count;
|
|
687
|
+
const neighbors = outSeams.neighbors;
|
|
688
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
689
|
+
const base = i * 8;
|
|
690
|
+
const space = leaves.space[i];
|
|
691
|
+
const level = leaves.level[i];
|
|
692
|
+
const x = leaves.x[i];
|
|
693
|
+
const y = leaves.y[i];
|
|
694
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
695
|
+
const outOffset = base + dir * 2;
|
|
696
|
+
neighbors[outOffset + 0] = U32_EMPTY;
|
|
697
|
+
neighbors[outOffset + 1] = U32_EMPTY;
|
|
698
|
+
scratchTile.space = space;
|
|
699
|
+
scratchTile.level = level;
|
|
700
|
+
scratchTile.x = x;
|
|
701
|
+
scratchTile.y = y;
|
|
702
|
+
if (!surface.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
703
|
+
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
704
|
+
if (j !== U32_EMPTY) {
|
|
705
|
+
neighbors[outOffset + 0] = j;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (level > 0) {
|
|
709
|
+
const px = x >>> 1;
|
|
710
|
+
const py = y >>> 1;
|
|
711
|
+
scratchParentTile.space = space;
|
|
712
|
+
scratchParentTile.level = level - 1;
|
|
713
|
+
scratchParentTile.x = px;
|
|
714
|
+
scratchParentTile.y = py;
|
|
715
|
+
if (surface.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
716
|
+
j = lookupSpatialIndexRaw(
|
|
717
|
+
index,
|
|
718
|
+
scratchParentNbr.space,
|
|
719
|
+
scratchParentNbr.level,
|
|
720
|
+
scratchParentNbr.x,
|
|
721
|
+
scratchParentNbr.y
|
|
722
|
+
);
|
|
723
|
+
if (j !== U32_EMPTY) {
|
|
724
|
+
neighbors[outOffset + 0] = j;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
646
728
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
729
|
+
const childLevel = scratchNbr.level + 1;
|
|
730
|
+
const x2 = scratchNbr.x << 1 >>> 0;
|
|
731
|
+
const y2 = scratchNbr.y << 1 >>> 0;
|
|
732
|
+
let ax = 0;
|
|
733
|
+
let ay = 0;
|
|
734
|
+
let bx = 0;
|
|
735
|
+
let by = 0;
|
|
736
|
+
switch (dir) {
|
|
737
|
+
case Dir.LEFT:
|
|
738
|
+
ax = x2 + 1;
|
|
739
|
+
ay = y2;
|
|
740
|
+
bx = x2 + 1;
|
|
741
|
+
by = y2 + 1;
|
|
742
|
+
break;
|
|
743
|
+
case Dir.RIGHT:
|
|
744
|
+
ax = x2;
|
|
745
|
+
ay = y2;
|
|
746
|
+
bx = x2;
|
|
747
|
+
by = y2 + 1;
|
|
748
|
+
break;
|
|
749
|
+
case Dir.TOP:
|
|
750
|
+
ax = x2;
|
|
751
|
+
ay = y2 + 1;
|
|
752
|
+
bx = x2 + 1;
|
|
753
|
+
by = y2 + 1;
|
|
754
|
+
break;
|
|
755
|
+
case Dir.BOTTOM:
|
|
756
|
+
ax = x2;
|
|
757
|
+
ay = y2;
|
|
758
|
+
bx = x2 + 1;
|
|
759
|
+
by = y2;
|
|
760
|
+
break;
|
|
651
761
|
}
|
|
652
|
-
|
|
762
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, ax, ay);
|
|
763
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 0] = j;
|
|
764
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, bx, by);
|
|
765
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
|
|
653
766
|
}
|
|
654
767
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Get all leaf nodes as an array of node objects
|
|
684
|
-
*/
|
|
685
|
-
getLeafNodes() {
|
|
686
|
-
const leafNodes = [];
|
|
687
|
-
for (let i = 0; i < this.nodeCount; i++) {
|
|
688
|
-
if (this.nodeView.getLeaf(i)) {
|
|
689
|
-
leafNodes.push({
|
|
690
|
-
level: this.nodeView.getLevel(i),
|
|
691
|
-
x: this.nodeView.getX(i),
|
|
692
|
-
y: this.nodeView.getY(i)
|
|
693
|
-
});
|
|
768
|
+
return outSeams;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function createFlatSurface(cfg) {
|
|
772
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
773
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
774
|
+
const surface = {
|
|
775
|
+
spaceCount: 1,
|
|
776
|
+
neighborSameLevel(tile, dir, out) {
|
|
777
|
+
const level = tile.level;
|
|
778
|
+
const x = tile.x;
|
|
779
|
+
const y = tile.y;
|
|
780
|
+
let nx = x;
|
|
781
|
+
let ny = y;
|
|
782
|
+
switch (dir) {
|
|
783
|
+
case Dir.LEFT:
|
|
784
|
+
nx = x - 1;
|
|
785
|
+
break;
|
|
786
|
+
case Dir.RIGHT:
|
|
787
|
+
nx = x + 1;
|
|
788
|
+
break;
|
|
789
|
+
case Dir.TOP:
|
|
790
|
+
ny = y - 1;
|
|
791
|
+
break;
|
|
792
|
+
case Dir.BOTTOM:
|
|
793
|
+
ny = y + 1;
|
|
794
|
+
break;
|
|
694
795
|
}
|
|
796
|
+
if (nx < 0 || ny < 0) return false;
|
|
797
|
+
const maxCoord = (1 << level) - 1;
|
|
798
|
+
if (nx > maxCoord || ny > maxCoord) return false;
|
|
799
|
+
out.space = 0;
|
|
800
|
+
out.level = level;
|
|
801
|
+
out.x = nx >>> 0;
|
|
802
|
+
out.y = ny >>> 0;
|
|
803
|
+
return true;
|
|
804
|
+
},
|
|
805
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
806
|
+
const level = tile.level;
|
|
807
|
+
const scale = 1 / (1 << level);
|
|
808
|
+
const size = cfg.rootSize * scale;
|
|
809
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
810
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
811
|
+
const centerX = minX + 0.5 * size;
|
|
812
|
+
const centerY = cfg.origin.y;
|
|
813
|
+
const centerZ = minZ + 0.5 * size;
|
|
814
|
+
out.cx = centerX - cameraOrigin.x;
|
|
815
|
+
out.cy = centerY - cameraOrigin.y;
|
|
816
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
817
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
695
818
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
* Release internal resources associated with this quadtree
|
|
712
|
-
*/
|
|
713
|
-
destroy() {
|
|
714
|
-
this.nodeView.destroy();
|
|
715
|
-
this.nodeCount = 0;
|
|
716
|
-
this.deepestLevel = 0;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Set the configuration
|
|
720
|
-
*/
|
|
721
|
-
setConfig(config, reset = false) {
|
|
722
|
-
this.config = config;
|
|
723
|
-
if (reset) {
|
|
724
|
-
this.initialize();
|
|
819
|
+
};
|
|
820
|
+
return surface;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function createCubeSphereSurface(_cfg) {
|
|
824
|
+
return {
|
|
825
|
+
spaceCount: 6,
|
|
826
|
+
neighborSameLevel(_tile, _dir, _out) {
|
|
827
|
+
return false;
|
|
828
|
+
},
|
|
829
|
+
tileBounds(_tile, _cameraOrigin, out) {
|
|
830
|
+
out.cx = 0;
|
|
831
|
+
out.cy = 0;
|
|
832
|
+
out.cz = 0;
|
|
833
|
+
out.r = Number.MAX_VALUE;
|
|
725
834
|
}
|
|
726
|
-
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("terrainInstanceIdTask").cache("once");
|
|
839
|
+
|
|
840
|
+
const rootSize = work.param(256).displayName("rootSize");
|
|
841
|
+
const origin = work.param({ x: 0, y: 0, z: 0 }).displayName(
|
|
842
|
+
"origin"
|
|
843
|
+
);
|
|
844
|
+
const innerTileSegments = work.param(14).displayName("innerTileSegments");
|
|
845
|
+
const skirtScale = work.param(100).displayName("skirtScale");
|
|
846
|
+
const heightmapScale = work.param(1).displayName("heightmapScale");
|
|
847
|
+
const maxNodes = work.param(1028).displayName("maxNodes");
|
|
848
|
+
const maxLevel = work.param(16).displayName("maxLevel");
|
|
849
|
+
const quadtreeUpdate = work.param({
|
|
850
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
851
|
+
mode: "distance",
|
|
852
|
+
distanceFactor: 1.5
|
|
853
|
+
}).displayName("quadtreeUpdate");
|
|
854
|
+
|
|
855
|
+
const quadtreeConfigTask = work.task((get, work) => {
|
|
856
|
+
const rootSizeVal = get(rootSize);
|
|
857
|
+
const originVal = get(origin);
|
|
858
|
+
const maxNodesVal = get(maxNodes);
|
|
859
|
+
const maxLevelVal = get(maxLevel);
|
|
860
|
+
return work(() => {
|
|
861
|
+
const surface = createFlatSurface({
|
|
862
|
+
rootSize: rootSizeVal,
|
|
863
|
+
origin: originVal
|
|
864
|
+
});
|
|
865
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surface);
|
|
866
|
+
return {
|
|
867
|
+
state,
|
|
868
|
+
surface
|
|
869
|
+
};
|
|
870
|
+
});
|
|
871
|
+
}).displayName("quadtreeConfigTask");
|
|
872
|
+
const quadtreeUpdateTask = work.task((get, work) => {
|
|
873
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
874
|
+
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
875
|
+
let outLeaves = void 0;
|
|
876
|
+
return work(() => {
|
|
877
|
+
outLeaves = update(
|
|
878
|
+
quadtreeConfig.state,
|
|
879
|
+
quadtreeConfig.surface,
|
|
880
|
+
quadtreeUpdateConfig,
|
|
881
|
+
outLeaves
|
|
882
|
+
);
|
|
883
|
+
return outLeaves;
|
|
884
|
+
});
|
|
885
|
+
}).displayName("quadtreeUpdateTask");
|
|
886
|
+
const leafStorageTask = work.task((get, work) => {
|
|
887
|
+
const maxNodesVal = get(maxNodes);
|
|
888
|
+
return work(() => {
|
|
889
|
+
const data = new Int32Array(maxNodesVal * 4);
|
|
890
|
+
const attribute = new webgpu.StorageBufferAttribute(data, 4);
|
|
891
|
+
const node = tsl.storage(attribute, "i32", 1).toReadOnly();
|
|
892
|
+
return { data, attribute, node };
|
|
893
|
+
});
|
|
894
|
+
}).displayName("leafStorageTask");
|
|
895
|
+
const leafGpuBufferTask = work.task((get, work) => {
|
|
896
|
+
const leafSet = get(quadtreeUpdateTask);
|
|
897
|
+
const leafStorage = get(leafStorageTask);
|
|
898
|
+
return work(() => {
|
|
899
|
+
const bufferCapacity = leafStorage.data.length / 4;
|
|
900
|
+
const leafCount = Math.min(leafSet.count, bufferCapacity);
|
|
901
|
+
for (let i = 0; i < leafCount; i += 1) {
|
|
902
|
+
const offset = i * 4;
|
|
903
|
+
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
904
|
+
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
905
|
+
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
906
|
+
leafStorage.data[offset + 3] = 1;
|
|
907
|
+
}
|
|
908
|
+
leafStorage.attribute.needsUpdate = true;
|
|
909
|
+
leafStorage.node.needsUpdate = true;
|
|
910
|
+
return {
|
|
911
|
+
count: leafCount,
|
|
912
|
+
data: leafStorage.data,
|
|
913
|
+
attribute: leafStorage.attribute,
|
|
914
|
+
node: leafStorage.node
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
}).displayName("leafGpuBufferTask");
|
|
918
|
+
|
|
919
|
+
function createTerrainUniforms(params) {
|
|
920
|
+
const sanitizedId = params.instanceId?.replace(/-/g, "_");
|
|
921
|
+
const suffix = sanitizedId ? `_${sanitizedId}` : "";
|
|
922
|
+
const uRootOrigin = tsl.uniform(
|
|
923
|
+
new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
|
|
924
|
+
).setName(`uRootOrigin${suffix}`);
|
|
925
|
+
const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
|
|
926
|
+
const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
|
|
927
|
+
`uInnerTileSegments${suffix}`
|
|
928
|
+
);
|
|
929
|
+
const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
930
|
+
const uHeightmapScale = tsl.uniform(tsl.float(params.heightmapScale)).setName(`uHeightmapScale${suffix}`);
|
|
931
|
+
return {
|
|
932
|
+
uRootOrigin,
|
|
933
|
+
uRootSize,
|
|
934
|
+
uInnerTileSegments,
|
|
935
|
+
uSkirtScale,
|
|
936
|
+
uHeightmapScale
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const scratchVector3 = new three.Vector3();
|
|
941
|
+
const createUniformsTask = work.task((get, work) => {
|
|
942
|
+
const uniformParams = {
|
|
943
|
+
rootOrigin: get(origin),
|
|
944
|
+
rootSize: get(rootSize),
|
|
945
|
+
innerTileSegments: get(innerTileSegments),
|
|
946
|
+
skirtScale: get(skirtScale),
|
|
947
|
+
heightmapScale: get(heightmapScale),
|
|
948
|
+
instanceId: get(instanceIdTask)
|
|
949
|
+
};
|
|
950
|
+
return work(() => createTerrainUniforms(uniformParams));
|
|
951
|
+
}).displayName("createTerrainUniformsTask").cache("once");
|
|
952
|
+
const updateUniformsTask = work.task((get, work) => {
|
|
953
|
+
const terrainUniformsContext = get(createUniformsTask);
|
|
954
|
+
const rootSizeVal = get(rootSize);
|
|
955
|
+
const rootOrigin = get(origin);
|
|
956
|
+
const innerTileSegmentsVal = get(innerTileSegments);
|
|
957
|
+
const skirtScaleVal = get(skirtScale);
|
|
958
|
+
const heightmapScaleVal = get(heightmapScale);
|
|
959
|
+
return work(() => {
|
|
960
|
+
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
961
|
+
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
962
|
+
rootOrigin.x,
|
|
963
|
+
rootOrigin.y,
|
|
964
|
+
rootOrigin.z
|
|
965
|
+
);
|
|
966
|
+
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
967
|
+
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
968
|
+
terrainUniformsContext.uHeightmapScale.value = heightmapScaleVal;
|
|
969
|
+
return terrainUniformsContext;
|
|
970
|
+
});
|
|
971
|
+
}).displayName("updateTerrainUniformsTask");
|
|
972
|
+
|
|
973
|
+
const positionNodeTask = work.task((get, work) => {
|
|
974
|
+
const leafStorage = get(leafStorageTask);
|
|
975
|
+
const terrainUniforms = get(createUniformsTask);
|
|
976
|
+
return work(() => createTileWorldPosition(leafStorage, terrainUniforms));
|
|
977
|
+
}).displayName("terrainVertextPositionNodeTask");
|
|
978
|
+
|
|
979
|
+
function terrainGraph() {
|
|
980
|
+
return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(leafGpuBufferTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask);
|
|
727
981
|
}
|
|
982
|
+
const terrainTasks = {
|
|
983
|
+
instanceId: instanceIdTask,
|
|
984
|
+
quadtreeConfig: quadtreeConfigTask,
|
|
985
|
+
quadtreeUpdate: quadtreeUpdateTask,
|
|
986
|
+
leafStorage: leafStorageTask,
|
|
987
|
+
leafGpuBuffer: leafGpuBufferTask,
|
|
988
|
+
createUniforms: createUniformsTask,
|
|
989
|
+
updateUniforms: updateUniformsTask,
|
|
990
|
+
positionNode: positionNodeTask
|
|
991
|
+
};
|
|
728
992
|
|
|
729
|
-
exports.
|
|
993
|
+
exports.Dir = Dir;
|
|
730
994
|
exports.TerrainGeometry = TerrainGeometry;
|
|
731
|
-
exports.
|
|
732
|
-
exports.
|
|
995
|
+
exports.TerrainMesh = TerrainMesh;
|
|
996
|
+
exports.U32_EMPTY = U32_EMPTY;
|
|
997
|
+
exports.allocLeafSet = allocLeafSet;
|
|
998
|
+
exports.allocSeamTable = allocSeamTable;
|
|
999
|
+
exports.beginUpdate = beginUpdate;
|
|
1000
|
+
exports.blendAngleCorrectedNormals = blendAngleCorrectedNormals;
|
|
1001
|
+
exports.buildLeafIndex = buildLeafIndex;
|
|
1002
|
+
exports.buildSeams2to1 = buildSeams2to1;
|
|
1003
|
+
exports.createCubeSphereSurface = createCubeSphereSurface;
|
|
1004
|
+
exports.createFlatSurface = createFlatSurface;
|
|
1005
|
+
exports.createSpatialIndex = createSpatialIndex;
|
|
1006
|
+
exports.createState = createState;
|
|
1007
|
+
exports.createTerrainUniforms = createTerrainUniforms;
|
|
1008
|
+
exports.createTileWorldPosition = createTileWorldPosition;
|
|
1009
|
+
exports.createUniformsTask = createUniformsTask;
|
|
1010
|
+
exports.deriveNormalZ = deriveNormalZ;
|
|
1011
|
+
exports.heightmapScale = heightmapScale;
|
|
1012
|
+
exports.innerTileSegments = innerTileSegments;
|
|
1013
|
+
exports.instanceIdTask = instanceIdTask;
|
|
733
1014
|
exports.isSkirtUV = isSkirtUV;
|
|
734
1015
|
exports.isSkirtVertex = isSkirtVertex;
|
|
735
|
-
exports.
|
|
1016
|
+
exports.leafGpuBufferTask = leafGpuBufferTask;
|
|
1017
|
+
exports.leafStorageTask = leafStorageTask;
|
|
1018
|
+
exports.maxLevel = maxLevel;
|
|
1019
|
+
exports.maxNodes = maxNodes;
|
|
1020
|
+
exports.origin = origin;
|
|
1021
|
+
exports.positionNodeTask = positionNodeTask;
|
|
1022
|
+
exports.quadtreeConfigTask = quadtreeConfigTask;
|
|
1023
|
+
exports.quadtreeUpdate = quadtreeUpdate;
|
|
1024
|
+
exports.quadtreeUpdateTask = quadtreeUpdateTask;
|
|
1025
|
+
exports.resetLeafSet = resetLeafSet;
|
|
1026
|
+
exports.resetSeamTable = resetSeamTable;
|
|
1027
|
+
exports.rootSize = rootSize;
|
|
1028
|
+
exports.skirtScale = skirtScale;
|
|
1029
|
+
exports.terrainGraph = terrainGraph;
|
|
1030
|
+
exports.terrainTasks = terrainTasks;
|
|
1031
|
+
exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
|
|
1032
|
+
exports.update = update;
|
|
1033
|
+
exports.updateUniformsTask = updateUniformsTask;
|
|
1034
|
+
exports.vectorSpaceToTextureSpace = vectorSpaceToTextureSpace;
|