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

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
@@ -361,6 +361,7 @@ function compileComputePipeline(stages, width, options) {
361
361
  WORKGROUP_X,
362
362
  WORKGROUP_Y
363
363
  ];
364
+ const midPipelineExecute = options?.midPipelineExecute;
364
365
  const uInstanceCount = tsl.uniform(0, "uint").setName("uInstanceCount");
365
366
  const stagedKernelCache = /* @__PURE__ */ new Map();
366
367
  function clampWorkgroupToLimits(requested, limits) {
@@ -421,281 +422,14 @@ function compileComputePipeline(stages, width, options) {
421
422
  }
422
423
  const dispatchX = Math.ceil(width / workgroupX);
423
424
  const dispatchY = Math.ceil(width / workgroupY);
424
- for (const kernel of stagedKernels) {
425
- renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
426
- }
427
- }
428
- return { execute };
429
- }
430
-
431
- function resolveType(format) {
432
- return format === "rgba16float" ? three.HalfFloatType : three.FloatType;
433
- }
434
- function resolveFilter(mode) {
435
- return mode === "linear" ? three.LinearFilter : three.NearestFilter;
436
- }
437
- function configureStorageTexture(texture2, format, filter) {
438
- texture2.format = three.RGBAFormat;
439
- texture2.type = resolveType(format);
440
- texture2.magFilter = resolveFilter(filter);
441
- texture2.minFilter = resolveFilter(filter);
442
- texture2.wrapS = three.ClampToEdgeWrapping;
443
- texture2.wrapT = three.ClampToEdgeWrapping;
444
- texture2.generateMipmaps = false;
445
- texture2.needsUpdate = true;
446
- }
447
- function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
448
- let currentEdgeVertexCount = edgeVertexCount;
449
- let currentTileCount = tileCount;
450
- const tex = new webgpu.StorageArrayTexture(
451
- edgeVertexCount,
452
- edgeVertexCount,
453
- tileCount
454
- );
455
- tex.name = "terrainField";
456
- configureStorageTexture(tex, options.format, options.filter);
457
- return {
458
- backendType: "array-texture",
459
- get edgeVertexCount() {
460
- return currentEdgeVertexCount;
461
- },
462
- get tileCount() {
463
- return currentTileCount;
464
- },
465
- texture: tex,
466
- uv(ix, iy, _tileIndex) {
467
- return tsl.vec2(ix.toFloat(), iy.toFloat());
468
- },
469
- texel(ix, iy, tileIndex) {
470
- return tsl.ivec3(ix, iy, tileIndex);
471
- },
472
- sample(u, v, tileIndex) {
473
- return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
474
- },
475
- resize(width, height, nextTileCount) {
476
- currentEdgeVertexCount = width;
477
- currentTileCount = nextTileCount;
478
- tex.setSize(width, height, nextTileCount);
479
- tex.needsUpdate = true;
480
- }
481
- };
482
- }
483
- function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
484
- const tilesPerRowNode = tsl.int(tilesPerRow);
485
- const edge = tsl.int(edgeVertexCount);
486
- const tile = tsl.int(tileIndex);
487
- const col = tile.mod(tilesPerRowNode);
488
- const row = tile.div(tilesPerRowNode);
489
- const atlasX = col.mul(edge).add(tsl.int(ix));
490
- const atlasY = row.mul(edge).add(tsl.int(iy));
491
- return { atlasX, atlasY };
492
- }
493
- function AtlasBackend(edgeVertexCount, tileCount, options) {
494
- let currentEdgeVertexCount = edgeVertexCount;
495
- let currentTileCount = tileCount;
496
- let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
497
- const atlasSize = tilesPerRow * edgeVertexCount;
498
- const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
499
- tex.name = "terrainFieldAtlas";
500
- configureStorageTexture(tex, options.format, options.filter);
501
- return {
502
- backendType: "atlas",
503
- get edgeVertexCount() {
504
- return currentEdgeVertexCount;
505
- },
506
- get tileCount() {
507
- return currentTileCount;
508
- },
509
- texture: tex,
510
- uv(ix, iy, tileIndex) {
511
- const { atlasX, atlasY } = atlasCoord(
512
- tilesPerRow,
513
- currentEdgeVertexCount,
514
- ix,
515
- iy,
516
- tileIndex
517
- );
518
- const currentAtlasSize = tsl.float(tilesPerRow * currentEdgeVertexCount);
519
- return tsl.vec2(
520
- atlasX.toFloat().add(0.5).div(currentAtlasSize),
521
- atlasY.toFloat().add(0.5).div(currentAtlasSize)
522
- );
523
- },
524
- texel(ix, iy, tileIndex) {
525
- const { atlasX, atlasY } = atlasCoord(
526
- tilesPerRow,
527
- currentEdgeVertexCount,
528
- ix,
529
- iy,
530
- tileIndex
531
- );
532
- return tsl.ivec2(atlasX, atlasY);
533
- },
534
- sample(u, v, tileIndex) {
535
- const tile = tsl.int(tileIndex);
536
- const tilesPerRowNode = tsl.int(tilesPerRow);
537
- const col = tile.mod(tilesPerRowNode);
538
- const row = tile.div(tilesPerRowNode);
539
- const invTilesPerRow = tsl.float(1 / tilesPerRow);
540
- const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
541
- const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
542
- return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
543
- },
544
- resize(width, height, nextTileCount) {
545
- currentEdgeVertexCount = width;
546
- currentTileCount = nextTileCount;
547
- tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
548
- const nextAtlasSize = tilesPerRow * width;
549
- const image = tex.image;
550
- image.width = nextAtlasSize;
551
- image.height = nextAtlasSize;
552
- tex.needsUpdate = true;
553
- }
554
- };
555
- }
556
- function texture3DBackend(edgeVertexCount, tileCount, options) {
557
- const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
558
- return { ...storage, backendType: "texture-3d" };
559
- }
560
- function tryGetDeviceLimits(renderer) {
561
- const backend = renderer;
562
- return backend.backend?.device?.limits ?? {};
563
- }
564
- function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
565
- const filter = options.filter ?? "linear";
566
- const format = options.format ?? "rgba16float";
567
- const forcedBackend = options.backend;
568
- if (forcedBackend === "atlas") {
569
- return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
570
- }
571
- if (forcedBackend === "texture-3d") {
572
- return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
573
- }
574
- if (forcedBackend === "array-texture") {
575
- return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
576
- }
577
- const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
578
- const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
579
- if (tileCount > maxLayers) {
580
- return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
581
- }
582
- return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
583
- }
584
- function storeTerrainField(storage, ix, iy, tileIndex, value) {
585
- if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
586
- return tsl.textureStore(
587
- storage.texture,
588
- tsl.uvec3(tsl.int(ix), tsl.int(iy), tsl.int(tileIndex)),
589
- value
590
- );
591
- }
592
- return tsl.textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
593
- }
594
- function loadTerrainField(storage, ix, iy, tileIndex) {
595
- if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
596
- return tsl.textureLoad(storage.texture, tsl.ivec2(tsl.int(ix), tsl.int(iy)), tsl.int(0)).depth(
597
- tsl.int(tileIndex)
598
- );
599
- }
600
- return tsl.textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), tsl.int(0));
601
- }
602
- function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
603
- return loadTerrainField(storage, ix, iy, tileIndex).r;
604
- }
605
- function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
606
- const raw = loadTerrainField(storage, ix, iy, tileIndex);
607
- return tsl.vec3(raw.g, raw.b, raw.a);
608
- }
609
- function sampleTerrainField(storage, u, v, tileIndex) {
610
- return storage.sample(u, v, tileIndex);
611
- }
612
- function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
613
- return sampleTerrainField(storage, u, v, tileIndex).r;
614
- }
615
- function packTerrainFieldSample(height, normal) {
616
- return tsl.vec4(height, normal.x, normal.y, normal.z);
617
- }
618
-
619
- const createElevation = (tile, uniforms, elevationFn) => {
620
- return function perVertexElevation(nodeIndex, localCoordinates) {
621
- const ix = tsl.int(localCoordinates.x);
622
- const iy = tsl.int(localCoordinates.y);
623
- const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
624
- const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
625
- const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
626
- const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
627
- const rootSize = uniforms.uRootSize.toVar();
628
- return elevationFn({
629
- worldPosition,
630
- rootSize,
631
- rootUV,
632
- tileOriginVec2: tile.tileOriginVec2(nodeIndex),
633
- tileSize: tile.tileSize(nodeIndex),
634
- tileLevel: tile.tileLevel(nodeIndex),
635
- nodeIndex: tsl.int(nodeIndex),
636
- tileUV
637
- });
638
- };
639
- };
640
-
641
- const HALF_PI = Math.PI * 0.5;
642
- const FIELD_INNER_TEXEL_OFFSET = 1.5;
643
- const FIELD_EDGE_EXTRA_TEXELS = 3;
644
- function sphereTileArcLength(radius, levelDivisor) {
645
- return radius * HALF_PI / levelDivisor;
646
- }
647
- function decodeLeafTile(leafStorage, nodeIndex) {
648
- const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
649
- return {
650
- level: leafStorage.node.element(nodeOffset).toInt(),
651
- x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
652
- y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
653
- face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
654
- };
655
- }
656
- function faceUVFromTileLocal(tile, localU, localV, baseU = tsl.float(1), baseV = tsl.float(1)) {
657
- const levelScale = tsl.pow(tsl.float(2), tile.level.toFloat());
658
- const nU = baseU.mul(levelScale);
659
- const nV = baseV.mul(levelScale);
660
- return tsl.vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
661
- }
662
- function createTileCompute(leafStorage, uniforms, projection) {
663
- const baseU = tsl.float(projection.baseResolution?.u ?? 1);
664
- const baseV = tsl.float(projection.baseResolution?.v ?? 1);
665
- const tileLevel = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
666
- const tileFace = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
667
- const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
668
- const tile = decodeLeafTile(leafStorage, nodeIndex);
669
- return tsl.vec2(tile.x, tile.y);
670
- });
671
- const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
672
- const tile = decodeLeafTile(leafStorage, nodeIndex);
673
- const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
674
- const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
675
- const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
676
- return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
677
- });
678
- const shared = {
679
- tileLevel: (nodeIndex) => tileLevel(nodeIndex),
680
- tileFace: (nodeIndex) => tileFace(nodeIndex),
681
- tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
682
- tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
683
- };
684
- const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
685
- return {
686
- ...shared,
687
- tileSize: parts.tileSize,
688
- rootUVCompute: parts.rootUV,
689
- tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
690
- };
691
- }
692
- function tileLocalToFieldUV(localCoord, innerSegments) {
693
- const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
694
- return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
695
- }
696
- function tileLocalToFieldUVNumber(localCoord, innerSegments) {
697
- const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
698
- return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
425
+ for (let stageIndex = 0; stageIndex < stagedKernels.length; stageIndex += 1) {
426
+ renderer.compute(stagedKernels[stageIndex], [dispatchX, dispatchY, instanceCount]);
427
+ if (midPipelineExecute && stagedKernels.length > 1 && stageIndex === stagedKernels.length - 2) {
428
+ midPipelineExecute(renderer, instanceCount);
429
+ }
430
+ }
431
+ }
432
+ return { execute };
699
433
  }
700
434
 
701
435
  function createLeafStorage(maxNodes) {
@@ -937,20 +671,15 @@ function beginUpdate(state, topology, params) {
937
671
 
938
672
  function shouldSplit(bounds, level, maxLevel, params) {
939
673
  if (level >= maxLevel) return false;
940
- const mode = params.mode ?? "distance";
941
674
  const cx = bounds.cx;
942
675
  const cy = bounds.cy;
943
676
  const cz = bounds.cz;
944
677
  const distSq = cx * cx + cy * cy + cz * cz;
945
678
  const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
946
- if (mode === "screen") {
947
- const proj = params.projectionFactor ?? 0;
948
- const target = params.targetPixels ?? 0;
949
- if (proj <= 0 || target <= 0) {
950
- const f2 = params.distanceFactor ?? 2;
951
- const threshold2 = bounds.r * f2;
952
- return safeDistSq < threshold2 * threshold2;
953
- }
679
+ if (params.mode === "screen") {
680
+ const proj = params.projectionFactor;
681
+ const target = params.targetPixels;
682
+ if (proj <= 0 || target <= 0) return false;
954
683
  const left = bounds.r * bounds.r * proj * proj;
955
684
  const right = safeDistSq * target * target;
956
685
  return left > right;
@@ -984,7 +713,7 @@ function refineLeaves(state, topology, params, outLeaves) {
984
713
  let elevationRange;
985
714
  if (params.tileElevationRange) {
986
715
  const range = state.scratchElevationRange;
987
- if (params.tileElevationRange(space, level, x, y, range)) {
716
+ if (params.tileElevationRange(tile, range)) {
988
717
  elevationRange = range;
989
718
  }
990
719
  }
@@ -1087,19 +816,9 @@ function balance2to1(state, topology, params, leaves) {
1087
816
  }
1088
817
 
1089
818
  function update(state, topology, params, outLeaves) {
1090
- const cam = params.cameraOrigin;
1091
- const elevation = params.elevationAtCameraXZ ?? 0;
1092
- const origX = cam.x;
1093
- const origY = cam.y;
1094
- const origZ = cam.z;
1095
- topology.projection.cpu.cameraSurfaceOffset(cam, elevation);
1096
819
  beginUpdate(state, topology, params);
1097
820
  const leaves = refineLeaves(state, topology, params, outLeaves);
1098
- const result = balance2to1(state, topology, params, leaves);
1099
- cam.x = origX;
1100
- cam.y = origY;
1101
- cam.z = origZ;
1102
- return result;
821
+ return balance2to1(state, topology, params, leaves);
1103
822
  }
1104
823
 
1105
824
  const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
@@ -1196,94 +915,370 @@ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1196
915
  return outSeams;
1197
916
  }
1198
917
 
1199
- function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1200
- return tsl.Fn(
1201
- ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
1202
- const iEdge = tsl.int(edgeVertexCount);
1203
- const verticesPerNode = iEdge.mul(iEdge);
1204
- const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1205
- const xLeft = tsl.int(ix).sub(tsl.int(1));
1206
- const xRight = tsl.int(ix).add(tsl.int(1));
1207
- const yUp = tsl.int(iy).sub(tsl.int(1));
1208
- const yDown = tsl.int(iy).add(tsl.int(1));
1209
- const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
1210
- const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
1211
- const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1212
- const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1213
- const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
1214
- const stepWorld = tileSize.div(innerSegments);
1215
- const inv2Step = tsl.float(0.5).div(stepWorld);
1216
- const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
1217
- const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
1218
- return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
1219
- }
1220
- );
918
+ function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
919
+ return tsl.Fn(
920
+ ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
921
+ const iEdge = tsl.int(edgeVertexCount);
922
+ const verticesPerNode = iEdge.mul(iEdge);
923
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
924
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
925
+ const xRight = tsl.int(ix).add(tsl.int(1));
926
+ const yUp = tsl.int(iy).sub(tsl.int(1));
927
+ const yDown = tsl.int(iy).add(tsl.int(1));
928
+ const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
929
+ const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
930
+ const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
931
+ const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
932
+ const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
933
+ const stepWorld = tileSize.div(innerSegments);
934
+ const inv2Step = tsl.float(0.5).div(stepWorld);
935
+ const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
936
+ const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
937
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
938
+ }
939
+ );
940
+ }
941
+ function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
942
+ return tsl.Fn(([nodeIndex, ix, iy, elevationScale]) => {
943
+ const iEdge = tsl.int(edgeVertexCount);
944
+ const verticesPerNode = iEdge.mul(iEdge);
945
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
946
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
947
+ const xRight = tsl.int(ix).add(tsl.int(1));
948
+ const yUp = tsl.int(iy).sub(tsl.int(1));
949
+ const yDown = tsl.int(iy).add(tsl.int(1));
950
+ const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
951
+ const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
952
+ const pLeft = positionAt(xLeft, tsl.int(iy), heightAt(xLeft, tsl.int(iy)));
953
+ const pRight = positionAt(xRight, tsl.int(iy), heightAt(xRight, tsl.int(iy)));
954
+ const pUp = positionAt(tsl.int(ix), yUp, heightAt(tsl.int(ix), yUp));
955
+ const pDown = positionAt(tsl.int(ix), yDown, heightAt(tsl.int(ix), yDown));
956
+ const tangentU = pRight.sub(pLeft);
957
+ const tangentV = pDown.sub(pUp);
958
+ const normal = tsl.cross(tangentU, tangentV).normalize();
959
+ const dir = dirAt(tsl.int(ix), tsl.int(iy));
960
+ return normal.mul(normal.dot(dir).sign());
961
+ });
962
+ }
963
+
964
+ const isSkirtVertex = tsl.Fn(([segments]) => {
965
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
966
+ const vIndex = tsl.int(tsl.vertexIndex);
967
+ const segmentEdges = tsl.int(segmentsNode.add(3));
968
+ const vx = vIndex.mod(segmentEdges);
969
+ const vy = vIndex.div(segmentEdges);
970
+ const last = segmentEdges.sub(tsl.int(1));
971
+ return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
972
+ });
973
+ const isSkirtUV = tsl.Fn(([segments]) => {
974
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
975
+ const ux = tsl.uv().x;
976
+ const uy = tsl.uv().y;
977
+ const segmentCount = segmentsNode.add(2);
978
+ const segmentStep = tsl.float(1).div(segmentCount);
979
+ const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
980
+ const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
981
+ return innerX.and(innerY).not();
982
+ });
983
+
984
+ const TILE_BOUNDS_FLOATS_PER_TILE = 4;
985
+ const TILE_BOUNDS_LOD_MIN_OFFSET = 0;
986
+ const TILE_BOUNDS_LOD_MAX_OFFSET = 1;
987
+ const TILE_BOUNDS_PACK_MIN_OFFSET = 2;
988
+ const TILE_BOUNDS_PACK_MAX_OFFSET = 3;
989
+ const TERRAIN_FIELD_PACK_EPSILON = 1e-4;
990
+ function resolveType(format) {
991
+ return format === "rgba16float" ? three.HalfFloatType : three.FloatType;
992
+ }
993
+ function resolveFilter(mode) {
994
+ return mode === "linear" ? three.LinearFilter : three.NearestFilter;
995
+ }
996
+ function configureStorageTexture(texture2, format, filter) {
997
+ texture2.format = three.RGBAFormat;
998
+ texture2.type = resolveType(format);
999
+ texture2.magFilter = resolveFilter(filter);
1000
+ texture2.minFilter = resolveFilter(filter);
1001
+ texture2.wrapS = three.ClampToEdgeWrapping;
1002
+ texture2.wrapT = three.ClampToEdgeWrapping;
1003
+ texture2.generateMipmaps = false;
1004
+ texture2.needsUpdate = true;
1005
+ }
1006
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
1007
+ let currentEdgeVertexCount = edgeVertexCount;
1008
+ let currentTileCount = tileCount;
1009
+ const tex = new webgpu.StorageArrayTexture(
1010
+ edgeVertexCount,
1011
+ edgeVertexCount,
1012
+ tileCount
1013
+ );
1014
+ tex.name = "terrainField";
1015
+ configureStorageTexture(tex, options.format, options.filter);
1016
+ return {
1017
+ backendType: "array-texture",
1018
+ get edgeVertexCount() {
1019
+ return currentEdgeVertexCount;
1020
+ },
1021
+ get tileCount() {
1022
+ return currentTileCount;
1023
+ },
1024
+ texture: tex,
1025
+ uv(ix, iy, _tileIndex) {
1026
+ return tsl.vec2(ix.toFloat(), iy.toFloat());
1027
+ },
1028
+ texel(ix, iy, tileIndex) {
1029
+ return tsl.ivec3(ix, iy, tileIndex);
1030
+ },
1031
+ sample(u, v, tileIndex) {
1032
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
1033
+ },
1034
+ resize(width, height, nextTileCount) {
1035
+ currentEdgeVertexCount = width;
1036
+ currentTileCount = nextTileCount;
1037
+ tex.setSize(width, height, nextTileCount);
1038
+ tex.needsUpdate = true;
1039
+ }
1040
+ };
1041
+ }
1042
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
1043
+ const tilesPerRowNode = tsl.int(tilesPerRow);
1044
+ const edge = tsl.int(edgeVertexCount);
1045
+ const tile = tsl.int(tileIndex);
1046
+ const col = tile.mod(tilesPerRowNode);
1047
+ const row = tile.div(tilesPerRowNode);
1048
+ const atlasX = col.mul(edge).add(tsl.int(ix));
1049
+ const atlasY = row.mul(edge).add(tsl.int(iy));
1050
+ return { atlasX, atlasY };
1051
+ }
1052
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
1053
+ let currentEdgeVertexCount = edgeVertexCount;
1054
+ let currentTileCount = tileCount;
1055
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
1056
+ const atlasSize = tilesPerRow * edgeVertexCount;
1057
+ const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
1058
+ tex.name = "terrainFieldAtlas";
1059
+ configureStorageTexture(tex, options.format, options.filter);
1060
+ return {
1061
+ backendType: "atlas",
1062
+ get edgeVertexCount() {
1063
+ return currentEdgeVertexCount;
1064
+ },
1065
+ get tileCount() {
1066
+ return currentTileCount;
1067
+ },
1068
+ texture: tex,
1069
+ uv(ix, iy, tileIndex) {
1070
+ const { atlasX, atlasY } = atlasCoord(
1071
+ tilesPerRow,
1072
+ currentEdgeVertexCount,
1073
+ ix,
1074
+ iy,
1075
+ tileIndex
1076
+ );
1077
+ const currentAtlasSize = tsl.float(tilesPerRow * currentEdgeVertexCount);
1078
+ return tsl.vec2(
1079
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
1080
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
1081
+ );
1082
+ },
1083
+ texel(ix, iy, tileIndex) {
1084
+ const { atlasX, atlasY } = atlasCoord(
1085
+ tilesPerRow,
1086
+ currentEdgeVertexCount,
1087
+ ix,
1088
+ iy,
1089
+ tileIndex
1090
+ );
1091
+ return tsl.ivec2(atlasX, atlasY);
1092
+ },
1093
+ sample(u, v, tileIndex) {
1094
+ const tile = tsl.int(tileIndex);
1095
+ const tilesPerRowNode = tsl.int(tilesPerRow);
1096
+ const col = tile.mod(tilesPerRowNode);
1097
+ const row = tile.div(tilesPerRowNode);
1098
+ const invTilesPerRow = tsl.float(1 / tilesPerRow);
1099
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
1100
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
1101
+ return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
1102
+ },
1103
+ resize(width, height, nextTileCount) {
1104
+ currentEdgeVertexCount = width;
1105
+ currentTileCount = nextTileCount;
1106
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
1107
+ const nextAtlasSize = tilesPerRow * width;
1108
+ const image = tex.image;
1109
+ image.width = nextAtlasSize;
1110
+ image.height = nextAtlasSize;
1111
+ tex.needsUpdate = true;
1112
+ }
1113
+ };
1114
+ }
1115
+ function texture3DBackend(edgeVertexCount, tileCount, options) {
1116
+ const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
1117
+ return { ...storage, backendType: "texture-3d" };
1118
+ }
1119
+ function tryGetDeviceLimits(renderer) {
1120
+ const backend = renderer;
1121
+ return backend.backend?.device?.limits ?? {};
1122
+ }
1123
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
1124
+ const filter = options.filter ?? "linear";
1125
+ const format = options.format ?? "rgba16float";
1126
+ const forcedBackend = options.backend;
1127
+ if (forcedBackend === "atlas") {
1128
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
1129
+ }
1130
+ if (forcedBackend === "texture-3d") {
1131
+ return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
1132
+ }
1133
+ if (forcedBackend === "array-texture") {
1134
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
1135
+ }
1136
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
1137
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
1138
+ if (tileCount > maxLayers) {
1139
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
1140
+ }
1141
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
1142
+ }
1143
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
1144
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
1145
+ return tsl.textureStore(
1146
+ storage.texture,
1147
+ tsl.uvec3(tsl.int(ix), tsl.int(iy), tsl.int(tileIndex)),
1148
+ value
1149
+ );
1150
+ }
1151
+ return tsl.textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
1152
+ }
1153
+ function loadTerrainField(storage, ix, iy, tileIndex) {
1154
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
1155
+ return tsl.textureLoad(storage.texture, tsl.ivec2(tsl.int(ix), tsl.int(iy)), tsl.int(0)).depth(
1156
+ tsl.int(tileIndex)
1157
+ );
1158
+ }
1159
+ return tsl.textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), tsl.int(0));
1160
+ }
1161
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
1162
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
1163
+ }
1164
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
1165
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
1166
+ return tsl.vec3(raw.g, raw.b, raw.a);
1167
+ }
1168
+ function sampleTerrainField(storage, u, v, tileIndex) {
1169
+ return storage.sample(u, v, tileIndex);
1170
+ }
1171
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
1172
+ return sampleTerrainField(storage, u, v, tileIndex).r;
1173
+ }
1174
+ function packTerrainFieldSample(height, normal) {
1175
+ return tsl.vec4(height, normal.x, normal.y, normal.z);
1176
+ }
1177
+ function loadTilePackBounds(boundsNode, tileIndex) {
1178
+ const base = tsl.int(tileIndex).mul(tsl.int(TILE_BOUNDS_FLOATS_PER_TILE));
1179
+ return {
1180
+ packMin: boundsNode.element(base.add(tsl.int(TILE_BOUNDS_PACK_MIN_OFFSET))),
1181
+ packMax: boundsNode.element(base.add(tsl.int(TILE_BOUNDS_PACK_MAX_OFFSET)))
1182
+ };
1183
+ }
1184
+ function packNormalizedTerrainFieldSample(height, normal, packMin, packMax) {
1185
+ const span = tsl.max(packMax.sub(packMin), tsl.float(TERRAIN_FIELD_PACK_EPSILON));
1186
+ const normalized = height.sub(packMin).div(span);
1187
+ return tsl.vec4(normalized, normal.x, normal.y, normal.z);
1188
+ }
1189
+ function denormalizeTerrainFieldElevation(normalized, packMin, packMax) {
1190
+ const span = tsl.max(packMax.sub(packMin), tsl.float(TERRAIN_FIELD_PACK_EPSILON));
1191
+ return packMin.add(normalized.mul(span));
1192
+ }
1193
+
1194
+ const HALF_PI = Math.PI * 0.5;
1195
+ const FIELD_INNER_TEXEL_OFFSET = 1.5;
1196
+ const FIELD_EDGE_EXTRA_TEXELS = 3;
1197
+ function sphereTileArcLength(radius, levelDivisor) {
1198
+ return radius * HALF_PI / levelDivisor;
1221
1199
  }
1222
- function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
1223
- return tsl.Fn(([nodeIndex, ix, iy, elevationScale]) => {
1224
- const iEdge = tsl.int(edgeVertexCount);
1225
- const verticesPerNode = iEdge.mul(iEdge);
1226
- const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1227
- const xLeft = tsl.int(ix).sub(tsl.int(1));
1228
- const xRight = tsl.int(ix).add(tsl.int(1));
1229
- const yUp = tsl.int(iy).sub(tsl.int(1));
1230
- const yDown = tsl.int(iy).add(tsl.int(1));
1231
- const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
1232
- const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
1233
- const pLeft = positionAt(xLeft, tsl.int(iy), heightAt(xLeft, tsl.int(iy)));
1234
- const pRight = positionAt(xRight, tsl.int(iy), heightAt(xRight, tsl.int(iy)));
1235
- const pUp = positionAt(tsl.int(ix), yUp, heightAt(tsl.int(ix), yUp));
1236
- const pDown = positionAt(tsl.int(ix), yDown, heightAt(tsl.int(ix), yDown));
1237
- const tangentU = pRight.sub(pLeft);
1238
- const tangentV = pDown.sub(pUp);
1239
- const normal = tsl.cross(tangentU, tangentV).normalize();
1240
- const dir = dirAt(tsl.int(ix), tsl.int(iy));
1241
- return normal.mul(normal.dot(dir).sign());
1200
+ function decodeLeafTile(leafStorage, nodeIndex) {
1201
+ const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
1202
+ return {
1203
+ level: leafStorage.node.element(nodeOffset).toInt(),
1204
+ x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
1205
+ y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
1206
+ face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
1207
+ };
1208
+ }
1209
+ function faceUVFromTileLocal(tile, localU, localV, baseU = tsl.float(1), baseV = tsl.float(1)) {
1210
+ const levelScale = tsl.pow(tsl.float(2), tile.level.toFloat());
1211
+ const nU = baseU.mul(levelScale);
1212
+ const nV = baseV.mul(levelScale);
1213
+ return tsl.vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
1214
+ }
1215
+ function createTileCompute(leafStorage, uniforms, projection) {
1216
+ const baseU = tsl.float(projection.baseResolution?.u ?? 1);
1217
+ const baseV = tsl.float(projection.baseResolution?.v ?? 1);
1218
+ const tileLevel = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
1219
+ const tileFace = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
1220
+ const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
1221
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
1222
+ return tsl.vec2(tile.x, tile.y);
1223
+ });
1224
+ const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
1225
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
1226
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
1227
+ const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
1228
+ const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
1229
+ return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
1242
1230
  });
1231
+ const shared = {
1232
+ tileLevel: (nodeIndex) => tileLevel(nodeIndex),
1233
+ tileFace: (nodeIndex) => tileFace(nodeIndex),
1234
+ tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
1235
+ tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
1236
+ };
1237
+ const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
1238
+ return {
1239
+ ...shared,
1240
+ tileSize: parts.tileSize,
1241
+ rootUVCompute: parts.rootUV,
1242
+ tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
1243
+ };
1244
+ }
1245
+ function tileLocalToFieldUV(localCoord, innerSegments) {
1246
+ const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
1247
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
1248
+ }
1249
+ function tileLocalToFieldUVNumber(localCoord, innerSegments) {
1250
+ const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
1251
+ return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
1243
1252
  }
1244
1253
 
1245
- const isSkirtVertex = tsl.Fn(([segments]) => {
1246
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1247
- const vIndex = tsl.int(tsl.vertexIndex);
1248
- const segmentEdges = tsl.int(segmentsNode.add(3));
1249
- const vx = vIndex.mod(segmentEdges);
1250
- const vy = vIndex.div(segmentEdges);
1251
- const last = segmentEdges.sub(tsl.int(1));
1252
- return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
1253
- });
1254
- const isSkirtUV = tsl.Fn(([segments]) => {
1255
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1256
- const ux = tsl.uv().x;
1257
- const uy = tsl.uv().y;
1258
- const segmentCount = segmentsNode.add(2);
1259
- const segmentStep = tsl.float(1).div(segmentCount);
1260
- const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
1261
- const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
1262
- return innerX.and(innerY).not();
1263
- });
1264
-
1265
- function createTileElevation(terrainUniforms, terrainFieldStorage) {
1266
- if (!terrainFieldStorage) return tsl.float(0);
1254
+ function createTileElevation(terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1255
+ if (!terrainFieldStorage || !tileBoundsNode) return tsl.float(0);
1267
1256
  const innerSegs = terrainUniforms.uInnerTileSegments;
1268
1257
  const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1269
1258
  const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1270
- return sampleTerrainFieldElevation(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex)).mul(
1259
+ const normalized = sampleTerrainFieldElevation(
1260
+ terrainFieldStorage,
1261
+ u,
1262
+ v,
1263
+ tsl.int(tsl.instanceIndex)
1264
+ );
1265
+ const { packMin, packMax } = loadTilePackBounds(tileBoundsNode, tsl.int(tsl.instanceIndex));
1266
+ return denormalizeTerrainFieldElevation(normalized, packMin, packMax).mul(
1271
1267
  terrainUniforms.uElevationScale
1272
1268
  );
1273
1269
  }
1274
1270
  function loadWorldNormal(terrainUniforms, terrainFieldStorage) {
1275
- const nodeIndex = tsl.int(tsl.instanceIndex);
1276
- const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
1277
- const localVertexIndex = tsl.int(tsl.vertexIndex);
1278
- const ix = localVertexIndex.mod(edgeVertexCount);
1279
- const iy = localVertexIndex.div(edgeVertexCount);
1280
- return loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1271
+ const innerSegs = terrainUniforms.uInnerTileSegments;
1272
+ const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1273
+ const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1274
+ const raw = sampleTerrainField(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex));
1275
+ return tsl.vec3(raw.g, raw.b, raw.a).normalize();
1281
1276
  }
1282
1277
  function assignWorldNormal(terrainUniforms, terrainFieldStorage) {
1283
1278
  if (!terrainFieldStorage) return;
1284
1279
  tsl.normalLocal.assign(tsl.Fn(() => loadWorldNormal(terrainUniforms, terrainFieldStorage))());
1285
1280
  }
1286
- function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1281
+ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1287
1282
  return tsl.Fn(() => {
1288
1283
  const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
1289
1284
  const rootSize = terrainUniforms.uRootSize.toVar();
@@ -1297,7 +1292,11 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1297
1292
  const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
1298
1293
  const worldX = centerX.add(clampedX.mul(size));
1299
1294
  const worldZ = centerZ.add(clampedZ.mul(size));
1300
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1295
+ const yElevation = createTileElevation(
1296
+ terrainUniforms,
1297
+ terrainFieldStorage,
1298
+ tileBoundsNode
1299
+ );
1301
1300
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1302
1301
  const baseY = rootOrigin.y.add(yElevation);
1303
1302
  const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
@@ -1306,7 +1305,7 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1306
1305
  return tsl.vec3(worldX, worldY, worldZ);
1307
1306
  })();
1308
1307
  }
1309
- function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, baseU = 1, baseV = 1) {
1308
+ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, tileBoundsNode, baseU = 1, baseV = 1) {
1310
1309
  const fBaseU = tsl.float(baseU);
1311
1310
  const fBaseV = tsl.float(baseV);
1312
1311
  return tsl.Fn(() => {
@@ -1315,7 +1314,11 @@ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainF
1315
1314
  const localU = tsl.positionLocal.x.max(half.negate()).min(half).add(half);
1316
1315
  const localV = tsl.positionLocal.z.max(half.negate()).min(half).add(half);
1317
1316
  const faceUV = faceUVFromTileLocal(tile, localU, localV, fBaseU, fBaseV);
1318
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1317
+ const yElevation = createTileElevation(
1318
+ terrainUniforms,
1319
+ terrainFieldStorage,
1320
+ tileBoundsNode
1321
+ );
1319
1322
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1320
1323
  const displacement = tsl.select(
1321
1324
  skirtVertex,
@@ -1500,26 +1503,6 @@ function cpuRaycast(query, ray, config, options) {
1500
1503
  distance: ray.origin.distanceTo(point)
1501
1504
  };
1502
1505
  }
1503
- function cpuRaycastBoundsOnly(ray, config, options) {
1504
- const bounds = getTerrainBounds(config);
1505
- const planeY = (config.minY + config.maxY) * 0.5;
1506
- const dirY = ray.direction.y;
1507
- if (Math.abs(dirY) < 1e-8) return null;
1508
- const t = (planeY - ray.origin.y) / dirY;
1509
- if (t < 0) return null;
1510
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1511
- if (t > maxDistance) return null;
1512
- const point = new three.Vector3();
1513
- ray.at(t, point);
1514
- if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
1515
- return null;
1516
- }
1517
- return {
1518
- position: point,
1519
- normal: new three.Vector3(0, 1, 0),
1520
- distance: ray.origin.distanceTo(point)
1521
- };
1522
- }
1523
1506
  function intersectRaySphere(ray, cx, cy, cz, radius) {
1524
1507
  const ox = ray.origin.x - cx;
1525
1508
  const oy = ray.origin.y - cy;
@@ -1585,22 +1568,6 @@ function cubeSphereRaycast(query, ray, params, options) {
1585
1568
  distance: ray.origin.distanceTo(sample.position)
1586
1569
  };
1587
1570
  }
1588
- function cubeSphereRaycastBoundsOnly(ray, params, options) {
1589
- const shell = intersectRaySphere(ray, params.centerX, params.centerY, params.centerZ, params.radius);
1590
- if (!shell) return null;
1591
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1592
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1593
- if (t < 0 || t > maxDistance) return null;
1594
- const point = new three.Vector3();
1595
- ray.at(t, point);
1596
- const normal = new three.Vector3(
1597
- point.x - params.centerX,
1598
- point.y - params.centerY,
1599
- point.z - params.centerZ
1600
- ).normalize();
1601
- if (params.invert) normal.negate();
1602
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1603
- }
1604
1571
  function torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams) {
1605
1572
  positionToTorusParams(
1606
1573
  px,
@@ -1655,28 +1622,6 @@ function torusRaycast(query, ray, params, options) {
1655
1622
  distance: ray.origin.distanceTo(sample.position)
1656
1623
  };
1657
1624
  }
1658
- function torusRaycastBoundsOnly(ray, params, options) {
1659
- const shell = intersectRaySphere(
1660
- ray,
1661
- params.centerX,
1662
- params.centerY,
1663
- params.centerZ,
1664
- params.outerRadius
1665
- );
1666
- if (!shell) return null;
1667
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1668
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1669
- if (t < 0 || t > maxDistance) return null;
1670
- const point = new three.Vector3();
1671
- ray.at(t, point);
1672
- const normal = new three.Vector3(
1673
- point.x - params.centerX,
1674
- point.y - params.centerY,
1675
- point.z - params.centerZ
1676
- ).normalize();
1677
- if (params.invert) normal.negate();
1678
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1679
- }
1680
1625
 
1681
1626
  function createTerrainQuery(cache) {
1682
1627
  return {
@@ -1790,7 +1735,12 @@ function createFlatProjection() {
1790
1735
  faceOutward: false,
1791
1736
  gpu: {
1792
1737
  renderVertexPosition(ctx) {
1793
- return createFlatRenderVertexPosition(ctx.leafStorage, ctx.uniforms, ctx.terrainFieldStorage);
1738
+ return createFlatRenderVertexPosition(
1739
+ ctx.leafStorage,
1740
+ ctx.uniforms,
1741
+ ctx.terrainFieldStorage,
1742
+ ctx.tileBoundsNode
1743
+ );
1794
1744
  },
1795
1745
  createTileComputeParts: createFlatTileComputeParts,
1796
1746
  createFieldNormal(ctx) {
@@ -1802,9 +1752,6 @@ function createFlatProjection() {
1802
1752
  }
1803
1753
  },
1804
1754
  cpu: {
1805
- cameraSurfaceOffset(cam, elevation) {
1806
- cam.y -= elevation;
1807
- },
1808
1755
  createSurfaceOps() {
1809
1756
  return null;
1810
1757
  },
@@ -1813,26 +1760,44 @@ function createFlatProjection() {
1813
1760
  },
1814
1761
  raycast(ctx) {
1815
1762
  const { ray, options, terrainQuery, config } = ctx;
1816
- if (terrainQuery) {
1817
- const precise = cpuRaycast(terrainQuery, ray, config, options);
1818
- if (precise) return precise;
1819
- }
1820
- const coarse = cpuRaycastBoundsOnly(ray, config, options);
1821
- if (coarse && terrainQuery) {
1822
- const sample = terrainQuery.sampleTerrain(coarse.position.x, coarse.position.z);
1823
- if (sample.valid) {
1824
- coarse.position.y = sample.elevation;
1825
- coarse.normal.copy(sample.normal);
1826
- }
1827
- }
1828
- return coarse;
1763
+ if (!terrainQuery) return null;
1764
+ return cpuRaycast(terrainQuery, ray, config, options);
1829
1765
  }
1830
1766
  }
1831
1767
  };
1832
1768
  }
1833
1769
 
1770
+ function boundingSphereFromPoints(px, py, pz, count, cameraOrigin, out) {
1771
+ let sumX = 0;
1772
+ let sumY = 0;
1773
+ let sumZ = 0;
1774
+ for (let i = 0; i < count; i++) {
1775
+ sumX += px[i];
1776
+ sumY += py[i];
1777
+ sumZ += pz[i];
1778
+ }
1779
+ const cX = sumX / count;
1780
+ const cY = sumY / count;
1781
+ const cZ = sumZ / count;
1782
+ let maxDistSq = 0;
1783
+ for (let i = 0; i < count; i++) {
1784
+ const dx = px[i] - cX;
1785
+ const dy = py[i] - cY;
1786
+ const dz = pz[i] - cZ;
1787
+ const dSq = dx * dx + dy * dy + dz * dz;
1788
+ if (dSq > maxDistSq) maxDistSq = dSq;
1789
+ }
1790
+ out.cx = cX - cameraOrigin.x;
1791
+ out.cy = cY - cameraOrigin.y;
1792
+ out.cz = cZ - cameraOrigin.z;
1793
+ out.r = Math.sqrt(maxDistSq);
1794
+ }
1795
+
1834
1796
  function createFlatTopology(cfg) {
1835
1797
  const halfRoot = 0.5 * cfg.rootSize;
1798
+ const px = new Float64Array(8);
1799
+ const py = new Float64Array(8);
1800
+ const pz = new Float64Array(8);
1836
1801
  const topology = {
1837
1802
  spaceCount: 1,
1838
1803
  maxRootCount: 1,
@@ -1872,15 +1837,26 @@ function createFlatTopology(cfg) {
1872
1837
  const size = cfg.rootSize * scale;
1873
1838
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1874
1839
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1875
- const centerX = minX + 0.5 * size;
1876
- const centerZ = minZ + 0.5 * size;
1877
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1878
- out.cx = centerX - cameraOrigin.x;
1879
- out.cy = centerY - cameraOrigin.y;
1880
- out.cz = centerZ - cameraOrigin.z;
1881
- const halfDiag = 0.7071067811865476 * size;
1882
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1883
- out.r = halfDiag + vertExtent;
1840
+ const maxX = minX + size;
1841
+ const maxZ = minZ + size;
1842
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1843
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1844
+ let pointCount = 0;
1845
+ for (let i = 0; i < 4; i++) {
1846
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1847
+ const cornerZ = i < 2 ? minZ : maxZ;
1848
+ px[pointCount] = cornerX;
1849
+ py[pointCount] = yLo;
1850
+ pz[pointCount] = cornerZ;
1851
+ pointCount += 1;
1852
+ if (elevationRange) {
1853
+ px[pointCount] = cornerX;
1854
+ py[pointCount] = yHi;
1855
+ pz[pointCount] = cornerZ;
1856
+ pointCount += 1;
1857
+ }
1858
+ }
1859
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1884
1860
  },
1885
1861
  rootTiles(_cameraOrigin, out) {
1886
1862
  const root = out[0];
@@ -1898,6 +1874,9 @@ function createInfiniteFlatTopology(cfg) {
1898
1874
  const halfRoot = 0.5 * cfg.rootSize;
1899
1875
  const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1900
1876
  const rootWidth = rootGridRadius * 2 + 1;
1877
+ const px = new Float64Array(8);
1878
+ const py = new Float64Array(8);
1879
+ const pz = new Float64Array(8);
1901
1880
  return {
1902
1881
  spaceCount: 1,
1903
1882
  maxRootCount: rootWidth * rootWidth,
@@ -1931,15 +1910,26 @@ function createInfiniteFlatTopology(cfg) {
1931
1910
  const size = cfg.rootSize * scale;
1932
1911
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1933
1912
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1934
- const centerX = minX + 0.5 * size;
1935
- const centerZ = minZ + 0.5 * size;
1936
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1937
- out.cx = centerX - cameraOrigin.x;
1938
- out.cy = centerY - cameraOrigin.y;
1939
- out.cz = centerZ - cameraOrigin.z;
1940
- const halfDiag = 0.7071067811865476 * size;
1941
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1942
- out.r = halfDiag + vertExtent;
1913
+ const maxX = minX + size;
1914
+ const maxZ = minZ + size;
1915
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1916
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1917
+ let pointCount = 0;
1918
+ for (let i = 0; i < 4; i++) {
1919
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1920
+ const cornerZ = i < 2 ? minZ : maxZ;
1921
+ px[pointCount] = cornerX;
1922
+ py[pointCount] = yLo;
1923
+ pz[pointCount] = cornerZ;
1924
+ pointCount += 1;
1925
+ if (elevationRange) {
1926
+ px[pointCount] = cornerX;
1927
+ py[pointCount] = yHi;
1928
+ pz[pointCount] = cornerZ;
1929
+ pointCount += 1;
1930
+ }
1931
+ }
1932
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1943
1933
  },
1944
1934
  rootTiles(cameraOrigin, out) {
1945
1935
  const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
@@ -2313,9 +2303,11 @@ function packedSampleFromTileResult(params, tileResult) {
2313
2303
  fieldV,
2314
2304
  safeTileIndex
2315
2305
  ).toVar();
2306
+ const { packMin, packMax } = loadTilePackBounds(params.tileBoundsNode, safeTileIndex);
2307
+ const elevation = denormalizeTerrainFieldElevation(sampled.r, packMin, packMax);
2316
2308
  const normal = tsl.vec3(sampled.g, sampled.b, sampled.a);
2317
2309
  const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2318
- return tsl.vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2310
+ return tsl.vec4(elevation, normal.x, normal.y, normal.z).mul(valid);
2319
2311
  }
2320
2312
  function createTerrainSampleNode(params) {
2321
2313
  const tileLookup = createTileIndexFromWorldPosition(
@@ -2475,7 +2467,6 @@ function createCubeSphereProjection(config) {
2475
2467
  const nx = dx / len;
2476
2468
  const ny = dy / len;
2477
2469
  const nz = dz / len;
2478
- const dirSign = invert ? -1 : 1;
2479
2470
  dirScratch[0] = nx;
2480
2471
  dirScratch[1] = ny;
2481
2472
  dirScratch[2] = nz;
@@ -2484,9 +2475,9 @@ function createCubeSphereProjection(config) {
2484
2475
  out.space = face;
2485
2476
  out.u = uvScratch[0];
2486
2477
  out.v = uvScratch[1];
2487
- out.dirX = nx * dirSign;
2488
- out.dirY = ny * dirSign;
2489
- out.dirZ = nz * dirSign;
2478
+ out.dirX = nx;
2479
+ out.dirY = ny;
2480
+ out.dirZ = nz;
2490
2481
  return true;
2491
2482
  },
2492
2483
  surfacePosition(key, elevation, outVec) {
@@ -2519,7 +2510,8 @@ function createCubeSphereProjection(config) {
2519
2510
  let nx = tuy * tvz - tuz * tvy;
2520
2511
  let ny = tuz * tvx - tux * tvz;
2521
2512
  let nz = tux * tvy - tuy * tvx;
2522
- if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
2513
+ const outwardSign = invert ? -1 : 1;
2514
+ if ((nx * key.dirX + ny * key.dirY + nz * key.dirZ) * outwardSign < 0) {
2523
2515
  nx = -nx;
2524
2516
  ny = -ny;
2525
2517
  nz = -nz;
@@ -2543,7 +2535,8 @@ function createCubeSphereProjection(config) {
2543
2535
  const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
2544
2536
  const r = invert ? ctx.uniforms.uRadius.toVar().sub(displacement) : ctx.uniforms.uRadius.toVar().add(displacement);
2545
2537
  return ctx.uniforms.uRootOrigin.toVar().add(dir.mul(r));
2546
- }
2538
+ },
2539
+ ctx.tileBoundsNode
2547
2540
  );
2548
2541
  },
2549
2542
  createTileComputeParts: createSphereTileComputeParts,
@@ -2573,19 +2566,6 @@ function createCubeSphereProjection(config) {
2573
2566
  augmentSampler: augmentCubeSphereSampler
2574
2567
  },
2575
2568
  cpu: {
2576
- cameraSurfaceOffset(cam, elevation) {
2577
- const dx = cam.x - center.x;
2578
- const dy = cam.y - center.y;
2579
- const dz = cam.z - center.z;
2580
- const len = Math.hypot(dx, dy, dz);
2581
- if (len > 1e-12) {
2582
- const sign = invert ? 1 : -1;
2583
- const inv = sign * elevation / len;
2584
- cam.x += dx * inv;
2585
- cam.y += dy * inv;
2586
- cam.z += dz * inv;
2587
- }
2588
- },
2589
2569
  createSurfaceOps() {
2590
2570
  return surfaceOps;
2591
2571
  },
@@ -2596,6 +2576,7 @@ function createCubeSphereProjection(config) {
2596
2576
  return { query, surfaceQuery, sphereQuery };
2597
2577
  },
2598
2578
  raycast(ctx) {
2579
+ if (!ctx.sphereQuery) return null;
2599
2580
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2600
2581
  const dispMax = range ? Math.max(0, range.max - center.y) : radius * 0.1;
2601
2582
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING$1;
@@ -2607,11 +2588,7 @@ function createCubeSphereProjection(config) {
2607
2588
  maxRadius: radius + outerPadding,
2608
2589
  invert
2609
2590
  };
2610
- if (ctx.sphereQuery) {
2611
- const precise = cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2612
- if (precise) return precise;
2613
- }
2614
- return cubeSphereRaycastBoundsOnly(ctx.ray, params, ctx.options);
2591
+ return cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2615
2592
  }
2616
2593
  }
2617
2594
  };
@@ -2702,8 +2679,6 @@ function createCubeSphereTopology(cfg) {
2702
2679
  spaceCount: 6,
2703
2680
  maxRootCount: 6,
2704
2681
  projection: createCubeSphereProjection({ radius, center, invert: cfg.invert }),
2705
- radius,
2706
- center,
2707
2682
  neighborSameLevel(tile, dir, out) {
2708
2683
  const level = tile.level;
2709
2684
  const n = 1 << level;
@@ -2740,48 +2715,29 @@ function createCubeSphereTopology(cfg) {
2740
2715
  const u1 = (tile.x + 1) / n;
2741
2716
  const v0 = tile.y / n;
2742
2717
  const v1 = (tile.y + 1) / n;
2743
- const cornersU = [u0, u1, u0, u1];
2744
- const cornersV = [v0, v0, v1, v1];
2745
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2718
+ const shellLo = radius + (elevationRange ? elevationRange.min : 0);
2719
+ const shellHi = elevationRange ? radius + elevationRange.max : 0;
2746
2720
  let pointCount = 0;
2747
- let sumX = 0;
2748
- let sumY = 0;
2749
- let sumZ = 0;
2750
2721
  for (let i = 0; i < 4; i++) {
2751
- faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
2722
+ const u = (i & 1) === 0 ? u0 : u1;
2723
+ const v = i < 2 ? v0 : v1;
2724
+ faceUVToCube(tile.space, u, v, cube);
2752
2725
  const len = Math.hypot(cube[0], cube[1], cube[2]);
2753
2726
  const dirX = cube[0] / len;
2754
2727
  const dirY = cube[1] / len;
2755
2728
  const dirZ = cube[2] / len;
2756
- for (let di = 0; di < disps.length; di++) {
2757
- const shellRadius = radius + disps[di];
2758
- const sx = center.x + dirX * shellRadius;
2759
- const sy = center.y + dirY * shellRadius;
2760
- const sz = center.z + dirZ * shellRadius;
2761
- px[pointCount] = sx;
2762
- py[pointCount] = sy;
2763
- pz[pointCount] = sz;
2764
- sumX += sx;
2765
- sumY += sy;
2766
- sumZ += sz;
2729
+ px[pointCount] = center.x + dirX * shellLo;
2730
+ py[pointCount] = center.y + dirY * shellLo;
2731
+ pz[pointCount] = center.z + dirZ * shellLo;
2732
+ pointCount += 1;
2733
+ if (elevationRange) {
2734
+ px[pointCount] = center.x + dirX * shellHi;
2735
+ py[pointCount] = center.y + dirY * shellHi;
2736
+ pz[pointCount] = center.z + dirZ * shellHi;
2767
2737
  pointCount += 1;
2768
2738
  }
2769
2739
  }
2770
- const cX = sumX / pointCount;
2771
- const cY = sumY / pointCount;
2772
- const cZ = sumZ / pointCount;
2773
- let maxDistSq = 0;
2774
- for (let i = 0; i < pointCount; i++) {
2775
- const dx = px[i] - cX;
2776
- const dy = py[i] - cY;
2777
- const dz = pz[i] - cZ;
2778
- const dSq = dx * dx + dy * dy + dz * dz;
2779
- if (dSq > maxDistSq) maxDistSq = dSq;
2780
- }
2781
- out.cx = cX - cameraOrigin.x;
2782
- out.cy = cY - cameraOrigin.y;
2783
- out.cz = cZ - cameraOrigin.z;
2784
- out.r = Math.sqrt(maxDistSq);
2740
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
2785
2741
  },
2786
2742
  rootTiles(_cameraOrigin, out) {
2787
2743
  for (let s = 0; s < 6; s++) {
@@ -2928,6 +2884,7 @@ function createTorusProjection(config) {
2928
2884
  ctx.uniforms,
2929
2885
  ctx.terrainFieldStorage,
2930
2886
  (_tile, faceUV, displacement) => torusPosition(geometry, faceUV.x, faceUV.y, displacement),
2887
+ ctx.tileBoundsNode,
2931
2888
  baseU,
2932
2889
  baseV
2933
2890
  );
@@ -2952,13 +2909,6 @@ function createTorusProjection(config) {
2952
2909
  }
2953
2910
  },
2954
2911
  cpu: {
2955
- cameraSurfaceOffset(cam, elevation) {
2956
- positionToTorusParams(cam.x, cam.y, cam.z, majorRadius, center, params);
2957
- torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
2958
- cam.x -= normalScratch[0] * elevation;
2959
- cam.y -= normalScratch[1] * elevation;
2960
- cam.z -= normalScratch[2] * elevation;
2961
- },
2962
2912
  createSurfaceOps() {
2963
2913
  return surfaceOps;
2964
2914
  },
@@ -2968,6 +2918,7 @@ function createTorusProjection(config) {
2968
2918
  return { query, surfaceQuery, sphereQuery: null };
2969
2919
  },
2970
2920
  raycast(ctx) {
2921
+ if (!ctx.surfaceQuery) return null;
2971
2922
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2972
2923
  const dispMax = range ? Math.max(0, range.max - ctx.config.originY) : minorRadius * 0.5;
2973
2924
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING;
@@ -2980,11 +2931,7 @@ function createTorusProjection(config) {
2980
2931
  outerRadius: majorRadius + minorRadius + outerPadding,
2981
2932
  invert
2982
2933
  };
2983
- if (ctx.surfaceQuery) {
2984
- const precise = torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2985
- if (precise) return precise;
2986
- }
2987
- return torusRaycastBoundsOnly(ctx.ray, raycastParams, ctx.options);
2934
+ return torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2988
2935
  }
2989
2936
  }
2990
2937
  };
@@ -3017,8 +2964,6 @@ function createTorusTopology(cfg) {
3017
2964
  baseU,
3018
2965
  baseV
3019
2966
  }),
3020
- radius: majorRadius + minorRadius,
3021
- center,
3022
2967
  neighborSameLevel(tile, dir, out) {
3023
2968
  const { nU, nV } = levelResolution(tile.level);
3024
2969
  let nx = tile.x;
@@ -3049,42 +2994,28 @@ function createTorusTopology(cfg) {
3049
2994
  const v0 = tile.y / nV;
3050
2995
  const stepU = 1 / nU;
3051
2996
  const stepV = 1 / nV;
3052
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2997
+ const dispLo = elevationRange ? elevationRange.min : 0;
2998
+ const dispHi = elevationRange ? elevationRange.max : 0;
3053
2999
  let pointCount = 0;
3054
- let sumX = 0;
3055
- let sumY = 0;
3056
- let sumZ = 0;
3057
3000
  for (let sj = 0; sj <= 2; sj++) {
3058
3001
  for (let si = 0; si <= 2; si++) {
3059
3002
  const u = u0 + si * stepU / 2;
3060
3003
  const v = v0 + sj * stepV / 2;
3061
- for (let di = 0; di < disps.length; di++) {
3062
- torusUVToPoint(u, v, majorRadius, minorRadius, disps[di], center, corner, invert);
3004
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispLo, center, corner, invert);
3005
+ px[pointCount] = corner[0];
3006
+ py[pointCount] = corner[1];
3007
+ pz[pointCount] = corner[2];
3008
+ pointCount += 1;
3009
+ if (elevationRange) {
3010
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispHi, center, corner, invert);
3063
3011
  px[pointCount] = corner[0];
3064
3012
  py[pointCount] = corner[1];
3065
3013
  pz[pointCount] = corner[2];
3066
- sumX += corner[0];
3067
- sumY += corner[1];
3068
- sumZ += corner[2];
3069
3014
  pointCount += 1;
3070
3015
  }
3071
3016
  }
3072
3017
  }
3073
- const cX = sumX / pointCount;
3074
- const cY = sumY / pointCount;
3075
- const cZ = sumZ / pointCount;
3076
- let maxDistSq = 0;
3077
- for (let i = 0; i < pointCount; i++) {
3078
- const dx = px[i] - cX;
3079
- const dy = py[i] - cY;
3080
- const dz = pz[i] - cZ;
3081
- const dSq = dx * dx + dy * dy + dz * dz;
3082
- if (dSq > maxDistSq) maxDistSq = dSq;
3083
- }
3084
- out.cx = cX - cameraOrigin.x;
3085
- out.cy = cY - cameraOrigin.y;
3086
- out.cz = cZ - cameraOrigin.z;
3087
- out.r = Math.sqrt(maxDistSq);
3018
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
3088
3019
  },
3089
3020
  rootTiles(_cameraOrigin, out) {
3090
3021
  let count = 0;
@@ -3180,8 +3111,8 @@ function buildTileElevationPyramid(pyramid, index, tileBounds, leafCount) {
3180
3111
  const level = index.keysLevel[slot];
3181
3112
  const x = index.keysX[slot];
3182
3113
  const y = index.keysY[slot];
3183
- const rawMin = tileBounds[leafIndex * 2];
3184
- const rawMax = tileBounds[leafIndex * 2 + 1];
3114
+ const rawMin = tileBounds[leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3115
+ const rawMax = tileBounds[leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3185
3116
  for (let ancestorLevel = level; ancestorLevel >= 0; ancestorLevel--) {
3186
3117
  const shift = level - ancestorLevel;
3187
3118
  mergeRange(
@@ -3335,8 +3266,8 @@ function createTerrainSnapshotState(maxNodes, maxLevel, totalElements) {
3335
3266
  backElevation: new Float32Array(totalElements),
3336
3267
  frontIndex: createSpatialIndex(maxNodes),
3337
3268
  backIndex: createSpatialIndex(maxNodes),
3338
- frontTileBounds: new Float32Array(maxNodes * 2),
3339
- backTileBounds: new Float32Array(maxNodes * 2),
3269
+ frontTileBounds: new Float32Array(maxNodes * TILE_BOUNDS_FLOATS_PER_TILE),
3270
+ backTileBounds: new Float32Array(maxNodes * TILE_BOUNDS_FLOATS_PER_TILE),
3340
3271
  frontLeafCount: 0,
3341
3272
  globalRange: null,
3342
3273
  hasSnapshot: false,
@@ -3377,7 +3308,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3377
3308
  let boundsValid = activeLeafCount === 0;
3378
3309
  if (boundsFilled) {
3379
3310
  for (let i = 0; i < activeLeafCount; i += 1) {
3380
- if ((state.backTileBounds[i * 2 + 1] ?? 0) !== 0) {
3311
+ if ((state.backTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET] ?? 0) !== 0) {
3381
3312
  boundsValid = true;
3382
3313
  break;
3383
3314
  }
@@ -3399,8 +3330,8 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3399
3330
  let gMin = Infinity;
3400
3331
  let gMax = -Infinity;
3401
3332
  for (let i = 0; i < activeLeafCount; i++) {
3402
- const rawMin = state.frontTileBounds[i * 2];
3403
- const rawMax = state.frontTileBounds[i * 2 + 1];
3333
+ const rawMin = state.frontTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3334
+ const rawMax = state.frontTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3404
3335
  const a = originY + rawMin * elevationScale;
3405
3336
  const b = originY + rawMax * elevationScale;
3406
3337
  gMin = Math.min(gMin, a, b);
@@ -3436,7 +3367,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3436
3367
  boundsAttribute,
3437
3368
  state.boundsReadback,
3438
3369
  state.backTileBounds,
3439
- activeLeafCount * 2,
3370
+ activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE,
3440
3371
  "terrainBoundsReadback"
3441
3372
  );
3442
3373
  }
@@ -3455,7 +3386,9 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3455
3386
  if (boundsResult) {
3456
3387
  const rawBounds = new Float32Array(boundsResult);
3457
3388
  state.backTileBounds.fill(0);
3458
- state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
3389
+ state.backTileBounds.set(
3390
+ rawBounds.subarray(0, activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE)
3391
+ );
3459
3392
  boundsFilled = true;
3460
3393
  }
3461
3394
  applySnapshot(boundsFilled);
@@ -3569,8 +3502,8 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3569
3502
  };
3570
3503
  const tileBoundsFromLookup = (lookup, elevationBase) => {
3571
3504
  if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
3572
- const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
3573
- const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
3505
+ const rawMin = state.frontTileBounds[lookup.leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3506
+ const rawMax = state.frontTileBounds[lookup.leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3574
3507
  const a = elevationBase + rawMin * config.elevationScale;
3575
3508
  const b = elevationBase + rawMax * config.elevationScale;
3576
3509
  return {
@@ -3648,6 +3581,9 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3648
3581
  get hasSurface() {
3649
3582
  return surfaceOps !== null;
3650
3583
  },
3584
+ setSurfaceOps(nextSurfaceOps) {
3585
+ surfaceOps = nextSurfaceOps;
3586
+ },
3651
3587
  updateConfig(nextConfig) {
3652
3588
  config = nextConfig;
3653
3589
  shape.edgeVertexCount = config.innerTileSegments + 3;
@@ -3795,72 +3731,132 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3795
3731
  return lookupTileElevationRange(state.elevationPyramid, space, level, x, y, out);
3796
3732
  }
3797
3733
  };
3798
- return api;
3734
+ return api;
3735
+ }
3736
+
3737
+ const createElevation = (tile, uniforms, elevationFn) => {
3738
+ return function perVertexElevation(nodeIndex, localCoordinates) {
3739
+ const ix = tsl.int(localCoordinates.x);
3740
+ const iy = tsl.int(localCoordinates.y);
3741
+ const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
3742
+ const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
3743
+ const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
3744
+ const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
3745
+ const rootSize = uniforms.uRootSize.toVar();
3746
+ return elevationFn({
3747
+ worldPosition,
3748
+ rootSize,
3749
+ rootUV,
3750
+ tileOriginVec2: tile.tileOriginVec2(nodeIndex),
3751
+ tileSize: tile.tileSize(nodeIndex),
3752
+ tileLevel: tile.tileLevel(nodeIndex),
3753
+ nodeIndex: tsl.int(nodeIndex),
3754
+ tileUV
3755
+ });
3756
+ };
3757
+ };
3758
+
3759
+ function createTerrainUniforms(params) {
3760
+ const sanitizedId = params.instanceId?.replace(/-/g, "_");
3761
+ const suffix = sanitizedId ? `_${sanitizedId}` : "";
3762
+ const uRootOrigin = tsl.uniform(
3763
+ new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
3764
+ ).setName(`uRootOrigin${suffix}`);
3765
+ const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
3766
+ const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
3767
+ `uInnerTileSegments${suffix}`
3768
+ );
3769
+ const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
3770
+ const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
3771
+ const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
3772
+ return {
3773
+ uRootOrigin,
3774
+ uRootSize,
3775
+ uInnerTileSegments,
3776
+ uSkirtScale,
3777
+ uElevationScale,
3778
+ uRadius
3779
+ };
3799
3780
  }
3800
3781
 
3801
- const WGSIZE = 64;
3802
- function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
3803
- const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
3804
- return tsl.Fn(() => {
3805
- const sharedMin = tsl.workgroupArray("float", WGSIZE);
3806
- const sharedMax = tsl.workgroupArray("float", WGSIZE);
3807
- const tid = tsl.int(tsl.localId.x);
3808
- const tileIdx = tsl.int(tsl.workgroupId.z);
3809
- const baseOffset = tileIdx.mul(tsl.int(verticesPerNode));
3810
- const start = tid.mul(tsl.int(elemsPerThread));
3811
- const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
3812
- const localMin = tsl.float(1e10).toVar("localMin");
3813
- const localMax = tsl.float(-1e10).toVar("localMax");
3814
- tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
3815
- const h = elevationFieldNode.element(baseOffset.add(i));
3816
- localMin.assign(tsl.min(localMin, h));
3817
- localMax.assign(tsl.max(localMax, h));
3818
- });
3819
- sharedMin.element(tid).assign(localMin);
3820
- sharedMax.element(tid).assign(localMax);
3821
- tsl.workgroupBarrier();
3822
- tsl.If(tid.equal(tsl.int(0)), () => {
3823
- const finalMin = tsl.float(1e10).toVar("finalMin");
3824
- const finalMax = tsl.float(-1e10).toVar("finalMax");
3825
- tsl.Loop(WGSIZE, ({ i }) => {
3826
- finalMin.assign(tsl.min(finalMin, sharedMin.element(i)));
3827
- finalMax.assign(tsl.max(finalMax, sharedMax.element(i)));
3828
- });
3829
- const outIdx = tileIdx.mul(tsl.int(2));
3830
- boundsNode.element(outIdx).assign(finalMin);
3831
- boundsNode.element(outIdx.add(tsl.int(1))).assign(finalMax);
3832
- });
3833
- })().computeKernel([WGSIZE, 1, 1]);
3834
- }
3835
- const tileBoundsContextTask = work.task((get, work) => {
3836
- const elevationFieldContext = get(createElevationFieldContextTask);
3837
- const maxNodesValue = get(maxNodes);
3782
+ const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
3783
+
3784
+ const scratchVector3 = new three.Vector3();
3785
+ const createUniformsTask = work.task((get, work) => {
3786
+ const uniformParams = {
3787
+ rootOrigin: get(origin),
3788
+ rootSize: get(rootSize),
3789
+ innerTileSegments: get(innerTileSegments),
3790
+ skirtScale: get(skirtScale),
3791
+ elevationScale: get(elevationScale),
3792
+ radius: get(radius),
3793
+ instanceId: get(instanceIdTask)
3794
+ };
3795
+ return work(() => createTerrainUniforms(uniformParams));
3796
+ }).displayName("createUniformsTask").cache("once");
3797
+ const updateUniformsTask = work.task((get, work) => {
3798
+ const terrainUniformsContext = get(createUniformsTask);
3799
+ const rootSizeVal = get(rootSize);
3800
+ const rootOrigin = get(origin);
3801
+ const innerTileSegmentsVal = get(innerTileSegments);
3802
+ const skirtScaleVal = get(skirtScale);
3803
+ const elevationScaleVal = get(elevationScale);
3804
+ const radiusVal = get(radius);
3805
+ return work(() => {
3806
+ terrainUniformsContext.uRootSize.value = rootSizeVal;
3807
+ terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
3808
+ rootOrigin.x,
3809
+ rootOrigin.y,
3810
+ rootOrigin.z
3811
+ );
3812
+ terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
3813
+ terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
3814
+ terrainUniformsContext.uElevationScale.value = elevationScaleVal;
3815
+ terrainUniformsContext.uRadius.value = radiusVal;
3816
+ return terrainUniformsContext;
3817
+ });
3818
+ }).displayName("updateUniformsTask");
3819
+
3820
+ const createElevationFieldContextTask = work.task((get, work) => {
3838
3821
  const edgeVertexCount = get(innerTileSegments) + 3;
3822
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
3823
+ const totalElements = get(maxNodes) * verticesPerNode;
3839
3824
  return work(() => {
3840
- const data = new Float32Array(maxNodesValue * 2);
3825
+ const data = new Float32Array(totalElements);
3841
3826
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
3842
- attribute.name = "tileBounds";
3843
- const node = tsl.storage(attribute, "float", maxNodesValue * 2).setName(
3844
- "tileBounds"
3845
- );
3846
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
3847
- const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
3848
- return { data, attribute, node, kernel };
3827
+ attribute.name = "elevationField";
3828
+ const node = tsl.storage(attribute, "float", totalElements).setName("elevationField");
3829
+ return {
3830
+ data,
3831
+ attribute,
3832
+ node
3833
+ };
3849
3834
  });
3850
- }).displayName("tileBoundsContextTask");
3851
- const tileBoundsReductionTask = work.task(
3852
- (get, work, { resources }) => {
3853
- get(executeComputeTask);
3854
- const boundsContext = get(tileBoundsContextTask);
3855
- const leafState = get(leafGpuBufferTask);
3856
- return work(() => {
3857
- if (resources?.renderer && leafState.count > 0) {
3858
- resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
3835
+ }).displayName("createElevationFieldContextTask");
3836
+ const tileNodesTask = work.task((get, work) => {
3837
+ const leafStorage = get(leafStorageTask);
3838
+ const uniforms = get(updateUniformsTask);
3839
+ const topology = get(topologyTask);
3840
+ return work(() => {
3841
+ return createTileCompute(leafStorage, uniforms, topology.projection);
3842
+ });
3843
+ }).displayName("tileNodesTask");
3844
+ const elevationFieldStageTask = work.task((get, work) => {
3845
+ const tile = get(tileNodesTask);
3846
+ const uniforms = get(updateUniformsTask);
3847
+ const elevationFieldContext = get(createElevationFieldContextTask);
3848
+ const userElevationFn = get(elevationFn);
3849
+ return work(() => {
3850
+ const heightFn = createElevationFunction(userElevationFn);
3851
+ const heightWriteFn = createElevation(tile, uniforms, heightFn);
3852
+ return [
3853
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
3854
+ const height = heightWriteFn(nodeIndex, localCoordinates);
3855
+ elevationFieldContext.node.element(globalVertexIndex).assign(height);
3859
3856
  }
3860
- return boundsContext;
3861
- });
3862
- }
3863
- ).displayName("tileBoundsReductionTask").lane("gpu");
3857
+ ];
3858
+ });
3859
+ }).displayName("elevationFieldStageTask");
3864
3860
 
3865
3861
  const terrainQueryTask = work.task((get, work) => {
3866
3862
  const maxNodesValue = get(maxNodes);
@@ -3874,6 +3870,7 @@ const terrainQueryTask = work.task((get, work) => {
3874
3870
  const projection = topologyValue.projection;
3875
3871
  return work((prev) => {
3876
3872
  const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projection.kind}`;
3873
+ const resolvedRadius = projection.radius ?? radiusValue;
3877
3874
  const configValues = {
3878
3875
  rootSize: rootSizeValue,
3879
3876
  originX: originValue.x,
@@ -3882,7 +3879,7 @@ const terrainQueryTask = work.task((get, work) => {
3882
3879
  innerTileSegments: innerTileSegmentsValue,
3883
3880
  elevationScale: elevationScaleValue,
3884
3881
  maxLevel: maxLevelValue,
3885
- radius: topologyValue.radius ?? radiusValue,
3882
+ radius: resolvedRadius,
3886
3883
  baseU: projection.baseResolution?.u ?? 1,
3887
3884
  baseV: projection.baseResolution?.v ?? 1
3888
3885
  };
@@ -3897,9 +3894,15 @@ const terrainQueryTask = work.task((get, work) => {
3897
3894
  query = runtime.query;
3898
3895
  surfaceQuery = runtime.surfaceQuery;
3899
3896
  sphereQuery = runtime.sphereQuery;
3897
+ } else if (prev?.projection !== projection) {
3898
+ cache.setSurfaceOps(projection.cpu.createSurfaceOps());
3899
+ const runtime = projection.cpu.createRuntimeQueries(cache);
3900
+ query = runtime.query;
3901
+ surfaceQuery = runtime.surfaceQuery;
3902
+ sphereQuery = runtime.sphereQuery;
3900
3903
  }
3901
3904
  cache.updateConfig(configValues);
3902
- return { cache, query, surfaceQuery, sphereQuery, shapeKey };
3905
+ return { cache, query, surfaceQuery, sphereQuery, shapeKey, projection };
3903
3906
  });
3904
3907
  }).displayName("terrainQueryTask");
3905
3908
  const terrainReadbackTask = work.task(
@@ -3946,27 +3949,19 @@ const quadtreeConfigTask = work.task((get, work) => {
3946
3949
  const quadtreeUpdateTask = work.task((get, work) => {
3947
3950
  const quadtreeConfig = get(quadtreeConfigTask);
3948
3951
  const quadtreeUpdateConfig = get(quadtreeUpdate);
3949
- const { query: terrainQuery, surfaceQuery, cache } = get(terrainQueryTask);
3952
+ const { cache } = get(terrainQueryTask);
3950
3953
  const elevationScaleValue = get(elevationScale);
3951
3954
  let outLeaves = void 0;
3952
- const cameraPosition = new three.Vector3();
3953
3955
  const elevationRangeScratch = { min: 0, max: 0 };
3954
- return work(() => {
3955
- const cam = quadtreeUpdateConfig.cameraOrigin;
3956
- if (surfaceQuery) {
3957
- cameraPosition.set(cam.x, cam.y, cam.z);
3958
- quadtreeUpdateConfig.elevationAtCameraXZ = surfaceQuery.getElevationByPosition(cameraPosition) ?? 0;
3959
- } else {
3960
- quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
3956
+ quadtreeUpdateConfig.tileElevationRange = (tile, out) => {
3957
+ if (!cache.getTileElevationRange(tile.space, tile.level, tile.x, tile.y, elevationRangeScratch)) {
3958
+ return false;
3961
3959
  }
3962
- quadtreeUpdateConfig.tileElevationRange = (space, level, x, y, out) => {
3963
- if (!cache.getTileElevationRange(space, level, x, y, elevationRangeScratch)) {
3964
- return false;
3965
- }
3966
- out.min = elevationRangeScratch.min * elevationScaleValue;
3967
- out.max = elevationRangeScratch.max * elevationScaleValue;
3968
- return true;
3969
- };
3960
+ out.min = elevationRangeScratch.min * elevationScaleValue;
3961
+ out.max = elevationRangeScratch.max * elevationScaleValue;
3962
+ return true;
3963
+ };
3964
+ return work(() => {
3970
3965
  outLeaves = update(
3971
3966
  quadtreeConfig.state,
3972
3967
  quadtreeConfig.topology,
@@ -4004,107 +3999,88 @@ const leafGpuBufferTask = work.task((get, work) => {
4004
3999
  });
4005
4000
  }).displayName("leafGpuBufferTask");
4006
4001
 
4007
- function createTerrainUniforms(params) {
4008
- const sanitizedId = params.instanceId?.replace(/-/g, "_");
4009
- const suffix = sanitizedId ? `_${sanitizedId}` : "";
4010
- const uRootOrigin = tsl.uniform(
4011
- new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
4012
- ).setName(`uRootOrigin${suffix}`);
4013
- const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
4014
- const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
4015
- `uInnerTileSegments${suffix}`
4016
- );
4017
- const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
4018
- const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
4019
- const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
4020
- return {
4021
- uRootOrigin,
4022
- uRootSize,
4023
- uInnerTileSegments,
4024
- uSkirtScale,
4025
- uElevationScale,
4026
- uRadius
4027
- };
4002
+ const WGSIZE = 64;
4003
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
4004
+ const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
4005
+ return tsl.Fn(() => {
4006
+ const sharedLodMin = tsl.workgroupArray("float", WGSIZE);
4007
+ const sharedLodMax = tsl.workgroupArray("float", WGSIZE);
4008
+ const sharedPackMin = tsl.workgroupArray("float", WGSIZE);
4009
+ const sharedPackMax = tsl.workgroupArray("float", WGSIZE);
4010
+ const tid = tsl.int(tsl.localId.x);
4011
+ const tileIdx = tsl.int(tsl.workgroupId.z);
4012
+ const baseOffset = tileIdx.mul(tsl.int(verticesPerNode));
4013
+ const start = tid.mul(tsl.int(elemsPerThread));
4014
+ const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
4015
+ const localLodMin = tsl.float(1e10).toVar("localLodMin");
4016
+ const localLodMax = tsl.float(-1e10).toVar("localLodMax");
4017
+ const localPackMin = tsl.float(1e10).toVar("localPackMin");
4018
+ const localPackMax = tsl.float(-1e10).toVar("localPackMax");
4019
+ const edge = tsl.int(edgeVertexCount);
4020
+ const lastEdge = tsl.int(edgeVertexCount - 1);
4021
+ tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
4022
+ const ix = tsl.int(i).mod(edge);
4023
+ const iy = tsl.int(i).div(edge);
4024
+ const isSkirt = ix.equal(tsl.int(0)).or(ix.equal(lastEdge)).or(iy.equal(tsl.int(0))).or(iy.equal(lastEdge));
4025
+ const h = elevationFieldNode.element(baseOffset.add(i));
4026
+ localPackMin.assign(tsl.min(localPackMin, h));
4027
+ localPackMax.assign(tsl.max(localPackMax, h));
4028
+ tsl.If(isSkirt.not(), () => {
4029
+ localLodMin.assign(tsl.min(localLodMin, h));
4030
+ localLodMax.assign(tsl.max(localLodMax, h));
4031
+ });
4032
+ });
4033
+ sharedLodMin.element(tid).assign(localLodMin);
4034
+ sharedLodMax.element(tid).assign(localLodMax);
4035
+ sharedPackMin.element(tid).assign(localPackMin);
4036
+ sharedPackMax.element(tid).assign(localPackMax);
4037
+ tsl.workgroupBarrier();
4038
+ tsl.If(tid.equal(tsl.int(0)), () => {
4039
+ const finalLodMin = tsl.float(1e10).toVar("finalLodMin");
4040
+ const finalLodMax = tsl.float(-1e10).toVar("finalLodMax");
4041
+ const finalPackMin = tsl.float(1e10).toVar("finalPackMin");
4042
+ const finalPackMax = tsl.float(-1e10).toVar("finalPackMax");
4043
+ tsl.Loop(WGSIZE, ({ i }) => {
4044
+ finalLodMin.assign(tsl.min(finalLodMin, sharedLodMin.element(i)));
4045
+ finalLodMax.assign(tsl.max(finalLodMax, sharedLodMax.element(i)));
4046
+ finalPackMin.assign(tsl.min(finalPackMin, sharedPackMin.element(i)));
4047
+ finalPackMax.assign(tsl.max(finalPackMax, sharedPackMax.element(i)));
4048
+ });
4049
+ const outIdx = tileIdx.mul(tsl.int(TILE_BOUNDS_FLOATS_PER_TILE));
4050
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_LOD_MIN_OFFSET))).assign(finalLodMin);
4051
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_LOD_MAX_OFFSET))).assign(finalLodMax);
4052
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_PACK_MIN_OFFSET))).assign(finalPackMin);
4053
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_PACK_MAX_OFFSET))).assign(finalPackMax);
4054
+ });
4055
+ })().computeKernel([WGSIZE, 1, 1]);
4028
4056
  }
4029
-
4030
- const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
4031
-
4032
- const scratchVector3 = new three.Vector3();
4033
- const createUniformsTask = work.task((get, work) => {
4034
- const uniformParams = {
4035
- rootOrigin: get(origin),
4036
- rootSize: get(rootSize),
4037
- innerTileSegments: get(innerTileSegments),
4038
- skirtScale: get(skirtScale),
4039
- elevationScale: get(elevationScale),
4040
- radius: get(radius),
4041
- instanceId: get(instanceIdTask)
4042
- };
4043
- return work(() => createTerrainUniforms(uniformParams));
4044
- }).displayName("createUniformsTask").cache("once");
4045
- const updateUniformsTask = work.task((get, work) => {
4046
- const terrainUniformsContext = get(createUniformsTask);
4047
- const rootSizeVal = get(rootSize);
4048
- const rootOrigin = get(origin);
4049
- const innerTileSegmentsVal = get(innerTileSegments);
4050
- const skirtScaleVal = get(skirtScale);
4051
- const elevationScaleVal = get(elevationScale);
4052
- const radiusVal = get(radius);
4053
- return work(() => {
4054
- terrainUniformsContext.uRootSize.value = rootSizeVal;
4055
- terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
4056
- rootOrigin.x,
4057
- rootOrigin.y,
4058
- rootOrigin.z
4059
- );
4060
- terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
4061
- terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
4062
- terrainUniformsContext.uElevationScale.value = elevationScaleVal;
4063
- terrainUniformsContext.uRadius.value = radiusVal;
4064
- return terrainUniformsContext;
4065
- });
4066
- }).displayName("updateUniformsTask");
4067
-
4068
- const createElevationFieldContextTask = work.task((get, work) => {
4057
+ function runTileBoundsReduction(renderer, boundsContext, leafCount) {
4058
+ if (leafCount > 0) {
4059
+ renderer.compute(boundsContext.kernel, [1, 1, leafCount]);
4060
+ }
4061
+ }
4062
+ const tileBoundsContextTask = work.task((get, work) => {
4063
+ const elevationFieldContext = get(createElevationFieldContextTask);
4064
+ const maxNodesValue = get(maxNodes);
4069
4065
  const edgeVertexCount = get(innerTileSegments) + 3;
4070
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
4071
- const totalElements = get(maxNodes) * verticesPerNode;
4072
4066
  return work(() => {
4073
- const data = new Float32Array(totalElements);
4067
+ const floatCount = maxNodesValue * TILE_BOUNDS_FLOATS_PER_TILE;
4068
+ const data = new Float32Array(floatCount);
4074
4069
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
4075
- attribute.name = "elevationField";
4076
- const node = tsl.storage(attribute, "float", totalElements).setName("elevationField");
4077
- return {
4078
- data,
4079
- attribute,
4080
- node
4081
- };
4082
- });
4083
- }).displayName("createElevationFieldContextTask");
4084
- const tileNodesTask = work.task((get, work) => {
4085
- const leafStorage = get(leafStorageTask);
4086
- const uniforms = get(updateUniformsTask);
4087
- const topology = get(topologyTask);
4088
- return work(() => {
4089
- return createTileCompute(leafStorage, uniforms, topology.projection);
4090
- });
4091
- }).displayName("tileNodesTask");
4092
- const elevationFieldStageTask = work.task((get, work) => {
4093
- const tile = get(tileNodesTask);
4094
- const uniforms = get(updateUniformsTask);
4095
- const elevationFieldContext = get(createElevationFieldContextTask);
4096
- const userElevationFn = get(elevationFn);
4097
- return work(() => {
4098
- const heightFn = createElevationFunction(userElevationFn);
4099
- const heightWriteFn = createElevation(tile, uniforms, heightFn);
4100
- return [
4101
- (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
4102
- const height = heightWriteFn(nodeIndex, localCoordinates);
4103
- elevationFieldContext.node.element(globalVertexIndex).assign(height);
4104
- }
4105
- ];
4070
+ attribute.name = "tileBounds";
4071
+ const node = tsl.storage(attribute, "float", floatCount).setName(
4072
+ "tileBounds"
4073
+ );
4074
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
4075
+ const kernel = buildReductionKernel(
4076
+ elevationFieldContext.node,
4077
+ node,
4078
+ verticesPerNode,
4079
+ edgeVertexCount
4080
+ );
4081
+ return { data, attribute, node, kernel };
4106
4082
  });
4107
- }).displayName("elevationFieldStageTask");
4083
+ }).displayName("tileBoundsContextTask");
4108
4084
 
4109
4085
  const createTerrainFieldTextureTask = work.task(
4110
4086
  (get, work, { resources }) => {
@@ -4129,6 +4105,7 @@ const terrainFieldStageTask = work.task((get, work) => {
4129
4105
  const tile = get(tileNodesTask);
4130
4106
  const uniforms = get(updateUniformsTask);
4131
4107
  const topology = get(topologyTask);
4108
+ const boundsContext = get(tileBoundsContextTask);
4132
4109
  return work(() => {
4133
4110
  const computeNormal = topology.projection.gpu.createFieldNormal({
4134
4111
  elevationFieldNode: elevationFieldContext.node,
@@ -4143,12 +4120,13 @@ const terrainFieldStageTask = work.task((get, work) => {
4143
4120
  const iy = tsl.int(localCoordinates.y);
4144
4121
  const height = elevationFieldContext.node.element(globalVertexIndex);
4145
4122
  const normal = computeNormal(nodeIndex, ix, iy);
4123
+ const { packMin, packMax } = loadTilePackBounds(boundsContext.node, nodeIndex);
4146
4124
  storeTerrainField(
4147
4125
  terrainFieldStorage,
4148
4126
  ix,
4149
4127
  iy,
4150
4128
  nodeIndex,
4151
- packTerrainFieldSample(height, normal)
4129
+ packNormalizedTerrainFieldSample(height, normal, packMin, packMax)
4152
4130
  );
4153
4131
  }
4154
4132
  ];
@@ -4160,23 +4138,28 @@ function createComputePipelineTasks(leafStageTask) {
4160
4138
  const compile = work.task((get, work) => {
4161
4139
  const pipeline = get(leafStageTask);
4162
4140
  const edgeVertexCount = get(innerTileSegments) + 3;
4141
+ const boundsContext = get(tileBoundsContextTask);
4163
4142
  return work(
4164
4143
  () => compileComputePipeline(pipeline, edgeVertexCount, {
4165
- })
4144
+ midPipelineExecute: (renderer, instanceCount) => {
4145
+ runTileBoundsReduction(renderer, boundsContext, instanceCount);
4146
+ }
4147
+ })
4166
4148
  );
4167
4149
  }).displayName("compileComputeTask");
4168
- const execute = work.task(
4169
- (get, work, { resources }) => {
4170
- const { execute: run } = get(compile);
4171
- const leafState = get(leafGpuBufferTask);
4172
- return work(
4173
- () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4174
- }
4175
- );
4176
- }
4177
- ).displayName("executeComputeTask").lane("gpu");
4150
+ const execute = work.task((get, work, { resources }) => {
4151
+ const { execute: run } = get(compile);
4152
+ const leafState = get(leafGpuBufferTask);
4153
+ return work(() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4154
+ });
4155
+ }).displayName("executeComputeTask").lane("gpu");
4178
4156
  return { compile, execute };
4179
4157
  }
4158
+ const tileBoundsReductionTask = work.task((get, work) => {
4159
+ get(executeComputeTask);
4160
+ const boundsContext = get(tileBoundsContextTask);
4161
+ return work(() => boundsContext);
4162
+ }).displayName("tileBoundsReductionTask").lane("gpu");
4180
4163
 
4181
4164
  const gpuSpatialIndexStorageTask = work.task((get, work) => {
4182
4165
  const maxNodesValue = get(maxNodes);
@@ -4194,6 +4177,7 @@ const gpuSpatialIndexUploadTask = work.task((get, work) => {
4194
4177
 
4195
4178
  const createTerrainSamplerTask = work.task((get, work) => {
4196
4179
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4180
+ const tileBoundsContext = get(tileBoundsContextTask);
4197
4181
  const spatialIndex = get(gpuSpatialIndexStorageTask);
4198
4182
  const uniforms = get(updateUniformsTask);
4199
4183
  const elevationCallback = get(elevationFn);
@@ -4202,6 +4186,7 @@ const createTerrainSamplerTask = work.task((get, work) => {
4202
4186
  return work(
4203
4187
  () => createTerrainSampler({
4204
4188
  terrainFieldStorage,
4189
+ tileBoundsNode: tileBoundsContext.node,
4205
4190
  spatialIndex,
4206
4191
  uniforms,
4207
4192
  elevationCallback,
@@ -4215,12 +4200,14 @@ const positionNodeTask = work.task((get, work) => {
4215
4200
  const leafStorage = get(leafStorageTask);
4216
4201
  const terrainUniforms = get(updateUniformsTask);
4217
4202
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4203
+ const tileBoundsContext = get(tileBoundsContextTask);
4218
4204
  const topology = get(topologyTask);
4219
4205
  return work(
4220
4206
  () => topology.projection.gpu.renderVertexPosition({
4221
4207
  leafStorage,
4222
4208
  uniforms: terrainUniforms,
4223
- terrainFieldStorage
4209
+ terrainFieldStorage,
4210
+ tileBoundsNode: tileBoundsContext.node
4224
4211
  })
4225
4212
  );
4226
4213
  }).displayName("positionNodeTask");
@@ -4341,6 +4328,17 @@ function terrainGraph() {
4341
4328
  return g;
4342
4329
  }
4343
4330
 
4331
+ const decodeUint16RG = tsl.Fn(
4332
+ ([sample]) => sample.r.mul(tsl.float(256)).add(sample.g).div(tsl.float(257))
4333
+ );
4334
+ const sampleHeightmapMeters = tsl.Fn(
4335
+ ([heightmapTexture, uv, minM, _maxM, rangeM]) => {
4336
+ const sample = tsl.texture(heightmapTexture, uv);
4337
+ const normalized = decodeUint16RG(sample);
4338
+ return minM.add(normalized.mul(rangeM));
4339
+ }
4340
+ );
4341
+
4344
4342
  const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
4345
4343
  return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
4346
4344
  });
@@ -4398,6 +4396,12 @@ exports.AtlasBackend = AtlasBackend;
4398
4396
  exports.CUBE_FACES = CUBE_FACES;
4399
4397
  exports.CUBE_FACE_COUNT = CUBE_FACE_COUNT;
4400
4398
  exports.Dir = Dir;
4399
+ exports.TERRAIN_FIELD_PACK_EPSILON = TERRAIN_FIELD_PACK_EPSILON;
4400
+ exports.TILE_BOUNDS_FLOATS_PER_TILE = TILE_BOUNDS_FLOATS_PER_TILE;
4401
+ exports.TILE_BOUNDS_LOD_MAX_OFFSET = TILE_BOUNDS_LOD_MAX_OFFSET;
4402
+ exports.TILE_BOUNDS_LOD_MIN_OFFSET = TILE_BOUNDS_LOD_MIN_OFFSET;
4403
+ exports.TILE_BOUNDS_PACK_MAX_OFFSET = TILE_BOUNDS_PACK_MAX_OFFSET;
4404
+ exports.TILE_BOUNDS_PACK_MIN_OFFSET = TILE_BOUNDS_PACK_MIN_OFFSET;
4401
4405
  exports.TerrainGeometry = TerrainGeometry;
4402
4406
  exports.TerrainMesh = TerrainMesh;
4403
4407
  exports.U32_EMPTY = U32_EMPTY;
@@ -4434,6 +4438,8 @@ exports.cubeFaceDirection = cubeFaceDirection;
4434
4438
  exports.cubeFaceFromDirection = cubeFaceFromDirection;
4435
4439
  exports.cubeFacePoint = cubeFacePoint;
4436
4440
  exports.cubeFaceUVFromDirection = cubeFaceUVFromDirection;
4441
+ exports.decodeUint16RG = decodeUint16RG;
4442
+ exports.denormalizeTerrainFieldElevation = denormalizeTerrainFieldElevation;
4437
4443
  exports.deriveNormalZ = deriveNormalZ;
4438
4444
  exports.directionToFace = directionToFace;
4439
4445
  exports.directionToFaceUV = directionToFaceUV;
@@ -4456,9 +4462,11 @@ exports.leafStorageTask = leafStorageTask;
4456
4462
  exports.loadTerrainField = loadTerrainField;
4457
4463
  exports.loadTerrainFieldElevation = loadTerrainFieldElevation;
4458
4464
  exports.loadTerrainFieldNormal = loadTerrainFieldNormal;
4465
+ exports.loadTilePackBounds = loadTilePackBounds;
4459
4466
  exports.maxLevel = maxLevel;
4460
4467
  exports.maxNodes = maxNodes;
4461
4468
  exports.origin = origin;
4469
+ exports.packNormalizedTerrainFieldSample = packNormalizedTerrainFieldSample;
4462
4470
  exports.packTerrainFieldSample = packTerrainFieldSample;
4463
4471
  exports.positionNodeTask = positionNodeTask;
4464
4472
  exports.positionToTorusParams = positionToTorusParams;
@@ -4469,6 +4477,8 @@ exports.radius = radius;
4469
4477
  exports.resetLeafSet = resetLeafSet;
4470
4478
  exports.resetSeamTable = resetSeamTable;
4471
4479
  exports.rootSize = rootSize;
4480
+ exports.runTileBoundsReduction = runTileBoundsReduction;
4481
+ exports.sampleHeightmapMeters = sampleHeightmapMeters;
4472
4482
  exports.sampleTerrainField = sampleTerrainField;
4473
4483
  exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
4474
4484
  exports.skirtScale = skirtScale;
@@ -4483,6 +4493,8 @@ exports.terrainRaycastTask = terrainRaycastTask;
4483
4493
  exports.terrainReadbackTask = terrainReadbackTask;
4484
4494
  exports.terrainTasks = terrainTasks;
4485
4495
  exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
4496
+ exports.tileBoundsContextTask = tileBoundsContextTask;
4497
+ exports.tileBoundsReductionTask = tileBoundsReductionTask;
4486
4498
  exports.tileNodesTask = tileNodesTask;
4487
4499
  exports.topology = topology;
4488
4500
  exports.topologyTask = topologyTask;