@hello-terrain/three 0.0.0-alpha.8 → 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
@@ -647,25 +647,6 @@ const createElevation = (tile, uniforms, elevationFn) => {
647
647
  });
648
648
  };
649
649
  };
650
- const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => tsl.Fn(() => {
651
- const nodeIndex = tsl.int(tsl.instanceIndex);
652
- const intEdge = tsl.int(edgeVertexCount);
653
- const innerSegments = tsl.int(edgeVertexCount).sub(3);
654
- const fInnerSegments = tsl.float(innerSegments);
655
- const last = intEdge.sub(tsl.int(1));
656
- const u = positionLocal.x.add(tsl.float(0.5));
657
- const v = positionLocal.z.add(tsl.float(0.5));
658
- const x = u.mul(fInnerSegments).round().toInt().add(tsl.int(1));
659
- const y = v.mul(fInnerSegments).round().toInt().add(tsl.int(1));
660
- const xClamped = tsl.min(tsl.max(x, tsl.int(0)), last);
661
- const yClamped = tsl.min(tsl.max(y, tsl.int(0)), last);
662
- return loadTerrainFieldElevation(
663
- terrainFieldStorage,
664
- xClamped,
665
- yClamped,
666
- nodeIndex
667
- );
668
- });
669
650
 
670
651
  function createTileCompute(leafStorage, uniforms) {
671
652
  const tileLevel = tsl.Fn(([nodeIndex]) => {
@@ -732,6 +713,10 @@ function createTileCompute(leafStorage, uniforms) {
732
713
  tileVertexWorldPositionCompute
733
714
  };
734
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
+ }
735
720
 
736
721
  const rootSize = work.param(256).displayName("rootSize");
737
722
  const origin = work.param({
@@ -848,12 +833,12 @@ function ensureChildren(store, parentId) {
848
833
  return childBase;
849
834
  }
850
835
 
851
- function nextPow2(n) {
836
+ function nextPow2$1(n) {
852
837
  let x = 1;
853
838
  while (x < n) x <<= 1;
854
839
  return x;
855
840
  }
856
- function mix32(x) {
841
+ function mix32$1(x) {
857
842
  x >>>= 0;
858
843
  x ^= x >>> 16;
859
844
  x = Math.imul(x, 2146121005) >>> 0;
@@ -862,12 +847,12 @@ function mix32(x) {
862
847
  x ^= x >>> 16;
863
848
  return x >>> 0;
864
849
  }
865
- function hashKey(space, level, x, y) {
866
- const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
867
- 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);
868
853
  }
869
854
  function createSpatialIndex(maxEntries) {
870
- const size = nextPow2(Math.max(2, maxEntries * 2));
855
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
871
856
  return {
872
857
  size,
873
858
  mask: size - 1,
@@ -892,7 +877,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
892
877
  const l = level & 255;
893
878
  const xx = x >>> 0;
894
879
  const yy = y >>> 0;
895
- let slot = hashKey(s, l, xx, yy) & index.mask;
880
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
896
881
  for (let probes = 0; probes < index.size; probes++) {
897
882
  if (index.stamp[slot] !== index.stampGen) {
898
883
  index.stamp[slot] = index.stampGen;
@@ -916,7 +901,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
916
901
  const l = level & 255;
917
902
  const xx = x >>> 0;
918
903
  const yy = y >>> 0;
919
- let slot = hashKey(s, l, xx, yy) & index.mask;
904
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
920
905
  for (let probes = 0; probes < index.size; probes++) {
921
906
  if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
922
907
  if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
@@ -1677,6 +1662,275 @@ function createComputePipelineTasks(leafStageTask) {
1677
1662
  return { compile, execute };
1678
1663
  }
1679
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
+
1680
1934
  const isSkirtVertex = tsl.Fn(([segments]) => {
1681
1935
  const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1682
1936
  const vIndex = tsl.int(tsl.vertexIndex);
@@ -1720,14 +1974,15 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1720
1974
  }
1721
1975
  function createTileElevation(terrainUniforms, terrainFieldStorage) {
1722
1976
  if (!terrainFieldStorage) return tsl.float(0);
1723
- const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1724
- 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(
1725
1981
  terrainFieldStorage,
1726
- edgeVertexCount,
1727
- tsl.positionLocal
1728
- )().mul(
1729
- terrainUniforms.uElevationScale
1730
- );
1982
+ u,
1983
+ v,
1984
+ tsl.int(tsl.instanceIndex)
1985
+ ).mul(terrainUniforms.uElevationScale);
1731
1986
  }
1732
1987
  function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1733
1988
  if (!terrainFieldStorage) return;
@@ -1736,7 +1991,12 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1736
1991
  const localVertexIndex = tsl.int(tsl.vertexIndex);
1737
1992
  const ix = localVertexIndex.mod(edgeVertexCount);
1738
1993
  const iy = localVertexIndex.div(edgeVertexCount);
1739
- const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1994
+ const normalXZ = loadTerrainFieldNormal(
1995
+ terrainFieldStorage,
1996
+ ix,
1997
+ iy,
1998
+ nodeIndex
1999
+ );
1740
2000
  const nx = normalXZ.x;
1741
2001
  const nz = normalXZ.y;
1742
2002
  const nySq = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0));
@@ -1744,10 +2004,16 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1744
2004
  tsl.normalLocal.assign(tsl.vec3(nx, ny, nz));
1745
2005
  }
1746
2006
  function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1747
- const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
2007
+ const baseWorldPosition = createTileBaseWorldPosition(
2008
+ leafStorage,
2009
+ terrainUniforms
2010
+ );
1748
2011
  return tsl.Fn(() => {
1749
2012
  const base = baseWorldPosition();
1750
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
2013
+ const yElevation = createTileElevation(
2014
+ terrainUniforms,
2015
+ terrainFieldStorage
2016
+ );
1751
2017
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1752
2018
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1753
2019
  const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
@@ -1770,7 +2036,7 @@ const positionNodeTask = work.task((get, work) => {
1770
2036
  }).displayName("positionNodeTask");
1771
2037
 
1772
2038
  function terrainGraph() {
1773
- 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);
1774
2040
  }
1775
2041
  const terrainTasks = {
1776
2042
  instanceId: instanceIdTask,
@@ -1779,12 +2045,15 @@ const terrainTasks = {
1779
2045
  leafStorage: leafStorageTask,
1780
2046
  surface: surfaceTask,
1781
2047
  leafGpuBuffer: leafGpuBufferTask,
2048
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2049
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
1782
2050
  createUniforms: createUniformsTask,
1783
2051
  updateUniforms: updateUniformsTask,
1784
2052
  positionNode: positionNodeTask,
1785
2053
  createElevationFieldContext: createElevationFieldContextTask,
1786
2054
  createTileNodes: tileNodesTask,
1787
2055
  createTerrainFieldTexture: createTerrainFieldTextureTask,
2056
+ createTerrainSampler: createTerrainSamplerTask,
1788
2057
  elevationFieldStage: elevationFieldStageTask,
1789
2058
  terrainFieldStage: terrainFieldStageTask,
1790
2059
  compileCompute: compileComputeTask,
@@ -1866,6 +2135,8 @@ exports.createSpatialIndex = createSpatialIndex;
1866
2135
  exports.createState = createState;
1867
2136
  exports.createTerrainFieldStorage = createTerrainFieldStorage;
1868
2137
  exports.createTerrainFieldTextureTask = createTerrainFieldTextureTask;
2138
+ exports.createTerrainSampler = createTerrainSampler;
2139
+ exports.createTerrainSamplerTask = createTerrainSamplerTask;
1869
2140
  exports.createTerrainUniforms = createTerrainUniforms;
1870
2141
  exports.createUniformsTask = createUniformsTask;
1871
2142
  exports.deriveNormalZ = deriveNormalZ;
@@ -1874,6 +2145,8 @@ exports.elevationFn = elevationFn;
1874
2145
  exports.elevationScale = elevationScale;
1875
2146
  exports.executeComputeTask = executeComputeTask;
1876
2147
  exports.getDeviceComputeLimits = getDeviceComputeLimits;
2148
+ exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
2149
+ exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
1877
2150
  exports.innerTileSegments = innerTileSegments;
1878
2151
  exports.instanceIdTask = instanceIdTask;
1879
2152
  exports.isSkirtUV = isSkirtUV;
package/dist/index.d.cts CHANGED
@@ -458,6 +458,41 @@ declare function createTileCompute(leafStorage: LeafStorageState, uniforms: Terr
458
458
  tileVertexWorldPositionCompute: three_src_nodes_TSL_js.ShaderNodeFn<[number | Node, number | Node, number | Node]>;
459
459
  };
460
460
 
461
+ interface ElevationParams {
462
+ worldPosition: Node$1;
463
+ rootSize: Node$1;
464
+ rootUV: Node$1;
465
+ tileUV: Node$1;
466
+ tileLevel: Node$1;
467
+ tileSize: Node$1;
468
+ tileOriginVec2: Node$1;
469
+ nodeIndex: Node$1;
470
+ }
471
+ type ElevationCallback = (params: ElevationParams) => Node$1;
472
+
473
+ interface GpuSpatialIndexContext {
474
+ data: Uint32Array<ArrayBuffer>;
475
+ size: number;
476
+ mask: number;
477
+ stampGen: UniformNode<number>;
478
+ attribute: StorageBufferAttribute;
479
+ node: StorageBufferNode;
480
+ }
481
+ interface TerrainSampler {
482
+ sampleElevation: (worldX: Node, worldZ: Node) => Node;
483
+ sampleNormal: (worldX: Node, worldZ: Node) => Node;
484
+ sampleTerrain: (worldX: Node, worldZ: Node) => Node;
485
+ sampleValidity: (worldX: Node, worldZ: Node) => Node;
486
+ evaluateElevation: (worldX: Node, worldZ: Node) => Node;
487
+ evaluateNormal: (worldX: Node, worldZ: Node, epsilon?: Node) => Node;
488
+ }
489
+ interface CreateTerrainSamplerParams {
490
+ terrainFieldStorage: TerrainFieldStorage;
491
+ spatialIndex: GpuSpatialIndexContext;
492
+ uniforms: TerrainUniformsContext;
493
+ elevationCallback: ElevationCallback;
494
+ }
495
+
461
496
  interface QuadtreeConfigState {
462
497
  state: QuadtreeState;
463
498
  surface: Surface;
@@ -478,12 +513,15 @@ interface TerrainTasks {
478
513
  surface: TaskRef<Surface>;
479
514
  leafStorage: TaskRef<LeafStorageState>;
480
515
  leafGpuBuffer: TaskRef<LeafGpuBufferState>;
516
+ gpuSpatialIndexStorage: TaskRef<GpuSpatialIndexContext>;
517
+ gpuSpatialIndexUpload: TaskRef<GpuSpatialIndexContext>;
481
518
  createUniforms: TaskRef<TerrainUniformsContext>;
482
519
  updateUniforms: TaskRef<TerrainUniformsContext>;
483
520
  positionNode: TaskRef<ShaderCallNodeInternal>;
484
521
  createElevationFieldContext: TaskRef<ElevationFieldContext>;
485
522
  createTileNodes: TaskRef<ReturnType<typeof createTileCompute>>;
486
523
  createTerrainFieldTexture: TaskRef<TerrainFieldStorage>;
524
+ createTerrainSampler: TaskRef<TerrainSampler>;
487
525
  elevationFieldStage: TaskRef<ComputePipeline>;
488
526
  terrainFieldStage: TaskRef<ComputePipeline>;
489
527
  compileCompute: TaskRef<{
@@ -513,6 +551,9 @@ declare const tileNodesTask: _hello_terrain_work.Task<{
513
551
  */
514
552
  declare const elevationFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
515
553
 
554
+ declare const gpuSpatialIndexStorageTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
555
+ declare const gpuSpatialIndexUploadTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
556
+
516
557
  /** Generates a unique instance ID per graph (cached once). */
517
558
  declare const instanceIdTask: _hello_terrain_work.Task<`${string}-${string}-${string}-${string}-${string}`, string, unknown>;
518
559
 
@@ -529,17 +570,7 @@ declare const createTerrainFieldTextureTask: _hello_terrain_work.Task<any, strin
529
570
  */
530
571
  declare const terrainFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
531
572
 
532
- interface ElevationParams {
533
- worldPosition: Node$1;
534
- rootSize: Node$1;
535
- rootUV: Node$1;
536
- tileUV: Node$1;
537
- tileLevel: Node$1;
538
- tileSize: Node$1;
539
- tileOriginVec2: Node$1;
540
- nodeIndex: Node$1;
541
- }
542
- type ElevationCallback = (params: ElevationParams) => Node$1;
573
+ declare const createTerrainSamplerTask: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
543
574
 
544
575
  /** Root tile size in world units. */
545
576
  declare const rootSize: _hello_terrain_work.ParamRef<number>;
@@ -644,6 +675,8 @@ declare const terrainTasks: {
644
675
  attribute: three_webgpu.StorageBufferAttribute;
645
676
  node: three_webgpu.StorageBufferNode;
646
677
  }, string, unknown>;
678
+ readonly gpuSpatialIndexStorage: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
679
+ readonly gpuSpatialIndexUpload: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
647
680
  readonly createUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
648
681
  readonly updateUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
649
682
  readonly positionNode: _hello_terrain_work.Task<three_src_nodes_TSL_js.ShaderCallNodeInternal, string, unknown>;
@@ -662,6 +695,7 @@ declare const terrainTasks: {
662
695
  readonly createTerrainFieldTexture: _hello_terrain_work.Task<any, string, {
663
696
  renderer: WebGPURenderer;
664
697
  }>;
698
+ readonly createTerrainSampler: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
665
699
  readonly elevationFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
666
700
  readonly terrainFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
667
701
  readonly compileCompute: _hello_terrain_work.Task<{
@@ -679,6 +713,8 @@ type ComputeDeviceLimits = {
679
713
  };
680
714
  declare function getDeviceComputeLimits(renderer: WebGPURenderer): ComputeDeviceLimits;
681
715
 
716
+ declare function createTerrainSampler(params: CreateTerrainSamplerParams): TerrainSampler;
717
+
682
718
  /**
683
719
  * Maps a value or node from texture space [0, 1] to vector space [-1, 1].
684
720
  *
@@ -751,5 +787,5 @@ declare const voronoiCells: three_src_nodes_TSL_js.ShaderNodeFn<[three_tsl.Proxi
751
787
  uv: Node;
752
788
  }>]>;
753
789
 
754
- export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
755
- export type { ComputePipeline, ComputeStageCallback, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
790
+ export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainSampler, createTerrainSamplerTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
791
+ export type { ComputePipeline, ComputeStageCallback, CreateTerrainSamplerParams, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, GpuSpatialIndexContext, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainSampler, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
package/dist/index.d.mts CHANGED
@@ -458,6 +458,41 @@ declare function createTileCompute(leafStorage: LeafStorageState, uniforms: Terr
458
458
  tileVertexWorldPositionCompute: three_src_nodes_TSL_js.ShaderNodeFn<[number | Node, number | Node, number | Node]>;
459
459
  };
460
460
 
461
+ interface ElevationParams {
462
+ worldPosition: Node$1;
463
+ rootSize: Node$1;
464
+ rootUV: Node$1;
465
+ tileUV: Node$1;
466
+ tileLevel: Node$1;
467
+ tileSize: Node$1;
468
+ tileOriginVec2: Node$1;
469
+ nodeIndex: Node$1;
470
+ }
471
+ type ElevationCallback = (params: ElevationParams) => Node$1;
472
+
473
+ interface GpuSpatialIndexContext {
474
+ data: Uint32Array<ArrayBuffer>;
475
+ size: number;
476
+ mask: number;
477
+ stampGen: UniformNode<number>;
478
+ attribute: StorageBufferAttribute;
479
+ node: StorageBufferNode;
480
+ }
481
+ interface TerrainSampler {
482
+ sampleElevation: (worldX: Node, worldZ: Node) => Node;
483
+ sampleNormal: (worldX: Node, worldZ: Node) => Node;
484
+ sampleTerrain: (worldX: Node, worldZ: Node) => Node;
485
+ sampleValidity: (worldX: Node, worldZ: Node) => Node;
486
+ evaluateElevation: (worldX: Node, worldZ: Node) => Node;
487
+ evaluateNormal: (worldX: Node, worldZ: Node, epsilon?: Node) => Node;
488
+ }
489
+ interface CreateTerrainSamplerParams {
490
+ terrainFieldStorage: TerrainFieldStorage;
491
+ spatialIndex: GpuSpatialIndexContext;
492
+ uniforms: TerrainUniformsContext;
493
+ elevationCallback: ElevationCallback;
494
+ }
495
+
461
496
  interface QuadtreeConfigState {
462
497
  state: QuadtreeState;
463
498
  surface: Surface;
@@ -478,12 +513,15 @@ interface TerrainTasks {
478
513
  surface: TaskRef<Surface>;
479
514
  leafStorage: TaskRef<LeafStorageState>;
480
515
  leafGpuBuffer: TaskRef<LeafGpuBufferState>;
516
+ gpuSpatialIndexStorage: TaskRef<GpuSpatialIndexContext>;
517
+ gpuSpatialIndexUpload: TaskRef<GpuSpatialIndexContext>;
481
518
  createUniforms: TaskRef<TerrainUniformsContext>;
482
519
  updateUniforms: TaskRef<TerrainUniformsContext>;
483
520
  positionNode: TaskRef<ShaderCallNodeInternal>;
484
521
  createElevationFieldContext: TaskRef<ElevationFieldContext>;
485
522
  createTileNodes: TaskRef<ReturnType<typeof createTileCompute>>;
486
523
  createTerrainFieldTexture: TaskRef<TerrainFieldStorage>;
524
+ createTerrainSampler: TaskRef<TerrainSampler>;
487
525
  elevationFieldStage: TaskRef<ComputePipeline>;
488
526
  terrainFieldStage: TaskRef<ComputePipeline>;
489
527
  compileCompute: TaskRef<{
@@ -513,6 +551,9 @@ declare const tileNodesTask: _hello_terrain_work.Task<{
513
551
  */
514
552
  declare const elevationFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
515
553
 
554
+ declare const gpuSpatialIndexStorageTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
555
+ declare const gpuSpatialIndexUploadTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
556
+
516
557
  /** Generates a unique instance ID per graph (cached once). */
517
558
  declare const instanceIdTask: _hello_terrain_work.Task<`${string}-${string}-${string}-${string}-${string}`, string, unknown>;
518
559
 
@@ -529,17 +570,7 @@ declare const createTerrainFieldTextureTask: _hello_terrain_work.Task<any, strin
529
570
  */
530
571
  declare const terrainFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
531
572
 
532
- interface ElevationParams {
533
- worldPosition: Node$1;
534
- rootSize: Node$1;
535
- rootUV: Node$1;
536
- tileUV: Node$1;
537
- tileLevel: Node$1;
538
- tileSize: Node$1;
539
- tileOriginVec2: Node$1;
540
- nodeIndex: Node$1;
541
- }
542
- type ElevationCallback = (params: ElevationParams) => Node$1;
573
+ declare const createTerrainSamplerTask: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
543
574
 
544
575
  /** Root tile size in world units. */
545
576
  declare const rootSize: _hello_terrain_work.ParamRef<number>;
@@ -644,6 +675,8 @@ declare const terrainTasks: {
644
675
  attribute: three_webgpu.StorageBufferAttribute;
645
676
  node: three_webgpu.StorageBufferNode;
646
677
  }, string, unknown>;
678
+ readonly gpuSpatialIndexStorage: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
679
+ readonly gpuSpatialIndexUpload: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
647
680
  readonly createUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
648
681
  readonly updateUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
649
682
  readonly positionNode: _hello_terrain_work.Task<three_src_nodes_TSL_js.ShaderCallNodeInternal, string, unknown>;
@@ -662,6 +695,7 @@ declare const terrainTasks: {
662
695
  readonly createTerrainFieldTexture: _hello_terrain_work.Task<any, string, {
663
696
  renderer: WebGPURenderer;
664
697
  }>;
698
+ readonly createTerrainSampler: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
665
699
  readonly elevationFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
666
700
  readonly terrainFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
667
701
  readonly compileCompute: _hello_terrain_work.Task<{
@@ -679,6 +713,8 @@ type ComputeDeviceLimits = {
679
713
  };
680
714
  declare function getDeviceComputeLimits(renderer: WebGPURenderer): ComputeDeviceLimits;
681
715
 
716
+ declare function createTerrainSampler(params: CreateTerrainSamplerParams): TerrainSampler;
717
+
682
718
  /**
683
719
  * Maps a value or node from texture space [0, 1] to vector space [-1, 1].
684
720
  *
@@ -751,5 +787,5 @@ declare const voronoiCells: three_src_nodes_TSL_js.ShaderNodeFn<[three_tsl.Proxi
751
787
  uv: Node;
752
788
  }>]>;
753
789
 
754
- export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
755
- export type { ComputePipeline, ComputeStageCallback, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
790
+ export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainSampler, createTerrainSamplerTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
791
+ export type { ComputePipeline, ComputeStageCallback, CreateTerrainSamplerParams, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, GpuSpatialIndexContext, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainSampler, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
package/dist/index.d.ts CHANGED
@@ -458,6 +458,41 @@ declare function createTileCompute(leafStorage: LeafStorageState, uniforms: Terr
458
458
  tileVertexWorldPositionCompute: three_src_nodes_TSL_js.ShaderNodeFn<[number | Node, number | Node, number | Node]>;
459
459
  };
460
460
 
461
+ interface ElevationParams {
462
+ worldPosition: Node$1;
463
+ rootSize: Node$1;
464
+ rootUV: Node$1;
465
+ tileUV: Node$1;
466
+ tileLevel: Node$1;
467
+ tileSize: Node$1;
468
+ tileOriginVec2: Node$1;
469
+ nodeIndex: Node$1;
470
+ }
471
+ type ElevationCallback = (params: ElevationParams) => Node$1;
472
+
473
+ interface GpuSpatialIndexContext {
474
+ data: Uint32Array<ArrayBuffer>;
475
+ size: number;
476
+ mask: number;
477
+ stampGen: UniformNode<number>;
478
+ attribute: StorageBufferAttribute;
479
+ node: StorageBufferNode;
480
+ }
481
+ interface TerrainSampler {
482
+ sampleElevation: (worldX: Node, worldZ: Node) => Node;
483
+ sampleNormal: (worldX: Node, worldZ: Node) => Node;
484
+ sampleTerrain: (worldX: Node, worldZ: Node) => Node;
485
+ sampleValidity: (worldX: Node, worldZ: Node) => Node;
486
+ evaluateElevation: (worldX: Node, worldZ: Node) => Node;
487
+ evaluateNormal: (worldX: Node, worldZ: Node, epsilon?: Node) => Node;
488
+ }
489
+ interface CreateTerrainSamplerParams {
490
+ terrainFieldStorage: TerrainFieldStorage;
491
+ spatialIndex: GpuSpatialIndexContext;
492
+ uniforms: TerrainUniformsContext;
493
+ elevationCallback: ElevationCallback;
494
+ }
495
+
461
496
  interface QuadtreeConfigState {
462
497
  state: QuadtreeState;
463
498
  surface: Surface;
@@ -478,12 +513,15 @@ interface TerrainTasks {
478
513
  surface: TaskRef<Surface>;
479
514
  leafStorage: TaskRef<LeafStorageState>;
480
515
  leafGpuBuffer: TaskRef<LeafGpuBufferState>;
516
+ gpuSpatialIndexStorage: TaskRef<GpuSpatialIndexContext>;
517
+ gpuSpatialIndexUpload: TaskRef<GpuSpatialIndexContext>;
481
518
  createUniforms: TaskRef<TerrainUniformsContext>;
482
519
  updateUniforms: TaskRef<TerrainUniformsContext>;
483
520
  positionNode: TaskRef<ShaderCallNodeInternal>;
484
521
  createElevationFieldContext: TaskRef<ElevationFieldContext>;
485
522
  createTileNodes: TaskRef<ReturnType<typeof createTileCompute>>;
486
523
  createTerrainFieldTexture: TaskRef<TerrainFieldStorage>;
524
+ createTerrainSampler: TaskRef<TerrainSampler>;
487
525
  elevationFieldStage: TaskRef<ComputePipeline>;
488
526
  terrainFieldStage: TaskRef<ComputePipeline>;
489
527
  compileCompute: TaskRef<{
@@ -513,6 +551,9 @@ declare const tileNodesTask: _hello_terrain_work.Task<{
513
551
  */
514
552
  declare const elevationFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
515
553
 
554
+ declare const gpuSpatialIndexStorageTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
555
+ declare const gpuSpatialIndexUploadTask: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
556
+
516
557
  /** Generates a unique instance ID per graph (cached once). */
517
558
  declare const instanceIdTask: _hello_terrain_work.Task<`${string}-${string}-${string}-${string}-${string}`, string, unknown>;
518
559
 
@@ -529,17 +570,7 @@ declare const createTerrainFieldTextureTask: _hello_terrain_work.Task<any, strin
529
570
  */
530
571
  declare const terrainFieldStageTask: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
531
572
 
532
- interface ElevationParams {
533
- worldPosition: Node$1;
534
- rootSize: Node$1;
535
- rootUV: Node$1;
536
- tileUV: Node$1;
537
- tileLevel: Node$1;
538
- tileSize: Node$1;
539
- tileOriginVec2: Node$1;
540
- nodeIndex: Node$1;
541
- }
542
- type ElevationCallback = (params: ElevationParams) => Node$1;
573
+ declare const createTerrainSamplerTask: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
543
574
 
544
575
  /** Root tile size in world units. */
545
576
  declare const rootSize: _hello_terrain_work.ParamRef<number>;
@@ -644,6 +675,8 @@ declare const terrainTasks: {
644
675
  attribute: three_webgpu.StorageBufferAttribute;
645
676
  node: three_webgpu.StorageBufferNode;
646
677
  }, string, unknown>;
678
+ readonly gpuSpatialIndexStorage: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
679
+ readonly gpuSpatialIndexUpload: _hello_terrain_work.Task<GpuSpatialIndexContext, string, unknown>;
647
680
  readonly createUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
648
681
  readonly updateUniforms: _hello_terrain_work.Task<TerrainUniformsContext, string, unknown>;
649
682
  readonly positionNode: _hello_terrain_work.Task<three_src_nodes_TSL_js.ShaderCallNodeInternal, string, unknown>;
@@ -662,6 +695,7 @@ declare const terrainTasks: {
662
695
  readonly createTerrainFieldTexture: _hello_terrain_work.Task<any, string, {
663
696
  renderer: WebGPURenderer;
664
697
  }>;
698
+ readonly createTerrainSampler: _hello_terrain_work.Task<TerrainSampler, string, unknown>;
665
699
  readonly elevationFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
666
700
  readonly terrainFieldStage: _hello_terrain_work.Task<ComputePipeline, string, unknown>;
667
701
  readonly compileCompute: _hello_terrain_work.Task<{
@@ -679,6 +713,8 @@ type ComputeDeviceLimits = {
679
713
  };
680
714
  declare function getDeviceComputeLimits(renderer: WebGPURenderer): ComputeDeviceLimits;
681
715
 
716
+ declare function createTerrainSampler(params: CreateTerrainSamplerParams): TerrainSampler;
717
+
682
718
  /**
683
719
  * Maps a value or node from texture space [0, 1] to vector space [-1, 1].
684
720
  *
@@ -751,5 +787,5 @@ declare const voronoiCells: three_src_nodes_TSL_js.ShaderNodeFn<[three_tsl.Proxi
751
787
  uv: Node;
752
788
  }>]>;
753
789
 
754
- export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
755
- export type { ComputePipeline, ComputeStageCallback, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
790
+ export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainSampler, createTerrainSamplerTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
791
+ export type { ComputePipeline, ComputeStageCallback, CreateTerrainSamplerParams, CubeSphereSurfaceConfig, ElevationCallback, ElevationFieldContext, ElevationParams, FlatSurfaceConfig, GpuSpatialIndexContext, InfiniteFlatSurfaceConfig, IntNodeInput, LeafGpuBufferState, LeafSet, LeafStorageState, LodMode, QuadtreeConfig, QuadtreeConfigState, QuadtreeState, SeamTable, SpatialIndex, Surface, TerrainFieldStorage, TerrainFieldStorageBackendType, TerrainFieldStorageFormat, TerrainFieldStorageOptions, TerrainGraph, TerrainSampler, TerrainTasks, TerrainUniformsContext, TerrainUniformsParams, TileBounds, TileId, UpdateParams };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 as Vector3$1 } from 'three';
2
2
  import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 } from 'three/webgpu';
3
3
  import { param, task, graph } from '@hello-terrain/work';
4
- import { uniform, Fn, float, globalId, int, vec2, uint, If, workgroupBarrier, textureStore, uvec3, vec4, texture, ivec2, ivec3, textureLoad, instanceIndex, min, max, pow, vec3, storage, vertexIndex, uv, select, positionLocal, normalLocal, remap, dot, varyingProperty, mx_noise_float, Loop, mix } from 'three/tsl';
4
+ import { uniform, Fn, float, globalId, int, vec2, uint, If, workgroupBarrier, textureStore, uvec3, vec4, texture, ivec2, ivec3, textureLoad, pow, vec3, storage, Loop, Break, vertexIndex, uv, select, instanceIndex, positionLocal, normalLocal, remap, dot, varyingProperty, mx_noise_float, mix } from 'three/tsl';
5
5
  import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
6
6
 
7
7
  class TerrainGeometry extends BufferGeometry {
@@ -645,25 +645,6 @@ const createElevation = (tile, uniforms, elevationFn) => {
645
645
  });
646
646
  };
647
647
  };
648
- const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => Fn(() => {
649
- const nodeIndex = int(instanceIndex);
650
- const intEdge = int(edgeVertexCount);
651
- const innerSegments = int(edgeVertexCount).sub(3);
652
- const fInnerSegments = float(innerSegments);
653
- const last = intEdge.sub(int(1));
654
- const u = positionLocal.x.add(float(0.5));
655
- const v = positionLocal.z.add(float(0.5));
656
- const x = u.mul(fInnerSegments).round().toInt().add(int(1));
657
- const y = v.mul(fInnerSegments).round().toInt().add(int(1));
658
- const xClamped = min(max(x, int(0)), last);
659
- const yClamped = min(max(y, int(0)), last);
660
- return loadTerrainFieldElevation(
661
- terrainFieldStorage,
662
- xClamped,
663
- yClamped,
664
- nodeIndex
665
- );
666
- });
667
648
 
668
649
  function createTileCompute(leafStorage, uniforms) {
669
650
  const tileLevel = Fn(([nodeIndex]) => {
@@ -730,6 +711,10 @@ function createTileCompute(leafStorage, uniforms) {
730
711
  tileVertexWorldPositionCompute
731
712
  };
732
713
  }
714
+ function tileLocalToFieldUV(localCoord, innerSegments) {
715
+ const edge = float(innerSegments).add(float(3));
716
+ return float(localCoord).mul(float(innerSegments)).add(float(1.5)).div(edge);
717
+ }
733
718
 
734
719
  const rootSize = param(256).displayName("rootSize");
735
720
  const origin = param({
@@ -846,12 +831,12 @@ function ensureChildren(store, parentId) {
846
831
  return childBase;
847
832
  }
848
833
 
849
- function nextPow2(n) {
834
+ function nextPow2$1(n) {
850
835
  let x = 1;
851
836
  while (x < n) x <<= 1;
852
837
  return x;
853
838
  }
854
- function mix32(x) {
839
+ function mix32$1(x) {
855
840
  x >>>= 0;
856
841
  x ^= x >>> 16;
857
842
  x = Math.imul(x, 2146121005) >>> 0;
@@ -860,12 +845,12 @@ function mix32(x) {
860
845
  x ^= x >>> 16;
861
846
  return x >>> 0;
862
847
  }
863
- function hashKey(space, level, x, y) {
864
- const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
865
- return mix32(h);
848
+ function hashKey$1(space, level, x, y) {
849
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
850
+ return mix32$1(h);
866
851
  }
867
852
  function createSpatialIndex(maxEntries) {
868
- const size = nextPow2(Math.max(2, maxEntries * 2));
853
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
869
854
  return {
870
855
  size,
871
856
  mask: size - 1,
@@ -890,7 +875,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
890
875
  const l = level & 255;
891
876
  const xx = x >>> 0;
892
877
  const yy = y >>> 0;
893
- let slot = hashKey(s, l, xx, yy) & index.mask;
878
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
894
879
  for (let probes = 0; probes < index.size; probes++) {
895
880
  if (index.stamp[slot] !== index.stampGen) {
896
881
  index.stamp[slot] = index.stampGen;
@@ -914,7 +899,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
914
899
  const l = level & 255;
915
900
  const xx = x >>> 0;
916
901
  const yy = y >>> 0;
917
- let slot = hashKey(s, l, xx, yy) & index.mask;
902
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
918
903
  for (let probes = 0; probes < index.size; probes++) {
919
904
  if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
920
905
  if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
@@ -1675,6 +1660,275 @@ function createComputePipelineTasks(leafStageTask) {
1675
1660
  return { compile, execute };
1676
1661
  }
1677
1662
 
1663
+ const SLOT_STRIDE = 6;
1664
+ function nextPow2(n) {
1665
+ let x = 1;
1666
+ while (x < n) x <<= 1;
1667
+ return x;
1668
+ }
1669
+ function createGpuSpatialIndex(maxEntries) {
1670
+ const size = nextPow2(Math.max(2, maxEntries * 2));
1671
+ const data = new Uint32Array(size * SLOT_STRIDE);
1672
+ const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
1673
+ const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
1674
+ const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
1675
+ return {
1676
+ data,
1677
+ size,
1678
+ mask: size - 1,
1679
+ stampGen,
1680
+ attribute,
1681
+ node
1682
+ };
1683
+ }
1684
+ function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
1685
+ if (gpuIndex.size !== cpuIndex.size) {
1686
+ throw new Error(
1687
+ `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
1688
+ );
1689
+ }
1690
+ for (let i = 0; i < cpuIndex.size; i += 1) {
1691
+ const base = i * SLOT_STRIDE;
1692
+ gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
1693
+ gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
1694
+ gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
1695
+ gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
1696
+ gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
1697
+ gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
1698
+ }
1699
+ gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
1700
+ gpuIndex.attribute.needsUpdate = true;
1701
+ gpuIndex.node.needsUpdate = true;
1702
+ }
1703
+ function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
1704
+ const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
1705
+ return spatialIndex.node.element(offset).toUint();
1706
+ }
1707
+ const mix32 = Fn(([x]) => {
1708
+ const v = uint(x).toVar();
1709
+ v.assign(v.bitXor(v.shiftRight(uint(16))));
1710
+ v.assign(v.mul(uint(2146121005)));
1711
+ v.assign(v.bitXor(v.shiftRight(uint(15))));
1712
+ v.assign(v.mul(uint(2221713035)));
1713
+ v.assign(v.bitXor(v.shiftRight(uint(16))));
1714
+ return v;
1715
+ });
1716
+ const hashKey = Fn(([space, level, x, y]) => {
1717
+ const s = uint(space).bitAnd(uint(255));
1718
+ const l = uint(level).bitAnd(uint(255));
1719
+ const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32(uint(x))).bitXor(mix32(uint(y)));
1720
+ return mix32(h);
1721
+ });
1722
+ const createGpuSpatialLookup = (spatialIndex) => {
1723
+ const slotCount = spatialIndex.size;
1724
+ const mask = uint(spatialIndex.mask);
1725
+ const stampGen = spatialIndex.stampGen.toUint();
1726
+ const emptyValue = int(-1);
1727
+ return Fn(([space, level, x, y]) => {
1728
+ const s = uint(space).bitAnd(uint(255));
1729
+ const l = uint(level).bitAnd(uint(255));
1730
+ const xx = uint(x);
1731
+ const yy = uint(y);
1732
+ const result = emptyValue.toVar();
1733
+ const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
1734
+ const probes = int(0).toVar();
1735
+ Loop(slotCount, () => {
1736
+ const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
1737
+ If(stamp.notEqual(stampGen), () => {
1738
+ Break();
1739
+ });
1740
+ const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
1741
+ const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
1742
+ const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
1743
+ const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
1744
+ If(
1745
+ ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
1746
+ () => {
1747
+ result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
1748
+ Break();
1749
+ }
1750
+ );
1751
+ slot.assign(slot.add(uint(1)).bitAnd(mask));
1752
+ probes.addAssign(1);
1753
+ });
1754
+ return result;
1755
+ });
1756
+ };
1757
+ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
1758
+ const lookup = createGpuSpatialLookup(spatialIndex);
1759
+ const levelCount = Math.max(1, maxLevel + 1);
1760
+ return Fn(([worldX, worldZ]) => {
1761
+ const rootOrigin = uniforms.uRootOrigin.toVar();
1762
+ const rootSize = uniforms.uRootSize.toVar();
1763
+ const halfRoot = rootSize.mul(float(0.5));
1764
+ const tileIndex = int(-1).toVar();
1765
+ const tileU = float(0).toVar();
1766
+ const tileV = float(0).toVar();
1767
+ const i = int(0).toVar();
1768
+ Loop(levelCount, () => {
1769
+ const level = int(maxLevel).sub(i).toVar();
1770
+ const scale = pow(float(2), level.toFloat());
1771
+ const tileSize = rootSize.div(scale);
1772
+ const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
1773
+ const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
1774
+ const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
1775
+ If(maybeIndex.greaterThanEqual(int(0)), () => {
1776
+ const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
1777
+ const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
1778
+ tileIndex.assign(maybeIndex);
1779
+ tileU.assign(worldX.sub(minX).div(tileSize));
1780
+ tileV.assign(worldZ.sub(minZ).div(tileSize));
1781
+ Break();
1782
+ });
1783
+ i.addAssign(1);
1784
+ });
1785
+ return vec3(tileIndex.toFloat(), tileU, tileV);
1786
+ });
1787
+ };
1788
+
1789
+ const gpuSpatialIndexStorageTask = task((get, work) => {
1790
+ const maxNodesValue = get(maxNodes);
1791
+ return work(() => createGpuSpatialIndex(maxNodesValue));
1792
+ }).displayName("gpuSpatialIndexStorageTask");
1793
+ const gpuSpatialIndexUploadTask = task((get, work) => {
1794
+ const quadtreeConfig = get(quadtreeConfigTask);
1795
+ get(quadtreeUpdateTask);
1796
+ const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
1797
+ return work(() => {
1798
+ uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
1799
+ return gpuSpatialIndex;
1800
+ });
1801
+ }).displayName("gpuSpatialIndexUploadTask");
1802
+
1803
+ function createTerrainSampleNode(params) {
1804
+ const tileLookup = createTileIndexFromWorldPosition(
1805
+ params.spatialIndex,
1806
+ params.uniforms,
1807
+ maxLevel.get()
1808
+ );
1809
+ return Fn(([worldX, worldZ]) => {
1810
+ const tileResult = tileLookup(worldX, worldZ).toVar();
1811
+ const tileIndex = int(tileResult.x).toVar();
1812
+ const safeTileIndex = tileIndex.max(int(0)).toVar();
1813
+ const u = tileResult.y.toVar();
1814
+ const v = tileResult.z.toVar();
1815
+ const fieldU = tileLocalToFieldUV(
1816
+ u,
1817
+ params.uniforms.uInnerTileSegments
1818
+ ).toVar();
1819
+ const fieldV = tileLocalToFieldUV(
1820
+ v,
1821
+ params.uniforms.uInnerTileSegments
1822
+ ).toVar();
1823
+ const found = tileIndex.greaterThanEqual(int(0)).toVar();
1824
+ const sampled = sampleTerrainField(
1825
+ params.terrainFieldStorage,
1826
+ fieldU,
1827
+ fieldV,
1828
+ safeTileIndex
1829
+ ).toVar();
1830
+ const nx = sampled.g.toVar();
1831
+ const nz = sampled.b.toVar();
1832
+ const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
1833
+ const valid = found.select(float(1), float(0)).toVar();
1834
+ return vec4(
1835
+ sampled.r.mul(valid),
1836
+ nx.mul(valid),
1837
+ ny.mul(valid),
1838
+ nz.mul(valid)
1839
+ );
1840
+ });
1841
+ }
1842
+ function createTerrainSampler(params) {
1843
+ const elevationNode = createElevationFunction(params.elevationCallback);
1844
+ const terrainSampleAt = createTerrainSampleNode(params);
1845
+ const evaluateElevationAt = Fn(([worldX, worldZ]) => {
1846
+ const rootOrigin = params.uniforms.uRootOrigin.toVar();
1847
+ const rootSize = params.uniforms.uRootSize.toVar();
1848
+ const centeredX = worldX.sub(rootOrigin.x);
1849
+ const centeredZ = worldZ.sub(rootOrigin.z);
1850
+ const rootUV = vec2(
1851
+ centeredX.div(rootSize).add(0.5),
1852
+ centeredZ.div(rootSize).mul(float(-1)).add(0.5)
1853
+ ).toVar();
1854
+ return elevationNode({
1855
+ worldPosition: vec3(worldX, rootOrigin.y, worldZ),
1856
+ rootSize,
1857
+ rootUV,
1858
+ tileUV: rootUV,
1859
+ tileLevel: int(0),
1860
+ tileSize: rootSize,
1861
+ tileOriginVec2: vec2(0, 0),
1862
+ nodeIndex: int(0)
1863
+ });
1864
+ });
1865
+ const sampleTerrain = Fn(
1866
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
1867
+ );
1868
+ const sampleElevation = Fn(
1869
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
1870
+ );
1871
+ const sampleNormal = Fn(
1872
+ ([worldX, worldZ]) => vec3(
1873
+ terrainSampleAt(worldX, worldZ).y,
1874
+ terrainSampleAt(worldX, worldZ).z,
1875
+ terrainSampleAt(worldX, worldZ).w
1876
+ )
1877
+ );
1878
+ const sampleValidity = Fn(
1879
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(float(0)).select(float(1), float(0))
1880
+ );
1881
+ const evaluateElevation = Fn(
1882
+ ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
1883
+ );
1884
+ const evaluateNormalNode = Fn(
1885
+ ([worldX, worldZ, epsilon]) => {
1886
+ const eps = epsilon ?? float(0.1);
1887
+ const elevationScale = params.uniforms.uElevationScale.toVar();
1888
+ const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
1889
+ elevationScale
1890
+ );
1891
+ const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
1892
+ elevationScale
1893
+ );
1894
+ const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
1895
+ elevationScale
1896
+ );
1897
+ const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
1898
+ elevationScale
1899
+ );
1900
+ const inv2eps = float(0.5).div(eps);
1901
+ const dhdx = hR.sub(hL).mul(inv2eps);
1902
+ const dhdz = hU.sub(hD).mul(inv2eps);
1903
+ return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
1904
+ }
1905
+ );
1906
+ const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
1907
+ return {
1908
+ sampleElevation,
1909
+ sampleNormal,
1910
+ sampleTerrain,
1911
+ sampleValidity,
1912
+ evaluateElevation,
1913
+ evaluateNormal
1914
+ };
1915
+ }
1916
+
1917
+ const createTerrainSamplerTask = task((get, work) => {
1918
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1919
+ const spatialIndex = get(gpuSpatialIndexStorageTask);
1920
+ const uniforms = get(createUniformsTask);
1921
+ const elevationCallback = get(elevationFn);
1922
+ return work(
1923
+ () => createTerrainSampler({
1924
+ terrainFieldStorage,
1925
+ spatialIndex,
1926
+ uniforms,
1927
+ elevationCallback
1928
+ })
1929
+ );
1930
+ }).displayName("createTerrainSamplerTask");
1931
+
1678
1932
  const isSkirtVertex = Fn(([segments]) => {
1679
1933
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1680
1934
  const vIndex = int(vertexIndex);
@@ -1718,14 +1972,15 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1718
1972
  }
1719
1973
  function createTileElevation(terrainUniforms, terrainFieldStorage) {
1720
1974
  if (!terrainFieldStorage) return float(0);
1721
- const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1722
- return readElevationFieldAtPositionLocal(
1975
+ const innerSegs = terrainUniforms.uInnerTileSegments;
1976
+ const u = tileLocalToFieldUV(positionLocal.x.add(float(0.5)), innerSegs);
1977
+ const v = tileLocalToFieldUV(positionLocal.z.add(float(0.5)), innerSegs);
1978
+ return sampleTerrainFieldElevation(
1723
1979
  terrainFieldStorage,
1724
- edgeVertexCount,
1725
- positionLocal
1726
- )().mul(
1727
- terrainUniforms.uElevationScale
1728
- );
1980
+ u,
1981
+ v,
1982
+ int(instanceIndex)
1983
+ ).mul(terrainUniforms.uElevationScale);
1729
1984
  }
1730
1985
  function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1731
1986
  if (!terrainFieldStorage) return;
@@ -1734,7 +1989,12 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1734
1989
  const localVertexIndex = int(vertexIndex);
1735
1990
  const ix = localVertexIndex.mod(edgeVertexCount);
1736
1991
  const iy = localVertexIndex.div(edgeVertexCount);
1737
- const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1992
+ const normalXZ = loadTerrainFieldNormal(
1993
+ terrainFieldStorage,
1994
+ ix,
1995
+ iy,
1996
+ nodeIndex
1997
+ );
1738
1998
  const nx = normalXZ.x;
1739
1999
  const nz = normalXZ.y;
1740
2000
  const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
@@ -1742,10 +2002,16 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1742
2002
  normalLocal.assign(vec3(nx, ny, nz));
1743
2003
  }
1744
2004
  function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1745
- const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
2005
+ const baseWorldPosition = createTileBaseWorldPosition(
2006
+ leafStorage,
2007
+ terrainUniforms
2008
+ );
1746
2009
  return Fn(() => {
1747
2010
  const base = baseWorldPosition();
1748
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
2011
+ const yElevation = createTileElevation(
2012
+ terrainUniforms,
2013
+ terrainFieldStorage
2014
+ );
1749
2015
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1750
2016
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1751
2017
  const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
@@ -1768,7 +2034,7 @@ const positionNodeTask = task((get, work) => {
1768
2034
  }).displayName("positionNodeTask");
1769
2035
 
1770
2036
  function terrainGraph() {
1771
- return 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);
2037
+ return 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);
1772
2038
  }
1773
2039
  const terrainTasks = {
1774
2040
  instanceId: instanceIdTask,
@@ -1777,12 +2043,15 @@ const terrainTasks = {
1777
2043
  leafStorage: leafStorageTask,
1778
2044
  surface: surfaceTask,
1779
2045
  leafGpuBuffer: leafGpuBufferTask,
2046
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2047
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
1780
2048
  createUniforms: createUniformsTask,
1781
2049
  updateUniforms: updateUniformsTask,
1782
2050
  positionNode: positionNodeTask,
1783
2051
  createElevationFieldContext: createElevationFieldContextTask,
1784
2052
  createTileNodes: tileNodesTask,
1785
2053
  createTerrainFieldTexture: createTerrainFieldTextureTask,
2054
+ createTerrainSampler: createTerrainSamplerTask,
1786
2055
  elevationFieldStage: elevationFieldStageTask,
1787
2056
  terrainFieldStage: terrainFieldStageTask,
1788
2057
  compileCompute: compileComputeTask,
@@ -1841,4 +2110,4 @@ const voronoiCells = Fn((params) => {
1841
2110
  return k;
1842
2111
  });
1843
2112
 
1844
- export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
2113
+ export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainSampler, createTerrainSamplerTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/kenjinp/hello-terrain.git"
8
8
  },
9
- "version": "0.0.0-alpha.8",
9
+ "version": "0.0.0-alpha.9",
10
10
  "type": "module",
11
11
  "main": "./dist/index.mjs",
12
12
  "module": "./dist/index.mjs",
@@ -29,12 +29,12 @@
29
29
  "mitata": "^1.0.34",
30
30
  "unbuild": "^3.5.0",
31
31
  "vitest": "^4.0.16",
32
- "@config/oxlint": "0.1.0",
32
+ "@config/typescript": "0.1.0",
33
33
  "@config/oxfmt": "0.1.0",
34
- "@config/typescript": "0.1.0"
34
+ "@config/oxlint": "0.1.0"
35
35
  },
36
36
  "dependencies": {
37
- "@hello-terrain/work": "0.2.1"
37
+ "@hello-terrain/work": "0.3.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "unbuild",