@hello-terrain/three 0.0.0-alpha.10 → 0.0.0-alpha.11

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
@@ -7,28 +7,27 @@ const tsl = require('three/tsl');
7
7
  const TSL_js = require('three/src/nodes/TSL.js');
8
8
 
9
9
  class TerrainGeometry extends three.BufferGeometry {
10
- constructor(innerSegments = 14, extendUV = false) {
10
+ /**
11
+ * @param flipWinding Reverse triangle winding so front faces point the
12
+ * opposite way. The default winding makes flat tiles front-face `+Y`; the
13
+ * cube-sphere maps `(u→right, v→up)`, which would otherwise leave the
14
+ * planet's outer shell back-facing, so it passes `flipWinding` to render
15
+ * the outer surface with `FrontSide`.
16
+ */
17
+ constructor(innerSegments = 14, extendUV = false, flipWinding = false) {
11
18
  super();
12
19
  if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
13
- throw new Error(
14
- `Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
15
- );
20
+ throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
16
21
  }
17
22
  try {
18
- this.setIndex(this.generateIndices(innerSegments));
23
+ this.setIndex(this.generateIndices(innerSegments, flipWinding));
19
24
  this.setAttribute(
20
25
  "position",
21
- new three.BufferAttribute(
22
- new Float32Array(this.generatePositions(innerSegments)),
23
- 3
24
- )
26
+ new three.BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
25
27
  );
26
28
  this.setAttribute(
27
29
  "normal",
28
- new three.BufferAttribute(
29
- new Float32Array(this.generateNormals(innerSegments)),
30
- 3
31
- )
30
+ new three.BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
32
31
  );
33
32
  this.setAttribute(
34
33
  "uv",
@@ -103,12 +102,16 @@ class TerrainGeometry extends three.BufferGeometry {
103
102
  * triangle 1: a, c, b
104
103
  * triangle 2: b, c, d
105
104
  */
106
- generateIndices(innerSegments) {
105
+ generateIndices(innerSegments, flipWinding = false) {
107
106
  const innerEdgeVertexCount = innerSegments + 1;
108
107
  const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
109
108
  const indices = [];
110
109
  const cellsPerEdge = edgeVertexCountWithSkirt - 1;
111
110
  const mid = Math.floor(cellsPerEdge / 2);
111
+ const pushTri = (v0, v1, v2) => {
112
+ if (flipWinding) indices.push(v0, v2, v1);
113
+ else indices.push(v0, v1, v2);
114
+ };
112
115
  for (let y = 0; y < cellsPerEdge; y++) {
113
116
  for (let x = 0; x < cellsPerEdge; x++) {
114
117
  const a = y * edgeVertexCountWithSkirt + x;
@@ -125,11 +128,11 @@ class TerrainGeometry extends three.BufferGeometry {
125
128
  useDefaultDiagonal = (x + y) % 2 === 0;
126
129
  }
127
130
  if (useDefaultDiagonal) {
128
- indices.push(a, d, b);
129
- indices.push(a, c, d);
131
+ pushTri(a, d, b);
132
+ pushTri(a, c, d);
130
133
  } else {
131
- indices.push(a, c, b);
132
- indices.push(b, c, d);
134
+ pushTri(a, c, b);
135
+ pushTri(b, c, d);
133
136
  }
134
137
  }
135
138
  }
@@ -220,32 +223,70 @@ class TerrainGeometry extends three.BufferGeometry {
220
223
  }
221
224
  }
222
225
 
226
+ const rootSize = work.param(256).displayName("rootSize");
227
+ const origin = work.param({
228
+ x: 0,
229
+ y: 0,
230
+ z: 0
231
+ }).displayName("origin");
232
+ const innerTileSegments = work.param(61).displayName("innerTileSegments");
233
+ const skirtScale = work.param(100).displayName("skirtScale");
234
+ const elevationScale = work.param(1).displayName("elevationScale");
235
+ const radius = work.param(1e3).displayName("radius");
236
+ const maxNodes = work.param(1024).displayName("maxNodes");
237
+ const maxLevel = work.param(16).displayName("maxLevel");
238
+ const quadtreeUpdate = work.param({
239
+ cameraOrigin: { x: 0, y: 0, z: 0 },
240
+ mode: "distance",
241
+ distanceFactor: 1.5
242
+ }).displayName("quadtreeUpdate");
243
+ const topology = work.param(null).displayName("topology");
244
+ const terrainFieldFilter = work.param("linear").displayName(
245
+ "terrainFieldFilter"
246
+ );
247
+ const elevationFn = work.param(() => tsl.float(0));
248
+
223
249
  const defaultTerrainMeshParams = {
224
- innerTileSegments: 14,
250
+ // Source of truth is the `innerTileSegments` param itself.
251
+ innerTileSegments: innerTileSegments.get(),
225
252
  maxNodes: 1024,
226
- material: new webgpu.MeshStandardNodeMaterial()
253
+ material: new webgpu.MeshStandardNodeMaterial(),
254
+ flipWinding: false
227
255
  };
228
256
  class TerrainMesh extends webgpu.InstancedMesh {
229
257
  _innerTileSegments;
230
258
  _maxNodes;
259
+ _flipWinding;
231
260
  terrainRaycast = null;
232
261
  constructor(params = defaultTerrainMeshParams) {
233
262
  const mergedParams = { ...defaultTerrainMeshParams, ...params };
234
- const { innerTileSegments, maxNodes, material } = mergedParams;
235
- const geometry = new TerrainGeometry(innerTileSegments, true);
263
+ const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
264
+ const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
236
265
  super(geometry, material, maxNodes);
237
266
  this.frustumCulled = false;
238
267
  this._innerTileSegments = innerTileSegments;
239
268
  this._maxNodes = maxNodes;
269
+ this._flipWinding = flipWinding;
240
270
  }
241
271
  get innerTileSegments() {
242
272
  return this._innerTileSegments;
243
273
  }
244
274
  set innerTileSegments(tileSegments) {
275
+ if (tileSegments === this._innerTileSegments) return;
245
276
  const oldGeometry = this.geometry;
246
- this.geometry = new TerrainGeometry(tileSegments, true);
277
+ this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
247
278
  this._innerTileSegments = tileSegments;
248
- setTimeout(oldGeometry.dispose);
279
+ setTimeout(() => oldGeometry.dispose());
280
+ }
281
+ get flipWinding() {
282
+ return this._flipWinding;
283
+ }
284
+ set flipWinding(flip) {
285
+ if (flip === this._flipWinding) return;
286
+ const oldGeometry = this.geometry;
287
+ this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
288
+ this._flipWinding = flip;
289
+ setTimeout(() => oldGeometry.dispose());
249
290
  }
250
291
  get maxNodes() {
251
292
  return this._maxNodes;
@@ -311,13 +352,8 @@ function compileComputePipeline(stages, width, options) {
311
352
  WORKGROUP_X,
312
353
  WORKGROUP_Y
313
354
  ];
314
- const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
315
355
  const uInstanceCount = tsl.uniform(0, "uint");
316
- let singleKernel;
317
356
  const stagedKernelCache = /* @__PURE__ */ new Map();
318
- function canRunSingleKernel(widthValue, limits) {
319
- return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
320
- }
321
357
  function clampWorkgroupToLimits(requested, limits) {
322
358
  let x = Math.max(1, Math.floor(requested[0]));
323
359
  let y = Math.max(1, Math.floor(requested[1]));
@@ -333,37 +369,6 @@ function compileComputePipeline(stages, width, options) {
333
369
  );
334
370
  return [x, y];
335
371
  }
336
- function buildSingleKernel(workgroupSize) {
337
- return tsl.Fn(() => {
338
- bindings?.forEach((b) => b.toVar());
339
- const fWidth = tsl.float(width);
340
- const activeIndex = tsl.globalId.z;
341
- const nodeIndex = tsl.int(activeIndex).toVar();
342
- const iWidth = tsl.int(width);
343
- const ix = tsl.int(tsl.globalId.x);
344
- const iy = tsl.int(tsl.globalId.y);
345
- const texelSize = tsl.vec2(1, 1).div(fWidth);
346
- const localCoordinates = tsl.vec2(tsl.globalId.x, tsl.globalId.y);
347
- const localUVCoords = localCoordinates.div(fWidth);
348
- const verticesPerNode = iWidth.mul(iWidth);
349
- const globalIndex = tsl.int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
350
- const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(tsl.uint(activeIndex).lessThan(uInstanceCount)).toVar();
351
- for (let i = 0; i < stages.length; i++) {
352
- if (i > 0) {
353
- tsl.workgroupBarrier();
354
- }
355
- tsl.If(inBounds, () => {
356
- stages[i](
357
- nodeIndex,
358
- globalIndex,
359
- localUVCoords,
360
- localCoordinates,
361
- texelSize
362
- );
363
- });
364
- }
365
- })().computeKernel(workgroupSize);
366
- }
367
372
  function buildStagedKernels(workgroupSize) {
368
373
  return stages.map(
369
374
  (stage) => tsl.Fn(() => {
@@ -394,15 +399,7 @@ function compileComputePipeline(stages, width, options) {
394
399
  }
395
400
  function execute(renderer, instanceCount) {
396
401
  const limits = getDeviceComputeLimits(renderer);
397
- const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
398
402
  uInstanceCount.value = instanceCount;
399
- if (canUseSingleKernel) {
400
- if (!singleKernel) {
401
- singleKernel = buildSingleKernel([width, width, 1]);
402
- }
403
- renderer.compute(singleKernel, [1, 1, instanceCount]);
404
- return;
405
- }
406
403
  const [workgroupX, workgroupY] = clampWorkgroupToLimits(
407
404
  preferredWorkgroup,
408
405
  limits
@@ -545,40 +542,9 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
545
542
  }
546
543
  };
547
544
  }
548
- function Texture3DBackend(edgeVertexCount, tileCount, options) {
549
- let currentEdgeVertexCount = edgeVertexCount;
550
- let currentTileCount = tileCount;
551
- const tex = new webgpu.StorageArrayTexture(
552
- edgeVertexCount,
553
- edgeVertexCount,
554
- tileCount
555
- );
556
- configureStorageTexture(tex, options.format, options.filter);
557
- return {
558
- backendType: "texture-3d",
559
- get edgeVertexCount() {
560
- return currentEdgeVertexCount;
561
- },
562
- get tileCount() {
563
- return currentTileCount;
564
- },
565
- texture: tex,
566
- uv(ix, iy, _tileIndex) {
567
- return tsl.vec2(ix.toFloat(), iy.toFloat());
568
- },
569
- texel(ix, iy, tileIndex) {
570
- return tsl.ivec3(ix, iy, tileIndex);
571
- },
572
- sample(u, v, tileIndex) {
573
- return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
574
- },
575
- resize(width, height, nextTileCount) {
576
- currentEdgeVertexCount = width;
577
- currentTileCount = nextTileCount;
578
- tex.setSize(width, height, nextTileCount);
579
- tex.needsUpdate = true;
580
- }
581
- };
545
+ function texture3DBackend(edgeVertexCount, tileCount, options) {
546
+ const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
547
+ return { ...storage, backendType: "texture-3d" };
582
548
  }
583
549
  function tryGetDeviceLimits(renderer) {
584
550
  const backend = renderer;
@@ -592,7 +558,7 @@ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options
592
558
  return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
593
559
  }
594
560
  if (forcedBackend === "texture-3d") {
595
- return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
561
+ return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
596
562
  }
597
563
  if (forcedBackend === "array-texture") {
598
564
  return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
@@ -635,10 +601,6 @@ function sampleTerrainField(storage, u, v, tileIndex) {
635
601
  function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
636
602
  return sampleTerrainField(storage, u, v, tileIndex).r;
637
603
  }
638
- function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
639
- const raw = sampleTerrainField(storage, u, v, tileIndex);
640
- return tsl.vec2(raw.g, raw.b);
641
- }
642
604
  function packTerrainFieldSample(height, normalXZ, extra = tsl.float(0)) {
643
605
  return tsl.vec4(height, normalXZ.x, normalXZ.y, extra);
644
606
  }
@@ -665,23 +627,132 @@ const createElevation = (tile, uniforms, elevationFn) => {
665
627
  };
666
628
  };
667
629
 
668
- function createTileCompute(leafStorage, uniforms) {
630
+ const CUBE_FACE_COUNT = 6;
631
+ const CUBE_FACES = [
632
+ // 0: +X
633
+ { forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
634
+ // 1: -X
635
+ { forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
636
+ // 2: +Y (north pole)
637
+ { forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
638
+ // 3: -Y (south pole)
639
+ { forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
640
+ // 4: +Z
641
+ { forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
642
+ // 5: -Z
643
+ { forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
644
+ ];
645
+
646
+ function vec3Const(v) {
647
+ return tsl.vec3(tsl.float(v[0]), tsl.float(v[1]), tsl.float(v[2]));
648
+ }
649
+ function selectFaceVec3(face, pick) {
650
+ const last = CUBE_FACES.length - 1;
651
+ let acc = vec3Const(pick(CUBE_FACES[last]));
652
+ for (let i = last - 1; i >= 0; i--) {
653
+ acc = tsl.select(tsl.int(face).equal(tsl.int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
654
+ }
655
+ return acc;
656
+ }
657
+ function cubeFaceBasis(face) {
658
+ return {
659
+ forward: selectFaceVec3(face, (f) => f.forward),
660
+ right: selectFaceVec3(face, (f) => f.right),
661
+ up: selectFaceVec3(face, (f) => f.up)
662
+ };
663
+ }
664
+ function cubeFacePoint(basis, u, v) {
665
+ const s = tsl.float(u).mul(2).sub(1);
666
+ const t = tsl.float(v).mul(2).sub(1);
667
+ return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
668
+ }
669
+ function cubeFaceDirection(basis, u, v) {
670
+ return cubeFacePoint(basis, u, v).normalize();
671
+ }
672
+ function tangentFromAxis(dir, axis) {
673
+ return axis.sub(dir.mul(dir.dot(axis))).normalize();
674
+ }
675
+ function unpackTangentNormal(nx, nz) {
676
+ const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0)).sqrt();
677
+ return tsl.vec3(nx, ny, nz);
678
+ }
679
+ function sphereTangentFrameNormal(dir, basis, tangentNormal) {
680
+ const n = tsl.vec3(tangentNormal);
681
+ const tu = tangentFromAxis(dir, basis.right);
682
+ const tv = tangentFromAxis(dir, basis.up);
683
+ return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
684
+ }
685
+ function cubeFaceFromDirection(dir) {
686
+ const d = tsl.vec3(dir);
687
+ const ax = d.x.abs();
688
+ const ay = d.y.abs();
689
+ const az = d.z.abs();
690
+ const faceX = tsl.select(d.x.greaterThanEqual(tsl.float(0)), tsl.int(0), tsl.int(1));
691
+ const faceY = tsl.select(d.y.greaterThanEqual(tsl.float(0)), tsl.int(2), tsl.int(3));
692
+ const faceZ = tsl.select(d.z.greaterThanEqual(tsl.float(0)), tsl.int(4), tsl.int(5));
693
+ const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
694
+ const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
695
+ return tsl.select(xDominant, faceX, tsl.select(yDominant, faceY, faceZ));
696
+ }
697
+ function cubeFaceUVFromDirection(basis, dir) {
698
+ const d = tsl.vec3(dir);
699
+ const p = d.div(d.dot(basis.forward));
700
+ const s = p.dot(basis.right);
701
+ const t = p.dot(basis.up);
702
+ return tsl.vec2(s.add(tsl.float(1)).mul(tsl.float(0.5)), t.add(tsl.float(1)).mul(tsl.float(0.5)));
703
+ }
704
+
705
+ const HALF_PI = Math.PI * 0.5;
706
+ const FIELD_INNER_TEXEL_OFFSET = 1.5;
707
+ const FIELD_EDGE_EXTRA_TEXELS = 3;
708
+ function sphereTileArcLength(radius, levelDivisor) {
709
+ return radius * HALF_PI / levelDivisor;
710
+ }
711
+ function decodeLeafTile(leafStorage, nodeIndex) {
712
+ const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
713
+ return {
714
+ level: leafStorage.node.element(nodeOffset).toInt(),
715
+ x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
716
+ y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
717
+ face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
718
+ };
719
+ }
720
+ function faceUVFromTileLocal(tile, localU, localV) {
721
+ const n = tsl.pow(tsl.float(2), tile.level.toFloat());
722
+ return tsl.vec2(tile.x.add(localU).div(n), tile.y.add(localV).div(n));
723
+ }
724
+ function createTileCompute(leafStorage, uniforms, projection = "flat") {
725
+ const isSphere = projection === "cubeSphere";
669
726
  const tileLevel = tsl.Fn(([nodeIndex]) => {
670
- const nodeOffset = nodeIndex.mul(tsl.int(4));
671
- return leafStorage.node.element(nodeOffset).toInt();
727
+ return decodeLeafTile(leafStorage, nodeIndex).level;
728
+ });
729
+ const tileFace = tsl.Fn(([nodeIndex]) => {
730
+ return decodeLeafTile(leafStorage, nodeIndex).face;
672
731
  });
673
732
  const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
674
- const nodeOffset = nodeIndex.mul(tsl.int(4));
675
- const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
676
- const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
677
- return tsl.vec2(nodeX, nodeY);
733
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
734
+ return tsl.vec2(tile.x, tile.y);
678
735
  });
679
736
  const tileSize = tsl.Fn(([nodeIndex]) => {
680
- const rootSize = uniforms.uRootSize.toVar();
681
737
  const level = tileLevel(nodeIndex);
682
- return tsl.float(rootSize).div(tsl.pow(tsl.float(2), level.toFloat()));
738
+ const divisor = tsl.pow(tsl.float(2), level.toFloat());
739
+ if (isSphere) {
740
+ return uniforms.uRadius.toVar().mul(tsl.float(HALF_PI)).div(divisor);
741
+ }
742
+ const rootSize = uniforms.uRootSize.toVar();
743
+ return tsl.float(rootSize).div(divisor);
744
+ });
745
+ const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
746
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
747
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
748
+ const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
749
+ const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
750
+ return faceUVFromTileLocal(tile, localU, localV);
683
751
  });
684
752
  const rootUVCompute = tsl.Fn(([nodeIndex, ix, iy]) => {
753
+ if (isSphere) {
754
+ return tileFaceUV(nodeIndex, ix, iy);
755
+ }
685
756
  const nodeVec2 = tileOriginVec2(nodeIndex);
686
757
  const nodeX = nodeVec2.x;
687
758
  const nodeY = nodeVec2.y;
@@ -706,6 +777,12 @@ function createTileCompute(leafStorage, uniforms) {
706
777
  const tileVertexWorldPositionCompute = tsl.Fn(
707
778
  ([nodeIndex, ix, iy]) => {
708
779
  const rootOrigin = uniforms.uRootOrigin.toVar();
780
+ if (isSphere) {
781
+ const faceUV = tileFaceUV(nodeIndex, ix, iy);
782
+ const basis = cubeFaceBasis(tileFace(nodeIndex));
783
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
784
+ return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
785
+ }
709
786
  const nodeVec2 = tileOriginVec2(nodeIndex);
710
787
  const nodeX = nodeVec2.x;
711
788
  const nodeY = nodeVec2.y;
@@ -724,36 +801,22 @@ function createTileCompute(leafStorage, uniforms) {
724
801
  );
725
802
  return {
726
803
  tileLevel,
804
+ tileFace,
727
805
  tileOriginVec2,
728
806
  tileSize,
807
+ tileFaceUV,
729
808
  rootUVCompute,
730
809
  tileVertexWorldPositionCompute
731
810
  };
732
811
  }
733
- function tileLocalToFieldUV$1(localCoord, innerSegments) {
734
- const edge = tsl.float(innerSegments).add(tsl.float(3));
735
- return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(1.5)).div(edge);
812
+ function tileLocalToFieldUV(localCoord, innerSegments) {
813
+ const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
814
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
815
+ }
816
+ function tileLocalToFieldUVNumber(localCoord, innerSegments) {
817
+ const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
818
+ return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
736
819
  }
737
-
738
- const rootSize = work.param(256).displayName("rootSize");
739
- const origin = work.param({
740
- x: 0,
741
- y: 0,
742
- z: 0
743
- }).displayName("origin");
744
- const innerTileSegments = work.param(13).displayName("innerTileSegments");
745
- const skirtScale = work.param(100).displayName("skirtScale");
746
- const elevationScale = work.param(1).displayName("elevationScale");
747
- const maxNodes = work.param(1024).displayName("maxNodes");
748
- const maxLevel = work.param(16).displayName("maxLevel");
749
- const quadtreeUpdate = work.param({
750
- cameraOrigin: { x: 0, y: 0, z: 0 },
751
- mode: "distance",
752
- distanceFactor: 1.5
753
- }).displayName("quadtreeUpdate");
754
- const surface = work.param(null).displayName("surface");
755
- const terrainFieldFilter = work.param("linear").displayName("terrainFieldFilter");
756
- const elevationFn = work.param(() => tsl.float(0));
757
820
 
758
821
  function createLeafStorage(maxNodes) {
759
822
  const data = new Int32Array(maxNodes * 4);
@@ -938,10 +1001,10 @@ function buildLeafIndex(leaves, out) {
938
1001
  return index;
939
1002
  }
940
1003
 
941
- function createState(cfg, surface) {
942
- const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
1004
+ function createState(cfg, topology) {
1005
+ const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
943
1006
  const scratchRootTiles = [];
944
- for (let i = 0; i < surface.maxRootCount; i++) {
1007
+ for (let i = 0; i < topology.maxRootCount; i++) {
945
1008
  scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
946
1009
  }
947
1010
  return {
@@ -951,7 +1014,7 @@ function createState(cfg, surface) {
951
1014
  leafNodeIds: new Uint32Array(cfg.maxNodes),
952
1015
  leafIndex: createSpatialIndex(cfg.maxNodes),
953
1016
  stack: new Uint32Array(cfg.maxNodes),
954
- rootNodeIds: new Uint32Array(surface.maxRootCount),
1017
+ rootNodeIds: new Uint32Array(topology.maxRootCount),
955
1018
  rootCount: 0,
956
1019
  splitQueue: new Uint32Array(cfg.maxNodes),
957
1020
  splitStamp: new Uint16Array(cfg.maxNodes),
@@ -960,25 +1023,25 @@ function createState(cfg, surface) {
960
1023
  scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
961
1024
  scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
962
1025
  scratchRootTiles,
963
- spaceCount: surface.spaceCount
1026
+ spaceCount: topology.spaceCount
964
1027
  };
965
1028
  }
966
- function beginUpdate(state, surface, params) {
967
- if (surface.spaceCount !== state.spaceCount) {
1029
+ function beginUpdate(state, topology, params) {
1030
+ if (topology.spaceCount !== state.spaceCount) {
968
1031
  throw new Error(
969
- `Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
1032
+ `Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
970
1033
  );
971
1034
  }
972
- if (surface.maxRootCount !== state.rootNodeIds.length) {
1035
+ if (topology.maxRootCount !== state.rootNodeIds.length) {
973
1036
  throw new Error(
974
- `Surface maxRootCount changed (${state.rootNodeIds.length} -> ${surface.maxRootCount}). Create a new quadtree state.`
1037
+ `Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
975
1038
  );
976
1039
  }
977
1040
  beginFrame(state.store);
978
1041
  state.rootCount = 0;
979
- const rootCount = surface.rootTiles(params.cameraOrigin, state.scratchRootTiles);
980
- if (rootCount < 0 || rootCount > surface.maxRootCount) {
981
- throw new Error(`Surface returned invalid root count (${rootCount}).`);
1042
+ const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
1043
+ if (rootCount < 0 || rootCount > topology.maxRootCount) {
1044
+ throw new Error(`Topology returned invalid root count (${rootCount}).`);
982
1045
  }
983
1046
  for (let i = 0; i < rootCount; i++) {
984
1047
  const rootId = allocNode(state.store, state.scratchRootTiles[i]);
@@ -1015,7 +1078,7 @@ function shouldSplit(bounds, level, maxLevel, params) {
1015
1078
  return safeDistSq < threshold * threshold;
1016
1079
  }
1017
1080
 
1018
- function refineLeaves(state, surface, params, outLeaves) {
1081
+ function refineLeaves(state, topology, params, outLeaves) {
1019
1082
  const leaves = outLeaves ?? state.leaves;
1020
1083
  resetLeafSet(leaves);
1021
1084
  const store = state.store;
@@ -1036,7 +1099,7 @@ function refineLeaves(state, surface, params, outLeaves) {
1036
1099
  tile.x = x;
1037
1100
  tile.y = y;
1038
1101
  const bounds = state.scratchBounds;
1039
- surface.tileBounds(tile, params.cameraOrigin, bounds);
1102
+ topology.tileBounds(tile, params.cameraOrigin, bounds);
1040
1103
  if (hasChildren(store, nodeId)) {
1041
1104
  const base = store.firstChild[nodeId];
1042
1105
  stack[sp++] = base + 3;
@@ -1084,7 +1147,7 @@ function scheduleSplit(state, nodeId, count) {
1084
1147
  state.splitQueue[count] = nodeId;
1085
1148
  return count + 1;
1086
1149
  }
1087
- function balance2to1(state, surface, params, leaves) {
1150
+ function balance2to1(state, topology, params, leaves) {
1088
1151
  const maxIters = state.cfg.maxLevel + 1;
1089
1152
  for (let iter = 0; iter < maxIters; iter++) {
1090
1153
  const index = buildLeafIndex(leaves, state.leafIndex);
@@ -1105,7 +1168,7 @@ function balance2to1(state, surface, params, leaves) {
1105
1168
  tile.x = leafX >>> shift;
1106
1169
  tile.y = leafY >>> shift;
1107
1170
  const neighbor = state.scratchNeighbor;
1108
- if (!surface.neighborSameLevel(tile, dir, neighbor)) break;
1171
+ if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
1109
1172
  const j = lookupSpatialIndexRaw(
1110
1173
  index,
1111
1174
  neighbor.space,
@@ -1129,18 +1192,38 @@ function balance2to1(state, surface, params, leaves) {
1129
1192
  if (base !== U32_EMPTY) anySplit = true;
1130
1193
  }
1131
1194
  if (!anySplit) return leaves;
1132
- refineLeaves(state, surface, params, leaves);
1195
+ refineLeaves(state, topology, params, leaves);
1133
1196
  }
1134
1197
  return leaves;
1135
1198
  }
1136
1199
 
1137
- function update(state, surface, params, outLeaves) {
1138
- const origY = params.cameraOrigin.y;
1139
- params.cameraOrigin.y -= params.elevationAtCameraXZ ?? 0;
1140
- beginUpdate(state, surface, params);
1141
- const leaves = refineLeaves(state, surface, params, outLeaves);
1142
- const result = balance2to1(state, surface, params, leaves);
1143
- params.cameraOrigin.y = origY;
1200
+ function update(state, topology, params, outLeaves) {
1201
+ const cam = params.cameraOrigin;
1202
+ const elevation = params.elevationAtCameraXZ ?? 0;
1203
+ const origX = cam.x;
1204
+ const origY = cam.y;
1205
+ const origZ = cam.z;
1206
+ if (topology.projection === "cubeSphere") {
1207
+ const center = topology.center ?? { x: 0, y: 0, z: 0 };
1208
+ const dx = cam.x - center.x;
1209
+ const dy = cam.y - center.y;
1210
+ const dz = cam.z - center.z;
1211
+ const len = Math.hypot(dx, dy, dz);
1212
+ if (len > 1e-12) {
1213
+ const inv = elevation / len;
1214
+ cam.x -= dx * inv;
1215
+ cam.y -= dy * inv;
1216
+ cam.z -= dz * inv;
1217
+ }
1218
+ } else {
1219
+ cam.y -= elevation;
1220
+ }
1221
+ beginUpdate(state, topology, params);
1222
+ const leaves = refineLeaves(state, topology, params, outLeaves);
1223
+ const result = balance2to1(state, topology, params, leaves);
1224
+ cam.x = origX;
1225
+ cam.y = origY;
1226
+ cam.z = origZ;
1144
1227
  return result;
1145
1228
  }
1146
1229
 
@@ -1148,7 +1231,7 @@ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
1148
1231
  const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
1149
1232
  const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
1150
1233
  const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
1151
- function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1234
+ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1152
1235
  if (outSeams.capacity < leaves.count) {
1153
1236
  throw new Error("SeamTable capacity is smaller than LeafSet.count.");
1154
1237
  }
@@ -1169,7 +1252,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1169
1252
  scratchTile.level = level;
1170
1253
  scratchTile.x = x;
1171
1254
  scratchTile.y = y;
1172
- if (!surface.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1255
+ if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1173
1256
  let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
1174
1257
  if (j !== U32_EMPTY) {
1175
1258
  neighbors[outOffset + 0] = j;
@@ -1182,7 +1265,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1182
1265
  scratchParentTile.level = level - 1;
1183
1266
  scratchParentTile.x = px;
1184
1267
  scratchParentTile.y = py;
1185
- if (surface.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1268
+ if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1186
1269
  j = lookupSpatialIndexRaw(
1187
1270
  index,
1188
1271
  scratchParentNbr.space,
@@ -1238,10 +1321,10 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1238
1321
  return outSeams;
1239
1322
  }
1240
1323
 
1241
- function createFlatSurface(cfg) {
1324
+ function createFlatTopology(cfg) {
1242
1325
  const halfRoot = 0.5 * cfg.rootSize;
1243
1326
  const maxHeight = cfg.maxHeight ?? 0;
1244
- const surface = {
1327
+ const topology = {
1245
1328
  spaceCount: 1,
1246
1329
  maxRootCount: 1,
1247
1330
  neighborSameLevel(tile, dir, out) {
@@ -1296,10 +1379,10 @@ function createFlatSurface(cfg) {
1296
1379
  return 1;
1297
1380
  }
1298
1381
  };
1299
- return surface;
1382
+ return topology;
1300
1383
  }
1301
1384
 
1302
- function createInfiniteFlatSurface(cfg) {
1385
+ function createInfiniteFlatTopology(cfg) {
1303
1386
  const halfRoot = 0.5 * cfg.rootSize;
1304
1387
  const maxHeight = cfg.maxHeight ?? 0;
1305
1388
  const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
@@ -1363,18 +1446,158 @@ function createInfiniteFlatSurface(cfg) {
1363
1446
  };
1364
1447
  }
1365
1448
 
1366
- function createCubeSphereSurface(_cfg) {
1449
+ const DEG_TO_RAD = Math.PI / 180;
1450
+ const RAD_TO_DEG = 180 / Math.PI;
1451
+ function dot(a, b) {
1452
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1453
+ }
1454
+ function faceUVToCube(face, u, v, out) {
1455
+ const f = CUBE_FACES[face];
1456
+ const s = 2 * u - 1;
1457
+ const t = 2 * v - 1;
1458
+ out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
1459
+ out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
1460
+ out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
1461
+ }
1462
+ function directionToFace(d) {
1463
+ const ax = Math.abs(d[0]);
1464
+ const ay = Math.abs(d[1]);
1465
+ const az = Math.abs(d[2]);
1466
+ if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
1467
+ if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
1468
+ return d[2] >= 0 ? 4 : 5;
1469
+ }
1470
+ function directionToFaceUV(face, d, out) {
1471
+ const f = CUBE_FACES[face];
1472
+ const denom = dot(d, f.forward);
1473
+ const inv = 1 / denom;
1474
+ const px = d[0] * inv;
1475
+ const py = d[1] * inv;
1476
+ const pz = d[2] * inv;
1477
+ const p = [px, py, pz];
1478
+ const s = dot(p, f.right);
1479
+ const t = dot(p, f.up);
1480
+ out[0] = (s + 1) * 0.5;
1481
+ out[1] = (t + 1) * 0.5;
1482
+ }
1483
+ function latLongToDirection(latDeg, lonDeg, out) {
1484
+ const lat = latDeg * DEG_TO_RAD;
1485
+ const lon = lonDeg * DEG_TO_RAD;
1486
+ const cosLat = Math.cos(lat);
1487
+ out[0] = cosLat * Math.sin(lon);
1488
+ out[1] = Math.sin(lat);
1489
+ out[2] = cosLat * Math.cos(lon);
1490
+ }
1491
+ function directionToLatLong(d) {
1492
+ const len = Math.hypot(d[0], d[1], d[2]) || 1;
1493
+ const y = Math.max(-1, Math.min(1, d[1] / len));
1494
+ return {
1495
+ latitude: Math.asin(y) * RAD_TO_DEG,
1496
+ longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
1497
+ };
1498
+ }
1499
+
1500
+ function createCubeSphereTopology(cfg) {
1501
+ const radius = cfg.radius;
1502
+ const maxHeight = cfg.maxHeight ?? 0;
1503
+ const center = cfg.center ?? { x: 0, y: 0, z: 0 };
1504
+ const cube = [0, 0, 0];
1505
+ const uv = [0, 0];
1506
+ function crossFaceNeighbor(face, level, nx, ny, out) {
1507
+ const n = 1 << level;
1508
+ const u = (nx + 0.5) / n;
1509
+ const v = (ny + 0.5) / n;
1510
+ faceUVToCube(face, u, v, cube);
1511
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
1512
+ const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
1513
+ const nbrFace = directionToFace(dir);
1514
+ directionToFaceUV(nbrFace, dir, uv);
1515
+ let bx = Math.floor(uv[0] * n);
1516
+ let by = Math.floor(uv[1] * n);
1517
+ if (bx < 0) bx = 0;
1518
+ else if (bx > n - 1) bx = n - 1;
1519
+ if (by < 0) by = 0;
1520
+ else if (by > n - 1) by = n - 1;
1521
+ out.space = nbrFace;
1522
+ out.level = level;
1523
+ out.x = bx;
1524
+ out.y = by;
1525
+ }
1367
1526
  return {
1368
1527
  spaceCount: 6,
1369
1528
  maxRootCount: 6,
1370
- neighborSameLevel(_tile, _dir, _out) {
1371
- return false;
1529
+ neighborSameLevel(tile, dir, out) {
1530
+ const level = tile.level;
1531
+ const n = 1 << level;
1532
+ let nx = tile.x;
1533
+ let ny = tile.y;
1534
+ switch (dir) {
1535
+ case 0:
1536
+ nx -= 1;
1537
+ break;
1538
+ case 1:
1539
+ nx += 1;
1540
+ break;
1541
+ case 2:
1542
+ ny -= 1;
1543
+ break;
1544
+ case 3:
1545
+ ny += 1;
1546
+ break;
1547
+ }
1548
+ if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
1549
+ out.space = tile.space;
1550
+ out.level = level;
1551
+ out.x = nx;
1552
+ out.y = ny;
1553
+ return true;
1554
+ }
1555
+ crossFaceNeighbor(tile.space, level, nx, ny, out);
1556
+ return true;
1372
1557
  },
1373
- tileBounds(_tile, _cameraOrigin, out) {
1374
- out.cx = 0;
1375
- out.cy = 0;
1376
- out.cz = 0;
1377
- out.r = Number.MAX_VALUE;
1558
+ tileBounds(tile, cameraOrigin, out) {
1559
+ const level = tile.level;
1560
+ const n = 1 << level;
1561
+ const u0 = tile.x / n;
1562
+ const u1 = (tile.x + 1) / n;
1563
+ const v0 = tile.y / n;
1564
+ const v1 = (tile.y + 1) / n;
1565
+ const cornersU = [u0, u1, u0, u1];
1566
+ const cornersV = [v0, v0, v1, v1];
1567
+ let sumX = 0;
1568
+ let sumY = 0;
1569
+ let sumZ = 0;
1570
+ const px = [0, 0, 0, 0];
1571
+ const py = [0, 0, 0, 0];
1572
+ const pz = [0, 0, 0, 0];
1573
+ for (let i = 0; i < 4; i++) {
1574
+ faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
1575
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
1576
+ const sx = center.x + cube[0] / len * radius;
1577
+ const sy = center.y + cube[1] / len * radius;
1578
+ const sz = center.z + cube[2] / len * radius;
1579
+ px[i] = sx;
1580
+ py[i] = sy;
1581
+ pz[i] = sz;
1582
+ sumX += sx;
1583
+ sumY += sy;
1584
+ sumZ += sz;
1585
+ }
1586
+ const cX = sumX * 0.25;
1587
+ const cY = sumY * 0.25;
1588
+ const cZ = sumZ * 0.25;
1589
+ let maxDistSq = 0;
1590
+ for (let i = 0; i < 4; i++) {
1591
+ const dx = px[i] - cX;
1592
+ const dy = py[i] - cY;
1593
+ const dz = pz[i] - cZ;
1594
+ const dSq = dx * dx + dy * dy + dz * dz;
1595
+ if (dSq > maxDistSq) maxDistSq = dSq;
1596
+ }
1597
+ out.cx = cX - cameraOrigin.x;
1598
+ out.cy = cY - cameraOrigin.y;
1599
+ out.cz = cZ - cameraOrigin.z;
1600
+ out.r = Math.sqrt(maxDistSq) + maxHeight;
1378
1601
  },
1379
1602
  rootTiles(_cameraOrigin, out) {
1380
1603
  for (let s = 0; s < 6; s++) {
@@ -1385,10 +1608,141 @@ function createCubeSphereSurface(_cfg) {
1385
1608
  root.y = 0;
1386
1609
  }
1387
1610
  return 6;
1388
- }
1611
+ },
1612
+ projection: "cubeSphere",
1613
+ radius,
1614
+ center
1389
1615
  };
1390
1616
  }
1391
1617
 
1618
+ function readHeight(elevation, shape, leafIndex, ix, iy) {
1619
+ const base = leafIndex * shape.verticesPerNode;
1620
+ return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
1621
+ }
1622
+ function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
1623
+ const max = shape.edgeVertexCount - 1;
1624
+ const x = Math.max(0, Math.min(max, gx));
1625
+ const y = Math.max(0, Math.min(max, gy));
1626
+ const x0 = Math.floor(x);
1627
+ const y0 = Math.floor(y);
1628
+ const x1 = Math.min(max, x0 + 1);
1629
+ const y1 = Math.min(max, y0 + 1);
1630
+ const tx = x - x0;
1631
+ const ty = y - y0;
1632
+ const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
1633
+ const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
1634
+ const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
1635
+ const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
1636
+ const hx0 = h00 + (h10 - h00) * tx;
1637
+ const hx1 = h01 + (h11 - h01) * tx;
1638
+ return hx0 + (hx1 - hx0) * ty;
1639
+ }
1640
+ function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
1641
+ const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
1642
+ const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
1643
+ const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
1644
+ const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
1645
+ const inv2Step = 0.5 / stepWorld;
1646
+ out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
1647
+ out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
1648
+ return out;
1649
+ }
1650
+
1651
+ const MISSED_LOOKUP = Object.freeze({
1652
+ found: false,
1653
+ leafIndex: -1,
1654
+ space: -1,
1655
+ level: -1,
1656
+ tileX: -1,
1657
+ tileY: -1,
1658
+ tileSize: 0,
1659
+ localU: 0,
1660
+ localV: 0
1661
+ });
1662
+ function lookupTile(index, config, worldX, worldZ) {
1663
+ const halfRoot = config.rootSize * 0.5;
1664
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
1665
+ const scale = 2 ** level;
1666
+ const tileSize = config.rootSize / scale;
1667
+ const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
1668
+ const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
1669
+ const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
1670
+ if (leafIndex !== U32_EMPTY) {
1671
+ const tileMinX = config.originX + tileX * tileSize - halfRoot;
1672
+ const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
1673
+ return {
1674
+ found: true,
1675
+ leafIndex,
1676
+ space: 0,
1677
+ level,
1678
+ tileX,
1679
+ tileY,
1680
+ tileSize,
1681
+ localU: (worldX - tileMinX) / tileSize,
1682
+ localV: (worldZ - tileMinZ) / tileSize
1683
+ };
1684
+ }
1685
+ }
1686
+ return MISSED_LOOKUP;
1687
+ }
1688
+ function clamp01(value) {
1689
+ return value < 0 ? 0 : value > 1 ? 1 : value;
1690
+ }
1691
+ function lookupTileByFaceUV(index, config, face, u, v) {
1692
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
1693
+ const n = 2 ** level;
1694
+ let tileX = Math.floor(u * n);
1695
+ let tileY = Math.floor(v * n);
1696
+ if (tileX < 0) tileX = 0;
1697
+ else if (tileX > n - 1) tileX = n - 1;
1698
+ if (tileY < 0) tileY = 0;
1699
+ else if (tileY > n - 1) tileY = n - 1;
1700
+ const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
1701
+ if (leafIndex !== U32_EMPTY) {
1702
+ const tileSize = sphereTileArcLength(config.radius, n);
1703
+ return {
1704
+ found: true,
1705
+ leafIndex,
1706
+ space: face,
1707
+ level,
1708
+ tileX,
1709
+ tileY,
1710
+ tileSize,
1711
+ localU: clamp01(u * n - tileX),
1712
+ localV: clamp01(v * n - tileY)
1713
+ };
1714
+ }
1715
+ }
1716
+ return MISSED_LOOKUP;
1717
+ }
1718
+ function lookupTileForDirection(index, config, dx, dy, dz, dirScratch, uvScratch) {
1719
+ if (config.projection !== "cubeSphere") return MISSED_LOOKUP;
1720
+ const len = Math.hypot(dx, dy, dz);
1721
+ if (len === 0) return MISSED_LOOKUP;
1722
+ dirScratch[0] = dx / len;
1723
+ dirScratch[1] = dy / len;
1724
+ dirScratch[2] = dz / len;
1725
+ const face = directionToFace(dirScratch);
1726
+ directionToFaceUV(face, dirScratch, uvScratch);
1727
+ return lookupTileByFaceUV(index, config, face, uvScratch[0], uvScratch[1]);
1728
+ }
1729
+
1730
+ function createTerrainSnapshotState(maxNodes, totalElements) {
1731
+ return {
1732
+ frontElevation: new Float32Array(totalElements),
1733
+ backElevation: new Float32Array(totalElements),
1734
+ frontIndex: createSpatialIndex(maxNodes),
1735
+ backIndex: createSpatialIndex(maxNodes),
1736
+ frontTileBounds: new Float32Array(maxNodes * 2),
1737
+ backTileBounds: new Float32Array(maxNodes * 2),
1738
+ frontLeafCount: 0,
1739
+ globalRange: null,
1740
+ hasSnapshot: false,
1741
+ readbackPending: false,
1742
+ generation: 0,
1743
+ lastScheduledStampGen: -1
1744
+ };
1745
+ }
1392
1746
  function cloneSpatialIndex(target, source) {
1393
1747
  if (target.size !== source.size) {
1394
1748
  throw new Error(
@@ -1404,218 +1758,278 @@ function cloneSpatialIndex(target, source) {
1404
1758
  target.keysY.set(source.keysY);
1405
1759
  target.values.set(source.values);
1406
1760
  }
1407
- function tileLocalToFieldUV(localCoord, innerSegments) {
1408
- const edge = innerSegments + 3;
1409
- return (localCoord * innerSegments + 1.5) / edge;
1761
+ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
1762
+ if (state.readbackPending) return;
1763
+ const withReadback = renderer;
1764
+ if (!withReadback.getArrayBufferAsync) return;
1765
+ if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
1766
+ cloneSpatialIndex(state.backIndex, spatialIndex);
1767
+ state.lastScheduledStampGen = spatialIndex.stampGen;
1768
+ const { activeLeafCount, totalElements, elevationScale, originY } = captured;
1769
+ state.readbackPending = true;
1770
+ const elevationPromise = withReadback.getArrayBufferAsync(attribute);
1771
+ const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
1772
+ const onComplete = (elevResult, boundsResult) => {
1773
+ const data = new Float32Array(elevResult);
1774
+ state.backElevation.fill(0);
1775
+ state.backElevation.set(data.subarray(0, totalElements));
1776
+ let boundsValid = activeLeafCount === 0;
1777
+ if (boundsResult) {
1778
+ const rawBounds = new Float32Array(boundsResult);
1779
+ state.backTileBounds.fill(0);
1780
+ state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
1781
+ for (let i = 0; i < activeLeafCount; i += 1) {
1782
+ if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
1783
+ boundsValid = true;
1784
+ break;
1785
+ }
1786
+ }
1787
+ }
1788
+ const oldFrontElevation = state.frontElevation;
1789
+ const oldFrontIndex = state.frontIndex;
1790
+ state.frontElevation = state.backElevation;
1791
+ state.frontIndex = state.backIndex;
1792
+ state.frontLeafCount = activeLeafCount;
1793
+ state.backElevation = oldFrontElevation;
1794
+ state.backIndex = oldFrontIndex;
1795
+ if (boundsResult && boundsValid) {
1796
+ const oldFrontBounds = state.frontTileBounds;
1797
+ state.frontTileBounds = state.backTileBounds;
1798
+ state.backTileBounds = oldFrontBounds;
1799
+ }
1800
+ if (boundsResult && boundsValid && activeLeafCount > 0) {
1801
+ let gMin = Infinity;
1802
+ let gMax = -Infinity;
1803
+ for (let i = 0; i < activeLeafCount; i++) {
1804
+ const rawMin = state.frontTileBounds[i * 2];
1805
+ const rawMax = state.frontTileBounds[i * 2 + 1];
1806
+ const a = originY + rawMin * elevationScale;
1807
+ const b = originY + rawMax * elevationScale;
1808
+ gMin = Math.min(gMin, a, b);
1809
+ gMax = Math.max(gMax, a, b);
1810
+ }
1811
+ state.globalRange = { min: gMin, max: gMax };
1812
+ }
1813
+ state.hasSnapshot = true;
1814
+ state.generation += 1;
1815
+ };
1816
+ if (boundsPromise) {
1817
+ Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
1818
+ state.readbackPending = false;
1819
+ });
1820
+ } else {
1821
+ elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
1822
+ state.readbackPending = false;
1823
+ });
1824
+ }
1410
1825
  }
1826
+
1411
1827
  function createCpuTerrainCache(maxNodes, initialConfig) {
1412
1828
  let config = initialConfig;
1413
- let edgeVertexCount = config.innerTileSegments + 3;
1414
- let verticesPerNode = edgeVertexCount * edgeVertexCount;
1415
- let totalElements = maxNodes * verticesPerNode;
1416
- let frontElevation = new Float32Array(totalElements);
1417
- let backElevation = new Float32Array(totalElements);
1418
- let frontIndex = createSpatialIndex(maxNodes);
1419
- let backIndex = createSpatialIndex(maxNodes);
1420
- let frontTileBounds = new Float32Array(maxNodes * 2);
1421
- let backTileBounds = new Float32Array(maxNodes * 2);
1422
- let frontLeafCount = 0;
1423
- let globalRange = null;
1424
- let hasSnapshot = false;
1425
- let readbackPending = false;
1426
- let generationCount = 0;
1427
- let lastScheduledStampGen = -1;
1428
- const readHeight = (leafIndex, ix, iy) => {
1429
- const base = leafIndex * verticesPerNode;
1430
- return frontElevation[base + iy * edgeVertexCount + ix] ?? 0;
1829
+ const shape = {
1830
+ edgeVertexCount: config.innerTileSegments + 3,
1831
+ verticesPerNode: 0
1431
1832
  };
1432
- const sampleGridBilinear = (leafIndex, gx, gy) => {
1433
- const max = edgeVertexCount - 1;
1434
- const x = Math.max(0, Math.min(max, gx));
1435
- const y = Math.max(0, Math.min(max, gy));
1436
- const x0 = Math.floor(x);
1437
- const y0 = Math.floor(y);
1438
- const x1 = Math.min(max, x0 + 1);
1439
- const y1 = Math.min(max, y0 + 1);
1440
- const tx = x - x0;
1441
- const ty = y - y0;
1442
- const h00 = readHeight(leafIndex, x0, y0);
1443
- const h10 = readHeight(leafIndex, x1, y0);
1444
- const h01 = readHeight(leafIndex, x0, y1);
1445
- const h11 = readHeight(leafIndex, x1, y1);
1446
- const hx0 = h00 + (h10 - h00) * tx;
1447
- const hx1 = h01 + (h11 - h01) * tx;
1448
- return hx0 + (hx1 - hx0) * ty;
1833
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
1834
+ let totalElements = maxNodes * shape.verticesPerNode;
1835
+ const state = createTerrainSnapshotState(
1836
+ maxNodes,
1837
+ totalElements
1838
+ );
1839
+ const dirScratch = [0, 0, 0];
1840
+ const uvScratch = [0, 0];
1841
+ const llScratch = [0, 0, 0];
1842
+ const gridScratch = { gx: 0, gy: 0 };
1843
+ const gradientScratch = { dhdu: 0, dhdv: 0 };
1844
+ const gridCoordsFromLookup = (lookup) => {
1845
+ const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
1846
+ const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
1847
+ gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
1848
+ gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
1849
+ return gridScratch;
1850
+ };
1851
+ const rawHeightFromLookup = (lookup) => {
1852
+ const g = gridCoordsFromLookup(lookup);
1853
+ return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
1449
1854
  };
1450
1855
  const computeNormal = (leafIndex, gx, gy, tileSize) => {
1451
- const hLeft = sampleGridBilinear(leafIndex, gx - 1, gy);
1452
- const hRight = sampleGridBilinear(leafIndex, gx + 1, gy);
1453
- const hUp = sampleGridBilinear(leafIndex, gx, gy - 1);
1454
- const hDown = sampleGridBilinear(leafIndex, gx, gy + 1);
1455
1856
  const stepWorld = tileSize / config.innerTileSegments;
1456
- const inv2Step = 0.5 / stepWorld;
1457
- const dhdx = (hRight - hLeft) * config.elevationScale * inv2Step;
1458
- const dhdz = (hDown - hUp) * config.elevationScale * inv2Step;
1459
- return new three.Vector3(-dhdx, 1, -dhdz).normalize();
1857
+ const { dhdu, dhdv } = elevationGradientAt(
1858
+ state.frontElevation,
1859
+ shape,
1860
+ leafIndex,
1861
+ gx,
1862
+ gy,
1863
+ stepWorld,
1864
+ config.elevationScale,
1865
+ gradientScratch
1866
+ );
1867
+ return new three.Vector3(-dhdu, 1, -dhdv).normalize();
1460
1868
  };
1461
- const lookupTile = (worldX, worldZ) => {
1462
- const halfRoot = config.rootSize * 0.5;
1463
- for (let level = config.maxLevel; level >= 0; level -= 1) {
1464
- const scale = 2 ** level;
1465
- const tileSize = config.rootSize / scale;
1466
- const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
1467
- const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
1468
- const leafIndex = lookupSpatialIndexRaw(
1469
- frontIndex,
1470
- 0,
1471
- level,
1472
- tileX,
1473
- tileY
1474
- );
1475
- if (leafIndex !== U32_EMPTY) {
1476
- const tileMinX = config.originX + tileX * tileSize - halfRoot;
1477
- const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
1478
- return {
1479
- found: true,
1480
- leafIndex,
1481
- level,
1482
- tileX,
1483
- tileY,
1484
- tileSize,
1485
- localU: (worldX - tileMinX) / tileSize,
1486
- localV: (worldZ - tileMinZ) / tileSize
1487
- };
1488
- }
1489
- }
1490
- return {
1491
- found: false,
1492
- leafIndex: -1,
1493
- level: -1,
1494
- tileX: -1,
1495
- tileY: -1,
1496
- tileSize: 0,
1497
- localU: 0,
1498
- localV: 0
1499
- };
1869
+ const computeSphereNormal = (leafIndex, gx, gy, tileSize, face, dirX, dirY, dirZ) => {
1870
+ const stepWorld = tileSize / config.innerTileSegments;
1871
+ const { dhdu, dhdv } = elevationGradientAt(
1872
+ state.frontElevation,
1873
+ shape,
1874
+ leafIndex,
1875
+ gx,
1876
+ gy,
1877
+ stepWorld,
1878
+ config.elevationScale,
1879
+ gradientScratch
1880
+ );
1881
+ const f = CUBE_FACES[face];
1882
+ const dDotR = dirX * f.right[0] + dirY * f.right[1] + dirZ * f.right[2];
1883
+ let tux = f.right[0] - dirX * dDotR;
1884
+ let tuy = f.right[1] - dirY * dDotR;
1885
+ let tuz = f.right[2] - dirZ * dDotR;
1886
+ const tuLen = Math.hypot(tux, tuy, tuz) || 1;
1887
+ tux /= tuLen;
1888
+ tuy /= tuLen;
1889
+ tuz /= tuLen;
1890
+ const dDotU = dirX * f.up[0] + dirY * f.up[1] + dirZ * f.up[2];
1891
+ let tvx = f.up[0] - dirX * dDotU;
1892
+ let tvy = f.up[1] - dirY * dDotU;
1893
+ let tvz = f.up[2] - dirZ * dDotU;
1894
+ const tvLen = Math.hypot(tvx, tvy, tvz) || 1;
1895
+ tvx /= tvLen;
1896
+ tvy /= tvLen;
1897
+ tvz /= tvLen;
1898
+ const nx = -dhdu;
1899
+ const ny = 1;
1900
+ const nz = -dhdv;
1901
+ return new three.Vector3(
1902
+ tux * nx + dirX * ny + tvx * nz,
1903
+ tuy * nx + dirY * ny + tvy * nz,
1904
+ tuz * nx + dirZ * ny + tvz * nz
1905
+ ).normalize();
1500
1906
  };
1501
1907
  const sampleFromLookup = (lookup) => {
1502
- const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
1503
- const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
1504
- const gx = fieldU * (edgeVertexCount - 1);
1505
- const gy = fieldV * (edgeVertexCount - 1);
1506
- const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
1908
+ const height = rawHeightFromLookup(lookup);
1507
1909
  const scaledHeight = config.originY + height * config.elevationScale;
1508
- const normal = computeNormal(lookup.leafIndex, gx, gy, lookup.tileSize);
1910
+ const normal = computeNormal(
1911
+ lookup.leafIndex,
1912
+ gridScratch.gx,
1913
+ gridScratch.gy,
1914
+ lookup.tileSize
1915
+ );
1509
1916
  return { elevation: scaledHeight, normal, valid: true };
1510
1917
  };
1511
- const sampleElevationFromLookup = (lookup) => {
1512
- const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
1513
- const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
1514
- const gx = fieldU * (edgeVertexCount - 1);
1515
- const gy = fieldV * (edgeVertexCount - 1);
1516
- const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
1517
- const scaledHeight = config.originY + height * config.elevationScale;
1518
- return { elevation: scaledHeight, valid: true };
1519
- };
1520
1918
  const sampleTerrain = (worldX, worldZ) => {
1521
- if (!hasSnapshot) {
1919
+ if (!state.hasSnapshot) {
1522
1920
  return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1523
1921
  }
1524
- const lookup = lookupTile(worldX, worldZ);
1922
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1525
1923
  if (!lookup.found) {
1526
1924
  return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1527
1925
  }
1528
1926
  return sampleFromLookup(lookup);
1529
1927
  };
1530
1928
  const getElevation = (worldX, worldZ) => {
1531
- if (!hasSnapshot) {
1929
+ if (!state.hasSnapshot) {
1532
1930
  return { elevation: 0, valid: false };
1533
1931
  }
1534
- const lookup = lookupTile(worldX, worldZ);
1932
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1535
1933
  if (!lookup.found) {
1536
1934
  return { elevation: 0, valid: false };
1537
1935
  }
1538
- return sampleElevationFromLookup(lookup);
1936
+ const height = rawHeightFromLookup(lookup);
1937
+ return {
1938
+ elevation: config.originY + height * config.elevationScale,
1939
+ valid: true
1940
+ };
1941
+ };
1942
+ const invalidSurfaceSample = (dx, dy, dz) => ({
1943
+ position: new three.Vector3(),
1944
+ normal: new three.Vector3(0, 1, 0),
1945
+ direction: new three.Vector3(dx, dy, dz),
1946
+ elevation: 0,
1947
+ valid: false
1948
+ });
1949
+ const lookupDirection = (dx, dy, dz) => lookupTileForDirection(state.frontIndex, config, dx, dy, dz, dirScratch, uvScratch);
1950
+ const sampleSurfaceByDirection = (dx, dy, dz) => {
1951
+ if (!state.hasSnapshot || config.projection !== "cubeSphere") {
1952
+ return invalidSurfaceSample(dx, dy, dz);
1953
+ }
1954
+ const len = Math.hypot(dx, dy, dz);
1955
+ if (len === 0) return invalidSurfaceSample(0, 0, 0);
1956
+ const nx = dx / len;
1957
+ const ny = dy / len;
1958
+ const nz = dz / len;
1959
+ const lookup = lookupDirection(nx, ny, nz);
1960
+ if (!lookup.found) return invalidSurfaceSample(nx, ny, nz);
1961
+ const height = rawHeightFromLookup(lookup);
1962
+ const elevation = height * config.elevationScale;
1963
+ const r = config.radius + elevation;
1964
+ const position = new three.Vector3(
1965
+ config.originX + nx * r,
1966
+ config.originY + ny * r,
1967
+ config.originZ + nz * r
1968
+ );
1969
+ const normal = computeSphereNormal(
1970
+ lookup.leafIndex,
1971
+ gridScratch.gx,
1972
+ gridScratch.gy,
1973
+ lookup.tileSize,
1974
+ lookup.space,
1975
+ nx,
1976
+ ny,
1977
+ nz
1978
+ );
1979
+ return {
1980
+ position,
1981
+ normal,
1982
+ direction: new three.Vector3(nx, ny, nz),
1983
+ elevation,
1984
+ valid: true
1985
+ };
1986
+ };
1987
+ const tileFromLookup = (lookup) => {
1988
+ if (!lookup.found) return null;
1989
+ return {
1990
+ space: lookup.space,
1991
+ level: lookup.level,
1992
+ x: lookup.tileX,
1993
+ y: lookup.tileY,
1994
+ index: lookup.leafIndex
1995
+ };
1996
+ };
1997
+ const tileBoundsFromLookup = (lookup, elevationBase) => {
1998
+ if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
1999
+ const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
2000
+ const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
2001
+ const a = elevationBase + rawMin * config.elevationScale;
2002
+ const b = elevationBase + rawMax * config.elevationScale;
2003
+ return {
2004
+ space: lookup.space,
2005
+ level: lookup.level,
2006
+ x: lookup.tileX,
2007
+ y: lookup.tileY,
2008
+ index: lookup.leafIndex,
2009
+ minElevation: Math.min(a, b),
2010
+ maxElevation: Math.max(a, b)
2011
+ };
1539
2012
  };
1540
2013
  const api = {
1541
2014
  get generation() {
1542
- return generationCount;
2015
+ return state.generation;
1543
2016
  },
1544
2017
  get ready() {
1545
- return hasSnapshot;
2018
+ return state.hasSnapshot;
1546
2019
  },
1547
2020
  updateConfig(nextConfig) {
1548
2021
  config = nextConfig;
1549
- edgeVertexCount = config.innerTileSegments + 3;
1550
- verticesPerNode = edgeVertexCount * edgeVertexCount;
1551
- totalElements = maxNodes * verticesPerNode;
2022
+ shape.edgeVertexCount = config.innerTileSegments + 3;
2023
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
2024
+ totalElements = maxNodes * shape.verticesPerNode;
1552
2025
  },
1553
2026
  triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
1554
- if (readbackPending) return;
1555
- const withReadback = renderer;
1556
- if (!withReadback.getArrayBufferAsync) return;
1557
- if (spatialIndex.stampGen === lastScheduledStampGen) return;
1558
- cloneSpatialIndex(backIndex, spatialIndex);
1559
- lastScheduledStampGen = spatialIndex.stampGen;
1560
- const capturedLeafCount = activeLeafCount ?? 0;
1561
- const capturedScale = config.elevationScale;
1562
- const capturedOriginY = config.originY;
1563
- readbackPending = true;
1564
- const elevationPromise = withReadback.getArrayBufferAsync(attribute);
1565
- const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
1566
- const onComplete = (elevResult, boundsResult) => {
1567
- const data = new Float32Array(elevResult);
1568
- backElevation.fill(0);
1569
- backElevation.set(data.subarray(0, totalElements));
1570
- let boundsValid = capturedLeafCount === 0;
1571
- if (boundsResult) {
1572
- const rawBounds = new Float32Array(boundsResult);
1573
- backTileBounds.fill(0);
1574
- backTileBounds.set(rawBounds.subarray(0, capturedLeafCount * 2));
1575
- for (let i = 0; i < capturedLeafCount; i += 1) {
1576
- if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
1577
- boundsValid = true;
1578
- break;
1579
- }
1580
- }
1581
- }
1582
- const oldFrontElevation = frontElevation;
1583
- const oldFrontIndex = frontIndex;
1584
- frontElevation = backElevation;
1585
- frontIndex = backIndex;
1586
- frontLeafCount = capturedLeafCount;
1587
- backElevation = oldFrontElevation;
1588
- backIndex = oldFrontIndex;
1589
- if (boundsResult && boundsValid) {
1590
- const oldFrontBounds = frontTileBounds;
1591
- frontTileBounds = backTileBounds;
1592
- backTileBounds = oldFrontBounds;
1593
- }
1594
- if (boundsResult && boundsValid && capturedLeafCount > 0) {
1595
- let gMin = Infinity;
1596
- let gMax = -Infinity;
1597
- for (let i = 0; i < capturedLeafCount; i++) {
1598
- const rawMin = frontTileBounds[i * 2];
1599
- const rawMax = frontTileBounds[i * 2 + 1];
1600
- const a = capturedOriginY + rawMin * capturedScale;
1601
- const b = capturedOriginY + rawMax * capturedScale;
1602
- gMin = Math.min(gMin, a, b);
1603
- gMax = Math.max(gMax, a, b);
1604
- }
1605
- globalRange = { min: gMin, max: gMax };
1606
- }
1607
- hasSnapshot = true;
1608
- generationCount += 1;
1609
- };
1610
- if (boundsPromise) {
1611
- Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
1612
- readbackPending = false;
1613
- });
1614
- } else {
1615
- elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
1616
- readbackPending = false;
1617
- });
1618
- }
2027
+ triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
2028
+ activeLeafCount: activeLeafCount ?? 0,
2029
+ totalElements,
2030
+ elevationScale: config.elevationScale,
2031
+ originY: config.originY
2032
+ });
1619
2033
  },
1620
2034
  getElevation(worldX, worldZ) {
1621
2035
  const sample = getElevation(worldX, worldZ);
@@ -1625,43 +2039,26 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1625
2039
  return sampleTerrain(worldX, worldZ).normal;
1626
2040
  },
1627
2041
  getTile(worldX, worldZ) {
1628
- if (!hasSnapshot) return null;
1629
- const lookup = lookupTile(worldX, worldZ);
1630
- if (!lookup.found) return null;
1631
- return {
1632
- level: lookup.level,
1633
- x: lookup.tileX,
1634
- y: lookup.tileY,
1635
- index: lookup.leafIndex
1636
- };
2042
+ if (!state.hasSnapshot) return null;
2043
+ return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
1637
2044
  },
1638
2045
  getTileBounds(worldX, worldZ) {
1639
- if (!hasSnapshot) return null;
1640
- const lookup = lookupTile(worldX, worldZ);
1641
- if (!lookup.found || lookup.leafIndex >= frontLeafCount) return null;
1642
- const rawMin = frontTileBounds[lookup.leafIndex * 2];
1643
- const rawMax = frontTileBounds[lookup.leafIndex * 2 + 1];
1644
- const a = config.originY + rawMin * config.elevationScale;
1645
- const b = config.originY + rawMax * config.elevationScale;
1646
- return {
1647
- level: lookup.level,
1648
- x: lookup.tileX,
1649
- y: lookup.tileY,
1650
- index: lookup.leafIndex,
1651
- minElevation: Math.min(a, b),
1652
- maxElevation: Math.max(a, b)
1653
- };
2046
+ if (!state.hasSnapshot) return null;
2047
+ return tileBoundsFromLookup(
2048
+ lookupTile(state.frontIndex, config, worldX, worldZ),
2049
+ config.originY
2050
+ );
1654
2051
  },
1655
2052
  getGlobalElevationRange() {
1656
- return globalRange;
2053
+ return state.globalRange;
1657
2054
  },
1658
2055
  sampleTerrainBatch(positions) {
1659
2056
  const count = Math.floor(positions.length / 2);
1660
2057
  const elevations = new Float32Array(count);
1661
2058
  const normals = new Float32Array(count * 3);
1662
2059
  const valid = new Uint8Array(count);
1663
- if (!hasSnapshot) {
1664
- return { elevations, normals, valid, generation: generationCount };
2060
+ if (!state.hasSnapshot) {
2061
+ return { elevations, normals, valid, generation: state.generation };
1665
2062
  }
1666
2063
  let lastTile;
1667
2064
  for (let i = 0; i < count; i += 1) {
@@ -1672,6 +2069,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1672
2069
  lookup = {
1673
2070
  found: true,
1674
2071
  leafIndex: lastTile.leafIndex,
2072
+ space: 0,
1675
2073
  level: lastTile.level,
1676
2074
  tileX: lastTile.tileX,
1677
2075
  tileY: lastTile.tileY,
@@ -1680,7 +2078,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1680
2078
  localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
1681
2079
  };
1682
2080
  } else {
1683
- lookup = lookupTile(worldX, worldZ);
2081
+ lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1684
2082
  if (lookup.found) {
1685
2083
  lastTile = {
1686
2084
  leafIndex: lookup.leafIndex,
@@ -1706,9 +2104,133 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1706
2104
  normals[i * 3 + 2] = sample.normal.z;
1707
2105
  valid[i] = 1;
1708
2106
  }
1709
- return { elevations, normals, valid, generation: generationCount };
2107
+ return { elevations, normals, valid, generation: state.generation };
2108
+ },
2109
+ sampleTerrain,
2110
+ // --- Cube-sphere queries ---
2111
+ sampleTerrainByDirection(direction) {
2112
+ return sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2113
+ },
2114
+ sampleTerrainByPosition(position) {
2115
+ return sampleSurfaceByDirection(
2116
+ position.x - config.originX,
2117
+ position.y - config.originY,
2118
+ position.z - config.originZ
2119
+ );
2120
+ },
2121
+ sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
2122
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2123
+ return sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
1710
2124
  },
1711
- sampleTerrain
2125
+ getElevationByDirection(direction) {
2126
+ const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2127
+ return sample.valid ? sample.elevation : null;
2128
+ },
2129
+ getElevationByPosition(position) {
2130
+ const sample = sampleSurfaceByDirection(
2131
+ position.x - config.originX,
2132
+ position.y - config.originY,
2133
+ position.z - config.originZ
2134
+ );
2135
+ return sample.valid ? sample.elevation : null;
2136
+ },
2137
+ getElevationByLatLong(latitudeDeg, longitudeDeg) {
2138
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2139
+ const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
2140
+ return sample.valid ? sample.elevation : null;
2141
+ },
2142
+ getNormalByDirection(direction) {
2143
+ const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2144
+ return sample.valid ? sample.normal : null;
2145
+ },
2146
+ getNormalByPosition(position) {
2147
+ const sample = sampleSurfaceByDirection(
2148
+ position.x - config.originX,
2149
+ position.y - config.originY,
2150
+ position.z - config.originZ
2151
+ );
2152
+ return sample.valid ? sample.normal : null;
2153
+ },
2154
+ getNormalByLatLong(latitudeDeg, longitudeDeg) {
2155
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2156
+ const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
2157
+ return sample.valid ? sample.normal : null;
2158
+ },
2159
+ getTileByDirection(direction) {
2160
+ if (!state.hasSnapshot) return null;
2161
+ return tileFromLookup(lookupDirection(direction.x, direction.y, direction.z));
2162
+ },
2163
+ getTileByPosition(position) {
2164
+ if (!state.hasSnapshot) return null;
2165
+ return tileFromLookup(
2166
+ lookupDirection(
2167
+ position.x - config.originX,
2168
+ position.y - config.originY,
2169
+ position.z - config.originZ
2170
+ )
2171
+ );
2172
+ },
2173
+ getTileByLatLong(latitudeDeg, longitudeDeg) {
2174
+ if (!state.hasSnapshot) return null;
2175
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2176
+ return tileFromLookup(lookupDirection(llScratch[0], llScratch[1], llScratch[2]));
2177
+ },
2178
+ getTileBoundsByDirection(direction) {
2179
+ if (!state.hasSnapshot) return null;
2180
+ return tileBoundsFromLookup(
2181
+ lookupDirection(direction.x, direction.y, direction.z),
2182
+ 0
2183
+ );
2184
+ },
2185
+ getTileBoundsByPosition(position) {
2186
+ if (!state.hasSnapshot) return null;
2187
+ return tileBoundsFromLookup(
2188
+ lookupDirection(
2189
+ position.x - config.originX,
2190
+ position.y - config.originY,
2191
+ position.z - config.originZ
2192
+ ),
2193
+ 0
2194
+ );
2195
+ },
2196
+ getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
2197
+ if (!state.hasSnapshot) return null;
2198
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2199
+ return tileBoundsFromLookup(
2200
+ lookupDirection(llScratch[0], llScratch[1], llScratch[2]),
2201
+ 0
2202
+ );
2203
+ },
2204
+ sampleTerrainBatchByDirection(directions) {
2205
+ const count = Math.floor(directions.length / 3);
2206
+ const positions = new Float32Array(count * 3);
2207
+ const normals = new Float32Array(count * 3);
2208
+ const elevations = new Float32Array(count);
2209
+ const valid = new Uint8Array(count);
2210
+ if (!state.hasSnapshot || config.projection !== "cubeSphere") {
2211
+ return { positions, normals, elevations, valid, generation: state.generation };
2212
+ }
2213
+ for (let i = 0; i < count; i += 1) {
2214
+ const sample = sampleSurfaceByDirection(
2215
+ directions[i * 3] ?? 0,
2216
+ directions[i * 3 + 1] ?? 0,
2217
+ directions[i * 3 + 2] ?? 0
2218
+ );
2219
+ if (!sample.valid) {
2220
+ normals[i * 3 + 1] = 1;
2221
+ continue;
2222
+ }
2223
+ positions[i * 3] = sample.position.x;
2224
+ positions[i * 3 + 1] = sample.position.y;
2225
+ positions[i * 3 + 2] = sample.position.z;
2226
+ normals[i * 3] = sample.normal.x;
2227
+ normals[i * 3 + 1] = sample.normal.y;
2228
+ normals[i * 3 + 2] = sample.normal.z;
2229
+ elevations[i] = sample.elevation;
2230
+ valid[i] = 1;
2231
+ }
2232
+ return { positions, normals, elevations, valid, generation: state.generation };
2233
+ }
1712
2234
  };
1713
2235
  return api;
1714
2236
  }
@@ -1741,6 +2263,61 @@ function createTerrainQuery(cache) {
1741
2263
  }
1742
2264
  };
1743
2265
  }
2266
+ function createTerrainSphereQuery(cache) {
2267
+ return {
2268
+ get generation() {
2269
+ return cache.generation;
2270
+ },
2271
+ getElevationByDirection(direction) {
2272
+ return cache.getElevationByDirection(direction);
2273
+ },
2274
+ getElevationByPosition(position) {
2275
+ return cache.getElevationByPosition(position);
2276
+ },
2277
+ getElevationByLatLong(latitudeDeg, longitudeDeg) {
2278
+ return cache.getElevationByLatLong(latitudeDeg, longitudeDeg);
2279
+ },
2280
+ getNormalByDirection(direction) {
2281
+ return cache.getNormalByDirection(direction);
2282
+ },
2283
+ getNormalByPosition(position) {
2284
+ return cache.getNormalByPosition(position);
2285
+ },
2286
+ getNormalByLatLong(latitudeDeg, longitudeDeg) {
2287
+ return cache.getNormalByLatLong(latitudeDeg, longitudeDeg);
2288
+ },
2289
+ sampleTerrainByDirection(direction) {
2290
+ return cache.sampleTerrainByDirection(direction);
2291
+ },
2292
+ sampleTerrainByPosition(position) {
2293
+ return cache.sampleTerrainByPosition(position);
2294
+ },
2295
+ sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
2296
+ return cache.sampleTerrainByLatLong(latitudeDeg, longitudeDeg);
2297
+ },
2298
+ getTileByDirection(direction) {
2299
+ return cache.getTileByDirection(direction);
2300
+ },
2301
+ getTileByPosition(position) {
2302
+ return cache.getTileByPosition(position);
2303
+ },
2304
+ getTileByLatLong(latitudeDeg, longitudeDeg) {
2305
+ return cache.getTileByLatLong(latitudeDeg, longitudeDeg);
2306
+ },
2307
+ getTileBoundsByDirection(direction) {
2308
+ return cache.getTileBoundsByDirection(direction);
2309
+ },
2310
+ getTileBoundsByPosition(position) {
2311
+ return cache.getTileBoundsByPosition(position);
2312
+ },
2313
+ getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
2314
+ return cache.getTileBoundsByLatLong(latitudeDeg, longitudeDeg);
2315
+ },
2316
+ sampleTerrainBatchByDirection(directions) {
2317
+ return cache.sampleTerrainBatchByDirection(directions);
2318
+ }
2319
+ };
2320
+ }
1744
2321
 
1745
2322
  const WGSIZE = 64;
1746
2323
  function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
@@ -1810,8 +2387,11 @@ const terrainQueryTask = work.task((get, work) => {
1810
2387
  const rootSizeValue = get(rootSize);
1811
2388
  const originValue = get(origin);
1812
2389
  const elevationScaleValue = get(elevationScale);
2390
+ const radiusValue = get(radius);
2391
+ const topologyValue = get(topologyTask);
2392
+ const projectionValue = topologyValue.projection ?? "flat";
1813
2393
  return work((prev) => {
1814
- const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
2394
+ const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projectionValue}`;
1815
2395
  const configValues = {
1816
2396
  rootSize: rootSizeValue,
1817
2397
  originX: originValue.x,
@@ -1819,16 +2399,20 @@ const terrainQueryTask = work.task((get, work) => {
1819
2399
  originZ: originValue.z,
1820
2400
  innerTileSegments: innerTileSegmentsValue,
1821
2401
  elevationScale: elevationScaleValue,
1822
- maxLevel: maxLevelValue
2402
+ maxLevel: maxLevelValue,
2403
+ projection: projectionValue,
2404
+ radius: topologyValue.radius ?? radiusValue
1823
2405
  };
1824
2406
  let cache = prev?.cache;
1825
2407
  let query = prev?.query;
2408
+ let sphereQuery = prev?.sphereQuery ?? null;
1826
2409
  if (!cache || !query || prev?.shapeKey !== shapeKey) {
1827
2410
  cache = createCpuTerrainCache(maxNodesValue, configValues);
1828
2411
  query = createTerrainQuery(cache);
2412
+ sphereQuery = projectionValue === "cubeSphere" ? createTerrainSphereQuery(cache) : null;
1829
2413
  }
1830
2414
  cache.updateConfig(configValues);
1831
- return { cache, query, shapeKey };
2415
+ return { cache, query, sphereQuery, shapeKey };
1832
2416
  });
1833
2417
  }).displayName("terrainQueryTask");
1834
2418
  const terrainReadbackTask = work.task(
@@ -1851,38 +2435,44 @@ const terrainReadbackTask = work.task(
1851
2435
  }
1852
2436
  ).displayName("terrainReadbackTask").lane("gpu");
1853
2437
 
1854
- const surfaceTask = work.task((get, work) => {
1855
- const customSurface = get(surface);
2438
+ const topologyTask = work.task((get, work) => {
2439
+ const customTopology = get(topology);
1856
2440
  const rootSizeVal = get(rootSize);
1857
2441
  const originVal = get(origin);
1858
2442
  return work(() => {
1859
- if (customSurface) return customSurface;
1860
- return createFlatSurface({ rootSize: rootSizeVal, origin: originVal });
2443
+ if (customTopology) return customTopology;
2444
+ return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
1861
2445
  });
1862
- }).displayName("surfaceTask");
2446
+ }).displayName("topologyTask");
1863
2447
  const quadtreeConfigTask = work.task((get, work) => {
1864
- const surfaceVal = get(surfaceTask);
2448
+ const topologyVal = get(topologyTask);
1865
2449
  const maxNodesVal = get(maxNodes);
1866
2450
  const maxLevelVal = get(maxLevel);
1867
2451
  return work(() => {
1868
- const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surfaceVal);
2452
+ const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
1869
2453
  return {
1870
2454
  state,
1871
- surface: surfaceVal
2455
+ topology: topologyVal
1872
2456
  };
1873
2457
  });
1874
2458
  }).displayName("quadtreeConfigTask");
1875
2459
  const quadtreeUpdateTask = work.task((get, work) => {
1876
2460
  const quadtreeConfig = get(quadtreeConfigTask);
1877
2461
  const quadtreeUpdateConfig = get(quadtreeUpdate);
1878
- const { query: terrainQuery } = get(terrainQueryTask);
2462
+ const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
1879
2463
  let outLeaves = void 0;
2464
+ const cameraPosition = new three.Vector3();
1880
2465
  return work(() => {
1881
2466
  const cam = quadtreeUpdateConfig.cameraOrigin;
1882
- quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
2467
+ if (sphereQuery) {
2468
+ cameraPosition.set(cam.x, cam.y, cam.z);
2469
+ quadtreeUpdateConfig.elevationAtCameraXZ = sphereQuery.getElevationByPosition(cameraPosition) ?? 0;
2470
+ } else {
2471
+ quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
2472
+ }
1883
2473
  outLeaves = update(
1884
2474
  quadtreeConfig.state,
1885
- quadtreeConfig.surface,
2475
+ quadtreeConfig.topology,
1886
2476
  quadtreeUpdateConfig,
1887
2477
  outLeaves
1888
2478
  );
@@ -1904,7 +2494,7 @@ const leafGpuBufferTask = work.task((get, work) => {
1904
2494
  leafStorage.data[offset] = leafSet.level[i] ?? 0;
1905
2495
  leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
1906
2496
  leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
1907
- leafStorage.data[offset + 3] = 1;
2497
+ leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
1908
2498
  }
1909
2499
  leafStorage.attribute.needsUpdate = true;
1910
2500
  leafStorage.node.needsUpdate = true;
@@ -1946,12 +2536,14 @@ function createTerrainUniforms(params) {
1946
2536
  );
1947
2537
  const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
1948
2538
  const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
2539
+ const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
1949
2540
  return {
1950
2541
  uRootOrigin,
1951
2542
  uRootSize,
1952
2543
  uInnerTileSegments,
1953
2544
  uSkirtScale,
1954
- uElevationScale
2545
+ uElevationScale,
2546
+ uRadius
1955
2547
  };
1956
2548
  }
1957
2549
 
@@ -1965,6 +2557,7 @@ const createUniformsTask = work.task((get, work) => {
1965
2557
  innerTileSegments: get(innerTileSegments),
1966
2558
  skirtScale: get(skirtScale),
1967
2559
  elevationScale: get(elevationScale),
2560
+ radius: get(radius),
1968
2561
  instanceId: get(instanceIdTask)
1969
2562
  };
1970
2563
  return work(() => createTerrainUniforms(uniformParams));
@@ -1976,6 +2569,7 @@ const updateUniformsTask = work.task((get, work) => {
1976
2569
  const innerTileSegmentsVal = get(innerTileSegments);
1977
2570
  const skirtScaleVal = get(skirtScale);
1978
2571
  const elevationScaleVal = get(elevationScale);
2572
+ const radiusVal = get(radius);
1979
2573
  return work(() => {
1980
2574
  terrainUniformsContext.uRootSize.value = rootSizeVal;
1981
2575
  terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
@@ -1986,6 +2580,7 @@ const updateUniformsTask = work.task((get, work) => {
1986
2580
  terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
1987
2581
  terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
1988
2582
  terrainUniformsContext.uElevationScale.value = elevationScaleVal;
2583
+ terrainUniformsContext.uRadius.value = radiusVal;
1989
2584
  return terrainUniformsContext;
1990
2585
  });
1991
2586
  }).displayName("updateUniformsTask");
@@ -2008,8 +2603,9 @@ const createElevationFieldContextTask = work.task((get, work) => {
2008
2603
  const tileNodesTask = work.task((get, work) => {
2009
2604
  const leafStorage = get(leafStorageTask);
2010
2605
  const uniforms = get(updateUniformsTask);
2606
+ const topology = get(topologyTask);
2011
2607
  return work(() => {
2012
- return createTileCompute(leafStorage, uniforms);
2608
+ return createTileCompute(leafStorage, uniforms, topology.projection ?? "flat");
2013
2609
  });
2014
2610
  }).displayName("tileNodesTask");
2015
2611
  const elevationFieldStageTask = work.task((get, work) => {
@@ -2106,33 +2702,14 @@ const terrainFieldStageTask = work.task((get, work) => {
2106
2702
  });
2107
2703
  }).displayName("terrainFieldStageTask");
2108
2704
 
2109
- const compileComputeTask = work.task((get, work) => {
2110
- const pipeline = get(terrainFieldStageTask);
2111
- const edgeVertexCount = get(innerTileSegments) + 3;
2112
- return work(
2113
- () => compileComputePipeline(pipeline, edgeVertexCount, {
2114
- preferSingleKernelWhenPossible: false
2115
- })
2116
- );
2117
- }).displayName("compileComputeTask");
2118
- const executeComputeTask = work.task(
2119
- (get, work, { resources }) => {
2120
- const { execute } = get(compileComputeTask);
2121
- const leafState = get(leafGpuBufferTask);
2122
- return work(
2123
- () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
2124
- }
2125
- );
2126
- }
2127
- ).displayName("executeComputeTask").lane("gpu");
2705
+ const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
2128
2706
  function createComputePipelineTasks(leafStageTask) {
2129
2707
  const compile = work.task((get, work) => {
2130
2708
  const pipeline = get(leafStageTask);
2131
2709
  const edgeVertexCount = get(innerTileSegments) + 3;
2132
2710
  return work(
2133
2711
  () => compileComputePipeline(pipeline, edgeVertexCount, {
2134
- preferSingleKernelWhenPossible: false
2135
- })
2712
+ })
2136
2713
  );
2137
2714
  }).displayName("compileComputeTask");
2138
2715
  const execute = work.task(
@@ -2273,6 +2850,38 @@ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
2273
2850
  return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2274
2851
  });
2275
2852
  };
2853
+ const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
2854
+ const lookup = createGpuSpatialLookup(spatialIndex);
2855
+ const levelCount = Math.max(1, maxLevel + 1);
2856
+ return tsl.Fn(([direction]) => {
2857
+ const dir = tsl.vec3(direction).normalize().toVar();
2858
+ const face = cubeFaceFromDirection(dir).toVar();
2859
+ const basis = cubeFaceBasis(face);
2860
+ const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
2861
+ const u = faceUV.x.toVar();
2862
+ const v = faceUV.y.toVar();
2863
+ const tileIndex = tsl.int(-1).toVar();
2864
+ const tileU = tsl.float(0).toVar();
2865
+ const tileV = tsl.float(0).toVar();
2866
+ const i = tsl.int(0).toVar();
2867
+ tsl.Loop(levelCount, () => {
2868
+ const level = tsl.int(maxLevel).sub(i).toVar();
2869
+ const n = tsl.pow(tsl.float(2), level.toFloat()).toVar();
2870
+ const nInt = tsl.int(n).toVar();
2871
+ const tileX = u.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
2872
+ const tileY = v.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
2873
+ const maybeIndex = lookup(face, level, tileX, tileY).toVar();
2874
+ tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
2875
+ tileIndex.assign(maybeIndex);
2876
+ tileU.assign(u.mul(n).sub(tileX.toFloat()));
2877
+ tileV.assign(v.mul(n).sub(tileY.toFloat()));
2878
+ tsl.Break();
2879
+ });
2880
+ i.addAssign(1);
2881
+ });
2882
+ return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2883
+ });
2884
+ };
2276
2885
 
2277
2886
  const gpuSpatialIndexStorageTask = work.task((get, work) => {
2278
2887
  const maxNodesValue = get(maxNodes);
@@ -2288,43 +2897,44 @@ const gpuSpatialIndexUploadTask = work.task((get, work) => {
2288
2897
  });
2289
2898
  }).displayName("gpuSpatialIndexUploadTask");
2290
2899
 
2900
+ function packedSampleFromTileResult(params, tileResult) {
2901
+ const tileIndex = tsl.int(tileResult.x).toVar();
2902
+ const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
2903
+ const fieldU = tileLocalToFieldUV(
2904
+ tileResult.y,
2905
+ params.uniforms.uInnerTileSegments
2906
+ ).toVar();
2907
+ const fieldV = tileLocalToFieldUV(
2908
+ tileResult.z,
2909
+ params.uniforms.uInnerTileSegments
2910
+ ).toVar();
2911
+ const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
2912
+ const sampled = sampleTerrainField(
2913
+ params.terrainFieldStorage,
2914
+ fieldU,
2915
+ fieldV,
2916
+ safeTileIndex
2917
+ ).toVar();
2918
+ const normal = unpackTangentNormal(sampled.g, sampled.b);
2919
+ const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2920
+ return tsl.vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2921
+ }
2291
2922
  function createTerrainSampleNode(params) {
2292
2923
  const tileLookup = createTileIndexFromWorldPosition(
2293
2924
  params.spatialIndex,
2294
2925
  params.uniforms,
2295
- maxLevel.get()
2926
+ params.maxLevel
2296
2927
  );
2297
2928
  return tsl.Fn(([worldX, worldZ]) => {
2298
2929
  const tileResult = tileLookup(worldX, worldZ).toVar();
2299
- const tileIndex = tsl.int(tileResult.x).toVar();
2300
- const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
2301
- const u = tileResult.y.toVar();
2302
- const v = tileResult.z.toVar();
2303
- const fieldU = tileLocalToFieldUV$1(
2304
- u,
2305
- params.uniforms.uInnerTileSegments
2306
- ).toVar();
2307
- const fieldV = tileLocalToFieldUV$1(
2308
- v,
2309
- params.uniforms.uInnerTileSegments
2310
- ).toVar();
2311
- const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
2312
- const sampled = sampleTerrainField(
2313
- params.terrainFieldStorage,
2314
- fieldU,
2315
- fieldV,
2316
- safeTileIndex
2317
- ).toVar();
2318
- const nx = sampled.g.toVar();
2319
- const nz = sampled.b.toVar();
2320
- const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
2321
- const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2322
- return tsl.vec4(
2323
- sampled.r.mul(valid),
2324
- nx.mul(valid),
2325
- ny.mul(valid),
2326
- nz.mul(valid)
2327
- );
2930
+ return packedSampleFromTileResult(params, tileResult);
2931
+ });
2932
+ }
2933
+ function createTerrainSampleNodeByDirection(params) {
2934
+ const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
2935
+ return tsl.Fn(([direction]) => {
2936
+ const tileResult = tileLookup(direction).toVar();
2937
+ return packedSampleFromTileResult(params, tileResult);
2328
2938
  });
2329
2939
  }
2330
2940
  function createTerrainSampler(params) {
@@ -2356,16 +2966,14 @@ function createTerrainSampler(params) {
2356
2966
  const sampleElevation = tsl.Fn(
2357
2967
  ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
2358
2968
  );
2359
- const sampleNormal = tsl.Fn(
2360
- ([worldX, worldZ]) => tsl.vec3(
2361
- terrainSampleAt(worldX, worldZ).y,
2362
- terrainSampleAt(worldX, worldZ).z,
2363
- terrainSampleAt(worldX, worldZ).w
2364
- )
2365
- );
2366
- const sampleValidity = tsl.Fn(
2367
- ([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))
2368
- );
2969
+ const sampleNormal = tsl.Fn(([worldX, worldZ]) => {
2970
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2971
+ return tsl.vec3(sample.y, sample.z, sample.w);
2972
+ });
2973
+ const sampleValidity = tsl.Fn(([worldX, worldZ]) => {
2974
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2975
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
2976
+ });
2369
2977
  const evaluateElevation = tsl.Fn(
2370
2978
  ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
2371
2979
  );
@@ -2392,7 +3000,7 @@ function createTerrainSampler(params) {
2392
3000
  }
2393
3001
  );
2394
3002
  const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
2395
- return {
3003
+ const sampler = {
2396
3004
  sampleElevation,
2397
3005
  sampleNormal,
2398
3006
  sampleTerrain,
@@ -2400,6 +3008,26 @@ function createTerrainSampler(params) {
2400
3008
  evaluateElevation,
2401
3009
  evaluateNormal
2402
3010
  };
3011
+ if (params.projection === "cubeSphere") {
3012
+ const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
3013
+ sampler.sampleTerrainByDirection = tsl.Fn(
3014
+ ([direction]) => terrainSampleByDir(direction)
3015
+ );
3016
+ sampler.sampleElevationByDirection = tsl.Fn(
3017
+ ([direction]) => terrainSampleByDir(direction).x
3018
+ );
3019
+ sampler.sampleValidityByDirection = tsl.Fn(([direction]) => {
3020
+ const sample = terrainSampleByDir(direction).toVar();
3021
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
3022
+ });
3023
+ sampler.sampleNormalByDirection = tsl.Fn(([direction]) => {
3024
+ const dir = tsl.vec3(direction).normalize().toVar();
3025
+ const packed = terrainSampleByDir(direction).toVar();
3026
+ const basis = cubeFaceBasis(cubeFaceFromDirection(dir));
3027
+ return sphereTangentFrameNormal(dir, basis, tsl.vec3(packed.y, packed.z, packed.w));
3028
+ });
3029
+ }
3030
+ return sampler;
2403
3031
  }
2404
3032
 
2405
3033
  const createTerrainSamplerTask = work.task((get, work) => {
@@ -2407,12 +3035,16 @@ const createTerrainSamplerTask = work.task((get, work) => {
2407
3035
  const spatialIndex = get(gpuSpatialIndexStorageTask);
2408
3036
  const uniforms = get(updateUniformsTask);
2409
3037
  const elevationCallback = get(elevationFn);
3038
+ const maxLevelValue = get(maxLevel);
3039
+ const projection = get(topologyTask).projection ?? "flat";
2410
3040
  return work(
2411
3041
  () => createTerrainSampler({
2412
3042
  terrainFieldStorage,
2413
3043
  spatialIndex,
2414
3044
  uniforms,
2415
- elevationCallback
3045
+ elevationCallback,
3046
+ maxLevel: maxLevelValue,
3047
+ projection
2416
3048
  })
2417
3049
  );
2418
3050
  }).displayName("createTerrainSamplerTask");
@@ -2439,18 +3071,14 @@ const isSkirtUV = tsl.Fn(([segments]) => {
2439
3071
 
2440
3072
  function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
2441
3073
  return tsl.Fn(() => {
2442
- const nodeIndex = tsl.int(tsl.instanceIndex);
2443
- const nodeOffset = nodeIndex.mul(tsl.int(4));
2444
- const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
2445
- const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
2446
- const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
3074
+ const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
2447
3075
  const rootSize = terrainUniforms.uRootSize.toVar();
2448
3076
  const rootOrigin = terrainUniforms.uRootOrigin.toVar();
2449
3077
  const half = tsl.float(0.5);
2450
- const size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
3078
+ const size = rootSize.div(tsl.pow(tsl.float(2), tile.level.toFloat()));
2451
3079
  const halfRoot = rootSize.mul(half);
2452
- const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
2453
- const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
3080
+ const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
3081
+ const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
2454
3082
  const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
2455
3083
  const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
2456
3084
  const worldX = centerX.add(clampedX.mul(size));
@@ -2461,49 +3089,78 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
2461
3089
  function createTileElevation(terrainUniforms, terrainFieldStorage) {
2462
3090
  if (!terrainFieldStorage) return tsl.float(0);
2463
3091
  const innerSegs = terrainUniforms.uInnerTileSegments;
2464
- const u = tileLocalToFieldUV$1(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
2465
- const v = tileLocalToFieldUV$1(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
2466
- return sampleTerrainFieldElevation(
2467
- terrainFieldStorage,
2468
- u,
2469
- v,
2470
- tsl.int(tsl.instanceIndex)
2471
- ).mul(terrainUniforms.uElevationScale);
2472
- }
2473
- function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
3092
+ const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
3093
+ const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
3094
+ return sampleTerrainFieldElevation(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex)).mul(
3095
+ terrainUniforms.uElevationScale
3096
+ );
3097
+ }
3098
+ function createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
2474
3099
  if (!terrainFieldStorage) return;
3100
+ tsl.normalLocal.assign(
3101
+ createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection)
3102
+ );
3103
+ }
3104
+ function loadTangentNormal(terrainUniforms, terrainFieldStorage) {
2475
3105
  const nodeIndex = tsl.int(tsl.instanceIndex);
2476
3106
  const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
2477
3107
  const localVertexIndex = tsl.int(tsl.vertexIndex);
2478
3108
  const ix = localVertexIndex.mod(edgeVertexCount);
2479
3109
  const iy = localVertexIndex.div(edgeVertexCount);
2480
- const normalXZ = loadTerrainFieldNormal(
2481
- terrainFieldStorage,
2482
- ix,
2483
- iy,
2484
- nodeIndex
2485
- );
2486
- const nx = normalXZ.x;
2487
- const nz = normalXZ.y;
2488
- const nySq = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0));
2489
- const ny = nySq.sqrt();
2490
- tsl.normalLocal.assign(tsl.vec3(nx, ny, nz));
2491
- }
2492
- function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
2493
- const baseWorldPosition = createTileBaseWorldPosition(
2494
- leafStorage,
2495
- terrainUniforms
2496
- );
3110
+ const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
3111
+ const normal = unpackTangentNormal(normalXZ.x, normalXZ.y);
3112
+ return { ix, iy, normal };
3113
+ }
3114
+ function createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
3115
+ if (!terrainFieldStorage) return tsl.vec3(0, 1, 0);
3116
+ if (projection === "cubeSphere") {
3117
+ return tsl.Fn(() => {
3118
+ const { ix, iy, normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
3119
+ const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
3120
+ const innerSeg = terrainUniforms.uInnerTileSegments.toVar().toFloat();
3121
+ const localU = ix.toFloat().sub(tsl.float(1)).div(innerSeg).max(tsl.float(0)).min(tsl.float(1));
3122
+ const localV = iy.toFloat().sub(tsl.float(1)).div(innerSeg).max(tsl.float(0)).min(tsl.float(1));
3123
+ const faceUV = faceUVFromTileLocal(tile, localU, localV);
3124
+ const basis = cubeFaceBasis(tile.face);
3125
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
3126
+ return sphereTangentFrameNormal(dir, basis, normal);
3127
+ })();
3128
+ }
3129
+ return tsl.Fn(() => {
3130
+ const { normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
3131
+ return normal;
3132
+ })();
3133
+ }
3134
+ function createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
3135
+ return tsl.Fn(() => {
3136
+ const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
3137
+ const half = tsl.float(0.5);
3138
+ const localU = tsl.positionLocal.x.max(half.negate()).min(half).add(half);
3139
+ const localV = tsl.positionLocal.z.max(half.negate()).min(half).add(half);
3140
+ const faceUV = faceUVFromTileLocal(tile, localU, localV);
3141
+ const basis = cubeFaceBasis(tile.face);
3142
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
3143
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
3144
+ const baseRadius = terrainUniforms.uRadius.toVar().add(yElevation);
3145
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
3146
+ const r = tsl.select(skirtVertex, baseRadius.sub(terrainUniforms.uSkirtScale.toVar()), baseRadius);
3147
+ createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "cubeSphere");
3148
+ const origin = terrainUniforms.uRootOrigin.toVar();
3149
+ return origin.add(dir.mul(r));
3150
+ })();
3151
+ }
3152
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
3153
+ if (projection === "cubeSphere") {
3154
+ return createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage);
3155
+ }
3156
+ const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
2497
3157
  return tsl.Fn(() => {
2498
3158
  const base = baseWorldPosition();
2499
- const yElevation = createTileElevation(
2500
- terrainUniforms,
2501
- terrainFieldStorage
2502
- );
3159
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
2503
3160
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
2504
3161
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
2505
3162
  const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
2506
- createNormalAssignment(terrainUniforms, terrainFieldStorage);
3163
+ createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "flat");
2507
3164
  return tsl.vec3(base.x, worldY, base.z);
2508
3165
  })();
2509
3166
  }
@@ -2512,11 +3169,13 @@ const positionNodeTask = work.task((get, work) => {
2512
3169
  const leafStorage = get(leafStorageTask);
2513
3170
  const terrainUniforms = get(updateUniformsTask);
2514
3171
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
3172
+ const topology = get(topologyTask);
2515
3173
  return work(
2516
3174
  () => createTileWorldPosition(
2517
3175
  leafStorage,
2518
3176
  terrainUniforms,
2519
- terrainFieldStorage
3177
+ terrainFieldStorage,
3178
+ topology.projection ?? "flat"
2520
3179
  )
2521
3180
  );
2522
3181
  }).displayName("positionNodeTask");
@@ -2557,71 +3216,33 @@ function getTerrainBounds(config) {
2557
3216
  maxZ: config.originZ + halfRoot
2558
3217
  };
2559
3218
  }
2560
- function terrainSignedDistanceFromBounds(query, worldX, worldY, worldZ) {
2561
- const tileBounds = query.getTileBounds(worldX, worldZ);
2562
- if (tileBounds) {
2563
- if (worldY > tileBounds.maxElevation) {
2564
- return worldY - tileBounds.maxElevation;
2565
- }
2566
- if (worldY < tileBounds.minElevation) {
2567
- return worldY - tileBounds.minElevation;
3219
+ function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
3220
+ if (!skipBoundsFastPath) {
3221
+ const tileBounds = query.getTileBounds(worldX, worldZ);
3222
+ if (tileBounds) {
3223
+ if (worldY > tileBounds.maxElevation) {
3224
+ return worldY - tileBounds.maxElevation;
3225
+ }
3226
+ if (worldY < tileBounds.minElevation) {
3227
+ return worldY - tileBounds.minElevation;
3228
+ }
2568
3229
  }
2569
3230
  }
2570
3231
  const elevation = query.getElevation(worldX, worldZ);
2571
3232
  if (!Number.isFinite(elevation)) return void 0;
2572
3233
  return worldY - elevation;
2573
3234
  }
2574
- function terrainSignedDistancePrecise(query, worldX, worldY, worldZ) {
2575
- const elevation = query.getElevation(worldX, worldZ);
2576
- if (!Number.isFinite(elevation)) return void 0;
2577
- return worldY - elevation;
2578
- }
2579
- function cpuRaycast(query, ray, config, options) {
2580
- const bounds = getTerrainBounds(config);
2581
- const segment = intersectRayAabb(
2582
- ray,
2583
- bounds.minX,
2584
- config.minY,
2585
- bounds.minZ,
2586
- bounds.maxX,
2587
- config.maxY,
2588
- bounds.maxZ
2589
- );
2590
- if (!segment) return null;
2591
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
2592
- let startT = Math.max(0, segment.tMin);
2593
- const endT = Math.min(segment.tMax, maxDistance);
2594
- if (endT < startT) return null;
2595
- const maxSteps = Math.max(8, options?.maxSteps ?? 128);
2596
- const refinementSteps = Math.max(1, options?.refinementSteps ?? 8);
2597
- const point = new three.Vector3();
3235
+ function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
2598
3236
  let prevT = startT;
2599
3237
  ray.at(prevT, point);
2600
- let prevSignedDistance = terrainSignedDistanceFromBounds(
2601
- query,
2602
- point.x,
2603
- point.y,
2604
- point.z
2605
- );
3238
+ let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
2606
3239
  if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
2607
- const sample = query.sampleTerrain(point.x, point.z);
2608
- if (!sample.valid) return null;
2609
- point.y = sample.elevation;
2610
- return {
2611
- position: point.clone(),
2612
- normal: sample.normal.clone(),
2613
- distance: ray.origin.distanceTo(point)
2614
- };
3240
+ return startT;
2615
3241
  }
2616
- for (let i = 1; i <= maxSteps; i += 1) {
2617
- const t = startT + (endT - startT) * i / maxSteps;
3242
+ for (let i = 1; i <= options.maxSteps; i += 1) {
3243
+ const t = startT + (endT - startT) * i / options.maxSteps;
2618
3244
  ray.at(t, point);
2619
- const signedDistance = terrainSignedDistanceFromBounds(
2620
- query,
2621
- point.x,
2622
- point.y,
2623
- point.z
2624
- );
3245
+ const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
2625
3246
  if (signedDistance === void 0) {
2626
3247
  prevSignedDistance = void 0;
2627
3248
  prevT = t;
@@ -2630,15 +3251,10 @@ function cpuRaycast(query, ray, config, options) {
2630
3251
  if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
2631
3252
  let lo = prevT;
2632
3253
  let hi = t;
2633
- for (let r = 0; r < refinementSteps; r += 1) {
3254
+ for (let r = 0; r < options.refinementSteps; r += 1) {
2634
3255
  const mid = (lo + hi) * 0.5;
2635
3256
  ray.at(mid, point);
2636
- const midDistance = terrainSignedDistancePrecise(
2637
- query,
2638
- point.x,
2639
- point.y,
2640
- point.z
2641
- );
3257
+ const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
2642
3258
  if (midDistance === void 0) {
2643
3259
  lo = mid;
2644
3260
  continue;
@@ -2646,22 +3262,53 @@ function cpuRaycast(query, ray, config, options) {
2646
3262
  if (midDistance > 0) lo = mid;
2647
3263
  else hi = mid;
2648
3264
  }
2649
- const hitT = hi;
2650
- ray.at(hitT, point);
2651
- const sample = query.sampleTerrain(point.x, point.z);
2652
- if (!sample.valid) return null;
2653
- point.y = sample.elevation;
2654
- return {
2655
- position: point.clone(),
2656
- normal: sample.normal.clone(),
2657
- distance: ray.origin.distanceTo(point)
2658
- };
3265
+ return hi;
2659
3266
  }
2660
3267
  prevSignedDistance = signedDistance;
2661
3268
  prevT = t;
2662
3269
  }
2663
3270
  return null;
2664
3271
  }
3272
+ function cpuRaycast(query, ray, config, options) {
3273
+ const bounds = getTerrainBounds(config);
3274
+ const segment = intersectRayAabb(
3275
+ ray,
3276
+ bounds.minX,
3277
+ config.minY,
3278
+ bounds.minZ,
3279
+ bounds.maxX,
3280
+ config.maxY,
3281
+ bounds.maxZ
3282
+ );
3283
+ if (!segment) return null;
3284
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3285
+ const startT = Math.max(0, segment.tMin);
3286
+ const endT = Math.min(segment.tMax, maxDistance);
3287
+ if (endT < startT) return null;
3288
+ const point = new three.Vector3();
3289
+ const hitT = marchSignedDistance(
3290
+ ray,
3291
+ startT,
3292
+ endT,
3293
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
3294
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
3295
+ {
3296
+ maxSteps: Math.max(8, options?.maxSteps ?? 128),
3297
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
3298
+ },
3299
+ point
3300
+ );
3301
+ if (hitT === null) return null;
3302
+ ray.at(hitT, point);
3303
+ const sample = query.sampleTerrain(point.x, point.z);
3304
+ if (!sample.valid) return null;
3305
+ point.y = sample.elevation;
3306
+ return {
3307
+ position: point.clone(),
3308
+ normal: sample.normal.clone(),
3309
+ distance: ray.origin.distanceTo(point)
3310
+ };
3311
+ }
2665
3312
  function cpuRaycastBoundsOnly(ray, config, options) {
2666
3313
  const bounds = getTerrainBounds(config);
2667
3314
  const planeY = (config.minY + config.maxY) * 0.5;
@@ -2682,12 +3329,102 @@ function cpuRaycastBoundsOnly(ray, config, options) {
2682
3329
  distance: ray.origin.distanceTo(point)
2683
3330
  };
2684
3331
  }
3332
+ function intersectRaySphere(ray, cx, cy, cz, radius) {
3333
+ const ox = ray.origin.x - cx;
3334
+ const oy = ray.origin.y - cy;
3335
+ const oz = ray.origin.z - cz;
3336
+ const dx = ray.direction.x;
3337
+ const dy = ray.direction.y;
3338
+ const dz = ray.direction.z;
3339
+ const a = dx * dx + dy * dy + dz * dz;
3340
+ const b = 2 * (ox * dx + oy * dy + oz * dz);
3341
+ const c = ox * ox + oy * oy + oz * oz - radius * radius;
3342
+ const disc = b * b - 4 * a * c;
3343
+ if (disc < 0) return null;
3344
+ const sqrtDisc = Math.sqrt(disc);
3345
+ const inv2a = 1 / (2 * a);
3346
+ return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
3347
+ }
3348
+ function sphereSignedDistance(query, config, px, py, pz, scratchDir) {
3349
+ const cx = config.centerX ?? 0;
3350
+ const cy = config.centerY ?? 0;
3351
+ const cz = config.centerZ ?? 0;
3352
+ const radius = config.radius ?? 0;
3353
+ const dx = px - cx;
3354
+ const dy = py - cy;
3355
+ const dz = pz - cz;
3356
+ const dist = Math.hypot(dx, dy, dz);
3357
+ scratchDir.set(dx, dy, dz);
3358
+ const elevation = query.getElevationByDirection(scratchDir);
3359
+ if (elevation === null) return void 0;
3360
+ return dist - (radius + elevation);
3361
+ }
3362
+ function cubeSphereRaycast(query, ray, config, options) {
3363
+ const cx = config.centerX ?? 0;
3364
+ const cy = config.centerY ?? 0;
3365
+ const cz = config.centerZ ?? 0;
3366
+ const radius = config.radius ?? 0;
3367
+ const outerRadius = config.maxRadius ?? radius;
3368
+ const shell = intersectRaySphere(ray, cx, cy, cz, outerRadius);
3369
+ if (!shell) return null;
3370
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3371
+ const startT = Math.max(0, shell.t0);
3372
+ const endT = Math.min(shell.t1, maxDistance);
3373
+ if (endT < startT) return null;
3374
+ const scratchDir = new three.Vector3();
3375
+ const point = new three.Vector3();
3376
+ const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, config, px, py, pz, scratchDir);
3377
+ const hitT = marchSignedDistance(
3378
+ ray,
3379
+ startT,
3380
+ endT,
3381
+ signedDistanceAt,
3382
+ signedDistanceAt,
3383
+ {
3384
+ maxSteps: Math.max(8, options?.maxSteps ?? 256),
3385
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
3386
+ },
3387
+ point
3388
+ );
3389
+ if (hitT === null) return null;
3390
+ ray.at(hitT, point);
3391
+ const sample = query.sampleTerrainByPosition(point);
3392
+ if (!sample.valid) return null;
3393
+ return {
3394
+ position: sample.position.clone(),
3395
+ normal: sample.normal.clone(),
3396
+ distance: ray.origin.distanceTo(sample.position)
3397
+ };
3398
+ }
3399
+ function cubeSphereRaycastBoundsOnly(ray, config, options) {
3400
+ const cx = config.centerX ?? 0;
3401
+ const cy = config.centerY ?? 0;
3402
+ const cz = config.centerZ ?? 0;
3403
+ const radius = config.radius ?? 0;
3404
+ const shell = intersectRaySphere(ray, cx, cy, cz, radius);
3405
+ if (!shell) return null;
3406
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3407
+ const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
3408
+ if (t < 0 || t > maxDistance) return null;
3409
+ const point = new three.Vector3();
3410
+ ray.at(t, point);
3411
+ const normal = new three.Vector3(point.x - cx, point.y - cy, point.z - cz).normalize();
3412
+ return { position: point, normal, distance: ray.origin.distanceTo(point) };
3413
+ }
2685
3414
 
2686
3415
  function createTerrainRaycast(params) {
2687
3416
  return {
2688
3417
  pick(ray, options) {
2689
3418
  const config = params.getConfig();
2690
3419
  const terrainQuery = params.getTerrainQuery();
3420
+ if (config.projection === "cubeSphere") {
3421
+ const sphereQuery = params.getSphereQuery();
3422
+ if (sphereQuery) {
3423
+ const precise = cubeSphereRaycast(sphereQuery, ray, config, options);
3424
+ if (precise) return precise;
3425
+ }
3426
+ return cubeSphereRaycastBoundsOnly(ray, config, options);
3427
+ }
2691
3428
  if (terrainQuery) {
2692
3429
  const precise = cpuRaycast(terrainQuery, ray, config, options);
2693
3430
  if (precise) return precise;
@@ -2712,29 +3449,47 @@ const BOUNDS_PADDING = 1;
2712
3449
  const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
2713
3450
  const terrainRaycastTask = work.task(
2714
3451
  (get, work) => {
2715
- const { query: terrainQuery } = get(terrainQueryTask);
3452
+ const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
2716
3453
  const rootSizeValue = get(rootSize);
2717
3454
  const originValue = get(origin);
2718
3455
  const elevationScaleValue = get(elevationScale);
3456
+ const radiusValue = get(radius);
3457
+ const topologyValue = get(topologyTask);
3458
+ const projection = topologyValue.projection ?? "flat";
3459
+ const sphereRadius = topologyValue.radius ?? radiusValue;
2719
3460
  return work((prev) => {
2720
3461
  let raycast = prev;
2721
3462
  let state = raycast?.[RAYCAST_STATE];
2722
3463
  if (!state) {
2723
3464
  state = {
2724
3465
  terrainQuery: null,
3466
+ sphereQuery: null,
2725
3467
  bounds: {
2726
3468
  rootSize: 0,
2727
3469
  originX: 0,
2728
3470
  originZ: 0,
2729
3471
  minY: 0,
2730
- maxY: 0
3472
+ maxY: 0,
3473
+ projection: "flat",
3474
+ centerX: 0,
3475
+ centerY: 0,
3476
+ centerZ: 0,
3477
+ radius: 0,
3478
+ minRadius: 0,
3479
+ maxRadius: 0
2731
3480
  }
2732
3481
  };
2733
3482
  }
2734
3483
  state.terrainQuery = terrainQuery;
3484
+ state.sphereQuery = sphereQuery;
2735
3485
  state.bounds.rootSize = rootSizeValue;
2736
3486
  state.bounds.originX = originValue.x;
2737
3487
  state.bounds.originZ = originValue.z;
3488
+ state.bounds.projection = projection;
3489
+ state.bounds.centerX = originValue.x;
3490
+ state.bounds.centerY = originValue.y;
3491
+ state.bounds.centerZ = originValue.z;
3492
+ state.bounds.radius = sphereRadius;
2738
3493
  const range = terrainQuery.getGlobalElevationRange();
2739
3494
  if (range) {
2740
3495
  state.bounds.minY = range.min - BOUNDS_PADDING;
@@ -2744,9 +3499,19 @@ const terrainRaycastTask = work.task(
2744
3499
  state.bounds.minY = originValue.y - verticalExtent;
2745
3500
  state.bounds.maxY = originValue.y + verticalExtent;
2746
3501
  }
3502
+ const elevationExtent = Math.max(1, Math.abs(elevationScaleValue));
3503
+ let dispMin = -elevationExtent;
3504
+ let dispMax = elevationExtent;
3505
+ if (range) {
3506
+ dispMin = range.min - originValue.y;
3507
+ dispMax = range.max - originValue.y;
3508
+ }
3509
+ state.bounds.minRadius = Math.max(0, sphereRadius + dispMin - BOUNDS_PADDING);
3510
+ state.bounds.maxRadius = sphereRadius + dispMax + BOUNDS_PADDING;
2747
3511
  if (!raycast) {
2748
3512
  raycast = createTerrainRaycast({
2749
3513
  getTerrainQuery: () => state.terrainQuery,
3514
+ getSphereQuery: () => state.sphereQuery,
2750
3515
  getConfig: () => state.bounds
2751
3516
  });
2752
3517
  }
@@ -2756,15 +3521,12 @@ const terrainRaycastTask = work.task(
2756
3521
  }
2757
3522
  ).displayName("terrainRaycastTask");
2758
3523
 
2759
- function terrainGraph() {
2760
- 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).add(tileBoundsContextTask).add(tileBoundsReductionTask).add(terrainQueryTask).add(terrainReadbackTask).add(terrainRaycastTask);
2761
- }
2762
3524
  const terrainTasks = {
2763
3525
  instanceId: instanceIdTask,
2764
3526
  quadtreeConfig: quadtreeConfigTask,
2765
3527
  quadtreeUpdate: quadtreeUpdateTask,
2766
3528
  leafStorage: leafStorageTask,
2767
- surface: surfaceTask,
3529
+ topology: topologyTask,
2768
3530
  leafGpuBuffer: leafGpuBufferTask,
2769
3531
  gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2770
3532
  gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
@@ -2785,6 +3547,13 @@ const terrainTasks = {
2785
3547
  terrainReadback: terrainReadbackTask,
2786
3548
  terrainRaycast: terrainRaycastTask
2787
3549
  };
3550
+ function terrainGraph() {
3551
+ const g = work.graph();
3552
+ for (const t of Object.values(terrainTasks)) {
3553
+ g.add(t);
3554
+ }
3555
+ return g;
3556
+ }
2788
3557
 
2789
3558
  const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
2790
3559
  return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
@@ -2840,10 +3609,11 @@ const voronoiCells = tsl.Fn((params) => {
2840
3609
 
2841
3610
  exports.ArrayTextureBackend = ArrayTextureBackend;
2842
3611
  exports.AtlasBackend = AtlasBackend;
3612
+ exports.CUBE_FACES = CUBE_FACES;
3613
+ exports.CUBE_FACE_COUNT = CUBE_FACE_COUNT;
2843
3614
  exports.Dir = Dir;
2844
3615
  exports.TerrainGeometry = TerrainGeometry;
2845
3616
  exports.TerrainMesh = TerrainMesh;
2846
- exports.Texture3DBackend = Texture3DBackend;
2847
3617
  exports.U32_EMPTY = U32_EMPTY;
2848
3618
  exports.allocLeafSet = allocLeafSet;
2849
3619
  exports.allocSeamTable = allocSeamTable;
@@ -2853,10 +3623,10 @@ exports.buildLeafIndex = buildLeafIndex;
2853
3623
  exports.buildSeams2to1 = buildSeams2to1;
2854
3624
  exports.compileComputeTask = compileComputeTask;
2855
3625
  exports.createComputePipelineTasks = createComputePipelineTasks;
2856
- exports.createCubeSphereSurface = createCubeSphereSurface;
3626
+ exports.createCubeSphereTopology = createCubeSphereTopology;
2857
3627
  exports.createElevationFieldContextTask = createElevationFieldContextTask;
2858
- exports.createFlatSurface = createFlatSurface;
2859
- exports.createInfiniteFlatSurface = createInfiniteFlatSurface;
3628
+ exports.createFlatTopology = createFlatTopology;
3629
+ exports.createInfiniteFlatTopology = createInfiniteFlatTopology;
2860
3630
  exports.createSpatialIndex = createSpatialIndex;
2861
3631
  exports.createState = createState;
2862
3632
  exports.createTerrainFieldStorage = createTerrainFieldStorage;
@@ -2865,13 +3635,23 @@ exports.createTerrainQuery = createTerrainQuery;
2865
3635
  exports.createTerrainRaycast = createTerrainRaycast;
2866
3636
  exports.createTerrainSampler = createTerrainSampler;
2867
3637
  exports.createTerrainSamplerTask = createTerrainSamplerTask;
3638
+ exports.createTerrainSphereQuery = createTerrainSphereQuery;
2868
3639
  exports.createTerrainUniforms = createTerrainUniforms;
2869
3640
  exports.createUniformsTask = createUniformsTask;
3641
+ exports.cubeFaceBasis = cubeFaceBasis;
3642
+ exports.cubeFaceDirection = cubeFaceDirection;
3643
+ exports.cubeFaceFromDirection = cubeFaceFromDirection;
3644
+ exports.cubeFacePoint = cubeFacePoint;
3645
+ exports.cubeFaceUVFromDirection = cubeFaceUVFromDirection;
2870
3646
  exports.deriveNormalZ = deriveNormalZ;
3647
+ exports.directionToFace = directionToFace;
3648
+ exports.directionToFaceUV = directionToFaceUV;
3649
+ exports.directionToLatLong = directionToLatLong;
2871
3650
  exports.elevationFieldStageTask = elevationFieldStageTask;
2872
3651
  exports.elevationFn = elevationFn;
2873
3652
  exports.elevationScale = elevationScale;
2874
3653
  exports.executeComputeTask = executeComputeTask;
3654
+ exports.faceUVToCube = faceUVToCube;
2875
3655
  exports.getDeviceComputeLimits = getDeviceComputeLimits;
2876
3656
  exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
2877
3657
  exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
@@ -2879,6 +3659,7 @@ exports.innerTileSegments = innerTileSegments;
2879
3659
  exports.instanceIdTask = instanceIdTask;
2880
3660
  exports.isSkirtUV = isSkirtUV;
2881
3661
  exports.isSkirtVertex = isSkirtVertex;
3662
+ exports.latLongToDirection = latLongToDirection;
2882
3663
  exports.leafGpuBufferTask = leafGpuBufferTask;
2883
3664
  exports.leafStorageTask = leafStorageTask;
2884
3665
  exports.loadTerrainField = loadTerrainField;
@@ -2892,16 +3673,16 @@ exports.positionNodeTask = positionNodeTask;
2892
3673
  exports.quadtreeConfigTask = quadtreeConfigTask;
2893
3674
  exports.quadtreeUpdate = quadtreeUpdate;
2894
3675
  exports.quadtreeUpdateTask = quadtreeUpdateTask;
3676
+ exports.radius = radius;
2895
3677
  exports.resetLeafSet = resetLeafSet;
2896
3678
  exports.resetSeamTable = resetSeamTable;
2897
3679
  exports.rootSize = rootSize;
2898
3680
  exports.sampleTerrainField = sampleTerrainField;
2899
3681
  exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
2900
- exports.sampleTerrainFieldNormal = sampleTerrainFieldNormal;
2901
3682
  exports.skirtScale = skirtScale;
3683
+ exports.sphereTangentFrameNormal = sphereTangentFrameNormal;
2902
3684
  exports.storeTerrainField = storeTerrainField;
2903
- exports.surface = surface;
2904
- exports.surfaceTask = surfaceTask;
3685
+ exports.tangentFromAxis = tangentFromAxis;
2905
3686
  exports.terrainFieldFilter = terrainFieldFilter;
2906
3687
  exports.terrainFieldStageTask = terrainFieldStageTask;
2907
3688
  exports.terrainGraph = terrainGraph;
@@ -2911,6 +3692,9 @@ exports.terrainReadbackTask = terrainReadbackTask;
2911
3692
  exports.terrainTasks = terrainTasks;
2912
3693
  exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
2913
3694
  exports.tileNodesTask = tileNodesTask;
3695
+ exports.topology = topology;
3696
+ exports.topologyTask = topologyTask;
3697
+ exports.unpackTangentNormal = unpackTangentNormal;
2914
3698
  exports.update = update;
2915
3699
  exports.updateUniformsTask = updateUniformsTask;
2916
3700
  exports.vElevation = vElevation;