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