@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.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import * as THREE from 'three';
1
2
  import { BufferGeometry, BufferAttribute } from 'three';
2
3
  import { Fn, int, vertexIndex, uv, float } from 'three/tsl';
3
4
 
@@ -233,4 +234,481 @@ const isSkirtUV = Fn(([segments]) => {
233
234
  return innerX.and(innerY).not();
234
235
  });
235
236
 
236
- export { TerrainGeometry, isSkirtUV, isSkirtVertex };
237
+ const CHILDREN_STRIDE = 4;
238
+ const NEIGHBORS_STRIDE = 4;
239
+ const NODE_STRIDE = 4;
240
+ const U_INT_16_MAX_VALUE = 65535;
241
+ const EMPTY_SENTINEL_VALUE = U_INT_16_MAX_VALUE;
242
+ class QuadtreeNodeView {
243
+ maxNodeCount;
244
+ childrenIndicesBuffer;
245
+ neighborsIndicesBuffer;
246
+ nodeBuffer;
247
+ leafNodeMask;
248
+ leafNodeCountBuffer;
249
+ activeLeafIndices;
250
+ activeLeafCount = 0;
251
+ constructor(maxNodeCount, childrenIndicesBuffer, neighborsIndicesBuffer, nodeBuffer, leafNodeMask, leafNodeCountBuffer) {
252
+ this.maxNodeCount = maxNodeCount;
253
+ this.childrenIndicesBuffer = childrenIndicesBuffer ?? new Uint16Array(CHILDREN_STRIDE * maxNodeCount);
254
+ this.neighborsIndicesBuffer = neighborsIndicesBuffer ?? new Uint16Array(NEIGHBORS_STRIDE * maxNodeCount);
255
+ this.nodeBuffer = nodeBuffer ?? new Int32Array(NODE_STRIDE * maxNodeCount);
256
+ this.leafNodeMask = leafNodeMask ?? new Uint8Array(maxNodeCount);
257
+ this.leafNodeCountBuffer = leafNodeCountBuffer ?? new Uint16Array(1);
258
+ this.activeLeafIndices = new Uint16Array(maxNodeCount);
259
+ this.clear();
260
+ }
261
+ /**
262
+ * Clear all buffers
263
+ */
264
+ clear() {
265
+ this.nodeBuffer.fill(0);
266
+ this.childrenIndicesBuffer.fill(EMPTY_SENTINEL_VALUE);
267
+ this.neighborsIndicesBuffer.fill(EMPTY_SENTINEL_VALUE);
268
+ this.leafNodeMask.fill(0);
269
+ this.leafNodeCountBuffer[0] = 0;
270
+ this.activeLeafCount = 0;
271
+ }
272
+ /**
273
+ * Get buffer references for direct access (useful for GPU operations)
274
+ */
275
+ getBuffers() {
276
+ return {
277
+ childrenIndicesBuffer: this.childrenIndicesBuffer,
278
+ neighborsIndicesBuffer: this.neighborsIndicesBuffer,
279
+ nodeBuffer: this.nodeBuffer,
280
+ leafNodeMask: this.leafNodeMask
281
+ };
282
+ }
283
+ /**
284
+ * Get the maximum node count
285
+ */
286
+ getMaxNodeCount() {
287
+ return this.maxNodeCount;
288
+ }
289
+ // Getters for individual buffer values
290
+ getLevel(index) {
291
+ return this.nodeBuffer[index * NODE_STRIDE];
292
+ }
293
+ getX(index) {
294
+ return this.nodeBuffer[index * NODE_STRIDE + 1];
295
+ }
296
+ getY(index) {
297
+ return this.nodeBuffer[index * NODE_STRIDE + 2];
298
+ }
299
+ getLeafNodeCount() {
300
+ return this.leafNodeCountBuffer[0];
301
+ }
302
+ getLeaf(index) {
303
+ return this.leafNodeMask[index] === 1;
304
+ }
305
+ getChildren(index) {
306
+ const offset = index * CHILDREN_STRIDE;
307
+ return [
308
+ this.childrenIndicesBuffer[offset],
309
+ this.childrenIndicesBuffer[offset + 1],
310
+ this.childrenIndicesBuffer[offset + 2],
311
+ this.childrenIndicesBuffer[offset + 3]
312
+ ];
313
+ }
314
+ getNeighbors(index) {
315
+ const offset = index * NEIGHBORS_STRIDE;
316
+ return [
317
+ this.neighborsIndicesBuffer[offset],
318
+ this.neighborsIndicesBuffer[offset + 1],
319
+ this.neighborsIndicesBuffer[offset + 2],
320
+ this.neighborsIndicesBuffer[offset + 3]
321
+ ];
322
+ }
323
+ // Setters for individual buffer values
324
+ setLevel(index, level) {
325
+ this.nodeBuffer[index * NODE_STRIDE] = level;
326
+ }
327
+ setX(index, x) {
328
+ this.nodeBuffer[index * NODE_STRIDE + 1] = x;
329
+ }
330
+ setY(index, y) {
331
+ this.nodeBuffer[index * NODE_STRIDE + 2] = y;
332
+ }
333
+ setLeaf(index, leaf) {
334
+ const wasLeaf = this.leafNodeMask[index] === 1;
335
+ const newValue = leaf ? 1 : 0;
336
+ if (leaf && !wasLeaf) {
337
+ this.leafNodeCountBuffer[0]++;
338
+ this.leafNodeMask[index] = 1;
339
+ this.activeLeafIndices[this.activeLeafCount] = index;
340
+ this.activeLeafCount++;
341
+ this.setChildren(index, [
342
+ EMPTY_SENTINEL_VALUE,
343
+ EMPTY_SENTINEL_VALUE,
344
+ EMPTY_SENTINEL_VALUE,
345
+ EMPTY_SENTINEL_VALUE
346
+ ]);
347
+ } else if (!leaf && wasLeaf) {
348
+ this.leafNodeCountBuffer[0]--;
349
+ this.leafNodeMask[index] = 0;
350
+ }
351
+ this.nodeBuffer[index * NODE_STRIDE + 3] = newValue;
352
+ }
353
+ setChildren(index, children) {
354
+ const offset = index * CHILDREN_STRIDE;
355
+ this.childrenIndicesBuffer[offset] = children[0];
356
+ this.childrenIndicesBuffer[offset + 1] = children[1];
357
+ this.childrenIndicesBuffer[offset + 2] = children[2];
358
+ this.childrenIndicesBuffer[offset + 3] = children[3];
359
+ }
360
+ setNeighbors(index, neighbors) {
361
+ const offset = index * NEIGHBORS_STRIDE;
362
+ this.neighborsIndicesBuffer[offset] = neighbors[0];
363
+ this.neighborsIndicesBuffer[offset + 1] = neighbors[1];
364
+ this.neighborsIndicesBuffer[offset + 2] = neighbors[2];
365
+ this.neighborsIndicesBuffer[offset + 3] = neighbors[3];
366
+ }
367
+ /**
368
+ * Get array of active leaf node indices with count (zero-copy, no allocation)
369
+ */
370
+ getActiveLeafNodeIndices() {
371
+ return {
372
+ indices: this.activeLeafIndices,
373
+ count: this.activeLeafCount
374
+ };
375
+ }
376
+ /**
377
+ * Release internal buffers and mark this view as destroyed
378
+ */
379
+ destroy() {
380
+ this.childrenIndicesBuffer = new Uint16Array(0);
381
+ this.neighborsIndicesBuffer = new Uint16Array(0);
382
+ this.nodeBuffer = new Int32Array(0);
383
+ this.leafNodeMask = new Uint8Array(0);
384
+ this.leafNodeCountBuffer = new Uint16Array(0);
385
+ this.maxNodeCount = 0;
386
+ }
387
+ clone() {
388
+ return new QuadtreeNodeView(
389
+ this.maxNodeCount,
390
+ this.childrenIndicesBuffer,
391
+ this.neighborsIndicesBuffer,
392
+ this.nodeBuffer,
393
+ this.leafNodeMask,
394
+ this.leafNodeCountBuffer
395
+ );
396
+ }
397
+ }
398
+
399
+ function distanceBasedSubdivision(factor = 2) {
400
+ return (ctx) => {
401
+ if (ctx.nodeSize <= ctx.minNodeSize) {
402
+ return false;
403
+ }
404
+ return ctx.distance < ctx.nodeSize * factor;
405
+ };
406
+ }
407
+ function screenSpaceSubdivision(options) {
408
+ const targetTrianglePixels = options.targetTrianglePixels ?? 6;
409
+ const tileSegments = options.tileSegments ?? 13;
410
+ return (ctx) => {
411
+ if (ctx.nodeSize <= ctx.minNodeSize) {
412
+ return false;
413
+ }
414
+ const screenInfo = options.getScreenSpaceInfo();
415
+ if (!screenInfo) {
416
+ return ctx.distance < ctx.nodeSize * 2;
417
+ }
418
+ const safeDistance = Math.max(ctx.distance, 1e-3);
419
+ const tileScreenSize = ctx.nodeSize / safeDistance * screenInfo.projectionFactor;
420
+ const triangleScreenSize = tileScreenSize / tileSegments;
421
+ return triangleScreenSize > targetTrianglePixels;
422
+ };
423
+ }
424
+ function computeScreenSpaceInfo(fovY, screenHeight) {
425
+ const projectionFactor = screenHeight / (2 * Math.tan(fovY / 2));
426
+ return { projectionFactor, screenHeight };
427
+ }
428
+
429
+ const tempVector3 = new THREE.Vector3();
430
+ const tempBox3 = new THREE.Box3();
431
+ const tempMin = new THREE.Vector3();
432
+ const tempMax = new THREE.Vector3();
433
+ class Quadtree {
434
+ nodeCount = 0;
435
+ deepestLevel = 0;
436
+ config;
437
+ nodeView;
438
+ subdivisionStrategy;
439
+ // Pre-allocated buffers to avoid object creation
440
+ tempChildIndices = [-1, -1, -1, -1];
441
+ tempNeighborIndices = [-1, -1, -1, -1];
442
+ /**
443
+ * Create a new Quadtree.
444
+ *
445
+ * @param config Quadtree configuration parameters
446
+ * @param subdivisionStrategy Strategy function for subdivision decisions.
447
+ * Defaults to distanceBasedSubdivision(2).
448
+ * @param nodeView Optional pre-allocated NodeView for buffer reuse
449
+ */
450
+ constructor(config, subdivisionStrategy, nodeView) {
451
+ this.config = config;
452
+ this.subdivisionStrategy = subdivisionStrategy ?? distanceBasedSubdivision(2);
453
+ this.nodeView = nodeView ?? new QuadtreeNodeView(config.maxNodes);
454
+ this.initialize();
455
+ }
456
+ /**
457
+ * Set the subdivision strategy.
458
+ * Use this to change LOD behavior at runtime.
459
+ *
460
+ * @param strategy The subdivision strategy function
461
+ */
462
+ setSubdivisionStrategy(strategy) {
463
+ this.subdivisionStrategy = strategy;
464
+ }
465
+ /**
466
+ * Get the current subdivision strategy
467
+ */
468
+ getSubdivisionStrategy() {
469
+ return this.subdivisionStrategy;
470
+ }
471
+ initialize() {
472
+ this.nodeView.clear();
473
+ this.nodeCount = 0;
474
+ this.deepestLevel = 0;
475
+ this.createNode(0, 0, 0);
476
+ }
477
+ /**
478
+ * Update the quadtree based on the given position and return the index
479
+ * of the leaf node that best corresponds to the position (closest leaf).
480
+ */
481
+ update(position, frustum) {
482
+ this.reset();
483
+ const closestLeafIndex = this.updateNode(0, position, frustum);
484
+ return closestLeafIndex;
485
+ }
486
+ /**
487
+ * Recursively update a node and its children based on distance and size criteria
488
+ * and return the closest leaf node index to the provided position.
489
+ */
490
+ updateNode(nodeIndex, position, frustum) {
491
+ const level = this.nodeView.getLevel(nodeIndex);
492
+ const nodeSize = this.config.rootSize / (1 << level);
493
+ const nodeX = this.nodeView.getX(nodeIndex);
494
+ const nodeY = this.nodeView.getY(nodeIndex);
495
+ const minX = this.config.origin.x + (nodeX * nodeSize - 0.5 * this.config.rootSize);
496
+ const minZ = this.config.origin.z + (nodeY * nodeSize - 0.5 * this.config.rootSize);
497
+ const worldX = minX + 0.5 * nodeSize;
498
+ const worldZ = minZ + 0.5 * nodeSize;
499
+ if (frustum) {
500
+ const altitude = Math.abs(position.y - this.config.origin.y);
501
+ const verticalHalfExtent = this.config.rootSize + altitude;
502
+ const minY = this.config.origin.y - verticalHalfExtent;
503
+ const maxY = this.config.origin.y + verticalHalfExtent;
504
+ tempMin.set(minX, minY, minZ);
505
+ tempMax.set(minX + nodeSize, maxY, minZ + nodeSize);
506
+ tempBox3.set(tempMin, tempMax);
507
+ if (!frustum.intersectsBox(tempBox3)) {
508
+ this.nodeView.setLeaf(nodeIndex, false);
509
+ return -1;
510
+ }
511
+ }
512
+ tempVector3.set(worldX, this.config.origin.y, worldZ);
513
+ const distance = position.distanceTo(tempVector3);
514
+ const shouldSubdivide = this.shouldSubdivide(level, distance, nodeSize);
515
+ if (shouldSubdivide && level < this.config.maxLevel) {
516
+ if (this.nodeCount + 4 > this.config.maxNodes) {
517
+ this.nodeView.setLeaf(nodeIndex, true);
518
+ return nodeIndex;
519
+ }
520
+ this.subdivideNode(nodeIndex);
521
+ const children = this.nodeView.getChildren(nodeIndex);
522
+ let bestLeafIndex = -1;
523
+ let bestDistSq = Number.POSITIVE_INFINITY;
524
+ for (let i = 0; i < 4; i++) {
525
+ if (children[i] !== -1) {
526
+ const leafIdx = this.updateNode(children[i], position, frustum);
527
+ if (leafIdx !== -1) {
528
+ const leafLevel = this.nodeView.getLevel(leafIdx);
529
+ const size = this.config.rootSize / (1 << leafLevel);
530
+ const x = this.nodeView.getX(leafIdx);
531
+ const y = this.nodeView.getY(leafIdx);
532
+ const cx = this.config.origin.x + ((x + 0.5) * size - 0.5 * this.config.rootSize);
533
+ const cz = this.config.origin.z + ((y + 0.5) * size - 0.5 * this.config.rootSize);
534
+ const dx = position.x - cx;
535
+ const dz = position.z - cz;
536
+ const d2 = dx * dx + dz * dz;
537
+ if (d2 < bestDistSq) {
538
+ bestDistSq = d2;
539
+ bestLeafIndex = leafIdx;
540
+ }
541
+ }
542
+ }
543
+ }
544
+ this.nodeView.setLeaf(nodeIndex, false);
545
+ return bestLeafIndex;
546
+ }
547
+ this.nodeView.setLeaf(nodeIndex, true);
548
+ return nodeIndex;
549
+ }
550
+ /**
551
+ * Determine if a node should be subdivided using the configured strategy
552
+ */
553
+ shouldSubdivide(level, distance, nodeSize) {
554
+ const context = {
555
+ level,
556
+ distance,
557
+ nodeSize,
558
+ minNodeSize: this.config.minNodeSize,
559
+ rootSize: this.config.rootSize
560
+ };
561
+ return this.subdivisionStrategy(context);
562
+ }
563
+ /**
564
+ * Create a new node and return its index
565
+ */
566
+ createNode(level, x, y) {
567
+ if (this.nodeCount >= this.config.maxNodes) {
568
+ console.warn("Maximum node count reached, skipping node creation");
569
+ return -1;
570
+ }
571
+ if (level > this.deepestLevel) {
572
+ this.deepestLevel = level;
573
+ }
574
+ this.tempChildIndices[0] = EMPTY_SENTINEL_VALUE;
575
+ this.tempChildIndices[1] = EMPTY_SENTINEL_VALUE;
576
+ this.tempChildIndices[2] = EMPTY_SENTINEL_VALUE;
577
+ this.tempChildIndices[3] = EMPTY_SENTINEL_VALUE;
578
+ this.tempNeighborIndices[0] = EMPTY_SENTINEL_VALUE;
579
+ this.tempNeighborIndices[1] = EMPTY_SENTINEL_VALUE;
580
+ this.tempNeighborIndices[2] = EMPTY_SENTINEL_VALUE;
581
+ this.tempNeighborIndices[3] = EMPTY_SENTINEL_VALUE;
582
+ const nodeIndex = this.nodeCount++;
583
+ this.nodeView.setLevel(nodeIndex, level);
584
+ this.nodeView.setX(nodeIndex, x);
585
+ this.nodeView.setY(nodeIndex, y);
586
+ this.nodeView.setChildren(nodeIndex, this.tempChildIndices);
587
+ this.nodeView.setNeighbors(nodeIndex, this.tempNeighborIndices);
588
+ this.nodeView.setLeaf(nodeIndex, false);
589
+ return nodeIndex;
590
+ }
591
+ /**
592
+ * Subdivide a node by creating its four children
593
+ */
594
+ subdivideNode(nodeIndex) {
595
+ const childLevel = this.nodeView.getLevel(nodeIndex) + 1;
596
+ const childX = this.nodeView.getX(nodeIndex) * 2;
597
+ const childY = this.nodeView.getY(nodeIndex) * 2;
598
+ const childIndices = [
599
+ this.createNode(childLevel, childX, childY),
600
+ // top-left
601
+ this.createNode(childLevel, childX + 1, childY),
602
+ // top-right
603
+ this.createNode(childLevel, childX, childY + 1),
604
+ // bottom-left
605
+ this.createNode(childLevel, childX + 1, childY + 1)
606
+ // bottom-right
607
+ ];
608
+ if (childIndices.some((index) => index === -1)) {
609
+ console.warn("Failed to create all children, skipping subdivision");
610
+ return;
611
+ }
612
+ this.nodeView.setChildren(nodeIndex, childIndices);
613
+ this.updateChildNeighbors(nodeIndex, childIndices);
614
+ }
615
+ /**
616
+ * Update neighbor relationships for child nodes
617
+ */
618
+ updateChildNeighbors(_parentIndex, childIndices) {
619
+ for (let i = 0; i < 4; i++) {
620
+ const childIndex = childIndices[i];
621
+ this.tempNeighborIndices[0] = EMPTY_SENTINEL_VALUE;
622
+ this.tempNeighborIndices[1] = EMPTY_SENTINEL_VALUE;
623
+ this.tempNeighborIndices[2] = EMPTY_SENTINEL_VALUE;
624
+ this.tempNeighborIndices[3] = EMPTY_SENTINEL_VALUE;
625
+ const childX = i % 2;
626
+ const childY = Math.floor(i / 2);
627
+ if (childX === 0 && i + 1 < 4) {
628
+ this.tempNeighborIndices[1] = childIndices[i + 1];
629
+ } else if (childX === 1 && i - 1 >= 0) {
630
+ this.tempNeighborIndices[0] = childIndices[i - 1];
631
+ }
632
+ if (childY === 0 && i + 2 < 4) {
633
+ this.tempNeighborIndices[3] = childIndices[i + 2];
634
+ } else if (childY === 1 && i - 2 >= 0) {
635
+ this.tempNeighborIndices[2] = childIndices[i - 2];
636
+ }
637
+ this.nodeView.setNeighbors(childIndex, this.tempNeighborIndices);
638
+ }
639
+ }
640
+ /**
641
+ * Get the deepest subdivision level currently in the quadtree
642
+ */
643
+ getDeepestLevel() {
644
+ return this.deepestLevel;
645
+ }
646
+ /**
647
+ * Get the total number of nodes
648
+ */
649
+ getNodeCount() {
650
+ return this.nodeCount;
651
+ }
652
+ getLeafNodeCount() {
653
+ return this.nodeView.getLeafNodeCount();
654
+ }
655
+ /**
656
+ * Get active leaf node indices for efficient GPU processing
657
+ */
658
+ getActiveLeafNodeIndices() {
659
+ return this.nodeView.getActiveLeafNodeIndices();
660
+ }
661
+ /**
662
+ * Get the configuration
663
+ */
664
+ getConfig() {
665
+ return this.config;
666
+ }
667
+ /**
668
+ * Get all leaf nodes as an array of node objects
669
+ */
670
+ getLeafNodes() {
671
+ const leafNodes = [];
672
+ for (let i = 0; i < this.nodeCount; i++) {
673
+ if (this.nodeView.getLeaf(i)) {
674
+ leafNodes.push({
675
+ level: this.nodeView.getLevel(i),
676
+ x: this.nodeView.getX(i),
677
+ y: this.nodeView.getY(i)
678
+ });
679
+ }
680
+ }
681
+ return leafNodes;
682
+ }
683
+ /**
684
+ * Reset the quadtree
685
+ */
686
+ reset() {
687
+ this.initialize();
688
+ }
689
+ /**
690
+ * Get the NodeView instance for direct access
691
+ */
692
+ getNodeView() {
693
+ return this.nodeView;
694
+ }
695
+ /**
696
+ * Release internal resources associated with this quadtree
697
+ */
698
+ destroy() {
699
+ this.nodeView.destroy();
700
+ this.nodeCount = 0;
701
+ this.deepestLevel = 0;
702
+ }
703
+ /**
704
+ * Set the configuration
705
+ */
706
+ setConfig(config, reset = false) {
707
+ this.config = config;
708
+ if (reset) {
709
+ this.initialize();
710
+ }
711
+ }
712
+ }
713
+
714
+ export { Quadtree, TerrainGeometry, computeScreenSpaceInfo, distanceBasedSubdivision, isSkirtUV, isSkirtVertex, screenSpaceSubdivision };
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@hello-terrain/three",
3
3
  "description": "High performance terrain system for three.js",
4
- "version": "0.0.0-alpha.1",
4
+ "homepage": "http://hello-terrain.kenny.wtf",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/kenjinp/hello-terrain.git"
8
+ },
9
+ "version": "0.0.0-alpha.2",
5
10
  "type": "module",
6
11
  "main": "./dist/index.mjs",
7
12
  "module": "./dist/index.mjs",