@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.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, 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 {
@@ -409,25 +409,25 @@ function resolveType(format) {
409
409
  function resolveFilter(mode) {
410
410
  return mode === "linear" ? LinearFilter : NearestFilter;
411
411
  }
412
- function configureStorageTexture(texture, format, filter) {
413
- texture.format = RGBAFormat;
414
- texture.type = resolveType(format);
415
- texture.magFilter = resolveFilter(filter);
416
- texture.minFilter = resolveFilter(filter);
417
- texture.wrapS = ClampToEdgeWrapping;
418
- texture.wrapT = ClampToEdgeWrapping;
419
- texture.generateMipmaps = false;
420
- texture.needsUpdate = true;
412
+ function configureStorageTexture(texture2, format, filter) {
413
+ texture2.format = RGBAFormat;
414
+ texture2.type = resolveType(format);
415
+ texture2.magFilter = resolveFilter(filter);
416
+ texture2.minFilter = resolveFilter(filter);
417
+ texture2.wrapS = ClampToEdgeWrapping;
418
+ texture2.wrapT = ClampToEdgeWrapping;
419
+ texture2.generateMipmaps = false;
420
+ texture2.needsUpdate = true;
421
421
  }
422
422
  function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
423
423
  let currentEdgeVertexCount = edgeVertexCount;
424
424
  let currentTileCount = tileCount;
425
- const texture = new StorageArrayTexture(
425
+ const tex = new StorageArrayTexture(
426
426
  edgeVertexCount,
427
427
  edgeVertexCount,
428
428
  tileCount
429
429
  );
430
- configureStorageTexture(texture, options.format, options.filter);
430
+ configureStorageTexture(tex, options.format, options.filter);
431
431
  return {
432
432
  backendType: "array-texture",
433
433
  get edgeVertexCount() {
@@ -436,18 +436,21 @@ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
436
436
  get tileCount() {
437
437
  return currentTileCount;
438
438
  },
439
- texture,
439
+ texture: tex,
440
440
  uv(ix, iy, _tileIndex) {
441
441
  return vec2(ix.toFloat(), iy.toFloat());
442
442
  },
443
443
  texel(ix, iy, tileIndex) {
444
444
  return ivec3(ix, iy, tileIndex);
445
445
  },
446
+ sample(u, v, tileIndex) {
447
+ return texture(tex, vec2(u, v)).depth(int(tileIndex));
448
+ },
446
449
  resize(width, height, nextTileCount) {
447
450
  currentEdgeVertexCount = width;
448
451
  currentTileCount = nextTileCount;
449
- texture.setSize(width, height, nextTileCount);
450
- texture.needsUpdate = true;
452
+ tex.setSize(width, height, nextTileCount);
453
+ tex.needsUpdate = true;
451
454
  }
452
455
  };
453
456
  }
@@ -466,8 +469,8 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
466
469
  let currentTileCount = tileCount;
467
470
  let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
468
471
  const atlasSize = tilesPerRow * edgeVertexCount;
469
- const texture = new StorageTexture(atlasSize, atlasSize);
470
- configureStorageTexture(texture, options.format, options.filter);
472
+ const tex = new StorageTexture(atlasSize, atlasSize);
473
+ configureStorageTexture(tex, options.format, options.filter);
471
474
  return {
472
475
  backendType: "atlas",
473
476
  get edgeVertexCount() {
@@ -476,7 +479,7 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
476
479
  get tileCount() {
477
480
  return currentTileCount;
478
481
  },
479
- texture,
482
+ texture: tex,
480
483
  uv(ix, iy, tileIndex) {
481
484
  const { atlasX, atlasY } = atlasCoord(
482
485
  tilesPerRow,
@@ -501,27 +504,37 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
501
504
  );
502
505
  return ivec2(atlasX, atlasY);
503
506
  },
507
+ sample(u, v, tileIndex) {
508
+ const tile = int(tileIndex);
509
+ const tilesPerRowNode = int(tilesPerRow);
510
+ const col = tile.mod(tilesPerRowNode);
511
+ const row = tile.div(tilesPerRowNode);
512
+ const invTilesPerRow = float(1 / tilesPerRow);
513
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
514
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
515
+ return texture(tex, vec2(atlasU, atlasV));
516
+ },
504
517
  resize(width, height, nextTileCount) {
505
518
  currentEdgeVertexCount = width;
506
519
  currentTileCount = nextTileCount;
507
520
  tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
508
521
  const nextAtlasSize = tilesPerRow * width;
509
- const image = texture.image;
522
+ const image = tex.image;
510
523
  image.width = nextAtlasSize;
511
524
  image.height = nextAtlasSize;
512
- texture.needsUpdate = true;
525
+ tex.needsUpdate = true;
513
526
  }
514
527
  };
515
528
  }
516
529
  function Texture3DBackend(edgeVertexCount, tileCount, options) {
517
530
  let currentEdgeVertexCount = edgeVertexCount;
518
531
  let currentTileCount = tileCount;
519
- const texture = new StorageArrayTexture(
532
+ const tex = new StorageArrayTexture(
520
533
  edgeVertexCount,
521
534
  edgeVertexCount,
522
535
  tileCount
523
536
  );
524
- configureStorageTexture(texture, options.format, options.filter);
537
+ configureStorageTexture(tex, options.format, options.filter);
525
538
  return {
526
539
  backendType: "texture-3d",
527
540
  get edgeVertexCount() {
@@ -530,18 +543,21 @@ function Texture3DBackend(edgeVertexCount, tileCount, options) {
530
543
  get tileCount() {
531
544
  return currentTileCount;
532
545
  },
533
- texture,
546
+ texture: tex,
534
547
  uv(ix, iy, _tileIndex) {
535
548
  return vec2(ix.toFloat(), iy.toFloat());
536
549
  },
537
550
  texel(ix, iy, tileIndex) {
538
551
  return ivec3(ix, iy, tileIndex);
539
552
  },
553
+ sample(u, v, tileIndex) {
554
+ return texture(tex, vec2(u, v)).depth(int(tileIndex));
555
+ },
540
556
  resize(width, height, nextTileCount) {
541
557
  currentEdgeVertexCount = width;
542
558
  currentTileCount = nextTileCount;
543
- texture.setSize(width, height, nextTileCount);
544
- texture.needsUpdate = true;
559
+ tex.setSize(width, height, nextTileCount);
560
+ tex.needsUpdate = true;
545
561
  }
546
562
  };
547
563
  }
@@ -550,7 +566,7 @@ function tryGetDeviceLimits(renderer) {
550
566
  return backend.backend?.device?.limits ?? {};
551
567
  }
552
568
  function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
553
- const filter = options.filter ?? "nearest";
569
+ const filter = options.filter ?? "linear";
554
570
  const format = options.format ?? "rgba16float";
555
571
  const forcedBackend = options.backend;
556
572
  if (forcedBackend === "atlas") {
@@ -591,8 +607,18 @@ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
591
607
  return loadTerrainField(storage, ix, iy, tileIndex).r;
592
608
  }
593
609
  function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
594
- const sample = loadTerrainField(storage, ix, iy, tileIndex);
595
- return vec2(sample.g, sample.b);
610
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
611
+ return vec2(raw.g, raw.b);
612
+ }
613
+ function sampleTerrainField(storage, u, v, tileIndex) {
614
+ return storage.sample(u, v, tileIndex);
615
+ }
616
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
617
+ return sampleTerrainField(storage, u, v, tileIndex).r;
618
+ }
619
+ function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
620
+ const raw = sampleTerrainField(storage, u, v, tileIndex);
621
+ return vec2(raw.g, raw.b);
596
622
  }
597
623
  function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
598
624
  return vec4(height, normalXZ.x, normalXZ.y, extra);
@@ -619,25 +645,6 @@ const createElevation = (tile, uniforms, elevationFn) => {
619
645
  });
620
646
  };
621
647
  };
622
- const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => Fn(() => {
623
- const nodeIndex = int(instanceIndex);
624
- const intEdge = int(edgeVertexCount);
625
- const innerSegments = int(edgeVertexCount).sub(3);
626
- const fInnerSegments = float(innerSegments);
627
- const last = intEdge.sub(int(1));
628
- const u = positionLocal.x.add(float(0.5));
629
- const v = positionLocal.z.add(float(0.5));
630
- const x = u.mul(fInnerSegments).round().toInt().add(int(1));
631
- const y = v.mul(fInnerSegments).round().toInt().add(int(1));
632
- const xClamped = min(max(x, int(0)), last);
633
- const yClamped = min(max(y, int(0)), last);
634
- return loadTerrainFieldElevation(
635
- terrainFieldStorage,
636
- xClamped,
637
- yClamped,
638
- nodeIndex
639
- );
640
- });
641
648
 
642
649
  function createTileCompute(leafStorage, uniforms) {
643
650
  const tileLevel = Fn(([nodeIndex]) => {
@@ -704,6 +711,10 @@ function createTileCompute(leafStorage, uniforms) {
704
711
  tileVertexWorldPositionCompute
705
712
  };
706
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
+ }
707
718
 
708
719
  const rootSize = param(256).displayName("rootSize");
709
720
  const origin = param({
@@ -722,6 +733,7 @@ const quadtreeUpdate = param({
722
733
  distanceFactor: 1.5
723
734
  }).displayName("quadtreeUpdate");
724
735
  const surface = param(null).displayName("surface");
736
+ const terrainFieldFilter = param("linear").displayName("terrainFieldFilter");
725
737
  const elevationFn = param(() => float(0));
726
738
 
727
739
  function createLeafStorage(maxNodes) {
@@ -819,12 +831,12 @@ function ensureChildren(store, parentId) {
819
831
  return childBase;
820
832
  }
821
833
 
822
- function nextPow2(n) {
834
+ function nextPow2$1(n) {
823
835
  let x = 1;
824
836
  while (x < n) x <<= 1;
825
837
  return x;
826
838
  }
827
- function mix32(x) {
839
+ function mix32$1(x) {
828
840
  x >>>= 0;
829
841
  x ^= x >>> 16;
830
842
  x = Math.imul(x, 2146121005) >>> 0;
@@ -833,12 +845,12 @@ function mix32(x) {
833
845
  x ^= x >>> 16;
834
846
  return x >>> 0;
835
847
  }
836
- function hashKey(space, level, x, y) {
837
- const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
838
- 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);
839
851
  }
840
852
  function createSpatialIndex(maxEntries) {
841
- const size = nextPow2(Math.max(2, maxEntries * 2));
853
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
842
854
  return {
843
855
  size,
844
856
  mask: size - 1,
@@ -863,7 +875,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
863
875
  const l = level & 255;
864
876
  const xx = x >>> 0;
865
877
  const yy = y >>> 0;
866
- let slot = hashKey(s, l, xx, yy) & index.mask;
878
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
867
879
  for (let probes = 0; probes < index.size; probes++) {
868
880
  if (index.stamp[slot] !== index.stampGen) {
869
881
  index.stamp[slot] = index.stampGen;
@@ -887,7 +899,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
887
899
  const l = level & 255;
888
900
  const xx = x >>> 0;
889
901
  const yy = y >>> 0;
890
- let slot = hashKey(s, l, xx, yy) & index.mask;
902
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
891
903
  for (let probes = 0; probes < index.size; probes++) {
892
904
  if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
893
905
  if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
@@ -1533,11 +1545,13 @@ const createTerrainFieldTextureTask = task(
1533
1545
  (get, work, { resources }) => {
1534
1546
  const edgeVertexCount = get(innerTileSegments) + 3;
1535
1547
  const maxNodesValue = get(maxNodes);
1548
+ const filter = get(terrainFieldFilter);
1536
1549
  return work(
1537
1550
  () => createTerrainFieldStorage(
1538
1551
  edgeVertexCount,
1539
1552
  maxNodesValue,
1540
- resources?.renderer
1553
+ resources?.renderer,
1554
+ { filter }
1541
1555
  )
1542
1556
  );
1543
1557
  }
@@ -1646,6 +1660,275 @@ function createComputePipelineTasks(leafStageTask) {
1646
1660
  return { compile, execute };
1647
1661
  }
1648
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
+
1649
1932
  const isSkirtVertex = Fn(([segments]) => {
1650
1933
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1651
1934
  const vIndex = int(vertexIndex);
@@ -1689,14 +1972,15 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1689
1972
  }
1690
1973
  function createTileElevation(terrainUniforms, terrainFieldStorage) {
1691
1974
  if (!terrainFieldStorage) return float(0);
1692
- const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1693
- 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(
1694
1979
  terrainFieldStorage,
1695
- edgeVertexCount,
1696
- positionLocal
1697
- )().mul(
1698
- terrainUniforms.uElevationScale
1699
- );
1980
+ u,
1981
+ v,
1982
+ int(instanceIndex)
1983
+ ).mul(terrainUniforms.uElevationScale);
1700
1984
  }
1701
1985
  function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1702
1986
  if (!terrainFieldStorage) return;
@@ -1705,7 +1989,12 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1705
1989
  const localVertexIndex = int(vertexIndex);
1706
1990
  const ix = localVertexIndex.mod(edgeVertexCount);
1707
1991
  const iy = localVertexIndex.div(edgeVertexCount);
1708
- const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1992
+ const normalXZ = loadTerrainFieldNormal(
1993
+ terrainFieldStorage,
1994
+ ix,
1995
+ iy,
1996
+ nodeIndex
1997
+ );
1709
1998
  const nx = normalXZ.x;
1710
1999
  const nz = normalXZ.y;
1711
2000
  const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
@@ -1713,10 +2002,16 @@ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1713
2002
  normalLocal.assign(vec3(nx, ny, nz));
1714
2003
  }
1715
2004
  function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1716
- const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
2005
+ const baseWorldPosition = createTileBaseWorldPosition(
2006
+ leafStorage,
2007
+ terrainUniforms
2008
+ );
1717
2009
  return Fn(() => {
1718
2010
  const base = baseWorldPosition();
1719
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
2011
+ const yElevation = createTileElevation(
2012
+ terrainUniforms,
2013
+ terrainFieldStorage
2014
+ );
1720
2015
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1721
2016
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1722
2017
  const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
@@ -1739,7 +2034,7 @@ const positionNodeTask = task((get, work) => {
1739
2034
  }).displayName("positionNodeTask");
1740
2035
 
1741
2036
  function terrainGraph() {
1742
- 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);
1743
2038
  }
1744
2039
  const terrainTasks = {
1745
2040
  instanceId: instanceIdTask,
@@ -1748,12 +2043,15 @@ const terrainTasks = {
1748
2043
  leafStorage: leafStorageTask,
1749
2044
  surface: surfaceTask,
1750
2045
  leafGpuBuffer: leafGpuBufferTask,
2046
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2047
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
1751
2048
  createUniforms: createUniformsTask,
1752
2049
  updateUniforms: updateUniformsTask,
1753
2050
  positionNode: positionNodeTask,
1754
2051
  createElevationFieldContext: createElevationFieldContextTask,
1755
2052
  createTileNodes: tileNodesTask,
1756
2053
  createTerrainFieldTexture: createTerrainFieldTextureTask,
2054
+ createTerrainSampler: createTerrainSamplerTask,
1757
2055
  elevationFieldStage: elevationFieldStageTask,
1758
2056
  terrainFieldStage: terrainFieldStageTask,
1759
2057
  compileCompute: compileComputeTask,
@@ -1812,4 +2110,4 @@ const voronoiCells = Fn((params) => {
1812
2110
  return k;
1813
2111
  });
1814
2112
 
1815
- 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, skirtScale, storeTerrainField, surface, surfaceTask, 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.7",
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",