@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 CHANGED
@@ -1,9 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const three = require('three');
3
+ const THREE = require('three');
4
4
  const tsl = require('three/tsl');
5
5
 
6
- class TerrainGeometry extends three.BufferGeometry {
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 three.BufferAttribute(
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 three.BufferAttribute(
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 three.BufferAttribute(
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;