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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,29 +1,31 @@
1
- import { BufferGeometry, BufferAttribute } from 'three';
2
- import { Fn, int, vertexIndex, uv, float } from 'three/tsl';
1
+ import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 } from 'three';
2
+ import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 as Vector3$1 } from 'three/webgpu';
3
+ import { param, task, graph } from '@hello-terrain/work';
4
+ import { float, uniform, Fn, globalId, int, vec2, uint, If, textureStore, uvec3, vec4, texture, ivec2, ivec3, textureLoad, select, vec3, pow, storage, workgroupArray, localId, workgroupId, min, Loop, max, workgroupBarrier, Break, vertexIndex, uv, instanceIndex, positionLocal, normalLocal, remap, dot as dot$1, varyingProperty, mx_noise_float, mix } from 'three/tsl';
5
+ import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
3
6
 
4
7
  class TerrainGeometry extends BufferGeometry {
5
- 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) {
6
16
  super();
7
17
  if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
8
- throw new Error(
9
- `Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
10
- );
18
+ throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
11
19
  }
12
20
  try {
13
- this.setIndex(this.generateIndices(innerSegments));
21
+ this.setIndex(this.generateIndices(innerSegments, flipWinding));
14
22
  this.setAttribute(
15
23
  "position",
16
- new BufferAttribute(
17
- new Float32Array(this.generatePositions(innerSegments)),
18
- 3
19
- )
24
+ new BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
20
25
  );
21
26
  this.setAttribute(
22
27
  "normal",
23
- new BufferAttribute(
24
- new Float32Array(this.generateNormals(innerSegments)),
25
- 3
26
- )
28
+ new BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
27
29
  );
28
30
  this.setAttribute(
29
31
  "uv",
@@ -59,17 +61,19 @@ class TerrainGeometry extends BufferGeometry {
59
61
  * | / | \ | / | \ |
60
62
  * o---o---o---o---o
61
63
  *
62
- * INNER GRID (consistent diagonal, no rotational symmetry):
63
- * o---o---o
64
- * | \ | \ |
65
- * o---o---o
66
- * | \ | \ |
67
- * o---o---o
64
+ * INNER GRID (alternating diagonals checkerboard pattern):
65
+ * o---o---o---o---o
66
+ * | \ | / | \ | / |
67
+ * o---o---o---o---o
68
+ * | / | \ | / | \ |
69
+ * o---o---o---o---o
70
+ * | \ | / | \ | / |
71
+ * o---o---o---o---o
68
72
  *
69
73
  * Where o = vertex
70
74
  * Each square cell is split into 2 triangles.
71
75
  * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
72
- * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
76
+ * - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
73
77
  *
74
78
  * Vertex layout (for innerSegments = 2):
75
79
  *
@@ -96,12 +100,16 @@ class TerrainGeometry extends BufferGeometry {
96
100
  * triangle 1: a, c, b
97
101
  * triangle 2: b, c, d
98
102
  */
99
- generateIndices(innerSegments) {
103
+ generateIndices(innerSegments, flipWinding = false) {
100
104
  const innerEdgeVertexCount = innerSegments + 1;
101
105
  const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
102
106
  const indices = [];
103
107
  const cellsPerEdge = edgeVertexCountWithSkirt - 1;
104
108
  const mid = Math.floor(cellsPerEdge / 2);
109
+ const pushTri = (v0, v1, v2) => {
110
+ if (flipWinding) indices.push(v0, v2, v1);
111
+ else indices.push(v0, v1, v2);
112
+ };
105
113
  for (let y = 0; y < cellsPerEdge; y++) {
106
114
  for (let x = 0; x < cellsPerEdge; x++) {
107
115
  const a = y * edgeVertexCountWithSkirt + x;
@@ -115,14 +123,14 @@ class TerrainGeometry extends BufferGeometry {
115
123
  const topHalf = y < mid;
116
124
  useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
117
125
  } else {
118
- useDefaultDiagonal = true;
126
+ useDefaultDiagonal = (x + y) % 2 === 0;
119
127
  }
120
128
  if (useDefaultDiagonal) {
121
- indices.push(a, d, b);
122
- indices.push(a, c, d);
129
+ pushTri(a, d, b);
130
+ pushTri(a, c, d);
123
131
  } else {
124
- indices.push(a, c, b);
125
- indices.push(b, c, d);
132
+ pushTri(a, c, b);
133
+ pushTri(b, c, d);
126
134
  }
127
135
  }
128
136
  }
@@ -213,6 +221,2832 @@ class TerrainGeometry extends BufferGeometry {
213
221
  }
214
222
  }
215
223
 
224
+ const rootSize = param(256).displayName("rootSize");
225
+ const origin = param({
226
+ x: 0,
227
+ y: 0,
228
+ z: 0
229
+ }).displayName("origin");
230
+ const innerTileSegments = param(61).displayName("innerTileSegments");
231
+ const skirtScale = param(100).displayName("skirtScale");
232
+ const elevationScale = param(1).displayName("elevationScale");
233
+ const radius = param(1e3).displayName("radius");
234
+ const maxNodes = param(1024).displayName("maxNodes");
235
+ const maxLevel = param(16).displayName("maxLevel");
236
+ const quadtreeUpdate = param({
237
+ cameraOrigin: { x: 0, y: 0, z: 0 },
238
+ mode: "distance",
239
+ distanceFactor: 1.5
240
+ }).displayName("quadtreeUpdate");
241
+ const topology = param(null).displayName("topology");
242
+ const terrainFieldFilter = param("linear").displayName(
243
+ "terrainFieldFilter"
244
+ );
245
+ const elevationFn = param(() => float(0));
246
+
247
+ const defaultTerrainMeshParams = {
248
+ // Source of truth is the `innerTileSegments` param itself.
249
+ innerTileSegments: innerTileSegments.get(),
250
+ maxNodes: 1024,
251
+ material: new MeshStandardNodeMaterial(),
252
+ flipWinding: false
253
+ };
254
+ class TerrainMesh extends InstancedMesh {
255
+ _innerTileSegments;
256
+ _maxNodes;
257
+ _flipWinding;
258
+ terrainRaycast = null;
259
+ constructor(params = defaultTerrainMeshParams) {
260
+ const mergedParams = { ...defaultTerrainMeshParams, ...params };
261
+ const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
262
+ const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
263
+ super(geometry, material, maxNodes);
264
+ this.frustumCulled = false;
265
+ this._innerTileSegments = innerTileSegments;
266
+ this._maxNodes = maxNodes;
267
+ this._flipWinding = flipWinding;
268
+ }
269
+ get innerTileSegments() {
270
+ return this._innerTileSegments;
271
+ }
272
+ set innerTileSegments(tileSegments) {
273
+ if (tileSegments === this._innerTileSegments) return;
274
+ const oldGeometry = this.geometry;
275
+ this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
276
+ this._innerTileSegments = tileSegments;
277
+ setTimeout(() => oldGeometry.dispose());
278
+ }
279
+ get flipWinding() {
280
+ return this._flipWinding;
281
+ }
282
+ set flipWinding(flip) {
283
+ if (flip === this._flipWinding) return;
284
+ const oldGeometry = this.geometry;
285
+ this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
286
+ this._flipWinding = flip;
287
+ setTimeout(() => oldGeometry.dispose());
288
+ }
289
+ get maxNodes() {
290
+ return this._maxNodes;
291
+ }
292
+ set maxNodes(maxNodes) {
293
+ if (!Number.isInteger(maxNodes) || maxNodes < 1) {
294
+ throw new Error(`Invalid maxNodes: ${maxNodes}. Must be a positive integer.`);
295
+ }
296
+ if (maxNodes === this._maxNodes) return;
297
+ const oldMax = this._maxNodes;
298
+ const nextMatrix = new Float32Array(maxNodes * 16);
299
+ const oldMatrixArray = this.instanceMatrix.array;
300
+ nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
301
+ this.instanceMatrix = new InstancedBufferAttribute(nextMatrix, 16);
302
+ if (this.instanceColor) {
303
+ const itemSize = this.instanceColor.itemSize;
304
+ const nextColor = new Float32Array(maxNodes * itemSize);
305
+ const oldColorArray = this.instanceColor.array;
306
+ nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
307
+ this.instanceColor = new InstancedBufferAttribute(nextColor, itemSize);
308
+ }
309
+ this._maxNodes = maxNodes;
310
+ this.count = Math.min(this.count, maxNodes);
311
+ this.instanceMatrix.needsUpdate = true;
312
+ if (this.instanceColor) this.instanceColor.needsUpdate = true;
313
+ if (maxNodes < oldMax && this.count >= maxNodes) {
314
+ this.count = maxNodes;
315
+ }
316
+ }
317
+ raycast(raycaster, intersects) {
318
+ if (!this.terrainRaycast) {
319
+ super.raycast(raycaster, intersects);
320
+ return;
321
+ }
322
+ const result = this.terrainRaycast.pick(raycaster.ray);
323
+ if (!result) return;
324
+ intersects.push({
325
+ distance: result.distance,
326
+ point: result.position.clone(),
327
+ normal: result.normal.clone(),
328
+ object: this,
329
+ face: null,
330
+ faceIndex: -1
331
+ });
332
+ }
333
+ }
334
+
335
+ function getDeviceComputeLimits(renderer) {
336
+ const backend = renderer.backend;
337
+ const limits = backend?.device?.limits;
338
+ return {
339
+ maxWorkgroupSizeX: limits?.maxComputeWorkgroupSizeX ?? 256,
340
+ maxWorkgroupSizeY: limits?.maxComputeWorkgroupSizeY ?? 256,
341
+ maxWorkgroupInvocations: limits?.maxComputeWorkgroupInvocations ?? 256
342
+ };
343
+ }
344
+
345
+ const WORKGROUP_X = 16;
346
+ const WORKGROUP_Y = 16;
347
+ function compileComputePipeline(stages, width, options) {
348
+ const bindings = options?.bindings;
349
+ const preferredWorkgroup = options?.workgroupSize ?? [
350
+ WORKGROUP_X,
351
+ WORKGROUP_Y
352
+ ];
353
+ const uInstanceCount = uniform(0, "uint");
354
+ const stagedKernelCache = /* @__PURE__ */ new Map();
355
+ function clampWorkgroupToLimits(requested, limits) {
356
+ let x = Math.max(1, Math.floor(requested[0]));
357
+ let y = Math.max(1, Math.floor(requested[1]));
358
+ x = Math.min(x, limits.maxWorkgroupSizeX);
359
+ y = Math.min(y, limits.maxWorkgroupSizeY);
360
+ y = Math.min(
361
+ y,
362
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / x))
363
+ );
364
+ x = Math.min(
365
+ x,
366
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / y))
367
+ );
368
+ return [x, y];
369
+ }
370
+ function buildStagedKernels(workgroupSize) {
371
+ return stages.map(
372
+ (stage) => Fn(() => {
373
+ bindings?.forEach((b) => b.toVar());
374
+ const fWidth = float(width);
375
+ const activeIndex = globalId.z;
376
+ const nodeIndex = int(activeIndex).toVar();
377
+ const iWidth = int(width);
378
+ const ix = int(globalId.x);
379
+ const iy = int(globalId.y);
380
+ const texelSize = vec2(1, 1).div(fWidth);
381
+ const localCoordinates = vec2(globalId.x, globalId.y);
382
+ const localUVCoords = localCoordinates.div(fWidth);
383
+ const verticesPerNode = iWidth.mul(iWidth);
384
+ const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
385
+ const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
386
+ If(inBounds, () => {
387
+ stage(
388
+ nodeIndex,
389
+ globalIndex,
390
+ localUVCoords,
391
+ localCoordinates,
392
+ texelSize
393
+ );
394
+ });
395
+ })().computeKernel(workgroupSize)
396
+ );
397
+ }
398
+ function execute(renderer, instanceCount) {
399
+ const limits = getDeviceComputeLimits(renderer);
400
+ uInstanceCount.value = instanceCount;
401
+ const [workgroupX, workgroupY] = clampWorkgroupToLimits(
402
+ preferredWorkgroup,
403
+ limits
404
+ );
405
+ const cacheKey = `${workgroupX}x${workgroupY}`;
406
+ let stagedKernels = stagedKernelCache.get(cacheKey);
407
+ if (!stagedKernels) {
408
+ stagedKernels = buildStagedKernels([workgroupX, workgroupY, 1]);
409
+ stagedKernelCache.set(cacheKey, stagedKernels);
410
+ }
411
+ const dispatchX = Math.ceil(width / workgroupX);
412
+ const dispatchY = Math.ceil(width / workgroupY);
413
+ for (const kernel of stagedKernels) {
414
+ renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
415
+ }
416
+ }
417
+ return { execute };
418
+ }
419
+
420
+ function resolveType(format) {
421
+ return format === "rgba16float" ? HalfFloatType : FloatType;
422
+ }
423
+ function resolveFilter(mode) {
424
+ return mode === "linear" ? LinearFilter : NearestFilter;
425
+ }
426
+ function configureStorageTexture(texture2, format, filter) {
427
+ texture2.format = RGBAFormat;
428
+ texture2.type = resolveType(format);
429
+ texture2.magFilter = resolveFilter(filter);
430
+ texture2.minFilter = resolveFilter(filter);
431
+ texture2.wrapS = ClampToEdgeWrapping;
432
+ texture2.wrapT = ClampToEdgeWrapping;
433
+ texture2.generateMipmaps = false;
434
+ texture2.needsUpdate = true;
435
+ }
436
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
437
+ let currentEdgeVertexCount = edgeVertexCount;
438
+ let currentTileCount = tileCount;
439
+ const tex = new StorageArrayTexture(
440
+ edgeVertexCount,
441
+ edgeVertexCount,
442
+ tileCount
443
+ );
444
+ configureStorageTexture(tex, options.format, options.filter);
445
+ return {
446
+ backendType: "array-texture",
447
+ get edgeVertexCount() {
448
+ return currentEdgeVertexCount;
449
+ },
450
+ get tileCount() {
451
+ return currentTileCount;
452
+ },
453
+ texture: tex,
454
+ uv(ix, iy, _tileIndex) {
455
+ return vec2(ix.toFloat(), iy.toFloat());
456
+ },
457
+ texel(ix, iy, tileIndex) {
458
+ return ivec3(ix, iy, tileIndex);
459
+ },
460
+ sample(u, v, tileIndex) {
461
+ return texture(tex, vec2(u, v)).depth(int(tileIndex));
462
+ },
463
+ resize(width, height, nextTileCount) {
464
+ currentEdgeVertexCount = width;
465
+ currentTileCount = nextTileCount;
466
+ tex.setSize(width, height, nextTileCount);
467
+ tex.needsUpdate = true;
468
+ }
469
+ };
470
+ }
471
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
472
+ const tilesPerRowNode = int(tilesPerRow);
473
+ const edge = int(edgeVertexCount);
474
+ const tile = int(tileIndex);
475
+ const col = tile.mod(tilesPerRowNode);
476
+ const row = tile.div(tilesPerRowNode);
477
+ const atlasX = col.mul(edge).add(int(ix));
478
+ const atlasY = row.mul(edge).add(int(iy));
479
+ return { atlasX, atlasY };
480
+ }
481
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
482
+ let currentEdgeVertexCount = edgeVertexCount;
483
+ let currentTileCount = tileCount;
484
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
485
+ const atlasSize = tilesPerRow * edgeVertexCount;
486
+ const tex = new StorageTexture(atlasSize, atlasSize);
487
+ configureStorageTexture(tex, options.format, options.filter);
488
+ return {
489
+ backendType: "atlas",
490
+ get edgeVertexCount() {
491
+ return currentEdgeVertexCount;
492
+ },
493
+ get tileCount() {
494
+ return currentTileCount;
495
+ },
496
+ texture: tex,
497
+ uv(ix, iy, tileIndex) {
498
+ const { atlasX, atlasY } = atlasCoord(
499
+ tilesPerRow,
500
+ currentEdgeVertexCount,
501
+ ix,
502
+ iy,
503
+ tileIndex
504
+ );
505
+ const currentAtlasSize = float(tilesPerRow * currentEdgeVertexCount);
506
+ return vec2(
507
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
508
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
509
+ );
510
+ },
511
+ texel(ix, iy, tileIndex) {
512
+ const { atlasX, atlasY } = atlasCoord(
513
+ tilesPerRow,
514
+ currentEdgeVertexCount,
515
+ ix,
516
+ iy,
517
+ tileIndex
518
+ );
519
+ return ivec2(atlasX, atlasY);
520
+ },
521
+ sample(u, v, tileIndex) {
522
+ const tile = int(tileIndex);
523
+ const tilesPerRowNode = int(tilesPerRow);
524
+ const col = tile.mod(tilesPerRowNode);
525
+ const row = tile.div(tilesPerRowNode);
526
+ const invTilesPerRow = float(1 / tilesPerRow);
527
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
528
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
529
+ return texture(tex, vec2(atlasU, atlasV));
530
+ },
531
+ resize(width, height, nextTileCount) {
532
+ currentEdgeVertexCount = width;
533
+ currentTileCount = nextTileCount;
534
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
535
+ const nextAtlasSize = tilesPerRow * width;
536
+ const image = tex.image;
537
+ image.width = nextAtlasSize;
538
+ image.height = nextAtlasSize;
539
+ tex.needsUpdate = true;
540
+ }
541
+ };
542
+ }
543
+ function texture3DBackend(edgeVertexCount, tileCount, options) {
544
+ const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
545
+ return { ...storage, backendType: "texture-3d" };
546
+ }
547
+ function tryGetDeviceLimits(renderer) {
548
+ const backend = renderer;
549
+ return backend.backend?.device?.limits ?? {};
550
+ }
551
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
552
+ const filter = options.filter ?? "linear";
553
+ const format = options.format ?? "rgba16float";
554
+ const forcedBackend = options.backend;
555
+ if (forcedBackend === "atlas") {
556
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
557
+ }
558
+ if (forcedBackend === "texture-3d") {
559
+ return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
560
+ }
561
+ if (forcedBackend === "array-texture") {
562
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
563
+ }
564
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
565
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
566
+ if (tileCount > maxLayers) {
567
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
568
+ }
569
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
570
+ }
571
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
572
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
573
+ return textureStore(
574
+ storage.texture,
575
+ uvec3(int(ix), int(iy), int(tileIndex)),
576
+ value
577
+ );
578
+ }
579
+ return textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
580
+ }
581
+ function loadTerrainField(storage, ix, iy, tileIndex) {
582
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
583
+ return textureLoad(storage.texture, ivec2(int(ix), int(iy)), int(0)).depth(
584
+ int(tileIndex)
585
+ );
586
+ }
587
+ return textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), int(0));
588
+ }
589
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
590
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
591
+ }
592
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
593
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
594
+ return vec2(raw.g, raw.b);
595
+ }
596
+ function sampleTerrainField(storage, u, v, tileIndex) {
597
+ return storage.sample(u, v, tileIndex);
598
+ }
599
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
600
+ return sampleTerrainField(storage, u, v, tileIndex).r;
601
+ }
602
+ function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
603
+ return vec4(height, normalXZ.x, normalXZ.y, extra);
604
+ }
605
+
606
+ const createElevation = (tile, uniforms, elevationFn) => {
607
+ return function perVertexElevation(nodeIndex, localCoordinates) {
608
+ const ix = int(localCoordinates.x);
609
+ const iy = int(localCoordinates.y);
610
+ const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(int(3));
611
+ const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
612
+ const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
613
+ const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
614
+ const rootSize = uniforms.uRootSize.toVar();
615
+ return elevationFn({
616
+ worldPosition,
617
+ rootSize,
618
+ rootUV,
619
+ tileOriginVec2: tile.tileOriginVec2(nodeIndex),
620
+ tileSize: tile.tileSize(nodeIndex),
621
+ tileLevel: tile.tileLevel(nodeIndex),
622
+ nodeIndex: int(nodeIndex),
623
+ tileUV
624
+ });
625
+ };
626
+ };
627
+
628
+ const CUBE_FACE_COUNT = 6;
629
+ const CUBE_FACES = [
630
+ // 0: +X
631
+ { forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
632
+ // 1: -X
633
+ { forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
634
+ // 2: +Y (north pole)
635
+ { forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
636
+ // 3: -Y (south pole)
637
+ { forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
638
+ // 4: +Z
639
+ { forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
640
+ // 5: -Z
641
+ { forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
642
+ ];
643
+
644
+ function vec3Const(v) {
645
+ return vec3(float(v[0]), float(v[1]), float(v[2]));
646
+ }
647
+ function selectFaceVec3(face, pick) {
648
+ const last = CUBE_FACES.length - 1;
649
+ let acc = vec3Const(pick(CUBE_FACES[last]));
650
+ for (let i = last - 1; i >= 0; i--) {
651
+ acc = select(int(face).equal(int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
652
+ }
653
+ return acc;
654
+ }
655
+ function cubeFaceBasis(face) {
656
+ return {
657
+ forward: selectFaceVec3(face, (f) => f.forward),
658
+ right: selectFaceVec3(face, (f) => f.right),
659
+ up: selectFaceVec3(face, (f) => f.up)
660
+ };
661
+ }
662
+ function cubeFacePoint(basis, u, v) {
663
+ const s = float(u).mul(2).sub(1);
664
+ const t = float(v).mul(2).sub(1);
665
+ return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
666
+ }
667
+ function cubeFaceDirection(basis, u, v) {
668
+ return cubeFacePoint(basis, u, v).normalize();
669
+ }
670
+ function tangentFromAxis(dir, axis) {
671
+ return axis.sub(dir.mul(dir.dot(axis))).normalize();
672
+ }
673
+ function unpackTangentNormal(nx, nz) {
674
+ const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0)).sqrt();
675
+ return vec3(nx, ny, nz);
676
+ }
677
+ function sphereTangentFrameNormal(dir, basis, tangentNormal) {
678
+ const n = vec3(tangentNormal);
679
+ const tu = tangentFromAxis(dir, basis.right);
680
+ const tv = tangentFromAxis(dir, basis.up);
681
+ return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
682
+ }
683
+ function cubeFaceFromDirection(dir) {
684
+ const d = vec3(dir);
685
+ const ax = d.x.abs();
686
+ const ay = d.y.abs();
687
+ const az = d.z.abs();
688
+ const faceX = select(d.x.greaterThanEqual(float(0)), int(0), int(1));
689
+ const faceY = select(d.y.greaterThanEqual(float(0)), int(2), int(3));
690
+ const faceZ = select(d.z.greaterThanEqual(float(0)), int(4), int(5));
691
+ const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
692
+ const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
693
+ return select(xDominant, faceX, select(yDominant, faceY, faceZ));
694
+ }
695
+ function cubeFaceUVFromDirection(basis, dir) {
696
+ const d = vec3(dir);
697
+ const p = d.div(d.dot(basis.forward));
698
+ const s = p.dot(basis.right);
699
+ const t = p.dot(basis.up);
700
+ return vec2(s.add(float(1)).mul(float(0.5)), t.add(float(1)).mul(float(0.5)));
701
+ }
702
+
703
+ const HALF_PI = Math.PI * 0.5;
704
+ const FIELD_INNER_TEXEL_OFFSET = 1.5;
705
+ const FIELD_EDGE_EXTRA_TEXELS = 3;
706
+ function sphereTileArcLength(radius, levelDivisor) {
707
+ return radius * HALF_PI / levelDivisor;
708
+ }
709
+ function decodeLeafTile(leafStorage, nodeIndex) {
710
+ const nodeOffset = int(nodeIndex).mul(int(4));
711
+ return {
712
+ level: leafStorage.node.element(nodeOffset).toInt(),
713
+ x: leafStorage.node.element(nodeOffset.add(int(1))).toFloat(),
714
+ y: leafStorage.node.element(nodeOffset.add(int(2))).toFloat(),
715
+ face: leafStorage.node.element(nodeOffset.add(int(3))).toInt()
716
+ };
717
+ }
718
+ function faceUVFromTileLocal(tile, localU, localV) {
719
+ const n = pow(float(2), tile.level.toFloat());
720
+ return vec2(tile.x.add(localU).div(n), tile.y.add(localV).div(n));
721
+ }
722
+ function createTileCompute(leafStorage, uniforms, projection = "flat") {
723
+ const isSphere = projection === "cubeSphere";
724
+ const tileLevel = Fn(([nodeIndex]) => {
725
+ return decodeLeafTile(leafStorage, nodeIndex).level;
726
+ });
727
+ const tileFace = Fn(([nodeIndex]) => {
728
+ return decodeLeafTile(leafStorage, nodeIndex).face;
729
+ });
730
+ const tileOriginVec2 = Fn(([nodeIndex]) => {
731
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
732
+ return vec2(tile.x, tile.y);
733
+ });
734
+ const tileSize = Fn(([nodeIndex]) => {
735
+ const level = tileLevel(nodeIndex);
736
+ const divisor = pow(float(2), level.toFloat());
737
+ if (isSphere) {
738
+ return uniforms.uRadius.toVar().mul(float(HALF_PI)).div(divisor);
739
+ }
740
+ const rootSize = uniforms.uRootSize.toVar();
741
+ return float(rootSize).div(divisor);
742
+ });
743
+ const tileFaceUV = Fn(([nodeIndex, ix, iy]) => {
744
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
745
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
746
+ const localU = int(ix).toFloat().sub(float(1)).div(fInnerSegments);
747
+ const localV = int(iy).toFloat().sub(float(1)).div(fInnerSegments);
748
+ return faceUVFromTileLocal(tile, localU, localV);
749
+ });
750
+ const rootUVCompute = Fn(([nodeIndex, ix, iy]) => {
751
+ if (isSphere) {
752
+ return tileFaceUV(nodeIndex, ix, iy);
753
+ }
754
+ const nodeVec2 = tileOriginVec2(nodeIndex);
755
+ const nodeX = nodeVec2.x;
756
+ const nodeY = nodeVec2.y;
757
+ const rootSize = uniforms.uRootSize.toVar();
758
+ const rootOrigin = uniforms.uRootOrigin.toVar();
759
+ const size = tileSize(nodeIndex);
760
+ const half = float(0.5);
761
+ const halfRoot = float(rootSize).mul(half);
762
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
763
+ const texelSpacing = size.div(fInnerSegments);
764
+ const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
765
+ const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
766
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
767
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
768
+ const centeredX = worldX.sub(rootOrigin.x);
769
+ const centeredZ = worldZ.sub(rootOrigin.z);
770
+ return vec2(
771
+ centeredX.div(rootSize).add(half),
772
+ centeredZ.div(rootSize).mul(float(-1)).add(half)
773
+ );
774
+ });
775
+ const tileVertexWorldPositionCompute = Fn(
776
+ ([nodeIndex, ix, iy]) => {
777
+ const rootOrigin = uniforms.uRootOrigin.toVar();
778
+ if (isSphere) {
779
+ const faceUV = tileFaceUV(nodeIndex, ix, iy);
780
+ const basis = cubeFaceBasis(tileFace(nodeIndex));
781
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
782
+ return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
783
+ }
784
+ const nodeVec2 = tileOriginVec2(nodeIndex);
785
+ const nodeX = nodeVec2.x;
786
+ const nodeY = nodeVec2.y;
787
+ const rootSize = uniforms.uRootSize.toVar();
788
+ const size = tileSize(nodeIndex);
789
+ const half = float(0.5);
790
+ const halfRoot = float(rootSize).mul(half);
791
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
792
+ const texelSpacing = size.div(fInnerSegments);
793
+ const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
794
+ const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
795
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
796
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
797
+ return vec3(worldX, rootOrigin.y, worldZ);
798
+ }
799
+ );
800
+ return {
801
+ tileLevel,
802
+ tileFace,
803
+ tileOriginVec2,
804
+ tileSize,
805
+ tileFaceUV,
806
+ rootUVCompute,
807
+ tileVertexWorldPositionCompute
808
+ };
809
+ }
810
+ function tileLocalToFieldUV(localCoord, innerSegments) {
811
+ const edge = float(innerSegments).add(float(FIELD_EDGE_EXTRA_TEXELS));
812
+ return float(localCoord).mul(float(innerSegments)).add(float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
813
+ }
814
+ function tileLocalToFieldUVNumber(localCoord, innerSegments) {
815
+ const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
816
+ return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
817
+ }
818
+
819
+ function createLeafStorage(maxNodes) {
820
+ const data = new Int32Array(maxNodes * 4);
821
+ const attribute = new StorageBufferAttribute(data, 4);
822
+ const node = storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
823
+ return { data, attribute, node };
824
+ }
825
+
826
+ const Dir = {
827
+ LEFT: 0,
828
+ RIGHT: 1,
829
+ TOP: 2,
830
+ BOTTOM: 3
831
+ };
832
+ const U32_EMPTY = 4294967295;
833
+ function allocLeafSet(capacity) {
834
+ return {
835
+ capacity,
836
+ count: 0,
837
+ space: new Uint8Array(capacity),
838
+ level: new Uint8Array(capacity),
839
+ x: new Int32Array(capacity),
840
+ y: new Int32Array(capacity)
841
+ };
842
+ }
843
+ function resetLeafSet(leaves) {
844
+ leaves.count = 0;
845
+ }
846
+ function allocSeamTable(capacity) {
847
+ return {
848
+ capacity,
849
+ count: 0,
850
+ stride: 8,
851
+ neighbors: new Uint32Array(capacity * 8)
852
+ };
853
+ }
854
+ function resetSeamTable(seams) {
855
+ seams.count = 0;
856
+ }
857
+
858
+ function createNodeStore(maxNodes, spaceCount) {
859
+ return {
860
+ maxNodes,
861
+ nodesUsed: 0,
862
+ currentGen: 1,
863
+ gen: new Uint16Array(maxNodes),
864
+ space: new Uint8Array(maxNodes),
865
+ level: new Uint8Array(maxNodes),
866
+ x: new Int32Array(maxNodes),
867
+ y: new Int32Array(maxNodes),
868
+ firstChild: new Uint32Array(maxNodes),
869
+ flags: new Uint8Array(maxNodes),
870
+ roots: new Uint32Array(spaceCount)
871
+ };
872
+ }
873
+ function beginFrame(store) {
874
+ store.nodesUsed = 0;
875
+ store.currentGen = store.currentGen + 1 & 65535;
876
+ if (store.currentGen === 0) {
877
+ store.gen.fill(0);
878
+ store.currentGen = 1;
879
+ }
880
+ }
881
+ function allocNode(store, tile) {
882
+ const id = store.nodesUsed;
883
+ if (id >= store.maxNodes) return U32_EMPTY;
884
+ store.nodesUsed = id + 1;
885
+ store.gen[id] = store.currentGen;
886
+ store.space[id] = tile.space;
887
+ store.level[id] = tile.level;
888
+ store.x[id] = tile.x;
889
+ store.y[id] = tile.y;
890
+ store.firstChild[id] = U32_EMPTY;
891
+ store.flags[id] = 0;
892
+ return id;
893
+ }
894
+ function hasChildren(store, nodeId) {
895
+ return store.firstChild[nodeId] !== U32_EMPTY;
896
+ }
897
+ function ensureChildren(store, parentId) {
898
+ const existing = store.firstChild[parentId];
899
+ if (existing !== U32_EMPTY) return existing;
900
+ const childBase = store.nodesUsed;
901
+ if (childBase + 4 > store.maxNodes) return U32_EMPTY;
902
+ const space = store.space[parentId];
903
+ const level = store.level[parentId] + 1;
904
+ const px = store.x[parentId] << 1;
905
+ const py = store.y[parentId] << 1;
906
+ allocNode(store, { space, level, x: px, y: py });
907
+ allocNode(store, { space, level, x: px + 1, y: py });
908
+ allocNode(store, { space, level, x: px, y: py + 1 });
909
+ allocNode(store, { space, level, x: px + 1, y: py + 1 });
910
+ store.firstChild[parentId] = childBase;
911
+ return childBase;
912
+ }
913
+
914
+ function nextPow2$1(n) {
915
+ let x = 1;
916
+ while (x < n) x <<= 1;
917
+ return x;
918
+ }
919
+ function mix32$1(x) {
920
+ x >>>= 0;
921
+ x ^= x >>> 16;
922
+ x = Math.imul(x, 2146121005) >>> 0;
923
+ x ^= x >>> 15;
924
+ x = Math.imul(x, 2221713035) >>> 0;
925
+ x ^= x >>> 16;
926
+ return x >>> 0;
927
+ }
928
+ function hashKey$1(space, level, x, y) {
929
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
930
+ return mix32$1(h);
931
+ }
932
+ function createSpatialIndex(maxEntries) {
933
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
934
+ return {
935
+ size,
936
+ mask: size - 1,
937
+ stampGen: 1,
938
+ stamp: new Uint16Array(size),
939
+ keysSpace: new Uint8Array(size),
940
+ keysLevel: new Uint8Array(size),
941
+ keysX: new Uint32Array(size),
942
+ keysY: new Uint32Array(size),
943
+ values: new Uint32Array(size)
944
+ };
945
+ }
946
+ function resetSpatialIndex(index) {
947
+ index.stampGen = index.stampGen + 1 & 65535;
948
+ if (index.stampGen === 0) {
949
+ index.stamp.fill(0);
950
+ index.stampGen = 1;
951
+ }
952
+ }
953
+ function insertSpatialIndexRaw(index, space, level, x, y, value) {
954
+ const s = space & 255;
955
+ const l = level & 255;
956
+ const xx = x >>> 0;
957
+ const yy = y >>> 0;
958
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
959
+ for (let probes = 0; probes < index.size; probes++) {
960
+ if (index.stamp[slot] !== index.stampGen) {
961
+ index.stamp[slot] = index.stampGen;
962
+ index.keysSpace[slot] = s;
963
+ index.keysLevel[slot] = l;
964
+ index.keysX[slot] = xx;
965
+ index.keysY[slot] = yy;
966
+ index.values[slot] = value >>> 0;
967
+ return;
968
+ }
969
+ if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
970
+ index.values[slot] = value >>> 0;
971
+ return;
972
+ }
973
+ slot = slot + 1 & index.mask;
974
+ }
975
+ throw new Error("SpatialIndex is full (no empty slot found).");
976
+ }
977
+ function lookupSpatialIndexRaw(index, space, level, x, y) {
978
+ const s = space & 255;
979
+ const l = level & 255;
980
+ const xx = x >>> 0;
981
+ const yy = y >>> 0;
982
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
983
+ for (let probes = 0; probes < index.size; probes++) {
984
+ if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
985
+ if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
986
+ return index.values[slot];
987
+ }
988
+ slot = slot + 1 & index.mask;
989
+ }
990
+ return U32_EMPTY;
991
+ }
992
+
993
+ function buildLeafIndex(leaves, out) {
994
+ const index = out ?? createSpatialIndex(leaves.count);
995
+ resetSpatialIndex(index);
996
+ for (let i = 0; i < leaves.count; i++) {
997
+ insertSpatialIndexRaw(index, leaves.space[i], leaves.level[i], leaves.x[i], leaves.y[i], i);
998
+ }
999
+ return index;
1000
+ }
1001
+
1002
+ function createState(cfg, topology) {
1003
+ const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
1004
+ const scratchRootTiles = [];
1005
+ for (let i = 0; i < topology.maxRootCount; i++) {
1006
+ scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
1007
+ }
1008
+ return {
1009
+ cfg,
1010
+ store,
1011
+ leaves: allocLeafSet(cfg.maxNodes),
1012
+ leafNodeIds: new Uint32Array(cfg.maxNodes),
1013
+ leafIndex: createSpatialIndex(cfg.maxNodes),
1014
+ stack: new Uint32Array(cfg.maxNodes),
1015
+ rootNodeIds: new Uint32Array(topology.maxRootCount),
1016
+ rootCount: 0,
1017
+ splitQueue: new Uint32Array(cfg.maxNodes),
1018
+ splitStamp: new Uint16Array(cfg.maxNodes),
1019
+ splitGen: 1,
1020
+ scratchTile: { space: 0, level: 0, x: 0, y: 0 },
1021
+ scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
1022
+ scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
1023
+ scratchRootTiles,
1024
+ spaceCount: topology.spaceCount
1025
+ };
1026
+ }
1027
+ function beginUpdate(state, topology, params) {
1028
+ if (topology.spaceCount !== state.spaceCount) {
1029
+ throw new Error(
1030
+ `Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
1031
+ );
1032
+ }
1033
+ if (topology.maxRootCount !== state.rootNodeIds.length) {
1034
+ throw new Error(
1035
+ `Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
1036
+ );
1037
+ }
1038
+ beginFrame(state.store);
1039
+ state.rootCount = 0;
1040
+ const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
1041
+ if (rootCount < 0 || rootCount > topology.maxRootCount) {
1042
+ throw new Error(`Topology returned invalid root count (${rootCount}).`);
1043
+ }
1044
+ for (let i = 0; i < rootCount; i++) {
1045
+ const rootId = allocNode(state.store, state.scratchRootTiles[i]);
1046
+ if (rootId === U32_EMPTY) {
1047
+ throw new Error("Failed to allocate root node (maxNodes too small).");
1048
+ }
1049
+ state.rootNodeIds[i] = rootId;
1050
+ state.rootCount = i + 1;
1051
+ }
1052
+ }
1053
+
1054
+ function shouldSplit(bounds, level, maxLevel, params) {
1055
+ if (level >= maxLevel) return false;
1056
+ const mode = params.mode ?? "distance";
1057
+ const cx = bounds.cx;
1058
+ const cy = bounds.cy;
1059
+ const cz = bounds.cz;
1060
+ const distSq = cx * cx + cy * cy + cz * cz;
1061
+ const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
1062
+ if (mode === "screen") {
1063
+ const proj = params.projectionFactor ?? 0;
1064
+ const target = params.targetPixels ?? 0;
1065
+ if (proj <= 0 || target <= 0) {
1066
+ const f2 = params.distanceFactor ?? 2;
1067
+ const threshold2 = bounds.r * f2;
1068
+ return safeDistSq < threshold2 * threshold2;
1069
+ }
1070
+ const left = bounds.r * bounds.r * proj * proj;
1071
+ const right = safeDistSq * target * target;
1072
+ return left > right;
1073
+ }
1074
+ const f = params.distanceFactor ?? 2;
1075
+ const threshold = bounds.r * f;
1076
+ return safeDistSq < threshold * threshold;
1077
+ }
1078
+
1079
+ function refineLeaves(state, topology, params, outLeaves) {
1080
+ const leaves = outLeaves ?? state.leaves;
1081
+ resetLeafSet(leaves);
1082
+ const store = state.store;
1083
+ const stack = state.stack;
1084
+ let sp = 0;
1085
+ for (let i = 0; i < state.rootCount; i++) {
1086
+ stack[sp++] = state.rootNodeIds[i];
1087
+ }
1088
+ while (sp > 0) {
1089
+ const nodeId = stack[--sp];
1090
+ const level = store.level[nodeId];
1091
+ const space = store.space[nodeId];
1092
+ const x = store.x[nodeId];
1093
+ const y = store.y[nodeId];
1094
+ const tile = state.scratchTile;
1095
+ tile.space = space;
1096
+ tile.level = level;
1097
+ tile.x = x;
1098
+ tile.y = y;
1099
+ const bounds = state.scratchBounds;
1100
+ topology.tileBounds(tile, params.cameraOrigin, bounds);
1101
+ if (hasChildren(store, nodeId)) {
1102
+ const base = store.firstChild[nodeId];
1103
+ stack[sp++] = base + 3;
1104
+ stack[sp++] = base + 2;
1105
+ stack[sp++] = base + 1;
1106
+ stack[sp++] = base + 0;
1107
+ continue;
1108
+ }
1109
+ const split = shouldSplit(bounds, level, state.cfg.maxLevel, params);
1110
+ if (split) {
1111
+ const base = ensureChildren(store, nodeId);
1112
+ if (base !== U32_EMPTY) {
1113
+ stack[sp++] = base + 3;
1114
+ stack[sp++] = base + 2;
1115
+ stack[sp++] = base + 1;
1116
+ stack[sp++] = base + 0;
1117
+ continue;
1118
+ }
1119
+ }
1120
+ const i = leaves.count;
1121
+ if (i >= leaves.capacity) {
1122
+ throw new Error("LeafSet capacity exceeded.");
1123
+ }
1124
+ leaves.space[i] = space;
1125
+ leaves.level[i] = level;
1126
+ leaves.x[i] = x;
1127
+ leaves.y[i] = y;
1128
+ state.leafNodeIds[i] = nodeId;
1129
+ leaves.count = i + 1;
1130
+ }
1131
+ return leaves;
1132
+ }
1133
+
1134
+ function resetSplitMarks(state) {
1135
+ state.splitGen = state.splitGen + 1 & 65535;
1136
+ if (state.splitGen === 0) {
1137
+ state.splitStamp.fill(0);
1138
+ state.splitGen = 1;
1139
+ }
1140
+ }
1141
+ function scheduleSplit(state, nodeId, count) {
1142
+ if (nodeId === U32_EMPTY) return count;
1143
+ if (state.splitStamp[nodeId] === state.splitGen) return count;
1144
+ state.splitStamp[nodeId] = state.splitGen;
1145
+ state.splitQueue[count] = nodeId;
1146
+ return count + 1;
1147
+ }
1148
+ function balance2to1(state, topology, params, leaves) {
1149
+ const maxIters = state.cfg.maxLevel + 1;
1150
+ for (let iter = 0; iter < maxIters; iter++) {
1151
+ const index = buildLeafIndex(leaves, state.leafIndex);
1152
+ resetSplitMarks(state);
1153
+ let splitCount = 0;
1154
+ for (let i = 0; i < leaves.count; i++) {
1155
+ const leafLevel = leaves.level[i];
1156
+ if (leafLevel < 2) continue;
1157
+ const leafSpace = leaves.space[i];
1158
+ const leafX = leaves.x[i];
1159
+ const leafY = leaves.y[i];
1160
+ for (let dir = 0; dir < 4; dir++) {
1161
+ for (let candidateLevel = leafLevel - 2; candidateLevel >= 0; candidateLevel--) {
1162
+ const shift = leafLevel - candidateLevel;
1163
+ const tile = state.scratchTile;
1164
+ tile.space = leafSpace;
1165
+ tile.level = candidateLevel;
1166
+ tile.x = leafX >>> shift;
1167
+ tile.y = leafY >>> shift;
1168
+ const neighbor = state.scratchNeighbor;
1169
+ if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
1170
+ const j = lookupSpatialIndexRaw(
1171
+ index,
1172
+ neighbor.space,
1173
+ neighbor.level,
1174
+ neighbor.x,
1175
+ neighbor.y
1176
+ );
1177
+ if (j !== U32_EMPTY) {
1178
+ splitCount = scheduleSplit(state, state.leafNodeIds[j], splitCount);
1179
+ break;
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ if (splitCount === 0) return leaves;
1185
+ let anySplit = false;
1186
+ for (let k = 0; k < splitCount; k++) {
1187
+ const nodeId = state.splitQueue[k];
1188
+ if (state.store.level[nodeId] >= state.cfg.maxLevel) continue;
1189
+ const base = ensureChildren(state.store, nodeId);
1190
+ if (base !== U32_EMPTY) anySplit = true;
1191
+ }
1192
+ if (!anySplit) return leaves;
1193
+ refineLeaves(state, topology, params, leaves);
1194
+ }
1195
+ return leaves;
1196
+ }
1197
+
1198
+ function update(state, topology, params, outLeaves) {
1199
+ const cam = params.cameraOrigin;
1200
+ const elevation = params.elevationAtCameraXZ ?? 0;
1201
+ const origX = cam.x;
1202
+ const origY = cam.y;
1203
+ const origZ = cam.z;
1204
+ if (topology.projection === "cubeSphere") {
1205
+ const center = topology.center ?? { x: 0, y: 0, z: 0 };
1206
+ const dx = cam.x - center.x;
1207
+ const dy = cam.y - center.y;
1208
+ const dz = cam.z - center.z;
1209
+ const len = Math.hypot(dx, dy, dz);
1210
+ if (len > 1e-12) {
1211
+ const inv = elevation / len;
1212
+ cam.x -= dx * inv;
1213
+ cam.y -= dy * inv;
1214
+ cam.z -= dz * inv;
1215
+ }
1216
+ } else {
1217
+ cam.y -= elevation;
1218
+ }
1219
+ beginUpdate(state, topology, params);
1220
+ const leaves = refineLeaves(state, topology, params, outLeaves);
1221
+ const result = balance2to1(state, topology, params, leaves);
1222
+ cam.x = origX;
1223
+ cam.y = origY;
1224
+ cam.z = origZ;
1225
+ return result;
1226
+ }
1227
+
1228
+ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
1229
+ const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
1230
+ const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
1231
+ const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
1232
+ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1233
+ if (outSeams.capacity < leaves.count) {
1234
+ throw new Error("SeamTable capacity is smaller than LeafSet.count.");
1235
+ }
1236
+ const index = buildLeafIndex(leaves, outIndex);
1237
+ outSeams.count = leaves.count;
1238
+ const neighbors = outSeams.neighbors;
1239
+ for (let i = 0; i < leaves.count; i++) {
1240
+ const base = i * 8;
1241
+ const space = leaves.space[i];
1242
+ const level = leaves.level[i];
1243
+ const x = leaves.x[i];
1244
+ const y = leaves.y[i];
1245
+ for (let dir = 0; dir < 4; dir++) {
1246
+ const outOffset = base + dir * 2;
1247
+ neighbors[outOffset + 0] = U32_EMPTY;
1248
+ neighbors[outOffset + 1] = U32_EMPTY;
1249
+ scratchTile.space = space;
1250
+ scratchTile.level = level;
1251
+ scratchTile.x = x;
1252
+ scratchTile.y = y;
1253
+ if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1254
+ let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
1255
+ if (j !== U32_EMPTY) {
1256
+ neighbors[outOffset + 0] = j;
1257
+ continue;
1258
+ }
1259
+ if (level > 0) {
1260
+ const px = x >>> 1;
1261
+ const py = y >>> 1;
1262
+ scratchParentTile.space = space;
1263
+ scratchParentTile.level = level - 1;
1264
+ scratchParentTile.x = px;
1265
+ scratchParentTile.y = py;
1266
+ if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1267
+ j = lookupSpatialIndexRaw(
1268
+ index,
1269
+ scratchParentNbr.space,
1270
+ scratchParentNbr.level,
1271
+ scratchParentNbr.x,
1272
+ scratchParentNbr.y
1273
+ );
1274
+ if (j !== U32_EMPTY) {
1275
+ neighbors[outOffset + 0] = j;
1276
+ continue;
1277
+ }
1278
+ }
1279
+ }
1280
+ const childLevel = scratchNbr.level + 1;
1281
+ const x2 = scratchNbr.x << 1 >>> 0;
1282
+ const y2 = scratchNbr.y << 1 >>> 0;
1283
+ let ax = 0;
1284
+ let ay = 0;
1285
+ let bx = 0;
1286
+ let by = 0;
1287
+ switch (dir) {
1288
+ case Dir.LEFT:
1289
+ ax = x2 + 1;
1290
+ ay = y2;
1291
+ bx = x2 + 1;
1292
+ by = y2 + 1;
1293
+ break;
1294
+ case Dir.RIGHT:
1295
+ ax = x2;
1296
+ ay = y2;
1297
+ bx = x2;
1298
+ by = y2 + 1;
1299
+ break;
1300
+ case Dir.TOP:
1301
+ ax = x2;
1302
+ ay = y2 + 1;
1303
+ bx = x2 + 1;
1304
+ by = y2 + 1;
1305
+ break;
1306
+ case Dir.BOTTOM:
1307
+ ax = x2;
1308
+ ay = y2;
1309
+ bx = x2 + 1;
1310
+ by = y2;
1311
+ break;
1312
+ }
1313
+ j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, ax, ay);
1314
+ if (j !== U32_EMPTY) neighbors[outOffset + 0] = j;
1315
+ j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, bx, by);
1316
+ if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
1317
+ }
1318
+ }
1319
+ return outSeams;
1320
+ }
1321
+
1322
+ function createFlatTopology(cfg) {
1323
+ const halfRoot = 0.5 * cfg.rootSize;
1324
+ const maxHeight = cfg.maxHeight ?? 0;
1325
+ const topology = {
1326
+ spaceCount: 1,
1327
+ maxRootCount: 1,
1328
+ neighborSameLevel(tile, dir, out) {
1329
+ const level = tile.level;
1330
+ const x = tile.x;
1331
+ const y = tile.y;
1332
+ let nx = x;
1333
+ let ny = y;
1334
+ switch (dir) {
1335
+ case Dir.LEFT:
1336
+ nx = x - 1;
1337
+ break;
1338
+ case Dir.RIGHT:
1339
+ nx = x + 1;
1340
+ break;
1341
+ case Dir.TOP:
1342
+ ny = y - 1;
1343
+ break;
1344
+ case Dir.BOTTOM:
1345
+ ny = y + 1;
1346
+ break;
1347
+ }
1348
+ if (nx < 0 || ny < 0) return false;
1349
+ const maxCoord = (1 << level) - 1;
1350
+ if (nx > maxCoord || ny > maxCoord) return false;
1351
+ out.space = 0;
1352
+ out.level = level;
1353
+ out.x = nx;
1354
+ out.y = ny;
1355
+ return true;
1356
+ },
1357
+ tileBounds(tile, cameraOrigin, out) {
1358
+ const level = tile.level;
1359
+ const scale = 1 / (1 << level);
1360
+ const size = cfg.rootSize * scale;
1361
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1362
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1363
+ const centerX = minX + 0.5 * size;
1364
+ const centerY = cfg.origin.y;
1365
+ const centerZ = minZ + 0.5 * size;
1366
+ out.cx = centerX - cameraOrigin.x;
1367
+ out.cy = centerY - cameraOrigin.y;
1368
+ out.cz = centerZ - cameraOrigin.z;
1369
+ out.r = 0.7071067811865476 * size + maxHeight;
1370
+ },
1371
+ rootTiles(_cameraOrigin, out) {
1372
+ const root = out[0];
1373
+ root.space = 0;
1374
+ root.level = 0;
1375
+ root.x = 0;
1376
+ root.y = 0;
1377
+ return 1;
1378
+ }
1379
+ };
1380
+ return topology;
1381
+ }
1382
+
1383
+ function createInfiniteFlatTopology(cfg) {
1384
+ const halfRoot = 0.5 * cfg.rootSize;
1385
+ const maxHeight = cfg.maxHeight ?? 0;
1386
+ const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1387
+ const rootWidth = rootGridRadius * 2 + 1;
1388
+ return {
1389
+ spaceCount: 1,
1390
+ maxRootCount: rootWidth * rootWidth,
1391
+ neighborSameLevel(tile, dir, out) {
1392
+ let nx = tile.x;
1393
+ let ny = tile.y;
1394
+ switch (dir) {
1395
+ case Dir.LEFT:
1396
+ nx = tile.x - 1;
1397
+ break;
1398
+ case Dir.RIGHT:
1399
+ nx = tile.x + 1;
1400
+ break;
1401
+ case Dir.TOP:
1402
+ ny = tile.y - 1;
1403
+ break;
1404
+ case Dir.BOTTOM:
1405
+ ny = tile.y + 1;
1406
+ break;
1407
+ }
1408
+ out.space = tile.space;
1409
+ out.level = tile.level;
1410
+ out.x = nx;
1411
+ out.y = ny;
1412
+ return true;
1413
+ },
1414
+ tileBounds(tile, cameraOrigin, out) {
1415
+ const level = tile.level;
1416
+ const scale = 1 / (1 << level);
1417
+ const size = cfg.rootSize * scale;
1418
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1419
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1420
+ const centerX = minX + 0.5 * size;
1421
+ const centerY = cfg.origin.y;
1422
+ const centerZ = minZ + 0.5 * size;
1423
+ out.cx = centerX - cameraOrigin.x;
1424
+ out.cy = centerY - cameraOrigin.y;
1425
+ out.cz = centerZ - cameraOrigin.z;
1426
+ out.r = 0.7071067811865476 * size + maxHeight;
1427
+ },
1428
+ rootTiles(cameraOrigin, out) {
1429
+ const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
1430
+ const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
1431
+ let index = 0;
1432
+ for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
1433
+ for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
1434
+ const root = out[index];
1435
+ root.space = 0;
1436
+ root.level = 0;
1437
+ root.x = camRootX + dx;
1438
+ root.y = camRootY + dy;
1439
+ index++;
1440
+ }
1441
+ }
1442
+ return index;
1443
+ }
1444
+ };
1445
+ }
1446
+
1447
+ const DEG_TO_RAD = Math.PI / 180;
1448
+ const RAD_TO_DEG = 180 / Math.PI;
1449
+ function dot(a, b) {
1450
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1451
+ }
1452
+ function faceUVToCube(face, u, v, out) {
1453
+ const f = CUBE_FACES[face];
1454
+ const s = 2 * u - 1;
1455
+ const t = 2 * v - 1;
1456
+ out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
1457
+ out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
1458
+ out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
1459
+ }
1460
+ function directionToFace(d) {
1461
+ const ax = Math.abs(d[0]);
1462
+ const ay = Math.abs(d[1]);
1463
+ const az = Math.abs(d[2]);
1464
+ if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
1465
+ if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
1466
+ return d[2] >= 0 ? 4 : 5;
1467
+ }
1468
+ function directionToFaceUV(face, d, out) {
1469
+ const f = CUBE_FACES[face];
1470
+ const denom = dot(d, f.forward);
1471
+ const inv = 1 / denom;
1472
+ const px = d[0] * inv;
1473
+ const py = d[1] * inv;
1474
+ const pz = d[2] * inv;
1475
+ const p = [px, py, pz];
1476
+ const s = dot(p, f.right);
1477
+ const t = dot(p, f.up);
1478
+ out[0] = (s + 1) * 0.5;
1479
+ out[1] = (t + 1) * 0.5;
1480
+ }
1481
+ function latLongToDirection(latDeg, lonDeg, out) {
1482
+ const lat = latDeg * DEG_TO_RAD;
1483
+ const lon = lonDeg * DEG_TO_RAD;
1484
+ const cosLat = Math.cos(lat);
1485
+ out[0] = cosLat * Math.sin(lon);
1486
+ out[1] = Math.sin(lat);
1487
+ out[2] = cosLat * Math.cos(lon);
1488
+ }
1489
+ function directionToLatLong(d) {
1490
+ const len = Math.hypot(d[0], d[1], d[2]) || 1;
1491
+ const y = Math.max(-1, Math.min(1, d[1] / len));
1492
+ return {
1493
+ latitude: Math.asin(y) * RAD_TO_DEG,
1494
+ longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
1495
+ };
1496
+ }
1497
+
1498
+ function createCubeSphereTopology(cfg) {
1499
+ const radius = cfg.radius;
1500
+ const maxHeight = cfg.maxHeight ?? 0;
1501
+ const center = cfg.center ?? { x: 0, y: 0, z: 0 };
1502
+ const cube = [0, 0, 0];
1503
+ const uv = [0, 0];
1504
+ function crossFaceNeighbor(face, level, nx, ny, out) {
1505
+ const n = 1 << level;
1506
+ const u = (nx + 0.5) / n;
1507
+ const v = (ny + 0.5) / n;
1508
+ faceUVToCube(face, u, v, cube);
1509
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
1510
+ const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
1511
+ const nbrFace = directionToFace(dir);
1512
+ directionToFaceUV(nbrFace, dir, uv);
1513
+ let bx = Math.floor(uv[0] * n);
1514
+ let by = Math.floor(uv[1] * n);
1515
+ if (bx < 0) bx = 0;
1516
+ else if (bx > n - 1) bx = n - 1;
1517
+ if (by < 0) by = 0;
1518
+ else if (by > n - 1) by = n - 1;
1519
+ out.space = nbrFace;
1520
+ out.level = level;
1521
+ out.x = bx;
1522
+ out.y = by;
1523
+ }
1524
+ return {
1525
+ spaceCount: 6,
1526
+ maxRootCount: 6,
1527
+ neighborSameLevel(tile, dir, out) {
1528
+ const level = tile.level;
1529
+ const n = 1 << level;
1530
+ let nx = tile.x;
1531
+ let ny = tile.y;
1532
+ switch (dir) {
1533
+ case 0:
1534
+ nx -= 1;
1535
+ break;
1536
+ case 1:
1537
+ nx += 1;
1538
+ break;
1539
+ case 2:
1540
+ ny -= 1;
1541
+ break;
1542
+ case 3:
1543
+ ny += 1;
1544
+ break;
1545
+ }
1546
+ if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
1547
+ out.space = tile.space;
1548
+ out.level = level;
1549
+ out.x = nx;
1550
+ out.y = ny;
1551
+ return true;
1552
+ }
1553
+ crossFaceNeighbor(tile.space, level, nx, ny, out);
1554
+ return true;
1555
+ },
1556
+ tileBounds(tile, cameraOrigin, out) {
1557
+ const level = tile.level;
1558
+ const n = 1 << level;
1559
+ const u0 = tile.x / n;
1560
+ const u1 = (tile.x + 1) / n;
1561
+ const v0 = tile.y / n;
1562
+ const v1 = (tile.y + 1) / n;
1563
+ const cornersU = [u0, u1, u0, u1];
1564
+ const cornersV = [v0, v0, v1, v1];
1565
+ let sumX = 0;
1566
+ let sumY = 0;
1567
+ let sumZ = 0;
1568
+ const px = [0, 0, 0, 0];
1569
+ const py = [0, 0, 0, 0];
1570
+ const pz = [0, 0, 0, 0];
1571
+ for (let i = 0; i < 4; i++) {
1572
+ faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
1573
+ const len = Math.hypot(cube[0], cube[1], cube[2]);
1574
+ const sx = center.x + cube[0] / len * radius;
1575
+ const sy = center.y + cube[1] / len * radius;
1576
+ const sz = center.z + cube[2] / len * radius;
1577
+ px[i] = sx;
1578
+ py[i] = sy;
1579
+ pz[i] = sz;
1580
+ sumX += sx;
1581
+ sumY += sy;
1582
+ sumZ += sz;
1583
+ }
1584
+ const cX = sumX * 0.25;
1585
+ const cY = sumY * 0.25;
1586
+ const cZ = sumZ * 0.25;
1587
+ let maxDistSq = 0;
1588
+ for (let i = 0; i < 4; i++) {
1589
+ const dx = px[i] - cX;
1590
+ const dy = py[i] - cY;
1591
+ const dz = pz[i] - cZ;
1592
+ const dSq = dx * dx + dy * dy + dz * dz;
1593
+ if (dSq > maxDistSq) maxDistSq = dSq;
1594
+ }
1595
+ out.cx = cX - cameraOrigin.x;
1596
+ out.cy = cY - cameraOrigin.y;
1597
+ out.cz = cZ - cameraOrigin.z;
1598
+ out.r = Math.sqrt(maxDistSq) + maxHeight;
1599
+ },
1600
+ rootTiles(_cameraOrigin, out) {
1601
+ for (let s = 0; s < 6; s++) {
1602
+ const root = out[s];
1603
+ root.space = s;
1604
+ root.level = 0;
1605
+ root.x = 0;
1606
+ root.y = 0;
1607
+ }
1608
+ return 6;
1609
+ },
1610
+ projection: "cubeSphere",
1611
+ radius,
1612
+ center
1613
+ };
1614
+ }
1615
+
1616
+ function readHeight(elevation, shape, leafIndex, ix, iy) {
1617
+ const base = leafIndex * shape.verticesPerNode;
1618
+ return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
1619
+ }
1620
+ function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
1621
+ const max = shape.edgeVertexCount - 1;
1622
+ const x = Math.max(0, Math.min(max, gx));
1623
+ const y = Math.max(0, Math.min(max, gy));
1624
+ const x0 = Math.floor(x);
1625
+ const y0 = Math.floor(y);
1626
+ const x1 = Math.min(max, x0 + 1);
1627
+ const y1 = Math.min(max, y0 + 1);
1628
+ const tx = x - x0;
1629
+ const ty = y - y0;
1630
+ const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
1631
+ const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
1632
+ const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
1633
+ const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
1634
+ const hx0 = h00 + (h10 - h00) * tx;
1635
+ const hx1 = h01 + (h11 - h01) * tx;
1636
+ return hx0 + (hx1 - hx0) * ty;
1637
+ }
1638
+ function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
1639
+ const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
1640
+ const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
1641
+ const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
1642
+ const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
1643
+ const inv2Step = 0.5 / stepWorld;
1644
+ out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
1645
+ out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
1646
+ return out;
1647
+ }
1648
+
1649
+ const MISSED_LOOKUP = Object.freeze({
1650
+ found: false,
1651
+ leafIndex: -1,
1652
+ space: -1,
1653
+ level: -1,
1654
+ tileX: -1,
1655
+ tileY: -1,
1656
+ tileSize: 0,
1657
+ localU: 0,
1658
+ localV: 0
1659
+ });
1660
+ function lookupTile(index, config, worldX, worldZ) {
1661
+ const halfRoot = config.rootSize * 0.5;
1662
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
1663
+ const scale = 2 ** level;
1664
+ const tileSize = config.rootSize / scale;
1665
+ const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
1666
+ const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
1667
+ const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
1668
+ if (leafIndex !== U32_EMPTY) {
1669
+ const tileMinX = config.originX + tileX * tileSize - halfRoot;
1670
+ const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
1671
+ return {
1672
+ found: true,
1673
+ leafIndex,
1674
+ space: 0,
1675
+ level,
1676
+ tileX,
1677
+ tileY,
1678
+ tileSize,
1679
+ localU: (worldX - tileMinX) / tileSize,
1680
+ localV: (worldZ - tileMinZ) / tileSize
1681
+ };
1682
+ }
1683
+ }
1684
+ return MISSED_LOOKUP;
1685
+ }
1686
+ function clamp01(value) {
1687
+ return value < 0 ? 0 : value > 1 ? 1 : value;
1688
+ }
1689
+ function lookupTileByFaceUV(index, config, face, u, v) {
1690
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
1691
+ const n = 2 ** level;
1692
+ let tileX = Math.floor(u * n);
1693
+ let tileY = Math.floor(v * n);
1694
+ if (tileX < 0) tileX = 0;
1695
+ else if (tileX > n - 1) tileX = n - 1;
1696
+ if (tileY < 0) tileY = 0;
1697
+ else if (tileY > n - 1) tileY = n - 1;
1698
+ const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
1699
+ if (leafIndex !== U32_EMPTY) {
1700
+ const tileSize = sphereTileArcLength(config.radius, n);
1701
+ return {
1702
+ found: true,
1703
+ leafIndex,
1704
+ space: face,
1705
+ level,
1706
+ tileX,
1707
+ tileY,
1708
+ tileSize,
1709
+ localU: clamp01(u * n - tileX),
1710
+ localV: clamp01(v * n - tileY)
1711
+ };
1712
+ }
1713
+ }
1714
+ return MISSED_LOOKUP;
1715
+ }
1716
+ function lookupTileForDirection(index, config, dx, dy, dz, dirScratch, uvScratch) {
1717
+ if (config.projection !== "cubeSphere") return MISSED_LOOKUP;
1718
+ const len = Math.hypot(dx, dy, dz);
1719
+ if (len === 0) return MISSED_LOOKUP;
1720
+ dirScratch[0] = dx / len;
1721
+ dirScratch[1] = dy / len;
1722
+ dirScratch[2] = dz / len;
1723
+ const face = directionToFace(dirScratch);
1724
+ directionToFaceUV(face, dirScratch, uvScratch);
1725
+ return lookupTileByFaceUV(index, config, face, uvScratch[0], uvScratch[1]);
1726
+ }
1727
+
1728
+ function createTerrainSnapshotState(maxNodes, totalElements) {
1729
+ return {
1730
+ frontElevation: new Float32Array(totalElements),
1731
+ backElevation: new Float32Array(totalElements),
1732
+ frontIndex: createSpatialIndex(maxNodes),
1733
+ backIndex: createSpatialIndex(maxNodes),
1734
+ frontTileBounds: new Float32Array(maxNodes * 2),
1735
+ backTileBounds: new Float32Array(maxNodes * 2),
1736
+ frontLeafCount: 0,
1737
+ globalRange: null,
1738
+ hasSnapshot: false,
1739
+ readbackPending: false,
1740
+ generation: 0,
1741
+ lastScheduledStampGen: -1
1742
+ };
1743
+ }
1744
+ function cloneSpatialIndex(target, source) {
1745
+ if (target.size !== source.size) {
1746
+ throw new Error(
1747
+ `SpatialIndex size mismatch (target=${target.size}, source=${source.size}).`
1748
+ );
1749
+ }
1750
+ target.mask = source.mask;
1751
+ target.stampGen = source.stampGen;
1752
+ target.stamp.set(source.stamp);
1753
+ target.keysSpace.set(source.keysSpace);
1754
+ target.keysLevel.set(source.keysLevel);
1755
+ target.keysX.set(source.keysX);
1756
+ target.keysY.set(source.keysY);
1757
+ target.values.set(source.values);
1758
+ }
1759
+ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
1760
+ if (state.readbackPending) return;
1761
+ const withReadback = renderer;
1762
+ if (!withReadback.getArrayBufferAsync) return;
1763
+ if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
1764
+ cloneSpatialIndex(state.backIndex, spatialIndex);
1765
+ state.lastScheduledStampGen = spatialIndex.stampGen;
1766
+ const { activeLeafCount, totalElements, elevationScale, originY } = captured;
1767
+ state.readbackPending = true;
1768
+ const elevationPromise = withReadback.getArrayBufferAsync(attribute);
1769
+ const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
1770
+ const onComplete = (elevResult, boundsResult) => {
1771
+ const data = new Float32Array(elevResult);
1772
+ state.backElevation.fill(0);
1773
+ state.backElevation.set(data.subarray(0, totalElements));
1774
+ let boundsValid = activeLeafCount === 0;
1775
+ if (boundsResult) {
1776
+ const rawBounds = new Float32Array(boundsResult);
1777
+ state.backTileBounds.fill(0);
1778
+ state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
1779
+ for (let i = 0; i < activeLeafCount; i += 1) {
1780
+ if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
1781
+ boundsValid = true;
1782
+ break;
1783
+ }
1784
+ }
1785
+ }
1786
+ const oldFrontElevation = state.frontElevation;
1787
+ const oldFrontIndex = state.frontIndex;
1788
+ state.frontElevation = state.backElevation;
1789
+ state.frontIndex = state.backIndex;
1790
+ state.frontLeafCount = activeLeafCount;
1791
+ state.backElevation = oldFrontElevation;
1792
+ state.backIndex = oldFrontIndex;
1793
+ if (boundsResult && boundsValid) {
1794
+ const oldFrontBounds = state.frontTileBounds;
1795
+ state.frontTileBounds = state.backTileBounds;
1796
+ state.backTileBounds = oldFrontBounds;
1797
+ }
1798
+ if (boundsResult && boundsValid && activeLeafCount > 0) {
1799
+ let gMin = Infinity;
1800
+ let gMax = -Infinity;
1801
+ for (let i = 0; i < activeLeafCount; i++) {
1802
+ const rawMin = state.frontTileBounds[i * 2];
1803
+ const rawMax = state.frontTileBounds[i * 2 + 1];
1804
+ const a = originY + rawMin * elevationScale;
1805
+ const b = originY + rawMax * elevationScale;
1806
+ gMin = Math.min(gMin, a, b);
1807
+ gMax = Math.max(gMax, a, b);
1808
+ }
1809
+ state.globalRange = { min: gMin, max: gMax };
1810
+ }
1811
+ state.hasSnapshot = true;
1812
+ state.generation += 1;
1813
+ };
1814
+ if (boundsPromise) {
1815
+ Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
1816
+ state.readbackPending = false;
1817
+ });
1818
+ } else {
1819
+ elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
1820
+ state.readbackPending = false;
1821
+ });
1822
+ }
1823
+ }
1824
+
1825
+ function createCpuTerrainCache(maxNodes, initialConfig) {
1826
+ let config = initialConfig;
1827
+ const shape = {
1828
+ edgeVertexCount: config.innerTileSegments + 3,
1829
+ verticesPerNode: 0
1830
+ };
1831
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
1832
+ let totalElements = maxNodes * shape.verticesPerNode;
1833
+ const state = createTerrainSnapshotState(
1834
+ maxNodes,
1835
+ totalElements
1836
+ );
1837
+ const dirScratch = [0, 0, 0];
1838
+ const uvScratch = [0, 0];
1839
+ const llScratch = [0, 0, 0];
1840
+ const gridScratch = { gx: 0, gy: 0 };
1841
+ const gradientScratch = { dhdu: 0, dhdv: 0 };
1842
+ const gridCoordsFromLookup = (lookup) => {
1843
+ const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
1844
+ const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
1845
+ gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
1846
+ gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
1847
+ return gridScratch;
1848
+ };
1849
+ const rawHeightFromLookup = (lookup) => {
1850
+ const g = gridCoordsFromLookup(lookup);
1851
+ return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
1852
+ };
1853
+ const computeNormal = (leafIndex, gx, gy, tileSize) => {
1854
+ const stepWorld = tileSize / config.innerTileSegments;
1855
+ const { dhdu, dhdv } = elevationGradientAt(
1856
+ state.frontElevation,
1857
+ shape,
1858
+ leafIndex,
1859
+ gx,
1860
+ gy,
1861
+ stepWorld,
1862
+ config.elevationScale,
1863
+ gradientScratch
1864
+ );
1865
+ return new Vector3(-dhdu, 1, -dhdv).normalize();
1866
+ };
1867
+ const computeSphereNormal = (leafIndex, gx, gy, tileSize, face, dirX, dirY, dirZ) => {
1868
+ const stepWorld = tileSize / config.innerTileSegments;
1869
+ const { dhdu, dhdv } = elevationGradientAt(
1870
+ state.frontElevation,
1871
+ shape,
1872
+ leafIndex,
1873
+ gx,
1874
+ gy,
1875
+ stepWorld,
1876
+ config.elevationScale,
1877
+ gradientScratch
1878
+ );
1879
+ const f = CUBE_FACES[face];
1880
+ const dDotR = dirX * f.right[0] + dirY * f.right[1] + dirZ * f.right[2];
1881
+ let tux = f.right[0] - dirX * dDotR;
1882
+ let tuy = f.right[1] - dirY * dDotR;
1883
+ let tuz = f.right[2] - dirZ * dDotR;
1884
+ const tuLen = Math.hypot(tux, tuy, tuz) || 1;
1885
+ tux /= tuLen;
1886
+ tuy /= tuLen;
1887
+ tuz /= tuLen;
1888
+ const dDotU = dirX * f.up[0] + dirY * f.up[1] + dirZ * f.up[2];
1889
+ let tvx = f.up[0] - dirX * dDotU;
1890
+ let tvy = f.up[1] - dirY * dDotU;
1891
+ let tvz = f.up[2] - dirZ * dDotU;
1892
+ const tvLen = Math.hypot(tvx, tvy, tvz) || 1;
1893
+ tvx /= tvLen;
1894
+ tvy /= tvLen;
1895
+ tvz /= tvLen;
1896
+ const nx = -dhdu;
1897
+ const ny = 1;
1898
+ const nz = -dhdv;
1899
+ return new Vector3(
1900
+ tux * nx + dirX * ny + tvx * nz,
1901
+ tuy * nx + dirY * ny + tvy * nz,
1902
+ tuz * nx + dirZ * ny + tvz * nz
1903
+ ).normalize();
1904
+ };
1905
+ const sampleFromLookup = (lookup) => {
1906
+ const height = rawHeightFromLookup(lookup);
1907
+ const scaledHeight = config.originY + height * config.elevationScale;
1908
+ const normal = computeNormal(
1909
+ lookup.leafIndex,
1910
+ gridScratch.gx,
1911
+ gridScratch.gy,
1912
+ lookup.tileSize
1913
+ );
1914
+ return { elevation: scaledHeight, normal, valid: true };
1915
+ };
1916
+ const sampleTerrain = (worldX, worldZ) => {
1917
+ if (!state.hasSnapshot) {
1918
+ return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
1919
+ }
1920
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1921
+ if (!lookup.found) {
1922
+ return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
1923
+ }
1924
+ return sampleFromLookup(lookup);
1925
+ };
1926
+ const getElevation = (worldX, worldZ) => {
1927
+ if (!state.hasSnapshot) {
1928
+ return { elevation: 0, valid: false };
1929
+ }
1930
+ const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
1931
+ if (!lookup.found) {
1932
+ return { elevation: 0, valid: false };
1933
+ }
1934
+ const height = rawHeightFromLookup(lookup);
1935
+ return {
1936
+ elevation: config.originY + height * config.elevationScale,
1937
+ valid: true
1938
+ };
1939
+ };
1940
+ const invalidSurfaceSample = (dx, dy, dz) => ({
1941
+ position: new Vector3(),
1942
+ normal: new Vector3(0, 1, 0),
1943
+ direction: new Vector3(dx, dy, dz),
1944
+ elevation: 0,
1945
+ valid: false
1946
+ });
1947
+ const lookupDirection = (dx, dy, dz) => lookupTileForDirection(state.frontIndex, config, dx, dy, dz, dirScratch, uvScratch);
1948
+ const sampleSurfaceByDirection = (dx, dy, dz) => {
1949
+ if (!state.hasSnapshot || config.projection !== "cubeSphere") {
1950
+ return invalidSurfaceSample(dx, dy, dz);
1951
+ }
1952
+ const len = Math.hypot(dx, dy, dz);
1953
+ if (len === 0) return invalidSurfaceSample(0, 0, 0);
1954
+ const nx = dx / len;
1955
+ const ny = dy / len;
1956
+ const nz = dz / len;
1957
+ const lookup = lookupDirection(nx, ny, nz);
1958
+ if (!lookup.found) return invalidSurfaceSample(nx, ny, nz);
1959
+ const height = rawHeightFromLookup(lookup);
1960
+ const elevation = height * config.elevationScale;
1961
+ const r = config.radius + elevation;
1962
+ const position = new Vector3(
1963
+ config.originX + nx * r,
1964
+ config.originY + ny * r,
1965
+ config.originZ + nz * r
1966
+ );
1967
+ const normal = computeSphereNormal(
1968
+ lookup.leafIndex,
1969
+ gridScratch.gx,
1970
+ gridScratch.gy,
1971
+ lookup.tileSize,
1972
+ lookup.space,
1973
+ nx,
1974
+ ny,
1975
+ nz
1976
+ );
1977
+ return {
1978
+ position,
1979
+ normal,
1980
+ direction: new Vector3(nx, ny, nz),
1981
+ elevation,
1982
+ valid: true
1983
+ };
1984
+ };
1985
+ const tileFromLookup = (lookup) => {
1986
+ if (!lookup.found) return null;
1987
+ return {
1988
+ space: lookup.space,
1989
+ level: lookup.level,
1990
+ x: lookup.tileX,
1991
+ y: lookup.tileY,
1992
+ index: lookup.leafIndex
1993
+ };
1994
+ };
1995
+ const tileBoundsFromLookup = (lookup, elevationBase) => {
1996
+ if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
1997
+ const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
1998
+ const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
1999
+ const a = elevationBase + rawMin * config.elevationScale;
2000
+ const b = elevationBase + rawMax * config.elevationScale;
2001
+ return {
2002
+ space: lookup.space,
2003
+ level: lookup.level,
2004
+ x: lookup.tileX,
2005
+ y: lookup.tileY,
2006
+ index: lookup.leafIndex,
2007
+ minElevation: Math.min(a, b),
2008
+ maxElevation: Math.max(a, b)
2009
+ };
2010
+ };
2011
+ const api = {
2012
+ get generation() {
2013
+ return state.generation;
2014
+ },
2015
+ get ready() {
2016
+ return state.hasSnapshot;
2017
+ },
2018
+ updateConfig(nextConfig) {
2019
+ config = nextConfig;
2020
+ shape.edgeVertexCount = config.innerTileSegments + 3;
2021
+ shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
2022
+ totalElements = maxNodes * shape.verticesPerNode;
2023
+ },
2024
+ triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
2025
+ triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
2026
+ activeLeafCount: activeLeafCount ?? 0,
2027
+ totalElements,
2028
+ elevationScale: config.elevationScale,
2029
+ originY: config.originY
2030
+ });
2031
+ },
2032
+ getElevation(worldX, worldZ) {
2033
+ const sample = getElevation(worldX, worldZ);
2034
+ return sample.valid ? sample.elevation : null;
2035
+ },
2036
+ getNormal(worldX, worldZ) {
2037
+ return sampleTerrain(worldX, worldZ).normal;
2038
+ },
2039
+ getTile(worldX, worldZ) {
2040
+ if (!state.hasSnapshot) return null;
2041
+ return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
2042
+ },
2043
+ getTileBounds(worldX, worldZ) {
2044
+ if (!state.hasSnapshot) return null;
2045
+ return tileBoundsFromLookup(
2046
+ lookupTile(state.frontIndex, config, worldX, worldZ),
2047
+ config.originY
2048
+ );
2049
+ },
2050
+ getGlobalElevationRange() {
2051
+ return state.globalRange;
2052
+ },
2053
+ sampleTerrainBatch(positions) {
2054
+ const count = Math.floor(positions.length / 2);
2055
+ const elevations = new Float32Array(count);
2056
+ const normals = new Float32Array(count * 3);
2057
+ const valid = new Uint8Array(count);
2058
+ if (!state.hasSnapshot) {
2059
+ return { elevations, normals, valid, generation: state.generation };
2060
+ }
2061
+ let lastTile;
2062
+ for (let i = 0; i < count; i += 1) {
2063
+ const worldX = positions[i * 2] ?? 0;
2064
+ const worldZ = positions[i * 2 + 1] ?? 0;
2065
+ let lookup;
2066
+ if (lastTile && worldX >= lastTile.tileMinX && worldX <= lastTile.tileMinX + lastTile.tileSize && worldZ >= lastTile.tileMinZ && worldZ <= lastTile.tileMinZ + lastTile.tileSize) {
2067
+ lookup = {
2068
+ found: true,
2069
+ leafIndex: lastTile.leafIndex,
2070
+ space: 0,
2071
+ level: lastTile.level,
2072
+ tileX: lastTile.tileX,
2073
+ tileY: lastTile.tileY,
2074
+ tileSize: lastTile.tileSize,
2075
+ localU: (worldX - lastTile.tileMinX) / lastTile.tileSize,
2076
+ localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
2077
+ };
2078
+ } else {
2079
+ lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
2080
+ if (lookup.found) {
2081
+ lastTile = {
2082
+ leafIndex: lookup.leafIndex,
2083
+ level: lookup.level,
2084
+ tileX: lookup.tileX,
2085
+ tileY: lookup.tileY,
2086
+ tileSize: lookup.tileSize,
2087
+ tileMinX: worldX - lookup.localU * lookup.tileSize,
2088
+ tileMinZ: worldZ - lookup.localV * lookup.tileSize
2089
+ };
2090
+ } else {
2091
+ lastTile = void 0;
2092
+ }
2093
+ }
2094
+ if (!lookup?.found) {
2095
+ normals[i * 3 + 1] = 1;
2096
+ continue;
2097
+ }
2098
+ const sample = sampleFromLookup(lookup);
2099
+ elevations[i] = sample.elevation;
2100
+ normals[i * 3] = sample.normal.x;
2101
+ normals[i * 3 + 1] = sample.normal.y;
2102
+ normals[i * 3 + 2] = sample.normal.z;
2103
+ valid[i] = 1;
2104
+ }
2105
+ return { elevations, normals, valid, generation: state.generation };
2106
+ },
2107
+ sampleTerrain,
2108
+ // --- Cube-sphere queries ---
2109
+ sampleTerrainByDirection(direction) {
2110
+ return sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2111
+ },
2112
+ sampleTerrainByPosition(position) {
2113
+ return sampleSurfaceByDirection(
2114
+ position.x - config.originX,
2115
+ position.y - config.originY,
2116
+ position.z - config.originZ
2117
+ );
2118
+ },
2119
+ sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
2120
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2121
+ return sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
2122
+ },
2123
+ getElevationByDirection(direction) {
2124
+ const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2125
+ return sample.valid ? sample.elevation : null;
2126
+ },
2127
+ getElevationByPosition(position) {
2128
+ const sample = sampleSurfaceByDirection(
2129
+ position.x - config.originX,
2130
+ position.y - config.originY,
2131
+ position.z - config.originZ
2132
+ );
2133
+ return sample.valid ? sample.elevation : null;
2134
+ },
2135
+ getElevationByLatLong(latitudeDeg, longitudeDeg) {
2136
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2137
+ const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
2138
+ return sample.valid ? sample.elevation : null;
2139
+ },
2140
+ getNormalByDirection(direction) {
2141
+ const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
2142
+ return sample.valid ? sample.normal : null;
2143
+ },
2144
+ getNormalByPosition(position) {
2145
+ const sample = sampleSurfaceByDirection(
2146
+ position.x - config.originX,
2147
+ position.y - config.originY,
2148
+ position.z - config.originZ
2149
+ );
2150
+ return sample.valid ? sample.normal : null;
2151
+ },
2152
+ getNormalByLatLong(latitudeDeg, longitudeDeg) {
2153
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2154
+ const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
2155
+ return sample.valid ? sample.normal : null;
2156
+ },
2157
+ getTileByDirection(direction) {
2158
+ if (!state.hasSnapshot) return null;
2159
+ return tileFromLookup(lookupDirection(direction.x, direction.y, direction.z));
2160
+ },
2161
+ getTileByPosition(position) {
2162
+ if (!state.hasSnapshot) return null;
2163
+ return tileFromLookup(
2164
+ lookupDirection(
2165
+ position.x - config.originX,
2166
+ position.y - config.originY,
2167
+ position.z - config.originZ
2168
+ )
2169
+ );
2170
+ },
2171
+ getTileByLatLong(latitudeDeg, longitudeDeg) {
2172
+ if (!state.hasSnapshot) return null;
2173
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2174
+ return tileFromLookup(lookupDirection(llScratch[0], llScratch[1], llScratch[2]));
2175
+ },
2176
+ getTileBoundsByDirection(direction) {
2177
+ if (!state.hasSnapshot) return null;
2178
+ return tileBoundsFromLookup(
2179
+ lookupDirection(direction.x, direction.y, direction.z),
2180
+ 0
2181
+ );
2182
+ },
2183
+ getTileBoundsByPosition(position) {
2184
+ if (!state.hasSnapshot) return null;
2185
+ return tileBoundsFromLookup(
2186
+ lookupDirection(
2187
+ position.x - config.originX,
2188
+ position.y - config.originY,
2189
+ position.z - config.originZ
2190
+ ),
2191
+ 0
2192
+ );
2193
+ },
2194
+ getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
2195
+ if (!state.hasSnapshot) return null;
2196
+ latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
2197
+ return tileBoundsFromLookup(
2198
+ lookupDirection(llScratch[0], llScratch[1], llScratch[2]),
2199
+ 0
2200
+ );
2201
+ },
2202
+ sampleTerrainBatchByDirection(directions) {
2203
+ const count = Math.floor(directions.length / 3);
2204
+ const positions = new Float32Array(count * 3);
2205
+ const normals = new Float32Array(count * 3);
2206
+ const elevations = new Float32Array(count);
2207
+ const valid = new Uint8Array(count);
2208
+ if (!state.hasSnapshot || config.projection !== "cubeSphere") {
2209
+ return { positions, normals, elevations, valid, generation: state.generation };
2210
+ }
2211
+ for (let i = 0; i < count; i += 1) {
2212
+ const sample = sampleSurfaceByDirection(
2213
+ directions[i * 3] ?? 0,
2214
+ directions[i * 3 + 1] ?? 0,
2215
+ directions[i * 3 + 2] ?? 0
2216
+ );
2217
+ if (!sample.valid) {
2218
+ normals[i * 3 + 1] = 1;
2219
+ continue;
2220
+ }
2221
+ positions[i * 3] = sample.position.x;
2222
+ positions[i * 3 + 1] = sample.position.y;
2223
+ positions[i * 3 + 2] = sample.position.z;
2224
+ normals[i * 3] = sample.normal.x;
2225
+ normals[i * 3 + 1] = sample.normal.y;
2226
+ normals[i * 3 + 2] = sample.normal.z;
2227
+ elevations[i] = sample.elevation;
2228
+ valid[i] = 1;
2229
+ }
2230
+ return { positions, normals, elevations, valid, generation: state.generation };
2231
+ }
2232
+ };
2233
+ return api;
2234
+ }
2235
+
2236
+ function createTerrainQuery(cache) {
2237
+ return {
2238
+ get generation() {
2239
+ return cache.generation;
2240
+ },
2241
+ getElevation(worldX, worldZ) {
2242
+ return cache.getElevation(worldX, worldZ);
2243
+ },
2244
+ getNormal(worldX, worldZ) {
2245
+ return cache.getNormal(worldX, worldZ);
2246
+ },
2247
+ getTile(worldX, worldZ) {
2248
+ return cache.getTile(worldX, worldZ);
2249
+ },
2250
+ getTileBounds(worldX, worldZ) {
2251
+ return cache.getTileBounds(worldX, worldZ);
2252
+ },
2253
+ getGlobalElevationRange() {
2254
+ return cache.getGlobalElevationRange();
2255
+ },
2256
+ sampleTerrain(worldX, worldZ) {
2257
+ return cache.sampleTerrain(worldX, worldZ);
2258
+ },
2259
+ sampleTerrainBatch(positions) {
2260
+ return cache.sampleTerrainBatch(positions);
2261
+ }
2262
+ };
2263
+ }
2264
+ function createTerrainSphereQuery(cache) {
2265
+ return {
2266
+ get generation() {
2267
+ return cache.generation;
2268
+ },
2269
+ getElevationByDirection(direction) {
2270
+ return cache.getElevationByDirection(direction);
2271
+ },
2272
+ getElevationByPosition(position) {
2273
+ return cache.getElevationByPosition(position);
2274
+ },
2275
+ getElevationByLatLong(latitudeDeg, longitudeDeg) {
2276
+ return cache.getElevationByLatLong(latitudeDeg, longitudeDeg);
2277
+ },
2278
+ getNormalByDirection(direction) {
2279
+ return cache.getNormalByDirection(direction);
2280
+ },
2281
+ getNormalByPosition(position) {
2282
+ return cache.getNormalByPosition(position);
2283
+ },
2284
+ getNormalByLatLong(latitudeDeg, longitudeDeg) {
2285
+ return cache.getNormalByLatLong(latitudeDeg, longitudeDeg);
2286
+ },
2287
+ sampleTerrainByDirection(direction) {
2288
+ return cache.sampleTerrainByDirection(direction);
2289
+ },
2290
+ sampleTerrainByPosition(position) {
2291
+ return cache.sampleTerrainByPosition(position);
2292
+ },
2293
+ sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
2294
+ return cache.sampleTerrainByLatLong(latitudeDeg, longitudeDeg);
2295
+ },
2296
+ getTileByDirection(direction) {
2297
+ return cache.getTileByDirection(direction);
2298
+ },
2299
+ getTileByPosition(position) {
2300
+ return cache.getTileByPosition(position);
2301
+ },
2302
+ getTileByLatLong(latitudeDeg, longitudeDeg) {
2303
+ return cache.getTileByLatLong(latitudeDeg, longitudeDeg);
2304
+ },
2305
+ getTileBoundsByDirection(direction) {
2306
+ return cache.getTileBoundsByDirection(direction);
2307
+ },
2308
+ getTileBoundsByPosition(position) {
2309
+ return cache.getTileBoundsByPosition(position);
2310
+ },
2311
+ getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
2312
+ return cache.getTileBoundsByLatLong(latitudeDeg, longitudeDeg);
2313
+ },
2314
+ sampleTerrainBatchByDirection(directions) {
2315
+ return cache.sampleTerrainBatchByDirection(directions);
2316
+ }
2317
+ };
2318
+ }
2319
+
2320
+ const WGSIZE = 64;
2321
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
2322
+ const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
2323
+ return Fn(() => {
2324
+ const sharedMin = workgroupArray("float", WGSIZE);
2325
+ const sharedMax = workgroupArray("float", WGSIZE);
2326
+ const tid = int(localId.x);
2327
+ const tileIdx = int(workgroupId.z);
2328
+ const baseOffset = tileIdx.mul(int(verticesPerNode));
2329
+ const start = tid.mul(int(elemsPerThread));
2330
+ const end = min(start.add(int(elemsPerThread)), int(verticesPerNode));
2331
+ const localMin = float(1e10).toVar("localMin");
2332
+ const localMax = float(-1e10).toVar("localMax");
2333
+ Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
2334
+ const h = elevationFieldNode.element(baseOffset.add(i));
2335
+ localMin.assign(min(localMin, h));
2336
+ localMax.assign(max(localMax, h));
2337
+ });
2338
+ sharedMin.element(tid).assign(localMin);
2339
+ sharedMax.element(tid).assign(localMax);
2340
+ workgroupBarrier();
2341
+ If(tid.equal(int(0)), () => {
2342
+ const finalMin = float(1e10).toVar("finalMin");
2343
+ const finalMax = float(-1e10).toVar("finalMax");
2344
+ Loop(WGSIZE, ({ i }) => {
2345
+ finalMin.assign(min(finalMin, sharedMin.element(i)));
2346
+ finalMax.assign(max(finalMax, sharedMax.element(i)));
2347
+ });
2348
+ const outIdx = tileIdx.mul(int(2));
2349
+ boundsNode.element(outIdx).assign(finalMin);
2350
+ boundsNode.element(outIdx.add(int(1))).assign(finalMax);
2351
+ });
2352
+ })().computeKernel([WGSIZE, 1, 1]);
2353
+ }
2354
+ const tileBoundsContextTask = task((get, work) => {
2355
+ const elevationFieldContext = get(createElevationFieldContextTask);
2356
+ const maxNodesValue = get(maxNodes);
2357
+ const edgeVertexCount = get(innerTileSegments) + 3;
2358
+ return work(() => {
2359
+ const data = new Float32Array(maxNodesValue * 2);
2360
+ const attribute = new StorageBufferAttribute(data, 1);
2361
+ const node = storage(attribute, "float", maxNodesValue * 2);
2362
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
2363
+ const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
2364
+ return { data, attribute, node, kernel };
2365
+ });
2366
+ }).displayName("tileBoundsContextTask");
2367
+ const tileBoundsReductionTask = task(
2368
+ (get, work, { resources }) => {
2369
+ get(executeComputeTask);
2370
+ const boundsContext = get(tileBoundsContextTask);
2371
+ const leafState = get(leafGpuBufferTask);
2372
+ return work(() => {
2373
+ if (resources?.renderer && leafState.count > 0) {
2374
+ resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
2375
+ }
2376
+ return boundsContext;
2377
+ });
2378
+ }
2379
+ ).displayName("tileBoundsReductionTask").lane("gpu");
2380
+
2381
+ const terrainQueryTask = task((get, work) => {
2382
+ const maxNodesValue = get(maxNodes);
2383
+ const innerTileSegmentsValue = get(innerTileSegments);
2384
+ const maxLevelValue = get(maxLevel);
2385
+ const rootSizeValue = get(rootSize);
2386
+ const originValue = get(origin);
2387
+ const elevationScaleValue = get(elevationScale);
2388
+ const radiusValue = get(radius);
2389
+ const topologyValue = get(topologyTask);
2390
+ const projectionValue = topologyValue.projection ?? "flat";
2391
+ return work((prev) => {
2392
+ const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projectionValue}`;
2393
+ const configValues = {
2394
+ rootSize: rootSizeValue,
2395
+ originX: originValue.x,
2396
+ originY: originValue.y,
2397
+ originZ: originValue.z,
2398
+ innerTileSegments: innerTileSegmentsValue,
2399
+ elevationScale: elevationScaleValue,
2400
+ maxLevel: maxLevelValue,
2401
+ projection: projectionValue,
2402
+ radius: topologyValue.radius ?? radiusValue
2403
+ };
2404
+ let cache = prev?.cache;
2405
+ let query = prev?.query;
2406
+ let sphereQuery = prev?.sphereQuery ?? null;
2407
+ if (!cache || !query || prev?.shapeKey !== shapeKey) {
2408
+ cache = createCpuTerrainCache(maxNodesValue, configValues);
2409
+ query = createTerrainQuery(cache);
2410
+ sphereQuery = projectionValue === "cubeSphere" ? createTerrainSphereQuery(cache) : null;
2411
+ }
2412
+ cache.updateConfig(configValues);
2413
+ return { cache, query, sphereQuery, shapeKey };
2414
+ });
2415
+ }).displayName("terrainQueryTask");
2416
+ const terrainReadbackTask = task(
2417
+ (get, work, { resources }) => {
2418
+ const boundsContext = get(tileBoundsReductionTask);
2419
+ const elevationFieldContext = get(createElevationFieldContextTask);
2420
+ const quadtreeConfig = get(quadtreeConfigTask);
2421
+ const leafState = get(leafGpuBufferTask);
2422
+ const { cache } = get(terrainQueryTask);
2423
+ return work(() => {
2424
+ if (!resources?.renderer) return;
2425
+ cache.triggerReadback(
2426
+ resources.renderer,
2427
+ elevationFieldContext.attribute,
2428
+ quadtreeConfig.state.leafIndex,
2429
+ boundsContext.attribute,
2430
+ leafState.count
2431
+ );
2432
+ });
2433
+ }
2434
+ ).displayName("terrainReadbackTask").lane("gpu");
2435
+
2436
+ const topologyTask = task((get, work) => {
2437
+ const customTopology = get(topology);
2438
+ const rootSizeVal = get(rootSize);
2439
+ const originVal = get(origin);
2440
+ return work(() => {
2441
+ if (customTopology) return customTopology;
2442
+ return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
2443
+ });
2444
+ }).displayName("topologyTask");
2445
+ const quadtreeConfigTask = task((get, work) => {
2446
+ const topologyVal = get(topologyTask);
2447
+ const maxNodesVal = get(maxNodes);
2448
+ const maxLevelVal = get(maxLevel);
2449
+ return work(() => {
2450
+ const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
2451
+ return {
2452
+ state,
2453
+ topology: topologyVal
2454
+ };
2455
+ });
2456
+ }).displayName("quadtreeConfigTask");
2457
+ const quadtreeUpdateTask = task((get, work) => {
2458
+ const quadtreeConfig = get(quadtreeConfigTask);
2459
+ const quadtreeUpdateConfig = get(quadtreeUpdate);
2460
+ const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
2461
+ let outLeaves = void 0;
2462
+ const cameraPosition = new Vector3();
2463
+ return work(() => {
2464
+ const cam = quadtreeUpdateConfig.cameraOrigin;
2465
+ if (sphereQuery) {
2466
+ cameraPosition.set(cam.x, cam.y, cam.z);
2467
+ quadtreeUpdateConfig.elevationAtCameraXZ = sphereQuery.getElevationByPosition(cameraPosition) ?? 0;
2468
+ } else {
2469
+ quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
2470
+ }
2471
+ outLeaves = update(
2472
+ quadtreeConfig.state,
2473
+ quadtreeConfig.topology,
2474
+ quadtreeUpdateConfig,
2475
+ outLeaves
2476
+ );
2477
+ return outLeaves;
2478
+ });
2479
+ }).displayName("quadtreeUpdateTask");
2480
+ const leafStorageTask = task((get, work) => {
2481
+ const maxNodesVal = get(maxNodes);
2482
+ return work(() => createLeafStorage(maxNodesVal));
2483
+ }).displayName("leafStorageTask");
2484
+ const leafGpuBufferTask = task((get, work) => {
2485
+ const leafSet = get(quadtreeUpdateTask);
2486
+ const leafStorage = get(leafStorageTask);
2487
+ return work(() => {
2488
+ const bufferCapacity = leafStorage.data.length / 4;
2489
+ const leafCount = Math.min(leafSet.count, bufferCapacity);
2490
+ for (let i = 0; i < leafCount; i += 1) {
2491
+ const offset = i * 4;
2492
+ leafStorage.data[offset] = leafSet.level[i] ?? 0;
2493
+ leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
2494
+ leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
2495
+ leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
2496
+ }
2497
+ leafStorage.attribute.needsUpdate = true;
2498
+ leafStorage.node.needsUpdate = true;
2499
+ return {
2500
+ count: leafCount,
2501
+ data: leafStorage.data,
2502
+ attribute: leafStorage.attribute,
2503
+ node: leafStorage.node
2504
+ };
2505
+ });
2506
+ }).displayName("leafGpuBufferTask");
2507
+
2508
+ function createElevationFunction(callback) {
2509
+ const tslFunction = (args) => {
2510
+ const params = {
2511
+ worldPosition: args.worldPosition,
2512
+ rootSize: args.rootSize,
2513
+ rootUV: args.rootUV,
2514
+ tileUV: args.tileUV,
2515
+ tileLevel: args.tileLevel,
2516
+ tileSize: args.tileSize,
2517
+ tileOriginVec2: args.tileOriginVec2,
2518
+ nodeIndex: args.nodeIndex
2519
+ };
2520
+ return callback(params);
2521
+ };
2522
+ return Fn$1(tslFunction);
2523
+ }
2524
+
2525
+ function createTerrainUniforms(params) {
2526
+ const sanitizedId = params.instanceId?.replace(/-/g, "_");
2527
+ const suffix = sanitizedId ? `_${sanitizedId}` : "";
2528
+ const uRootOrigin = uniform(
2529
+ new Vector3$1(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
2530
+ ).setName(`uRootOrigin${suffix}`);
2531
+ const uRootSize = uniform(float(params.rootSize)).setName(`uRootSize${suffix}`);
2532
+ const uInnerTileSegments = uniform(int(params.innerTileSegments)).setName(
2533
+ `uInnerTileSegments${suffix}`
2534
+ );
2535
+ const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
2536
+ const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
2537
+ const uRadius = uniform(float(params.radius)).setName(`uRadius${suffix}`);
2538
+ return {
2539
+ uRootOrigin,
2540
+ uRootSize,
2541
+ uInnerTileSegments,
2542
+ uSkirtScale,
2543
+ uElevationScale,
2544
+ uRadius
2545
+ };
2546
+ }
2547
+
2548
+ const instanceIdTask = task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
2549
+
2550
+ const scratchVector3 = new Vector3();
2551
+ const createUniformsTask = task((get, work) => {
2552
+ const uniformParams = {
2553
+ rootOrigin: get(origin),
2554
+ rootSize: get(rootSize),
2555
+ innerTileSegments: get(innerTileSegments),
2556
+ skirtScale: get(skirtScale),
2557
+ elevationScale: get(elevationScale),
2558
+ radius: get(radius),
2559
+ instanceId: get(instanceIdTask)
2560
+ };
2561
+ return work(() => createTerrainUniforms(uniformParams));
2562
+ }).displayName("createUniformsTask").cache("once");
2563
+ const updateUniformsTask = task((get, work) => {
2564
+ const terrainUniformsContext = get(createUniformsTask);
2565
+ const rootSizeVal = get(rootSize);
2566
+ const rootOrigin = get(origin);
2567
+ const innerTileSegmentsVal = get(innerTileSegments);
2568
+ const skirtScaleVal = get(skirtScale);
2569
+ const elevationScaleVal = get(elevationScale);
2570
+ const radiusVal = get(radius);
2571
+ return work(() => {
2572
+ terrainUniformsContext.uRootSize.value = rootSizeVal;
2573
+ terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
2574
+ rootOrigin.x,
2575
+ rootOrigin.y,
2576
+ rootOrigin.z
2577
+ );
2578
+ terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
2579
+ terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
2580
+ terrainUniformsContext.uElevationScale.value = elevationScaleVal;
2581
+ terrainUniformsContext.uRadius.value = radiusVal;
2582
+ return terrainUniformsContext;
2583
+ });
2584
+ }).displayName("updateUniformsTask");
2585
+
2586
+ const createElevationFieldContextTask = task((get, work) => {
2587
+ const edgeVertexCount = get(innerTileSegments) + 3;
2588
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
2589
+ const totalElements = get(maxNodes) * verticesPerNode;
2590
+ return work(() => {
2591
+ const data = new Float32Array(totalElements);
2592
+ const attribute = new StorageBufferAttribute(data, 1);
2593
+ const node = storage(attribute, "float", totalElements);
2594
+ return {
2595
+ data,
2596
+ attribute,
2597
+ node
2598
+ };
2599
+ });
2600
+ }).displayName("createElevationFieldContextTask");
2601
+ const tileNodesTask = task((get, work) => {
2602
+ const leafStorage = get(leafStorageTask);
2603
+ const uniforms = get(updateUniformsTask);
2604
+ const topology = get(topologyTask);
2605
+ return work(() => {
2606
+ return createTileCompute(leafStorage, uniforms, topology.projection ?? "flat");
2607
+ });
2608
+ }).displayName("tileNodesTask");
2609
+ const elevationFieldStageTask = task((get, work) => {
2610
+ const tile = get(tileNodesTask);
2611
+ const uniforms = get(updateUniformsTask);
2612
+ const elevationFieldContext = get(createElevationFieldContextTask);
2613
+ const userElevationFn = get(elevationFn);
2614
+ return work(() => {
2615
+ const heightFn = createElevationFunction(userElevationFn);
2616
+ const heightWriteFn = createElevation(tile, uniforms, heightFn);
2617
+ return [
2618
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
2619
+ const height = heightWriteFn(nodeIndex, localCoordinates);
2620
+ elevationFieldContext.node.element(globalVertexIndex).assign(height);
2621
+ }
2622
+ ];
2623
+ });
2624
+ }).displayName("elevationFieldStageTask");
2625
+
2626
+ const createTerrainFieldTextureTask = task(
2627
+ (get, work, { resources }) => {
2628
+ const edgeVertexCount = get(innerTileSegments) + 3;
2629
+ const maxNodesValue = get(maxNodes);
2630
+ const filter = get(terrainFieldFilter);
2631
+ return work(
2632
+ () => createTerrainFieldStorage(
2633
+ edgeVertexCount,
2634
+ maxNodesValue,
2635
+ resources?.renderer,
2636
+ { filter }
2637
+ )
2638
+ );
2639
+ }
2640
+ ).displayName("createTerrainFieldTextureTask");
2641
+ function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
2642
+ return Fn(
2643
+ ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
2644
+ const iEdge = int(edgeVertexCount);
2645
+ const verticesPerNode = iEdge.mul(iEdge);
2646
+ const baseOffset = int(nodeIndex).mul(verticesPerNode);
2647
+ const xLeft = int(ix).sub(int(1));
2648
+ const xRight = int(ix).add(int(1));
2649
+ const yUp = int(iy).sub(int(1));
2650
+ const yDown = int(iy).add(int(1));
2651
+ const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
2652
+ const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
2653
+ const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
2654
+ const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
2655
+ const innerSegments = float(iEdge).sub(float(3));
2656
+ const stepWorld = tileSize.div(innerSegments);
2657
+ const inv2Step = float(0.5).div(stepWorld);
2658
+ const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
2659
+ const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
2660
+ const normal = vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
2661
+ return vec2(normal.x, normal.z);
2662
+ }
2663
+ );
2664
+ }
2665
+ const terrainFieldStageTask = task((get, work) => {
2666
+ const upstream = get(elevationFieldStageTask);
2667
+ const elevationFieldContext = get(createElevationFieldContextTask);
2668
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
2669
+ const tileEdgeVertexCount = get(innerTileSegments) + 3;
2670
+ const tile = get(tileNodesTask);
2671
+ const uniforms = get(updateUniformsTask);
2672
+ return work(() => {
2673
+ const computeNormal = createNormalFromElevationField(
2674
+ elevationFieldContext.node,
2675
+ tileEdgeVertexCount
2676
+ );
2677
+ return [
2678
+ ...upstream,
2679
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
2680
+ const ix = int(localCoordinates.x);
2681
+ const iy = int(localCoordinates.y);
2682
+ const tileSize = tile.tileSize(nodeIndex);
2683
+ const height = elevationFieldContext.node.element(globalVertexIndex);
2684
+ const normalXZ = computeNormal(
2685
+ nodeIndex,
2686
+ tileSize,
2687
+ ix,
2688
+ iy,
2689
+ uniforms.uElevationScale
2690
+ );
2691
+ storeTerrainField(
2692
+ terrainFieldStorage,
2693
+ ix,
2694
+ iy,
2695
+ nodeIndex,
2696
+ packTerrainFieldSample(height, normalXZ)
2697
+ );
2698
+ }
2699
+ ];
2700
+ });
2701
+ }).displayName("terrainFieldStageTask");
2702
+
2703
+ const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
2704
+ function createComputePipelineTasks(leafStageTask) {
2705
+ const compile = task((get, work) => {
2706
+ const pipeline = get(leafStageTask);
2707
+ const edgeVertexCount = get(innerTileSegments) + 3;
2708
+ return work(
2709
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
2710
+ })
2711
+ );
2712
+ }).displayName("compileComputeTask");
2713
+ const execute = task(
2714
+ (get, work, { resources }) => {
2715
+ const { execute: run } = get(compile);
2716
+ const leafState = get(leafGpuBufferTask);
2717
+ return work(
2718
+ () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
2719
+ }
2720
+ );
2721
+ }
2722
+ ).displayName("executeComputeTask").lane("gpu");
2723
+ return { compile, execute };
2724
+ }
2725
+
2726
+ const SLOT_STRIDE = 6;
2727
+ function nextPow2(n) {
2728
+ let x = 1;
2729
+ while (x < n) x <<= 1;
2730
+ return x;
2731
+ }
2732
+ function createGpuSpatialIndex(maxEntries) {
2733
+ const size = nextPow2(Math.max(2, maxEntries * 2));
2734
+ const data = new Uint32Array(size * SLOT_STRIDE);
2735
+ const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
2736
+ const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
2737
+ const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
2738
+ return {
2739
+ data,
2740
+ size,
2741
+ mask: size - 1,
2742
+ stampGen,
2743
+ attribute,
2744
+ node
2745
+ };
2746
+ }
2747
+ function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
2748
+ if (gpuIndex.size !== cpuIndex.size) {
2749
+ throw new Error(
2750
+ `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
2751
+ );
2752
+ }
2753
+ for (let i = 0; i < cpuIndex.size; i += 1) {
2754
+ const base = i * SLOT_STRIDE;
2755
+ gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
2756
+ gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
2757
+ gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
2758
+ gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
2759
+ gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
2760
+ gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
2761
+ }
2762
+ gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
2763
+ gpuIndex.attribute.needsUpdate = true;
2764
+ gpuIndex.node.needsUpdate = true;
2765
+ }
2766
+ function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
2767
+ const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
2768
+ return spatialIndex.node.element(offset).toUint();
2769
+ }
2770
+ const mix32 = Fn(([x]) => {
2771
+ const v = uint(x).toVar();
2772
+ v.assign(v.bitXor(v.shiftRight(uint(16))));
2773
+ v.assign(v.mul(uint(2146121005)));
2774
+ v.assign(v.bitXor(v.shiftRight(uint(15))));
2775
+ v.assign(v.mul(uint(2221713035)));
2776
+ v.assign(v.bitXor(v.shiftRight(uint(16))));
2777
+ return v;
2778
+ });
2779
+ const hashKey = Fn(([space, level, x, y]) => {
2780
+ const s = uint(space).bitAnd(uint(255));
2781
+ const l = uint(level).bitAnd(uint(255));
2782
+ const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32(uint(x))).bitXor(mix32(uint(y)));
2783
+ return mix32(h);
2784
+ });
2785
+ const createGpuSpatialLookup = (spatialIndex) => {
2786
+ const slotCount = spatialIndex.size;
2787
+ const mask = uint(spatialIndex.mask);
2788
+ const stampGen = spatialIndex.stampGen.toUint();
2789
+ const emptyValue = int(-1);
2790
+ return Fn(([space, level, x, y]) => {
2791
+ const s = uint(space).bitAnd(uint(255));
2792
+ const l = uint(level).bitAnd(uint(255));
2793
+ const xx = uint(x);
2794
+ const yy = uint(y);
2795
+ const result = emptyValue.toVar();
2796
+ const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
2797
+ const probes = int(0).toVar();
2798
+ Loop(slotCount, () => {
2799
+ const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
2800
+ If(stamp.notEqual(stampGen), () => {
2801
+ Break();
2802
+ });
2803
+ const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
2804
+ const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
2805
+ const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
2806
+ const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
2807
+ If(
2808
+ ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
2809
+ () => {
2810
+ result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
2811
+ Break();
2812
+ }
2813
+ );
2814
+ slot.assign(slot.add(uint(1)).bitAnd(mask));
2815
+ probes.addAssign(1);
2816
+ });
2817
+ return result;
2818
+ });
2819
+ };
2820
+ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
2821
+ const lookup = createGpuSpatialLookup(spatialIndex);
2822
+ const levelCount = Math.max(1, maxLevel + 1);
2823
+ return Fn(([worldX, worldZ]) => {
2824
+ const rootOrigin = uniforms.uRootOrigin.toVar();
2825
+ const rootSize = uniforms.uRootSize.toVar();
2826
+ const halfRoot = rootSize.mul(float(0.5));
2827
+ const tileIndex = int(-1).toVar();
2828
+ const tileU = float(0).toVar();
2829
+ const tileV = float(0).toVar();
2830
+ const i = int(0).toVar();
2831
+ Loop(levelCount, () => {
2832
+ const level = int(maxLevel).sub(i).toVar();
2833
+ const scale = pow(float(2), level.toFloat());
2834
+ const tileSize = rootSize.div(scale);
2835
+ const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
2836
+ const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
2837
+ const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
2838
+ If(maybeIndex.greaterThanEqual(int(0)), () => {
2839
+ const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
2840
+ const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
2841
+ tileIndex.assign(maybeIndex);
2842
+ tileU.assign(worldX.sub(minX).div(tileSize));
2843
+ tileV.assign(worldZ.sub(minZ).div(tileSize));
2844
+ Break();
2845
+ });
2846
+ i.addAssign(1);
2847
+ });
2848
+ return vec3(tileIndex.toFloat(), tileU, tileV);
2849
+ });
2850
+ };
2851
+ const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
2852
+ const lookup = createGpuSpatialLookup(spatialIndex);
2853
+ const levelCount = Math.max(1, maxLevel + 1);
2854
+ return Fn(([direction]) => {
2855
+ const dir = vec3(direction).normalize().toVar();
2856
+ const face = cubeFaceFromDirection(dir).toVar();
2857
+ const basis = cubeFaceBasis(face);
2858
+ const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
2859
+ const u = faceUV.x.toVar();
2860
+ const v = faceUV.y.toVar();
2861
+ const tileIndex = int(-1).toVar();
2862
+ const tileU = float(0).toVar();
2863
+ const tileV = float(0).toVar();
2864
+ const i = int(0).toVar();
2865
+ Loop(levelCount, () => {
2866
+ const level = int(maxLevel).sub(i).toVar();
2867
+ const n = pow(float(2), level.toFloat()).toVar();
2868
+ const nInt = int(n).toVar();
2869
+ const tileX = u.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
2870
+ const tileY = v.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
2871
+ const maybeIndex = lookup(face, level, tileX, tileY).toVar();
2872
+ If(maybeIndex.greaterThanEqual(int(0)), () => {
2873
+ tileIndex.assign(maybeIndex);
2874
+ tileU.assign(u.mul(n).sub(tileX.toFloat()));
2875
+ tileV.assign(v.mul(n).sub(tileY.toFloat()));
2876
+ Break();
2877
+ });
2878
+ i.addAssign(1);
2879
+ });
2880
+ return vec3(tileIndex.toFloat(), tileU, tileV);
2881
+ });
2882
+ };
2883
+
2884
+ const gpuSpatialIndexStorageTask = task((get, work) => {
2885
+ const maxNodesValue = get(maxNodes);
2886
+ return work(() => createGpuSpatialIndex(maxNodesValue));
2887
+ }).displayName("gpuSpatialIndexStorageTask");
2888
+ const gpuSpatialIndexUploadTask = task((get, work) => {
2889
+ const quadtreeConfig = get(quadtreeConfigTask);
2890
+ get(quadtreeUpdateTask);
2891
+ const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
2892
+ return work(() => {
2893
+ uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
2894
+ return gpuSpatialIndex;
2895
+ });
2896
+ }).displayName("gpuSpatialIndexUploadTask");
2897
+
2898
+ function packedSampleFromTileResult(params, tileResult) {
2899
+ const tileIndex = int(tileResult.x).toVar();
2900
+ const safeTileIndex = tileIndex.max(int(0)).toVar();
2901
+ const fieldU = tileLocalToFieldUV(
2902
+ tileResult.y,
2903
+ params.uniforms.uInnerTileSegments
2904
+ ).toVar();
2905
+ const fieldV = tileLocalToFieldUV(
2906
+ tileResult.z,
2907
+ params.uniforms.uInnerTileSegments
2908
+ ).toVar();
2909
+ const found = tileIndex.greaterThanEqual(int(0)).toVar();
2910
+ const sampled = sampleTerrainField(
2911
+ params.terrainFieldStorage,
2912
+ fieldU,
2913
+ fieldV,
2914
+ safeTileIndex
2915
+ ).toVar();
2916
+ const normal = unpackTangentNormal(sampled.g, sampled.b);
2917
+ const valid = found.select(float(1), float(0)).toVar();
2918
+ return vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2919
+ }
2920
+ function createTerrainSampleNode(params) {
2921
+ const tileLookup = createTileIndexFromWorldPosition(
2922
+ params.spatialIndex,
2923
+ params.uniforms,
2924
+ params.maxLevel
2925
+ );
2926
+ return Fn(([worldX, worldZ]) => {
2927
+ const tileResult = tileLookup(worldX, worldZ).toVar();
2928
+ return packedSampleFromTileResult(params, tileResult);
2929
+ });
2930
+ }
2931
+ function createTerrainSampleNodeByDirection(params) {
2932
+ const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
2933
+ return Fn(([direction]) => {
2934
+ const tileResult = tileLookup(direction).toVar();
2935
+ return packedSampleFromTileResult(params, tileResult);
2936
+ });
2937
+ }
2938
+ function createTerrainSampler(params) {
2939
+ const elevationNode = createElevationFunction(params.elevationCallback);
2940
+ const terrainSampleAt = createTerrainSampleNode(params);
2941
+ const evaluateElevationAt = Fn(([worldX, worldZ]) => {
2942
+ const rootOrigin = params.uniforms.uRootOrigin.toVar();
2943
+ const rootSize = params.uniforms.uRootSize.toVar();
2944
+ const centeredX = worldX.sub(rootOrigin.x);
2945
+ const centeredZ = worldZ.sub(rootOrigin.z);
2946
+ const rootUV = vec2(
2947
+ centeredX.div(rootSize).add(0.5),
2948
+ centeredZ.div(rootSize).mul(float(-1)).add(0.5)
2949
+ ).toVar();
2950
+ return elevationNode({
2951
+ worldPosition: vec3(worldX, rootOrigin.y, worldZ),
2952
+ rootSize,
2953
+ rootUV,
2954
+ tileUV: rootUV,
2955
+ tileLevel: int(0),
2956
+ tileSize: rootSize,
2957
+ tileOriginVec2: vec2(0, 0),
2958
+ nodeIndex: int(0)
2959
+ });
2960
+ });
2961
+ const sampleTerrain = Fn(
2962
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
2963
+ );
2964
+ const sampleElevation = Fn(
2965
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
2966
+ );
2967
+ const sampleNormal = Fn(([worldX, worldZ]) => {
2968
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2969
+ return vec3(sample.y, sample.z, sample.w);
2970
+ });
2971
+ const sampleValidity = Fn(([worldX, worldZ]) => {
2972
+ const sample = terrainSampleAt(worldX, worldZ).toVar();
2973
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
2974
+ });
2975
+ const evaluateElevation = Fn(
2976
+ ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
2977
+ );
2978
+ const evaluateNormalNode = Fn(
2979
+ ([worldX, worldZ, epsilon]) => {
2980
+ const eps = epsilon ?? float(0.1);
2981
+ const elevationScale = params.uniforms.uElevationScale.toVar();
2982
+ const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
2983
+ elevationScale
2984
+ );
2985
+ const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
2986
+ elevationScale
2987
+ );
2988
+ const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
2989
+ elevationScale
2990
+ );
2991
+ const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
2992
+ elevationScale
2993
+ );
2994
+ const inv2eps = float(0.5).div(eps);
2995
+ const dhdx = hR.sub(hL).mul(inv2eps);
2996
+ const dhdz = hU.sub(hD).mul(inv2eps);
2997
+ return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
2998
+ }
2999
+ );
3000
+ const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
3001
+ const sampler = {
3002
+ sampleElevation,
3003
+ sampleNormal,
3004
+ sampleTerrain,
3005
+ sampleValidity,
3006
+ evaluateElevation,
3007
+ evaluateNormal
3008
+ };
3009
+ if (params.projection === "cubeSphere") {
3010
+ const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
3011
+ sampler.sampleTerrainByDirection = Fn(
3012
+ ([direction]) => terrainSampleByDir(direction)
3013
+ );
3014
+ sampler.sampleElevationByDirection = Fn(
3015
+ ([direction]) => terrainSampleByDir(direction).x
3016
+ );
3017
+ sampler.sampleValidityByDirection = Fn(([direction]) => {
3018
+ const sample = terrainSampleByDir(direction).toVar();
3019
+ return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
3020
+ });
3021
+ sampler.sampleNormalByDirection = Fn(([direction]) => {
3022
+ const dir = vec3(direction).normalize().toVar();
3023
+ const packed = terrainSampleByDir(direction).toVar();
3024
+ const basis = cubeFaceBasis(cubeFaceFromDirection(dir));
3025
+ return sphereTangentFrameNormal(dir, basis, vec3(packed.y, packed.z, packed.w));
3026
+ });
3027
+ }
3028
+ return sampler;
3029
+ }
3030
+
3031
+ const createTerrainSamplerTask = task((get, work) => {
3032
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
3033
+ const spatialIndex = get(gpuSpatialIndexStorageTask);
3034
+ const uniforms = get(updateUniformsTask);
3035
+ const elevationCallback = get(elevationFn);
3036
+ const maxLevelValue = get(maxLevel);
3037
+ const projection = get(topologyTask).projection ?? "flat";
3038
+ return work(
3039
+ () => createTerrainSampler({
3040
+ terrainFieldStorage,
3041
+ spatialIndex,
3042
+ uniforms,
3043
+ elevationCallback,
3044
+ maxLevel: maxLevelValue,
3045
+ projection
3046
+ })
3047
+ );
3048
+ }).displayName("createTerrainSamplerTask");
3049
+
216
3050
  const isSkirtVertex = Fn(([segments]) => {
217
3051
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
218
3052
  const vIndex = int(vertexIndex);
@@ -233,4 +3067,542 @@ const isSkirtUV = Fn(([segments]) => {
233
3067
  return innerX.and(innerY).not();
234
3068
  });
235
3069
 
236
- export { TerrainGeometry, isSkirtUV, isSkirtVertex };
3070
+ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
3071
+ return Fn(() => {
3072
+ const tile = decodeLeafTile(leafStorage, int(instanceIndex));
3073
+ const rootSize = terrainUniforms.uRootSize.toVar();
3074
+ const rootOrigin = terrainUniforms.uRootOrigin.toVar();
3075
+ const half = float(0.5);
3076
+ const size = rootSize.div(pow(float(2), tile.level.toFloat()));
3077
+ const halfRoot = rootSize.mul(half);
3078
+ const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
3079
+ const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
3080
+ const clampedX = positionLocal.x.max(half.negate()).min(half);
3081
+ const clampedZ = positionLocal.z.max(half.negate()).min(half);
3082
+ const worldX = centerX.add(clampedX.mul(size));
3083
+ const worldZ = centerZ.add(clampedZ.mul(size));
3084
+ return vec3(worldX, rootOrigin.y, worldZ);
3085
+ });
3086
+ }
3087
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
3088
+ if (!terrainFieldStorage) return float(0);
3089
+ const innerSegs = terrainUniforms.uInnerTileSegments;
3090
+ const u = tileLocalToFieldUV(positionLocal.x.add(float(0.5)), innerSegs);
3091
+ const v = tileLocalToFieldUV(positionLocal.z.add(float(0.5)), innerSegs);
3092
+ return sampleTerrainFieldElevation(terrainFieldStorage, u, v, int(instanceIndex)).mul(
3093
+ terrainUniforms.uElevationScale
3094
+ );
3095
+ }
3096
+ function createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
3097
+ if (!terrainFieldStorage) return;
3098
+ normalLocal.assign(
3099
+ createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection)
3100
+ );
3101
+ }
3102
+ function loadTangentNormal(terrainUniforms, terrainFieldStorage) {
3103
+ const nodeIndex = int(instanceIndex);
3104
+ const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
3105
+ const localVertexIndex = int(vertexIndex);
3106
+ const ix = localVertexIndex.mod(edgeVertexCount);
3107
+ const iy = localVertexIndex.div(edgeVertexCount);
3108
+ const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
3109
+ const normal = unpackTangentNormal(normalXZ.x, normalXZ.y);
3110
+ return { ix, iy, normal };
3111
+ }
3112
+ function createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
3113
+ if (!terrainFieldStorage) return vec3(0, 1, 0);
3114
+ if (projection === "cubeSphere") {
3115
+ return Fn(() => {
3116
+ const { ix, iy, normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
3117
+ const tile = decodeLeafTile(leafStorage, int(instanceIndex));
3118
+ const innerSeg = terrainUniforms.uInnerTileSegments.toVar().toFloat();
3119
+ const localU = ix.toFloat().sub(float(1)).div(innerSeg).max(float(0)).min(float(1));
3120
+ const localV = iy.toFloat().sub(float(1)).div(innerSeg).max(float(0)).min(float(1));
3121
+ const faceUV = faceUVFromTileLocal(tile, localU, localV);
3122
+ const basis = cubeFaceBasis(tile.face);
3123
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
3124
+ return sphereTangentFrameNormal(dir, basis, normal);
3125
+ })();
3126
+ }
3127
+ return Fn(() => {
3128
+ const { normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
3129
+ return normal;
3130
+ })();
3131
+ }
3132
+ function createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
3133
+ return Fn(() => {
3134
+ const tile = decodeLeafTile(leafStorage, int(instanceIndex));
3135
+ const half = float(0.5);
3136
+ const localU = positionLocal.x.max(half.negate()).min(half).add(half);
3137
+ const localV = positionLocal.z.max(half.negate()).min(half).add(half);
3138
+ const faceUV = faceUVFromTileLocal(tile, localU, localV);
3139
+ const basis = cubeFaceBasis(tile.face);
3140
+ const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
3141
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
3142
+ const baseRadius = terrainUniforms.uRadius.toVar().add(yElevation);
3143
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
3144
+ const r = select(skirtVertex, baseRadius.sub(terrainUniforms.uSkirtScale.toVar()), baseRadius);
3145
+ createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "cubeSphere");
3146
+ const origin = terrainUniforms.uRootOrigin.toVar();
3147
+ return origin.add(dir.mul(r));
3148
+ })();
3149
+ }
3150
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
3151
+ if (projection === "cubeSphere") {
3152
+ return createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage);
3153
+ }
3154
+ const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
3155
+ return Fn(() => {
3156
+ const base = baseWorldPosition();
3157
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
3158
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
3159
+ const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
3160
+ const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
3161
+ createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "flat");
3162
+ return vec3(base.x, worldY, base.z);
3163
+ })();
3164
+ }
3165
+
3166
+ const positionNodeTask = task((get, work) => {
3167
+ const leafStorage = get(leafStorageTask);
3168
+ const terrainUniforms = get(updateUniformsTask);
3169
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
3170
+ const topology = get(topologyTask);
3171
+ return work(
3172
+ () => createTileWorldPosition(
3173
+ leafStorage,
3174
+ terrainUniforms,
3175
+ terrainFieldStorage,
3176
+ topology.projection ?? "flat"
3177
+ )
3178
+ );
3179
+ }).displayName("positionNodeTask");
3180
+
3181
+ function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
3182
+ let tMin = -Infinity;
3183
+ let tMax = Infinity;
3184
+ const origin = ray.origin;
3185
+ const dir = ray.direction;
3186
+ const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
3187
+ if (Math.abs(dirAxis) < 1e-8) {
3188
+ if (originAxis < minAxis || originAxis > maxAxis) return false;
3189
+ return true;
3190
+ }
3191
+ const inv = 1 / dirAxis;
3192
+ let t0 = (minAxis - originAxis) * inv;
3193
+ let t1 = (maxAxis - originAxis) * inv;
3194
+ if (t0 > t1) {
3195
+ const tmp = t0;
3196
+ t0 = t1;
3197
+ t1 = tmp;
3198
+ }
3199
+ tMin = Math.max(tMin, t0);
3200
+ tMax = Math.min(tMax, t1);
3201
+ return tMax >= tMin;
3202
+ };
3203
+ if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
3204
+ return null;
3205
+ }
3206
+ return { tMin, tMax };
3207
+ }
3208
+ function getTerrainBounds(config) {
3209
+ const halfRoot = config.rootSize * 0.5;
3210
+ return {
3211
+ minX: config.originX - halfRoot,
3212
+ maxX: config.originX + halfRoot,
3213
+ minZ: config.originZ - halfRoot,
3214
+ maxZ: config.originZ + halfRoot
3215
+ };
3216
+ }
3217
+ function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
3218
+ if (!skipBoundsFastPath) {
3219
+ const tileBounds = query.getTileBounds(worldX, worldZ);
3220
+ if (tileBounds) {
3221
+ if (worldY > tileBounds.maxElevation) {
3222
+ return worldY - tileBounds.maxElevation;
3223
+ }
3224
+ if (worldY < tileBounds.minElevation) {
3225
+ return worldY - tileBounds.minElevation;
3226
+ }
3227
+ }
3228
+ }
3229
+ const elevation = query.getElevation(worldX, worldZ);
3230
+ if (!Number.isFinite(elevation)) return void 0;
3231
+ return worldY - elevation;
3232
+ }
3233
+ function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
3234
+ let prevT = startT;
3235
+ ray.at(prevT, point);
3236
+ let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
3237
+ if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
3238
+ return startT;
3239
+ }
3240
+ for (let i = 1; i <= options.maxSteps; i += 1) {
3241
+ const t = startT + (endT - startT) * i / options.maxSteps;
3242
+ ray.at(t, point);
3243
+ const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
3244
+ if (signedDistance === void 0) {
3245
+ prevSignedDistance = void 0;
3246
+ prevT = t;
3247
+ continue;
3248
+ }
3249
+ if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
3250
+ let lo = prevT;
3251
+ let hi = t;
3252
+ for (let r = 0; r < options.refinementSteps; r += 1) {
3253
+ const mid = (lo + hi) * 0.5;
3254
+ ray.at(mid, point);
3255
+ const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
3256
+ if (midDistance === void 0) {
3257
+ lo = mid;
3258
+ continue;
3259
+ }
3260
+ if (midDistance > 0) lo = mid;
3261
+ else hi = mid;
3262
+ }
3263
+ return hi;
3264
+ }
3265
+ prevSignedDistance = signedDistance;
3266
+ prevT = t;
3267
+ }
3268
+ return null;
3269
+ }
3270
+ function cpuRaycast(query, ray, config, options) {
3271
+ const bounds = getTerrainBounds(config);
3272
+ const segment = intersectRayAabb(
3273
+ ray,
3274
+ bounds.minX,
3275
+ config.minY,
3276
+ bounds.minZ,
3277
+ bounds.maxX,
3278
+ config.maxY,
3279
+ bounds.maxZ
3280
+ );
3281
+ if (!segment) return null;
3282
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3283
+ const startT = Math.max(0, segment.tMin);
3284
+ const endT = Math.min(segment.tMax, maxDistance);
3285
+ if (endT < startT) return null;
3286
+ const point = new Vector3();
3287
+ const hitT = marchSignedDistance(
3288
+ ray,
3289
+ startT,
3290
+ endT,
3291
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
3292
+ (px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
3293
+ {
3294
+ maxSteps: Math.max(8, options?.maxSteps ?? 128),
3295
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
3296
+ },
3297
+ point
3298
+ );
3299
+ if (hitT === null) return null;
3300
+ ray.at(hitT, point);
3301
+ const sample = query.sampleTerrain(point.x, point.z);
3302
+ if (!sample.valid) return null;
3303
+ point.y = sample.elevation;
3304
+ return {
3305
+ position: point.clone(),
3306
+ normal: sample.normal.clone(),
3307
+ distance: ray.origin.distanceTo(point)
3308
+ };
3309
+ }
3310
+ function cpuRaycastBoundsOnly(ray, config, options) {
3311
+ const bounds = getTerrainBounds(config);
3312
+ const planeY = (config.minY + config.maxY) * 0.5;
3313
+ const dirY = ray.direction.y;
3314
+ if (Math.abs(dirY) < 1e-8) return null;
3315
+ const t = (planeY - ray.origin.y) / dirY;
3316
+ if (t < 0) return null;
3317
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3318
+ if (t > maxDistance) return null;
3319
+ const point = new Vector3();
3320
+ ray.at(t, point);
3321
+ if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
3322
+ return null;
3323
+ }
3324
+ return {
3325
+ position: point,
3326
+ normal: new Vector3(0, 1, 0),
3327
+ distance: ray.origin.distanceTo(point)
3328
+ };
3329
+ }
3330
+ function intersectRaySphere(ray, cx, cy, cz, radius) {
3331
+ const ox = ray.origin.x - cx;
3332
+ const oy = ray.origin.y - cy;
3333
+ const oz = ray.origin.z - cz;
3334
+ const dx = ray.direction.x;
3335
+ const dy = ray.direction.y;
3336
+ const dz = ray.direction.z;
3337
+ const a = dx * dx + dy * dy + dz * dz;
3338
+ const b = 2 * (ox * dx + oy * dy + oz * dz);
3339
+ const c = ox * ox + oy * oy + oz * oz - radius * radius;
3340
+ const disc = b * b - 4 * a * c;
3341
+ if (disc < 0) return null;
3342
+ const sqrtDisc = Math.sqrt(disc);
3343
+ const inv2a = 1 / (2 * a);
3344
+ return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
3345
+ }
3346
+ function sphereSignedDistance(query, config, px, py, pz, scratchDir) {
3347
+ const cx = config.centerX ?? 0;
3348
+ const cy = config.centerY ?? 0;
3349
+ const cz = config.centerZ ?? 0;
3350
+ const radius = config.radius ?? 0;
3351
+ const dx = px - cx;
3352
+ const dy = py - cy;
3353
+ const dz = pz - cz;
3354
+ const dist = Math.hypot(dx, dy, dz);
3355
+ scratchDir.set(dx, dy, dz);
3356
+ const elevation = query.getElevationByDirection(scratchDir);
3357
+ if (elevation === null) return void 0;
3358
+ return dist - (radius + elevation);
3359
+ }
3360
+ function cubeSphereRaycast(query, ray, config, options) {
3361
+ const cx = config.centerX ?? 0;
3362
+ const cy = config.centerY ?? 0;
3363
+ const cz = config.centerZ ?? 0;
3364
+ const radius = config.radius ?? 0;
3365
+ const outerRadius = config.maxRadius ?? radius;
3366
+ const shell = intersectRaySphere(ray, cx, cy, cz, outerRadius);
3367
+ if (!shell) return null;
3368
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3369
+ const startT = Math.max(0, shell.t0);
3370
+ const endT = Math.min(shell.t1, maxDistance);
3371
+ if (endT < startT) return null;
3372
+ const scratchDir = new Vector3();
3373
+ const point = new Vector3();
3374
+ const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, config, px, py, pz, scratchDir);
3375
+ const hitT = marchSignedDistance(
3376
+ ray,
3377
+ startT,
3378
+ endT,
3379
+ signedDistanceAt,
3380
+ signedDistanceAt,
3381
+ {
3382
+ maxSteps: Math.max(8, options?.maxSteps ?? 256),
3383
+ refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
3384
+ },
3385
+ point
3386
+ );
3387
+ if (hitT === null) return null;
3388
+ ray.at(hitT, point);
3389
+ const sample = query.sampleTerrainByPosition(point);
3390
+ if (!sample.valid) return null;
3391
+ return {
3392
+ position: sample.position.clone(),
3393
+ normal: sample.normal.clone(),
3394
+ distance: ray.origin.distanceTo(sample.position)
3395
+ };
3396
+ }
3397
+ function cubeSphereRaycastBoundsOnly(ray, config, options) {
3398
+ const cx = config.centerX ?? 0;
3399
+ const cy = config.centerY ?? 0;
3400
+ const cz = config.centerZ ?? 0;
3401
+ const radius = config.radius ?? 0;
3402
+ const shell = intersectRaySphere(ray, cx, cy, cz, radius);
3403
+ if (!shell) return null;
3404
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
3405
+ const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
3406
+ if (t < 0 || t > maxDistance) return null;
3407
+ const point = new Vector3();
3408
+ ray.at(t, point);
3409
+ const normal = new Vector3(point.x - cx, point.y - cy, point.z - cz).normalize();
3410
+ return { position: point, normal, distance: ray.origin.distanceTo(point) };
3411
+ }
3412
+
3413
+ function createTerrainRaycast(params) {
3414
+ return {
3415
+ pick(ray, options) {
3416
+ const config = params.getConfig();
3417
+ const terrainQuery = params.getTerrainQuery();
3418
+ if (config.projection === "cubeSphere") {
3419
+ const sphereQuery = params.getSphereQuery();
3420
+ if (sphereQuery) {
3421
+ const precise = cubeSphereRaycast(sphereQuery, ray, config, options);
3422
+ if (precise) return precise;
3423
+ }
3424
+ return cubeSphereRaycastBoundsOnly(ray, config, options);
3425
+ }
3426
+ if (terrainQuery) {
3427
+ const precise = cpuRaycast(terrainQuery, ray, config, options);
3428
+ if (precise) return precise;
3429
+ }
3430
+ const coarse = cpuRaycastBoundsOnly(ray, config, options);
3431
+ if (coarse && terrainQuery) {
3432
+ const sample = terrainQuery.sampleTerrain(
3433
+ coarse.position.x,
3434
+ coarse.position.z
3435
+ );
3436
+ if (sample.valid) {
3437
+ coarse.position.y = sample.elevation;
3438
+ coarse.normal.copy(sample.normal);
3439
+ }
3440
+ }
3441
+ return coarse;
3442
+ }
3443
+ };
3444
+ }
3445
+
3446
+ const BOUNDS_PADDING = 1;
3447
+ const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
3448
+ const terrainRaycastTask = task(
3449
+ (get, work) => {
3450
+ const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
3451
+ const rootSizeValue = get(rootSize);
3452
+ const originValue = get(origin);
3453
+ const elevationScaleValue = get(elevationScale);
3454
+ const radiusValue = get(radius);
3455
+ const topologyValue = get(topologyTask);
3456
+ const projection = topologyValue.projection ?? "flat";
3457
+ const sphereRadius = topologyValue.radius ?? radiusValue;
3458
+ return work((prev) => {
3459
+ let raycast = prev;
3460
+ let state = raycast?.[RAYCAST_STATE];
3461
+ if (!state) {
3462
+ state = {
3463
+ terrainQuery: null,
3464
+ sphereQuery: null,
3465
+ bounds: {
3466
+ rootSize: 0,
3467
+ originX: 0,
3468
+ originZ: 0,
3469
+ minY: 0,
3470
+ maxY: 0,
3471
+ projection: "flat",
3472
+ centerX: 0,
3473
+ centerY: 0,
3474
+ centerZ: 0,
3475
+ radius: 0,
3476
+ minRadius: 0,
3477
+ maxRadius: 0
3478
+ }
3479
+ };
3480
+ }
3481
+ state.terrainQuery = terrainQuery;
3482
+ state.sphereQuery = sphereQuery;
3483
+ state.bounds.rootSize = rootSizeValue;
3484
+ state.bounds.originX = originValue.x;
3485
+ state.bounds.originZ = originValue.z;
3486
+ state.bounds.projection = projection;
3487
+ state.bounds.centerX = originValue.x;
3488
+ state.bounds.centerY = originValue.y;
3489
+ state.bounds.centerZ = originValue.z;
3490
+ state.bounds.radius = sphereRadius;
3491
+ const range = terrainQuery.getGlobalElevationRange();
3492
+ if (range) {
3493
+ state.bounds.minY = range.min - BOUNDS_PADDING;
3494
+ state.bounds.maxY = range.max + BOUNDS_PADDING;
3495
+ } else {
3496
+ const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
3497
+ state.bounds.minY = originValue.y - verticalExtent;
3498
+ state.bounds.maxY = originValue.y + verticalExtent;
3499
+ }
3500
+ const elevationExtent = Math.max(1, Math.abs(elevationScaleValue));
3501
+ let dispMin = -elevationExtent;
3502
+ let dispMax = elevationExtent;
3503
+ if (range) {
3504
+ dispMin = range.min - originValue.y;
3505
+ dispMax = range.max - originValue.y;
3506
+ }
3507
+ state.bounds.minRadius = Math.max(0, sphereRadius + dispMin - BOUNDS_PADDING);
3508
+ state.bounds.maxRadius = sphereRadius + dispMax + BOUNDS_PADDING;
3509
+ if (!raycast) {
3510
+ raycast = createTerrainRaycast({
3511
+ getTerrainQuery: () => state.terrainQuery,
3512
+ getSphereQuery: () => state.sphereQuery,
3513
+ getConfig: () => state.bounds
3514
+ });
3515
+ }
3516
+ raycast[RAYCAST_STATE] = state;
3517
+ return raycast;
3518
+ });
3519
+ }
3520
+ ).displayName("terrainRaycastTask");
3521
+
3522
+ const terrainTasks = {
3523
+ instanceId: instanceIdTask,
3524
+ quadtreeConfig: quadtreeConfigTask,
3525
+ quadtreeUpdate: quadtreeUpdateTask,
3526
+ leafStorage: leafStorageTask,
3527
+ topology: topologyTask,
3528
+ leafGpuBuffer: leafGpuBufferTask,
3529
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
3530
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
3531
+ createUniforms: createUniformsTask,
3532
+ updateUniforms: updateUniformsTask,
3533
+ positionNode: positionNodeTask,
3534
+ createElevationFieldContext: createElevationFieldContextTask,
3535
+ createTileNodes: tileNodesTask,
3536
+ createTerrainFieldTexture: createTerrainFieldTextureTask,
3537
+ createTerrainSampler: createTerrainSamplerTask,
3538
+ elevationFieldStage: elevationFieldStageTask,
3539
+ terrainFieldStage: terrainFieldStageTask,
3540
+ compileCompute: compileComputeTask,
3541
+ executeCompute: executeComputeTask,
3542
+ tileBoundsContext: tileBoundsContextTask,
3543
+ tileBoundsReduction: tileBoundsReductionTask,
3544
+ terrainQuery: terrainQueryTask,
3545
+ terrainReadback: terrainReadbackTask,
3546
+ terrainRaycast: terrainRaycastTask
3547
+ };
3548
+ function terrainGraph() {
3549
+ const g = graph();
3550
+ for (const t of Object.values(terrainTasks)) {
3551
+ g.add(t);
3552
+ }
3553
+ return g;
3554
+ }
3555
+
3556
+ const textureSpaceToVectorSpace = Fn(([value]) => {
3557
+ return remap(value, float(0), float(1), float(-1), float(1));
3558
+ });
3559
+ const vectorSpaceToTextureSpace = Fn(([value]) => {
3560
+ return remap(value, float(-1), float(1), float(0), float(1));
3561
+ });
3562
+ const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
3563
+ const t = vec3(n1.x, n1.y, n1.z.add(1));
3564
+ const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
3565
+ const r = t.mul(dot$1(t, u)).sub(u.mul(t.z)).normalize();
3566
+ return r;
3567
+ });
3568
+ const deriveNormalZ = Fn(([normalXY]) => {
3569
+ const xy = normalXY.toVar();
3570
+ const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
3571
+ return vec3(xy.x, xy.y, z);
3572
+ });
3573
+
3574
+ const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
3575
+ const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
3576
+
3577
+ const cellCenter = Fn(({ cell }) => {
3578
+ return cell.add(mx_noise_float(cell.mul(Math.PI)));
3579
+ });
3580
+ const voronoiCells = Fn((params) => {
3581
+ const scale = float(params.scale);
3582
+ const facet = float(params.facet);
3583
+ const seed = float(params.seed);
3584
+ const pos = params.uv.mul(scale).add(seed);
3585
+ const midCell = pos.round().toVar();
3586
+ const minCell = midCell.toVar();
3587
+ const minDist = float(1).toVar();
3588
+ const cell = vec3(0, 0, 0).toVar();
3589
+ const dist = float().toVar();
3590
+ const i = float(0).toVar();
3591
+ Loop(27, () => {
3592
+ const ix = i.mod(3).sub(1);
3593
+ const iy = i.div(3).floor().mod(3).sub(1);
3594
+ const iz = i.div(9).floor().sub(1);
3595
+ cell.assign(midCell.add(vec3(ix, iy, iz)));
3596
+ dist.assign(pos.distance(cellCenter({ cell })).add(mx_noise_float(pos).div(5)));
3597
+ If(dist.lessThan(minDist), () => {
3598
+ minDist.assign(dist);
3599
+ minCell.assign(cell);
3600
+ });
3601
+ i.addAssign(1);
3602
+ });
3603
+ const n = mx_noise_float(minCell.mul(Math.PI)).toVar();
3604
+ const k = mix(minDist, n.add(1).div(2), facet);
3605
+ return k;
3606
+ });
3607
+
3608
+ export { ArrayTextureBackend, AtlasBackend, CUBE_FACES, CUBE_FACE_COUNT, Dir, TerrainGeometry, TerrainMesh, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereTopology, createElevationFieldContextTask, createFlatTopology, createInfiniteFlatTopology, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainQuery, createTerrainRaycast, createTerrainSampler, createTerrainSamplerTask, createTerrainSphereQuery, createTerrainUniforms, 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, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, radius, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, skirtScale, sphereTangentFrameNormal, storeTerrainField, tangentFromAxis, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainQueryTask, terrainRaycastTask, terrainReadbackTask, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, topology, topologyTask, unpackTangentNormal, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };