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

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,7 +1,10 @@
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
10
  constructor(innerSegments = 14, extendUV = false) {
@@ -61,17 +64,19 @@ class TerrainGeometry extends three.BufferGeometry {
61
64
  * | / | \ | / | \ |
62
65
  * o---o---o---o---o
63
66
  *
64
- * INNER GRID (consistent diagonal, no rotational symmetry):
65
- * o---o---o
66
- * | \ | \ |
67
- * o---o---o
68
- * | \ | \ |
69
- * o---o---o
67
+ * INNER GRID (alternating diagonals checkerboard pattern):
68
+ * o---o---o---o---o
69
+ * | \ | / | \ | / |
70
+ * o---o---o---o---o
71
+ * | / | \ | / | \ |
72
+ * o---o---o---o---o
73
+ * | \ | / | \ | / |
74
+ * o---o---o---o---o
70
75
  *
71
76
  * Where o = vertex
72
77
  * Each square cell is split into 2 triangles.
73
78
  * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
74
- * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
79
+ * - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
75
80
  *
76
81
  * Vertex layout (for innerSegments = 2):
77
82
  *
@@ -117,7 +122,7 @@ class TerrainGeometry extends three.BufferGeometry {
117
122
  const topHalf = y < mid;
118
123
  useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
119
124
  } else {
120
- useDefaultDiagonal = true;
125
+ useDefaultDiagonal = (x + y) % 2 === 0;
121
126
  }
122
127
  if (useDefaultDiagonal) {
123
128
  indices.push(a, d, b);
@@ -215,6 +220,2203 @@ class TerrainGeometry extends three.BufferGeometry {
215
220
  }
216
221
  }
217
222
 
223
+ const defaultTerrainMeshParams = {
224
+ innerTileSegments: 14,
225
+ maxNodes: 1024,
226
+ material: new webgpu.MeshStandardNodeMaterial()
227
+ };
228
+ class TerrainMesh extends webgpu.InstancedMesh {
229
+ _innerTileSegments;
230
+ _maxNodes;
231
+ terrainRaycast = null;
232
+ constructor(params = defaultTerrainMeshParams) {
233
+ const mergedParams = { ...defaultTerrainMeshParams, ...params };
234
+ const { innerTileSegments, maxNodes, material } = mergedParams;
235
+ const geometry = new TerrainGeometry(innerTileSegments, true);
236
+ super(geometry, material, maxNodes);
237
+ this.frustumCulled = false;
238
+ this._innerTileSegments = innerTileSegments;
239
+ this._maxNodes = maxNodes;
240
+ }
241
+ get innerTileSegments() {
242
+ return this._innerTileSegments;
243
+ }
244
+ set innerTileSegments(tileSegments) {
245
+ const oldGeometry = this.geometry;
246
+ this.geometry = new TerrainGeometry(tileSegments, true);
247
+ this._innerTileSegments = tileSegments;
248
+ setTimeout(oldGeometry.dispose);
249
+ }
250
+ get maxNodes() {
251
+ return this._maxNodes;
252
+ }
253
+ set maxNodes(maxNodes) {
254
+ if (!Number.isInteger(maxNodes) || maxNodes < 1) {
255
+ throw new Error(`Invalid maxNodes: ${maxNodes}. Must be a positive integer.`);
256
+ }
257
+ if (maxNodes === this._maxNodes) return;
258
+ const oldMax = this._maxNodes;
259
+ const nextMatrix = new Float32Array(maxNodes * 16);
260
+ const oldMatrixArray = this.instanceMatrix.array;
261
+ nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
262
+ this.instanceMatrix = new webgpu.InstancedBufferAttribute(nextMatrix, 16);
263
+ if (this.instanceColor) {
264
+ const itemSize = this.instanceColor.itemSize;
265
+ const nextColor = new Float32Array(maxNodes * itemSize);
266
+ const oldColorArray = this.instanceColor.array;
267
+ nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
268
+ this.instanceColor = new webgpu.InstancedBufferAttribute(nextColor, itemSize);
269
+ }
270
+ this._maxNodes = maxNodes;
271
+ this.count = Math.min(this.count, maxNodes);
272
+ this.instanceMatrix.needsUpdate = true;
273
+ if (this.instanceColor) this.instanceColor.needsUpdate = true;
274
+ if (maxNodes < oldMax && this.count >= maxNodes) {
275
+ this.count = maxNodes;
276
+ }
277
+ }
278
+ raycast(raycaster, intersects) {
279
+ if (!this.terrainRaycast) {
280
+ super.raycast(raycaster, intersects);
281
+ return;
282
+ }
283
+ const result = this.terrainRaycast.pick(raycaster.ray);
284
+ if (!result) return;
285
+ intersects.push({
286
+ distance: result.distance,
287
+ point: result.position.clone(),
288
+ normal: result.normal.clone(),
289
+ object: this,
290
+ face: null,
291
+ faceIndex: -1
292
+ });
293
+ }
294
+ }
295
+
296
+ function getDeviceComputeLimits(renderer) {
297
+ const backend = renderer.backend;
298
+ const limits = backend?.device?.limits;
299
+ return {
300
+ maxWorkgroupSizeX: limits?.maxComputeWorkgroupSizeX ?? 256,
301
+ maxWorkgroupSizeY: limits?.maxComputeWorkgroupSizeY ?? 256,
302
+ maxWorkgroupInvocations: limits?.maxComputeWorkgroupInvocations ?? 256
303
+ };
304
+ }
305
+
306
+ const WORKGROUP_X = 16;
307
+ const WORKGROUP_Y = 16;
308
+ function compileComputePipeline(stages, width, options) {
309
+ const bindings = options?.bindings;
310
+ const preferredWorkgroup = options?.workgroupSize ?? [
311
+ WORKGROUP_X,
312
+ WORKGROUP_Y
313
+ ];
314
+ const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
315
+ const uInstanceCount = tsl.uniform(0, "uint");
316
+ let singleKernel;
317
+ const stagedKernelCache = /* @__PURE__ */ new Map();
318
+ function canRunSingleKernel(widthValue, limits) {
319
+ return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
320
+ }
321
+ function clampWorkgroupToLimits(requested, limits) {
322
+ let x = Math.max(1, Math.floor(requested[0]));
323
+ let y = Math.max(1, Math.floor(requested[1]));
324
+ x = Math.min(x, limits.maxWorkgroupSizeX);
325
+ y = Math.min(y, limits.maxWorkgroupSizeY);
326
+ y = Math.min(
327
+ y,
328
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / x))
329
+ );
330
+ x = Math.min(
331
+ x,
332
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / y))
333
+ );
334
+ return [x, y];
335
+ }
336
+ function buildSingleKernel(workgroupSize) {
337
+ return tsl.Fn(() => {
338
+ bindings?.forEach((b) => b.toVar());
339
+ const fWidth = tsl.float(width);
340
+ const activeIndex = tsl.globalId.z;
341
+ const nodeIndex = tsl.int(activeIndex).toVar();
342
+ const iWidth = tsl.int(width);
343
+ const ix = tsl.int(tsl.globalId.x);
344
+ const iy = tsl.int(tsl.globalId.y);
345
+ const texelSize = tsl.vec2(1, 1).div(fWidth);
346
+ const localCoordinates = tsl.vec2(tsl.globalId.x, tsl.globalId.y);
347
+ const localUVCoords = localCoordinates.div(fWidth);
348
+ const verticesPerNode = iWidth.mul(iWidth);
349
+ const globalIndex = tsl.int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
350
+ const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(tsl.uint(activeIndex).lessThan(uInstanceCount)).toVar();
351
+ for (let i = 0; i < stages.length; i++) {
352
+ if (i > 0) {
353
+ tsl.workgroupBarrier();
354
+ }
355
+ tsl.If(inBounds, () => {
356
+ stages[i](
357
+ nodeIndex,
358
+ globalIndex,
359
+ localUVCoords,
360
+ localCoordinates,
361
+ texelSize
362
+ );
363
+ });
364
+ }
365
+ })().computeKernel(workgroupSize);
366
+ }
367
+ function buildStagedKernels(workgroupSize) {
368
+ return stages.map(
369
+ (stage) => tsl.Fn(() => {
370
+ bindings?.forEach((b) => b.toVar());
371
+ const fWidth = tsl.float(width);
372
+ const activeIndex = tsl.globalId.z;
373
+ const nodeIndex = tsl.int(activeIndex).toVar();
374
+ const iWidth = tsl.int(width);
375
+ const ix = tsl.int(tsl.globalId.x);
376
+ const iy = tsl.int(tsl.globalId.y);
377
+ const texelSize = tsl.vec2(1, 1).div(fWidth);
378
+ const localCoordinates = tsl.vec2(tsl.globalId.x, tsl.globalId.y);
379
+ const localUVCoords = localCoordinates.div(fWidth);
380
+ const verticesPerNode = iWidth.mul(iWidth);
381
+ const globalIndex = tsl.int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
382
+ const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(tsl.uint(activeIndex).lessThan(uInstanceCount)).toVar();
383
+ tsl.If(inBounds, () => {
384
+ stage(
385
+ nodeIndex,
386
+ globalIndex,
387
+ localUVCoords,
388
+ localCoordinates,
389
+ texelSize
390
+ );
391
+ });
392
+ })().computeKernel(workgroupSize)
393
+ );
394
+ }
395
+ function execute(renderer, instanceCount) {
396
+ const limits = getDeviceComputeLimits(renderer);
397
+ const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
398
+ uInstanceCount.value = instanceCount;
399
+ if (canUseSingleKernel) {
400
+ if (!singleKernel) {
401
+ singleKernel = buildSingleKernel([width, width, 1]);
402
+ }
403
+ renderer.compute(singleKernel, [1, 1, instanceCount]);
404
+ return;
405
+ }
406
+ const [workgroupX, workgroupY] = clampWorkgroupToLimits(
407
+ preferredWorkgroup,
408
+ limits
409
+ );
410
+ const cacheKey = `${workgroupX}x${workgroupY}`;
411
+ let stagedKernels = stagedKernelCache.get(cacheKey);
412
+ if (!stagedKernels) {
413
+ stagedKernels = buildStagedKernels([workgroupX, workgroupY, 1]);
414
+ stagedKernelCache.set(cacheKey, stagedKernels);
415
+ }
416
+ const dispatchX = Math.ceil(width / workgroupX);
417
+ const dispatchY = Math.ceil(width / workgroupY);
418
+ for (const kernel of stagedKernels) {
419
+ renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
420
+ }
421
+ }
422
+ return { execute };
423
+ }
424
+
425
+ function resolveType(format) {
426
+ return format === "rgba16float" ? three.HalfFloatType : three.FloatType;
427
+ }
428
+ function resolveFilter(mode) {
429
+ return mode === "linear" ? three.LinearFilter : three.NearestFilter;
430
+ }
431
+ function configureStorageTexture(texture2, format, filter) {
432
+ texture2.format = three.RGBAFormat;
433
+ texture2.type = resolveType(format);
434
+ texture2.magFilter = resolveFilter(filter);
435
+ texture2.minFilter = resolveFilter(filter);
436
+ texture2.wrapS = three.ClampToEdgeWrapping;
437
+ texture2.wrapT = three.ClampToEdgeWrapping;
438
+ texture2.generateMipmaps = false;
439
+ texture2.needsUpdate = true;
440
+ }
441
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
442
+ let currentEdgeVertexCount = edgeVertexCount;
443
+ let currentTileCount = tileCount;
444
+ const tex = new webgpu.StorageArrayTexture(
445
+ edgeVertexCount,
446
+ edgeVertexCount,
447
+ tileCount
448
+ );
449
+ configureStorageTexture(tex, options.format, options.filter);
450
+ return {
451
+ backendType: "array-texture",
452
+ get edgeVertexCount() {
453
+ return currentEdgeVertexCount;
454
+ },
455
+ get tileCount() {
456
+ return currentTileCount;
457
+ },
458
+ texture: tex,
459
+ uv(ix, iy, _tileIndex) {
460
+ return tsl.vec2(ix.toFloat(), iy.toFloat());
461
+ },
462
+ texel(ix, iy, tileIndex) {
463
+ return tsl.ivec3(ix, iy, tileIndex);
464
+ },
465
+ sample(u, v, tileIndex) {
466
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
467
+ },
468
+ resize(width, height, nextTileCount) {
469
+ currentEdgeVertexCount = width;
470
+ currentTileCount = nextTileCount;
471
+ tex.setSize(width, height, nextTileCount);
472
+ tex.needsUpdate = true;
473
+ }
474
+ };
475
+ }
476
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
477
+ const tilesPerRowNode = tsl.int(tilesPerRow);
478
+ const edge = tsl.int(edgeVertexCount);
479
+ const tile = tsl.int(tileIndex);
480
+ const col = tile.mod(tilesPerRowNode);
481
+ const row = tile.div(tilesPerRowNode);
482
+ const atlasX = col.mul(edge).add(tsl.int(ix));
483
+ const atlasY = row.mul(edge).add(tsl.int(iy));
484
+ return { atlasX, atlasY };
485
+ }
486
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
487
+ let currentEdgeVertexCount = edgeVertexCount;
488
+ let currentTileCount = tileCount;
489
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
490
+ const atlasSize = tilesPerRow * edgeVertexCount;
491
+ const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
492
+ configureStorageTexture(tex, options.format, options.filter);
493
+ return {
494
+ backendType: "atlas",
495
+ get edgeVertexCount() {
496
+ return currentEdgeVertexCount;
497
+ },
498
+ get tileCount() {
499
+ return currentTileCount;
500
+ },
501
+ texture: tex,
502
+ uv(ix, iy, tileIndex) {
503
+ const { atlasX, atlasY } = atlasCoord(
504
+ tilesPerRow,
505
+ currentEdgeVertexCount,
506
+ ix,
507
+ iy,
508
+ tileIndex
509
+ );
510
+ const currentAtlasSize = tsl.float(tilesPerRow * currentEdgeVertexCount);
511
+ return tsl.vec2(
512
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
513
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
514
+ );
515
+ },
516
+ texel(ix, iy, tileIndex) {
517
+ const { atlasX, atlasY } = atlasCoord(
518
+ tilesPerRow,
519
+ currentEdgeVertexCount,
520
+ ix,
521
+ iy,
522
+ tileIndex
523
+ );
524
+ return tsl.ivec2(atlasX, atlasY);
525
+ },
526
+ sample(u, v, tileIndex) {
527
+ const tile = tsl.int(tileIndex);
528
+ const tilesPerRowNode = tsl.int(tilesPerRow);
529
+ const col = tile.mod(tilesPerRowNode);
530
+ const row = tile.div(tilesPerRowNode);
531
+ const invTilesPerRow = tsl.float(1 / tilesPerRow);
532
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
533
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
534
+ return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
535
+ },
536
+ resize(width, height, nextTileCount) {
537
+ currentEdgeVertexCount = width;
538
+ currentTileCount = nextTileCount;
539
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
540
+ const nextAtlasSize = tilesPerRow * width;
541
+ const image = tex.image;
542
+ image.width = nextAtlasSize;
543
+ image.height = nextAtlasSize;
544
+ tex.needsUpdate = true;
545
+ }
546
+ };
547
+ }
548
+ function Texture3DBackend(edgeVertexCount, tileCount, options) {
549
+ let currentEdgeVertexCount = edgeVertexCount;
550
+ let currentTileCount = tileCount;
551
+ const tex = new webgpu.StorageArrayTexture(
552
+ edgeVertexCount,
553
+ edgeVertexCount,
554
+ tileCount
555
+ );
556
+ configureStorageTexture(tex, options.format, options.filter);
557
+ return {
558
+ backendType: "texture-3d",
559
+ get edgeVertexCount() {
560
+ return currentEdgeVertexCount;
561
+ },
562
+ get tileCount() {
563
+ return currentTileCount;
564
+ },
565
+ texture: tex,
566
+ uv(ix, iy, _tileIndex) {
567
+ return tsl.vec2(ix.toFloat(), iy.toFloat());
568
+ },
569
+ texel(ix, iy, tileIndex) {
570
+ return tsl.ivec3(ix, iy, tileIndex);
571
+ },
572
+ sample(u, v, tileIndex) {
573
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
574
+ },
575
+ resize(width, height, nextTileCount) {
576
+ currentEdgeVertexCount = width;
577
+ currentTileCount = nextTileCount;
578
+ tex.setSize(width, height, nextTileCount);
579
+ tex.needsUpdate = true;
580
+ }
581
+ };
582
+ }
583
+ function tryGetDeviceLimits(renderer) {
584
+ const backend = renderer;
585
+ return backend.backend?.device?.limits ?? {};
586
+ }
587
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
588
+ const filter = options.filter ?? "linear";
589
+ const format = options.format ?? "rgba16float";
590
+ const forcedBackend = options.backend;
591
+ if (forcedBackend === "atlas") {
592
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
593
+ }
594
+ if (forcedBackend === "texture-3d") {
595
+ return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
596
+ }
597
+ if (forcedBackend === "array-texture") {
598
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
599
+ }
600
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
601
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
602
+ if (tileCount > maxLayers) {
603
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
604
+ }
605
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
606
+ }
607
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
608
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
609
+ return tsl.textureStore(
610
+ storage.texture,
611
+ tsl.uvec3(tsl.int(ix), tsl.int(iy), tsl.int(tileIndex)),
612
+ value
613
+ );
614
+ }
615
+ return tsl.textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
616
+ }
617
+ function loadTerrainField(storage, ix, iy, tileIndex) {
618
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
619
+ return tsl.textureLoad(storage.texture, tsl.ivec2(tsl.int(ix), tsl.int(iy)), tsl.int(0)).depth(
620
+ tsl.int(tileIndex)
621
+ );
622
+ }
623
+ return tsl.textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), tsl.int(0));
624
+ }
625
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
626
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
627
+ }
628
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
629
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
630
+ return tsl.vec2(raw.g, raw.b);
631
+ }
632
+ function sampleTerrainField(storage, u, v, tileIndex) {
633
+ return storage.sample(u, v, tileIndex);
634
+ }
635
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
636
+ return sampleTerrainField(storage, u, v, tileIndex).r;
637
+ }
638
+ function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
639
+ const raw = sampleTerrainField(storage, u, v, tileIndex);
640
+ return tsl.vec2(raw.g, raw.b);
641
+ }
642
+ function packTerrainFieldSample(height, normalXZ, extra = tsl.float(0)) {
643
+ return tsl.vec4(height, normalXZ.x, normalXZ.y, extra);
644
+ }
645
+
646
+ const createElevation = (tile, uniforms, elevationFn) => {
647
+ return function perVertexElevation(nodeIndex, localCoordinates) {
648
+ const ix = tsl.int(localCoordinates.x);
649
+ const iy = tsl.int(localCoordinates.y);
650
+ const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
651
+ const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
652
+ const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
653
+ const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
654
+ const rootSize = uniforms.uRootSize.toVar();
655
+ return elevationFn({
656
+ worldPosition,
657
+ rootSize,
658
+ rootUV,
659
+ tileOriginVec2: tile.tileOriginVec2(nodeIndex),
660
+ tileSize: tile.tileSize(nodeIndex),
661
+ tileLevel: tile.tileLevel(nodeIndex),
662
+ nodeIndex: tsl.int(nodeIndex),
663
+ tileUV
664
+ });
665
+ };
666
+ };
667
+
668
+ function createTileCompute(leafStorage, uniforms) {
669
+ const tileLevel = tsl.Fn(([nodeIndex]) => {
670
+ const nodeOffset = nodeIndex.mul(tsl.int(4));
671
+ return leafStorage.node.element(nodeOffset).toInt();
672
+ });
673
+ const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
674
+ const nodeOffset = nodeIndex.mul(tsl.int(4));
675
+ const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
676
+ const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
677
+ return tsl.vec2(nodeX, nodeY);
678
+ });
679
+ const tileSize = tsl.Fn(([nodeIndex]) => {
680
+ const rootSize = uniforms.uRootSize.toVar();
681
+ const level = tileLevel(nodeIndex);
682
+ return tsl.float(rootSize).div(tsl.pow(tsl.float(2), level.toFloat()));
683
+ });
684
+ const rootUVCompute = tsl.Fn(([nodeIndex, ix, iy]) => {
685
+ const nodeVec2 = tileOriginVec2(nodeIndex);
686
+ const nodeX = nodeVec2.x;
687
+ const nodeY = nodeVec2.y;
688
+ const rootSize = uniforms.uRootSize.toVar();
689
+ const rootOrigin = uniforms.uRootOrigin.toVar();
690
+ const size = tileSize(nodeIndex);
691
+ const half = tsl.float(0.5);
692
+ const halfRoot = tsl.float(rootSize).mul(half);
693
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
694
+ const texelSpacing = size.div(fInnerSegments);
695
+ const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
696
+ const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
697
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
698
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
699
+ const centeredX = worldX.sub(rootOrigin.x);
700
+ const centeredZ = worldZ.sub(rootOrigin.z);
701
+ return tsl.vec2(
702
+ centeredX.div(rootSize).add(half),
703
+ centeredZ.div(rootSize).mul(tsl.float(-1)).add(half)
704
+ );
705
+ });
706
+ const tileVertexWorldPositionCompute = tsl.Fn(
707
+ ([nodeIndex, ix, iy]) => {
708
+ const rootOrigin = uniforms.uRootOrigin.toVar();
709
+ const nodeVec2 = tileOriginVec2(nodeIndex);
710
+ const nodeX = nodeVec2.x;
711
+ const nodeY = nodeVec2.y;
712
+ const rootSize = uniforms.uRootSize.toVar();
713
+ const size = tileSize(nodeIndex);
714
+ const half = tsl.float(0.5);
715
+ const halfRoot = tsl.float(rootSize).mul(half);
716
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
717
+ const texelSpacing = size.div(fInnerSegments);
718
+ const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
719
+ const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
720
+ const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
721
+ const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
722
+ return tsl.vec3(worldX, rootOrigin.y, worldZ);
723
+ }
724
+ );
725
+ return {
726
+ tileLevel,
727
+ tileOriginVec2,
728
+ tileSize,
729
+ rootUVCompute,
730
+ tileVertexWorldPositionCompute
731
+ };
732
+ }
733
+ function tileLocalToFieldUV$1(localCoord, innerSegments) {
734
+ const edge = tsl.float(innerSegments).add(tsl.float(3));
735
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(1.5)).div(edge);
736
+ }
737
+
738
+ const rootSize = work.param(256).displayName("rootSize");
739
+ const origin = work.param({
740
+ x: 0,
741
+ y: 0,
742
+ z: 0
743
+ }).displayName("origin");
744
+ const innerTileSegments = work.param(13).displayName("innerTileSegments");
745
+ const skirtScale = work.param(100).displayName("skirtScale");
746
+ const elevationScale = work.param(1).displayName("elevationScale");
747
+ const maxNodes = work.param(1024).displayName("maxNodes");
748
+ const maxLevel = work.param(16).displayName("maxLevel");
749
+ const quadtreeUpdate = work.param({
750
+ cameraOrigin: { x: 0, y: 0, z: 0 },
751
+ mode: "distance",
752
+ distanceFactor: 1.5
753
+ }).displayName("quadtreeUpdate");
754
+ const surface = work.param(null).displayName("surface");
755
+ const terrainFieldFilter = work.param("linear").displayName("terrainFieldFilter");
756
+ const elevationFn = work.param(() => tsl.float(0));
757
+
758
+ function createLeafStorage(maxNodes) {
759
+ const data = new Int32Array(maxNodes * 4);
760
+ const attribute = new webgpu.StorageBufferAttribute(data, 4);
761
+ const node = tsl.storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
762
+ return { data, attribute, node };
763
+ }
764
+
765
+ const Dir = {
766
+ LEFT: 0,
767
+ RIGHT: 1,
768
+ TOP: 2,
769
+ BOTTOM: 3
770
+ };
771
+ const U32_EMPTY = 4294967295;
772
+ function allocLeafSet(capacity) {
773
+ return {
774
+ capacity,
775
+ count: 0,
776
+ space: new Uint8Array(capacity),
777
+ level: new Uint8Array(capacity),
778
+ x: new Int32Array(capacity),
779
+ y: new Int32Array(capacity)
780
+ };
781
+ }
782
+ function resetLeafSet(leaves) {
783
+ leaves.count = 0;
784
+ }
785
+ function allocSeamTable(capacity) {
786
+ return {
787
+ capacity,
788
+ count: 0,
789
+ stride: 8,
790
+ neighbors: new Uint32Array(capacity * 8)
791
+ };
792
+ }
793
+ function resetSeamTable(seams) {
794
+ seams.count = 0;
795
+ }
796
+
797
+ function createNodeStore(maxNodes, spaceCount) {
798
+ return {
799
+ maxNodes,
800
+ nodesUsed: 0,
801
+ currentGen: 1,
802
+ gen: new Uint16Array(maxNodes),
803
+ space: new Uint8Array(maxNodes),
804
+ level: new Uint8Array(maxNodes),
805
+ x: new Int32Array(maxNodes),
806
+ y: new Int32Array(maxNodes),
807
+ firstChild: new Uint32Array(maxNodes),
808
+ flags: new Uint8Array(maxNodes),
809
+ roots: new Uint32Array(spaceCount)
810
+ };
811
+ }
812
+ function beginFrame(store) {
813
+ store.nodesUsed = 0;
814
+ store.currentGen = store.currentGen + 1 & 65535;
815
+ if (store.currentGen === 0) {
816
+ store.gen.fill(0);
817
+ store.currentGen = 1;
818
+ }
819
+ }
820
+ function allocNode(store, tile) {
821
+ const id = store.nodesUsed;
822
+ if (id >= store.maxNodes) return U32_EMPTY;
823
+ store.nodesUsed = id + 1;
824
+ store.gen[id] = store.currentGen;
825
+ store.space[id] = tile.space;
826
+ store.level[id] = tile.level;
827
+ store.x[id] = tile.x;
828
+ store.y[id] = tile.y;
829
+ store.firstChild[id] = U32_EMPTY;
830
+ store.flags[id] = 0;
831
+ return id;
832
+ }
833
+ function hasChildren(store, nodeId) {
834
+ return store.firstChild[nodeId] !== U32_EMPTY;
835
+ }
836
+ function ensureChildren(store, parentId) {
837
+ const existing = store.firstChild[parentId];
838
+ if (existing !== U32_EMPTY) return existing;
839
+ const childBase = store.nodesUsed;
840
+ if (childBase + 4 > store.maxNodes) return U32_EMPTY;
841
+ const space = store.space[parentId];
842
+ const level = store.level[parentId] + 1;
843
+ const px = store.x[parentId] << 1;
844
+ const py = store.y[parentId] << 1;
845
+ allocNode(store, { space, level, x: px, y: py });
846
+ allocNode(store, { space, level, x: px + 1, y: py });
847
+ allocNode(store, { space, level, x: px, y: py + 1 });
848
+ allocNode(store, { space, level, x: px + 1, y: py + 1 });
849
+ store.firstChild[parentId] = childBase;
850
+ return childBase;
851
+ }
852
+
853
+ function nextPow2$1(n) {
854
+ let x = 1;
855
+ while (x < n) x <<= 1;
856
+ return x;
857
+ }
858
+ function mix32$1(x) {
859
+ x >>>= 0;
860
+ x ^= x >>> 16;
861
+ x = Math.imul(x, 2146121005) >>> 0;
862
+ x ^= x >>> 15;
863
+ x = Math.imul(x, 2221713035) >>> 0;
864
+ x ^= x >>> 16;
865
+ return x >>> 0;
866
+ }
867
+ function hashKey$1(space, level, x, y) {
868
+ const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
869
+ return mix32$1(h);
870
+ }
871
+ function createSpatialIndex(maxEntries) {
872
+ const size = nextPow2$1(Math.max(2, maxEntries * 2));
873
+ return {
874
+ size,
875
+ mask: size - 1,
876
+ stampGen: 1,
877
+ stamp: new Uint16Array(size),
878
+ keysSpace: new Uint8Array(size),
879
+ keysLevel: new Uint8Array(size),
880
+ keysX: new Uint32Array(size),
881
+ keysY: new Uint32Array(size),
882
+ values: new Uint32Array(size)
883
+ };
884
+ }
885
+ function resetSpatialIndex(index) {
886
+ index.stampGen = index.stampGen + 1 & 65535;
887
+ if (index.stampGen === 0) {
888
+ index.stamp.fill(0);
889
+ index.stampGen = 1;
890
+ }
891
+ }
892
+ function insertSpatialIndexRaw(index, space, level, x, y, value) {
893
+ const s = space & 255;
894
+ const l = level & 255;
895
+ const xx = x >>> 0;
896
+ const yy = y >>> 0;
897
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
898
+ for (let probes = 0; probes < index.size; probes++) {
899
+ if (index.stamp[slot] !== index.stampGen) {
900
+ index.stamp[slot] = index.stampGen;
901
+ index.keysSpace[slot] = s;
902
+ index.keysLevel[slot] = l;
903
+ index.keysX[slot] = xx;
904
+ index.keysY[slot] = yy;
905
+ index.values[slot] = value >>> 0;
906
+ return;
907
+ }
908
+ if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
909
+ index.values[slot] = value >>> 0;
910
+ return;
911
+ }
912
+ slot = slot + 1 & index.mask;
913
+ }
914
+ throw new Error("SpatialIndex is full (no empty slot found).");
915
+ }
916
+ function lookupSpatialIndexRaw(index, space, level, x, y) {
917
+ const s = space & 255;
918
+ const l = level & 255;
919
+ const xx = x >>> 0;
920
+ const yy = y >>> 0;
921
+ let slot = hashKey$1(s, l, xx, yy) & index.mask;
922
+ for (let probes = 0; probes < index.size; probes++) {
923
+ if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
924
+ if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
925
+ return index.values[slot];
926
+ }
927
+ slot = slot + 1 & index.mask;
928
+ }
929
+ return U32_EMPTY;
930
+ }
931
+
932
+ function buildLeafIndex(leaves, out) {
933
+ const index = out ?? createSpatialIndex(leaves.count);
934
+ resetSpatialIndex(index);
935
+ for (let i = 0; i < leaves.count; i++) {
936
+ insertSpatialIndexRaw(index, leaves.space[i], leaves.level[i], leaves.x[i], leaves.y[i], i);
937
+ }
938
+ return index;
939
+ }
940
+
941
+ function createState(cfg, surface) {
942
+ const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
943
+ const scratchRootTiles = [];
944
+ for (let i = 0; i < surface.maxRootCount; i++) {
945
+ scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
946
+ }
947
+ return {
948
+ cfg,
949
+ store,
950
+ leaves: allocLeafSet(cfg.maxNodes),
951
+ leafNodeIds: new Uint32Array(cfg.maxNodes),
952
+ leafIndex: createSpatialIndex(cfg.maxNodes),
953
+ stack: new Uint32Array(cfg.maxNodes),
954
+ rootNodeIds: new Uint32Array(surface.maxRootCount),
955
+ rootCount: 0,
956
+ splitQueue: new Uint32Array(cfg.maxNodes),
957
+ splitStamp: new Uint16Array(cfg.maxNodes),
958
+ splitGen: 1,
959
+ scratchTile: { space: 0, level: 0, x: 0, y: 0 },
960
+ scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
961
+ scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
962
+ scratchRootTiles,
963
+ spaceCount: surface.spaceCount
964
+ };
965
+ }
966
+ function beginUpdate(state, surface, params) {
967
+ if (surface.spaceCount !== state.spaceCount) {
968
+ throw new Error(
969
+ `Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
970
+ );
971
+ }
972
+ if (surface.maxRootCount !== state.rootNodeIds.length) {
973
+ throw new Error(
974
+ `Surface maxRootCount changed (${state.rootNodeIds.length} -> ${surface.maxRootCount}). Create a new quadtree state.`
975
+ );
976
+ }
977
+ beginFrame(state.store);
978
+ state.rootCount = 0;
979
+ const rootCount = surface.rootTiles(params.cameraOrigin, state.scratchRootTiles);
980
+ if (rootCount < 0 || rootCount > surface.maxRootCount) {
981
+ throw new Error(`Surface returned invalid root count (${rootCount}).`);
982
+ }
983
+ for (let i = 0; i < rootCount; i++) {
984
+ const rootId = allocNode(state.store, state.scratchRootTiles[i]);
985
+ if (rootId === U32_EMPTY) {
986
+ throw new Error("Failed to allocate root node (maxNodes too small).");
987
+ }
988
+ state.rootNodeIds[i] = rootId;
989
+ state.rootCount = i + 1;
990
+ }
991
+ }
992
+
993
+ function shouldSplit(bounds, level, maxLevel, params) {
994
+ if (level >= maxLevel) return false;
995
+ const mode = params.mode ?? "distance";
996
+ const cx = bounds.cx;
997
+ const cy = bounds.cy;
998
+ const cz = bounds.cz;
999
+ const distSq = cx * cx + cy * cy + cz * cz;
1000
+ const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
1001
+ if (mode === "screen") {
1002
+ const proj = params.projectionFactor ?? 0;
1003
+ const target = params.targetPixels ?? 0;
1004
+ if (proj <= 0 || target <= 0) {
1005
+ const f2 = params.distanceFactor ?? 2;
1006
+ const threshold2 = bounds.r * f2;
1007
+ return safeDistSq < threshold2 * threshold2;
1008
+ }
1009
+ const left = bounds.r * bounds.r * proj * proj;
1010
+ const right = safeDistSq * target * target;
1011
+ return left > right;
1012
+ }
1013
+ const f = params.distanceFactor ?? 2;
1014
+ const threshold = bounds.r * f;
1015
+ return safeDistSq < threshold * threshold;
1016
+ }
1017
+
1018
+ function refineLeaves(state, surface, params, outLeaves) {
1019
+ const leaves = outLeaves ?? state.leaves;
1020
+ resetLeafSet(leaves);
1021
+ const store = state.store;
1022
+ const stack = state.stack;
1023
+ let sp = 0;
1024
+ for (let i = 0; i < state.rootCount; i++) {
1025
+ stack[sp++] = state.rootNodeIds[i];
1026
+ }
1027
+ while (sp > 0) {
1028
+ const nodeId = stack[--sp];
1029
+ const level = store.level[nodeId];
1030
+ const space = store.space[nodeId];
1031
+ const x = store.x[nodeId];
1032
+ const y = store.y[nodeId];
1033
+ const tile = state.scratchTile;
1034
+ tile.space = space;
1035
+ tile.level = level;
1036
+ tile.x = x;
1037
+ tile.y = y;
1038
+ const bounds = state.scratchBounds;
1039
+ surface.tileBounds(tile, params.cameraOrigin, bounds);
1040
+ if (hasChildren(store, nodeId)) {
1041
+ const base = store.firstChild[nodeId];
1042
+ stack[sp++] = base + 3;
1043
+ stack[sp++] = base + 2;
1044
+ stack[sp++] = base + 1;
1045
+ stack[sp++] = base + 0;
1046
+ continue;
1047
+ }
1048
+ const split = shouldSplit(bounds, level, state.cfg.maxLevel, params);
1049
+ if (split) {
1050
+ const base = ensureChildren(store, nodeId);
1051
+ if (base !== U32_EMPTY) {
1052
+ stack[sp++] = base + 3;
1053
+ stack[sp++] = base + 2;
1054
+ stack[sp++] = base + 1;
1055
+ stack[sp++] = base + 0;
1056
+ continue;
1057
+ }
1058
+ }
1059
+ const i = leaves.count;
1060
+ if (i >= leaves.capacity) {
1061
+ throw new Error("LeafSet capacity exceeded.");
1062
+ }
1063
+ leaves.space[i] = space;
1064
+ leaves.level[i] = level;
1065
+ leaves.x[i] = x;
1066
+ leaves.y[i] = y;
1067
+ state.leafNodeIds[i] = nodeId;
1068
+ leaves.count = i + 1;
1069
+ }
1070
+ return leaves;
1071
+ }
1072
+
1073
+ function resetSplitMarks(state) {
1074
+ state.splitGen = state.splitGen + 1 & 65535;
1075
+ if (state.splitGen === 0) {
1076
+ state.splitStamp.fill(0);
1077
+ state.splitGen = 1;
1078
+ }
1079
+ }
1080
+ function scheduleSplit(state, nodeId, count) {
1081
+ if (nodeId === U32_EMPTY) return count;
1082
+ if (state.splitStamp[nodeId] === state.splitGen) return count;
1083
+ state.splitStamp[nodeId] = state.splitGen;
1084
+ state.splitQueue[count] = nodeId;
1085
+ return count + 1;
1086
+ }
1087
+ function balance2to1(state, surface, params, leaves) {
1088
+ const maxIters = state.cfg.maxLevel + 1;
1089
+ for (let iter = 0; iter < maxIters; iter++) {
1090
+ const index = buildLeafIndex(leaves, state.leafIndex);
1091
+ resetSplitMarks(state);
1092
+ let splitCount = 0;
1093
+ for (let i = 0; i < leaves.count; i++) {
1094
+ const leafLevel = leaves.level[i];
1095
+ if (leafLevel < 2) continue;
1096
+ const leafSpace = leaves.space[i];
1097
+ const leafX = leaves.x[i];
1098
+ const leafY = leaves.y[i];
1099
+ for (let dir = 0; dir < 4; dir++) {
1100
+ for (let candidateLevel = leafLevel - 2; candidateLevel >= 0; candidateLevel--) {
1101
+ const shift = leafLevel - candidateLevel;
1102
+ const tile = state.scratchTile;
1103
+ tile.space = leafSpace;
1104
+ tile.level = candidateLevel;
1105
+ tile.x = leafX >>> shift;
1106
+ tile.y = leafY >>> shift;
1107
+ const neighbor = state.scratchNeighbor;
1108
+ if (!surface.neighborSameLevel(tile, dir, neighbor)) break;
1109
+ const j = lookupSpatialIndexRaw(
1110
+ index,
1111
+ neighbor.space,
1112
+ neighbor.level,
1113
+ neighbor.x,
1114
+ neighbor.y
1115
+ );
1116
+ if (j !== U32_EMPTY) {
1117
+ splitCount = scheduleSplit(state, state.leafNodeIds[j], splitCount);
1118
+ break;
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ if (splitCount === 0) return leaves;
1124
+ let anySplit = false;
1125
+ for (let k = 0; k < splitCount; k++) {
1126
+ const nodeId = state.splitQueue[k];
1127
+ if (state.store.level[nodeId] >= state.cfg.maxLevel) continue;
1128
+ const base = ensureChildren(state.store, nodeId);
1129
+ if (base !== U32_EMPTY) anySplit = true;
1130
+ }
1131
+ if (!anySplit) return leaves;
1132
+ refineLeaves(state, surface, params, leaves);
1133
+ }
1134
+ return leaves;
1135
+ }
1136
+
1137
+ function update(state, surface, params, outLeaves) {
1138
+ const origY = params.cameraOrigin.y;
1139
+ params.cameraOrigin.y -= params.elevationAtCameraXZ ?? 0;
1140
+ beginUpdate(state, surface, params);
1141
+ const leaves = refineLeaves(state, surface, params, outLeaves);
1142
+ const result = balance2to1(state, surface, params, leaves);
1143
+ params.cameraOrigin.y = origY;
1144
+ return result;
1145
+ }
1146
+
1147
+ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
1148
+ const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
1149
+ const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
1150
+ const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
1151
+ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
1152
+ if (outSeams.capacity < leaves.count) {
1153
+ throw new Error("SeamTable capacity is smaller than LeafSet.count.");
1154
+ }
1155
+ const index = buildLeafIndex(leaves, outIndex);
1156
+ outSeams.count = leaves.count;
1157
+ const neighbors = outSeams.neighbors;
1158
+ for (let i = 0; i < leaves.count; i++) {
1159
+ const base = i * 8;
1160
+ const space = leaves.space[i];
1161
+ const level = leaves.level[i];
1162
+ const x = leaves.x[i];
1163
+ const y = leaves.y[i];
1164
+ for (let dir = 0; dir < 4; dir++) {
1165
+ const outOffset = base + dir * 2;
1166
+ neighbors[outOffset + 0] = U32_EMPTY;
1167
+ neighbors[outOffset + 1] = U32_EMPTY;
1168
+ scratchTile.space = space;
1169
+ scratchTile.level = level;
1170
+ scratchTile.x = x;
1171
+ scratchTile.y = y;
1172
+ if (!surface.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
1173
+ let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
1174
+ if (j !== U32_EMPTY) {
1175
+ neighbors[outOffset + 0] = j;
1176
+ continue;
1177
+ }
1178
+ if (level > 0) {
1179
+ const px = x >>> 1;
1180
+ const py = y >>> 1;
1181
+ scratchParentTile.space = space;
1182
+ scratchParentTile.level = level - 1;
1183
+ scratchParentTile.x = px;
1184
+ scratchParentTile.y = py;
1185
+ if (surface.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
1186
+ j = lookupSpatialIndexRaw(
1187
+ index,
1188
+ scratchParentNbr.space,
1189
+ scratchParentNbr.level,
1190
+ scratchParentNbr.x,
1191
+ scratchParentNbr.y
1192
+ );
1193
+ if (j !== U32_EMPTY) {
1194
+ neighbors[outOffset + 0] = j;
1195
+ continue;
1196
+ }
1197
+ }
1198
+ }
1199
+ const childLevel = scratchNbr.level + 1;
1200
+ const x2 = scratchNbr.x << 1 >>> 0;
1201
+ const y2 = scratchNbr.y << 1 >>> 0;
1202
+ let ax = 0;
1203
+ let ay = 0;
1204
+ let bx = 0;
1205
+ let by = 0;
1206
+ switch (dir) {
1207
+ case Dir.LEFT:
1208
+ ax = x2 + 1;
1209
+ ay = y2;
1210
+ bx = x2 + 1;
1211
+ by = y2 + 1;
1212
+ break;
1213
+ case Dir.RIGHT:
1214
+ ax = x2;
1215
+ ay = y2;
1216
+ bx = x2;
1217
+ by = y2 + 1;
1218
+ break;
1219
+ case Dir.TOP:
1220
+ ax = x2;
1221
+ ay = y2 + 1;
1222
+ bx = x2 + 1;
1223
+ by = y2 + 1;
1224
+ break;
1225
+ case Dir.BOTTOM:
1226
+ ax = x2;
1227
+ ay = y2;
1228
+ bx = x2 + 1;
1229
+ by = y2;
1230
+ break;
1231
+ }
1232
+ j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, ax, ay);
1233
+ if (j !== U32_EMPTY) neighbors[outOffset + 0] = j;
1234
+ j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, bx, by);
1235
+ if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
1236
+ }
1237
+ }
1238
+ return outSeams;
1239
+ }
1240
+
1241
+ function createFlatSurface(cfg) {
1242
+ const halfRoot = 0.5 * cfg.rootSize;
1243
+ const maxHeight = cfg.maxHeight ?? 0;
1244
+ const surface = {
1245
+ spaceCount: 1,
1246
+ maxRootCount: 1,
1247
+ neighborSameLevel(tile, dir, out) {
1248
+ const level = tile.level;
1249
+ const x = tile.x;
1250
+ const y = tile.y;
1251
+ let nx = x;
1252
+ let ny = y;
1253
+ switch (dir) {
1254
+ case Dir.LEFT:
1255
+ nx = x - 1;
1256
+ break;
1257
+ case Dir.RIGHT:
1258
+ nx = x + 1;
1259
+ break;
1260
+ case Dir.TOP:
1261
+ ny = y - 1;
1262
+ break;
1263
+ case Dir.BOTTOM:
1264
+ ny = y + 1;
1265
+ break;
1266
+ }
1267
+ if (nx < 0 || ny < 0) return false;
1268
+ const maxCoord = (1 << level) - 1;
1269
+ if (nx > maxCoord || ny > maxCoord) return false;
1270
+ out.space = 0;
1271
+ out.level = level;
1272
+ out.x = nx;
1273
+ out.y = ny;
1274
+ return true;
1275
+ },
1276
+ tileBounds(tile, cameraOrigin, out) {
1277
+ const level = tile.level;
1278
+ const scale = 1 / (1 << level);
1279
+ const size = cfg.rootSize * scale;
1280
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1281
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1282
+ const centerX = minX + 0.5 * size;
1283
+ const centerY = cfg.origin.y;
1284
+ const centerZ = minZ + 0.5 * size;
1285
+ out.cx = centerX - cameraOrigin.x;
1286
+ out.cy = centerY - cameraOrigin.y;
1287
+ out.cz = centerZ - cameraOrigin.z;
1288
+ out.r = 0.7071067811865476 * size + maxHeight;
1289
+ },
1290
+ rootTiles(_cameraOrigin, out) {
1291
+ const root = out[0];
1292
+ root.space = 0;
1293
+ root.level = 0;
1294
+ root.x = 0;
1295
+ root.y = 0;
1296
+ return 1;
1297
+ }
1298
+ };
1299
+ return surface;
1300
+ }
1301
+
1302
+ function createInfiniteFlatSurface(cfg) {
1303
+ const halfRoot = 0.5 * cfg.rootSize;
1304
+ const maxHeight = cfg.maxHeight ?? 0;
1305
+ const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1306
+ const rootWidth = rootGridRadius * 2 + 1;
1307
+ return {
1308
+ spaceCount: 1,
1309
+ maxRootCount: rootWidth * rootWidth,
1310
+ neighborSameLevel(tile, dir, out) {
1311
+ let nx = tile.x;
1312
+ let ny = tile.y;
1313
+ switch (dir) {
1314
+ case Dir.LEFT:
1315
+ nx = tile.x - 1;
1316
+ break;
1317
+ case Dir.RIGHT:
1318
+ nx = tile.x + 1;
1319
+ break;
1320
+ case Dir.TOP:
1321
+ ny = tile.y - 1;
1322
+ break;
1323
+ case Dir.BOTTOM:
1324
+ ny = tile.y + 1;
1325
+ break;
1326
+ }
1327
+ out.space = tile.space;
1328
+ out.level = tile.level;
1329
+ out.x = nx;
1330
+ out.y = ny;
1331
+ return true;
1332
+ },
1333
+ tileBounds(tile, cameraOrigin, out) {
1334
+ const level = tile.level;
1335
+ const scale = 1 / (1 << level);
1336
+ const size = cfg.rootSize * scale;
1337
+ const minX = cfg.origin.x + (tile.x * size - halfRoot);
1338
+ const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1339
+ const centerX = minX + 0.5 * size;
1340
+ const centerY = cfg.origin.y;
1341
+ const centerZ = minZ + 0.5 * size;
1342
+ out.cx = centerX - cameraOrigin.x;
1343
+ out.cy = centerY - cameraOrigin.y;
1344
+ out.cz = centerZ - cameraOrigin.z;
1345
+ out.r = 0.7071067811865476 * size + maxHeight;
1346
+ },
1347
+ rootTiles(cameraOrigin, out) {
1348
+ const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
1349
+ const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
1350
+ let index = 0;
1351
+ for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
1352
+ for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
1353
+ const root = out[index];
1354
+ root.space = 0;
1355
+ root.level = 0;
1356
+ root.x = camRootX + dx;
1357
+ root.y = camRootY + dy;
1358
+ index++;
1359
+ }
1360
+ }
1361
+ return index;
1362
+ }
1363
+ };
1364
+ }
1365
+
1366
+ function createCubeSphereSurface(_cfg) {
1367
+ return {
1368
+ spaceCount: 6,
1369
+ maxRootCount: 6,
1370
+ neighborSameLevel(_tile, _dir, _out) {
1371
+ return false;
1372
+ },
1373
+ tileBounds(_tile, _cameraOrigin, out) {
1374
+ out.cx = 0;
1375
+ out.cy = 0;
1376
+ out.cz = 0;
1377
+ out.r = Number.MAX_VALUE;
1378
+ },
1379
+ rootTiles(_cameraOrigin, out) {
1380
+ for (let s = 0; s < 6; s++) {
1381
+ const root = out[s];
1382
+ root.space = s;
1383
+ root.level = 0;
1384
+ root.x = 0;
1385
+ root.y = 0;
1386
+ }
1387
+ return 6;
1388
+ }
1389
+ };
1390
+ }
1391
+
1392
+ function cloneSpatialIndex(target, source) {
1393
+ if (target.size !== source.size) {
1394
+ throw new Error(
1395
+ `SpatialIndex size mismatch (target=${target.size}, source=${source.size}).`
1396
+ );
1397
+ }
1398
+ target.mask = source.mask;
1399
+ target.stampGen = source.stampGen;
1400
+ target.stamp.set(source.stamp);
1401
+ target.keysSpace.set(source.keysSpace);
1402
+ target.keysLevel.set(source.keysLevel);
1403
+ target.keysX.set(source.keysX);
1404
+ target.keysY.set(source.keysY);
1405
+ target.values.set(source.values);
1406
+ }
1407
+ function tileLocalToFieldUV(localCoord, innerSegments) {
1408
+ const edge = innerSegments + 3;
1409
+ return (localCoord * innerSegments + 1.5) / edge;
1410
+ }
1411
+ function createCpuTerrainCache(maxNodes, initialConfig) {
1412
+ let config = initialConfig;
1413
+ let edgeVertexCount = config.innerTileSegments + 3;
1414
+ let verticesPerNode = edgeVertexCount * edgeVertexCount;
1415
+ let totalElements = maxNodes * verticesPerNode;
1416
+ let frontElevation = new Float32Array(totalElements);
1417
+ let backElevation = new Float32Array(totalElements);
1418
+ let frontIndex = createSpatialIndex(maxNodes);
1419
+ let backIndex = createSpatialIndex(maxNodes);
1420
+ let frontTileBounds = new Float32Array(maxNodes * 2);
1421
+ let backTileBounds = new Float32Array(maxNodes * 2);
1422
+ let frontLeafCount = 0;
1423
+ let globalRange = null;
1424
+ let hasSnapshot = false;
1425
+ let readbackPending = false;
1426
+ let generationCount = 0;
1427
+ let lastScheduledStampGen = -1;
1428
+ const readHeight = (leafIndex, ix, iy) => {
1429
+ const base = leafIndex * verticesPerNode;
1430
+ return frontElevation[base + iy * edgeVertexCount + ix] ?? 0;
1431
+ };
1432
+ const sampleGridBilinear = (leafIndex, gx, gy) => {
1433
+ const max = edgeVertexCount - 1;
1434
+ const x = Math.max(0, Math.min(max, gx));
1435
+ const y = Math.max(0, Math.min(max, gy));
1436
+ const x0 = Math.floor(x);
1437
+ const y0 = Math.floor(y);
1438
+ const x1 = Math.min(max, x0 + 1);
1439
+ const y1 = Math.min(max, y0 + 1);
1440
+ const tx = x - x0;
1441
+ const ty = y - y0;
1442
+ const h00 = readHeight(leafIndex, x0, y0);
1443
+ const h10 = readHeight(leafIndex, x1, y0);
1444
+ const h01 = readHeight(leafIndex, x0, y1);
1445
+ const h11 = readHeight(leafIndex, x1, y1);
1446
+ const hx0 = h00 + (h10 - h00) * tx;
1447
+ const hx1 = h01 + (h11 - h01) * tx;
1448
+ return hx0 + (hx1 - hx0) * ty;
1449
+ };
1450
+ const computeNormal = (leafIndex, gx, gy, tileSize) => {
1451
+ const hLeft = sampleGridBilinear(leafIndex, gx - 1, gy);
1452
+ const hRight = sampleGridBilinear(leafIndex, gx + 1, gy);
1453
+ const hUp = sampleGridBilinear(leafIndex, gx, gy - 1);
1454
+ const hDown = sampleGridBilinear(leafIndex, gx, gy + 1);
1455
+ const stepWorld = tileSize / config.innerTileSegments;
1456
+ const inv2Step = 0.5 / stepWorld;
1457
+ const dhdx = (hRight - hLeft) * config.elevationScale * inv2Step;
1458
+ const dhdz = (hDown - hUp) * config.elevationScale * inv2Step;
1459
+ return new three.Vector3(-dhdx, 1, -dhdz).normalize();
1460
+ };
1461
+ const lookupTile = (worldX, worldZ) => {
1462
+ const halfRoot = config.rootSize * 0.5;
1463
+ for (let level = config.maxLevel; level >= 0; level -= 1) {
1464
+ const scale = 2 ** level;
1465
+ const tileSize = config.rootSize / scale;
1466
+ const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
1467
+ const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
1468
+ const leafIndex = lookupSpatialIndexRaw(
1469
+ frontIndex,
1470
+ 0,
1471
+ level,
1472
+ tileX,
1473
+ tileY
1474
+ );
1475
+ if (leafIndex !== U32_EMPTY) {
1476
+ const tileMinX = config.originX + tileX * tileSize - halfRoot;
1477
+ const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
1478
+ return {
1479
+ found: true,
1480
+ leafIndex,
1481
+ level,
1482
+ tileX,
1483
+ tileY,
1484
+ tileSize,
1485
+ localU: (worldX - tileMinX) / tileSize,
1486
+ localV: (worldZ - tileMinZ) / tileSize
1487
+ };
1488
+ }
1489
+ }
1490
+ return {
1491
+ found: false,
1492
+ leafIndex: -1,
1493
+ level: -1,
1494
+ tileX: -1,
1495
+ tileY: -1,
1496
+ tileSize: 0,
1497
+ localU: 0,
1498
+ localV: 0
1499
+ };
1500
+ };
1501
+ const sampleFromLookup = (lookup) => {
1502
+ const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
1503
+ const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
1504
+ const gx = fieldU * (edgeVertexCount - 1);
1505
+ const gy = fieldV * (edgeVertexCount - 1);
1506
+ const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
1507
+ const scaledHeight = config.originY + height * config.elevationScale;
1508
+ const normal = computeNormal(lookup.leafIndex, gx, gy, lookup.tileSize);
1509
+ return { elevation: scaledHeight, normal, valid: true };
1510
+ };
1511
+ const sampleElevationFromLookup = (lookup) => {
1512
+ const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
1513
+ const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
1514
+ const gx = fieldU * (edgeVertexCount - 1);
1515
+ const gy = fieldV * (edgeVertexCount - 1);
1516
+ const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
1517
+ const scaledHeight = config.originY + height * config.elevationScale;
1518
+ return { elevation: scaledHeight, valid: true };
1519
+ };
1520
+ const sampleTerrain = (worldX, worldZ) => {
1521
+ if (!hasSnapshot) {
1522
+ return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1523
+ }
1524
+ const lookup = lookupTile(worldX, worldZ);
1525
+ if (!lookup.found) {
1526
+ return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
1527
+ }
1528
+ return sampleFromLookup(lookup);
1529
+ };
1530
+ const getElevation = (worldX, worldZ) => {
1531
+ if (!hasSnapshot) {
1532
+ return { elevation: 0, valid: false };
1533
+ }
1534
+ const lookup = lookupTile(worldX, worldZ);
1535
+ if (!lookup.found) {
1536
+ return { elevation: 0, valid: false };
1537
+ }
1538
+ return sampleElevationFromLookup(lookup);
1539
+ };
1540
+ const api = {
1541
+ get generation() {
1542
+ return generationCount;
1543
+ },
1544
+ get ready() {
1545
+ return hasSnapshot;
1546
+ },
1547
+ updateConfig(nextConfig) {
1548
+ config = nextConfig;
1549
+ edgeVertexCount = config.innerTileSegments + 3;
1550
+ verticesPerNode = edgeVertexCount * edgeVertexCount;
1551
+ totalElements = maxNodes * verticesPerNode;
1552
+ },
1553
+ triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
1554
+ if (readbackPending) return;
1555
+ const withReadback = renderer;
1556
+ if (!withReadback.getArrayBufferAsync) return;
1557
+ if (spatialIndex.stampGen === lastScheduledStampGen) return;
1558
+ cloneSpatialIndex(backIndex, spatialIndex);
1559
+ lastScheduledStampGen = spatialIndex.stampGen;
1560
+ const capturedLeafCount = activeLeafCount ?? 0;
1561
+ const capturedScale = config.elevationScale;
1562
+ const capturedOriginY = config.originY;
1563
+ readbackPending = true;
1564
+ const elevationPromise = withReadback.getArrayBufferAsync(attribute);
1565
+ const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
1566
+ const onComplete = (elevResult, boundsResult) => {
1567
+ const data = new Float32Array(elevResult);
1568
+ backElevation.fill(0);
1569
+ backElevation.set(data.subarray(0, totalElements));
1570
+ let boundsValid = capturedLeafCount === 0;
1571
+ if (boundsResult) {
1572
+ const rawBounds = new Float32Array(boundsResult);
1573
+ backTileBounds.fill(0);
1574
+ backTileBounds.set(rawBounds.subarray(0, capturedLeafCount * 2));
1575
+ for (let i = 0; i < capturedLeafCount; i += 1) {
1576
+ if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
1577
+ boundsValid = true;
1578
+ break;
1579
+ }
1580
+ }
1581
+ }
1582
+ const oldFrontElevation = frontElevation;
1583
+ const oldFrontIndex = frontIndex;
1584
+ frontElevation = backElevation;
1585
+ frontIndex = backIndex;
1586
+ frontLeafCount = capturedLeafCount;
1587
+ backElevation = oldFrontElevation;
1588
+ backIndex = oldFrontIndex;
1589
+ if (boundsResult && boundsValid) {
1590
+ const oldFrontBounds = frontTileBounds;
1591
+ frontTileBounds = backTileBounds;
1592
+ backTileBounds = oldFrontBounds;
1593
+ }
1594
+ if (boundsResult && boundsValid && capturedLeafCount > 0) {
1595
+ let gMin = Infinity;
1596
+ let gMax = -Infinity;
1597
+ for (let i = 0; i < capturedLeafCount; i++) {
1598
+ const rawMin = frontTileBounds[i * 2];
1599
+ const rawMax = frontTileBounds[i * 2 + 1];
1600
+ const a = capturedOriginY + rawMin * capturedScale;
1601
+ const b = capturedOriginY + rawMax * capturedScale;
1602
+ gMin = Math.min(gMin, a, b);
1603
+ gMax = Math.max(gMax, a, b);
1604
+ }
1605
+ globalRange = { min: gMin, max: gMax };
1606
+ }
1607
+ hasSnapshot = true;
1608
+ generationCount += 1;
1609
+ };
1610
+ if (boundsPromise) {
1611
+ Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
1612
+ readbackPending = false;
1613
+ });
1614
+ } else {
1615
+ elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
1616
+ readbackPending = false;
1617
+ });
1618
+ }
1619
+ },
1620
+ getElevation(worldX, worldZ) {
1621
+ const sample = getElevation(worldX, worldZ);
1622
+ return sample.valid ? sample.elevation : null;
1623
+ },
1624
+ getNormal(worldX, worldZ) {
1625
+ return sampleTerrain(worldX, worldZ).normal;
1626
+ },
1627
+ getTile(worldX, worldZ) {
1628
+ if (!hasSnapshot) return null;
1629
+ const lookup = lookupTile(worldX, worldZ);
1630
+ if (!lookup.found) return null;
1631
+ return {
1632
+ level: lookup.level,
1633
+ x: lookup.tileX,
1634
+ y: lookup.tileY,
1635
+ index: lookup.leafIndex
1636
+ };
1637
+ },
1638
+ getTileBounds(worldX, worldZ) {
1639
+ if (!hasSnapshot) return null;
1640
+ const lookup = lookupTile(worldX, worldZ);
1641
+ if (!lookup.found || lookup.leafIndex >= frontLeafCount) return null;
1642
+ const rawMin = frontTileBounds[lookup.leafIndex * 2];
1643
+ const rawMax = frontTileBounds[lookup.leafIndex * 2 + 1];
1644
+ const a = config.originY + rawMin * config.elevationScale;
1645
+ const b = config.originY + rawMax * config.elevationScale;
1646
+ return {
1647
+ level: lookup.level,
1648
+ x: lookup.tileX,
1649
+ y: lookup.tileY,
1650
+ index: lookup.leafIndex,
1651
+ minElevation: Math.min(a, b),
1652
+ maxElevation: Math.max(a, b)
1653
+ };
1654
+ },
1655
+ getGlobalElevationRange() {
1656
+ return globalRange;
1657
+ },
1658
+ sampleTerrainBatch(positions) {
1659
+ const count = Math.floor(positions.length / 2);
1660
+ const elevations = new Float32Array(count);
1661
+ const normals = new Float32Array(count * 3);
1662
+ const valid = new Uint8Array(count);
1663
+ if (!hasSnapshot) {
1664
+ return { elevations, normals, valid, generation: generationCount };
1665
+ }
1666
+ let lastTile;
1667
+ for (let i = 0; i < count; i += 1) {
1668
+ const worldX = positions[i * 2] ?? 0;
1669
+ const worldZ = positions[i * 2 + 1] ?? 0;
1670
+ let lookup;
1671
+ if (lastTile && worldX >= lastTile.tileMinX && worldX <= lastTile.tileMinX + lastTile.tileSize && worldZ >= lastTile.tileMinZ && worldZ <= lastTile.tileMinZ + lastTile.tileSize) {
1672
+ lookup = {
1673
+ found: true,
1674
+ leafIndex: lastTile.leafIndex,
1675
+ level: lastTile.level,
1676
+ tileX: lastTile.tileX,
1677
+ tileY: lastTile.tileY,
1678
+ tileSize: lastTile.tileSize,
1679
+ localU: (worldX - lastTile.tileMinX) / lastTile.tileSize,
1680
+ localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
1681
+ };
1682
+ } else {
1683
+ lookup = lookupTile(worldX, worldZ);
1684
+ if (lookup.found) {
1685
+ lastTile = {
1686
+ leafIndex: lookup.leafIndex,
1687
+ level: lookup.level,
1688
+ tileX: lookup.tileX,
1689
+ tileY: lookup.tileY,
1690
+ tileSize: lookup.tileSize,
1691
+ tileMinX: worldX - lookup.localU * lookup.tileSize,
1692
+ tileMinZ: worldZ - lookup.localV * lookup.tileSize
1693
+ };
1694
+ } else {
1695
+ lastTile = void 0;
1696
+ }
1697
+ }
1698
+ if (!lookup?.found) {
1699
+ normals[i * 3 + 1] = 1;
1700
+ continue;
1701
+ }
1702
+ const sample = sampleFromLookup(lookup);
1703
+ elevations[i] = sample.elevation;
1704
+ normals[i * 3] = sample.normal.x;
1705
+ normals[i * 3 + 1] = sample.normal.y;
1706
+ normals[i * 3 + 2] = sample.normal.z;
1707
+ valid[i] = 1;
1708
+ }
1709
+ return { elevations, normals, valid, generation: generationCount };
1710
+ },
1711
+ sampleTerrain
1712
+ };
1713
+ return api;
1714
+ }
1715
+
1716
+ function createTerrainQuery(cache) {
1717
+ return {
1718
+ get generation() {
1719
+ return cache.generation;
1720
+ },
1721
+ getElevation(worldX, worldZ) {
1722
+ return cache.getElevation(worldX, worldZ);
1723
+ },
1724
+ getNormal(worldX, worldZ) {
1725
+ return cache.getNormal(worldX, worldZ);
1726
+ },
1727
+ getTile(worldX, worldZ) {
1728
+ return cache.getTile(worldX, worldZ);
1729
+ },
1730
+ getTileBounds(worldX, worldZ) {
1731
+ return cache.getTileBounds(worldX, worldZ);
1732
+ },
1733
+ getGlobalElevationRange() {
1734
+ return cache.getGlobalElevationRange();
1735
+ },
1736
+ sampleTerrain(worldX, worldZ) {
1737
+ return cache.sampleTerrain(worldX, worldZ);
1738
+ },
1739
+ sampleTerrainBatch(positions) {
1740
+ return cache.sampleTerrainBatch(positions);
1741
+ }
1742
+ };
1743
+ }
1744
+
1745
+ const WGSIZE = 64;
1746
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
1747
+ const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
1748
+ return tsl.Fn(() => {
1749
+ const sharedMin = tsl.workgroupArray("float", WGSIZE);
1750
+ const sharedMax = tsl.workgroupArray("float", WGSIZE);
1751
+ const tid = tsl.int(tsl.localId.x);
1752
+ const tileIdx = tsl.int(tsl.workgroupId.z);
1753
+ const baseOffset = tileIdx.mul(tsl.int(verticesPerNode));
1754
+ const start = tid.mul(tsl.int(elemsPerThread));
1755
+ const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
1756
+ const localMin = tsl.float(1e10).toVar("localMin");
1757
+ const localMax = tsl.float(-1e10).toVar("localMax");
1758
+ tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
1759
+ const h = elevationFieldNode.element(baseOffset.add(i));
1760
+ localMin.assign(tsl.min(localMin, h));
1761
+ localMax.assign(tsl.max(localMax, h));
1762
+ });
1763
+ sharedMin.element(tid).assign(localMin);
1764
+ sharedMax.element(tid).assign(localMax);
1765
+ tsl.workgroupBarrier();
1766
+ tsl.If(tid.equal(tsl.int(0)), () => {
1767
+ const finalMin = tsl.float(1e10).toVar("finalMin");
1768
+ const finalMax = tsl.float(-1e10).toVar("finalMax");
1769
+ tsl.Loop(WGSIZE, ({ i }) => {
1770
+ finalMin.assign(tsl.min(finalMin, sharedMin.element(i)));
1771
+ finalMax.assign(tsl.max(finalMax, sharedMax.element(i)));
1772
+ });
1773
+ const outIdx = tileIdx.mul(tsl.int(2));
1774
+ boundsNode.element(outIdx).assign(finalMin);
1775
+ boundsNode.element(outIdx.add(tsl.int(1))).assign(finalMax);
1776
+ });
1777
+ })().computeKernel([WGSIZE, 1, 1]);
1778
+ }
1779
+ const tileBoundsContextTask = work.task((get, work) => {
1780
+ const elevationFieldContext = get(createElevationFieldContextTask);
1781
+ const maxNodesValue = get(maxNodes);
1782
+ const edgeVertexCount = get(innerTileSegments) + 3;
1783
+ return work(() => {
1784
+ const data = new Float32Array(maxNodesValue * 2);
1785
+ const attribute = new webgpu.StorageBufferAttribute(data, 1);
1786
+ const node = tsl.storage(attribute, "float", maxNodesValue * 2);
1787
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
1788
+ const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
1789
+ return { data, attribute, node, kernel };
1790
+ });
1791
+ }).displayName("tileBoundsContextTask");
1792
+ const tileBoundsReductionTask = work.task(
1793
+ (get, work, { resources }) => {
1794
+ get(executeComputeTask);
1795
+ const boundsContext = get(tileBoundsContextTask);
1796
+ const leafState = get(leafGpuBufferTask);
1797
+ return work(() => {
1798
+ if (resources?.renderer && leafState.count > 0) {
1799
+ resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
1800
+ }
1801
+ return boundsContext;
1802
+ });
1803
+ }
1804
+ ).displayName("tileBoundsReductionTask").lane("gpu");
1805
+
1806
+ const terrainQueryTask = work.task((get, work) => {
1807
+ const maxNodesValue = get(maxNodes);
1808
+ const innerTileSegmentsValue = get(innerTileSegments);
1809
+ const maxLevelValue = get(maxLevel);
1810
+ const rootSizeValue = get(rootSize);
1811
+ const originValue = get(origin);
1812
+ const elevationScaleValue = get(elevationScale);
1813
+ return work((prev) => {
1814
+ const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
1815
+ const configValues = {
1816
+ rootSize: rootSizeValue,
1817
+ originX: originValue.x,
1818
+ originY: originValue.y,
1819
+ originZ: originValue.z,
1820
+ innerTileSegments: innerTileSegmentsValue,
1821
+ elevationScale: elevationScaleValue,
1822
+ maxLevel: maxLevelValue
1823
+ };
1824
+ let cache = prev?.cache;
1825
+ let query = prev?.query;
1826
+ if (!cache || !query || prev?.shapeKey !== shapeKey) {
1827
+ cache = createCpuTerrainCache(maxNodesValue, configValues);
1828
+ query = createTerrainQuery(cache);
1829
+ }
1830
+ cache.updateConfig(configValues);
1831
+ return { cache, query, shapeKey };
1832
+ });
1833
+ }).displayName("terrainQueryTask");
1834
+ const terrainReadbackTask = work.task(
1835
+ (get, work, { resources }) => {
1836
+ const boundsContext = get(tileBoundsReductionTask);
1837
+ const elevationFieldContext = get(createElevationFieldContextTask);
1838
+ const quadtreeConfig = get(quadtreeConfigTask);
1839
+ const leafState = get(leafGpuBufferTask);
1840
+ const { cache } = get(terrainQueryTask);
1841
+ return work(() => {
1842
+ if (!resources?.renderer) return;
1843
+ cache.triggerReadback(
1844
+ resources.renderer,
1845
+ elevationFieldContext.attribute,
1846
+ quadtreeConfig.state.leafIndex,
1847
+ boundsContext.attribute,
1848
+ leafState.count
1849
+ );
1850
+ });
1851
+ }
1852
+ ).displayName("terrainReadbackTask").lane("gpu");
1853
+
1854
+ const surfaceTask = work.task((get, work) => {
1855
+ const customSurface = get(surface);
1856
+ const rootSizeVal = get(rootSize);
1857
+ const originVal = get(origin);
1858
+ return work(() => {
1859
+ if (customSurface) return customSurface;
1860
+ return createFlatSurface({ rootSize: rootSizeVal, origin: originVal });
1861
+ });
1862
+ }).displayName("surfaceTask");
1863
+ const quadtreeConfigTask = work.task((get, work) => {
1864
+ const surfaceVal = get(surfaceTask);
1865
+ const maxNodesVal = get(maxNodes);
1866
+ const maxLevelVal = get(maxLevel);
1867
+ return work(() => {
1868
+ const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surfaceVal);
1869
+ return {
1870
+ state,
1871
+ surface: surfaceVal
1872
+ };
1873
+ });
1874
+ }).displayName("quadtreeConfigTask");
1875
+ const quadtreeUpdateTask = work.task((get, work) => {
1876
+ const quadtreeConfig = get(quadtreeConfigTask);
1877
+ const quadtreeUpdateConfig = get(quadtreeUpdate);
1878
+ const { query: terrainQuery } = get(terrainQueryTask);
1879
+ let outLeaves = void 0;
1880
+ return work(() => {
1881
+ const cam = quadtreeUpdateConfig.cameraOrigin;
1882
+ quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
1883
+ outLeaves = update(
1884
+ quadtreeConfig.state,
1885
+ quadtreeConfig.surface,
1886
+ quadtreeUpdateConfig,
1887
+ outLeaves
1888
+ );
1889
+ return outLeaves;
1890
+ });
1891
+ }).displayName("quadtreeUpdateTask");
1892
+ const leafStorageTask = work.task((get, work) => {
1893
+ const maxNodesVal = get(maxNodes);
1894
+ return work(() => createLeafStorage(maxNodesVal));
1895
+ }).displayName("leafStorageTask");
1896
+ const leafGpuBufferTask = work.task((get, work) => {
1897
+ const leafSet = get(quadtreeUpdateTask);
1898
+ const leafStorage = get(leafStorageTask);
1899
+ return work(() => {
1900
+ const bufferCapacity = leafStorage.data.length / 4;
1901
+ const leafCount = Math.min(leafSet.count, bufferCapacity);
1902
+ for (let i = 0; i < leafCount; i += 1) {
1903
+ const offset = i * 4;
1904
+ leafStorage.data[offset] = leafSet.level[i] ?? 0;
1905
+ leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
1906
+ leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
1907
+ leafStorage.data[offset + 3] = 1;
1908
+ }
1909
+ leafStorage.attribute.needsUpdate = true;
1910
+ leafStorage.node.needsUpdate = true;
1911
+ return {
1912
+ count: leafCount,
1913
+ data: leafStorage.data,
1914
+ attribute: leafStorage.attribute,
1915
+ node: leafStorage.node
1916
+ };
1917
+ });
1918
+ }).displayName("leafGpuBufferTask");
1919
+
1920
+ function createElevationFunction(callback) {
1921
+ const tslFunction = (args) => {
1922
+ const params = {
1923
+ worldPosition: args.worldPosition,
1924
+ rootSize: args.rootSize,
1925
+ rootUV: args.rootUV,
1926
+ tileUV: args.tileUV,
1927
+ tileLevel: args.tileLevel,
1928
+ tileSize: args.tileSize,
1929
+ tileOriginVec2: args.tileOriginVec2,
1930
+ nodeIndex: args.nodeIndex
1931
+ };
1932
+ return callback(params);
1933
+ };
1934
+ return TSL_js.Fn(tslFunction);
1935
+ }
1936
+
1937
+ function createTerrainUniforms(params) {
1938
+ const sanitizedId = params.instanceId?.replace(/-/g, "_");
1939
+ const suffix = sanitizedId ? `_${sanitizedId}` : "";
1940
+ const uRootOrigin = tsl.uniform(
1941
+ new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
1942
+ ).setName(`uRootOrigin${suffix}`);
1943
+ const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
1944
+ const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
1945
+ `uInnerTileSegments${suffix}`
1946
+ );
1947
+ const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
1948
+ const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
1949
+ return {
1950
+ uRootOrigin,
1951
+ uRootSize,
1952
+ uInnerTileSegments,
1953
+ uSkirtScale,
1954
+ uElevationScale
1955
+ };
1956
+ }
1957
+
1958
+ const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
1959
+
1960
+ const scratchVector3 = new three.Vector3();
1961
+ const createUniformsTask = work.task((get, work) => {
1962
+ const uniformParams = {
1963
+ rootOrigin: get(origin),
1964
+ rootSize: get(rootSize),
1965
+ innerTileSegments: get(innerTileSegments),
1966
+ skirtScale: get(skirtScale),
1967
+ elevationScale: get(elevationScale),
1968
+ instanceId: get(instanceIdTask)
1969
+ };
1970
+ return work(() => createTerrainUniforms(uniformParams));
1971
+ }).displayName("createUniformsTask").cache("once");
1972
+ const updateUniformsTask = work.task((get, work) => {
1973
+ const terrainUniformsContext = get(createUniformsTask);
1974
+ const rootSizeVal = get(rootSize);
1975
+ const rootOrigin = get(origin);
1976
+ const innerTileSegmentsVal = get(innerTileSegments);
1977
+ const skirtScaleVal = get(skirtScale);
1978
+ const elevationScaleVal = get(elevationScale);
1979
+ return work(() => {
1980
+ terrainUniformsContext.uRootSize.value = rootSizeVal;
1981
+ terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
1982
+ rootOrigin.x,
1983
+ rootOrigin.y,
1984
+ rootOrigin.z
1985
+ );
1986
+ terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
1987
+ terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
1988
+ terrainUniformsContext.uElevationScale.value = elevationScaleVal;
1989
+ return terrainUniformsContext;
1990
+ });
1991
+ }).displayName("updateUniformsTask");
1992
+
1993
+ const createElevationFieldContextTask = work.task((get, work) => {
1994
+ const edgeVertexCount = get(innerTileSegments) + 3;
1995
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
1996
+ const totalElements = get(maxNodes) * verticesPerNode;
1997
+ return work(() => {
1998
+ const data = new Float32Array(totalElements);
1999
+ const attribute = new webgpu.StorageBufferAttribute(data, 1);
2000
+ const node = tsl.storage(attribute, "float", totalElements);
2001
+ return {
2002
+ data,
2003
+ attribute,
2004
+ node
2005
+ };
2006
+ });
2007
+ }).displayName("createElevationFieldContextTask");
2008
+ const tileNodesTask = work.task((get, work) => {
2009
+ const leafStorage = get(leafStorageTask);
2010
+ const uniforms = get(updateUniformsTask);
2011
+ return work(() => {
2012
+ return createTileCompute(leafStorage, uniforms);
2013
+ });
2014
+ }).displayName("tileNodesTask");
2015
+ const elevationFieldStageTask = work.task((get, work) => {
2016
+ const tile = get(tileNodesTask);
2017
+ const uniforms = get(updateUniformsTask);
2018
+ const elevationFieldContext = get(createElevationFieldContextTask);
2019
+ const userElevationFn = get(elevationFn);
2020
+ return work(() => {
2021
+ const heightFn = createElevationFunction(userElevationFn);
2022
+ const heightWriteFn = createElevation(tile, uniforms, heightFn);
2023
+ return [
2024
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
2025
+ const height = heightWriteFn(nodeIndex, localCoordinates);
2026
+ elevationFieldContext.node.element(globalVertexIndex).assign(height);
2027
+ }
2028
+ ];
2029
+ });
2030
+ }).displayName("elevationFieldStageTask");
2031
+
2032
+ const createTerrainFieldTextureTask = work.task(
2033
+ (get, work, { resources }) => {
2034
+ const edgeVertexCount = get(innerTileSegments) + 3;
2035
+ const maxNodesValue = get(maxNodes);
2036
+ const filter = get(terrainFieldFilter);
2037
+ return work(
2038
+ () => createTerrainFieldStorage(
2039
+ edgeVertexCount,
2040
+ maxNodesValue,
2041
+ resources?.renderer,
2042
+ { filter }
2043
+ )
2044
+ );
2045
+ }
2046
+ ).displayName("createTerrainFieldTextureTask");
2047
+ function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
2048
+ return tsl.Fn(
2049
+ ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
2050
+ const iEdge = tsl.int(edgeVertexCount);
2051
+ const verticesPerNode = iEdge.mul(iEdge);
2052
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
2053
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
2054
+ const xRight = tsl.int(ix).add(tsl.int(1));
2055
+ const yUp = tsl.int(iy).sub(tsl.int(1));
2056
+ const yDown = tsl.int(iy).add(tsl.int(1));
2057
+ const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
2058
+ const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
2059
+ const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
2060
+ const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
2061
+ const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
2062
+ const stepWorld = tileSize.div(innerSegments);
2063
+ const inv2Step = tsl.float(0.5).div(stepWorld);
2064
+ const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
2065
+ const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
2066
+ const normal = tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
2067
+ return tsl.vec2(normal.x, normal.z);
2068
+ }
2069
+ );
2070
+ }
2071
+ const terrainFieldStageTask = work.task((get, work) => {
2072
+ const upstream = get(elevationFieldStageTask);
2073
+ const elevationFieldContext = get(createElevationFieldContextTask);
2074
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
2075
+ const tileEdgeVertexCount = get(innerTileSegments) + 3;
2076
+ const tile = get(tileNodesTask);
2077
+ const uniforms = get(updateUniformsTask);
2078
+ return work(() => {
2079
+ const computeNormal = createNormalFromElevationField(
2080
+ elevationFieldContext.node,
2081
+ tileEdgeVertexCount
2082
+ );
2083
+ return [
2084
+ ...upstream,
2085
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
2086
+ const ix = tsl.int(localCoordinates.x);
2087
+ const iy = tsl.int(localCoordinates.y);
2088
+ const tileSize = tile.tileSize(nodeIndex);
2089
+ const height = elevationFieldContext.node.element(globalVertexIndex);
2090
+ const normalXZ = computeNormal(
2091
+ nodeIndex,
2092
+ tileSize,
2093
+ ix,
2094
+ iy,
2095
+ uniforms.uElevationScale
2096
+ );
2097
+ storeTerrainField(
2098
+ terrainFieldStorage,
2099
+ ix,
2100
+ iy,
2101
+ nodeIndex,
2102
+ packTerrainFieldSample(height, normalXZ)
2103
+ );
2104
+ }
2105
+ ];
2106
+ });
2107
+ }).displayName("terrainFieldStageTask");
2108
+
2109
+ const compileComputeTask = work.task((get, work) => {
2110
+ const pipeline = get(terrainFieldStageTask);
2111
+ const edgeVertexCount = get(innerTileSegments) + 3;
2112
+ return work(
2113
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
2114
+ preferSingleKernelWhenPossible: false
2115
+ })
2116
+ );
2117
+ }).displayName("compileComputeTask");
2118
+ const executeComputeTask = work.task(
2119
+ (get, work, { resources }) => {
2120
+ const { execute } = get(compileComputeTask);
2121
+ const leafState = get(leafGpuBufferTask);
2122
+ return work(
2123
+ () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
2124
+ }
2125
+ );
2126
+ }
2127
+ ).displayName("executeComputeTask").lane("gpu");
2128
+ function createComputePipelineTasks(leafStageTask) {
2129
+ const compile = work.task((get, work) => {
2130
+ const pipeline = get(leafStageTask);
2131
+ const edgeVertexCount = get(innerTileSegments) + 3;
2132
+ return work(
2133
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
2134
+ preferSingleKernelWhenPossible: false
2135
+ })
2136
+ );
2137
+ }).displayName("compileComputeTask");
2138
+ const execute = work.task(
2139
+ (get, work, { resources }) => {
2140
+ const { execute: run } = get(compile);
2141
+ const leafState = get(leafGpuBufferTask);
2142
+ return work(
2143
+ () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
2144
+ }
2145
+ );
2146
+ }
2147
+ ).displayName("executeComputeTask").lane("gpu");
2148
+ return { compile, execute };
2149
+ }
2150
+
2151
+ const SLOT_STRIDE = 6;
2152
+ function nextPow2(n) {
2153
+ let x = 1;
2154
+ while (x < n) x <<= 1;
2155
+ return x;
2156
+ }
2157
+ function createGpuSpatialIndex(maxEntries) {
2158
+ const size = nextPow2(Math.max(2, maxEntries * 2));
2159
+ const data = new Uint32Array(size * SLOT_STRIDE);
2160
+ const attribute = new webgpu.StorageBufferAttribute(data, SLOT_STRIDE);
2161
+ const node = tsl.storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
2162
+ const stampGen = tsl.uniform(tsl.uint(1)).setName("uGpuSpatialIndexStampGen");
2163
+ return {
2164
+ data,
2165
+ size,
2166
+ mask: size - 1,
2167
+ stampGen,
2168
+ attribute,
2169
+ node
2170
+ };
2171
+ }
2172
+ function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
2173
+ if (gpuIndex.size !== cpuIndex.size) {
2174
+ throw new Error(
2175
+ `Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
2176
+ );
2177
+ }
2178
+ for (let i = 0; i < cpuIndex.size; i += 1) {
2179
+ const base = i * SLOT_STRIDE;
2180
+ gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
2181
+ gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
2182
+ gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
2183
+ gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
2184
+ gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
2185
+ gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
2186
+ }
2187
+ gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
2188
+ gpuIndex.attribute.needsUpdate = true;
2189
+ gpuIndex.node.needsUpdate = true;
2190
+ }
2191
+ function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
2192
+ const offset = tsl.int(slot).mul(tsl.int(SLOT_STRIDE)).add(tsl.int(fieldOffset));
2193
+ return spatialIndex.node.element(offset).toUint();
2194
+ }
2195
+ const mix32 = tsl.Fn(([x]) => {
2196
+ const v = tsl.uint(x).toVar();
2197
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2198
+ v.assign(v.mul(tsl.uint(2146121005)));
2199
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(15))));
2200
+ v.assign(v.mul(tsl.uint(2221713035)));
2201
+ v.assign(v.bitXor(v.shiftRight(tsl.uint(16))));
2202
+ return v;
2203
+ });
2204
+ const hashKey = tsl.Fn(([space, level, x, y]) => {
2205
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
2206
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
2207
+ const h = s.bitXor(l.shiftLeft(tsl.uint(8))).bitXor(mix32(tsl.uint(x))).bitXor(mix32(tsl.uint(y)));
2208
+ return mix32(h);
2209
+ });
2210
+ const createGpuSpatialLookup = (spatialIndex) => {
2211
+ const slotCount = spatialIndex.size;
2212
+ const mask = tsl.uint(spatialIndex.mask);
2213
+ const stampGen = spatialIndex.stampGen.toUint();
2214
+ const emptyValue = tsl.int(-1);
2215
+ return tsl.Fn(([space, level, x, y]) => {
2216
+ const s = tsl.uint(space).bitAnd(tsl.uint(255));
2217
+ const l = tsl.uint(level).bitAnd(tsl.uint(255));
2218
+ const xx = tsl.uint(x);
2219
+ const yy = tsl.uint(y);
2220
+ const result = emptyValue.toVar();
2221
+ const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
2222
+ const probes = tsl.int(0).toVar();
2223
+ tsl.Loop(slotCount, () => {
2224
+ const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
2225
+ tsl.If(stamp.notEqual(stampGen), () => {
2226
+ tsl.Break();
2227
+ });
2228
+ const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
2229
+ const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
2230
+ const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
2231
+ const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
2232
+ tsl.If(
2233
+ ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
2234
+ () => {
2235
+ result.assign(tsl.int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
2236
+ tsl.Break();
2237
+ }
2238
+ );
2239
+ slot.assign(slot.add(tsl.uint(1)).bitAnd(mask));
2240
+ probes.addAssign(1);
2241
+ });
2242
+ return result;
2243
+ });
2244
+ };
2245
+ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
2246
+ const lookup = createGpuSpatialLookup(spatialIndex);
2247
+ const levelCount = Math.max(1, maxLevel + 1);
2248
+ return tsl.Fn(([worldX, worldZ]) => {
2249
+ const rootOrigin = uniforms.uRootOrigin.toVar();
2250
+ const rootSize = uniforms.uRootSize.toVar();
2251
+ const halfRoot = rootSize.mul(tsl.float(0.5));
2252
+ const tileIndex = tsl.int(-1).toVar();
2253
+ const tileU = tsl.float(0).toVar();
2254
+ const tileV = tsl.float(0).toVar();
2255
+ const i = tsl.int(0).toVar();
2256
+ tsl.Loop(levelCount, () => {
2257
+ const level = tsl.int(maxLevel).sub(i).toVar();
2258
+ const scale = tsl.pow(tsl.float(2), level.toFloat());
2259
+ const tileSize = rootSize.div(scale);
2260
+ const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
2261
+ const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
2262
+ const maybeIndex = lookup(tsl.int(0), level, tileX, tileY).toVar();
2263
+ tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
2264
+ const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
2265
+ const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
2266
+ tileIndex.assign(maybeIndex);
2267
+ tileU.assign(worldX.sub(minX).div(tileSize));
2268
+ tileV.assign(worldZ.sub(minZ).div(tileSize));
2269
+ tsl.Break();
2270
+ });
2271
+ i.addAssign(1);
2272
+ });
2273
+ return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
2274
+ });
2275
+ };
2276
+
2277
+ const gpuSpatialIndexStorageTask = work.task((get, work) => {
2278
+ const maxNodesValue = get(maxNodes);
2279
+ return work(() => createGpuSpatialIndex(maxNodesValue));
2280
+ }).displayName("gpuSpatialIndexStorageTask");
2281
+ const gpuSpatialIndexUploadTask = work.task((get, work) => {
2282
+ const quadtreeConfig = get(quadtreeConfigTask);
2283
+ get(quadtreeUpdateTask);
2284
+ const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
2285
+ return work(() => {
2286
+ uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
2287
+ return gpuSpatialIndex;
2288
+ });
2289
+ }).displayName("gpuSpatialIndexUploadTask");
2290
+
2291
+ function createTerrainSampleNode(params) {
2292
+ const tileLookup = createTileIndexFromWorldPosition(
2293
+ params.spatialIndex,
2294
+ params.uniforms,
2295
+ maxLevel.get()
2296
+ );
2297
+ return tsl.Fn(([worldX, worldZ]) => {
2298
+ const tileResult = tileLookup(worldX, worldZ).toVar();
2299
+ const tileIndex = tsl.int(tileResult.x).toVar();
2300
+ const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
2301
+ const u = tileResult.y.toVar();
2302
+ const v = tileResult.z.toVar();
2303
+ const fieldU = tileLocalToFieldUV$1(
2304
+ u,
2305
+ params.uniforms.uInnerTileSegments
2306
+ ).toVar();
2307
+ const fieldV = tileLocalToFieldUV$1(
2308
+ v,
2309
+ params.uniforms.uInnerTileSegments
2310
+ ).toVar();
2311
+ const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
2312
+ const sampled = sampleTerrainField(
2313
+ params.terrainFieldStorage,
2314
+ fieldU,
2315
+ fieldV,
2316
+ safeTileIndex
2317
+ ).toVar();
2318
+ const nx = sampled.g.toVar();
2319
+ const nz = sampled.b.toVar();
2320
+ const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
2321
+ const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2322
+ return tsl.vec4(
2323
+ sampled.r.mul(valid),
2324
+ nx.mul(valid),
2325
+ ny.mul(valid),
2326
+ nz.mul(valid)
2327
+ );
2328
+ });
2329
+ }
2330
+ function createTerrainSampler(params) {
2331
+ const elevationNode = createElevationFunction(params.elevationCallback);
2332
+ const terrainSampleAt = createTerrainSampleNode(params);
2333
+ const evaluateElevationAt = tsl.Fn(([worldX, worldZ]) => {
2334
+ const rootOrigin = params.uniforms.uRootOrigin.toVar();
2335
+ const rootSize = params.uniforms.uRootSize.toVar();
2336
+ const centeredX = worldX.sub(rootOrigin.x);
2337
+ const centeredZ = worldZ.sub(rootOrigin.z);
2338
+ const rootUV = tsl.vec2(
2339
+ centeredX.div(rootSize).add(0.5),
2340
+ centeredZ.div(rootSize).mul(tsl.float(-1)).add(0.5)
2341
+ ).toVar();
2342
+ return elevationNode({
2343
+ worldPosition: tsl.vec3(worldX, rootOrigin.y, worldZ),
2344
+ rootSize,
2345
+ rootUV,
2346
+ tileUV: rootUV,
2347
+ tileLevel: tsl.int(0),
2348
+ tileSize: rootSize,
2349
+ tileOriginVec2: tsl.vec2(0, 0),
2350
+ nodeIndex: tsl.int(0)
2351
+ });
2352
+ });
2353
+ const sampleTerrain = tsl.Fn(
2354
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
2355
+ );
2356
+ const sampleElevation = tsl.Fn(
2357
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
2358
+ );
2359
+ const sampleNormal = tsl.Fn(
2360
+ ([worldX, worldZ]) => tsl.vec3(
2361
+ terrainSampleAt(worldX, worldZ).y,
2362
+ terrainSampleAt(worldX, worldZ).z,
2363
+ terrainSampleAt(worldX, worldZ).w
2364
+ )
2365
+ );
2366
+ const sampleValidity = tsl.Fn(
2367
+ ([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0))
2368
+ );
2369
+ const evaluateElevation = tsl.Fn(
2370
+ ([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
2371
+ );
2372
+ const evaluateNormalNode = tsl.Fn(
2373
+ ([worldX, worldZ, epsilon]) => {
2374
+ const eps = epsilon ?? tsl.float(0.1);
2375
+ const elevationScale = params.uniforms.uElevationScale.toVar();
2376
+ const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
2377
+ elevationScale
2378
+ );
2379
+ const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
2380
+ elevationScale
2381
+ );
2382
+ const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
2383
+ elevationScale
2384
+ );
2385
+ const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
2386
+ elevationScale
2387
+ );
2388
+ const inv2eps = tsl.float(0.5).div(eps);
2389
+ const dhdx = hR.sub(hL).mul(inv2eps);
2390
+ const dhdz = hU.sub(hD).mul(inv2eps);
2391
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
2392
+ }
2393
+ );
2394
+ const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
2395
+ return {
2396
+ sampleElevation,
2397
+ sampleNormal,
2398
+ sampleTerrain,
2399
+ sampleValidity,
2400
+ evaluateElevation,
2401
+ evaluateNormal
2402
+ };
2403
+ }
2404
+
2405
+ const createTerrainSamplerTask = work.task((get, work) => {
2406
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
2407
+ const spatialIndex = get(gpuSpatialIndexStorageTask);
2408
+ const uniforms = get(updateUniformsTask);
2409
+ const elevationCallback = get(elevationFn);
2410
+ return work(
2411
+ () => createTerrainSampler({
2412
+ terrainFieldStorage,
2413
+ spatialIndex,
2414
+ uniforms,
2415
+ elevationCallback
2416
+ })
2417
+ );
2418
+ }).displayName("createTerrainSamplerTask");
2419
+
218
2420
  const isSkirtVertex = tsl.Fn(([segments]) => {
219
2421
  const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
220
2422
  const vIndex = tsl.int(tsl.vertexIndex);
@@ -235,6 +2437,483 @@ const isSkirtUV = tsl.Fn(([segments]) => {
235
2437
  return innerX.and(innerY).not();
236
2438
  });
237
2439
 
2440
+ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
2441
+ return tsl.Fn(() => {
2442
+ const nodeIndex = tsl.int(tsl.instanceIndex);
2443
+ const nodeOffset = nodeIndex.mul(tsl.int(4));
2444
+ const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
2445
+ const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
2446
+ const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
2447
+ const rootSize = terrainUniforms.uRootSize.toVar();
2448
+ const rootOrigin = terrainUniforms.uRootOrigin.toVar();
2449
+ const half = tsl.float(0.5);
2450
+ const size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
2451
+ const halfRoot = rootSize.mul(half);
2452
+ const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
2453
+ const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
2454
+ const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
2455
+ const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
2456
+ const worldX = centerX.add(clampedX.mul(size));
2457
+ const worldZ = centerZ.add(clampedZ.mul(size));
2458
+ return tsl.vec3(worldX, rootOrigin.y, worldZ);
2459
+ });
2460
+ }
2461
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
2462
+ if (!terrainFieldStorage) return tsl.float(0);
2463
+ const innerSegs = terrainUniforms.uInnerTileSegments;
2464
+ const u = tileLocalToFieldUV$1(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
2465
+ const v = tileLocalToFieldUV$1(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
2466
+ return sampleTerrainFieldElevation(
2467
+ terrainFieldStorage,
2468
+ u,
2469
+ v,
2470
+ tsl.int(tsl.instanceIndex)
2471
+ ).mul(terrainUniforms.uElevationScale);
2472
+ }
2473
+ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
2474
+ if (!terrainFieldStorage) return;
2475
+ const nodeIndex = tsl.int(tsl.instanceIndex);
2476
+ const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
2477
+ const localVertexIndex = tsl.int(tsl.vertexIndex);
2478
+ const ix = localVertexIndex.mod(edgeVertexCount);
2479
+ const iy = localVertexIndex.div(edgeVertexCount);
2480
+ const normalXZ = loadTerrainFieldNormal(
2481
+ terrainFieldStorage,
2482
+ ix,
2483
+ iy,
2484
+ nodeIndex
2485
+ );
2486
+ const nx = normalXZ.x;
2487
+ const nz = normalXZ.y;
2488
+ const nySq = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0));
2489
+ const ny = nySq.sqrt();
2490
+ tsl.normalLocal.assign(tsl.vec3(nx, ny, nz));
2491
+ }
2492
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
2493
+ const baseWorldPosition = createTileBaseWorldPosition(
2494
+ leafStorage,
2495
+ terrainUniforms
2496
+ );
2497
+ return tsl.Fn(() => {
2498
+ const base = baseWorldPosition();
2499
+ const yElevation = createTileElevation(
2500
+ terrainUniforms,
2501
+ terrainFieldStorage
2502
+ );
2503
+ const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
2504
+ const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
2505
+ const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
2506
+ createNormalAssignment(terrainUniforms, terrainFieldStorage);
2507
+ return tsl.vec3(base.x, worldY, base.z);
2508
+ })();
2509
+ }
2510
+
2511
+ const positionNodeTask = work.task((get, work) => {
2512
+ const leafStorage = get(leafStorageTask);
2513
+ const terrainUniforms = get(updateUniformsTask);
2514
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
2515
+ return work(
2516
+ () => createTileWorldPosition(
2517
+ leafStorage,
2518
+ terrainUniforms,
2519
+ terrainFieldStorage
2520
+ )
2521
+ );
2522
+ }).displayName("positionNodeTask");
2523
+
2524
+ function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
2525
+ let tMin = -Infinity;
2526
+ let tMax = Infinity;
2527
+ const origin = ray.origin;
2528
+ const dir = ray.direction;
2529
+ const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
2530
+ if (Math.abs(dirAxis) < 1e-8) {
2531
+ if (originAxis < minAxis || originAxis > maxAxis) return false;
2532
+ return true;
2533
+ }
2534
+ const inv = 1 / dirAxis;
2535
+ let t0 = (minAxis - originAxis) * inv;
2536
+ let t1 = (maxAxis - originAxis) * inv;
2537
+ if (t0 > t1) {
2538
+ const tmp = t0;
2539
+ t0 = t1;
2540
+ t1 = tmp;
2541
+ }
2542
+ tMin = Math.max(tMin, t0);
2543
+ tMax = Math.min(tMax, t1);
2544
+ return tMax >= tMin;
2545
+ };
2546
+ if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
2547
+ return null;
2548
+ }
2549
+ return { tMin, tMax };
2550
+ }
2551
+ function getTerrainBounds(config) {
2552
+ const halfRoot = config.rootSize * 0.5;
2553
+ return {
2554
+ minX: config.originX - halfRoot,
2555
+ maxX: config.originX + halfRoot,
2556
+ minZ: config.originZ - halfRoot,
2557
+ maxZ: config.originZ + halfRoot
2558
+ };
2559
+ }
2560
+ function terrainSignedDistanceFromBounds(query, worldX, worldY, worldZ) {
2561
+ const tileBounds = query.getTileBounds(worldX, worldZ);
2562
+ if (tileBounds) {
2563
+ if (worldY > tileBounds.maxElevation) {
2564
+ return worldY - tileBounds.maxElevation;
2565
+ }
2566
+ if (worldY < tileBounds.minElevation) {
2567
+ return worldY - tileBounds.minElevation;
2568
+ }
2569
+ }
2570
+ const elevation = query.getElevation(worldX, worldZ);
2571
+ if (!Number.isFinite(elevation)) return void 0;
2572
+ return worldY - elevation;
2573
+ }
2574
+ function terrainSignedDistancePrecise(query, worldX, worldY, worldZ) {
2575
+ const elevation = query.getElevation(worldX, worldZ);
2576
+ if (!Number.isFinite(elevation)) return void 0;
2577
+ return worldY - elevation;
2578
+ }
2579
+ function cpuRaycast(query, ray, config, options) {
2580
+ const bounds = getTerrainBounds(config);
2581
+ const segment = intersectRayAabb(
2582
+ ray,
2583
+ bounds.minX,
2584
+ config.minY,
2585
+ bounds.minZ,
2586
+ bounds.maxX,
2587
+ config.maxY,
2588
+ bounds.maxZ
2589
+ );
2590
+ if (!segment) return null;
2591
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
2592
+ let startT = Math.max(0, segment.tMin);
2593
+ const endT = Math.min(segment.tMax, maxDistance);
2594
+ if (endT < startT) return null;
2595
+ const maxSteps = Math.max(8, options?.maxSteps ?? 128);
2596
+ const refinementSteps = Math.max(1, options?.refinementSteps ?? 8);
2597
+ const point = new three.Vector3();
2598
+ let prevT = startT;
2599
+ ray.at(prevT, point);
2600
+ let prevSignedDistance = terrainSignedDistanceFromBounds(
2601
+ query,
2602
+ point.x,
2603
+ point.y,
2604
+ point.z
2605
+ );
2606
+ if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
2607
+ const sample = query.sampleTerrain(point.x, point.z);
2608
+ if (!sample.valid) return null;
2609
+ point.y = sample.elevation;
2610
+ return {
2611
+ position: point.clone(),
2612
+ normal: sample.normal.clone(),
2613
+ distance: ray.origin.distanceTo(point)
2614
+ };
2615
+ }
2616
+ for (let i = 1; i <= maxSteps; i += 1) {
2617
+ const t = startT + (endT - startT) * i / maxSteps;
2618
+ ray.at(t, point);
2619
+ const signedDistance = terrainSignedDistanceFromBounds(
2620
+ query,
2621
+ point.x,
2622
+ point.y,
2623
+ point.z
2624
+ );
2625
+ if (signedDistance === void 0) {
2626
+ prevSignedDistance = void 0;
2627
+ prevT = t;
2628
+ continue;
2629
+ }
2630
+ if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
2631
+ let lo = prevT;
2632
+ let hi = t;
2633
+ for (let r = 0; r < refinementSteps; r += 1) {
2634
+ const mid = (lo + hi) * 0.5;
2635
+ ray.at(mid, point);
2636
+ const midDistance = terrainSignedDistancePrecise(
2637
+ query,
2638
+ point.x,
2639
+ point.y,
2640
+ point.z
2641
+ );
2642
+ if (midDistance === void 0) {
2643
+ lo = mid;
2644
+ continue;
2645
+ }
2646
+ if (midDistance > 0) lo = mid;
2647
+ else hi = mid;
2648
+ }
2649
+ const hitT = hi;
2650
+ ray.at(hitT, point);
2651
+ const sample = query.sampleTerrain(point.x, point.z);
2652
+ if (!sample.valid) return null;
2653
+ point.y = sample.elevation;
2654
+ return {
2655
+ position: point.clone(),
2656
+ normal: sample.normal.clone(),
2657
+ distance: ray.origin.distanceTo(point)
2658
+ };
2659
+ }
2660
+ prevSignedDistance = signedDistance;
2661
+ prevT = t;
2662
+ }
2663
+ return null;
2664
+ }
2665
+ function cpuRaycastBoundsOnly(ray, config, options) {
2666
+ const bounds = getTerrainBounds(config);
2667
+ const planeY = (config.minY + config.maxY) * 0.5;
2668
+ const dirY = ray.direction.y;
2669
+ if (Math.abs(dirY) < 1e-8) return null;
2670
+ const t = (planeY - ray.origin.y) / dirY;
2671
+ if (t < 0) return null;
2672
+ const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
2673
+ if (t > maxDistance) return null;
2674
+ const point = new three.Vector3();
2675
+ ray.at(t, point);
2676
+ if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
2677
+ return null;
2678
+ }
2679
+ return {
2680
+ position: point,
2681
+ normal: new three.Vector3(0, 1, 0),
2682
+ distance: ray.origin.distanceTo(point)
2683
+ };
2684
+ }
2685
+
2686
+ function createTerrainRaycast(params) {
2687
+ return {
2688
+ pick(ray, options) {
2689
+ const config = params.getConfig();
2690
+ const terrainQuery = params.getTerrainQuery();
2691
+ if (terrainQuery) {
2692
+ const precise = cpuRaycast(terrainQuery, ray, config, options);
2693
+ if (precise) return precise;
2694
+ }
2695
+ const coarse = cpuRaycastBoundsOnly(ray, config, options);
2696
+ if (coarse && terrainQuery) {
2697
+ const sample = terrainQuery.sampleTerrain(
2698
+ coarse.position.x,
2699
+ coarse.position.z
2700
+ );
2701
+ if (sample.valid) {
2702
+ coarse.position.y = sample.elevation;
2703
+ coarse.normal.copy(sample.normal);
2704
+ }
2705
+ }
2706
+ return coarse;
2707
+ }
2708
+ };
2709
+ }
2710
+
2711
+ const BOUNDS_PADDING = 1;
2712
+ const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
2713
+ const terrainRaycastTask = work.task(
2714
+ (get, work) => {
2715
+ const { query: terrainQuery } = get(terrainQueryTask);
2716
+ const rootSizeValue = get(rootSize);
2717
+ const originValue = get(origin);
2718
+ const elevationScaleValue = get(elevationScale);
2719
+ return work((prev) => {
2720
+ let raycast = prev;
2721
+ let state = raycast?.[RAYCAST_STATE];
2722
+ if (!state) {
2723
+ state = {
2724
+ terrainQuery: null,
2725
+ bounds: {
2726
+ rootSize: 0,
2727
+ originX: 0,
2728
+ originZ: 0,
2729
+ minY: 0,
2730
+ maxY: 0
2731
+ }
2732
+ };
2733
+ }
2734
+ state.terrainQuery = terrainQuery;
2735
+ state.bounds.rootSize = rootSizeValue;
2736
+ state.bounds.originX = originValue.x;
2737
+ state.bounds.originZ = originValue.z;
2738
+ const range = terrainQuery.getGlobalElevationRange();
2739
+ if (range) {
2740
+ state.bounds.minY = range.min - BOUNDS_PADDING;
2741
+ state.bounds.maxY = range.max + BOUNDS_PADDING;
2742
+ } else {
2743
+ const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
2744
+ state.bounds.minY = originValue.y - verticalExtent;
2745
+ state.bounds.maxY = originValue.y + verticalExtent;
2746
+ }
2747
+ if (!raycast) {
2748
+ raycast = createTerrainRaycast({
2749
+ getTerrainQuery: () => state.terrainQuery,
2750
+ getConfig: () => state.bounds
2751
+ });
2752
+ }
2753
+ raycast[RAYCAST_STATE] = state;
2754
+ return raycast;
2755
+ });
2756
+ }
2757
+ ).displayName("terrainRaycastTask");
2758
+
2759
+ function terrainGraph() {
2760
+ return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(gpuSpatialIndexStorageTask).add(gpuSpatialIndexUploadTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(createTerrainSamplerTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask).add(tileBoundsContextTask).add(tileBoundsReductionTask).add(terrainQueryTask).add(terrainReadbackTask).add(terrainRaycastTask);
2761
+ }
2762
+ const terrainTasks = {
2763
+ instanceId: instanceIdTask,
2764
+ quadtreeConfig: quadtreeConfigTask,
2765
+ quadtreeUpdate: quadtreeUpdateTask,
2766
+ leafStorage: leafStorageTask,
2767
+ surface: surfaceTask,
2768
+ leafGpuBuffer: leafGpuBufferTask,
2769
+ gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
2770
+ gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
2771
+ createUniforms: createUniformsTask,
2772
+ updateUniforms: updateUniformsTask,
2773
+ positionNode: positionNodeTask,
2774
+ createElevationFieldContext: createElevationFieldContextTask,
2775
+ createTileNodes: tileNodesTask,
2776
+ createTerrainFieldTexture: createTerrainFieldTextureTask,
2777
+ createTerrainSampler: createTerrainSamplerTask,
2778
+ elevationFieldStage: elevationFieldStageTask,
2779
+ terrainFieldStage: terrainFieldStageTask,
2780
+ compileCompute: compileComputeTask,
2781
+ executeCompute: executeComputeTask,
2782
+ tileBoundsContext: tileBoundsContextTask,
2783
+ tileBoundsReduction: tileBoundsReductionTask,
2784
+ terrainQuery: terrainQueryTask,
2785
+ terrainReadback: terrainReadbackTask,
2786
+ terrainRaycast: terrainRaycastTask
2787
+ };
2788
+
2789
+ const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
2790
+ return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
2791
+ });
2792
+ const vectorSpaceToTextureSpace = tsl.Fn(([value]) => {
2793
+ return tsl.remap(value, tsl.float(-1), tsl.float(1), tsl.float(0), tsl.float(1));
2794
+ });
2795
+ const blendAngleCorrectedNormals = tsl.Fn(([n1, n2]) => {
2796
+ const t = tsl.vec3(n1.x, n1.y, n1.z.add(1));
2797
+ const u = tsl.vec3(n2.x.negate(), n2.y.negate(), n2.z);
2798
+ const r = t.mul(tsl.dot(t, u)).sub(u.mul(t.z)).normalize();
2799
+ return r;
2800
+ });
2801
+ const deriveNormalZ = tsl.Fn(([normalXY]) => {
2802
+ const xy = normalXY.toVar();
2803
+ const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
2804
+ return tsl.vec3(xy.x, xy.y, z);
2805
+ });
2806
+
2807
+ const vGlobalVertexIndex = /* @__PURE__ */ tsl.varyingProperty("int", "vGlobalVertexIndex");
2808
+ const vElevation = /* @__PURE__ */ tsl.varyingProperty("f32", "vElevation");
2809
+
2810
+ const cellCenter = tsl.Fn(({ cell }) => {
2811
+ return cell.add(tsl.mx_noise_float(cell.mul(Math.PI)));
2812
+ });
2813
+ const voronoiCells = tsl.Fn((params) => {
2814
+ const scale = tsl.float(params.scale);
2815
+ const facet = tsl.float(params.facet);
2816
+ const seed = tsl.float(params.seed);
2817
+ const pos = params.uv.mul(scale).add(seed);
2818
+ const midCell = pos.round().toVar();
2819
+ const minCell = midCell.toVar();
2820
+ const minDist = tsl.float(1).toVar();
2821
+ const cell = tsl.vec3(0, 0, 0).toVar();
2822
+ const dist = tsl.float().toVar();
2823
+ const i = tsl.float(0).toVar();
2824
+ tsl.Loop(27, () => {
2825
+ const ix = i.mod(3).sub(1);
2826
+ const iy = i.div(3).floor().mod(3).sub(1);
2827
+ const iz = i.div(9).floor().sub(1);
2828
+ cell.assign(midCell.add(tsl.vec3(ix, iy, iz)));
2829
+ dist.assign(pos.distance(cellCenter({ cell })).add(tsl.mx_noise_float(pos).div(5)));
2830
+ tsl.If(dist.lessThan(minDist), () => {
2831
+ minDist.assign(dist);
2832
+ minCell.assign(cell);
2833
+ });
2834
+ i.addAssign(1);
2835
+ });
2836
+ const n = tsl.mx_noise_float(minCell.mul(Math.PI)).toVar();
2837
+ const k = tsl.mix(minDist, n.add(1).div(2), facet);
2838
+ return k;
2839
+ });
2840
+
2841
+ exports.ArrayTextureBackend = ArrayTextureBackend;
2842
+ exports.AtlasBackend = AtlasBackend;
2843
+ exports.Dir = Dir;
238
2844
  exports.TerrainGeometry = TerrainGeometry;
2845
+ exports.TerrainMesh = TerrainMesh;
2846
+ exports.Texture3DBackend = Texture3DBackend;
2847
+ exports.U32_EMPTY = U32_EMPTY;
2848
+ exports.allocLeafSet = allocLeafSet;
2849
+ exports.allocSeamTable = allocSeamTable;
2850
+ exports.beginUpdate = beginUpdate;
2851
+ exports.blendAngleCorrectedNormals = blendAngleCorrectedNormals;
2852
+ exports.buildLeafIndex = buildLeafIndex;
2853
+ exports.buildSeams2to1 = buildSeams2to1;
2854
+ exports.compileComputeTask = compileComputeTask;
2855
+ exports.createComputePipelineTasks = createComputePipelineTasks;
2856
+ exports.createCubeSphereSurface = createCubeSphereSurface;
2857
+ exports.createElevationFieldContextTask = createElevationFieldContextTask;
2858
+ exports.createFlatSurface = createFlatSurface;
2859
+ exports.createInfiniteFlatSurface = createInfiniteFlatSurface;
2860
+ exports.createSpatialIndex = createSpatialIndex;
2861
+ exports.createState = createState;
2862
+ exports.createTerrainFieldStorage = createTerrainFieldStorage;
2863
+ exports.createTerrainFieldTextureTask = createTerrainFieldTextureTask;
2864
+ exports.createTerrainQuery = createTerrainQuery;
2865
+ exports.createTerrainRaycast = createTerrainRaycast;
2866
+ exports.createTerrainSampler = createTerrainSampler;
2867
+ exports.createTerrainSamplerTask = createTerrainSamplerTask;
2868
+ exports.createTerrainUniforms = createTerrainUniforms;
2869
+ exports.createUniformsTask = createUniformsTask;
2870
+ exports.deriveNormalZ = deriveNormalZ;
2871
+ exports.elevationFieldStageTask = elevationFieldStageTask;
2872
+ exports.elevationFn = elevationFn;
2873
+ exports.elevationScale = elevationScale;
2874
+ exports.executeComputeTask = executeComputeTask;
2875
+ exports.getDeviceComputeLimits = getDeviceComputeLimits;
2876
+ exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
2877
+ exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
2878
+ exports.innerTileSegments = innerTileSegments;
2879
+ exports.instanceIdTask = instanceIdTask;
239
2880
  exports.isSkirtUV = isSkirtUV;
240
2881
  exports.isSkirtVertex = isSkirtVertex;
2882
+ exports.leafGpuBufferTask = leafGpuBufferTask;
2883
+ exports.leafStorageTask = leafStorageTask;
2884
+ exports.loadTerrainField = loadTerrainField;
2885
+ exports.loadTerrainFieldElevation = loadTerrainFieldElevation;
2886
+ exports.loadTerrainFieldNormal = loadTerrainFieldNormal;
2887
+ exports.maxLevel = maxLevel;
2888
+ exports.maxNodes = maxNodes;
2889
+ exports.origin = origin;
2890
+ exports.packTerrainFieldSample = packTerrainFieldSample;
2891
+ exports.positionNodeTask = positionNodeTask;
2892
+ exports.quadtreeConfigTask = quadtreeConfigTask;
2893
+ exports.quadtreeUpdate = quadtreeUpdate;
2894
+ exports.quadtreeUpdateTask = quadtreeUpdateTask;
2895
+ exports.resetLeafSet = resetLeafSet;
2896
+ exports.resetSeamTable = resetSeamTable;
2897
+ exports.rootSize = rootSize;
2898
+ exports.sampleTerrainField = sampleTerrainField;
2899
+ exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
2900
+ exports.sampleTerrainFieldNormal = sampleTerrainFieldNormal;
2901
+ exports.skirtScale = skirtScale;
2902
+ exports.storeTerrainField = storeTerrainField;
2903
+ exports.surface = surface;
2904
+ exports.surfaceTask = surfaceTask;
2905
+ exports.terrainFieldFilter = terrainFieldFilter;
2906
+ exports.terrainFieldStageTask = terrainFieldStageTask;
2907
+ exports.terrainGraph = terrainGraph;
2908
+ exports.terrainQueryTask = terrainQueryTask;
2909
+ exports.terrainRaycastTask = terrainRaycastTask;
2910
+ exports.terrainReadbackTask = terrainReadbackTask;
2911
+ exports.terrainTasks = terrainTasks;
2912
+ exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
2913
+ exports.tileNodesTask = tileNodesTask;
2914
+ exports.update = update;
2915
+ exports.updateUniformsTask = updateUniformsTask;
2916
+ exports.vElevation = vElevation;
2917
+ exports.vGlobalVertexIndex = vGlobalVertexIndex;
2918
+ exports.vectorSpaceToTextureSpace = vectorSpaceToTextureSpace;
2919
+ exports.voronoiCells = voronoiCells;