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