@hello-terrain/three 0.0.0-alpha.13 → 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,283 +420,16 @@ 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]);
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
+ }
424
428
  }
425
429
  }
426
430
  return { execute };
427
431
  }
428
432
 
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;
697
- }
698
-
699
433
  function createLeafStorage(maxNodes) {
700
434
  const data = new Int32Array(maxNodes * 4);
701
435
  const attribute = new StorageBufferAttribute(data, 4);
@@ -1176,97 +910,373 @@ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1176
910
  if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
1177
911
  }
1178
912
  }
1179
- return outSeams;
913
+ return outSeams;
914
+ }
915
+
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));
1180
1190
  }
1181
1191
 
1182
- function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1183
- return Fn(
1184
- ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
1185
- const iEdge = int(edgeVertexCount);
1186
- const verticesPerNode = iEdge.mul(iEdge);
1187
- const baseOffset = int(nodeIndex).mul(verticesPerNode);
1188
- const xLeft = int(ix).sub(int(1));
1189
- const xRight = int(ix).add(int(1));
1190
- const yUp = int(iy).sub(int(1));
1191
- const yDown = int(iy).add(int(1));
1192
- const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
1193
- const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
1194
- const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
1195
- const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
1196
- const innerSegments = float(iEdge).sub(float(3));
1197
- const stepWorld = tileSize.div(innerSegments);
1198
- const inv2Step = float(0.5).div(stepWorld);
1199
- const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
1200
- const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
1201
- return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
1202
- }
1203
- );
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;
1204
1197
  }
1205
- function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
1206
- return Fn(([nodeIndex, ix, iy, elevationScale]) => {
1207
- const iEdge = int(edgeVertexCount);
1208
- const verticesPerNode = iEdge.mul(iEdge);
1209
- const baseOffset = int(nodeIndex).mul(verticesPerNode);
1210
- const xLeft = int(ix).sub(int(1));
1211
- const xRight = int(ix).add(int(1));
1212
- const yUp = int(iy).sub(int(1));
1213
- const yDown = int(iy).add(int(1));
1214
- const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
1215
- const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
1216
- const pLeft = positionAt(xLeft, int(iy), heightAt(xLeft, int(iy)));
1217
- const pRight = positionAt(xRight, int(iy), heightAt(xRight, int(iy)));
1218
- const pUp = positionAt(int(ix), yUp, heightAt(int(ix), yUp));
1219
- const pDown = positionAt(int(ix), yDown, heightAt(int(ix), yDown));
1220
- const tangentU = pRight.sub(pLeft);
1221
- const tangentV = pDown.sub(pUp);
1222
- const normal = cross(tangentU, tangentV).normalize();
1223
- const dir = dirAt(int(ix), int(iy));
1224
- 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);
1225
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;
1226
1250
  }
1227
1251
 
1228
- const isSkirtVertex = Fn(([segments]) => {
1229
- const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1230
- const vIndex = int(vertexIndex);
1231
- const segmentEdges = int(segmentsNode.add(3));
1232
- const vx = vIndex.mod(segmentEdges);
1233
- const vy = vIndex.div(segmentEdges);
1234
- const last = segmentEdges.sub(int(1));
1235
- return vx.equal(int(0)).or(vx.equal(last)).or(vy.equal(int(0))).or(vy.equal(last));
1236
- });
1237
- const isSkirtUV = Fn(([segments]) => {
1238
- const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1239
- const ux = uv().x;
1240
- const uy = uv().y;
1241
- const segmentCount = segmentsNode.add(2);
1242
- const segmentStep = float(1).div(segmentCount);
1243
- const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
1244
- const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
1245
- return innerX.and(innerY).not();
1246
- });
1247
-
1248
- function createTileElevation(terrainUniforms, terrainFieldStorage) {
1249
- if (!terrainFieldStorage) return float(0);
1252
+ function createTileElevation(terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1253
+ if (!terrainFieldStorage || !tileBoundsNode) return float(0);
1250
1254
  const innerSegs = terrainUniforms.uInnerTileSegments;
1251
1255
  const u = tileLocalToFieldUV(positionLocal.x.add(float(0.5)), innerSegs);
1252
1256
  const v = tileLocalToFieldUV(positionLocal.z.add(float(0.5)), innerSegs);
1253
- 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(
1254
1265
  terrainUniforms.uElevationScale
1255
1266
  );
1256
1267
  }
1257
1268
  function loadWorldNormal(terrainUniforms, terrainFieldStorage) {
1258
- const nodeIndex = int(instanceIndex);
1259
- const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
1260
- const localVertexIndex = int(vertexIndex);
1261
- const ix = localVertexIndex.mod(edgeVertexCount);
1262
- const iy = localVertexIndex.div(edgeVertexCount);
1263
- 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();
1264
1274
  }
1265
1275
  function assignWorldNormal(terrainUniforms, terrainFieldStorage) {
1266
1276
  if (!terrainFieldStorage) return;
1267
1277
  normalLocal.assign(Fn(() => loadWorldNormal(terrainUniforms, terrainFieldStorage))());
1268
1278
  }
1269
- function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1279
+ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1270
1280
  return Fn(() => {
1271
1281
  const tile = decodeLeafTile(leafStorage, int(instanceIndex));
1272
1282
  const rootSize = terrainUniforms.uRootSize.toVar();
@@ -1280,7 +1290,11 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1280
1290
  const clampedZ = positionLocal.z.max(half.negate()).min(half);
1281
1291
  const worldX = centerX.add(clampedX.mul(size));
1282
1292
  const worldZ = centerZ.add(clampedZ.mul(size));
1283
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1293
+ const yElevation = createTileElevation(
1294
+ terrainUniforms,
1295
+ terrainFieldStorage,
1296
+ tileBoundsNode
1297
+ );
1284
1298
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1285
1299
  const baseY = rootOrigin.y.add(yElevation);
1286
1300
  const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
@@ -1289,7 +1303,7 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1289
1303
  return vec3(worldX, worldY, worldZ);
1290
1304
  })();
1291
1305
  }
1292
- function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, baseU = 1, baseV = 1) {
1306
+ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, tileBoundsNode, baseU = 1, baseV = 1) {
1293
1307
  const fBaseU = float(baseU);
1294
1308
  const fBaseV = float(baseV);
1295
1309
  return Fn(() => {
@@ -1298,7 +1312,11 @@ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainF
1298
1312
  const localU = positionLocal.x.max(half.negate()).min(half).add(half);
1299
1313
  const localV = positionLocal.z.max(half.negate()).min(half).add(half);
1300
1314
  const faceUV = faceUVFromTileLocal(tile, localU, localV, fBaseU, fBaseV);
1301
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1315
+ const yElevation = createTileElevation(
1316
+ terrainUniforms,
1317
+ terrainFieldStorage,
1318
+ tileBoundsNode
1319
+ );
1302
1320
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1303
1321
  const displacement = select(
1304
1322
  skirtVertex,
@@ -1715,7 +1733,12 @@ function createFlatProjection() {
1715
1733
  faceOutward: false,
1716
1734
  gpu: {
1717
1735
  renderVertexPosition(ctx) {
1718
- return createFlatRenderVertexPosition(ctx.leafStorage, ctx.uniforms, ctx.terrainFieldStorage);
1736
+ return createFlatRenderVertexPosition(
1737
+ ctx.leafStorage,
1738
+ ctx.uniforms,
1739
+ ctx.terrainFieldStorage,
1740
+ ctx.tileBoundsNode
1741
+ );
1719
1742
  },
1720
1743
  createTileComputeParts: createFlatTileComputeParts,
1721
1744
  createFieldNormal(ctx) {
@@ -2278,9 +2301,11 @@ function packedSampleFromTileResult(params, tileResult) {
2278
2301
  fieldV,
2279
2302
  safeTileIndex
2280
2303
  ).toVar();
2304
+ const { packMin, packMax } = loadTilePackBounds(params.tileBoundsNode, safeTileIndex);
2305
+ const elevation = denormalizeTerrainFieldElevation(sampled.r, packMin, packMax);
2281
2306
  const normal = vec3(sampled.g, sampled.b, sampled.a);
2282
2307
  const valid = found.select(float(1), float(0)).toVar();
2283
- return vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2308
+ return vec4(elevation, normal.x, normal.y, normal.z).mul(valid);
2284
2309
  }
2285
2310
  function createTerrainSampleNode(params) {
2286
2311
  const tileLookup = createTileIndexFromWorldPosition(
@@ -2508,7 +2533,8 @@ function createCubeSphereProjection(config) {
2508
2533
  const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
2509
2534
  const r = invert ? ctx.uniforms.uRadius.toVar().sub(displacement) : ctx.uniforms.uRadius.toVar().add(displacement);
2510
2535
  return ctx.uniforms.uRootOrigin.toVar().add(dir.mul(r));
2511
- }
2536
+ },
2537
+ ctx.tileBoundsNode
2512
2538
  );
2513
2539
  },
2514
2540
  createTileComputeParts: createSphereTileComputeParts,
@@ -2856,6 +2882,7 @@ function createTorusProjection(config) {
2856
2882
  ctx.uniforms,
2857
2883
  ctx.terrainFieldStorage,
2858
2884
  (_tile, faceUV, displacement) => torusPosition(geometry, faceUV.x, faceUV.y, displacement),
2885
+ ctx.tileBoundsNode,
2859
2886
  baseU,
2860
2887
  baseV
2861
2888
  );
@@ -3082,8 +3109,8 @@ function buildTileElevationPyramid(pyramid, index, tileBounds, leafCount) {
3082
3109
  const level = index.keysLevel[slot];
3083
3110
  const x = index.keysX[slot];
3084
3111
  const y = index.keysY[slot];
3085
- const rawMin = tileBounds[leafIndex * 2];
3086
- 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];
3087
3114
  for (let ancestorLevel = level; ancestorLevel >= 0; ancestorLevel--) {
3088
3115
  const shift = level - ancestorLevel;
3089
3116
  mergeRange(
@@ -3237,8 +3264,8 @@ function createTerrainSnapshotState(maxNodes, maxLevel, totalElements) {
3237
3264
  backElevation: new Float32Array(totalElements),
3238
3265
  frontIndex: createSpatialIndex(maxNodes),
3239
3266
  backIndex: createSpatialIndex(maxNodes),
3240
- frontTileBounds: new Float32Array(maxNodes * 2),
3241
- 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),
3242
3269
  frontLeafCount: 0,
3243
3270
  globalRange: null,
3244
3271
  hasSnapshot: false,
@@ -3279,7 +3306,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3279
3306
  let boundsValid = activeLeafCount === 0;
3280
3307
  if (boundsFilled) {
3281
3308
  for (let i = 0; i < activeLeafCount; i += 1) {
3282
- 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) {
3283
3310
  boundsValid = true;
3284
3311
  break;
3285
3312
  }
@@ -3301,8 +3328,8 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3301
3328
  let gMin = Infinity;
3302
3329
  let gMax = -Infinity;
3303
3330
  for (let i = 0; i < activeLeafCount; i++) {
3304
- const rawMin = state.frontTileBounds[i * 2];
3305
- 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];
3306
3333
  const a = originY + rawMin * elevationScale;
3307
3334
  const b = originY + rawMax * elevationScale;
3308
3335
  gMin = Math.min(gMin, a, b);
@@ -3338,7 +3365,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3338
3365
  boundsAttribute,
3339
3366
  state.boundsReadback,
3340
3367
  state.backTileBounds,
3341
- activeLeafCount * 2,
3368
+ activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE,
3342
3369
  "terrainBoundsReadback"
3343
3370
  );
3344
3371
  }
@@ -3357,7 +3384,9 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3357
3384
  if (boundsResult) {
3358
3385
  const rawBounds = new Float32Array(boundsResult);
3359
3386
  state.backTileBounds.fill(0);
3360
- state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
3387
+ state.backTileBounds.set(
3388
+ rawBounds.subarray(0, activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE)
3389
+ );
3361
3390
  boundsFilled = true;
3362
3391
  }
3363
3392
  applySnapshot(boundsFilled);
@@ -3471,8 +3500,8 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3471
3500
  };
3472
3501
  const tileBoundsFromLookup = (lookup, elevationBase) => {
3473
3502
  if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
3474
- const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
3475
- 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];
3476
3505
  const a = elevationBase + rawMin * config.elevationScale;
3477
3506
  const b = elevationBase + rawMax * config.elevationScale;
3478
3507
  return {
@@ -3700,84 +3729,132 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3700
3729
  return lookupTileElevationRange(state.elevationPyramid, space, level, x, y, out);
3701
3730
  }
3702
3731
  };
3703
- return api;
3704
- }
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
+ };
3778
+ }
3779
+
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");
3705
3817
 
3706
- const WGSIZE = 64;
3707
- function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
3708
- const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
3709
- return Fn(() => {
3710
- const sharedMin = workgroupArray("float", WGSIZE);
3711
- const sharedMax = workgroupArray("float", WGSIZE);
3712
- const tid = int(localId.x);
3713
- const tileIdx = int(workgroupId.z);
3714
- const baseOffset = tileIdx.mul(int(verticesPerNode));
3715
- const start = tid.mul(int(elemsPerThread));
3716
- const end = min(start.add(int(elemsPerThread)), int(verticesPerNode));
3717
- const localMin = float(1e10).toVar("localMin");
3718
- const localMax = float(-1e10).toVar("localMax");
3719
- const edge = int(edgeVertexCount);
3720
- const lastEdge = int(edgeVertexCount - 1);
3721
- Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
3722
- const ix = int(i).mod(edge);
3723
- const iy = int(i).div(edge);
3724
- const isSkirt = ix.equal(int(0)).or(ix.equal(lastEdge)).or(iy.equal(int(0))).or(iy.equal(lastEdge));
3725
- If(isSkirt.not(), () => {
3726
- const h = elevationFieldNode.element(baseOffset.add(i));
3727
- localMin.assign(min(localMin, h));
3728
- localMax.assign(max(localMax, h));
3729
- });
3730
- });
3731
- sharedMin.element(tid).assign(localMin);
3732
- sharedMax.element(tid).assign(localMax);
3733
- workgroupBarrier();
3734
- If(tid.equal(int(0)), () => {
3735
- const finalMin = float(1e10).toVar("finalMin");
3736
- const finalMax = float(-1e10).toVar("finalMax");
3737
- Loop(WGSIZE, ({ i }) => {
3738
- finalMin.assign(min(finalMin, sharedMin.element(i)));
3739
- finalMax.assign(max(finalMax, sharedMax.element(i)));
3740
- });
3741
- const outIdx = tileIdx.mul(int(2));
3742
- boundsNode.element(outIdx).assign(finalMin);
3743
- boundsNode.element(outIdx.add(int(1))).assign(finalMax);
3744
- });
3745
- })().computeKernel([WGSIZE, 1, 1]);
3746
- }
3747
- const tileBoundsContextTask = task((get, work) => {
3748
- const elevationFieldContext = get(createElevationFieldContextTask);
3749
- const maxNodesValue = get(maxNodes);
3818
+ const createElevationFieldContextTask = task((get, work) => {
3750
3819
  const edgeVertexCount = get(innerTileSegments) + 3;
3820
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
3821
+ const totalElements = get(maxNodes) * verticesPerNode;
3751
3822
  return work(() => {
3752
- const data = new Float32Array(maxNodesValue * 2);
3823
+ const data = new Float32Array(totalElements);
3753
3824
  const attribute = new StorageBufferAttribute(data, 1);
3754
- attribute.name = "tileBounds";
3755
- const node = storage(attribute, "float", maxNodesValue * 2).setName(
3756
- "tileBounds"
3757
- );
3758
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
3759
- const kernel = buildReductionKernel(
3760
- elevationFieldContext.node,
3761
- node,
3762
- verticesPerNode,
3763
- edgeVertexCount
3764
- );
3765
- 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
+ };
3766
3832
  });
3767
- }).displayName("tileBoundsContextTask");
3768
- const tileBoundsReductionTask = task(
3769
- (get, work, { resources }) => {
3770
- get(executeComputeTask);
3771
- const boundsContext = get(tileBoundsContextTask);
3772
- const leafState = get(leafGpuBufferTask);
3773
- return work(() => {
3774
- if (resources?.renderer && leafState.count > 0) {
3775
- 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);
3776
3854
  }
3777
- return boundsContext;
3778
- });
3779
- }
3780
- ).displayName("tileBoundsReductionTask").lane("gpu");
3855
+ ];
3856
+ });
3857
+ }).displayName("elevationFieldStageTask");
3781
3858
 
3782
3859
  const terrainQueryTask = task((get, work) => {
3783
3860
  const maxNodesValue = get(maxNodes);
@@ -3920,107 +3997,88 @@ const leafGpuBufferTask = task((get, work) => {
3920
3997
  });
3921
3998
  }).displayName("leafGpuBufferTask");
3922
3999
 
3923
- function createTerrainUniforms(params) {
3924
- const sanitizedId = params.instanceId?.replace(/-/g, "_");
3925
- const suffix = sanitizedId ? `_${sanitizedId}` : "";
3926
- const uRootOrigin = uniform(
3927
- new Vector3$1(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
3928
- ).setName(`uRootOrigin${suffix}`);
3929
- const uRootSize = uniform(float(params.rootSize)).setName(`uRootSize${suffix}`);
3930
- const uInnerTileSegments = uniform(int(params.innerTileSegments)).setName(
3931
- `uInnerTileSegments${suffix}`
3932
- );
3933
- const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
3934
- const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
3935
- const uRadius = uniform(float(params.radius)).setName(`uRadius${suffix}`);
3936
- return {
3937
- uRootOrigin,
3938
- uRootSize,
3939
- uInnerTileSegments,
3940
- uSkirtScale,
3941
- uElevationScale,
3942
- uRadius
3943
- };
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]);
3944
4054
  }
3945
-
3946
- const instanceIdTask = task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
3947
-
3948
- const scratchVector3 = new Vector3();
3949
- const createUniformsTask = task((get, work) => {
3950
- const uniformParams = {
3951
- rootOrigin: get(origin),
3952
- rootSize: get(rootSize),
3953
- innerTileSegments: get(innerTileSegments),
3954
- skirtScale: get(skirtScale),
3955
- elevationScale: get(elevationScale),
3956
- radius: get(radius),
3957
- instanceId: get(instanceIdTask)
3958
- };
3959
- return work(() => createTerrainUniforms(uniformParams));
3960
- }).displayName("createUniformsTask").cache("once");
3961
- const updateUniformsTask = task((get, work) => {
3962
- const terrainUniformsContext = get(createUniformsTask);
3963
- const rootSizeVal = get(rootSize);
3964
- const rootOrigin = get(origin);
3965
- const innerTileSegmentsVal = get(innerTileSegments);
3966
- const skirtScaleVal = get(skirtScale);
3967
- const elevationScaleVal = get(elevationScale);
3968
- const radiusVal = get(radius);
3969
- return work(() => {
3970
- terrainUniformsContext.uRootSize.value = rootSizeVal;
3971
- terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
3972
- rootOrigin.x,
3973
- rootOrigin.y,
3974
- rootOrigin.z
3975
- );
3976
- terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
3977
- terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
3978
- terrainUniformsContext.uElevationScale.value = elevationScaleVal;
3979
- terrainUniformsContext.uRadius.value = radiusVal;
3980
- return terrainUniformsContext;
3981
- });
3982
- }).displayName("updateUniformsTask");
3983
-
3984
- 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);
3985
4063
  const edgeVertexCount = get(innerTileSegments) + 3;
3986
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
3987
- const totalElements = get(maxNodes) * verticesPerNode;
3988
4064
  return work(() => {
3989
- const data = new Float32Array(totalElements);
4065
+ const floatCount = maxNodesValue * TILE_BOUNDS_FLOATS_PER_TILE;
4066
+ const data = new Float32Array(floatCount);
3990
4067
  const attribute = new StorageBufferAttribute(data, 1);
3991
- attribute.name = "elevationField";
3992
- const node = storage(attribute, "float", totalElements).setName("elevationField");
3993
- return {
3994
- data,
3995
- attribute,
3996
- node
3997
- };
3998
- });
3999
- }).displayName("createElevationFieldContextTask");
4000
- const tileNodesTask = task((get, work) => {
4001
- const leafStorage = get(leafStorageTask);
4002
- const uniforms = get(updateUniformsTask);
4003
- const topology = get(topologyTask);
4004
- return work(() => {
4005
- return createTileCompute(leafStorage, uniforms, topology.projection);
4006
- });
4007
- }).displayName("tileNodesTask");
4008
- const elevationFieldStageTask = task((get, work) => {
4009
- const tile = get(tileNodesTask);
4010
- const uniforms = get(updateUniformsTask);
4011
- const elevationFieldContext = get(createElevationFieldContextTask);
4012
- const userElevationFn = get(elevationFn);
4013
- return work(() => {
4014
- const heightFn = createElevationFunction(userElevationFn);
4015
- const heightWriteFn = createElevation(tile, uniforms, heightFn);
4016
- return [
4017
- (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
4018
- const height = heightWriteFn(nodeIndex, localCoordinates);
4019
- elevationFieldContext.node.element(globalVertexIndex).assign(height);
4020
- }
4021
- ];
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 };
4022
4080
  });
4023
- }).displayName("elevationFieldStageTask");
4081
+ }).displayName("tileBoundsContextTask");
4024
4082
 
4025
4083
  const createTerrainFieldTextureTask = task(
4026
4084
  (get, work, { resources }) => {
@@ -4045,6 +4103,7 @@ const terrainFieldStageTask = task((get, work) => {
4045
4103
  const tile = get(tileNodesTask);
4046
4104
  const uniforms = get(updateUniformsTask);
4047
4105
  const topology = get(topologyTask);
4106
+ const boundsContext = get(tileBoundsContextTask);
4048
4107
  return work(() => {
4049
4108
  const computeNormal = topology.projection.gpu.createFieldNormal({
4050
4109
  elevationFieldNode: elevationFieldContext.node,
@@ -4059,12 +4118,13 @@ const terrainFieldStageTask = task((get, work) => {
4059
4118
  const iy = int(localCoordinates.y);
4060
4119
  const height = elevationFieldContext.node.element(globalVertexIndex);
4061
4120
  const normal = computeNormal(nodeIndex, ix, iy);
4121
+ const { packMin, packMax } = loadTilePackBounds(boundsContext.node, nodeIndex);
4062
4122
  storeTerrainField(
4063
4123
  terrainFieldStorage,
4064
4124
  ix,
4065
4125
  iy,
4066
4126
  nodeIndex,
4067
- packTerrainFieldSample(height, normal)
4127
+ packNormalizedTerrainFieldSample(height, normal, packMin, packMax)
4068
4128
  );
4069
4129
  }
4070
4130
  ];
@@ -4076,23 +4136,28 @@ function createComputePipelineTasks(leafStageTask) {
4076
4136
  const compile = task((get, work) => {
4077
4137
  const pipeline = get(leafStageTask);
4078
4138
  const edgeVertexCount = get(innerTileSegments) + 3;
4139
+ const boundsContext = get(tileBoundsContextTask);
4079
4140
  return work(
4080
4141
  () => compileComputePipeline(pipeline, edgeVertexCount, {
4081
- })
4142
+ midPipelineExecute: (renderer, instanceCount) => {
4143
+ runTileBoundsReduction(renderer, boundsContext, instanceCount);
4144
+ }
4145
+ })
4082
4146
  );
4083
4147
  }).displayName("compileComputeTask");
4084
- const execute = task(
4085
- (get, work, { resources }) => {
4086
- const { execute: run } = get(compile);
4087
- const leafState = get(leafGpuBufferTask);
4088
- return work(
4089
- () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4090
- }
4091
- );
4092
- }
4093
- ).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");
4094
4154
  return { compile, execute };
4095
4155
  }
4156
+ const tileBoundsReductionTask = task((get, work) => {
4157
+ get(executeComputeTask);
4158
+ const boundsContext = get(tileBoundsContextTask);
4159
+ return work(() => boundsContext);
4160
+ }).displayName("tileBoundsReductionTask").lane("gpu");
4096
4161
 
4097
4162
  const gpuSpatialIndexStorageTask = task((get, work) => {
4098
4163
  const maxNodesValue = get(maxNodes);
@@ -4110,6 +4175,7 @@ const gpuSpatialIndexUploadTask = task((get, work) => {
4110
4175
 
4111
4176
  const createTerrainSamplerTask = task((get, work) => {
4112
4177
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4178
+ const tileBoundsContext = get(tileBoundsContextTask);
4113
4179
  const spatialIndex = get(gpuSpatialIndexStorageTask);
4114
4180
  const uniforms = get(updateUniformsTask);
4115
4181
  const elevationCallback = get(elevationFn);
@@ -4118,6 +4184,7 @@ const createTerrainSamplerTask = task((get, work) => {
4118
4184
  return work(
4119
4185
  () => createTerrainSampler({
4120
4186
  terrainFieldStorage,
4187
+ tileBoundsNode: tileBoundsContext.node,
4121
4188
  spatialIndex,
4122
4189
  uniforms,
4123
4190
  elevationCallback,
@@ -4131,12 +4198,14 @@ const positionNodeTask = task((get, work) => {
4131
4198
  const leafStorage = get(leafStorageTask);
4132
4199
  const terrainUniforms = get(updateUniformsTask);
4133
4200
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4201
+ const tileBoundsContext = get(tileBoundsContextTask);
4134
4202
  const topology = get(topologyTask);
4135
4203
  return work(
4136
4204
  () => topology.projection.gpu.renderVertexPosition({
4137
4205
  leafStorage,
4138
4206
  uniforms: terrainUniforms,
4139
- terrainFieldStorage
4207
+ terrainFieldStorage,
4208
+ tileBoundsNode: tileBoundsContext.node
4140
4209
  })
4141
4210
  );
4142
4211
  }).displayName("positionNodeTask");
@@ -4257,6 +4326,17 @@ function terrainGraph() {
4257
4326
  return g;
4258
4327
  }
4259
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
+
4260
4340
  const textureSpaceToVectorSpace = Fn(([value]) => {
4261
4341
  return remap(value, float(0), float(1), float(-1), float(1));
4262
4342
  });
@@ -4309,4 +4389,4 @@ const voronoiCells = Fn((params) => {
4309
4389
  return k;
4310
4390
  });
4311
4391
 
4312
- 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 };