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