@hello-terrain/three 0.0.0-alpha.7 → 0.0.0-alpha.9

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
@@ -411,25 +411,25 @@ function resolveType(format) {
411
411
  function resolveFilter(mode) {
412
412
  return mode === "linear" ? three.LinearFilter : three.NearestFilter;
413
413
  }
414
- function configureStorageTexture(texture, format, filter) {
415
- texture.format = three.RGBAFormat;
416
- texture.type = resolveType(format);
417
- texture.magFilter = resolveFilter(filter);
418
- texture.minFilter = resolveFilter(filter);
419
- texture.wrapS = three.ClampToEdgeWrapping;
420
- texture.wrapT = three.ClampToEdgeWrapping;
421
- texture.generateMipmaps = false;
422
- texture.needsUpdate = true;
414
+ function configureStorageTexture(texture2, format, filter) {
415
+ texture2.format = three.RGBAFormat;
416
+ texture2.type = resolveType(format);
417
+ texture2.magFilter = resolveFilter(filter);
418
+ texture2.minFilter = resolveFilter(filter);
419
+ texture2.wrapS = three.ClampToEdgeWrapping;
420
+ texture2.wrapT = three.ClampToEdgeWrapping;
421
+ texture2.generateMipmaps = false;
422
+ texture2.needsUpdate = true;
423
423
  }
424
424
  function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
425
425
  let currentEdgeVertexCount = edgeVertexCount;
426
426
  let currentTileCount = tileCount;
427
- const texture = new webgpu.StorageArrayTexture(
427
+ const tex = new webgpu.StorageArrayTexture(
428
428
  edgeVertexCount,
429
429
  edgeVertexCount,
430
430
  tileCount
431
431
  );
432
- configureStorageTexture(texture, options.format, options.filter);
432
+ configureStorageTexture(tex, options.format, options.filter);
433
433
  return {
434
434
  backendType: "array-texture",
435
435
  get edgeVertexCount() {
@@ -438,18 +438,21 @@ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
438
438
  get tileCount() {
439
439
  return currentTileCount;
440
440
  },
441
- texture,
441
+ texture: tex,
442
442
  uv(ix, iy, _tileIndex) {
443
443
  return tsl.vec2(ix.toFloat(), iy.toFloat());
444
444
  },
445
445
  texel(ix, iy, tileIndex) {
446
446
  return tsl.ivec3(ix, iy, tileIndex);
447
447
  },
448
+ sample(u, v, tileIndex) {
449
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
450
+ },
448
451
  resize(width, height, nextTileCount) {
449
452
  currentEdgeVertexCount = width;
450
453
  currentTileCount = nextTileCount;
451
- texture.setSize(width, height, nextTileCount);
452
- texture.needsUpdate = true;
454
+ tex.setSize(width, height, nextTileCount);
455
+ tex.needsUpdate = true;
453
456
  }
454
457
  };
455
458
  }
@@ -468,8 +471,8 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
468
471
  let currentTileCount = tileCount;
469
472
  let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
470
473
  const atlasSize = tilesPerRow * edgeVertexCount;
471
- const texture = new webgpu.StorageTexture(atlasSize, atlasSize);
472
- configureStorageTexture(texture, options.format, options.filter);
474
+ const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
475
+ configureStorageTexture(tex, options.format, options.filter);
473
476
  return {
474
477
  backendType: "atlas",
475
478
  get edgeVertexCount() {
@@ -478,7 +481,7 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
478
481
  get tileCount() {
479
482
  return currentTileCount;
480
483
  },
481
- texture,
484
+ texture: tex,
482
485
  uv(ix, iy, tileIndex) {
483
486
  const { atlasX, atlasY } = atlasCoord(
484
487
  tilesPerRow,
@@ -503,27 +506,37 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
503
506
  );
504
507
  return tsl.ivec2(atlasX, atlasY);
505
508
  },
509
+ sample(u, v, tileIndex) {
510
+ const tile = tsl.int(tileIndex);
511
+ const tilesPerRowNode = tsl.int(tilesPerRow);
512
+ const col = tile.mod(tilesPerRowNode);
513
+ const row = tile.div(tilesPerRowNode);
514
+ const invTilesPerRow = tsl.float(1 / tilesPerRow);
515
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
516
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
517
+ return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
518
+ },
506
519
  resize(width, height, nextTileCount) {
507
520
  currentEdgeVertexCount = width;
508
521
  currentTileCount = nextTileCount;
509
522
  tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
510
523
  const nextAtlasSize = tilesPerRow * width;
511
- const image = texture.image;
524
+ const image = tex.image;
512
525
  image.width = nextAtlasSize;
513
526
  image.height = nextAtlasSize;
514
- texture.needsUpdate = true;
527
+ tex.needsUpdate = true;
515
528
  }
516
529
  };
517
530
  }
518
531
  function Texture3DBackend(edgeVertexCount, tileCount, options) {
519
532
  let currentEdgeVertexCount = edgeVertexCount;
520
533
  let currentTileCount = tileCount;
521
- const texture = new webgpu.StorageArrayTexture(
534
+ const tex = new webgpu.StorageArrayTexture(
522
535
  edgeVertexCount,
523
536
  edgeVertexCount,
524
537
  tileCount
525
538
  );
526
- configureStorageTexture(texture, options.format, options.filter);
539
+ configureStorageTexture(tex, options.format, options.filter);
527
540
  return {
528
541
  backendType: "texture-3d",
529
542
  get edgeVertexCount() {
@@ -532,18 +545,21 @@ function Texture3DBackend(edgeVertexCount, tileCount, options) {
532
545
  get tileCount() {
533
546
  return currentTileCount;
534
547
  },
535
- texture,
548
+ texture: tex,
536
549
  uv(ix, iy, _tileIndex) {
537
550
  return tsl.vec2(ix.toFloat(), iy.toFloat());
538
551
  },
539
552
  texel(ix, iy, tileIndex) {
540
553
  return tsl.ivec3(ix, iy, tileIndex);
541
554
  },
555
+ sample(u, v, tileIndex) {
556
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
557
+ },
542
558
  resize(width, height, nextTileCount) {
543
559
  currentEdgeVertexCount = width;
544
560
  currentTileCount = nextTileCount;
545
- texture.setSize(width, height, nextTileCount);
546
- texture.needsUpdate = true;
561
+ tex.setSize(width, height, nextTileCount);
562
+ tex.needsUpdate = true;
547
563
  }
548
564
  };
549
565
  }
@@ -552,7 +568,7 @@ function tryGetDeviceLimits(renderer) {
552
568
  return backend.backend?.device?.limits ?? {};
553
569
  }
554
570
  function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
555
- const filter = options.filter ?? "nearest";
571
+ const filter = options.filter ?? "linear";
556
572
  const format = options.format ?? "rgba16float";
557
573
  const forcedBackend = options.backend;
558
574
  if (forcedBackend === "atlas") {
@@ -593,8 +609,18 @@ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
593
609
  return loadTerrainField(storage, ix, iy, tileIndex).r;
594
610
  }
595
611
  function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
596
- const sample = loadTerrainField(storage, ix, iy, tileIndex);
597
- return tsl.vec2(sample.g, sample.b);
612
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
613
+ return tsl.vec2(raw.g, raw.b);
614
+ }
615
+ function sampleTerrainField(storage, u, v, tileIndex) {
616
+ return storage.sample(u, v, tileIndex);
617
+ }
618
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
619
+ return sampleTerrainField(storage, u, v, tileIndex).r;
620
+ }
621
+ function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
622
+ const raw = sampleTerrainField(storage, u, v, tileIndex);
623
+ return tsl.vec2(raw.g, raw.b);
598
624
  }
599
625
  function packTerrainFieldSample(height, normalXZ, extra = tsl.float(0)) {
600
626
  return tsl.vec4(height, normalXZ.x, normalXZ.y, extra);
@@ -621,25 +647,6 @@ const createElevation = (tile, uniforms, elevationFn) => {
621
647
  });
622
648
  };
623
649
  };
624
- const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => tsl.Fn(() => {
625
- const nodeIndex = tsl.int(tsl.instanceIndex);
626
- const intEdge = tsl.int(edgeVertexCount);
627
- const innerSegments = tsl.int(edgeVertexCount).sub(3);
628
- const fInnerSegments = tsl.float(innerSegments);
629
- const last = intEdge.sub(tsl.int(1));
630
- const u = positionLocal.x.add(tsl.float(0.5));
631
- const v = positionLocal.z.add(tsl.float(0.5));
632
- const x = u.mul(fInnerSegments).round().toInt().add(tsl.int(1));
633
- const y = v.mul(fInnerSegments).round().toInt().add(tsl.int(1));
634
- const xClamped = tsl.min(tsl.max(x, tsl.int(0)), last);
635
- const yClamped = tsl.min(tsl.max(y, tsl.int(0)), last);
636
- return loadTerrainFieldElevation(
637
- terrainFieldStorage,
638
- xClamped,
639
- yClamped,
640
- nodeIndex
641
- );
642
- });
643
650
 
644
651
  function createTileCompute(leafStorage, uniforms) {
645
652
  const tileLevel = tsl.Fn(([nodeIndex]) => {
@@ -706,6 +713,10 @@ function createTileCompute(leafStorage, uniforms) {
706
713
  tileVertexWorldPositionCompute
707
714
  };
708
715
  }
716
+ function tileLocalToFieldUV(localCoord, innerSegments) {
717
+ const edge = tsl.float(innerSegments).add(tsl.float(3));
718
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(1.5)).div(edge);
719
+ }
709
720
 
710
721
  const rootSize = work.param(256).displayName("rootSize");
711
722
  const origin = work.param({
@@ -724,6 +735,7 @@ const quadtreeUpdate = work.param({
724
735
  distanceFactor: 1.5
725
736
  }).displayName("quadtreeUpdate");
726
737
  const surface = work.param(null).displayName("surface");
738
+ const terrainFieldFilter = work.param("linear").displayName("terrainFieldFilter");
727
739
  const elevationFn = work.param(() => tsl.float(0));
728
740
 
729
741
  function createLeafStorage(maxNodes) {
@@ -821,12 +833,12 @@ function ensureChildren(store, parentId) {
821
833
  return childBase;
822
834
  }
823
835
 
824
- function nextPow2(n) {
836
+ function nextPow2$1(n) {
825
837
  let x = 1;
826
838
  while (x < n) x <<= 1;
827
839
  return x;
828
840
  }
829
- function mix32(x) {
841
+ function mix32$1(x) {
830
842
  x >>>= 0;
831
843
  x ^= x >>> 16;
832
844
  x = Math.imul(x, 2146121005) >>> 0;
@@ -835,12 +847,12 @@ function mix32(x) {
835
847
  x ^= x >>> 16;
836
848
  return x >>> 0;
837
849
  }
838
- function hashKey(space, level, x, y) {
839
- const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
840
- return mix32(h);
850
+ function hashKey$1(space, level, x, y) {
851
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
852
+ return mix32$1(h);
841
853
  }
842
854
  function createSpatialIndex(maxEntries) {
843
- const size = nextPow2(Math.max(2, maxEntries * 2));
855
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
844
856
  return {
845
857
  size,
846
858
  mask: size - 1,
@@ -865,7 +877,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
865
877
  const l = level & 255;
866
878
  const xx = x >>> 0;
867
879
  const yy = y >>> 0;
868
- let slot = hashKey(s, l, xx, yy) & index.mask;
880
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
869
881
  for (let probes = 0; probes < index.size; probes++) {
870
882
  if (index.stamp[slot] !== index.stampGen) {
871
883
  index.stamp[slot] = index.stampGen;
@@ -889,7 +901,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
889
901
  const l = level & 255;
890
902
  const xx = x >>> 0;
891
903
  const yy = y >>> 0;
892
- let slot = hashKey(s, l, xx, yy) & index.mask;
904
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
893
905
  for (let probes = 0; probes < index.size; probes++) {
894
906
  if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
895
907
  if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
@@ -1535,11 +1547,13 @@ const createTerrainFieldTextureTask = work.task(
1535
1547
  (get, work, { resources }) => {
1536
1548
  const edgeVertexCount = get(innerTileSegments) + 3;
1537
1549
  const maxNodesValue = get(maxNodes);
1550
+ const filter = get(terrainFieldFilter);
1538
1551
  return work(
1539
1552
  () => createTerrainFieldStorage(
1540
1553
  edgeVertexCount,
1541
1554
  maxNodesValue,
1542
- resources?.renderer
1555
+ resources?.renderer,
1556
+ { filter }
1543
1557
  )
1544
1558
  );
1545
1559
  }
@@ -1648,6 +1662,275 @@ function createComputePipelineTasks(leafStageTask) {
1648
1662
  return { compile, execute };
1649
1663
  }
1650
1664
 
1665
+ const SLOT_STRIDE = 6;
1666
+ function nextPow2(n) {
1667
+ let x = 1;
1668
+ while (x < n) x <<= 1;
1669
+ return x;
1670
+ }
1671
+ function createGpuSpatialIndex(maxEntries) {
1672
+ const size = nextPow2(Math.max(2, maxEntries * 2));
1673
+ const data = new Uint32Array(size * SLOT_STRIDE);
1674
+ const attribute = new webgpu.StorageBufferAttribute(data, SLOT_STRIDE);
1675
+ const node = tsl.storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
1676
+ const stampGen = tsl.uniform(tsl.uint(1)).setName("uGpuSpatialIndexStampGen");
1677
+ return {
1678
+ data,
1679
+ size,
1680
+ mask: size - 1,
1681
+ stampGen,
1682
+ attribute,
1683
+ node
1684
+ };
1685
+ }
1686
+ function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
1687
+ if (gpuIndex.size !== cpuIndex.size) {
1688
+ throw new Error(
1689
+ `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
1690
+ );
1691
+ }
1692
+ for (let i = 0; i < cpuIndex.size; i += 1) {
1693
+ const base = i * SLOT_STRIDE;
1694
+ gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
1695
+ gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
1696
+ gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
1697
+ gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
1698
+ gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
1699
+ gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
1700
+ }
1701
+ gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
1702
+ gpuIndex.attribute.needsUpdate = true;
1703
+ gpuIndex.node.needsUpdate = true;
1704
+ }
1705
+ function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
1706
+ const offset = tsl.int(slot).mul(tsl.int(SLOT_STRIDE)).add(tsl.int(fieldOffset));
1707
+ return spatialIndex.node.element(offset).toUint();
1708
+ }
1709
+ const mix32 = tsl.Fn(([x]) => {
1710
+ const v = tsl.uint(x).toVar();
1711
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
1712
+ v.assign(v.mul(tsl.uint(2146121005)));
1713
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(15))));
1714
+ v.assign(v.mul(tsl.uint(2221713035)));
1715
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
1716
+ return v;
1717
+ });
1718
+ const hashKey = tsl.Fn(([space, level, x, y]) => {
1719
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
1720
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
1721
+ const h = s.bitXor(l.shiftLeft(tsl.uint(8))).bitXor(mix32(tsl.uint(x))).bitXor(mix32(tsl.uint(y)));
1722
+ return mix32(h);
1723
+ });
1724
+ const createGpuSpatialLookup = (spatialIndex) => {
1725
+ const slotCount = spatialIndex.size;
1726
+ const mask = tsl.uint(spatialIndex.mask);
1727
+ const stampGen = spatialIndex.stampGen.toUint();
1728
+ const emptyValue = tsl.int(-1);
1729
+ return tsl.Fn(([space, level, x, y]) => {
1730
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
1731
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
1732
+ const xx = tsl.uint(x);
1733
+ const yy = tsl.uint(y);
1734
+ const result = emptyValue.toVar();
1735
+ const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
1736
+ const probes = tsl.int(0).toVar();
1737
+ tsl.Loop(slotCount, () => {
1738
+ const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
1739
+ tsl.If(stamp.notEqual(stampGen), () => {
1740
+ tsl.Break();
1741
+ });
1742
+ const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
1743
+ const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
1744
+ const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
1745
+ const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
1746
+ tsl.If(
1747
+ ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
1748
+ () => {
1749
+ result.assign(tsl.int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
1750
+ tsl.Break();
1751
+ }
1752
+ );
1753
+ slot.assign(slot.add(tsl.uint(1)).bitAnd(mask));
1754
+ probes.addAssign(1);
1755
+ });
1756
+ return result;
1757
+ });
1758
+ };
1759
+ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
1760
+ const lookup = createGpuSpatialLookup(spatialIndex);
1761
+ const levelCount = Math.max(1, maxLevel + 1);
1762
+ return tsl.Fn(([worldX, worldZ]) => {
1763
+ const rootOrigin = uniforms.uRootOrigin.toVar();
1764
+ const rootSize = uniforms.uRootSize.toVar();
1765
+ const halfRoot = rootSize.mul(tsl.float(0.5));
1766
+ const tileIndex = tsl.int(-1).toVar();
1767
+ const tileU = tsl.float(0).toVar();
1768
+ const tileV = tsl.float(0).toVar();
1769
+ const i = tsl.int(0).toVar();
1770
+ tsl.Loop(levelCount, () => {
1771
+ const level = tsl.int(maxLevel).sub(i).toVar();
1772
+ const scale = tsl.pow(tsl.float(2), level.toFloat());
1773
+ const tileSize = rootSize.div(scale);
1774
+ const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
1775
+ const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
1776
+ const maybeIndex = lookup(tsl.int(0), level, tileX, tileY).toVar();
1777
+ tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
1778
+ const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
1779
+ const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
1780
+ tileIndex.assign(maybeIndex);
1781
+ tileU.assign(worldX.sub(minX).div(tileSize));
1782
+ tileV.assign(worldZ.sub(minZ).div(tileSize));
1783
+ tsl.Break();
1784
+ });
1785
+ i.addAssign(1);
1786
+ });
1787
+ return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
1788
+ });
1789
+ };
1790
+
1791
+ const gpuSpatialIndexStorageTask = work.task((get, work) => {
1792
+ const maxNodesValue = get(maxNodes);
1793
+ return work(() => createGpuSpatialIndex(maxNodesValue));
1794
+ }).displayName("gpuSpatialIndexStorageTask");
1795
+ const gpuSpatialIndexUploadTask = work.task((get, work) => {
1796
+ const quadtreeConfig = get(quadtreeConfigTask);
1797
+ get(quadtreeUpdateTask);
1798
+ const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
1799
+ return work(() => {
1800
+ uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
1801
+ return gpuSpatialIndex;
1802
+ });
1803
+ }).displayName("gpuSpatialIndexUploadTask");
1804
+
1805
+ function createTerrainSampleNode(params) {
1806
+ const tileLookup = createTileIndexFromWorldPosition(
1807
+ params.spatialIndex,
1808
+ params.uniforms,
1809
+ maxLevel.get()
1810
+ );
1811
+ return tsl.Fn(([worldX, worldZ]) => {
1812
+ const tileResult = tileLookup(worldX, worldZ).toVar();
1813
+ const tileIndex = tsl.int(tileResult.x).toVar();
1814
+ const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
1815
+ const u = tileResult.y.toVar();
1816
+ const v = tileResult.z.toVar();
1817
+ const fieldU = tileLocalToFieldUV(
1818
+ u,
1819
+ params.uniforms.uInnerTileSegments
1820
+ ).toVar();
1821
+ const fieldV = tileLocalToFieldUV(
1822
+ v,
1823
+ params.uniforms.uInnerTileSegments
1824
+ ).toVar();
1825
+ const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
1826
+ const sampled = sampleTerrainField(
1827
+ params.terrainFieldStorage,
1828
+ fieldU,
1829
+ fieldV,
1830
+ safeTileIndex
1831
+ ).toVar();
1832
+ const nx = sampled.g.toVar();
1833
+ const nz = sampled.b.toVar();
1834
+ const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
1835
+ const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
1836
+ return tsl.vec4(
1837
+ sampled.r.mul(valid),
1838
+ nx.mul(valid),
1839
+ ny.mul(valid),
1840
+ nz.mul(valid)
1841
+ );
1842
+ });
1843
+ }
1844
+ function createTerrainSampler(params) {
1845
+ const elevationNode = createElevationFunction(params.elevationCallback);
1846
+ const terrainSampleAt = createTerrainSampleNode(params);
1847
+ const evaluateElevationAt = tsl.Fn(([worldX, worldZ]) => {
1848
+ const rootOrigin = params.uniforms.uRootOrigin.toVar();
1849
+ const rootSize = params.uniforms.uRootSize.toVar();
1850
+ const centeredX = worldX.sub(rootOrigin.x);
1851
+ const centeredZ = worldZ.sub(rootOrigin.z);
1852
+ const rootUV = tsl.vec2(
1853
+ centeredX.div(rootSize).add(0.5),
1854
+ centeredZ.div(rootSize).mul(tsl.float(-1)).add(0.5)
1855
+ ).toVar();
1856
+ return elevationNode({
1857
+ worldPosition: tsl.vec3(worldX, rootOrigin.y, worldZ),
1858
+ rootSize,
1859
+ rootUV,
1860
+ tileUV: rootUV,
1861
+ tileLevel: tsl.int(0),
1862
+ tileSize: rootSize,
1863
+ tileOriginVec2: tsl.vec2(0, 0),
1864
+ nodeIndex: tsl.int(0)
1865
+ });
1866
+ });
1867
+ const sampleTerrain = tsl.Fn(
1868
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
1869
+ );
1870
+ const sampleElevation = tsl.Fn(
1871
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
1872
+ );
1873
+ const sampleNormal = tsl.Fn(
1874
+ ([worldX, worldZ]) => tsl.vec3(
1875
+ terrainSampleAt(worldX, worldZ).y,
1876
+ terrainSampleAt(worldX, worldZ).z,
1877
+ terrainSampleAt(worldX, worldZ).w
1878
+ )
1879
+ );
1880
+ const sampleValidity = tsl.Fn(
1881
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0))
1882
+ );
1883
+ const evaluateElevation = tsl.Fn(
1884
+ ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
1885
+ );
1886
+ const evaluateNormalNode = tsl.Fn(
1887
+ ([worldX, worldZ, epsilon]) => {
1888
+ const eps = epsilon ?? tsl.float(0.1);
1889
+ const elevationScale = params.uniforms.uElevationScale.toVar();
1890
+ const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
1891
+ elevationScale
1892
+ );
1893
+ const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
1894
+ elevationScale
1895
+ );
1896
+ const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
1897
+ elevationScale
1898
+ );
1899
+ const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
1900
+ elevationScale
1901
+ );
1902
+ const inv2eps = tsl.float(0.5).div(eps);
1903
+ const dhdx = hR.sub(hL).mul(inv2eps);
1904
+ const dhdz = hU.sub(hD).mul(inv2eps);
1905
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
1906
+ }
1907
+ );
1908
+ const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
1909
+ return {
1910
+ sampleElevation,
1911
+ sampleNormal,
1912
+ sampleTerrain,
1913
+ sampleValidity,
1914
+ evaluateElevation,
1915
+ evaluateNormal
1916
+ };
1917
+ }
1918
+
1919
+ const createTerrainSamplerTask = work.task((get, work) => {
1920
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1921
+ const spatialIndex = get(gpuSpatialIndexStorageTask);
1922
+ const uniforms = get(createUniformsTask);
1923
+ const elevationCallback = get(elevationFn);
1924
+ return work(
1925
+ () => createTerrainSampler({
1926
+ terrainFieldStorage,
1927
+ spatialIndex,
1928
+ uniforms,
1929
+ elevationCallback
1930
+ })
1931
+ );
1932
+ }).displayName("createTerrainSamplerTask");
1933
+
1651
1934
  const isSkirtVertex = tsl.Fn(([segments]) => {
1652
1935
  const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1653
1936
  const vIndex = tsl.int(tsl.vertexIndex);
@@ -1691,14 +1974,15 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1691
1974
  }
1692
1975
  function createTileElevation(terrainUniforms, terrainFieldStorage) {
1693
1976
  if (!terrainFieldStorage) return tsl.float(0);
1694
- const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1695
- return readElevationFieldAtPositionLocal(
1977
+ const innerSegs = terrainUniforms.uInnerTileSegments;
1978
+ const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1979
+ const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1980
+ return sampleTerrainFieldElevation(
1696
1981
  terrainFieldStorage,
1697
- edgeVertexCount,
1698
- tsl.positionLocal
1699
- )().mul(
1700
- terrainUniforms.uElevationScale
1701
- );
1982
+ u,
1983
+ v,
1984
+ tsl.int(tsl.instanceIndex)
1985
+ ).mul(terrainUniforms.uElevationScale);
1702
1986
  }
1703
1987
  function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1704
1988
  if (!terrainFieldStorage) return;
@@ -1707,7 +1991,12 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1707
1991
  const localVertexIndex = tsl.int(tsl.vertexIndex);
1708
1992
  const ix = localVertexIndex.mod(edgeVertexCount);
1709
1993
  const iy = localVertexIndex.div(edgeVertexCount);
1710
- const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1994
+ const normalXZ = loadTerrainFieldNormal(
1995
+ terrainFieldStorage,
1996
+ ix,
1997
+ iy,
1998
+ nodeIndex
1999
+ );
1711
2000
  const nx = normalXZ.x;
1712
2001
  const nz = normalXZ.y;
1713
2002
  const nySq = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0));
@@ -1715,10 +2004,16 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1715
2004
  tsl.normalLocal.assign(tsl.vec3(nx, ny, nz));
1716
2005
  }
1717
2006
  function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1718
- const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
2007
+ const baseWorldPosition = createTileBaseWorldPosition(
2008
+ leafStorage,
2009
+ terrainUniforms
2010
+ );
1719
2011
  return tsl.Fn(() => {
1720
2012
  const base = baseWorldPosition();
1721
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
2013
+ const yElevation = createTileElevation(
2014
+ terrainUniforms,
2015
+ terrainFieldStorage
2016
+ );
1722
2017
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1723
2018
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1724
2019
  const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
@@ -1741,7 +2036,7 @@ const positionNodeTask = work.task((get, work) => {
1741
2036
  }).displayName("positionNodeTask");
1742
2037
 
1743
2038
  function terrainGraph() {
1744
- return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask);
2039
+ return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(gpuSpatialIndexStorageTask).add(gpuSpatialIndexUploadTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(createTerrainSamplerTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask);
1745
2040
  }
1746
2041
  const terrainTasks = {
1747
2042
  instanceId: instanceIdTask,
@@ -1750,12 +2045,15 @@ const terrainTasks = {
1750
2045
  leafStorage: leafStorageTask,
1751
2046
  surface: surfaceTask,
1752
2047
  leafGpuBuffer: leafGpuBufferTask,
2048
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2049
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
1753
2050
  createUniforms: createUniformsTask,
1754
2051
  updateUniforms: updateUniformsTask,
1755
2052
  positionNode: positionNodeTask,
1756
2053
  createElevationFieldContext: createElevationFieldContextTask,
1757
2054
  createTileNodes: tileNodesTask,
1758
2055
  createTerrainFieldTexture: createTerrainFieldTextureTask,
2056
+ createTerrainSampler: createTerrainSamplerTask,
1759
2057
  elevationFieldStage: elevationFieldStageTask,
1760
2058
  terrainFieldStage: terrainFieldStageTask,
1761
2059
  compileCompute: compileComputeTask,
@@ -1837,6 +2135,8 @@ exports.createSpatialIndex = createSpatialIndex;
1837
2135
  exports.createState = createState;
1838
2136
  exports.createTerrainFieldStorage = createTerrainFieldStorage;
1839
2137
  exports.createTerrainFieldTextureTask = createTerrainFieldTextureTask;
2138
+ exports.createTerrainSampler = createTerrainSampler;
2139
+ exports.createTerrainSamplerTask = createTerrainSamplerTask;
1840
2140
  exports.createTerrainUniforms = createTerrainUniforms;
1841
2141
  exports.createUniformsTask = createUniformsTask;
1842
2142
  exports.deriveNormalZ = deriveNormalZ;
@@ -1845,6 +2145,8 @@ exports.elevationFn = elevationFn;
1845
2145
  exports.elevationScale = elevationScale;
1846
2146
  exports.executeComputeTask = executeComputeTask;
1847
2147
  exports.getDeviceComputeLimits = getDeviceComputeLimits;
2148
+ exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
2149
+ exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
1848
2150
  exports.innerTileSegments = innerTileSegments;
1849
2151
  exports.instanceIdTask = instanceIdTask;
1850
2152
  exports.isSkirtUV = isSkirtUV;
@@ -1865,10 +2167,14 @@ exports.quadtreeUpdateTask = quadtreeUpdateTask;
1865
2167
  exports.resetLeafSet = resetLeafSet;
1866
2168
  exports.resetSeamTable = resetSeamTable;
1867
2169
  exports.rootSize = rootSize;
2170
+ exports.sampleTerrainField = sampleTerrainField;
2171
+ exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
2172
+ exports.sampleTerrainFieldNormal = sampleTerrainFieldNormal;
1868
2173
  exports.skirtScale = skirtScale;
1869
2174
  exports.storeTerrainField = storeTerrainField;
1870
2175
  exports.surface = surface;
1871
2176
  exports.surfaceTask = surfaceTask;
2177
+ exports.terrainFieldFilter = terrainFieldFilter;
1872
2178
  exports.terrainFieldStageTask = terrainFieldStageTask;
1873
2179
  exports.terrainGraph = terrainGraph;
1874
2180
  exports.terrainTasks = terrainTasks;