@hello-terrain/three 0.0.0-alpha.1 → 0.0.0-alpha.2
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 +500 -5
- package/dist/index.d.cts +259 -1
- package/dist/index.d.mts +259 -1
- package/dist/index.d.ts +259 -1
- package/dist/index.mjs +479 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const THREE = require('three');
|
|
4
4
|
const tsl = require('three/tsl');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function _interopNamespaceCompat(e) {
|
|
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 {
|
|
7
21
|
constructor(innerSegments = 14, extendUV = false) {
|
|
8
22
|
super();
|
|
9
23
|
if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
|
|
@@ -15,21 +29,21 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
15
29
|
this.setIndex(this.generateIndices(innerSegments));
|
|
16
30
|
this.setAttribute(
|
|
17
31
|
"position",
|
|
18
|
-
new
|
|
32
|
+
new THREE.BufferAttribute(
|
|
19
33
|
new Float32Array(this.generatePositions(innerSegments)),
|
|
20
34
|
3
|
|
21
35
|
)
|
|
22
36
|
);
|
|
23
37
|
this.setAttribute(
|
|
24
38
|
"normal",
|
|
25
|
-
new
|
|
39
|
+
new THREE.BufferAttribute(
|
|
26
40
|
new Float32Array(this.generateNormals(innerSegments)),
|
|
27
41
|
3
|
|
28
42
|
)
|
|
29
43
|
);
|
|
30
44
|
this.setAttribute(
|
|
31
45
|
"uv",
|
|
32
|
-
new
|
|
46
|
+
new THREE.BufferAttribute(
|
|
33
47
|
new Float32Array(
|
|
34
48
|
extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
|
|
35
49
|
),
|
|
@@ -235,6 +249,487 @@ const isSkirtUV = tsl.Fn(([segments]) => {
|
|
|
235
249
|
return innerX.and(innerY).not();
|
|
236
250
|
});
|
|
237
251
|
|
|
252
|
+
const CHILDREN_STRIDE = 4;
|
|
253
|
+
const NEIGHBORS_STRIDE = 4;
|
|
254
|
+
const NODE_STRIDE = 4;
|
|
255
|
+
const U_INT_16_MAX_VALUE = 65535;
|
|
256
|
+
const EMPTY_SENTINEL_VALUE = U_INT_16_MAX_VALUE;
|
|
257
|
+
class QuadtreeNodeView {
|
|
258
|
+
maxNodeCount;
|
|
259
|
+
childrenIndicesBuffer;
|
|
260
|
+
neighborsIndicesBuffer;
|
|
261
|
+
nodeBuffer;
|
|
262
|
+
leafNodeMask;
|
|
263
|
+
leafNodeCountBuffer;
|
|
264
|
+
activeLeafIndices;
|
|
265
|
+
activeLeafCount = 0;
|
|
266
|
+
constructor(maxNodeCount, childrenIndicesBuffer, neighborsIndicesBuffer, nodeBuffer, leafNodeMask, leafNodeCountBuffer) {
|
|
267
|
+
this.maxNodeCount = maxNodeCount;
|
|
268
|
+
this.childrenIndicesBuffer = childrenIndicesBuffer ?? new Uint16Array(CHILDREN_STRIDE * maxNodeCount);
|
|
269
|
+
this.neighborsIndicesBuffer = neighborsIndicesBuffer ?? new Uint16Array(NEIGHBORS_STRIDE * maxNodeCount);
|
|
270
|
+
this.nodeBuffer = nodeBuffer ?? new Int32Array(NODE_STRIDE * maxNodeCount);
|
|
271
|
+
this.leafNodeMask = leafNodeMask ?? new Uint8Array(maxNodeCount);
|
|
272
|
+
this.leafNodeCountBuffer = leafNodeCountBuffer ?? new Uint16Array(1);
|
|
273
|
+
this.activeLeafIndices = new Uint16Array(maxNodeCount);
|
|
274
|
+
this.clear();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Clear all buffers
|
|
278
|
+
*/
|
|
279
|
+
clear() {
|
|
280
|
+
this.nodeBuffer.fill(0);
|
|
281
|
+
this.childrenIndicesBuffer.fill(EMPTY_SENTINEL_VALUE);
|
|
282
|
+
this.neighborsIndicesBuffer.fill(EMPTY_SENTINEL_VALUE);
|
|
283
|
+
this.leafNodeMask.fill(0);
|
|
284
|
+
this.leafNodeCountBuffer[0] = 0;
|
|
285
|
+
this.activeLeafCount = 0;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get buffer references for direct access (useful for GPU operations)
|
|
289
|
+
*/
|
|
290
|
+
getBuffers() {
|
|
291
|
+
return {
|
|
292
|
+
childrenIndicesBuffer: this.childrenIndicesBuffer,
|
|
293
|
+
neighborsIndicesBuffer: this.neighborsIndicesBuffer,
|
|
294
|
+
nodeBuffer: this.nodeBuffer,
|
|
295
|
+
leafNodeMask: this.leafNodeMask
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get the maximum node count
|
|
300
|
+
*/
|
|
301
|
+
getMaxNodeCount() {
|
|
302
|
+
return this.maxNodeCount;
|
|
303
|
+
}
|
|
304
|
+
// Getters for individual buffer values
|
|
305
|
+
getLevel(index) {
|
|
306
|
+
return this.nodeBuffer[index * NODE_STRIDE];
|
|
307
|
+
}
|
|
308
|
+
getX(index) {
|
|
309
|
+
return this.nodeBuffer[index * NODE_STRIDE + 1];
|
|
310
|
+
}
|
|
311
|
+
getY(index) {
|
|
312
|
+
return this.nodeBuffer[index * NODE_STRIDE + 2];
|
|
313
|
+
}
|
|
314
|
+
getLeafNodeCount() {
|
|
315
|
+
return this.leafNodeCountBuffer[0];
|
|
316
|
+
}
|
|
317
|
+
getLeaf(index) {
|
|
318
|
+
return this.leafNodeMask[index] === 1;
|
|
319
|
+
}
|
|
320
|
+
getChildren(index) {
|
|
321
|
+
const offset = index * CHILDREN_STRIDE;
|
|
322
|
+
return [
|
|
323
|
+
this.childrenIndicesBuffer[offset],
|
|
324
|
+
this.childrenIndicesBuffer[offset + 1],
|
|
325
|
+
this.childrenIndicesBuffer[offset + 2],
|
|
326
|
+
this.childrenIndicesBuffer[offset + 3]
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
getNeighbors(index) {
|
|
330
|
+
const offset = index * NEIGHBORS_STRIDE;
|
|
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
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function distanceBasedSubdivision(factor = 2) {
|
|
415
|
+
return (ctx) => {
|
|
416
|
+
if (ctx.nodeSize <= ctx.minNodeSize) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
return ctx.distance < ctx.nodeSize * factor;
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function screenSpaceSubdivision(options) {
|
|
423
|
+
const targetTrianglePixels = options.targetTrianglePixels ?? 6;
|
|
424
|
+
const tileSegments = options.tileSegments ?? 13;
|
|
425
|
+
return (ctx) => {
|
|
426
|
+
if (ctx.nodeSize <= ctx.minNodeSize) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
const screenInfo = options.getScreenSpaceInfo();
|
|
430
|
+
if (!screenInfo) {
|
|
431
|
+
return ctx.distance < ctx.nodeSize * 2;
|
|
432
|
+
}
|
|
433
|
+
const safeDistance = Math.max(ctx.distance, 1e-3);
|
|
434
|
+
const tileScreenSize = ctx.nodeSize / safeDistance * screenInfo.projectionFactor;
|
|
435
|
+
const triangleScreenSize = tileScreenSize / tileSegments;
|
|
436
|
+
return triangleScreenSize > targetTrianglePixels;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function computeScreenSpaceInfo(fovY, screenHeight) {
|
|
440
|
+
const projectionFactor = screenHeight / (2 * Math.tan(fovY / 2));
|
|
441
|
+
return { projectionFactor, screenHeight };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const tempVector3 = new THREE__namespace.Vector3();
|
|
445
|
+
const tempBox3 = new THREE__namespace.Box3();
|
|
446
|
+
const tempMin = new THREE__namespace.Vector3();
|
|
447
|
+
const tempMax = new THREE__namespace.Vector3();
|
|
448
|
+
class Quadtree {
|
|
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();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Set the subdivision strategy.
|
|
473
|
+
* Use this to change LOD behavior at runtime.
|
|
474
|
+
*
|
|
475
|
+
* @param strategy The subdivision strategy function
|
|
476
|
+
*/
|
|
477
|
+
setSubdivisionStrategy(strategy) {
|
|
478
|
+
this.subdivisionStrategy = strategy;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get the current subdivision strategy
|
|
482
|
+
*/
|
|
483
|
+
getSubdivisionStrategy() {
|
|
484
|
+
return this.subdivisionStrategy;
|
|
485
|
+
}
|
|
486
|
+
initialize() {
|
|
487
|
+
this.nodeView.clear();
|
|
488
|
+
this.nodeCount = 0;
|
|
489
|
+
this.deepestLevel = 0;
|
|
490
|
+
this.createNode(0, 0, 0);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Update the quadtree based on the given position and return the index
|
|
494
|
+
* of the leaf node that best corresponds to the position (closest leaf).
|
|
495
|
+
*/
|
|
496
|
+
update(position, frustum) {
|
|
497
|
+
this.reset();
|
|
498
|
+
const closestLeafIndex = this.updateNode(0, position, frustum);
|
|
499
|
+
return closestLeafIndex;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Recursively update a node and its children based on distance and size criteria
|
|
503
|
+
* and return the closest leaf node index to the provided position.
|
|
504
|
+
*/
|
|
505
|
+
updateNode(nodeIndex, position, frustum) {
|
|
506
|
+
const level = this.nodeView.getLevel(nodeIndex);
|
|
507
|
+
const nodeSize = this.config.rootSize / (1 << level);
|
|
508
|
+
const nodeX = this.nodeView.getX(nodeIndex);
|
|
509
|
+
const nodeY = this.nodeView.getY(nodeIndex);
|
|
510
|
+
const minX = this.config.origin.x + (nodeX * nodeSize - 0.5 * this.config.rootSize);
|
|
511
|
+
const minZ = this.config.origin.z + (nodeY * nodeSize - 0.5 * this.config.rootSize);
|
|
512
|
+
const worldX = minX + 0.5 * nodeSize;
|
|
513
|
+
const worldZ = minZ + 0.5 * nodeSize;
|
|
514
|
+
if (frustum) {
|
|
515
|
+
const altitude = Math.abs(position.y - this.config.origin.y);
|
|
516
|
+
const verticalHalfExtent = this.config.rootSize + altitude;
|
|
517
|
+
const minY = this.config.origin.y - verticalHalfExtent;
|
|
518
|
+
const maxY = this.config.origin.y + verticalHalfExtent;
|
|
519
|
+
tempMin.set(minX, minY, minZ);
|
|
520
|
+
tempMax.set(minX + nodeSize, maxY, minZ + nodeSize);
|
|
521
|
+
tempBox3.set(tempMin, tempMax);
|
|
522
|
+
if (!frustum.intersectsBox(tempBox3)) {
|
|
523
|
+
this.nodeView.setLeaf(nodeIndex, false);
|
|
524
|
+
return -1;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
tempVector3.set(worldX, this.config.origin.y, worldZ);
|
|
528
|
+
const distance = position.distanceTo(tempVector3);
|
|
529
|
+
const shouldSubdivide = this.shouldSubdivide(level, distance, nodeSize);
|
|
530
|
+
if (shouldSubdivide && level < this.config.maxLevel) {
|
|
531
|
+
if (this.nodeCount + 4 > this.config.maxNodes) {
|
|
532
|
+
this.nodeView.setLeaf(nodeIndex, true);
|
|
533
|
+
return nodeIndex;
|
|
534
|
+
}
|
|
535
|
+
this.subdivideNode(nodeIndex);
|
|
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
|
+
}
|
|
558
|
+
}
|
|
559
|
+
this.nodeView.setLeaf(nodeIndex, false);
|
|
560
|
+
return bestLeafIndex;
|
|
561
|
+
}
|
|
562
|
+
this.nodeView.setLeaf(nodeIndex, true);
|
|
563
|
+
return nodeIndex;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Determine if a node should be subdivided using the configured strategy
|
|
567
|
+
*/
|
|
568
|
+
shouldSubdivide(level, distance, nodeSize) {
|
|
569
|
+
const context = {
|
|
570
|
+
level,
|
|
571
|
+
distance,
|
|
572
|
+
nodeSize,
|
|
573
|
+
minNodeSize: this.config.minNodeSize,
|
|
574
|
+
rootSize: this.config.rootSize
|
|
575
|
+
};
|
|
576
|
+
return this.subdivisionStrategy(context);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Create a new node and return its index
|
|
580
|
+
*/
|
|
581
|
+
createNode(level, x, y) {
|
|
582
|
+
if (this.nodeCount >= this.config.maxNodes) {
|
|
583
|
+
console.warn("Maximum node count reached, skipping node creation");
|
|
584
|
+
return -1;
|
|
585
|
+
}
|
|
586
|
+
if (level > this.deepestLevel) {
|
|
587
|
+
this.deepestLevel = level;
|
|
588
|
+
}
|
|
589
|
+
this.tempChildIndices[0] = EMPTY_SENTINEL_VALUE;
|
|
590
|
+
this.tempChildIndices[1] = EMPTY_SENTINEL_VALUE;
|
|
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;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Subdivide a node by creating its four children
|
|
608
|
+
*/
|
|
609
|
+
subdivideNode(nodeIndex) {
|
|
610
|
+
const childLevel = this.nodeView.getLevel(nodeIndex) + 1;
|
|
611
|
+
const childX = this.nodeView.getX(nodeIndex) * 2;
|
|
612
|
+
const childY = this.nodeView.getY(nodeIndex) * 2;
|
|
613
|
+
const childIndices = [
|
|
614
|
+
this.createNode(childLevel, childX, childY),
|
|
615
|
+
// top-left
|
|
616
|
+
this.createNode(childLevel, childX + 1, childY),
|
|
617
|
+
// top-right
|
|
618
|
+
this.createNode(childLevel, childX, childY + 1),
|
|
619
|
+
// bottom-left
|
|
620
|
+
this.createNode(childLevel, childX + 1, childY + 1)
|
|
621
|
+
// bottom-right
|
|
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);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Update neighbor relationships for child nodes
|
|
632
|
+
*/
|
|
633
|
+
updateChildNeighbors(_parentIndex, childIndices) {
|
|
634
|
+
for (let i = 0; i < 4; i++) {
|
|
635
|
+
const childIndex = childIndices[i];
|
|
636
|
+
this.tempNeighborIndices[0] = EMPTY_SENTINEL_VALUE;
|
|
637
|
+
this.tempNeighborIndices[1] = EMPTY_SENTINEL_VALUE;
|
|
638
|
+
this.tempNeighborIndices[2] = EMPTY_SENTINEL_VALUE;
|
|
639
|
+
this.tempNeighborIndices[3] = EMPTY_SENTINEL_VALUE;
|
|
640
|
+
const childX = i % 2;
|
|
641
|
+
const childY = Math.floor(i / 2);
|
|
642
|
+
if (childX === 0 && i + 1 < 4) {
|
|
643
|
+
this.tempNeighborIndices[1] = childIndices[i + 1];
|
|
644
|
+
} else if (childX === 1 && i - 1 >= 0) {
|
|
645
|
+
this.tempNeighborIndices[0] = childIndices[i - 1];
|
|
646
|
+
}
|
|
647
|
+
if (childY === 0 && i + 2 < 4) {
|
|
648
|
+
this.tempNeighborIndices[3] = childIndices[i + 2];
|
|
649
|
+
} else if (childY === 1 && i - 2 >= 0) {
|
|
650
|
+
this.tempNeighborIndices[2] = childIndices[i - 2];
|
|
651
|
+
}
|
|
652
|
+
this.nodeView.setNeighbors(childIndex, this.tempNeighborIndices);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Get the deepest subdivision level currently in the quadtree
|
|
657
|
+
*/
|
|
658
|
+
getDeepestLevel() {
|
|
659
|
+
return this.deepestLevel;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Get the total number of nodes
|
|
663
|
+
*/
|
|
664
|
+
getNodeCount() {
|
|
665
|
+
return this.nodeCount;
|
|
666
|
+
}
|
|
667
|
+
getLeafNodeCount() {
|
|
668
|
+
return this.nodeView.getLeafNodeCount();
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get active leaf node indices for efficient GPU processing
|
|
672
|
+
*/
|
|
673
|
+
getActiveLeafNodeIndices() {
|
|
674
|
+
return this.nodeView.getActiveLeafNodeIndices();
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get the configuration
|
|
678
|
+
*/
|
|
679
|
+
getConfig() {
|
|
680
|
+
return this.config;
|
|
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
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return leafNodes;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Reset the quadtree
|
|
700
|
+
*/
|
|
701
|
+
reset() {
|
|
702
|
+
this.initialize();
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Get the NodeView instance for direct access
|
|
706
|
+
*/
|
|
707
|
+
getNodeView() {
|
|
708
|
+
return this.nodeView;
|
|
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();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
exports.Quadtree = Quadtree;
|
|
238
730
|
exports.TerrainGeometry = TerrainGeometry;
|
|
731
|
+
exports.computeScreenSpaceInfo = computeScreenSpaceInfo;
|
|
732
|
+
exports.distanceBasedSubdivision = distanceBasedSubdivision;
|
|
239
733
|
exports.isSkirtUV = isSkirtUV;
|
|
240
734
|
exports.isSkirtVertex = isSkirtVertex;
|
|
735
|
+
exports.screenSpaceSubdivision = screenSpaceSubdivision;
|