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

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,38 +7,43 @@ 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));
19
- this.setAttribute(
20
- "position",
21
- new three.BufferAttribute(
22
- new Float32Array(this.generatePositions(innerSegments)),
23
- 3
24
- )
23
+ const index = this.generateIndices(innerSegments, flipWinding);
24
+ const indexAttribute = new three.BufferAttribute(new Uint32Array(index), 1);
25
+ indexAttribute.name = "terrainIndex";
26
+ this.setIndex(indexAttribute);
27
+ const positionAttribute = new three.BufferAttribute(
28
+ new Float32Array(this.generatePositions(innerSegments)),
29
+ 3
25
30
  );
26
- this.setAttribute(
27
- "normal",
28
- new three.BufferAttribute(
29
- new Float32Array(this.generateNormals(innerSegments)),
30
- 3
31
- )
31
+ positionAttribute.name = "terrainPosition";
32
+ this.setAttribute("position", positionAttribute);
33
+ const normalAttribute = new three.BufferAttribute(
34
+ new Float32Array(this.generateNormals(innerSegments)),
35
+ 3
32
36
  );
33
- this.setAttribute(
34
- "uv",
35
- new three.BufferAttribute(
36
- new Float32Array(
37
- extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
38
- ),
39
- 2
40
- )
37
+ normalAttribute.name = "terrainNormal";
38
+ this.setAttribute("normal", normalAttribute);
39
+ const uvAttribute = new three.BufferAttribute(
40
+ new Float32Array(
41
+ extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
42
+ ),
43
+ 2
41
44
  );
45
+ uvAttribute.name = "terrainUv";
46
+ this.setAttribute("uv", uvAttribute);
42
47
  } catch (error) {
43
48
  console.error("Error creating TerrainGeometry:", error);
44
49
  throw error;
@@ -103,12 +108,16 @@ class TerrainGeometry extends three.BufferGeometry {
103
108
  * triangle 1: a, c, b
104
109
  * triangle 2: b, c, d
105
110
  */
106
- generateIndices(innerSegments) {
111
+ generateIndices(innerSegments, flipWinding = false) {
107
112
  const innerEdgeVertexCount = innerSegments + 1;
108
113
  const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
109
114
  const indices = [];
110
115
  const cellsPerEdge = edgeVertexCountWithSkirt - 1;
111
116
  const mid = Math.floor(cellsPerEdge / 2);
117
+ const pushTri = (v0, v1, v2) => {
118
+ if (flipWinding) indices.push(v0, v2, v1);
119
+ else indices.push(v0, v1, v2);
120
+ };
112
121
  for (let y = 0; y < cellsPerEdge; y++) {
113
122
  for (let x = 0; x < cellsPerEdge; x++) {
114
123
  const a = y * edgeVertexCountWithSkirt + x;
@@ -125,11 +134,11 @@ class TerrainGeometry extends three.BufferGeometry {
125
134
  useDefaultDiagonal = (x + y) % 2 === 0;
126
135
  }
127
136
  if (useDefaultDiagonal) {
128
- indices.push(a, d, b);
129
- indices.push(a, c, d);
137
+ pushTri(a, d, b);
138
+ pushTri(a, c, d);
130
139
  } else {
131
- indices.push(a, c, b);
132
- indices.push(b, c, d);
140
+ pushTri(a, c, b);
141
+ pushTri(b, c, d);
133
142
  }
134
143
  }
135
144
  }
@@ -220,32 +229,71 @@ class TerrainGeometry extends three.BufferGeometry {
220
229
  }
221
230
  }
222
231
 
232
+ const rootSize = work.param(256).displayName("rootSize");
233
+ const origin = work.param({
234
+ x: 0,
235
+ y: 0,
236
+ z: 0
237
+ }).displayName("origin");
238
+ const innerTileSegments = work.param(61).displayName("innerTileSegments");
239
+ const skirtScale = work.param(100).displayName("skirtScale");
240
+ const elevationScale = work.param(1).displayName("elevationScale");
241
+ const radius = work.param(1e3).displayName("radius");
242
+ const maxNodes = work.param(1024).displayName("maxNodes");
243
+ const maxLevel = work.param(16).displayName("maxLevel");
244
+ const quadtreeUpdate = work.param({
245
+ cameraOrigin: { x: 0, y: 0, z: 0 },
246
+ mode: "distance",
247
+ distanceFactor: 1.5
248
+ }).displayName("quadtreeUpdate");
249
+ const topology = work.param(null).displayName("topology");
250
+ const terrainFieldFilter = work.param("linear").displayName(
251
+ "terrainFieldFilter"
252
+ );
253
+ const elevationFn = work.param(() => tsl.float(0));
254
+
223
255
  const defaultTerrainMeshParams = {
224
- innerTileSegments: 14,
256
+ // Source of truth is the `innerTileSegments` param itself.
257
+ innerTileSegments: innerTileSegments.get(),
225
258
  maxNodes: 1024,
226
- material: new webgpu.MeshStandardNodeMaterial()
259
+ material: new webgpu.MeshStandardNodeMaterial(),
260
+ flipWinding: false
227
261
  };
228
262
  class TerrainMesh extends webgpu.InstancedMesh {
229
263
  _innerTileSegments;
230
264
  _maxNodes;
265
+ _flipWinding;
231
266
  terrainRaycast = null;
232
267
  constructor(params = defaultTerrainMeshParams) {
233
268
  const mergedParams = { ...defaultTerrainMeshParams, ...params };
234
- const { innerTileSegments, maxNodes, material } = mergedParams;
235
- const geometry = new TerrainGeometry(innerTileSegments, true);
269
+ const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
270
+ const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
236
271
  super(geometry, material, maxNodes);
272
+ this.instanceMatrix.name = "terrainInstanceMatrix";
237
273
  this.frustumCulled = false;
238
274
  this._innerTileSegments = innerTileSegments;
239
275
  this._maxNodes = maxNodes;
276
+ this._flipWinding = flipWinding;
240
277
  }
241
278
  get innerTileSegments() {
242
279
  return this._innerTileSegments;
243
280
  }
244
281
  set innerTileSegments(tileSegments) {
282
+ if (tileSegments === this._innerTileSegments) return;
245
283
  const oldGeometry = this.geometry;
246
- this.geometry = new TerrainGeometry(tileSegments, true);
284
+ this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
247
285
  this._innerTileSegments = tileSegments;
248
- setTimeout(oldGeometry.dispose);
286
+ setTimeout(() => oldGeometry.dispose());
287
+ }
288
+ get flipWinding() {
289
+ return this._flipWinding;
290
+ }
291
+ set flipWinding(flip) {
292
+ if (flip === this._flipWinding) return;
293
+ const oldGeometry = this.geometry;
294
+ this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
295
+ this._flipWinding = flip;
296
+ setTimeout(() => oldGeometry.dispose());
249
297
  }
250
298
  get maxNodes() {
251
299
  return this._maxNodes;
@@ -260,12 +308,14 @@ class TerrainMesh extends webgpu.InstancedMesh {
260
308
  const oldMatrixArray = this.instanceMatrix.array;
261
309
  nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
262
310
  this.instanceMatrix = new webgpu.InstancedBufferAttribute(nextMatrix, 16);
311
+ this.instanceMatrix.name = "terrainInstanceMatrix";
263
312
  if (this.instanceColor) {
264
313
  const itemSize = this.instanceColor.itemSize;
265
314
  const nextColor = new Float32Array(maxNodes * itemSize);
266
315
  const oldColorArray = this.instanceColor.array;
267
316
  nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
268
317
  this.instanceColor = new webgpu.InstancedBufferAttribute(nextColor, itemSize);
318
+ this.instanceColor.name = "terrainInstanceColor";
269
319
  }
270
320
  this._maxNodes = maxNodes;
271
321
  this.count = Math.min(this.count, maxNodes);
@@ -311,13 +361,8 @@ function compileComputePipeline(stages, width, options) {
311
361
  WORKGROUP_X,
312
362
  WORKGROUP_Y
313
363
  ];
314
- const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
315
- const uInstanceCount = tsl.uniform(0, "uint");
316
- let singleKernel;
364
+ const uInstanceCount = tsl.uniform(0, "uint").setName("uInstanceCount");
317
365
  const stagedKernelCache = /* @__PURE__ */ new Map();
318
- function canRunSingleKernel(widthValue, limits) {
319
- return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
320
- }
321
366
  function clampWorkgroupToLimits(requested, limits) {
322
367
  let x = Math.max(1, Math.floor(requested[0]));
323
368
  let y = Math.max(1, Math.floor(requested[1]));
@@ -333,37 +378,6 @@ function compileComputePipeline(stages, width, options) {
333
378
  );
334
379
  return [x, y];
335
380
  }
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
381
  function buildStagedKernels(workgroupSize) {
368
382
  return stages.map(
369
383
  (stage) => tsl.Fn(() => {
@@ -394,15 +408,7 @@ function compileComputePipeline(stages, width, options) {
394
408
  }
395
409
  function execute(renderer, instanceCount) {
396
410
  const limits = getDeviceComputeLimits(renderer);
397
- const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
398
411
  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
412
  const [workgroupX, workgroupY] = clampWorkgroupToLimits(
407
413
  preferredWorkgroup,
408
414
  limits
@@ -446,6 +452,7 @@ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
446
452
  edgeVertexCount,
447
453
  tileCount
448
454
  );
455
+ tex.name = "terrainField";
449
456
  configureStorageTexture(tex, options.format, options.filter);
450
457
  return {
451
458
  backendType: "array-texture",
@@ -489,6 +496,7 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
489
496
  let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
490
497
  const atlasSize = tilesPerRow * edgeVertexCount;
491
498
  const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
499
+ tex.name = "terrainFieldAtlas";
492
500
  configureStorageTexture(tex, options.format, options.filter);
493
501
  return {
494
502
  backendType: "atlas",
@@ -545,40 +553,9 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
545
553
  }
546
554
  };
547
555
  }
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
- };
556
+ function texture3DBackend(edgeVertexCount, tileCount, options) {
557
+ const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
558
+ return { ...storage, backendType: "texture-3d" };
582
559
  }
583
560
  function tryGetDeviceLimits(renderer) {
584
561
  const backend = renderer;
@@ -592,7 +569,7 @@ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options
592
569
  return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
593
570
  }
594
571
  if (forcedBackend === "texture-3d") {
595
- return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
572
+ return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
596
573
  }
597
574
  if (forcedBackend === "array-texture") {
598
575
  return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
@@ -627,7 +604,7 @@ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
627
604
  }
628
605
  function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
629
606
  const raw = loadTerrainField(storage, ix, iy, tileIndex);
630
- return tsl.vec2(raw.g, raw.b);
607
+ return tsl.vec3(raw.g, raw.b, raw.a);
631
608
  }
632
609
  function sampleTerrainField(storage, u, v, tileIndex) {
633
610
  return storage.sample(u, v, tileIndex);
@@ -635,12 +612,8 @@ function sampleTerrainField(storage, u, v, tileIndex) {
635
612
  function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
636
613
  return sampleTerrainField(storage, u, v, tileIndex).r;
637
614
  }
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
- function packTerrainFieldSample(height, normalXZ, extra = tsl.float(0)) {
643
- return tsl.vec4(height, normalXZ.x, normalXZ.y, extra);
615
+ function packTerrainFieldSample(height, normal) {
616
+ return tsl.vec4(height, normal.x, normal.y, normal.z);
644
617
  }
645
618
 
646
619
  const createElevation = (tile, uniforms, elevationFn) => {
@@ -665,99 +638,70 @@ const createElevation = (tile, uniforms, elevationFn) => {
665
638
  };
666
639
  };
667
640
 
668
- function createTileCompute(leafStorage, uniforms) {
669
- const tileLevel = tsl.Fn(([nodeIndex]) => {
670
- const nodeOffset = nodeIndex.mul(tsl.int(4));
671
- return leafStorage.node.element(nodeOffset).toInt();
672
- });
641
+ const HALF_PI = Math.PI * 0.5;
642
+ const FIELD_INNER_TEXEL_OFFSET = 1.5;
643
+ const FIELD_EDGE_EXTRA_TEXELS = 3;
644
+ function sphereTileArcLength(radius, levelDivisor) {
645
+ return radius * HALF_PI / levelDivisor;
646
+ }
647
+ function decodeLeafTile(leafStorage, nodeIndex) {
648
+ const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
649
+ return {
650
+ level: leafStorage.node.element(nodeOffset).toInt(),
651
+ x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
652
+ y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
653
+ face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
654
+ };
655
+ }
656
+ function faceUVFromTileLocal(tile, localU, localV, baseU = tsl.float(1), baseV = tsl.float(1)) {
657
+ const levelScale = tsl.pow(tsl.float(2), tile.level.toFloat());
658
+ const nU = baseU.mul(levelScale);
659
+ const nV = baseV.mul(levelScale);
660
+ return tsl.vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
661
+ }
662
+ function createTileCompute(leafStorage, uniforms, projection) {
663
+ const baseU = tsl.float(projection.baseResolution?.u ?? 1);
664
+ const baseV = tsl.float(projection.baseResolution?.v ?? 1);
665
+ const tileLevel = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
666
+ const tileFace = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
673
667
  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);
678
- });
679
- const tileSize = tsl.Fn(([nodeIndex]) => {
680
- const rootSize = uniforms.uRootSize.toVar();
681
- const level = tileLevel(nodeIndex);
682
- return tsl.float(rootSize).div(tsl.pow(tsl.float(2), level.toFloat()));
668
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
669
+ return tsl.vec2(tile.x, tile.y);
683
670
  });
684
- const rootUVCompute = tsl.Fn(([nodeIndex, ix, iy]) => {
685
- const nodeVec2 = tileOriginVec2(nodeIndex);
686
- const nodeX = nodeVec2.x;
687
- const nodeY = nodeVec2.y;
688
- const rootSize = uniforms.uRootSize.toVar();
689
- const rootOrigin = uniforms.uRootOrigin.toVar();
690
- const size = tileSize(nodeIndex);
691
- const half = tsl.float(0.5);
692
- const halfRoot = tsl.float(rootSize).mul(half);
671
+ const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
672
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
693
673
  const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
694
- const texelSpacing = size.div(fInnerSegments);
695
- const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
696
- const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
697
- const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
698
- const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
699
- const centeredX = worldX.sub(rootOrigin.x);
700
- const centeredZ = worldZ.sub(rootOrigin.z);
701
- return tsl.vec2(
702
- centeredX.div(rootSize).add(half),
703
- centeredZ.div(rootSize).mul(tsl.float(-1)).add(half)
704
- );
674
+ const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
675
+ const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
676
+ return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
705
677
  });
706
- const tileVertexWorldPositionCompute = tsl.Fn(
707
- ([nodeIndex, ix, iy]) => {
708
- const rootOrigin = uniforms.uRootOrigin.toVar();
709
- const nodeVec2 = tileOriginVec2(nodeIndex);
710
- const nodeX = nodeVec2.x;
711
- const nodeY = nodeVec2.y;
712
- const rootSize = uniforms.uRootSize.toVar();
713
- const size = tileSize(nodeIndex);
714
- const half = tsl.float(0.5);
715
- const halfRoot = tsl.float(rootSize).mul(half);
716
- const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
717
- const texelSpacing = size.div(fInnerSegments);
718
- const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
719
- const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
720
- const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
721
- const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
722
- return tsl.vec3(worldX, rootOrigin.y, worldZ);
723
- }
724
- );
678
+ const shared = {
679
+ tileLevel: (nodeIndex) => tileLevel(nodeIndex),
680
+ tileFace: (nodeIndex) => tileFace(nodeIndex),
681
+ tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
682
+ tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
683
+ };
684
+ const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
725
685
  return {
726
- tileLevel,
727
- tileOriginVec2,
728
- tileSize,
729
- rootUVCompute,
730
- tileVertexWorldPositionCompute
686
+ ...shared,
687
+ tileSize: parts.tileSize,
688
+ rootUVCompute: parts.rootUV,
689
+ tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
731
690
  };
732
691
  }
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);
692
+ function tileLocalToFieldUV(localCoord, innerSegments) {
693
+ const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
694
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
695
+ }
696
+ function tileLocalToFieldUVNumber(localCoord, innerSegments) {
697
+ const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
698
+ return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
736
699
  }
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
700
 
758
701
  function createLeafStorage(maxNodes) {
759
702
  const data = new Int32Array(maxNodes * 4);
760
703
  const attribute = new webgpu.StorageBufferAttribute(data, 4);
704
+ attribute.name = "leafStorage";
761
705
  const node = tsl.storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
762
706
  return { data, attribute, node };
763
707
  }
@@ -850,12 +794,12 @@ function ensureChildren(store, parentId) {
850
794
  return childBase;
851
795
  }
852
796
 
853
- function nextPow2$1(n) {
797
+ function nextPow2$2(n) {
854
798
  let x = 1;
855
799
  while (x < n) x <<= 1;
856
800
  return x;
857
801
  }
858
- function mix32$1(x) {
802
+ function mix32$2(x) {
859
803
  x >>>= 0;
860
804
  x ^= x >>> 16;
861
805
  x = Math.imul(x, 2146121005) >>> 0;
@@ -864,12 +808,12 @@ function mix32$1(x) {
864
808
  x ^= x >>> 16;
865
809
  return x >>> 0;
866
810
  }
867
- function hashKey$1(space, level, x, y) {
868
- const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
869
- return mix32$1(h);
811
+ function hashKey$2(space, level, x, y) {
812
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32$2(x) >>> 0 ^ mix32$2(y) >>> 0;
813
+ return mix32$2(h);
870
814
  }
871
815
  function createSpatialIndex(maxEntries) {
872
- const size = nextPow2$1(Math.max(2, maxEntries * 2));
816
+ const size = nextPow2$2(Math.max(2, maxEntries * 2));
873
817
  return {
874
818
  size,
875
819
  mask: size - 1,
@@ -894,7 +838,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
894
838
  const l = level & 255;
895
839
  const xx = x >>> 0;
896
840
  const yy = y >>> 0;
897
- let slot = hashKey$1(s, l, xx, yy) & index.mask;
841
+ let slot = hashKey$2(s, l, xx, yy) & index.mask;
898
842
  for (let probes = 0; probes < index.size; probes++) {
899
843
  if (index.stamp[slot] !== index.stampGen) {
900
844
  index.stamp[slot] = index.stampGen;
@@ -918,7 +862,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
918
862
  const l = level & 255;
919
863
  const xx = x >>> 0;
920
864
  const yy = y >>> 0;
921
- let slot = hashKey$1(s, l, xx, yy) & index.mask;
865
+ let slot = hashKey$2(s, l, xx, yy) & index.mask;
922
866
  for (let probes = 0; probes < index.size; probes++) {
923
867
  if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
924
868
  if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
@@ -938,10 +882,10 @@ function buildLeafIndex(leaves, out) {
938
882
  return index;
939
883
  }
940
884
 
941
- function createState(cfg, surface) {
942
- const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
885
+ function createState(cfg, topology) {
886
+ const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
943
887
  const scratchRootTiles = [];
944
- for (let i = 0; i < surface.maxRootCount; i++) {
888
+ for (let i = 0; i < topology.maxRootCount; i++) {
945
889
  scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
946
890
  }
947
891
  return {
@@ -951,7 +895,7 @@ function createState(cfg, surface) {
951
895
  leafNodeIds: new Uint32Array(cfg.maxNodes),
952
896
  leafIndex: createSpatialIndex(cfg.maxNodes),
953
897
  stack: new Uint32Array(cfg.maxNodes),
954
- rootNodeIds: new Uint32Array(surface.maxRootCount),
898
+ rootNodeIds: new Uint32Array(topology.maxRootCount),
955
899
  rootCount: 0,
956
900
  splitQueue: new Uint32Array(cfg.maxNodes),
957
901
  splitStamp: new Uint16Array(cfg.maxNodes),
@@ -959,26 +903,27 @@ function createState(cfg, surface) {
959
903
  scratchTile: { space: 0, level: 0, x: 0, y: 0 },
960
904
  scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
961
905
  scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
906
+ scratchElevationRange: { min: 0, max: 0 },
962
907
  scratchRootTiles,
963
- spaceCount: surface.spaceCount
908
+ spaceCount: topology.spaceCount
964
909
  };
965
910
  }
966
- function beginUpdate(state, surface, params) {
967
- if (surface.spaceCount !== state.spaceCount) {
911
+ function beginUpdate(state, topology, params) {
912
+ if (topology.spaceCount !== state.spaceCount) {
968
913
  throw new Error(
969
- `Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
914
+ `Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
970
915
  );
971
916
  }
972
- if (surface.maxRootCount !== state.rootNodeIds.length) {
917
+ if (topology.maxRootCount !== state.rootNodeIds.length) {
973
918
  throw new Error(
974
- `Surface maxRootCount changed (${state.rootNodeIds.length} -> ${surface.maxRootCount}). Create a new quadtree state.`
919
+ `Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
975
920
  );
976
921
  }
977
922
  beginFrame(state.store);
978
923
  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}).`);
924
+ const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
925
+ if (rootCount < 0 || rootCount > topology.maxRootCount) {
926
+ throw new Error(`Topology returned invalid root count (${rootCount}).`);
982
927
  }
983
928
  for (let i = 0; i < rootCount; i++) {
984
929
  const rootId = allocNode(state.store, state.scratchRootTiles[i]);
@@ -1015,7 +960,7 @@ function shouldSplit(bounds, level, maxLevel, params) {
1015
960
  return safeDistSq < threshold * threshold;
1016
961
  }
1017
962
 
1018
- function refineLeaves(state, surface, params, outLeaves) {
963
+ function refineLeaves(state, topology, params, outLeaves) {
1019
964
  const leaves = outLeaves ?? state.leaves;
1020
965
  resetLeafSet(leaves);
1021
966
  const store = state.store;
@@ -1036,7 +981,14 @@ function refineLeaves(state, surface, params, outLeaves) {
1036
981
  tile.x = x;
1037
982
  tile.y = y;
1038
983
  const bounds = state.scratchBounds;
1039
- surface.tileBounds(tile, params.cameraOrigin, bounds);
984
+ let elevationRange;
985
+ if (params.tileElevationRange) {
986
+ const range = state.scratchElevationRange;
987
+ if (params.tileElevationRange(space, level, x, y, range)) {
988
+ elevationRange = range;
989
+ }
990
+ }
991
+ topology.tileBounds(tile, params.cameraOrigin, bounds, elevationRange);
1040
992
  if (hasChildren(store, nodeId)) {
1041
993
  const base = store.firstChild[nodeId];
1042
994
  stack[sp++] = base + 3;
@@ -1084,7 +1036,7 @@ function scheduleSplit(state, nodeId, count) {
1084
1036
  state.splitQueue[count] = nodeId;
1085
1037
  return count + 1;
1086
1038
  }
1087
- function balance2to1(state, surface, params, leaves) {
1039
+ function balance2to1(state, topology, params, leaves) {
1088
1040
  const maxIters = state.cfg.maxLevel + 1;
1089
1041
  for (let iter = 0; iter < maxIters; iter++) {
1090
1042
  const index = buildLeafIndex(leaves, state.leafIndex);
@@ -1105,7 +1057,7 @@ function balance2to1(state, surface, params, leaves) {
1105
1057
  tile.x = leafX >>> shift;
1106
1058
  tile.y = leafY >>> shift;
1107
1059
  const neighbor = state.scratchNeighbor;
1108
- if (!surface.neighborSameLevel(tile, dir, neighbor)) break;
1060
+ if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
1109
1061
  const j = lookupSpatialIndexRaw(
1110
1062
  index,
1111
1063
  neighbor.space,
@@ -1129,18 +1081,24 @@ function balance2to1(state, surface, params, leaves) {
1129
1081
  if (base !== U32_EMPTY) anySplit = true;
1130
1082
  }
1131
1083
  if (!anySplit) return leaves;
1132
- refineLeaves(state, surface, params, leaves);
1084
+ refineLeaves(state, topology, params, leaves);
1133
1085
  }
1134
1086
  return leaves;
1135
1087
  }
1136
1088
 
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;
1089
+ function update(state, topology, params, outLeaves) {
1090
+ const cam = params.cameraOrigin;
1091
+ const elevation = params.elevationAtCameraXZ ?? 0;
1092
+ const origX = cam.x;
1093
+ const origY = cam.y;
1094
+ const origZ = cam.z;
1095
+ topology.projection.cpu.cameraSurfaceOffset(cam, elevation);
1096
+ beginUpdate(state, topology, params);
1097
+ const leaves = refineLeaves(state, topology, params, outLeaves);
1098
+ const result = balance2to1(state, topology, params, leaves);
1099
+ cam.x = origX;
1100
+ cam.y = origY;
1101
+ cam.z = origZ;
1144
1102
  return result;
1145
1103
  }
1146
1104
 
@@ -1148,7 +1106,7 @@ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
1148
1106
  const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
1149
1107
  const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
1150
1108
  const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
1151
- function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1109
+ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1152
1110
  if (outSeams.capacity < leaves.count) {
1153
1111
  throw new Error("SeamTable capacity is smaller than LeafSet.count.");
1154
1112
  }
@@ -1169,7 +1127,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1169
1127
  scratchTile.level = level;
1170
1128
  scratchTile.x = x;
1171
1129
  scratchTile.y = y;
1172
- if (!surface.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1130
+ if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1173
1131
  let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
1174
1132
  if (j !== U32_EMPTY) {
1175
1133
  neighbors[outOffset + 0] = j;
@@ -1182,7 +1140,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1182
1140
  scratchParentTile.level = level - 1;
1183
1141
  scratchParentTile.x = px;
1184
1142
  scratchParentTile.y = py;
1185
- if (surface.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1143
+ if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1186
1144
  j = lookupSpatialIndexRaw(
1187
1145
  index,
1188
1146
  scratchParentNbr.space,
@@ -1238,157 +1196,2158 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1238
1196
  return outSeams;
1239
1197
  }
1240
1198
 
1241
- function createFlatSurface(cfg) {
1242
- const halfRoot = 0.5 * cfg.rootSize;
1243
- const maxHeight = cfg.maxHeight ?? 0;
1244
- const surface = {
1245
- spaceCount: 1,
1246
- maxRootCount: 1,
1247
- neighborSameLevel(tile, dir, out) {
1248
- const level = tile.level;
1249
- const x = tile.x;
1250
- const y = tile.y;
1251
- let nx = x;
1252
- let ny = y;
1253
- switch (dir) {
1254
- case Dir.LEFT:
1255
- nx = x - 1;
1256
- break;
1257
- case Dir.RIGHT:
1258
- nx = x + 1;
1259
- break;
1260
- case Dir.TOP:
1261
- ny = y - 1;
1262
- break;
1263
- case Dir.BOTTOM:
1264
- ny = y + 1;
1265
- break;
1266
- }
1267
- if (nx < 0 || ny < 0) return false;
1268
- const maxCoord = (1 << level) - 1;
1269
- if (nx > maxCoord || ny > maxCoord) return false;
1270
- out.space = 0;
1271
- out.level = level;
1272
- out.x = nx;
1273
- out.y = ny;
1274
- return true;
1275
- },
1276
- tileBounds(tile, cameraOrigin, out) {
1277
- const level = tile.level;
1278
- const scale = 1 / (1 << level);
1279
- const size = cfg.rootSize * scale;
1280
- const minX = cfg.origin.x + (tile.x * size - halfRoot);
1281
- const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1282
- const centerX = minX + 0.5 * size;
1283
- const centerY = cfg.origin.y;
1284
- const centerZ = minZ + 0.5 * size;
1285
- out.cx = centerX - cameraOrigin.x;
1286
- out.cy = centerY - cameraOrigin.y;
1287
- out.cz = centerZ - cameraOrigin.z;
1288
- out.r = 0.7071067811865476 * size + maxHeight;
1289
- },
1290
- rootTiles(_cameraOrigin, out) {
1291
- const root = out[0];
1292
- root.space = 0;
1293
- root.level = 0;
1294
- root.x = 0;
1295
- root.y = 0;
1296
- return 1;
1199
+ function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1200
+ return tsl.Fn(
1201
+ ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
1202
+ const iEdge = tsl.int(edgeVertexCount);
1203
+ const verticesPerNode = iEdge.mul(iEdge);
1204
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1205
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
1206
+ const xRight = tsl.int(ix).add(tsl.int(1));
1207
+ const yUp = tsl.int(iy).sub(tsl.int(1));
1208
+ const yDown = tsl.int(iy).add(tsl.int(1));
1209
+ const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
1210
+ const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
1211
+ const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1212
+ const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1213
+ const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
1214
+ const stepWorld = tileSize.div(innerSegments);
1215
+ const inv2Step = tsl.float(0.5).div(stepWorld);
1216
+ const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
1217
+ const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
1218
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
1297
1219
  }
1298
- };
1299
- return surface;
1220
+ );
1300
1221
  }
1301
-
1302
- function createInfiniteFlatSurface(cfg) {
1303
- const halfRoot = 0.5 * cfg.rootSize;
1304
- const maxHeight = cfg.maxHeight ?? 0;
1305
- const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1306
- const rootWidth = rootGridRadius * 2 + 1;
1307
- return {
1308
- spaceCount: 1,
1309
- maxRootCount: rootWidth * rootWidth,
1310
- neighborSameLevel(tile, dir, out) {
1311
- let nx = tile.x;
1312
- let ny = tile.y;
1313
- switch (dir) {
1314
- case Dir.LEFT:
1315
- nx = tile.x - 1;
1316
- break;
1317
- case Dir.RIGHT:
1318
- nx = tile.x + 1;
1319
- break;
1320
- case Dir.TOP:
1321
- ny = tile.y - 1;
1322
- break;
1323
- case Dir.BOTTOM:
1324
- ny = tile.y + 1;
1325
- break;
1326
- }
1327
- out.space = tile.space;
1328
- out.level = tile.level;
1329
- out.x = nx;
1330
- out.y = ny;
1331
- return true;
1332
- },
1333
- tileBounds(tile, cameraOrigin, out) {
1334
- const level = tile.level;
1335
- const scale = 1 / (1 << level);
1336
- const size = cfg.rootSize * scale;
1337
- const minX = cfg.origin.x + (tile.x * size - halfRoot);
1338
- const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1339
- const centerX = minX + 0.5 * size;
1340
- const centerY = cfg.origin.y;
1341
- const centerZ = minZ + 0.5 * size;
1342
- out.cx = centerX - cameraOrigin.x;
1343
- out.cy = centerY - cameraOrigin.y;
1344
- out.cz = centerZ - cameraOrigin.z;
1345
- out.r = 0.7071067811865476 * size + maxHeight;
1346
- },
1347
- rootTiles(cameraOrigin, out) {
1348
- const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
1349
- const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
1350
- let index = 0;
1351
- for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
1352
- for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
1353
- const root = out[index];
1354
- root.space = 0;
1355
- root.level = 0;
1356
- root.x = camRootX + dx;
1357
- root.y = camRootY + dy;
1358
- index++;
1359
- }
1360
- }
1361
- return index;
1362
- }
1363
- };
1222
+ function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
1223
+ return tsl.Fn(([nodeIndex, ix, iy, elevationScale]) => {
1224
+ const iEdge = tsl.int(edgeVertexCount);
1225
+ const verticesPerNode = iEdge.mul(iEdge);
1226
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1227
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
1228
+ const xRight = tsl.int(ix).add(tsl.int(1));
1229
+ const yUp = tsl.int(iy).sub(tsl.int(1));
1230
+ const yDown = tsl.int(iy).add(tsl.int(1));
1231
+ const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
1232
+ const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
1233
+ const pLeft = positionAt(xLeft, tsl.int(iy), heightAt(xLeft, tsl.int(iy)));
1234
+ const pRight = positionAt(xRight, tsl.int(iy), heightAt(xRight, tsl.int(iy)));
1235
+ const pUp = positionAt(tsl.int(ix), yUp, heightAt(tsl.int(ix), yUp));
1236
+ const pDown = positionAt(tsl.int(ix), yDown, heightAt(tsl.int(ix), yDown));
1237
+ const tangentU = pRight.sub(pLeft);
1238
+ const tangentV = pDown.sub(pUp);
1239
+ const normal = tsl.cross(tangentU, tangentV).normalize();
1240
+ const dir = dirAt(tsl.int(ix), tsl.int(iy));
1241
+ return normal.mul(normal.dot(dir).sign());
1242
+ });
1364
1243
  }
1365
1244
 
1366
- function createCubeSphereSurface(_cfg) {
1245
+ const isSkirtVertex = tsl.Fn(([segments]) => {
1246
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1247
+ const vIndex = tsl.int(tsl.vertexIndex);
1248
+ const segmentEdges = tsl.int(segmentsNode.add(3));
1249
+ const vx = vIndex.mod(segmentEdges);
1250
+ const vy = vIndex.div(segmentEdges);
1251
+ const last = segmentEdges.sub(tsl.int(1));
1252
+ return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
1253
+ });
1254
+ const isSkirtUV = tsl.Fn(([segments]) => {
1255
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1256
+ const ux = tsl.uv().x;
1257
+ const uy = tsl.uv().y;
1258
+ const segmentCount = segmentsNode.add(2);
1259
+ const segmentStep = tsl.float(1).div(segmentCount);
1260
+ const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
1261
+ const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
1262
+ return innerX.and(innerY).not();
1263
+ });
1264
+
1265
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
1266
+ if (!terrainFieldStorage) return tsl.float(0);
1267
+ const innerSegs = terrainUniforms.uInnerTileSegments;
1268
+ const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1269
+ const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1270
+ return sampleTerrainFieldElevation(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex)).mul(
1271
+ terrainUniforms.uElevationScale
1272
+ );
1273
+ }
1274
+ function loadWorldNormal(terrainUniforms, terrainFieldStorage) {
1275
+ const nodeIndex = tsl.int(tsl.instanceIndex);
1276
+ const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
1277
+ const localVertexIndex = tsl.int(tsl.vertexIndex);
1278
+ const ix = localVertexIndex.mod(edgeVertexCount);
1279
+ const iy = localVertexIndex.div(edgeVertexCount);
1280
+ return loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1281
+ }
1282
+ function assignWorldNormal(terrainUniforms, terrainFieldStorage) {
1283
+ if (!terrainFieldStorage) return;
1284
+ tsl.normalLocal.assign(tsl.Fn(() => loadWorldNormal(terrainUniforms, terrainFieldStorage))());
1285
+ }
1286
+ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1287
+ return tsl.Fn(() => {
1288
+ const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
1289
+ const rootSize = terrainUniforms.uRootSize.toVar();
1290
+ const rootOrigin = terrainUniforms.uRootOrigin.toVar();
1291
+ const half = tsl.float(0.5);
1292
+ const size = rootSize.div(tsl.pow(tsl.float(2), tile.level.toFloat()));
1293
+ const halfRoot = rootSize.mul(half);
1294
+ const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
1295
+ const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
1296
+ const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
1297
+ const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
1298
+ const worldX = centerX.add(clampedX.mul(size));
1299
+ const worldZ = centerZ.add(clampedZ.mul(size));
1300
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1301
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1302
+ const baseY = rootOrigin.y.add(yElevation);
1303
+ const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
1304
+ const worldY = tsl.select(skirtVertex, skirtY, baseY);
1305
+ assignWorldNormal(terrainUniforms, terrainFieldStorage);
1306
+ return tsl.vec3(worldX, worldY, worldZ);
1307
+ })();
1308
+ }
1309
+ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, baseU = 1, baseV = 1) {
1310
+ const fBaseU = tsl.float(baseU);
1311
+ const fBaseV = tsl.float(baseV);
1312
+ return tsl.Fn(() => {
1313
+ const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
1314
+ const half = tsl.float(0.5);
1315
+ const localU = tsl.positionLocal.x.max(half.negate()).min(half).add(half);
1316
+ const localV = tsl.positionLocal.z.max(half.negate()).min(half).add(half);
1317
+ const faceUV = faceUVFromTileLocal(tile, localU, localV, fBaseU, fBaseV);
1318
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1319
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1320
+ const displacement = tsl.select(
1321
+ skirtVertex,
1322
+ yElevation.sub(terrainUniforms.uSkirtScale.toVar()),
1323
+ yElevation
1324
+ );
1325
+ assignWorldNormal(terrainUniforms, terrainFieldStorage);
1326
+ return surfacePoint(tile, faceUV, displacement);
1327
+ })();
1328
+ }
1329
+
1330
+ const TWO_PI$2 = Math.PI * 2;
1331
+ function wrap01(t) {
1332
+ const w = t - Math.floor(t);
1333
+ return w >= 1 ? w - 1 : w;
1334
+ }
1335
+ function torusUVToPoint(u, v, majorRadius, minorRadius, displacement, center, out, invert = false) {
1336
+ const theta = TWO_PI$2 * u;
1337
+ const phi = TWO_PI$2 * v;
1338
+ const sinT = Math.sin(theta);
1339
+ const cosT = Math.cos(theta);
1340
+ const sinP = Math.sin(phi);
1341
+ const cosP = Math.cos(phi);
1342
+ const disp = invert ? -displacement : displacement;
1343
+ const tube = minorRadius + disp;
1344
+ const ring = majorRadius + tube * cosP;
1345
+ out[0] = center.x + ring * sinT;
1346
+ out[1] = center.y + tube * sinP;
1347
+ out[2] = center.z + ring * cosT;
1348
+ }
1349
+ function torusOutwardNormal$1(u, v, out, invert = false) {
1350
+ const theta = TWO_PI$2 * u;
1351
+ const phi = TWO_PI$2 * v;
1352
+ const sinT = Math.sin(theta);
1353
+ const cosT = Math.cos(theta);
1354
+ const sinP = Math.sin(phi);
1355
+ const cosP = Math.cos(phi);
1356
+ const s = invert ? -1 : 1;
1357
+ out[0] = cosP * sinT * s;
1358
+ out[1] = sinP * s;
1359
+ out[2] = cosP * cosT * s;
1360
+ }
1361
+ function positionToTorusParams(px, py, pz, majorRadius, center, out) {
1362
+ const qx = px - center.x;
1363
+ const qy = py - center.y;
1364
+ const qz = pz - center.z;
1365
+ const theta = Math.atan2(qx, qz);
1366
+ const rho = Math.hypot(qx, qz);
1367
+ const a = rho - majorRadius;
1368
+ const phi = Math.atan2(qy, a);
1369
+ out.u = wrap01(theta / TWO_PI$2);
1370
+ out.v = wrap01(phi / TWO_PI$2);
1371
+ out.tubeDistance = Math.hypot(a, qy);
1372
+ }
1373
+
1374
+ function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
1375
+ let tMin = -Infinity;
1376
+ let tMax = Infinity;
1377
+ const origin = ray.origin;
1378
+ const dir = ray.direction;
1379
+ const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
1380
+ if (Math.abs(dirAxis) < 1e-8) {
1381
+ if (originAxis < minAxis || originAxis > maxAxis) return false;
1382
+ return true;
1383
+ }
1384
+ const inv = 1 / dirAxis;
1385
+ let t0 = (minAxis - originAxis) * inv;
1386
+ let t1 = (maxAxis - originAxis) * inv;
1387
+ if (t0 > t1) {
1388
+ const tmp = t0;
1389
+ t0 = t1;
1390
+ t1 = tmp;
1391
+ }
1392
+ tMin = Math.max(tMin, t0);
1393
+ tMax = Math.min(tMax, t1);
1394
+ return tMax >= tMin;
1395
+ };
1396
+ if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
1397
+ return null;
1398
+ }
1399
+ return { tMin, tMax };
1400
+ }
1401
+ function getTerrainBounds(config) {
1402
+ const halfRoot = config.rootSize * 0.5;
1403
+ return {
1404
+ minX: config.originX - halfRoot,
1405
+ maxX: config.originX + halfRoot,
1406
+ minZ: config.originZ - halfRoot,
1407
+ maxZ: config.originZ + halfRoot
1408
+ };
1409
+ }
1410
+ function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
1411
+ if (!skipBoundsFastPath) {
1412
+ const tileBounds = query.getTileBounds(worldX, worldZ);
1413
+ if (tileBounds) {
1414
+ if (worldY > tileBounds.maxElevation) {
1415
+ return worldY - tileBounds.maxElevation;
1416
+ }
1417
+ if (worldY < tileBounds.minElevation) {
1418
+ return worldY - tileBounds.minElevation;
1419
+ }
1420
+ }
1421
+ }
1422
+ const elevation = query.getElevation(worldX, worldZ);
1423
+ if (!Number.isFinite(elevation)) return void 0;
1424
+ return worldY - elevation;
1425
+ }
1426
+ function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
1427
+ let prevT = startT;
1428
+ ray.at(prevT, point);
1429
+ let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
1430
+ if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
1431
+ return startT;
1432
+ }
1433
+ for (let i = 1; i <= options.maxSteps; i += 1) {
1434
+ const t = startT + (endT - startT) * i / options.maxSteps;
1435
+ ray.at(t, point);
1436
+ const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
1437
+ if (signedDistance === void 0) {
1438
+ prevSignedDistance = void 0;
1439
+ prevT = t;
1440
+ continue;
1441
+ }
1442
+ if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
1443
+ let lo = prevT;
1444
+ let hi = t;
1445
+ for (let r = 0; r < options.refinementSteps; r += 1) {
1446
+ const mid = (lo + hi) * 0.5;
1447
+ ray.at(mid, point);
1448
+ const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
1449
+ if (midDistance === void 0) {
1450
+ lo = mid;
1451
+ continue;
1452
+ }
1453
+ if (midDistance > 0) lo = mid;
1454
+ else hi = mid;
1455
+ }
1456
+ return hi;
1457
+ }
1458
+ prevSignedDistance = signedDistance;
1459
+ prevT = t;
1460
+ }
1461
+ return null;
1462
+ }
1463
+ function cpuRaycast(query, ray, config, options) {
1464
+ const bounds = getTerrainBounds(config);
1465
+ const segment = intersectRayAabb(
1466
+ ray,
1467
+ bounds.minX,
1468
+ config.minY,
1469
+ bounds.minZ,
1470
+ bounds.maxX,
1471
+ config.maxY,
1472
+ bounds.maxZ
1473
+ );
1474
+ if (!segment) return null;
1475
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1476
+ const startT = Math.max(0, segment.tMin);
1477
+ const endT = Math.min(segment.tMax, maxDistance);
1478
+ if (endT < startT) return null;
1479
+ const point = new three.Vector3();
1480
+ const hitT = marchSignedDistance(
1481
+ ray,
1482
+ startT,
1483
+ endT,
1484
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
1485
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
1486
+ {
1487
+ maxSteps: Math.max(8, options?.maxSteps ?? 128),
1488
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
1489
+ },
1490
+ point
1491
+ );
1492
+ if (hitT === null) return null;
1493
+ ray.at(hitT, point);
1494
+ const sample = query.sampleTerrain(point.x, point.z);
1495
+ if (!sample.valid) return null;
1496
+ point.y = sample.elevation;
1497
+ return {
1498
+ position: point.clone(),
1499
+ normal: sample.normal.clone(),
1500
+ distance: ray.origin.distanceTo(point)
1501
+ };
1502
+ }
1503
+ function cpuRaycastBoundsOnly(ray, config, options) {
1504
+ const bounds = getTerrainBounds(config);
1505
+ const planeY = (config.minY + config.maxY) * 0.5;
1506
+ const dirY = ray.direction.y;
1507
+ if (Math.abs(dirY) < 1e-8) return null;
1508
+ const t = (planeY - ray.origin.y) / dirY;
1509
+ if (t < 0) return null;
1510
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1511
+ if (t > maxDistance) return null;
1512
+ const point = new three.Vector3();
1513
+ ray.at(t, point);
1514
+ if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
1515
+ return null;
1516
+ }
1517
+ return {
1518
+ position: point,
1519
+ normal: new three.Vector3(0, 1, 0),
1520
+ distance: ray.origin.distanceTo(point)
1521
+ };
1522
+ }
1523
+ function intersectRaySphere(ray, cx, cy, cz, radius) {
1524
+ const ox = ray.origin.x - cx;
1525
+ const oy = ray.origin.y - cy;
1526
+ const oz = ray.origin.z - cz;
1527
+ const dx = ray.direction.x;
1528
+ const dy = ray.direction.y;
1529
+ const dz = ray.direction.z;
1530
+ const a = dx * dx + dy * dy + dz * dz;
1531
+ const b = 2 * (ox * dx + oy * dy + oz * dz);
1532
+ const c = ox * ox + oy * oy + oz * oz - radius * radius;
1533
+ const disc = b * b - 4 * a * c;
1534
+ if (disc < 0) return null;
1535
+ const sqrtDisc = Math.sqrt(disc);
1536
+ const inv2a = 1 / (2 * a);
1537
+ return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
1538
+ }
1539
+ function sphereSignedDistance(query, params, px, py, pz, scratchDir) {
1540
+ const dx = px - params.centerX;
1541
+ const dy = py - params.centerY;
1542
+ const dz = pz - params.centerZ;
1543
+ const dist = Math.hypot(dx, dy, dz);
1544
+ scratchDir.set(dx, dy, dz);
1545
+ const elevation = query.getElevationByDirection(scratchDir);
1546
+ if (elevation === null) return void 0;
1547
+ const s = params.invert ? -1 : 1;
1548
+ return s * (dist - (params.radius + s * elevation));
1549
+ }
1550
+ function cubeSphereRaycast(query, ray, params, options) {
1551
+ const shell = intersectRaySphere(
1552
+ ray,
1553
+ params.centerX,
1554
+ params.centerY,
1555
+ params.centerZ,
1556
+ params.maxRadius
1557
+ );
1558
+ if (!shell) return null;
1559
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1560
+ const startT = Math.max(0, shell.t0);
1561
+ const endT = Math.min(shell.t1, maxDistance);
1562
+ if (endT < startT) return null;
1563
+ const scratchDir = new three.Vector3();
1564
+ const point = new three.Vector3();
1565
+ const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, params, px, py, pz, scratchDir);
1566
+ const hitT = marchSignedDistance(
1567
+ ray,
1568
+ startT,
1569
+ endT,
1570
+ signedDistanceAt,
1571
+ signedDistanceAt,
1572
+ {
1573
+ maxSteps: Math.max(8, options?.maxSteps ?? 256),
1574
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
1575
+ },
1576
+ point
1577
+ );
1578
+ if (hitT === null) return null;
1579
+ ray.at(hitT, point);
1580
+ const sample = query.sampleTerrainByPosition(point);
1581
+ if (!sample.valid) return null;
1582
+ return {
1583
+ position: sample.position.clone(),
1584
+ normal: sample.normal.clone(),
1585
+ distance: ray.origin.distanceTo(sample.position)
1586
+ };
1587
+ }
1588
+ function cubeSphereRaycastBoundsOnly(ray, params, options) {
1589
+ const shell = intersectRaySphere(ray, params.centerX, params.centerY, params.centerZ, params.radius);
1590
+ if (!shell) return null;
1591
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1592
+ const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1593
+ if (t < 0 || t > maxDistance) return null;
1594
+ const point = new three.Vector3();
1595
+ ray.at(t, point);
1596
+ const normal = new three.Vector3(
1597
+ point.x - params.centerX,
1598
+ point.y - params.centerY,
1599
+ point.z - params.centerZ
1600
+ ).normalize();
1601
+ if (params.invert) normal.negate();
1602
+ return { position: point, normal, distance: ray.origin.distanceTo(point) };
1603
+ }
1604
+ function torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams) {
1605
+ positionToTorusParams(
1606
+ px,
1607
+ py,
1608
+ pz,
1609
+ params.majorRadius,
1610
+ { x: params.centerX, y: params.centerY, z: params.centerZ },
1611
+ scratchParams
1612
+ );
1613
+ scratchPoint.set(px, py, pz);
1614
+ const elevation = query.getElevationByPosition(scratchPoint);
1615
+ if (elevation === null) return void 0;
1616
+ const s = params.invert ? -1 : 1;
1617
+ return s * (scratchParams.tubeDistance - (params.minorRadius + s * elevation));
1618
+ }
1619
+ function torusRaycast(query, ray, params, options) {
1620
+ const shell = intersectRaySphere(
1621
+ ray,
1622
+ params.centerX,
1623
+ params.centerY,
1624
+ params.centerZ,
1625
+ params.outerRadius
1626
+ );
1627
+ if (!shell) return null;
1628
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1629
+ const startT = Math.max(0, shell.t0);
1630
+ const endT = Math.min(shell.t1, maxDistance);
1631
+ if (endT < startT) return null;
1632
+ const scratchPoint = new three.Vector3();
1633
+ const scratchParams = { u: 0, v: 0, tubeDistance: 0 };
1634
+ const point = new three.Vector3();
1635
+ const signedDistanceAt = (px, py, pz) => torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams);
1636
+ const hitT = marchSignedDistance(
1637
+ ray,
1638
+ startT,
1639
+ endT,
1640
+ signedDistanceAt,
1641
+ signedDistanceAt,
1642
+ {
1643
+ maxSteps: Math.max(8, options?.maxSteps ?? 256),
1644
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
1645
+ },
1646
+ point
1647
+ );
1648
+ if (hitT === null) return null;
1649
+ ray.at(hitT, point);
1650
+ const sample = query.sampleTerrainByPosition(point);
1651
+ if (!sample.valid) return null;
1652
+ return {
1653
+ position: sample.position.clone(),
1654
+ normal: sample.normal.clone(),
1655
+ distance: ray.origin.distanceTo(sample.position)
1656
+ };
1657
+ }
1658
+ function torusRaycastBoundsOnly(ray, params, options) {
1659
+ const shell = intersectRaySphere(
1660
+ ray,
1661
+ params.centerX,
1662
+ params.centerY,
1663
+ params.centerZ,
1664
+ params.outerRadius
1665
+ );
1666
+ if (!shell) return null;
1667
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1668
+ const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1669
+ if (t < 0 || t > maxDistance) return null;
1670
+ const point = new three.Vector3();
1671
+ ray.at(t, point);
1672
+ const normal = new three.Vector3(
1673
+ point.x - params.centerX,
1674
+ point.y - params.centerY,
1675
+ point.z - params.centerZ
1676
+ ).normalize();
1677
+ if (params.invert) normal.negate();
1678
+ return { position: point, normal, distance: ray.origin.distanceTo(point) };
1679
+ }
1680
+
1681
+ function createTerrainQuery(cache) {
1682
+ return {
1683
+ get generation() {
1684
+ return cache.generation;
1685
+ },
1686
+ getElevation(worldX, worldZ) {
1687
+ return cache.getElevation(worldX, worldZ);
1688
+ },
1689
+ getNormal(worldX, worldZ) {
1690
+ return cache.getNormal(worldX, worldZ);
1691
+ },
1692
+ getTile(worldX, worldZ) {
1693
+ return cache.getTile(worldX, worldZ);
1694
+ },
1695
+ getTileBounds(worldX, worldZ) {
1696
+ return cache.getTileBounds(worldX, worldZ);
1697
+ },
1698
+ getGlobalElevationRange() {
1699
+ return cache.getGlobalElevationRange();
1700
+ },
1701
+ sampleTerrain(worldX, worldZ) {
1702
+ return cache.sampleTerrain(worldX, worldZ);
1703
+ },
1704
+ sampleTerrainBatch(positions) {
1705
+ return cache.sampleTerrainBatch(positions);
1706
+ }
1707
+ };
1708
+ }
1709
+ function createTerrainSurfaceQuery(cache) {
1710
+ return {
1711
+ get generation() {
1712
+ return cache.generation;
1713
+ },
1714
+ getElevationByPosition(position) {
1715
+ return cache.getElevationBySurfacePosition(position.x, position.y, position.z);
1716
+ },
1717
+ getNormalByPosition(position) {
1718
+ return cache.getNormalBySurfacePosition(position.x, position.y, position.z);
1719
+ },
1720
+ sampleTerrainByPosition(position) {
1721
+ return cache.sampleSurfaceByPosition(position.x, position.y, position.z);
1722
+ },
1723
+ getTileByPosition(position) {
1724
+ return cache.getTileBySurfacePosition(position.x, position.y, position.z);
1725
+ },
1726
+ getTileBoundsByPosition(position) {
1727
+ return cache.getTileBoundsBySurfacePosition(position.x, position.y, position.z);
1728
+ },
1729
+ sampleTerrainBatchByPosition(positions) {
1730
+ return cache.sampleSurfaceBatchByPosition(positions);
1731
+ }
1732
+ };
1733
+ }
1734
+
1735
+ function createFlatTileComputeParts(ctx) {
1736
+ const { uniforms, shared } = ctx;
1737
+ const tileSize = tsl.Fn(([nodeIndex]) => {
1738
+ const level = shared.tileLevel(nodeIndex);
1739
+ const divisor = tsl.pow(tsl.float(2), level.toFloat());
1740
+ return tsl.float(uniforms.uRootSize.toVar()).div(divisor);
1741
+ });
1742
+ const rootUV = tsl.Fn(([nodeIndex, ix, iy]) => {
1743
+ const nodeVec2 = shared.tileOriginVec2(nodeIndex);
1744
+ const nodeX = nodeVec2.x;
1745
+ const nodeY = nodeVec2.y;
1746
+ const rootSize = uniforms.uRootSize.toVar();
1747
+ const rootOrigin = uniforms.uRootOrigin.toVar();
1748
+ const size = tileSize(nodeIndex);
1749
+ const half = tsl.float(0.5);
1750
+ const halfRoot = tsl.float(rootSize).mul(half);
1751
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
1752
+ const texelSpacing = size.div(fInnerSegments);
1753
+ const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
1754
+ const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
1755
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
1756
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
1757
+ const centeredX = worldX.sub(rootOrigin.x);
1758
+ const centeredZ = worldZ.sub(rootOrigin.z);
1759
+ return tsl.vec2(
1760
+ centeredX.div(rootSize).add(half),
1761
+ centeredZ.div(rootSize).mul(tsl.float(-1)).add(half)
1762
+ );
1763
+ });
1764
+ const tileVertexWorldPosition = tsl.Fn(([nodeIndex, ix, iy]) => {
1765
+ const rootOrigin = uniforms.uRootOrigin.toVar();
1766
+ const nodeVec2 = shared.tileOriginVec2(nodeIndex);
1767
+ const nodeX = nodeVec2.x;
1768
+ const nodeY = nodeVec2.y;
1769
+ const rootSize = uniforms.uRootSize.toVar();
1770
+ const size = tileSize(nodeIndex);
1771
+ const half = tsl.float(0.5);
1772
+ const halfRoot = tsl.float(rootSize).mul(half);
1773
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
1774
+ const texelSpacing = size.div(fInnerSegments);
1775
+ const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
1776
+ const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
1777
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
1778
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
1779
+ return tsl.vec3(worldX, rootOrigin.y, worldZ);
1780
+ });
1781
+ return {
1782
+ tileSize: (nodeIndex) => tileSize(nodeIndex),
1783
+ rootUV: (nodeIndex, ix, iy) => rootUV(nodeIndex, ix, iy),
1784
+ tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
1785
+ };
1786
+ }
1787
+ function createFlatProjection() {
1788
+ return {
1789
+ kind: "flat",
1790
+ faceOutward: false,
1791
+ gpu: {
1792
+ renderVertexPosition(ctx) {
1793
+ return createFlatRenderVertexPosition(ctx.leafStorage, ctx.uniforms, ctx.terrainFieldStorage);
1794
+ },
1795
+ createTileComputeParts: createFlatTileComputeParts,
1796
+ createFieldNormal(ctx) {
1797
+ const computeNormal = createFlatNormalFromElevationField(
1798
+ ctx.elevationFieldNode,
1799
+ ctx.edgeVertexCount
1800
+ );
1801
+ return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ctx.tile.tileSize(nodeIndex), ix, iy, ctx.uniforms.uElevationScale);
1802
+ }
1803
+ },
1804
+ cpu: {
1805
+ cameraSurfaceOffset(cam, elevation) {
1806
+ cam.y -= elevation;
1807
+ },
1808
+ createSurfaceOps() {
1809
+ return null;
1810
+ },
1811
+ createRuntimeQueries(cache) {
1812
+ return { query: createTerrainQuery(cache), surfaceQuery: null, sphereQuery: null };
1813
+ },
1814
+ raycast(ctx) {
1815
+ const { ray, options, terrainQuery, config } = ctx;
1816
+ if (terrainQuery) {
1817
+ const precise = cpuRaycast(terrainQuery, ray, config, options);
1818
+ if (precise) return precise;
1819
+ }
1820
+ const coarse = cpuRaycastBoundsOnly(ray, config, options);
1821
+ if (coarse && terrainQuery) {
1822
+ const sample = terrainQuery.sampleTerrain(coarse.position.x, coarse.position.z);
1823
+ if (sample.valid) {
1824
+ coarse.position.y = sample.elevation;
1825
+ coarse.normal.copy(sample.normal);
1826
+ }
1827
+ }
1828
+ return coarse;
1829
+ }
1830
+ }
1831
+ };
1832
+ }
1833
+
1834
+ function createFlatTopology(cfg) {
1835
+ const halfRoot = 0.5 * cfg.rootSize;
1836
+ const topology = {
1837
+ spaceCount: 1,
1838
+ maxRootCount: 1,
1839
+ projection: createFlatProjection(),
1840
+ neighborSameLevel(tile, dir, out) {
1841
+ const level = tile.level;
1842
+ const x = tile.x;
1843
+ const y = tile.y;
1844
+ let nx = x;
1845
+ let ny = y;
1846
+ switch (dir) {
1847
+ case Dir.LEFT:
1848
+ nx = x - 1;
1849
+ break;
1850
+ case Dir.RIGHT:
1851
+ nx = x + 1;
1852
+ break;
1853
+ case Dir.TOP:
1854
+ ny = y - 1;
1855
+ break;
1856
+ case Dir.BOTTOM:
1857
+ ny = y + 1;
1858
+ break;
1859
+ }
1860
+ if (nx < 0 || ny < 0) return false;
1861
+ const maxCoord = (1 << level) - 1;
1862
+ if (nx > maxCoord || ny > maxCoord) return false;
1863
+ out.space = 0;
1864
+ out.level = level;
1865
+ out.x = nx;
1866
+ out.y = ny;
1867
+ return true;
1868
+ },
1869
+ tileBounds(tile, cameraOrigin, out, elevationRange) {
1870
+ const level = tile.level;
1871
+ const scale = 1 / (1 << level);
1872
+ const size = cfg.rootSize * scale;
1873
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1874
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1875
+ const centerX = minX + 0.5 * size;
1876
+ const centerZ = minZ + 0.5 * size;
1877
+ const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1878
+ out.cx = centerX - cameraOrigin.x;
1879
+ out.cy = centerY - cameraOrigin.y;
1880
+ out.cz = centerZ - cameraOrigin.z;
1881
+ const halfDiag = 0.7071067811865476 * size;
1882
+ const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1883
+ out.r = halfDiag + vertExtent;
1884
+ },
1885
+ rootTiles(_cameraOrigin, out) {
1886
+ const root = out[0];
1887
+ root.space = 0;
1888
+ root.level = 0;
1889
+ root.x = 0;
1890
+ root.y = 0;
1891
+ return 1;
1892
+ }
1893
+ };
1894
+ return topology;
1895
+ }
1896
+
1897
+ function createInfiniteFlatTopology(cfg) {
1898
+ const halfRoot = 0.5 * cfg.rootSize;
1899
+ const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1900
+ const rootWidth = rootGridRadius * 2 + 1;
1901
+ return {
1902
+ spaceCount: 1,
1903
+ maxRootCount: rootWidth * rootWidth,
1904
+ projection: createFlatProjection(),
1905
+ neighborSameLevel(tile, dir, out) {
1906
+ let nx = tile.x;
1907
+ let ny = tile.y;
1908
+ switch (dir) {
1909
+ case Dir.LEFT:
1910
+ nx = tile.x - 1;
1911
+ break;
1912
+ case Dir.RIGHT:
1913
+ nx = tile.x + 1;
1914
+ break;
1915
+ case Dir.TOP:
1916
+ ny = tile.y - 1;
1917
+ break;
1918
+ case Dir.BOTTOM:
1919
+ ny = tile.y + 1;
1920
+ break;
1921
+ }
1922
+ out.space = tile.space;
1923
+ out.level = tile.level;
1924
+ out.x = nx;
1925
+ out.y = ny;
1926
+ return true;
1927
+ },
1928
+ tileBounds(tile, cameraOrigin, out, elevationRange) {
1929
+ const level = tile.level;
1930
+ const scale = 1 / (1 << level);
1931
+ const size = cfg.rootSize * scale;
1932
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1933
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1934
+ const centerX = minX + 0.5 * size;
1935
+ const centerZ = minZ + 0.5 * size;
1936
+ const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1937
+ out.cx = centerX - cameraOrigin.x;
1938
+ out.cy = centerY - cameraOrigin.y;
1939
+ out.cz = centerZ - cameraOrigin.z;
1940
+ const halfDiag = 0.7071067811865476 * size;
1941
+ const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1942
+ out.r = halfDiag + vertExtent;
1943
+ },
1944
+ rootTiles(cameraOrigin, out) {
1945
+ const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
1946
+ const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
1947
+ let index = 0;
1948
+ for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
1949
+ for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
1950
+ const root = out[index];
1951
+ root.space = 0;
1952
+ root.level = 0;
1953
+ root.x = camRootX + dx;
1954
+ root.y = camRootY + dy;
1955
+ index++;
1956
+ }
1957
+ }
1958
+ return index;
1959
+ }
1960
+ };
1961
+ }
1962
+
1963
+ const CUBE_FACE_COUNT = 6;
1964
+ const CUBE_FACES = [
1965
+ // 0: +X
1966
+ { forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
1967
+ // 1: -X
1968
+ { forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
1969
+ // 2: +Y (north pole)
1970
+ { forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1971
+ // 3: -Y (south pole)
1972
+ { forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
1973
+ // 4: +Z
1974
+ { forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
1975
+ // 5: -Z
1976
+ { forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
1977
+ ];
1978
+
1979
+ function vec3Const(v) {
1980
+ return tsl.vec3(tsl.float(v[0]), tsl.float(v[1]), tsl.float(v[2]));
1981
+ }
1982
+ function selectFaceVec3(face, pick) {
1983
+ const last = CUBE_FACES.length - 1;
1984
+ let acc = vec3Const(pick(CUBE_FACES[last]));
1985
+ for (let i = last - 1; i >= 0; i--) {
1986
+ acc = tsl.select(tsl.int(face).equal(tsl.int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
1987
+ }
1988
+ return acc;
1989
+ }
1990
+ function cubeFaceBasis(face) {
1991
+ return {
1992
+ forward: selectFaceVec3(face, (f) => f.forward),
1993
+ right: selectFaceVec3(face, (f) => f.right),
1994
+ up: selectFaceVec3(face, (f) => f.up)
1995
+ };
1996
+ }
1997
+ function cubeFacePoint(basis, u, v) {
1998
+ const s = tsl.float(u).mul(2).sub(1);
1999
+ const t = tsl.float(v).mul(2).sub(1);
2000
+ return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
2001
+ }
2002
+ function cubeFaceDirection(basis, u, v) {
2003
+ return cubeFacePoint(basis, u, v).normalize();
2004
+ }
2005
+ function tangentFromAxis(dir, axis) {
2006
+ return axis.sub(dir.mul(dir.dot(axis))).normalize();
2007
+ }
2008
+ function unpackTangentNormal(nx, nz) {
2009
+ const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0)).sqrt();
2010
+ return tsl.vec3(nx, ny, nz);
2011
+ }
2012
+ function sphereTangentFrameNormal(dir, basis, tangentNormal) {
2013
+ const n = tsl.vec3(tangentNormal);
2014
+ const tu = tangentFromAxis(dir, basis.right);
2015
+ const tv = tangentFromAxis(dir, basis.up);
2016
+ return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
2017
+ }
2018
+ function cubeFaceFromDirection(dir) {
2019
+ const d = tsl.vec3(dir);
2020
+ const ax = d.x.abs();
2021
+ const ay = d.y.abs();
2022
+ const az = d.z.abs();
2023
+ const faceX = tsl.select(d.x.greaterThanEqual(tsl.float(0)), tsl.int(0), tsl.int(1));
2024
+ const faceY = tsl.select(d.y.greaterThanEqual(tsl.float(0)), tsl.int(2), tsl.int(3));
2025
+ const faceZ = tsl.select(d.z.greaterThanEqual(tsl.float(0)), tsl.int(4), tsl.int(5));
2026
+ const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
2027
+ const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
2028
+ return tsl.select(xDominant, faceX, tsl.select(yDominant, faceY, faceZ));
2029
+ }
2030
+ function cubeFaceUVFromDirection(basis, dir) {
2031
+ const d = tsl.vec3(dir);
2032
+ const p = d.div(d.dot(basis.forward));
2033
+ const s = p.dot(basis.right);
2034
+ const t = p.dot(basis.up);
2035
+ return tsl.vec2(s.add(tsl.float(1)).mul(tsl.float(0.5)), t.add(tsl.float(1)).mul(tsl.float(0.5)));
2036
+ }
2037
+
2038
+ const DEG_TO_RAD = Math.PI / 180;
2039
+ const RAD_TO_DEG = 180 / Math.PI;
2040
+ function dot(a, b) {
2041
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2042
+ }
2043
+ function faceUVToCube(face, u, v, out) {
2044
+ const f = CUBE_FACES[face];
2045
+ const s = 2 * u - 1;
2046
+ const t = 2 * v - 1;
2047
+ out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
2048
+ out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
2049
+ out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
2050
+ }
2051
+ function directionToFace(d) {
2052
+ const ax = Math.abs(d[0]);
2053
+ const ay = Math.abs(d[1]);
2054
+ const az = Math.abs(d[2]);
2055
+ if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
2056
+ if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
2057
+ return d[2] >= 0 ? 4 : 5;
2058
+ }
2059
+ function directionToFaceUV(face, d, out) {
2060
+ const f = CUBE_FACES[face];
2061
+ const denom = dot(d, f.forward);
2062
+ const inv = 1 / denom;
2063
+ const px = d[0] * inv;
2064
+ const py = d[1] * inv;
2065
+ const pz = d[2] * inv;
2066
+ const p = [px, py, pz];
2067
+ const s = dot(p, f.right);
2068
+ const t = dot(p, f.up);
2069
+ out[0] = (s + 1) * 0.5;
2070
+ out[1] = (t + 1) * 0.5;
2071
+ }
2072
+ function latLongToDirection(latDeg, lonDeg, out) {
2073
+ const lat = latDeg * DEG_TO_RAD;
2074
+ const lon = lonDeg * DEG_TO_RAD;
2075
+ const cosLat = Math.cos(lat);
2076
+ out[0] = cosLat * Math.sin(lon);
2077
+ out[1] = Math.sin(lat);
2078
+ out[2] = cosLat * Math.cos(lon);
2079
+ }
2080
+ function directionToLatLong(d) {
2081
+ const len = Math.hypot(d[0], d[1], d[2]) || 1;
2082
+ const y = Math.max(-1, Math.min(1, d[1] / len));
2083
+ return {
2084
+ latitude: Math.asin(y) * RAD_TO_DEG,
2085
+ longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
2086
+ };
2087
+ }
2088
+
2089
+ function readHeight(elevation, shape, leafIndex, ix, iy) {
2090
+ const base = leafIndex * shape.verticesPerNode;
2091
+ return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
2092
+ }
2093
+ function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
2094
+ const max = shape.edgeVertexCount - 1;
2095
+ const x = Math.max(0, Math.min(max, gx));
2096
+ const y = Math.max(0, Math.min(max, gy));
2097
+ const x0 = Math.floor(x);
2098
+ const y0 = Math.floor(y);
2099
+ const x1 = Math.min(max, x0 + 1);
2100
+ const y1 = Math.min(max, y0 + 1);
2101
+ const tx = x - x0;
2102
+ const ty = y - y0;
2103
+ const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
2104
+ const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
2105
+ const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
2106
+ const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
2107
+ const hx0 = h00 + (h10 - h00) * tx;
2108
+ const hx1 = h01 + (h11 - h01) * tx;
2109
+ return hx0 + (hx1 - hx0) * ty;
2110
+ }
2111
+ function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
2112
+ const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
2113
+ const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
2114
+ const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
2115
+ const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
2116
+ const inv2Step = 0.5 / stepWorld;
2117
+ out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
2118
+ out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
2119
+ return out;
2120
+ }
2121
+
2122
+ function createElevationFunction(callback) {
2123
+ const tslFunction = (args) => {
2124
+ const params = {
2125
+ worldPosition: args.worldPosition,
2126
+ rootSize: args.rootSize,
2127
+ rootUV: args.rootUV,
2128
+ tileUV: args.tileUV,
2129
+ tileLevel: args.tileLevel,
2130
+ tileSize: args.tileSize,
2131
+ tileOriginVec2: args.tileOriginVec2,
2132
+ nodeIndex: args.nodeIndex
2133
+ };
2134
+ return callback(params);
2135
+ };
2136
+ return TSL_js.Fn(tslFunction);
2137
+ }
2138
+
2139
+ const SLOT_STRIDE = 6;
2140
+ function nextPow2$1(n) {
2141
+ let x = 1;
2142
+ while (x < n) x <<= 1;
2143
+ return x;
2144
+ }
2145
+ function createGpuSpatialIndex(maxEntries) {
2146
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
2147
+ const data = new Uint32Array(size * SLOT_STRIDE);
2148
+ const attribute = new webgpu.StorageBufferAttribute(data, SLOT_STRIDE);
2149
+ attribute.name = "gpuSpatialIndex";
2150
+ const node = tsl.storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
2151
+ const stampGen = tsl.uniform(tsl.uint(1)).setName("uGpuSpatialIndexStampGen");
2152
+ return {
2153
+ data,
2154
+ size,
2155
+ mask: size - 1,
2156
+ stampGen,
2157
+ attribute,
2158
+ node
2159
+ };
2160
+ }
2161
+ function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
2162
+ if (gpuIndex.size !== cpuIndex.size) {
2163
+ throw new Error(
2164
+ `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
2165
+ );
2166
+ }
2167
+ for (let i = 0; i < cpuIndex.size; i += 1) {
2168
+ const base = i * SLOT_STRIDE;
2169
+ gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
2170
+ gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
2171
+ gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
2172
+ gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
2173
+ gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
2174
+ gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
2175
+ }
2176
+ gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
2177
+ gpuIndex.attribute.needsUpdate = true;
2178
+ gpuIndex.node.needsUpdate = true;
2179
+ }
2180
+ function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
2181
+ const offset = tsl.int(slot).mul(tsl.int(SLOT_STRIDE)).add(tsl.int(fieldOffset));
2182
+ return spatialIndex.node.element(offset).toUint();
2183
+ }
2184
+ const mix32$1 = tsl.Fn(([x]) => {
2185
+ const v = tsl.uint(x).toVar();
2186
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2187
+ v.assign(v.mul(tsl.uint(2146121005)));
2188
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(15))));
2189
+ v.assign(v.mul(tsl.uint(2221713035)));
2190
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2191
+ return v;
2192
+ });
2193
+ const hashKey$1 = tsl.Fn(([space, level, x, y]) => {
2194
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
2195
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
2196
+ const h = s.bitXor(l.shiftLeft(tsl.uint(8))).bitXor(mix32$1(tsl.uint(x))).bitXor(mix32$1(tsl.uint(y)));
2197
+ return mix32$1(h);
2198
+ });
2199
+ const createGpuSpatialLookup = (spatialIndex) => {
2200
+ const slotCount = spatialIndex.size;
2201
+ const mask = tsl.uint(spatialIndex.mask);
2202
+ const stampGen = spatialIndex.stampGen.toUint();
2203
+ const emptyValue = tsl.int(-1);
2204
+ return tsl.Fn(([space, level, x, y]) => {
2205
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
2206
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
2207
+ const xx = tsl.uint(x);
2208
+ const yy = tsl.uint(y);
2209
+ const result = emptyValue.toVar();
2210
+ const slot = hashKey$1(s, l, xx, yy).bitAnd(mask).toVar();
2211
+ const probes = tsl.int(0).toVar();
2212
+ tsl.Loop(slotCount, () => {
2213
+ const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
2214
+ tsl.If(stamp.notEqual(stampGen), () => {
2215
+ tsl.Break();
2216
+ });
2217
+ const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
2218
+ const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
2219
+ const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
2220
+ const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
2221
+ tsl.If(
2222
+ ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
2223
+ () => {
2224
+ result.assign(tsl.int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
2225
+ tsl.Break();
2226
+ }
2227
+ );
2228
+ slot.assign(slot.add(tsl.uint(1)).bitAnd(mask));
2229
+ probes.addAssign(1);
2230
+ });
2231
+ return result;
2232
+ });
2233
+ };
2234
+ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
2235
+ const lookup = createGpuSpatialLookup(spatialIndex);
2236
+ const levelCount = Math.max(1, maxLevel + 1);
2237
+ return tsl.Fn(([worldX, worldZ]) => {
2238
+ const rootOrigin = uniforms.uRootOrigin.toVar();
2239
+ const rootSize = uniforms.uRootSize.toVar();
2240
+ const halfRoot = rootSize.mul(tsl.float(0.5));
2241
+ const tileIndex = tsl.int(-1).toVar();
2242
+ const tileU = tsl.float(0).toVar();
2243
+ const tileV = tsl.float(0).toVar();
2244
+ const i = tsl.int(0).toVar();
2245
+ tsl.Loop(levelCount, () => {
2246
+ const level = tsl.int(maxLevel).sub(i).toVar();
2247
+ const scale = tsl.pow(tsl.float(2), level.toFloat());
2248
+ const tileSize = rootSize.div(scale);
2249
+ const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
2250
+ const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
2251
+ const maybeIndex = lookup(tsl.int(0), level, tileX, tileY).toVar();
2252
+ tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
2253
+ const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
2254
+ const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
2255
+ tileIndex.assign(maybeIndex);
2256
+ tileU.assign(worldX.sub(minX).div(tileSize));
2257
+ tileV.assign(worldZ.sub(minZ).div(tileSize));
2258
+ tsl.Break();
2259
+ });
2260
+ i.addAssign(1);
2261
+ });
2262
+ return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2263
+ });
2264
+ };
2265
+ const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
2266
+ const lookup = createGpuSpatialLookup(spatialIndex);
2267
+ const levelCount = Math.max(1, maxLevel + 1);
2268
+ return tsl.Fn(([direction]) => {
2269
+ const dir = tsl.vec3(direction).normalize().toVar();
2270
+ const face = cubeFaceFromDirection(dir).toVar();
2271
+ const basis = cubeFaceBasis(face);
2272
+ const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
2273
+ const u = faceUV.x.toVar();
2274
+ const v = faceUV.y.toVar();
2275
+ const tileIndex = tsl.int(-1).toVar();
2276
+ const tileU = tsl.float(0).toVar();
2277
+ const tileV = tsl.float(0).toVar();
2278
+ const i = tsl.int(0).toVar();
2279
+ tsl.Loop(levelCount, () => {
2280
+ const level = tsl.int(maxLevel).sub(i).toVar();
2281
+ const n = tsl.pow(tsl.float(2), level.toFloat()).toVar();
2282
+ const nInt = tsl.int(n).toVar();
2283
+ const tileX = u.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
2284
+ const tileY = v.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
2285
+ const maybeIndex = lookup(face, level, tileX, tileY).toVar();
2286
+ tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
2287
+ tileIndex.assign(maybeIndex);
2288
+ tileU.assign(u.mul(n).sub(tileX.toFloat()));
2289
+ tileV.assign(v.mul(n).sub(tileY.toFloat()));
2290
+ tsl.Break();
2291
+ });
2292
+ i.addAssign(1);
2293
+ });
2294
+ return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2295
+ });
2296
+ };
2297
+
2298
+ function packedSampleFromTileResult(params, tileResult) {
2299
+ const tileIndex = tsl.int(tileResult.x).toVar();
2300
+ const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
2301
+ const fieldU = tileLocalToFieldUV(
2302
+ tileResult.y,
2303
+ params.uniforms.uInnerTileSegments
2304
+ ).toVar();
2305
+ const fieldV = tileLocalToFieldUV(
2306
+ tileResult.z,
2307
+ params.uniforms.uInnerTileSegments
2308
+ ).toVar();
2309
+ const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
2310
+ const sampled = sampleTerrainField(
2311
+ params.terrainFieldStorage,
2312
+ fieldU,
2313
+ fieldV,
2314
+ safeTileIndex
2315
+ ).toVar();
2316
+ const normal = tsl.vec3(sampled.g, sampled.b, sampled.a);
2317
+ const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2318
+ return tsl.vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2319
+ }
2320
+ function createTerrainSampleNode(params) {
2321
+ const tileLookup = createTileIndexFromWorldPosition(
2322
+ params.spatialIndex,
2323
+ params.uniforms,
2324
+ params.maxLevel
2325
+ );
2326
+ return tsl.Fn(([worldX, worldZ]) => {
2327
+ const tileResult = tileLookup(worldX, worldZ).toVar();
2328
+ return packedSampleFromTileResult(params, tileResult);
2329
+ });
2330
+ }
2331
+ function createTerrainSampleNodeByDirection(params) {
2332
+ const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
2333
+ return tsl.Fn(([direction]) => {
2334
+ const tileResult = tileLookup(direction).toVar();
2335
+ return packedSampleFromTileResult(params, tileResult);
2336
+ });
2337
+ }
2338
+ function augmentCubeSphereSampler(sampler, params) {
2339
+ const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
2340
+ sampler.sampleTerrainByDirection = tsl.Fn(([direction]) => terrainSampleByDir(direction));
2341
+ sampler.sampleElevationByDirection = tsl.Fn(
2342
+ ([direction]) => terrainSampleByDir(direction).x
2343
+ );
2344
+ sampler.sampleValidityByDirection = tsl.Fn(([direction]) => {
2345
+ const sample = terrainSampleByDir(direction).toVar();
2346
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
2347
+ });
2348
+ sampler.sampleNormalByDirection = tsl.Fn(([direction]) => {
2349
+ const packed = terrainSampleByDir(direction).toVar();
2350
+ return tsl.vec3(packed.y, packed.z, packed.w).normalize();
2351
+ });
2352
+ }
2353
+ function createTerrainSampler(params) {
2354
+ const elevationNode = createElevationFunction(params.elevationCallback);
2355
+ const terrainSampleAt = createTerrainSampleNode(params);
2356
+ const evaluateElevationAt = tsl.Fn(([worldX, worldZ]) => {
2357
+ const rootOrigin = params.uniforms.uRootOrigin.toVar();
2358
+ const rootSize = params.uniforms.uRootSize.toVar();
2359
+ const centeredX = worldX.sub(rootOrigin.x);
2360
+ const centeredZ = worldZ.sub(rootOrigin.z);
2361
+ const rootUV = tsl.vec2(
2362
+ centeredX.div(rootSize).add(0.5),
2363
+ centeredZ.div(rootSize).mul(tsl.float(-1)).add(0.5)
2364
+ ).toVar();
2365
+ return elevationNode({
2366
+ worldPosition: tsl.vec3(worldX, rootOrigin.y, worldZ),
2367
+ rootSize,
2368
+ rootUV,
2369
+ tileUV: rootUV,
2370
+ tileLevel: tsl.int(0),
2371
+ tileSize: rootSize,
2372
+ tileOriginVec2: tsl.vec2(0, 0),
2373
+ nodeIndex: tsl.int(0)
2374
+ });
2375
+ });
2376
+ const sampleTerrain = tsl.Fn(
2377
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
2378
+ );
2379
+ const sampleElevation = tsl.Fn(
2380
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
2381
+ );
2382
+ const sampleNormal = tsl.Fn(([worldX, worldZ]) => {
2383
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2384
+ return tsl.vec3(sample.y, sample.z, sample.w);
2385
+ });
2386
+ const sampleValidity = tsl.Fn(([worldX, worldZ]) => {
2387
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2388
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
2389
+ });
2390
+ const evaluateElevation = tsl.Fn(
2391
+ ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
2392
+ );
2393
+ const evaluateNormalNode = tsl.Fn(
2394
+ ([worldX, worldZ, epsilon]) => {
2395
+ const eps = epsilon ?? tsl.float(0.1);
2396
+ const elevationScale = params.uniforms.uElevationScale.toVar();
2397
+ const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
2398
+ elevationScale
2399
+ );
2400
+ const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
2401
+ elevationScale
2402
+ );
2403
+ const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
2404
+ elevationScale
2405
+ );
2406
+ const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
2407
+ elevationScale
2408
+ );
2409
+ const inv2eps = tsl.float(0.5).div(eps);
2410
+ const dhdx = hR.sub(hL).mul(inv2eps);
2411
+ const dhdz = hU.sub(hD).mul(inv2eps);
2412
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
2413
+ }
2414
+ );
2415
+ const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
2416
+ const sampler = {
2417
+ sampleElevation,
2418
+ sampleNormal,
2419
+ sampleTerrain,
2420
+ sampleValidity,
2421
+ evaluateElevation,
2422
+ evaluateNormal
2423
+ };
2424
+ params.projection.gpu.augmentSampler?.(sampler, params);
2425
+ return sampler;
2426
+ }
2427
+
2428
+ const RAYCAST_PADDING$1 = 1;
2429
+ function createSphereTileComputeParts(ctx) {
2430
+ const { uniforms, shared } = ctx;
2431
+ const tileSize = tsl.Fn(([nodeIndex]) => {
2432
+ const level = shared.tileLevel(nodeIndex);
2433
+ const divisor = tsl.pow(tsl.float(2), level.toFloat());
2434
+ return uniforms.uRadius.toVar().mul(tsl.float(HALF_PI)).div(divisor);
2435
+ });
2436
+ const tileVertexWorldPosition = tsl.Fn(([nodeIndex, ix, iy]) => {
2437
+ const rootOrigin = uniforms.uRootOrigin.toVar();
2438
+ const faceUV = shared.tileFaceUV(nodeIndex, ix, iy);
2439
+ const basis = cubeFaceBasis(shared.tileFace(nodeIndex));
2440
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
2441
+ return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
2442
+ });
2443
+ return {
2444
+ tileSize: (nodeIndex) => tileSize(nodeIndex),
2445
+ rootUV: (nodeIndex, ix, iy) => shared.tileFaceUV(nodeIndex, ix, iy),
2446
+ tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
2447
+ };
2448
+ }
2449
+ function createCubeSphereProjection(config) {
2450
+ const radius = config.radius;
2451
+ const center = config.center ?? { x: 0, y: 0, z: 0 };
2452
+ const invert = config.invert ?? false;
2453
+ const cubeScratch = [0, 0, 0];
2454
+ const uvScratch = [0, 0];
2455
+ const dirScratch = [0, 0, 0];
2456
+ const posLeft = [0, 0, 0];
2457
+ const posRight = [0, 0, 0];
2458
+ const posUp = [0, 0, 0];
2459
+ const posDown = [0, 0, 0];
2460
+ const neighborPos = (face, u, v, height, out) => {
2461
+ faceUVToCube(face, u, v, cubeScratch);
2462
+ const len = Math.hypot(cubeScratch[0], cubeScratch[1], cubeScratch[2]) || 1;
2463
+ const r = invert ? (radius - height) / len : (radius + height) / len;
2464
+ out[0] = cubeScratch[0] * r;
2465
+ out[1] = cubeScratch[1] * r;
2466
+ out[2] = cubeScratch[2] * r;
2467
+ };
2468
+ const surfaceOps = {
2469
+ positionToKey(px, py, pz, out) {
2470
+ const dx = px - center.x;
2471
+ const dy = py - center.y;
2472
+ const dz = pz - center.z;
2473
+ const len = Math.hypot(dx, dy, dz);
2474
+ if (len === 0) return false;
2475
+ const nx = dx / len;
2476
+ const ny = dy / len;
2477
+ const nz = dz / len;
2478
+ const dirSign = invert ? -1 : 1;
2479
+ dirScratch[0] = nx;
2480
+ dirScratch[1] = ny;
2481
+ dirScratch[2] = nz;
2482
+ const face = directionToFace(dirScratch);
2483
+ directionToFaceUV(face, dirScratch, uvScratch);
2484
+ out.space = face;
2485
+ out.u = uvScratch[0];
2486
+ out.v = uvScratch[1];
2487
+ out.dirX = nx * dirSign;
2488
+ out.dirY = ny * dirSign;
2489
+ out.dirZ = nz * dirSign;
2490
+ return true;
2491
+ },
2492
+ surfacePosition(key, elevation, outVec) {
2493
+ const r = invert ? radius - elevation : radius + elevation;
2494
+ outVec.set(center.x + key.dirX * r, center.y + key.dirY * r, center.z + key.dirZ * r);
2495
+ },
2496
+ surfaceNormal(key, ctx) {
2497
+ const scale = ctx.elevationScale;
2498
+ const duv = 1 / (ctx.innerTileSegments * 2 ** ctx.level);
2499
+ dirScratch[0] = key.dirX;
2500
+ dirScratch[1] = key.dirY;
2501
+ dirScratch[2] = key.dirZ;
2502
+ directionToFaceUV(key.space, dirScratch, uvScratch);
2503
+ const u = uvScratch[0];
2504
+ const v = uvScratch[1];
2505
+ const hLeft = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx - 1, ctx.gy) * scale;
2506
+ const hRight = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx + 1, ctx.gy) * scale;
2507
+ const hUp = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy - 1) * scale;
2508
+ const hDown = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy + 1) * scale;
2509
+ neighborPos(key.space, u - duv, v, hLeft, posLeft);
2510
+ neighborPos(key.space, u + duv, v, hRight, posRight);
2511
+ neighborPos(key.space, u, v - duv, hUp, posUp);
2512
+ neighborPos(key.space, u, v + duv, hDown, posDown);
2513
+ const tux = posRight[0] - posLeft[0];
2514
+ const tuy = posRight[1] - posLeft[1];
2515
+ const tuz = posRight[2] - posLeft[2];
2516
+ const tvx = posDown[0] - posUp[0];
2517
+ const tvy = posDown[1] - posUp[1];
2518
+ const tvz = posDown[2] - posUp[2];
2519
+ let nx = tuy * tvz - tuz * tvy;
2520
+ let ny = tuz * tvx - tux * tvz;
2521
+ let nz = tux * tvy - tuy * tvx;
2522
+ if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
2523
+ nx = -nx;
2524
+ ny = -ny;
2525
+ nz = -nz;
2526
+ }
2527
+ return new three.Vector3(nx, ny, nz).normalize();
2528
+ }
2529
+ };
2530
+ return {
2531
+ kind: "cubeSphere",
2532
+ radius,
2533
+ center,
2534
+ faceOutward: !invert,
2535
+ gpu: {
2536
+ renderVertexPosition(ctx) {
2537
+ return createCurvedRenderVertexPosition(
2538
+ ctx.leafStorage,
2539
+ ctx.uniforms,
2540
+ ctx.terrainFieldStorage,
2541
+ (tile, faceUV, displacement) => {
2542
+ const basis = cubeFaceBasis(tile.face);
2543
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
2544
+ const r = invert ? ctx.uniforms.uRadius.toVar().sub(displacement) : ctx.uniforms.uRadius.toVar().add(displacement);
2545
+ return ctx.uniforms.uRootOrigin.toVar().add(dir.mul(r));
2546
+ }
2547
+ );
2548
+ },
2549
+ createTileComputeParts: createSphereTileComputeParts,
2550
+ createFieldNormal(ctx) {
2551
+ const computeNormal = createDisplacedSurfaceNormalFromElevationField(
2552
+ ctx.elevationFieldNode,
2553
+ ctx.edgeVertexCount,
2554
+ (nodeIndex) => {
2555
+ const basis = cubeFaceBasis(ctx.tile.tileFace(nodeIndex));
2556
+ const r = ctx.uniforms.uRadius.toVar();
2557
+ return {
2558
+ positionAt: (gx, gy, height) => {
2559
+ const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
2560
+ const dir = cubeFaceDirection(basis, uv.x, uv.y);
2561
+ return invert ? dir.mul(r.sub(height)) : dir.mul(r.add(height));
2562
+ },
2563
+ dirAt: (gx, gy) => {
2564
+ const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
2565
+ const dir = cubeFaceDirection(basis, uv.x, uv.y);
2566
+ return invert ? dir.negate() : dir;
2567
+ }
2568
+ };
2569
+ }
2570
+ );
2571
+ return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ix, iy, ctx.uniforms.uElevationScale);
2572
+ },
2573
+ augmentSampler: augmentCubeSphereSampler
2574
+ },
2575
+ cpu: {
2576
+ cameraSurfaceOffset(cam, elevation) {
2577
+ const dx = cam.x - center.x;
2578
+ const dy = cam.y - center.y;
2579
+ const dz = cam.z - center.z;
2580
+ const len = Math.hypot(dx, dy, dz);
2581
+ if (len > 1e-12) {
2582
+ const sign = invert ? 1 : -1;
2583
+ const inv = sign * elevation / len;
2584
+ cam.x += dx * inv;
2585
+ cam.y += dy * inv;
2586
+ cam.z += dz * inv;
2587
+ }
2588
+ },
2589
+ createSurfaceOps() {
2590
+ return surfaceOps;
2591
+ },
2592
+ createRuntimeQueries(cache) {
2593
+ const query = createTerrainQuery(cache);
2594
+ const surfaceQuery = createTerrainSurfaceQuery(cache);
2595
+ const sphereQuery = createCubeSphereQuery(surfaceQuery, center);
2596
+ return { query, surfaceQuery, sphereQuery };
2597
+ },
2598
+ raycast(ctx) {
2599
+ const range = ctx.terrainQuery?.getGlobalElevationRange();
2600
+ const dispMax = range ? Math.max(0, range.max - center.y) : radius * 0.1;
2601
+ const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING$1;
2602
+ const params = {
2603
+ centerX: center.x,
2604
+ centerY: center.y,
2605
+ centerZ: center.z,
2606
+ radius,
2607
+ maxRadius: radius + outerPadding,
2608
+ invert
2609
+ };
2610
+ if (ctx.sphereQuery) {
2611
+ const precise = cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2612
+ if (precise) return precise;
2613
+ }
2614
+ return cubeSphereRaycastBoundsOnly(ctx.ray, params, ctx.options);
2615
+ }
2616
+ }
2617
+ };
2618
+ }
2619
+ function createCubeSphereQuery(surfaceQuery, center) {
2620
+ const scratch = new three.Vector3();
2621
+ const ll = [0, 0, 0];
2622
+ const positionFromDirection = (dx, dy, dz) => scratch.set(center.x + dx, center.y + dy, center.z + dz);
2623
+ return {
2624
+ get generation() {
2625
+ return surfaceQuery.generation;
2626
+ },
2627
+ getElevationByPosition: (position) => surfaceQuery.getElevationByPosition(position),
2628
+ getNormalByPosition: (position) => surfaceQuery.getNormalByPosition(position),
2629
+ sampleTerrainByPosition: (position) => surfaceQuery.sampleTerrainByPosition(position),
2630
+ getTileByPosition: (position) => surfaceQuery.getTileByPosition(position),
2631
+ getTileBoundsByPosition: (position) => surfaceQuery.getTileBoundsByPosition(position),
2632
+ sampleTerrainBatchByPosition: (positions) => surfaceQuery.sampleTerrainBatchByPosition(positions),
2633
+ getElevationByDirection: (direction) => surfaceQuery.getElevationByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
2634
+ getNormalByDirection: (direction) => surfaceQuery.getNormalByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
2635
+ sampleTerrainByDirection: (direction) => surfaceQuery.sampleTerrainByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
2636
+ getTileByDirection: (direction) => surfaceQuery.getTileByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
2637
+ getTileBoundsByDirection: (direction) => surfaceQuery.getTileBoundsByPosition(
2638
+ positionFromDirection(direction.x, direction.y, direction.z)
2639
+ ),
2640
+ getElevationByLatLong: (lat, lon) => {
2641
+ latLongToDirection(lat, lon, ll);
2642
+ return surfaceQuery.getElevationByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
2643
+ },
2644
+ getNormalByLatLong: (lat, lon) => {
2645
+ latLongToDirection(lat, lon, ll);
2646
+ return surfaceQuery.getNormalByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
2647
+ },
2648
+ sampleTerrainByLatLong: (lat, lon) => {
2649
+ latLongToDirection(lat, lon, ll);
2650
+ return surfaceQuery.sampleTerrainByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
2651
+ },
2652
+ getTileByLatLong: (lat, lon) => {
2653
+ latLongToDirection(lat, lon, ll);
2654
+ return surfaceQuery.getTileByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
2655
+ },
2656
+ getTileBoundsByLatLong: (lat, lon) => {
2657
+ latLongToDirection(lat, lon, ll);
2658
+ return surfaceQuery.getTileBoundsByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
2659
+ },
2660
+ sampleTerrainBatchByDirection: (directions) => {
2661
+ const count = Math.floor(directions.length / 3);
2662
+ const positions = new Float32Array(count * 3);
2663
+ for (let i = 0; i < count; i += 1) {
2664
+ positions[i * 3] = center.x + (directions[i * 3] ?? 0);
2665
+ positions[i * 3 + 1] = center.y + (directions[i * 3 + 1] ?? 0);
2666
+ positions[i * 3 + 2] = center.z + (directions[i * 3 + 2] ?? 0);
2667
+ }
2668
+ return surfaceQuery.sampleTerrainBatchByPosition(positions);
2669
+ }
2670
+ };
2671
+ }
2672
+
2673
+ function createCubeSphereTopology(cfg) {
2674
+ const radius = cfg.radius;
2675
+ const center = cfg.center ?? { x: 0, y: 0, z: 0 };
2676
+ const cube = [0, 0, 0];
2677
+ const uv = [0, 0];
2678
+ const px = new Float64Array(8);
2679
+ const py = new Float64Array(8);
2680
+ const pz = new Float64Array(8);
2681
+ function crossFaceNeighbor(face, level, nx, ny, out) {
2682
+ const n = 1 << level;
2683
+ const u = (nx + 0.5) / n;
2684
+ const v = (ny + 0.5) / n;
2685
+ faceUVToCube(face, u, v, cube);
2686
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
2687
+ const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
2688
+ const nbrFace = directionToFace(dir);
2689
+ directionToFaceUV(nbrFace, dir, uv);
2690
+ let bx = Math.floor(uv[0] * n);
2691
+ let by = Math.floor(uv[1] * n);
2692
+ if (bx < 0) bx = 0;
2693
+ else if (bx > n - 1) bx = n - 1;
2694
+ if (by < 0) by = 0;
2695
+ else if (by > n - 1) by = n - 1;
2696
+ out.space = nbrFace;
2697
+ out.level = level;
2698
+ out.x = bx;
2699
+ out.y = by;
2700
+ }
2701
+ return {
2702
+ spaceCount: 6,
2703
+ maxRootCount: 6,
2704
+ projection: createCubeSphereProjection({ radius, center, invert: cfg.invert }),
2705
+ radius,
2706
+ center,
2707
+ neighborSameLevel(tile, dir, out) {
2708
+ const level = tile.level;
2709
+ const n = 1 << level;
2710
+ let nx = tile.x;
2711
+ let ny = tile.y;
2712
+ switch (dir) {
2713
+ case 0:
2714
+ nx -= 1;
2715
+ break;
2716
+ case 1:
2717
+ nx += 1;
2718
+ break;
2719
+ case 2:
2720
+ ny -= 1;
2721
+ break;
2722
+ case 3:
2723
+ ny += 1;
2724
+ break;
2725
+ }
2726
+ if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
2727
+ out.space = tile.space;
2728
+ out.level = level;
2729
+ out.x = nx;
2730
+ out.y = ny;
2731
+ return true;
2732
+ }
2733
+ crossFaceNeighbor(tile.space, level, nx, ny, out);
2734
+ return true;
2735
+ },
2736
+ tileBounds(tile, cameraOrigin, out, elevationRange) {
2737
+ const level = tile.level;
2738
+ const n = 1 << level;
2739
+ const u0 = tile.x / n;
2740
+ const u1 = (tile.x + 1) / n;
2741
+ const v0 = tile.y / n;
2742
+ const v1 = (tile.y + 1) / n;
2743
+ const cornersU = [u0, u1, u0, u1];
2744
+ const cornersV = [v0, v0, v1, v1];
2745
+ const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2746
+ let pointCount = 0;
2747
+ let sumX = 0;
2748
+ let sumY = 0;
2749
+ let sumZ = 0;
2750
+ for (let i = 0; i < 4; i++) {
2751
+ faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
2752
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
2753
+ const dirX = cube[0] / len;
2754
+ const dirY = cube[1] / len;
2755
+ const dirZ = cube[2] / len;
2756
+ for (let di = 0; di < disps.length; di++) {
2757
+ const shellRadius = radius + disps[di];
2758
+ const sx = center.x + dirX * shellRadius;
2759
+ const sy = center.y + dirY * shellRadius;
2760
+ const sz = center.z + dirZ * shellRadius;
2761
+ px[pointCount] = sx;
2762
+ py[pointCount] = sy;
2763
+ pz[pointCount] = sz;
2764
+ sumX += sx;
2765
+ sumY += sy;
2766
+ sumZ += sz;
2767
+ pointCount += 1;
2768
+ }
2769
+ }
2770
+ const cX = sumX / pointCount;
2771
+ const cY = sumY / pointCount;
2772
+ const cZ = sumZ / pointCount;
2773
+ let maxDistSq = 0;
2774
+ for (let i = 0; i < pointCount; i++) {
2775
+ const dx = px[i] - cX;
2776
+ const dy = py[i] - cY;
2777
+ const dz = pz[i] - cZ;
2778
+ const dSq = dx * dx + dy * dy + dz * dz;
2779
+ if (dSq > maxDistSq) maxDistSq = dSq;
2780
+ }
2781
+ out.cx = cX - cameraOrigin.x;
2782
+ out.cy = cY - cameraOrigin.y;
2783
+ out.cz = cZ - cameraOrigin.z;
2784
+ out.r = Math.sqrt(maxDistSq);
2785
+ },
2786
+ rootTiles(_cameraOrigin, out) {
2787
+ for (let s = 0; s < 6; s++) {
2788
+ const root = out[s];
2789
+ root.space = s;
2790
+ root.level = 0;
2791
+ root.x = 0;
2792
+ root.y = 0;
2793
+ }
2794
+ return 6;
2795
+ }
2796
+ };
2797
+ }
2798
+
2799
+ const TWO_PI$1 = Math.PI * 2;
2800
+ function torusPosition(geometry, u, v, displacement) {
2801
+ const theta = tsl.float(u).mul(TWO_PI$1);
2802
+ const phi = tsl.float(v).mul(TWO_PI$1);
2803
+ const sinT = tsl.sin(theta);
2804
+ const cosT = tsl.cos(theta);
2805
+ const sinP = tsl.sin(phi);
2806
+ const cosP = tsl.cos(phi);
2807
+ const disp = tsl.select(tsl.bool(geometry.invert), displacement.negate(), displacement);
2808
+ const tube = disp.add(tsl.float(geometry.minorRadius));
2809
+ const ring = tube.mul(cosP).add(tsl.float(geometry.majorRadius));
2810
+ return tsl.vec3(
2811
+ ring.mul(sinT).add(tsl.float(geometry.center.x)),
2812
+ tube.mul(sinP).add(tsl.float(geometry.center.y)),
2813
+ ring.mul(cosT).add(tsl.float(geometry.center.z))
2814
+ );
2815
+ }
2816
+ function torusOutwardNormal(u, v, invert) {
2817
+ const theta = tsl.float(u).mul(TWO_PI$1);
2818
+ const phi = tsl.float(v).mul(TWO_PI$1);
2819
+ const sinT = tsl.sin(theta);
2820
+ const cosT = tsl.cos(theta);
2821
+ const sinP = tsl.sin(phi);
2822
+ const cosP = tsl.cos(phi);
2823
+ const normal = tsl.vec3(cosP.mul(sinT), sinP, cosP.mul(cosT)).normalize();
2824
+ return tsl.select(tsl.bool(invert), normal.negate(), normal);
2825
+ }
2826
+
2827
+ const TWO_PI = Math.PI * 2;
2828
+ const RAYCAST_PADDING = 1;
2829
+ const ZERO_CENTER = { x: 0, y: 0, z: 0 };
2830
+ function createTorusTileComputeParts(ctx, geometry) {
2831
+ const { shared } = ctx;
2832
+ const tileSize = tsl.Fn(([nodeIndex]) => {
2833
+ const level = shared.tileLevel(nodeIndex);
2834
+ const levelScale = tsl.pow(tsl.float(2), level.toFloat());
2835
+ return tsl.float(TWO_PI * geometry.majorRadius).div(tsl.float(geometry.baseU).mul(levelScale));
2836
+ });
2837
+ const tileVertexWorldPosition = tsl.Fn(([nodeIndex, ix, iy]) => {
2838
+ const faceUV = shared.tileFaceUV(nodeIndex, ix, iy);
2839
+ return torusPosition(geometry, faceUV.x, faceUV.y, tsl.float(0));
2840
+ });
2841
+ return {
2842
+ tileSize: (nodeIndex) => tileSize(nodeIndex),
2843
+ rootUV: (nodeIndex, ix, iy) => shared.tileFaceUV(nodeIndex, ix, iy),
2844
+ tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
2845
+ };
2846
+ }
2847
+ function createTorusProjection(config) {
2848
+ const majorRadius = config.majorRadius;
2849
+ const minorRadius = config.minorRadius;
2850
+ const center = config.center ?? { x: 0, y: 0, z: 0 };
2851
+ const invert = config.invert ?? false;
2852
+ const baseU = config.baseU ?? 1;
2853
+ const baseV = config.baseV ?? 1;
2854
+ const geometry = { majorRadius, minorRadius, center, invert, baseU};
2855
+ const params = { u: 0, v: 0, tubeDistance: 0 };
2856
+ const normalScratch = [0, 0, 0];
2857
+ const posLeft = [0, 0, 0];
2858
+ const posRight = [0, 0, 0];
2859
+ const posUp = [0, 0, 0];
2860
+ const posDown = [0, 0, 0];
2861
+ const surfaceOps = {
2862
+ positionToKey(px, py, pz, out) {
2863
+ positionToTorusParams(px, py, pz, majorRadius, center, params);
2864
+ torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
2865
+ out.space = 0;
2866
+ out.u = params.u;
2867
+ out.v = params.v;
2868
+ out.dirX = normalScratch[0];
2869
+ out.dirY = normalScratch[1];
2870
+ out.dirZ = normalScratch[2];
2871
+ return true;
2872
+ },
2873
+ surfacePosition(key, elevation, outVec) {
2874
+ torusUVToPoint(
2875
+ key.u,
2876
+ key.v,
2877
+ majorRadius,
2878
+ minorRadius,
2879
+ elevation,
2880
+ center,
2881
+ normalScratch,
2882
+ invert
2883
+ );
2884
+ outVec.set(normalScratch[0], normalScratch[1], normalScratch[2]);
2885
+ },
2886
+ surfaceNormal(key, ctx) {
2887
+ const scale = ctx.elevationScale;
2888
+ const levelScale = 2 ** ctx.level;
2889
+ const duvU = 1 / (ctx.innerTileSegments * baseU * levelScale);
2890
+ const duvV = 1 / (ctx.innerTileSegments * baseV * levelScale);
2891
+ const u = key.u;
2892
+ const v = key.v;
2893
+ const hLeft = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx - 1, ctx.gy) * scale;
2894
+ const hRight = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx + 1, ctx.gy) * scale;
2895
+ const hUp = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy - 1) * scale;
2896
+ const hDown = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy + 1) * scale;
2897
+ torusUVToPoint(u - duvU, v, majorRadius, minorRadius, hLeft, ZERO_CENTER, posLeft, invert);
2898
+ torusUVToPoint(u + duvU, v, majorRadius, minorRadius, hRight, ZERO_CENTER, posRight, invert);
2899
+ torusUVToPoint(u, v - duvV, majorRadius, minorRadius, hUp, ZERO_CENTER, posUp, invert);
2900
+ torusUVToPoint(u, v + duvV, majorRadius, minorRadius, hDown, ZERO_CENTER, posDown, invert);
2901
+ const tux = posRight[0] - posLeft[0];
2902
+ const tuy = posRight[1] - posLeft[1];
2903
+ const tuz = posRight[2] - posLeft[2];
2904
+ const tvx = posDown[0] - posUp[0];
2905
+ const tvy = posDown[1] - posUp[1];
2906
+ const tvz = posDown[2] - posUp[2];
2907
+ let nx = tuy * tvz - tuz * tvy;
2908
+ let ny = tuz * tvx - tux * tvz;
2909
+ let nz = tux * tvy - tuy * tvx;
2910
+ if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
2911
+ nx = -nx;
2912
+ ny = -ny;
2913
+ nz = -nz;
2914
+ }
2915
+ return new three.Vector3(nx, ny, nz).normalize();
2916
+ }
2917
+ };
1367
2918
  return {
1368
- spaceCount: 6,
1369
- maxRootCount: 6,
1370
- neighborSameLevel(_tile, _dir, _out) {
1371
- return false;
2919
+ kind: "torus",
2920
+ radius: majorRadius + minorRadius,
2921
+ center,
2922
+ faceOutward: !invert,
2923
+ baseResolution: { u: baseU, v: baseV },
2924
+ gpu: {
2925
+ renderVertexPosition(ctx) {
2926
+ return createCurvedRenderVertexPosition(
2927
+ ctx.leafStorage,
2928
+ ctx.uniforms,
2929
+ ctx.terrainFieldStorage,
2930
+ (_tile, faceUV, displacement) => torusPosition(geometry, faceUV.x, faceUV.y, displacement),
2931
+ baseU,
2932
+ baseV
2933
+ );
2934
+ },
2935
+ createTileComputeParts: (ctx) => createTorusTileComputeParts(ctx, geometry),
2936
+ createFieldNormal(ctx) {
2937
+ const computeNormal = createDisplacedSurfaceNormalFromElevationField(
2938
+ ctx.elevationFieldNode,
2939
+ ctx.edgeVertexCount,
2940
+ (nodeIndex) => ({
2941
+ positionAt: (gx, gy, height) => {
2942
+ const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
2943
+ return torusPosition(geometry, uv.x, uv.y, height);
2944
+ },
2945
+ dirAt: (gx, gy) => {
2946
+ const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
2947
+ return torusOutwardNormal(uv.x, uv.y, invert);
2948
+ }
2949
+ })
2950
+ );
2951
+ return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ix, iy, ctx.uniforms.uElevationScale);
2952
+ }
2953
+ },
2954
+ cpu: {
2955
+ cameraSurfaceOffset(cam, elevation) {
2956
+ positionToTorusParams(cam.x, cam.y, cam.z, majorRadius, center, params);
2957
+ torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
2958
+ cam.x -= normalScratch[0] * elevation;
2959
+ cam.y -= normalScratch[1] * elevation;
2960
+ cam.z -= normalScratch[2] * elevation;
2961
+ },
2962
+ createSurfaceOps() {
2963
+ return surfaceOps;
2964
+ },
2965
+ createRuntimeQueries(cache) {
2966
+ const query = createTerrainQuery(cache);
2967
+ const surfaceQuery = createTerrainSurfaceQuery(cache);
2968
+ return { query, surfaceQuery, sphereQuery: null };
2969
+ },
2970
+ raycast(ctx) {
2971
+ const range = ctx.terrainQuery?.getGlobalElevationRange();
2972
+ const dispMax = range ? Math.max(0, range.max - ctx.config.originY) : minorRadius * 0.5;
2973
+ const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING;
2974
+ const raycastParams = {
2975
+ centerX: center.x,
2976
+ centerY: center.y,
2977
+ centerZ: center.z,
2978
+ majorRadius,
2979
+ minorRadius,
2980
+ outerRadius: majorRadius + minorRadius + outerPadding,
2981
+ invert
2982
+ };
2983
+ if (ctx.surfaceQuery) {
2984
+ const precise = torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2985
+ if (precise) return precise;
2986
+ }
2987
+ return torusRaycastBoundsOnly(ctx.ray, raycastParams, ctx.options);
2988
+ }
2989
+ }
2990
+ };
2991
+ }
2992
+
2993
+ function createTorusTopology(cfg) {
2994
+ const majorRadius = cfg.majorRadius;
2995
+ const minorRadius = cfg.minorRadius;
2996
+ const center = cfg.center ?? { x: 0, y: 0, z: 0 };
2997
+ const invert = cfg.invert ?? false;
2998
+ const baseU = Math.max(1, Math.round(majorRadius / minorRadius));
2999
+ const baseV = 1;
3000
+ const corner = [0, 0, 0];
3001
+ const px = new Float64Array(18);
3002
+ const py = new Float64Array(18);
3003
+ const pz = new Float64Array(18);
3004
+ const wrap = (value, n) => (value % n + n) % n;
3005
+ const levelResolution = (level) => {
3006
+ const levelScale = 2 ** level;
3007
+ return { nU: baseU * levelScale, nV: baseV * levelScale };
3008
+ };
3009
+ return {
3010
+ spaceCount: 1,
3011
+ maxRootCount: baseU * baseV,
3012
+ projection: createTorusProjection({
3013
+ majorRadius,
3014
+ minorRadius,
3015
+ center,
3016
+ invert,
3017
+ baseU,
3018
+ baseV
3019
+ }),
3020
+ radius: majorRadius + minorRadius,
3021
+ center,
3022
+ neighborSameLevel(tile, dir, out) {
3023
+ const { nU, nV } = levelResolution(tile.level);
3024
+ let nx = tile.x;
3025
+ let ny = tile.y;
3026
+ switch (dir) {
3027
+ case Dir.LEFT:
3028
+ nx -= 1;
3029
+ break;
3030
+ case Dir.RIGHT:
3031
+ nx += 1;
3032
+ break;
3033
+ case Dir.TOP:
3034
+ ny -= 1;
3035
+ break;
3036
+ case Dir.BOTTOM:
3037
+ ny += 1;
3038
+ break;
3039
+ }
3040
+ out.space = 0;
3041
+ out.level = tile.level;
3042
+ out.x = wrap(nx, nU);
3043
+ out.y = wrap(ny, nV);
3044
+ return true;
1372
3045
  },
1373
- tileBounds(_tile, _cameraOrigin, out) {
1374
- out.cx = 0;
1375
- out.cy = 0;
1376
- out.cz = 0;
1377
- out.r = Number.MAX_VALUE;
3046
+ tileBounds(tile, cameraOrigin, out, elevationRange) {
3047
+ const { nU, nV } = levelResolution(tile.level);
3048
+ const u0 = tile.x / nU;
3049
+ const v0 = tile.y / nV;
3050
+ const stepU = 1 / nU;
3051
+ const stepV = 1 / nV;
3052
+ const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
3053
+ let pointCount = 0;
3054
+ let sumX = 0;
3055
+ let sumY = 0;
3056
+ let sumZ = 0;
3057
+ for (let sj = 0; sj <= 2; sj++) {
3058
+ for (let si = 0; si <= 2; si++) {
3059
+ const u = u0 + si * stepU / 2;
3060
+ const v = v0 + sj * stepV / 2;
3061
+ for (let di = 0; di < disps.length; di++) {
3062
+ torusUVToPoint(u, v, majorRadius, minorRadius, disps[di], center, corner, invert);
3063
+ px[pointCount] = corner[0];
3064
+ py[pointCount] = corner[1];
3065
+ pz[pointCount] = corner[2];
3066
+ sumX += corner[0];
3067
+ sumY += corner[1];
3068
+ sumZ += corner[2];
3069
+ pointCount += 1;
3070
+ }
3071
+ }
3072
+ }
3073
+ const cX = sumX / pointCount;
3074
+ const cY = sumY / pointCount;
3075
+ const cZ = sumZ / pointCount;
3076
+ let maxDistSq = 0;
3077
+ for (let i = 0; i < pointCount; i++) {
3078
+ const dx = px[i] - cX;
3079
+ const dy = py[i] - cY;
3080
+ const dz = pz[i] - cZ;
3081
+ const dSq = dx * dx + dy * dy + dz * dz;
3082
+ if (dSq > maxDistSq) maxDistSq = dSq;
3083
+ }
3084
+ out.cx = cX - cameraOrigin.x;
3085
+ out.cy = cY - cameraOrigin.y;
3086
+ out.cz = cZ - cameraOrigin.z;
3087
+ out.r = Math.sqrt(maxDistSq);
1378
3088
  },
1379
3089
  rootTiles(_cameraOrigin, out) {
1380
- for (let s = 0; s < 6; s++) {
1381
- const root = out[s];
1382
- root.space = s;
1383
- root.level = 0;
1384
- root.x = 0;
1385
- root.y = 0;
3090
+ let count = 0;
3091
+ for (let y = 0; y < baseV; y++) {
3092
+ for (let x = 0; x < baseU; x++) {
3093
+ const root = out[count];
3094
+ root.space = 0;
3095
+ root.level = 0;
3096
+ root.x = x;
3097
+ root.y = y;
3098
+ count += 1;
3099
+ }
1386
3100
  }
1387
- return 6;
3101
+ return count;
1388
3102
  }
1389
3103
  };
1390
3104
  }
1391
3105
 
3106
+ function nextPow2(n) {
3107
+ let x = 1;
3108
+ while (x < n) x <<= 1;
3109
+ return x;
3110
+ }
3111
+ function mix32(x) {
3112
+ x >>>= 0;
3113
+ x ^= x >>> 16;
3114
+ x = Math.imul(x, 2146121005) >>> 0;
3115
+ x ^= x >>> 15;
3116
+ x = Math.imul(x, 2221713035) >>> 0;
3117
+ x ^= x >>> 16;
3118
+ return x >>> 0;
3119
+ }
3120
+ function hashKey(space, level, x, y) {
3121
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
3122
+ return mix32(h);
3123
+ }
3124
+ function createTileElevationPyramid(maxNodes, maxLevel) {
3125
+ const size = nextPow2(Math.max(2, maxNodes * (maxLevel + 1) * 2));
3126
+ return {
3127
+ size,
3128
+ mask: size - 1,
3129
+ stampGen: 1,
3130
+ stamp: new Uint16Array(size),
3131
+ keysSpace: new Uint8Array(size),
3132
+ keysLevel: new Uint8Array(size),
3133
+ keysX: new Uint32Array(size),
3134
+ keysY: new Uint32Array(size),
3135
+ mins: new Float32Array(size),
3136
+ maxs: new Float32Array(size)
3137
+ };
3138
+ }
3139
+ function beginPyramidGeneration(pyramid) {
3140
+ pyramid.stampGen = pyramid.stampGen + 1 & 65535;
3141
+ if (pyramid.stampGen === 0) {
3142
+ pyramid.stamp.fill(0);
3143
+ pyramid.stampGen = 1;
3144
+ }
3145
+ }
3146
+ function mergeRange(pyramid, space, level, x, y, min, max) {
3147
+ const s = space & 255;
3148
+ const l = level & 255;
3149
+ const xx = x >>> 0;
3150
+ const yy = y >>> 0;
3151
+ let slot = hashKey(s, l, xx, yy) & pyramid.mask;
3152
+ for (let probes = 0; probes < pyramid.size; probes++) {
3153
+ if (pyramid.stamp[slot] !== pyramid.stampGen) {
3154
+ pyramid.stamp[slot] = pyramid.stampGen;
3155
+ pyramid.keysSpace[slot] = s;
3156
+ pyramid.keysLevel[slot] = l;
3157
+ pyramid.keysX[slot] = xx;
3158
+ pyramid.keysY[slot] = yy;
3159
+ pyramid.mins[slot] = min;
3160
+ pyramid.maxs[slot] = max;
3161
+ return;
3162
+ }
3163
+ if (pyramid.keysSpace[slot] === s && pyramid.keysLevel[slot] === l && pyramid.keysX[slot] === xx && pyramid.keysY[slot] === yy) {
3164
+ if (min < pyramid.mins[slot]) pyramid.mins[slot] = min;
3165
+ if (max > pyramid.maxs[slot]) pyramid.maxs[slot] = max;
3166
+ return;
3167
+ }
3168
+ slot = slot + 1 & pyramid.mask;
3169
+ }
3170
+ throw new Error("TileElevationPyramid is full (no empty slot found).");
3171
+ }
3172
+ function buildTileElevationPyramid(pyramid, index, tileBounds, leafCount) {
3173
+ beginPyramidGeneration(pyramid);
3174
+ const stampGen = index.stampGen;
3175
+ for (let slot = 0; slot < index.size; slot++) {
3176
+ if (index.stamp[slot] !== stampGen) continue;
3177
+ const leafIndex = index.values[slot];
3178
+ if (leafIndex >= leafCount) continue;
3179
+ const space = index.keysSpace[slot];
3180
+ const level = index.keysLevel[slot];
3181
+ const x = index.keysX[slot];
3182
+ const y = index.keysY[slot];
3183
+ const rawMin = tileBounds[leafIndex * 2];
3184
+ const rawMax = tileBounds[leafIndex * 2 + 1];
3185
+ for (let ancestorLevel = level; ancestorLevel >= 0; ancestorLevel--) {
3186
+ const shift = level - ancestorLevel;
3187
+ mergeRange(
3188
+ pyramid,
3189
+ space,
3190
+ ancestorLevel,
3191
+ x >>> shift,
3192
+ y >>> shift,
3193
+ rawMin,
3194
+ rawMax
3195
+ );
3196
+ }
3197
+ }
3198
+ }
3199
+ function lookupTileElevationRange(pyramid, space, level, x, y, out) {
3200
+ const s = space & 255;
3201
+ const l = level & 255;
3202
+ const xx = x >>> 0;
3203
+ const yy = y >>> 0;
3204
+ let slot = hashKey(s, l, xx, yy) & pyramid.mask;
3205
+ for (let probes = 0; probes < pyramid.size; probes++) {
3206
+ if (pyramid.stamp[slot] !== pyramid.stampGen) return false;
3207
+ if (pyramid.keysSpace[slot] === s && pyramid.keysLevel[slot] === l && pyramid.keysX[slot] === xx && pyramid.keysY[slot] === yy) {
3208
+ out.min = pyramid.mins[slot];
3209
+ out.max = pyramid.maxs[slot];
3210
+ return true;
3211
+ }
3212
+ slot = slot + 1 & pyramid.mask;
3213
+ }
3214
+ return false;
3215
+ }
3216
+
3217
+ const MISSED_LOOKUP = Object.freeze({
3218
+ found: false,
3219
+ leafIndex: -1,
3220
+ space: -1,
3221
+ level: -1,
3222
+ tileX: -1,
3223
+ tileY: -1,
3224
+ tileSize: 0,
3225
+ localU: 0,
3226
+ localV: 0
3227
+ });
3228
+ function lookupTile(index, config, worldX, worldZ) {
3229
+ const halfRoot = config.rootSize * 0.5;
3230
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
3231
+ const scale = 2 ** level;
3232
+ const tileSize = config.rootSize / scale;
3233
+ const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
3234
+ const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
3235
+ const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
3236
+ if (leafIndex !== U32_EMPTY) {
3237
+ const tileMinX = config.originX + tileX * tileSize - halfRoot;
3238
+ const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
3239
+ return {
3240
+ found: true,
3241
+ leafIndex,
3242
+ space: 0,
3243
+ level,
3244
+ tileX,
3245
+ tileY,
3246
+ tileSize,
3247
+ localU: (worldX - tileMinX) / tileSize,
3248
+ localV: (worldZ - tileMinZ) / tileSize
3249
+ };
3250
+ }
3251
+ }
3252
+ return MISSED_LOOKUP;
3253
+ }
3254
+ function clamp01(value) {
3255
+ return value < 0 ? 0 : value > 1 ? 1 : value;
3256
+ }
3257
+ function lookupTileByFaceUV(index, config, face, u, v) {
3258
+ const baseU = config.baseU ?? 1;
3259
+ const baseV = config.baseV ?? 1;
3260
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
3261
+ const levelScale = 2 ** level;
3262
+ const nU = baseU * levelScale;
3263
+ const nV = baseV * levelScale;
3264
+ let tileX = Math.floor(u * nU);
3265
+ let tileY = Math.floor(v * nV);
3266
+ if (tileX < 0) tileX = 0;
3267
+ else if (tileX > nU - 1) tileX = nU - 1;
3268
+ if (tileY < 0) tileY = 0;
3269
+ else if (tileY > nV - 1) tileY = nV - 1;
3270
+ const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
3271
+ if (leafIndex !== U32_EMPTY) {
3272
+ const tileSize = sphereTileArcLength(config.radius, nU);
3273
+ return {
3274
+ found: true,
3275
+ leafIndex,
3276
+ space: face,
3277
+ level,
3278
+ tileX,
3279
+ tileY,
3280
+ tileSize,
3281
+ localU: clamp01(u * nU - tileX),
3282
+ localV: clamp01(v * nV - tileY)
3283
+ };
3284
+ }
3285
+ }
3286
+ return MISSED_LOOKUP;
3287
+ }
3288
+
3289
+ function createReadbackSlot() {
3290
+ return { buffer: null, size: 0 };
3291
+ }
3292
+ function getBackend(renderer) {
3293
+ return renderer.backend;
3294
+ }
3295
+ function canDeviceReadback(renderer) {
3296
+ const backend = getBackend(renderer);
3297
+ return Boolean(backend?.device) && typeof backend?.get === "function";
3298
+ }
3299
+ async function readStorageBufferInto(renderer, attribute, slot, target, elementCount, label) {
3300
+ const backend = getBackend(renderer);
3301
+ const device = backend?.device;
3302
+ const source = backend?.get?.(attribute)?.buffer;
3303
+ if (!device || !source) return false;
3304
+ const requestedBytes = elementCount * Float32Array.BYTES_PER_ELEMENT;
3305
+ const copyBytes = Math.min(requestedBytes, source.size);
3306
+ if (copyBytes <= 0) return true;
3307
+ if (!slot.buffer || slot.size !== source.size) {
3308
+ slot.buffer?.destroy();
3309
+ slot.buffer = device.createBuffer({
3310
+ label,
3311
+ size: source.size,
3312
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
3313
+ });
3314
+ slot.size = source.size;
3315
+ }
3316
+ const staging = slot.buffer;
3317
+ const encoder = device.createCommandEncoder({ label });
3318
+ encoder.copyBufferToBuffer(source, 0, staging, 0, copyBytes);
3319
+ device.queue.submit([encoder.finish()]);
3320
+ await staging.mapAsync(GPUMapMode.READ, 0, copyBytes);
3321
+ const mapped = new Float32Array(staging.getMappedRange(0, copyBytes));
3322
+ target.set(mapped);
3323
+ staging.unmap();
3324
+ return true;
3325
+ }
3326
+ function disposeReadbackSlot(slot) {
3327
+ slot.buffer?.destroy();
3328
+ slot.buffer = null;
3329
+ slot.size = 0;
3330
+ }
3331
+
3332
+ function createTerrainSnapshotState(maxNodes, maxLevel, totalElements) {
3333
+ return {
3334
+ frontElevation: new Float32Array(totalElements),
3335
+ backElevation: new Float32Array(totalElements),
3336
+ frontIndex: createSpatialIndex(maxNodes),
3337
+ backIndex: createSpatialIndex(maxNodes),
3338
+ frontTileBounds: new Float32Array(maxNodes * 2),
3339
+ backTileBounds: new Float32Array(maxNodes * 2),
3340
+ frontLeafCount: 0,
3341
+ globalRange: null,
3342
+ hasSnapshot: false,
3343
+ readbackPending: false,
3344
+ generation: 0,
3345
+ lastScheduledStampGen: -1,
3346
+ elevationReadback: createReadbackSlot(),
3347
+ boundsReadback: createReadbackSlot(),
3348
+ elevationPyramid: createTileElevationPyramid(maxNodes, maxLevel)
3349
+ };
3350
+ }
1392
3351
  function cloneSpatialIndex(target, source) {
1393
3352
  if (target.size !== source.size) {
1394
3353
  throw new Error(
@@ -1404,218 +3363,308 @@ function cloneSpatialIndex(target, source) {
1404
3363
  target.keysY.set(source.keysY);
1405
3364
  target.values.set(source.values);
1406
3365
  }
1407
- function tileLocalToFieldUV(localCoord, innerSegments) {
1408
- const edge = innerSegments + 3;
1409
- return (localCoord * innerSegments + 1.5) / edge;
3366
+ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
3367
+ if (state.readbackPending) return;
3368
+ const withReadback = renderer;
3369
+ const useDeviceReadback = canDeviceReadback(renderer);
3370
+ if (!useDeviceReadback && !withReadback.getArrayBufferAsync) return;
3371
+ if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
3372
+ cloneSpatialIndex(state.backIndex, spatialIndex);
3373
+ state.lastScheduledStampGen = spatialIndex.stampGen;
3374
+ const { activeLeafCount, totalElements, verticesPerNode, elevationScale, originY } = captured;
3375
+ state.readbackPending = true;
3376
+ const applySnapshot = (boundsFilled) => {
3377
+ let boundsValid = activeLeafCount === 0;
3378
+ if (boundsFilled) {
3379
+ for (let i = 0; i < activeLeafCount; i += 1) {
3380
+ if ((state.backTileBounds[i * 2 + 1] ?? 0) !== 0) {
3381
+ boundsValid = true;
3382
+ break;
3383
+ }
3384
+ }
3385
+ }
3386
+ const oldFrontElevation = state.frontElevation;
3387
+ const oldFrontIndex = state.frontIndex;
3388
+ state.frontElevation = state.backElevation;
3389
+ state.frontIndex = state.backIndex;
3390
+ state.frontLeafCount = activeLeafCount;
3391
+ state.backElevation = oldFrontElevation;
3392
+ state.backIndex = oldFrontIndex;
3393
+ if (boundsFilled && boundsValid) {
3394
+ const oldFrontBounds = state.frontTileBounds;
3395
+ state.frontTileBounds = state.backTileBounds;
3396
+ state.backTileBounds = oldFrontBounds;
3397
+ }
3398
+ if (boundsFilled && boundsValid && activeLeafCount > 0) {
3399
+ let gMin = Infinity;
3400
+ let gMax = -Infinity;
3401
+ for (let i = 0; i < activeLeafCount; i++) {
3402
+ const rawMin = state.frontTileBounds[i * 2];
3403
+ const rawMax = state.frontTileBounds[i * 2 + 1];
3404
+ const a = originY + rawMin * elevationScale;
3405
+ const b = originY + rawMax * elevationScale;
3406
+ gMin = Math.min(gMin, a, b);
3407
+ gMax = Math.max(gMax, a, b);
3408
+ }
3409
+ state.globalRange = { min: gMin, max: gMax };
3410
+ buildTileElevationPyramid(
3411
+ state.elevationPyramid,
3412
+ state.frontIndex,
3413
+ state.frontTileBounds,
3414
+ activeLeafCount
3415
+ );
3416
+ }
3417
+ state.hasSnapshot = true;
3418
+ state.generation += 1;
3419
+ };
3420
+ if (useDeviceReadback) {
3421
+ const runDeviceReadback = async () => {
3422
+ state.backElevation.fill(0);
3423
+ await readStorageBufferInto(
3424
+ renderer,
3425
+ attribute,
3426
+ state.elevationReadback,
3427
+ state.backElevation,
3428
+ activeLeafCount * verticesPerNode,
3429
+ "terrainElevationReadback"
3430
+ );
3431
+ let boundsFilled = false;
3432
+ if (boundsAttribute) {
3433
+ state.backTileBounds.fill(0);
3434
+ boundsFilled = await readStorageBufferInto(
3435
+ renderer,
3436
+ boundsAttribute,
3437
+ state.boundsReadback,
3438
+ state.backTileBounds,
3439
+ activeLeafCount * 2,
3440
+ "terrainBoundsReadback"
3441
+ );
3442
+ }
3443
+ applySnapshot(boundsFilled);
3444
+ };
3445
+ runDeviceReadback().finally(() => {
3446
+ state.readbackPending = false;
3447
+ });
3448
+ return;
3449
+ }
3450
+ const onComplete = (elevResult, boundsResult) => {
3451
+ const data = new Float32Array(elevResult);
3452
+ state.backElevation.fill(0);
3453
+ state.backElevation.set(data.subarray(0, totalElements));
3454
+ let boundsFilled = false;
3455
+ if (boundsResult) {
3456
+ const rawBounds = new Float32Array(boundsResult);
3457
+ state.backTileBounds.fill(0);
3458
+ state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
3459
+ boundsFilled = true;
3460
+ }
3461
+ applySnapshot(boundsFilled);
3462
+ };
3463
+ const elevationPromise = withReadback.getArrayBufferAsync(attribute);
3464
+ const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
3465
+ if (boundsPromise) {
3466
+ Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
3467
+ state.readbackPending = false;
3468
+ });
3469
+ } else {
3470
+ elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
3471
+ state.readbackPending = false;
3472
+ });
3473
+ }
1410
3474
  }
1411
- function createCpuTerrainCache(maxNodes, initialConfig) {
3475
+ function disposeSnapshotReadback(state) {
3476
+ disposeReadbackSlot(state.elevationReadback);
3477
+ disposeReadbackSlot(state.boundsReadback);
3478
+ }
3479
+
3480
+ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
1412
3481
  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;
3482
+ const shape = {
3483
+ edgeVertexCount: config.innerTileSegments + 3,
3484
+ verticesPerNode: 0
1431
3485
  };
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;
3486
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
3487
+ let totalElements = maxNodes * shape.verticesPerNode;
3488
+ const state = createTerrainSnapshotState(
3489
+ maxNodes,
3490
+ initialConfig.maxLevel,
3491
+ totalElements
3492
+ );
3493
+ const gridScratch = { gx: 0, gy: 0 };
3494
+ const gradientScratch = { dhdu: 0, dhdv: 0 };
3495
+ const keyScratch = { space: 0, u: 0, v: 0, dirX: 0, dirY: 0, dirZ: 0 };
3496
+ const surfaceLookupConfig = () => ({
3497
+ rootSize: config.rootSize,
3498
+ originX: config.originX,
3499
+ originZ: config.originZ,
3500
+ maxLevel: config.maxLevel,
3501
+ radius: config.radius,
3502
+ baseU: config.baseU,
3503
+ baseV: config.baseV
3504
+ });
3505
+ const gridCoordsFromLookup = (lookup) => {
3506
+ const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
3507
+ const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
3508
+ gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
3509
+ gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
3510
+ return gridScratch;
3511
+ };
3512
+ const rawHeightFromLookup = (lookup) => {
3513
+ const g = gridCoordsFromLookup(lookup);
3514
+ return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
1449
3515
  };
1450
3516
  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
3517
  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();
1460
- };
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
- };
3518
+ const { dhdu, dhdv } = elevationGradientAt(
3519
+ state.frontElevation,
3520
+ shape,
3521
+ leafIndex,
3522
+ gx,
3523
+ gy,
3524
+ stepWorld,
3525
+ config.elevationScale,
3526
+ gradientScratch
3527
+ );
3528
+ return new three.Vector3(-dhdu, 1, -dhdv).normalize();
1500
3529
  };
1501
3530
  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);
3531
+ const height = rawHeightFromLookup(lookup);
1507
3532
  const scaledHeight = config.originY + height * config.elevationScale;
1508
- const normal = computeNormal(lookup.leafIndex, gx, gy, lookup.tileSize);
3533
+ const normal = computeNormal(lookup.leafIndex, gridScratch.gx, gridScratch.gy, lookup.tileSize);
1509
3534
  return { elevation: scaledHeight, normal, valid: true };
1510
3535
  };
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
3536
  const sampleTerrain = (worldX, worldZ) => {
1521
- if (!hasSnapshot) {
3537
+ if (!state.hasSnapshot) {
1522
3538
  return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1523
3539
  }
1524
- const lookup = lookupTile(worldX, worldZ);
3540
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1525
3541
  if (!lookup.found) {
1526
3542
  return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1527
3543
  }
1528
3544
  return sampleFromLookup(lookup);
1529
3545
  };
1530
3546
  const getElevation = (worldX, worldZ) => {
1531
- if (!hasSnapshot) {
3547
+ if (!state.hasSnapshot) {
1532
3548
  return { elevation: 0, valid: false };
1533
3549
  }
1534
- const lookup = lookupTile(worldX, worldZ);
3550
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1535
3551
  if (!lookup.found) {
1536
3552
  return { elevation: 0, valid: false };
1537
3553
  }
1538
- return sampleElevationFromLookup(lookup);
3554
+ const height = rawHeightFromLookup(lookup);
3555
+ return {
3556
+ elevation: config.originY + height * config.elevationScale,
3557
+ valid: true
3558
+ };
3559
+ };
3560
+ const tileFromLookup = (lookup) => {
3561
+ if (!lookup.found) return null;
3562
+ return {
3563
+ space: lookup.space,
3564
+ level: lookup.level,
3565
+ x: lookup.tileX,
3566
+ y: lookup.tileY,
3567
+ index: lookup.leafIndex
3568
+ };
3569
+ };
3570
+ const tileBoundsFromLookup = (lookup, elevationBase) => {
3571
+ if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
3572
+ const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
3573
+ const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
3574
+ const a = elevationBase + rawMin * config.elevationScale;
3575
+ const b = elevationBase + rawMax * config.elevationScale;
3576
+ return {
3577
+ space: lookup.space,
3578
+ level: lookup.level,
3579
+ x: lookup.tileX,
3580
+ y: lookup.tileY,
3581
+ index: lookup.leafIndex,
3582
+ minElevation: Math.min(a, b),
3583
+ maxElevation: Math.max(a, b)
3584
+ };
3585
+ };
3586
+ const invalidSurfaceSample = (dx, dy, dz) => ({
3587
+ position: new three.Vector3(),
3588
+ normal: new three.Vector3(0, 1, 0),
3589
+ direction: new three.Vector3(dx, dy, dz),
3590
+ elevation: 0,
3591
+ valid: false
3592
+ });
3593
+ const surfaceLookup = (px, py, pz) => {
3594
+ if (!surfaceOps || !surfaceOps.positionToKey(px, py, pz, keyScratch)) {
3595
+ return { found: false };
3596
+ }
3597
+ return lookupTileByFaceUV(
3598
+ state.frontIndex,
3599
+ surfaceLookupConfig(),
3600
+ keyScratch.space,
3601
+ keyScratch.u,
3602
+ keyScratch.v
3603
+ );
3604
+ };
3605
+ const sampleSurfaceByPosition = (px, py, pz) => {
3606
+ if (!state.hasSnapshot || !surfaceOps) return invalidSurfaceSample(0, 1, 0);
3607
+ if (!surfaceOps.positionToKey(px, py, pz, keyScratch)) {
3608
+ return invalidSurfaceSample(0, 1, 0);
3609
+ }
3610
+ const key = keyScratch;
3611
+ const lookup = lookupTileByFaceUV(
3612
+ state.frontIndex,
3613
+ surfaceLookupConfig(),
3614
+ key.space,
3615
+ key.u,
3616
+ key.v
3617
+ );
3618
+ if (!lookup.found) return invalidSurfaceSample(key.dirX, key.dirY, key.dirZ);
3619
+ const height = rawHeightFromLookup(lookup);
3620
+ const elevation = height * config.elevationScale;
3621
+ const position = new three.Vector3();
3622
+ surfaceOps.surfacePosition(key, elevation, position);
3623
+ const normal = surfaceOps.surfaceNormal(key, {
3624
+ elevation: state.frontElevation,
3625
+ shape,
3626
+ leafIndex: lookup.leafIndex,
3627
+ gx: gridScratch.gx,
3628
+ gy: gridScratch.gy,
3629
+ innerTileSegments: config.innerTileSegments,
3630
+ elevationScale: config.elevationScale,
3631
+ level: lookup.level
3632
+ });
3633
+ return {
3634
+ position,
3635
+ normal,
3636
+ direction: new three.Vector3(key.dirX, key.dirY, key.dirZ),
3637
+ elevation,
3638
+ valid: true
3639
+ };
1539
3640
  };
1540
3641
  const api = {
1541
3642
  get generation() {
1542
- return generationCount;
3643
+ return state.generation;
1543
3644
  },
1544
3645
  get ready() {
1545
- return hasSnapshot;
3646
+ return state.hasSnapshot;
3647
+ },
3648
+ get hasSurface() {
3649
+ return surfaceOps !== null;
1546
3650
  },
1547
3651
  updateConfig(nextConfig) {
1548
3652
  config = nextConfig;
1549
- edgeVertexCount = config.innerTileSegments + 3;
1550
- verticesPerNode = edgeVertexCount * edgeVertexCount;
1551
- totalElements = maxNodes * verticesPerNode;
3653
+ shape.edgeVertexCount = config.innerTileSegments + 3;
3654
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
3655
+ totalElements = maxNodes * shape.verticesPerNode;
1552
3656
  },
1553
3657
  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
- }
3658
+ triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
3659
+ activeLeafCount: activeLeafCount ?? 0,
3660
+ totalElements,
3661
+ verticesPerNode: shape.verticesPerNode,
3662
+ elevationScale: config.elevationScale,
3663
+ originY: config.originY
3664
+ });
3665
+ },
3666
+ dispose() {
3667
+ disposeSnapshotReadback(state);
1619
3668
  },
1620
3669
  getElevation(worldX, worldZ) {
1621
3670
  const sample = getElevation(worldX, worldZ);
@@ -1625,43 +3674,26 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1625
3674
  return sampleTerrain(worldX, worldZ).normal;
1626
3675
  },
1627
3676
  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
- };
3677
+ if (!state.hasSnapshot) return null;
3678
+ return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
1637
3679
  },
1638
3680
  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
- };
3681
+ if (!state.hasSnapshot) return null;
3682
+ return tileBoundsFromLookup(
3683
+ lookupTile(state.frontIndex, config, worldX, worldZ),
3684
+ config.originY
3685
+ );
1654
3686
  },
1655
3687
  getGlobalElevationRange() {
1656
- return globalRange;
3688
+ return state.globalRange;
1657
3689
  },
1658
3690
  sampleTerrainBatch(positions) {
1659
3691
  const count = Math.floor(positions.length / 2);
1660
3692
  const elevations = new Float32Array(count);
1661
3693
  const normals = new Float32Array(count * 3);
1662
3694
  const valid = new Uint8Array(count);
1663
- if (!hasSnapshot) {
1664
- return { elevations, normals, valid, generation: generationCount };
3695
+ if (!state.hasSnapshot) {
3696
+ return { elevations, normals, valid, generation: state.generation };
1665
3697
  }
1666
3698
  let lastTile;
1667
3699
  for (let i = 0; i < count; i += 1) {
@@ -1672,6 +3704,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1672
3704
  lookup = {
1673
3705
  found: true,
1674
3706
  leafIndex: lastTile.leafIndex,
3707
+ space: 0,
1675
3708
  level: lastTile.level,
1676
3709
  tileX: lastTile.tileX,
1677
3710
  tileY: lastTile.tileY,
@@ -1680,7 +3713,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1680
3713
  localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
1681
3714
  };
1682
3715
  } else {
1683
- lookup = lookupTile(worldX, worldZ);
3716
+ lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1684
3717
  if (lookup.found) {
1685
3718
  lastTile = {
1686
3719
  leafIndex: lookup.leafIndex,
@@ -1706,40 +3739,63 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
1706
3739
  normals[i * 3 + 2] = sample.normal.z;
1707
3740
  valid[i] = 1;
1708
3741
  }
1709
- return { elevations, normals, valid, generation: generationCount };
1710
- },
1711
- sampleTerrain
1712
- };
1713
- return api;
1714
- }
1715
-
1716
- function createTerrainQuery(cache) {
1717
- return {
1718
- get generation() {
1719
- return cache.generation;
1720
- },
1721
- getElevation(worldX, worldZ) {
1722
- return cache.getElevation(worldX, worldZ);
3742
+ return { elevations, normals, valid, generation: state.generation };
1723
3743
  },
1724
- getNormal(worldX, worldZ) {
1725
- return cache.getNormal(worldX, worldZ);
3744
+ sampleTerrain,
3745
+ // ── Generic surface ──
3746
+ sampleSurfaceByPosition,
3747
+ getElevationBySurfacePosition(px, py, pz) {
3748
+ const sample = sampleSurfaceByPosition(px, py, pz);
3749
+ return sample.valid ? sample.elevation : null;
1726
3750
  },
1727
- getTile(worldX, worldZ) {
1728
- return cache.getTile(worldX, worldZ);
3751
+ getNormalBySurfacePosition(px, py, pz) {
3752
+ const sample = sampleSurfaceByPosition(px, py, pz);
3753
+ return sample.valid ? sample.normal : null;
1729
3754
  },
1730
- getTileBounds(worldX, worldZ) {
1731
- return cache.getTileBounds(worldX, worldZ);
3755
+ getTileBySurfacePosition(px, py, pz) {
3756
+ if (!state.hasSnapshot || !surfaceOps) return null;
3757
+ return tileFromLookup(surfaceLookup(px, py, pz));
1732
3758
  },
1733
- getGlobalElevationRange() {
1734
- return cache.getGlobalElevationRange();
3759
+ getTileBoundsBySurfacePosition(px, py, pz) {
3760
+ if (!state.hasSnapshot || !surfaceOps) return null;
3761
+ return tileBoundsFromLookup(surfaceLookup(px, py, pz), 0);
1735
3762
  },
1736
- sampleTerrain(worldX, worldZ) {
1737
- return cache.sampleTerrain(worldX, worldZ);
3763
+ sampleSurfaceBatchByPosition(positions) {
3764
+ const count = Math.floor(positions.length / 3);
3765
+ const outPositions = new Float32Array(count * 3);
3766
+ const normals = new Float32Array(count * 3);
3767
+ const elevations = new Float32Array(count);
3768
+ const valid = new Uint8Array(count);
3769
+ if (!state.hasSnapshot || !surfaceOps) {
3770
+ return { positions: outPositions, normals, elevations, valid, generation: state.generation };
3771
+ }
3772
+ for (let i = 0; i < count; i += 1) {
3773
+ const sample = sampleSurfaceByPosition(
3774
+ positions[i * 3] ?? 0,
3775
+ positions[i * 3 + 1] ?? 0,
3776
+ positions[i * 3 + 2] ?? 0
3777
+ );
3778
+ if (!sample.valid) {
3779
+ normals[i * 3 + 1] = 1;
3780
+ continue;
3781
+ }
3782
+ outPositions[i * 3] = sample.position.x;
3783
+ outPositions[i * 3 + 1] = sample.position.y;
3784
+ outPositions[i * 3 + 2] = sample.position.z;
3785
+ normals[i * 3] = sample.normal.x;
3786
+ normals[i * 3 + 1] = sample.normal.y;
3787
+ normals[i * 3 + 2] = sample.normal.z;
3788
+ elevations[i] = sample.elevation;
3789
+ valid[i] = 1;
3790
+ }
3791
+ return { positions: outPositions, normals, elevations, valid, generation: state.generation };
1738
3792
  },
1739
- sampleTerrainBatch(positions) {
1740
- return cache.sampleTerrainBatch(positions);
3793
+ getTileElevationRange(space, level, x, y, out) {
3794
+ if (!state.hasSnapshot) return false;
3795
+ return lookupTileElevationRange(state.elevationPyramid, space, level, x, y, out);
1741
3796
  }
1742
3797
  };
3798
+ return api;
1743
3799
  }
1744
3800
 
1745
3801
  const WGSIZE = 64;
@@ -1783,7 +3839,10 @@ const tileBoundsContextTask = work.task((get, work) => {
1783
3839
  return work(() => {
1784
3840
  const data = new Float32Array(maxNodesValue * 2);
1785
3841
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
1786
- const node = tsl.storage(attribute, "float", maxNodesValue * 2);
3842
+ attribute.name = "tileBounds";
3843
+ const node = tsl.storage(attribute, "float", maxNodesValue * 2).setName(
3844
+ "tileBounds"
3845
+ );
1787
3846
  const verticesPerNode = edgeVertexCount * edgeVertexCount;
1788
3847
  const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
1789
3848
  return { data, attribute, node, kernel };
@@ -1810,8 +3869,11 @@ const terrainQueryTask = work.task((get, work) => {
1810
3869
  const rootSizeValue = get(rootSize);
1811
3870
  const originValue = get(origin);
1812
3871
  const elevationScaleValue = get(elevationScale);
3872
+ const radiusValue = get(radius);
3873
+ const topologyValue = get(topologyTask);
3874
+ const projection = topologyValue.projection;
1813
3875
  return work((prev) => {
1814
- const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
3876
+ const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projection.kind}`;
1815
3877
  const configValues = {
1816
3878
  rootSize: rootSizeValue,
1817
3879
  originX: originValue.x,
@@ -1819,16 +3881,25 @@ const terrainQueryTask = work.task((get, work) => {
1819
3881
  originZ: originValue.z,
1820
3882
  innerTileSegments: innerTileSegmentsValue,
1821
3883
  elevationScale: elevationScaleValue,
1822
- maxLevel: maxLevelValue
3884
+ maxLevel: maxLevelValue,
3885
+ radius: topologyValue.radius ?? radiusValue,
3886
+ baseU: projection.baseResolution?.u ?? 1,
3887
+ baseV: projection.baseResolution?.v ?? 1
1823
3888
  };
1824
3889
  let cache = prev?.cache;
1825
3890
  let query = prev?.query;
3891
+ let surfaceQuery = prev?.surfaceQuery ?? null;
3892
+ let sphereQuery = prev?.sphereQuery ?? null;
1826
3893
  if (!cache || !query || prev?.shapeKey !== shapeKey) {
1827
- cache = createCpuTerrainCache(maxNodesValue, configValues);
1828
- query = createTerrainQuery(cache);
3894
+ prev?.cache?.dispose();
3895
+ cache = createCpuTerrainCache(maxNodesValue, configValues, projection.cpu.createSurfaceOps());
3896
+ const runtime = projection.cpu.createRuntimeQueries(cache);
3897
+ query = runtime.query;
3898
+ surfaceQuery = runtime.surfaceQuery;
3899
+ sphereQuery = runtime.sphereQuery;
1829
3900
  }
1830
3901
  cache.updateConfig(configValues);
1831
- return { cache, query, shapeKey };
3902
+ return { cache, query, surfaceQuery, sphereQuery, shapeKey };
1832
3903
  });
1833
3904
  }).displayName("terrainQueryTask");
1834
3905
  const terrainReadbackTask = work.task(
@@ -1851,38 +3922,54 @@ const terrainReadbackTask = work.task(
1851
3922
  }
1852
3923
  ).displayName("terrainReadbackTask").lane("gpu");
1853
3924
 
1854
- const surfaceTask = work.task((get, work) => {
1855
- const customSurface = get(surface);
3925
+ const topologyTask = work.task((get, work) => {
3926
+ const customTopology = get(topology);
1856
3927
  const rootSizeVal = get(rootSize);
1857
3928
  const originVal = get(origin);
1858
3929
  return work(() => {
1859
- if (customSurface) return customSurface;
1860
- return createFlatSurface({ rootSize: rootSizeVal, origin: originVal });
3930
+ if (customTopology) return customTopology;
3931
+ return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
1861
3932
  });
1862
- }).displayName("surfaceTask");
3933
+ }).displayName("topologyTask");
1863
3934
  const quadtreeConfigTask = work.task((get, work) => {
1864
- const surfaceVal = get(surfaceTask);
3935
+ const topologyVal = get(topologyTask);
1865
3936
  const maxNodesVal = get(maxNodes);
1866
3937
  const maxLevelVal = get(maxLevel);
1867
3938
  return work(() => {
1868
- const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surfaceVal);
3939
+ const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
1869
3940
  return {
1870
3941
  state,
1871
- surface: surfaceVal
3942
+ topology: topologyVal
1872
3943
  };
1873
3944
  });
1874
3945
  }).displayName("quadtreeConfigTask");
1875
3946
  const quadtreeUpdateTask = work.task((get, work) => {
1876
3947
  const quadtreeConfig = get(quadtreeConfigTask);
1877
3948
  const quadtreeUpdateConfig = get(quadtreeUpdate);
1878
- const { query: terrainQuery } = get(terrainQueryTask);
3949
+ const { query: terrainQuery, surfaceQuery, cache } = get(terrainQueryTask);
3950
+ const elevationScaleValue = get(elevationScale);
1879
3951
  let outLeaves = void 0;
3952
+ const cameraPosition = new three.Vector3();
3953
+ const elevationRangeScratch = { min: 0, max: 0 };
1880
3954
  return work(() => {
1881
3955
  const cam = quadtreeUpdateConfig.cameraOrigin;
1882
- quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
3956
+ if (surfaceQuery) {
3957
+ cameraPosition.set(cam.x, cam.y, cam.z);
3958
+ quadtreeUpdateConfig.elevationAtCameraXZ = surfaceQuery.getElevationByPosition(cameraPosition) ?? 0;
3959
+ } else {
3960
+ quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
3961
+ }
3962
+ quadtreeUpdateConfig.tileElevationRange = (space, level, x, y, out) => {
3963
+ if (!cache.getTileElevationRange(space, level, x, y, elevationRangeScratch)) {
3964
+ return false;
3965
+ }
3966
+ out.min = elevationRangeScratch.min * elevationScaleValue;
3967
+ out.max = elevationRangeScratch.max * elevationScaleValue;
3968
+ return true;
3969
+ };
1883
3970
  outLeaves = update(
1884
3971
  quadtreeConfig.state,
1885
- quadtreeConfig.surface,
3972
+ quadtreeConfig.topology,
1886
3973
  quadtreeUpdateConfig,
1887
3974
  outLeaves
1888
3975
  );
@@ -1904,7 +3991,7 @@ const leafGpuBufferTask = work.task((get, work) => {
1904
3991
  leafStorage.data[offset] = leafSet.level[i] ?? 0;
1905
3992
  leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
1906
3993
  leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
1907
- leafStorage.data[offset + 3] = 1;
3994
+ leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
1908
3995
  }
1909
3996
  leafStorage.attribute.needsUpdate = true;
1910
3997
  leafStorage.node.needsUpdate = true;
@@ -1917,23 +4004,6 @@ const leafGpuBufferTask = work.task((get, work) => {
1917
4004
  });
1918
4005
  }).displayName("leafGpuBufferTask");
1919
4006
 
1920
- function createElevationFunction(callback) {
1921
- const tslFunction = (args) => {
1922
- const params = {
1923
- worldPosition: args.worldPosition,
1924
- rootSize: args.rootSize,
1925
- rootUV: args.rootUV,
1926
- tileUV: args.tileUV,
1927
- tileLevel: args.tileLevel,
1928
- tileSize: args.tileSize,
1929
- tileOriginVec2: args.tileOriginVec2,
1930
- nodeIndex: args.nodeIndex
1931
- };
1932
- return callback(params);
1933
- };
1934
- return TSL_js.Fn(tslFunction);
1935
- }
1936
-
1937
4007
  function createTerrainUniforms(params) {
1938
4008
  const sanitizedId = params.instanceId?.replace(/-/g, "_");
1939
4009
  const suffix = sanitizedId ? `_${sanitizedId}` : "";
@@ -1946,12 +4016,14 @@ function createTerrainUniforms(params) {
1946
4016
  );
1947
4017
  const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
1948
4018
  const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
4019
+ const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
1949
4020
  return {
1950
4021
  uRootOrigin,
1951
4022
  uRootSize,
1952
4023
  uInnerTileSegments,
1953
4024
  uSkirtScale,
1954
- uElevationScale
4025
+ uElevationScale,
4026
+ uRadius
1955
4027
  };
1956
4028
  }
1957
4029
 
@@ -1965,6 +4037,7 @@ const createUniformsTask = work.task((get, work) => {
1965
4037
  innerTileSegments: get(innerTileSegments),
1966
4038
  skirtScale: get(skirtScale),
1967
4039
  elevationScale: get(elevationScale),
4040
+ radius: get(radius),
1968
4041
  instanceId: get(instanceIdTask)
1969
4042
  };
1970
4043
  return work(() => createTerrainUniforms(uniformParams));
@@ -1976,6 +4049,7 @@ const updateUniformsTask = work.task((get, work) => {
1976
4049
  const innerTileSegmentsVal = get(innerTileSegments);
1977
4050
  const skirtScaleVal = get(skirtScale);
1978
4051
  const elevationScaleVal = get(elevationScale);
4052
+ const radiusVal = get(radius);
1979
4053
  return work(() => {
1980
4054
  terrainUniformsContext.uRootSize.value = rootSizeVal;
1981
4055
  terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
@@ -1986,6 +4060,7 @@ const updateUniformsTask = work.task((get, work) => {
1986
4060
  terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
1987
4061
  terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
1988
4062
  terrainUniformsContext.uElevationScale.value = elevationScaleVal;
4063
+ terrainUniformsContext.uRadius.value = radiusVal;
1989
4064
  return terrainUniformsContext;
1990
4065
  });
1991
4066
  }).displayName("updateUniformsTask");
@@ -1997,7 +4072,8 @@ const createElevationFieldContextTask = work.task((get, work) => {
1997
4072
  return work(() => {
1998
4073
  const data = new Float32Array(totalElements);
1999
4074
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
2000
- const node = tsl.storage(attribute, "float", totalElements);
4075
+ attribute.name = "elevationField";
4076
+ const node = tsl.storage(attribute, "float", totalElements).setName("elevationField");
2001
4077
  return {
2002
4078
  data,
2003
4079
  attribute,
@@ -2008,8 +4084,9 @@ const createElevationFieldContextTask = work.task((get, work) => {
2008
4084
  const tileNodesTask = work.task((get, work) => {
2009
4085
  const leafStorage = get(leafStorageTask);
2010
4086
  const uniforms = get(updateUniformsTask);
4087
+ const topology = get(topologyTask);
2011
4088
  return work(() => {
2012
- return createTileCompute(leafStorage, uniforms);
4089
+ return createTileCompute(leafStorage, uniforms, topology.projection);
2013
4090
  });
2014
4091
  }).displayName("tileNodesTask");
2015
4092
  const elevationFieldStageTask = work.task((get, work) => {
@@ -2044,30 +4121,6 @@ const createTerrainFieldTextureTask = work.task(
2044
4121
  );
2045
4122
  }
2046
4123
  ).displayName("createTerrainFieldTextureTask");
2047
- function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
2048
- return tsl.Fn(
2049
- ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
2050
- const iEdge = tsl.int(edgeVertexCount);
2051
- const verticesPerNode = iEdge.mul(iEdge);
2052
- const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
2053
- const xLeft = tsl.int(ix).sub(tsl.int(1));
2054
- const xRight = tsl.int(ix).add(tsl.int(1));
2055
- const yUp = tsl.int(iy).sub(tsl.int(1));
2056
- const yDown = tsl.int(iy).add(tsl.int(1));
2057
- const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
2058
- const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
2059
- const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
2060
- const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
2061
- const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
2062
- const stepWorld = tileSize.div(innerSegments);
2063
- const inv2Step = tsl.float(0.5).div(stepWorld);
2064
- const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
2065
- const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
2066
- const normal = tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
2067
- return tsl.vec2(normal.x, normal.z);
2068
- }
2069
- );
2070
- }
2071
4124
  const terrainFieldStageTask = work.task((get, work) => {
2072
4125
  const upstream = get(elevationFieldStageTask);
2073
4126
  const elevationFieldContext = get(createElevationFieldContextTask);
@@ -2075,635 +4128,115 @@ const terrainFieldStageTask = work.task((get, work) => {
2075
4128
  const tileEdgeVertexCount = get(innerTileSegments) + 3;
2076
4129
  const tile = get(tileNodesTask);
2077
4130
  const uniforms = get(updateUniformsTask);
4131
+ const topology = get(topologyTask);
2078
4132
  return work(() => {
2079
- const computeNormal = createNormalFromElevationField(
2080
- elevationFieldContext.node,
2081
- tileEdgeVertexCount
2082
- );
4133
+ const computeNormal = topology.projection.gpu.createFieldNormal({
4134
+ elevationFieldNode: elevationFieldContext.node,
4135
+ edgeVertexCount: tileEdgeVertexCount,
4136
+ tile,
4137
+ uniforms
4138
+ });
2083
4139
  return [
2084
4140
  ...upstream,
2085
4141
  (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
2086
4142
  const ix = tsl.int(localCoordinates.x);
2087
4143
  const iy = tsl.int(localCoordinates.y);
2088
- const tileSize = tile.tileSize(nodeIndex);
2089
4144
  const height = elevationFieldContext.node.element(globalVertexIndex);
2090
- const normalXZ = computeNormal(
2091
- nodeIndex,
2092
- tileSize,
2093
- ix,
2094
- iy,
2095
- uniforms.uElevationScale
2096
- );
4145
+ const normal = computeNormal(nodeIndex, ix, iy);
2097
4146
  storeTerrainField(
2098
4147
  terrainFieldStorage,
2099
4148
  ix,
2100
4149
  iy,
2101
4150
  nodeIndex,
2102
- packTerrainFieldSample(height, normalXZ)
4151
+ packTerrainFieldSample(height, normal)
2103
4152
  );
2104
- }
2105
- ];
2106
- });
2107
- }).displayName("terrainFieldStageTask");
2108
-
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");
2128
- function createComputePipelineTasks(leafStageTask) {
2129
- const compile = work.task((get, work) => {
2130
- const pipeline = get(leafStageTask);
2131
- const edgeVertexCount = get(innerTileSegments) + 3;
2132
- return work(
2133
- () => compileComputePipeline(pipeline, edgeVertexCount, {
2134
- preferSingleKernelWhenPossible: false
2135
- })
2136
- );
2137
- }).displayName("compileComputeTask");
2138
- const execute = work.task(
2139
- (get, work, { resources }) => {
2140
- const { execute: run } = get(compile);
2141
- const leafState = get(leafGpuBufferTask);
2142
- return work(
2143
- () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
2144
- }
2145
- );
2146
- }
2147
- ).displayName("executeComputeTask").lane("gpu");
2148
- return { compile, execute };
2149
- }
2150
-
2151
- const SLOT_STRIDE = 6;
2152
- function nextPow2(n) {
2153
- let x = 1;
2154
- while (x < n) x <<= 1;
2155
- return x;
2156
- }
2157
- function createGpuSpatialIndex(maxEntries) {
2158
- const size = nextPow2(Math.max(2, maxEntries * 2));
2159
- const data = new Uint32Array(size * SLOT_STRIDE);
2160
- const attribute = new webgpu.StorageBufferAttribute(data, SLOT_STRIDE);
2161
- const node = tsl.storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
2162
- const stampGen = tsl.uniform(tsl.uint(1)).setName("uGpuSpatialIndexStampGen");
2163
- return {
2164
- data,
2165
- size,
2166
- mask: size - 1,
2167
- stampGen,
2168
- attribute,
2169
- node
2170
- };
2171
- }
2172
- function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
2173
- if (gpuIndex.size !== cpuIndex.size) {
2174
- throw new Error(
2175
- `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
2176
- );
2177
- }
2178
- for (let i = 0; i < cpuIndex.size; i += 1) {
2179
- const base = i * SLOT_STRIDE;
2180
- gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
2181
- gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
2182
- gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
2183
- gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
2184
- gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
2185
- gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
2186
- }
2187
- gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
2188
- gpuIndex.attribute.needsUpdate = true;
2189
- gpuIndex.node.needsUpdate = true;
2190
- }
2191
- function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
2192
- const offset = tsl.int(slot).mul(tsl.int(SLOT_STRIDE)).add(tsl.int(fieldOffset));
2193
- return spatialIndex.node.element(offset).toUint();
2194
- }
2195
- const mix32 = tsl.Fn(([x]) => {
2196
- const v = tsl.uint(x).toVar();
2197
- v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2198
- v.assign(v.mul(tsl.uint(2146121005)));
2199
- v.assign(v.bitXor(v.shiftRight(tsl.uint(15))));
2200
- v.assign(v.mul(tsl.uint(2221713035)));
2201
- v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2202
- return v;
2203
- });
2204
- const hashKey = tsl.Fn(([space, level, x, y]) => {
2205
- const s = tsl.uint(space).bitAnd(tsl.uint(255));
2206
- const l = tsl.uint(level).bitAnd(tsl.uint(255));
2207
- const h = s.bitXor(l.shiftLeft(tsl.uint(8))).bitXor(mix32(tsl.uint(x))).bitXor(mix32(tsl.uint(y)));
2208
- return mix32(h);
2209
- });
2210
- const createGpuSpatialLookup = (spatialIndex) => {
2211
- const slotCount = spatialIndex.size;
2212
- const mask = tsl.uint(spatialIndex.mask);
2213
- const stampGen = spatialIndex.stampGen.toUint();
2214
- const emptyValue = tsl.int(-1);
2215
- return tsl.Fn(([space, level, x, y]) => {
2216
- const s = tsl.uint(space).bitAnd(tsl.uint(255));
2217
- const l = tsl.uint(level).bitAnd(tsl.uint(255));
2218
- const xx = tsl.uint(x);
2219
- const yy = tsl.uint(y);
2220
- const result = emptyValue.toVar();
2221
- const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
2222
- const probes = tsl.int(0).toVar();
2223
- tsl.Loop(slotCount, () => {
2224
- const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
2225
- tsl.If(stamp.notEqual(stampGen), () => {
2226
- tsl.Break();
2227
- });
2228
- const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
2229
- const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
2230
- const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
2231
- const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
2232
- tsl.If(
2233
- ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
2234
- () => {
2235
- result.assign(tsl.int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
2236
- tsl.Break();
2237
- }
2238
- );
2239
- slot.assign(slot.add(tsl.uint(1)).bitAnd(mask));
2240
- probes.addAssign(1);
2241
- });
2242
- return result;
2243
- });
2244
- };
2245
- const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
2246
- const lookup = createGpuSpatialLookup(spatialIndex);
2247
- const levelCount = Math.max(1, maxLevel + 1);
2248
- return tsl.Fn(([worldX, worldZ]) => {
2249
- const rootOrigin = uniforms.uRootOrigin.toVar();
2250
- const rootSize = uniforms.uRootSize.toVar();
2251
- const halfRoot = rootSize.mul(tsl.float(0.5));
2252
- const tileIndex = tsl.int(-1).toVar();
2253
- const tileU = tsl.float(0).toVar();
2254
- const tileV = tsl.float(0).toVar();
2255
- const i = tsl.int(0).toVar();
2256
- tsl.Loop(levelCount, () => {
2257
- const level = tsl.int(maxLevel).sub(i).toVar();
2258
- const scale = tsl.pow(tsl.float(2), level.toFloat());
2259
- const tileSize = rootSize.div(scale);
2260
- const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
2261
- const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
2262
- const maybeIndex = lookup(tsl.int(0), level, tileX, tileY).toVar();
2263
- tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
2264
- const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
2265
- const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
2266
- tileIndex.assign(maybeIndex);
2267
- tileU.assign(worldX.sub(minX).div(tileSize));
2268
- tileV.assign(worldZ.sub(minZ).div(tileSize));
2269
- tsl.Break();
2270
- });
2271
- i.addAssign(1);
2272
- });
2273
- return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2274
- });
2275
- };
2276
-
2277
- const gpuSpatialIndexStorageTask = work.task((get, work) => {
2278
- const maxNodesValue = get(maxNodes);
2279
- return work(() => createGpuSpatialIndex(maxNodesValue));
2280
- }).displayName("gpuSpatialIndexStorageTask");
2281
- const gpuSpatialIndexUploadTask = work.task((get, work) => {
2282
- const quadtreeConfig = get(quadtreeConfigTask);
2283
- get(quadtreeUpdateTask);
2284
- const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
2285
- return work(() => {
2286
- uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
2287
- return gpuSpatialIndex;
2288
- });
2289
- }).displayName("gpuSpatialIndexUploadTask");
2290
-
2291
- function createTerrainSampleNode(params) {
2292
- const tileLookup = createTileIndexFromWorldPosition(
2293
- params.spatialIndex,
2294
- params.uniforms,
2295
- maxLevel.get()
2296
- );
2297
- return tsl.Fn(([worldX, worldZ]) => {
2298
- 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
- );
2328
- });
2329
- }
2330
- function createTerrainSampler(params) {
2331
- const elevationNode = createElevationFunction(params.elevationCallback);
2332
- const terrainSampleAt = createTerrainSampleNode(params);
2333
- const evaluateElevationAt = tsl.Fn(([worldX, worldZ]) => {
2334
- const rootOrigin = params.uniforms.uRootOrigin.toVar();
2335
- const rootSize = params.uniforms.uRootSize.toVar();
2336
- const centeredX = worldX.sub(rootOrigin.x);
2337
- const centeredZ = worldZ.sub(rootOrigin.z);
2338
- const rootUV = tsl.vec2(
2339
- centeredX.div(rootSize).add(0.5),
2340
- centeredZ.div(rootSize).mul(tsl.float(-1)).add(0.5)
2341
- ).toVar();
2342
- return elevationNode({
2343
- worldPosition: tsl.vec3(worldX, rootOrigin.y, worldZ),
2344
- rootSize,
2345
- rootUV,
2346
- tileUV: rootUV,
2347
- tileLevel: tsl.int(0),
2348
- tileSize: rootSize,
2349
- tileOriginVec2: tsl.vec2(0, 0),
2350
- nodeIndex: tsl.int(0)
2351
- });
4153
+ }
4154
+ ];
2352
4155
  });
2353
- const sampleTerrain = tsl.Fn(
2354
- ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
2355
- );
2356
- const sampleElevation = tsl.Fn(
2357
- ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
2358
- );
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
- );
2369
- const evaluateElevation = tsl.Fn(
2370
- ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
2371
- );
2372
- const evaluateNormalNode = tsl.Fn(
2373
- ([worldX, worldZ, epsilon]) => {
2374
- const eps = epsilon ?? tsl.float(0.1);
2375
- const elevationScale = params.uniforms.uElevationScale.toVar();
2376
- const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
2377
- elevationScale
2378
- );
2379
- const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
2380
- elevationScale
2381
- );
2382
- const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
2383
- elevationScale
2384
- );
2385
- const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
2386
- elevationScale
4156
+ }).displayName("terrainFieldStageTask");
4157
+
4158
+ const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
4159
+ function createComputePipelineTasks(leafStageTask) {
4160
+ const compile = work.task((get, work) => {
4161
+ const pipeline = get(leafStageTask);
4162
+ const edgeVertexCount = get(innerTileSegments) + 3;
4163
+ return work(
4164
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
4165
+ })
4166
+ );
4167
+ }).displayName("compileComputeTask");
4168
+ const execute = work.task(
4169
+ (get, work, { resources }) => {
4170
+ const { execute: run } = get(compile);
4171
+ const leafState = get(leafGpuBufferTask);
4172
+ return work(
4173
+ () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4174
+ }
2387
4175
  );
2388
- const inv2eps = tsl.float(0.5).div(eps);
2389
- const dhdx = hR.sub(hL).mul(inv2eps);
2390
- const dhdz = hU.sub(hD).mul(inv2eps);
2391
- return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
2392
4176
  }
2393
- );
2394
- const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
2395
- return {
2396
- sampleElevation,
2397
- sampleNormal,
2398
- sampleTerrain,
2399
- sampleValidity,
2400
- evaluateElevation,
2401
- evaluateNormal
2402
- };
4177
+ ).displayName("executeComputeTask").lane("gpu");
4178
+ return { compile, execute };
2403
4179
  }
2404
4180
 
4181
+ const gpuSpatialIndexStorageTask = work.task((get, work) => {
4182
+ const maxNodesValue = get(maxNodes);
4183
+ return work(() => createGpuSpatialIndex(maxNodesValue));
4184
+ }).displayName("gpuSpatialIndexStorageTask");
4185
+ const gpuSpatialIndexUploadTask = work.task((get, work) => {
4186
+ const quadtreeConfig = get(quadtreeConfigTask);
4187
+ get(quadtreeUpdateTask);
4188
+ const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
4189
+ return work(() => {
4190
+ uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
4191
+ return gpuSpatialIndex;
4192
+ });
4193
+ }).displayName("gpuSpatialIndexUploadTask");
4194
+
2405
4195
  const createTerrainSamplerTask = work.task((get, work) => {
2406
4196
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
2407
4197
  const spatialIndex = get(gpuSpatialIndexStorageTask);
2408
4198
  const uniforms = get(updateUniformsTask);
2409
4199
  const elevationCallback = get(elevationFn);
4200
+ const maxLevelValue = get(maxLevel);
4201
+ const projection = get(topologyTask).projection;
2410
4202
  return work(
2411
4203
  () => createTerrainSampler({
2412
4204
  terrainFieldStorage,
2413
4205
  spatialIndex,
2414
4206
  uniforms,
2415
- elevationCallback
4207
+ elevationCallback,
4208
+ maxLevel: maxLevelValue,
4209
+ projection
2416
4210
  })
2417
4211
  );
2418
4212
  }).displayName("createTerrainSamplerTask");
2419
4213
 
2420
- const isSkirtVertex = tsl.Fn(([segments]) => {
2421
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
2422
- const vIndex = tsl.int(tsl.vertexIndex);
2423
- const segmentEdges = tsl.int(segmentsNode.add(3));
2424
- const vx = vIndex.mod(segmentEdges);
2425
- const vy = vIndex.div(segmentEdges);
2426
- const last = segmentEdges.sub(tsl.int(1));
2427
- return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
2428
- });
2429
- const isSkirtUV = tsl.Fn(([segments]) => {
2430
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
2431
- const ux = tsl.uv().x;
2432
- const uy = tsl.uv().y;
2433
- const segmentCount = segmentsNode.add(2);
2434
- const segmentStep = tsl.float(1).div(segmentCount);
2435
- const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
2436
- const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
2437
- return innerX.and(innerY).not();
2438
- });
2439
-
2440
- function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
2441
- 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();
2447
- const rootSize = terrainUniforms.uRootSize.toVar();
2448
- const rootOrigin = terrainUniforms.uRootOrigin.toVar();
2449
- const half = tsl.float(0.5);
2450
- const size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
2451
- 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);
2454
- const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
2455
- const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
2456
- const worldX = centerX.add(clampedX.mul(size));
2457
- const worldZ = centerZ.add(clampedZ.mul(size));
2458
- return tsl.vec3(worldX, rootOrigin.y, worldZ);
2459
- });
2460
- }
2461
- function createTileElevation(terrainUniforms, terrainFieldStorage) {
2462
- if (!terrainFieldStorage) return tsl.float(0);
2463
- 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) {
2474
- if (!terrainFieldStorage) return;
2475
- const nodeIndex = tsl.int(tsl.instanceIndex);
2476
- const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
2477
- const localVertexIndex = tsl.int(tsl.vertexIndex);
2478
- const ix = localVertexIndex.mod(edgeVertexCount);
2479
- 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
- );
2497
- return tsl.Fn(() => {
2498
- const base = baseWorldPosition();
2499
- const yElevation = createTileElevation(
2500
- terrainUniforms,
2501
- terrainFieldStorage
2502
- );
2503
- const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
2504
- const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
2505
- const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
2506
- createNormalAssignment(terrainUniforms, terrainFieldStorage);
2507
- return tsl.vec3(base.x, worldY, base.z);
2508
- })();
2509
- }
2510
-
2511
4214
  const positionNodeTask = work.task((get, work) => {
2512
4215
  const leafStorage = get(leafStorageTask);
2513
4216
  const terrainUniforms = get(updateUniformsTask);
2514
4217
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4218
+ const topology = get(topologyTask);
2515
4219
  return work(
2516
- () => createTileWorldPosition(
4220
+ () => topology.projection.gpu.renderVertexPosition({
2517
4221
  leafStorage,
2518
- terrainUniforms,
4222
+ uniforms: terrainUniforms,
2519
4223
  terrainFieldStorage
2520
- )
4224
+ })
2521
4225
  );
2522
4226
  }).displayName("positionNodeTask");
2523
4227
 
2524
- function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
2525
- let tMin = -Infinity;
2526
- let tMax = Infinity;
2527
- const origin = ray.origin;
2528
- const dir = ray.direction;
2529
- const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
2530
- if (Math.abs(dirAxis) < 1e-8) {
2531
- if (originAxis < minAxis || originAxis > maxAxis) return false;
2532
- return true;
2533
- }
2534
- const inv = 1 / dirAxis;
2535
- let t0 = (minAxis - originAxis) * inv;
2536
- let t1 = (maxAxis - originAxis) * inv;
2537
- if (t0 > t1) {
2538
- const tmp = t0;
2539
- t0 = t1;
2540
- t1 = tmp;
2541
- }
2542
- tMin = Math.max(tMin, t0);
2543
- tMax = Math.min(tMax, t1);
2544
- return tMax >= tMin;
2545
- };
2546
- if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
2547
- return null;
2548
- }
2549
- return { tMin, tMax };
2550
- }
2551
- function getTerrainBounds(config) {
2552
- const halfRoot = config.rootSize * 0.5;
2553
- return {
2554
- minX: config.originX - halfRoot,
2555
- maxX: config.originX + halfRoot,
2556
- minZ: config.originZ - halfRoot,
2557
- maxZ: config.originZ + halfRoot
2558
- };
2559
- }
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;
2568
- }
2569
- }
2570
- const elevation = query.getElevation(worldX, worldZ);
2571
- if (!Number.isFinite(elevation)) return void 0;
2572
- return worldY - elevation;
2573
- }
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();
2598
- let prevT = startT;
2599
- ray.at(prevT, point);
2600
- let prevSignedDistance = terrainSignedDistanceFromBounds(
2601
- query,
2602
- point.x,
2603
- point.y,
2604
- point.z
2605
- );
2606
- 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
- };
2615
- }
2616
- for (let i = 1; i <= maxSteps; i += 1) {
2617
- const t = startT + (endT - startT) * i / maxSteps;
2618
- ray.at(t, point);
2619
- const signedDistance = terrainSignedDistanceFromBounds(
2620
- query,
2621
- point.x,
2622
- point.y,
2623
- point.z
2624
- );
2625
- if (signedDistance === void 0) {
2626
- prevSignedDistance = void 0;
2627
- prevT = t;
2628
- continue;
2629
- }
2630
- if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
2631
- let lo = prevT;
2632
- let hi = t;
2633
- for (let r = 0; r < refinementSteps; r += 1) {
2634
- const mid = (lo + hi) * 0.5;
2635
- ray.at(mid, point);
2636
- const midDistance = terrainSignedDistancePrecise(
2637
- query,
2638
- point.x,
2639
- point.y,
2640
- point.z
2641
- );
2642
- if (midDistance === void 0) {
2643
- lo = mid;
2644
- continue;
2645
- }
2646
- if (midDistance > 0) lo = mid;
2647
- else hi = mid;
2648
- }
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
- };
2659
- }
2660
- prevSignedDistance = signedDistance;
2661
- prevT = t;
2662
- }
2663
- return null;
2664
- }
2665
- function cpuRaycastBoundsOnly(ray, config, options) {
2666
- const bounds = getTerrainBounds(config);
2667
- const planeY = (config.minY + config.maxY) * 0.5;
2668
- const dirY = ray.direction.y;
2669
- if (Math.abs(dirY) < 1e-8) return null;
2670
- const t = (planeY - ray.origin.y) / dirY;
2671
- if (t < 0) return null;
2672
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
2673
- if (t > maxDistance) return null;
2674
- const point = new three.Vector3();
2675
- ray.at(t, point);
2676
- if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
2677
- return null;
2678
- }
2679
- return {
2680
- position: point,
2681
- normal: new three.Vector3(0, 1, 0),
2682
- distance: ray.origin.distanceTo(point)
2683
- };
2684
- }
2685
-
2686
4228
  function createTerrainRaycast(params) {
2687
4229
  return {
2688
4230
  pick(ray, options) {
2689
- const config = params.getConfig();
2690
- const terrainQuery = params.getTerrainQuery();
2691
- if (terrainQuery) {
2692
- const precise = cpuRaycast(terrainQuery, ray, config, options);
2693
- if (precise) return precise;
2694
- }
2695
- const coarse = cpuRaycastBoundsOnly(ray, config, options);
2696
- if (coarse && terrainQuery) {
2697
- const sample = terrainQuery.sampleTerrain(
2698
- coarse.position.x,
2699
- coarse.position.z
2700
- );
2701
- if (sample.valid) {
2702
- coarse.position.y = sample.elevation;
2703
- coarse.normal.copy(sample.normal);
2704
- }
2705
- }
2706
- return coarse;
4231
+ const projection = params.getProjection();
4232
+ return projection.cpu.raycast({
4233
+ ray,
4234
+ options,
4235
+ terrainQuery: params.getTerrainQuery(),
4236
+ surfaceQuery: params.getSurfaceQuery(),
4237
+ sphereQuery: params.getSphereQuery(),
4238
+ config: params.getConfig()
4239
+ });
2707
4240
  }
2708
4241
  };
2709
4242
  }
@@ -2712,42 +4245,60 @@ const BOUNDS_PADDING = 1;
2712
4245
  const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
2713
4246
  const terrainRaycastTask = work.task(
2714
4247
  (get, work) => {
2715
- const { query: terrainQuery } = get(terrainQueryTask);
4248
+ const { query: terrainQuery, surfaceQuery, sphereQuery } = get(terrainQueryTask);
2716
4249
  const rootSizeValue = get(rootSize);
2717
4250
  const originValue = get(origin);
2718
4251
  const elevationScaleValue = get(elevationScale);
4252
+ const projection = get(topologyTask).projection;
2719
4253
  return work((prev) => {
2720
4254
  let raycast = prev;
2721
4255
  let state = raycast?.[RAYCAST_STATE];
2722
4256
  if (!state) {
2723
4257
  state = {
4258
+ projection,
2724
4259
  terrainQuery: null,
2725
- bounds: {
4260
+ surfaceQuery: null,
4261
+ sphereQuery: null,
4262
+ config: {
2726
4263
  rootSize: 0,
2727
4264
  originX: 0,
4265
+ originY: 0,
2728
4266
  originZ: 0,
2729
4267
  minY: 0,
2730
- maxY: 0
4268
+ maxY: 0,
4269
+ centerX: 0,
4270
+ centerY: 0,
4271
+ centerZ: 0
2731
4272
  }
2732
4273
  };
2733
4274
  }
4275
+ state.projection = projection;
2734
4276
  state.terrainQuery = terrainQuery;
2735
- state.bounds.rootSize = rootSizeValue;
2736
- state.bounds.originX = originValue.x;
2737
- state.bounds.originZ = originValue.z;
4277
+ state.surfaceQuery = surfaceQuery;
4278
+ state.sphereQuery = sphereQuery;
4279
+ state.config.rootSize = rootSizeValue;
4280
+ state.config.originX = originValue.x;
4281
+ state.config.originY = originValue.y;
4282
+ state.config.originZ = originValue.z;
4283
+ state.config.centerX = projection.center?.x ?? originValue.x;
4284
+ state.config.centerY = projection.center?.y ?? originValue.y;
4285
+ state.config.centerZ = projection.center?.z ?? originValue.z;
2738
4286
  const range = terrainQuery.getGlobalElevationRange();
2739
4287
  if (range) {
2740
- state.bounds.minY = range.min - BOUNDS_PADDING;
2741
- state.bounds.maxY = range.max + BOUNDS_PADDING;
4288
+ state.config.minY = range.min - BOUNDS_PADDING;
4289
+ state.config.maxY = range.max + BOUNDS_PADDING;
2742
4290
  } else {
2743
4291
  const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
2744
- state.bounds.minY = originValue.y - verticalExtent;
2745
- state.bounds.maxY = originValue.y + verticalExtent;
4292
+ state.config.minY = originValue.y - verticalExtent;
4293
+ state.config.maxY = originValue.y + verticalExtent;
2746
4294
  }
2747
4295
  if (!raycast) {
2748
4296
  raycast = createTerrainRaycast({
4297
+ getProjection: () => state.projection,
2749
4298
  getTerrainQuery: () => state.terrainQuery,
2750
- getConfig: () => state.bounds
4299
+ getSurfaceQuery: () => state.surfaceQuery,
4300
+ getSphereQuery: () => state.sphereQuery,
4301
+ getConfig: () => state.config
2751
4302
  });
2752
4303
  }
2753
4304
  raycast[RAYCAST_STATE] = state;
@@ -2756,15 +4307,12 @@ const terrainRaycastTask = work.task(
2756
4307
  }
2757
4308
  ).displayName("terrainRaycastTask");
2758
4309
 
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
4310
  const terrainTasks = {
2763
4311
  instanceId: instanceIdTask,
2764
4312
  quadtreeConfig: quadtreeConfigTask,
2765
4313
  quadtreeUpdate: quadtreeUpdateTask,
2766
4314
  leafStorage: leafStorageTask,
2767
- surface: surfaceTask,
4315
+ topology: topologyTask,
2768
4316
  leafGpuBuffer: leafGpuBufferTask,
2769
4317
  gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2770
4318
  gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
@@ -2785,6 +4333,13 @@ const terrainTasks = {
2785
4333
  terrainReadback: terrainReadbackTask,
2786
4334
  terrainRaycast: terrainRaycastTask
2787
4335
  };
4336
+ function terrainGraph() {
4337
+ const g = work.graph();
4338
+ for (const t of Object.values(terrainTasks)) {
4339
+ g.add(t);
4340
+ }
4341
+ return g;
4342
+ }
2788
4343
 
2789
4344
  const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
2790
4345
  return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
@@ -2840,23 +4395,27 @@ const voronoiCells = tsl.Fn((params) => {
2840
4395
 
2841
4396
  exports.ArrayTextureBackend = ArrayTextureBackend;
2842
4397
  exports.AtlasBackend = AtlasBackend;
4398
+ exports.CUBE_FACES = CUBE_FACES;
4399
+ exports.CUBE_FACE_COUNT = CUBE_FACE_COUNT;
2843
4400
  exports.Dir = Dir;
2844
4401
  exports.TerrainGeometry = TerrainGeometry;
2845
4402
  exports.TerrainMesh = TerrainMesh;
2846
- exports.Texture3DBackend = Texture3DBackend;
2847
4403
  exports.U32_EMPTY = U32_EMPTY;
2848
4404
  exports.allocLeafSet = allocLeafSet;
2849
4405
  exports.allocSeamTable = allocSeamTable;
4406
+ exports.augmentCubeSphereSampler = augmentCubeSphereSampler;
2850
4407
  exports.beginUpdate = beginUpdate;
2851
4408
  exports.blendAngleCorrectedNormals = blendAngleCorrectedNormals;
2852
4409
  exports.buildLeafIndex = buildLeafIndex;
2853
4410
  exports.buildSeams2to1 = buildSeams2to1;
2854
4411
  exports.compileComputeTask = compileComputeTask;
2855
4412
  exports.createComputePipelineTasks = createComputePipelineTasks;
2856
- exports.createCubeSphereSurface = createCubeSphereSurface;
4413
+ exports.createCubeSphereProjection = createCubeSphereProjection;
4414
+ exports.createCubeSphereTopology = createCubeSphereTopology;
2857
4415
  exports.createElevationFieldContextTask = createElevationFieldContextTask;
2858
- exports.createFlatSurface = createFlatSurface;
2859
- exports.createInfiniteFlatSurface = createInfiniteFlatSurface;
4416
+ exports.createFlatProjection = createFlatProjection;
4417
+ exports.createFlatTopology = createFlatTopology;
4418
+ exports.createInfiniteFlatTopology = createInfiniteFlatTopology;
2860
4419
  exports.createSpatialIndex = createSpatialIndex;
2861
4420
  exports.createState = createState;
2862
4421
  exports.createTerrainFieldStorage = createTerrainFieldStorage;
@@ -2865,13 +4424,25 @@ exports.createTerrainQuery = createTerrainQuery;
2865
4424
  exports.createTerrainRaycast = createTerrainRaycast;
2866
4425
  exports.createTerrainSampler = createTerrainSampler;
2867
4426
  exports.createTerrainSamplerTask = createTerrainSamplerTask;
4427
+ exports.createTerrainSurfaceQuery = createTerrainSurfaceQuery;
2868
4428
  exports.createTerrainUniforms = createTerrainUniforms;
4429
+ exports.createTorusProjection = createTorusProjection;
4430
+ exports.createTorusTopology = createTorusTopology;
2869
4431
  exports.createUniformsTask = createUniformsTask;
4432
+ exports.cubeFaceBasis = cubeFaceBasis;
4433
+ exports.cubeFaceDirection = cubeFaceDirection;
4434
+ exports.cubeFaceFromDirection = cubeFaceFromDirection;
4435
+ exports.cubeFacePoint = cubeFacePoint;
4436
+ exports.cubeFaceUVFromDirection = cubeFaceUVFromDirection;
2870
4437
  exports.deriveNormalZ = deriveNormalZ;
4438
+ exports.directionToFace = directionToFace;
4439
+ exports.directionToFaceUV = directionToFaceUV;
4440
+ exports.directionToLatLong = directionToLatLong;
2871
4441
  exports.elevationFieldStageTask = elevationFieldStageTask;
2872
4442
  exports.elevationFn = elevationFn;
2873
4443
  exports.elevationScale = elevationScale;
2874
4444
  exports.executeComputeTask = executeComputeTask;
4445
+ exports.faceUVToCube = faceUVToCube;
2875
4446
  exports.getDeviceComputeLimits = getDeviceComputeLimits;
2876
4447
  exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
2877
4448
  exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
@@ -2879,6 +4450,7 @@ exports.innerTileSegments = innerTileSegments;
2879
4450
  exports.instanceIdTask = instanceIdTask;
2880
4451
  exports.isSkirtUV = isSkirtUV;
2881
4452
  exports.isSkirtVertex = isSkirtVertex;
4453
+ exports.latLongToDirection = latLongToDirection;
2882
4454
  exports.leafGpuBufferTask = leafGpuBufferTask;
2883
4455
  exports.leafStorageTask = leafStorageTask;
2884
4456
  exports.loadTerrainField = loadTerrainField;
@@ -2889,19 +4461,20 @@ exports.maxNodes = maxNodes;
2889
4461
  exports.origin = origin;
2890
4462
  exports.packTerrainFieldSample = packTerrainFieldSample;
2891
4463
  exports.positionNodeTask = positionNodeTask;
4464
+ exports.positionToTorusParams = positionToTorusParams;
2892
4465
  exports.quadtreeConfigTask = quadtreeConfigTask;
2893
4466
  exports.quadtreeUpdate = quadtreeUpdate;
2894
4467
  exports.quadtreeUpdateTask = quadtreeUpdateTask;
4468
+ exports.radius = radius;
2895
4469
  exports.resetLeafSet = resetLeafSet;
2896
4470
  exports.resetSeamTable = resetSeamTable;
2897
4471
  exports.rootSize = rootSize;
2898
4472
  exports.sampleTerrainField = sampleTerrainField;
2899
4473
  exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
2900
- exports.sampleTerrainFieldNormal = sampleTerrainFieldNormal;
2901
4474
  exports.skirtScale = skirtScale;
4475
+ exports.sphereTangentFrameNormal = sphereTangentFrameNormal;
2902
4476
  exports.storeTerrainField = storeTerrainField;
2903
- exports.surface = surface;
2904
- exports.surfaceTask = surfaceTask;
4477
+ exports.tangentFromAxis = tangentFromAxis;
2905
4478
  exports.terrainFieldFilter = terrainFieldFilter;
2906
4479
  exports.terrainFieldStageTask = terrainFieldStageTask;
2907
4480
  exports.terrainGraph = terrainGraph;
@@ -2911,9 +4484,15 @@ exports.terrainReadbackTask = terrainReadbackTask;
2911
4484
  exports.terrainTasks = terrainTasks;
2912
4485
  exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
2913
4486
  exports.tileNodesTask = tileNodesTask;
4487
+ exports.topology = topology;
4488
+ exports.topologyTask = topologyTask;
4489
+ exports.torusOutwardNormal = torusOutwardNormal$1;
4490
+ exports.torusUVToPoint = torusUVToPoint;
4491
+ exports.unpackTangentNormal = unpackTangentNormal;
2914
4492
  exports.update = update;
2915
4493
  exports.updateUniformsTask = updateUniformsTask;
2916
4494
  exports.vElevation = vElevation;
2917
4495
  exports.vGlobalVertexIndex = vGlobalVertexIndex;
2918
4496
  exports.vectorSpaceToTextureSpace = vectorSpaceToTextureSpace;
2919
4497
  exports.voronoiCells = voronoiCells;
4498
+ exports.wrap01 = wrap01;