@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.cjs CHANGED
@@ -361,6 +361,7 @@ function compileComputePipeline(stages, width, options) {
361
361
  WORKGROUP_X,
362
362
  WORKGROUP_Y
363
363
  ];
364
+ const midPipelineExecute = options?.midPipelineExecute;
364
365
  const uInstanceCount = tsl.uniform(0, "uint").setName("uInstanceCount");
365
366
  const stagedKernelCache = /* @__PURE__ */ new Map();
366
367
  function clampWorkgroupToLimits(requested, limits) {
@@ -421,283 +422,16 @@ function compileComputePipeline(stages, width, options) {
421
422
  }
422
423
  const dispatchX = Math.ceil(width / workgroupX);
423
424
  const dispatchY = Math.ceil(width / workgroupY);
424
- for (const kernel of stagedKernels) {
425
- renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
425
+ for (let stageIndex = 0; stageIndex < stagedKernels.length; stageIndex += 1) {
426
+ renderer.compute(stagedKernels[stageIndex], [dispatchX, dispatchY, instanceCount]);
427
+ if (midPipelineExecute && stagedKernels.length > 1 && stageIndex === stagedKernels.length - 2) {
428
+ midPipelineExecute(renderer, instanceCount);
429
+ }
426
430
  }
427
431
  }
428
432
  return { execute };
429
433
  }
430
434
 
431
- function resolveType(format) {
432
- return format === "rgba16float" ? three.HalfFloatType : three.FloatType;
433
- }
434
- function resolveFilter(mode) {
435
- return mode === "linear" ? three.LinearFilter : three.NearestFilter;
436
- }
437
- function configureStorageTexture(texture2, format, filter) {
438
- texture2.format = three.RGBAFormat;
439
- texture2.type = resolveType(format);
440
- texture2.magFilter = resolveFilter(filter);
441
- texture2.minFilter = resolveFilter(filter);
442
- texture2.wrapS = three.ClampToEdgeWrapping;
443
- texture2.wrapT = three.ClampToEdgeWrapping;
444
- texture2.generateMipmaps = false;
445
- texture2.needsUpdate = true;
446
- }
447
- function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
448
- let currentEdgeVertexCount = edgeVertexCount;
449
- let currentTileCount = tileCount;
450
- const tex = new webgpu.StorageArrayTexture(
451
- edgeVertexCount,
452
- edgeVertexCount,
453
- tileCount
454
- );
455
- tex.name = "terrainField";
456
- configureStorageTexture(tex, options.format, options.filter);
457
- return {
458
- backendType: "array-texture",
459
- get edgeVertexCount() {
460
- return currentEdgeVertexCount;
461
- },
462
- get tileCount() {
463
- return currentTileCount;
464
- },
465
- texture: tex,
466
- uv(ix, iy, _tileIndex) {
467
- return tsl.vec2(ix.toFloat(), iy.toFloat());
468
- },
469
- texel(ix, iy, tileIndex) {
470
- return tsl.ivec3(ix, iy, tileIndex);
471
- },
472
- sample(u, v, tileIndex) {
473
- return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
474
- },
475
- resize(width, height, nextTileCount) {
476
- currentEdgeVertexCount = width;
477
- currentTileCount = nextTileCount;
478
- tex.setSize(width, height, nextTileCount);
479
- tex.needsUpdate = true;
480
- }
481
- };
482
- }
483
- function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
484
- const tilesPerRowNode = tsl.int(tilesPerRow);
485
- const edge = tsl.int(edgeVertexCount);
486
- const tile = tsl.int(tileIndex);
487
- const col = tile.mod(tilesPerRowNode);
488
- const row = tile.div(tilesPerRowNode);
489
- const atlasX = col.mul(edge).add(tsl.int(ix));
490
- const atlasY = row.mul(edge).add(tsl.int(iy));
491
- return { atlasX, atlasY };
492
- }
493
- function AtlasBackend(edgeVertexCount, tileCount, options) {
494
- let currentEdgeVertexCount = edgeVertexCount;
495
- let currentTileCount = tileCount;
496
- let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
497
- const atlasSize = tilesPerRow * edgeVertexCount;
498
- const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
499
- tex.name = "terrainFieldAtlas";
500
- configureStorageTexture(tex, options.format, options.filter);
501
- return {
502
- backendType: "atlas",
503
- get edgeVertexCount() {
504
- return currentEdgeVertexCount;
505
- },
506
- get tileCount() {
507
- return currentTileCount;
508
- },
509
- texture: tex,
510
- uv(ix, iy, tileIndex) {
511
- const { atlasX, atlasY } = atlasCoord(
512
- tilesPerRow,
513
- currentEdgeVertexCount,
514
- ix,
515
- iy,
516
- tileIndex
517
- );
518
- const currentAtlasSize = tsl.float(tilesPerRow * currentEdgeVertexCount);
519
- return tsl.vec2(
520
- atlasX.toFloat().add(0.5).div(currentAtlasSize),
521
- atlasY.toFloat().add(0.5).div(currentAtlasSize)
522
- );
523
- },
524
- texel(ix, iy, tileIndex) {
525
- const { atlasX, atlasY } = atlasCoord(
526
- tilesPerRow,
527
- currentEdgeVertexCount,
528
- ix,
529
- iy,
530
- tileIndex
531
- );
532
- return tsl.ivec2(atlasX, atlasY);
533
- },
534
- sample(u, v, tileIndex) {
535
- const tile = tsl.int(tileIndex);
536
- const tilesPerRowNode = tsl.int(tilesPerRow);
537
- const col = tile.mod(tilesPerRowNode);
538
- const row = tile.div(tilesPerRowNode);
539
- const invTilesPerRow = tsl.float(1 / tilesPerRow);
540
- const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
541
- const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
542
- return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
543
- },
544
- resize(width, height, nextTileCount) {
545
- currentEdgeVertexCount = width;
546
- currentTileCount = nextTileCount;
547
- tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
548
- const nextAtlasSize = tilesPerRow * width;
549
- const image = tex.image;
550
- image.width = nextAtlasSize;
551
- image.height = nextAtlasSize;
552
- tex.needsUpdate = true;
553
- }
554
- };
555
- }
556
- function texture3DBackend(edgeVertexCount, tileCount, options) {
557
- const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
558
- return { ...storage, backendType: "texture-3d" };
559
- }
560
- function tryGetDeviceLimits(renderer) {
561
- const backend = renderer;
562
- return backend.backend?.device?.limits ?? {};
563
- }
564
- function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
565
- const filter = options.filter ?? "linear";
566
- const format = options.format ?? "rgba16float";
567
- const forcedBackend = options.backend;
568
- if (forcedBackend === "atlas") {
569
- return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
570
- }
571
- if (forcedBackend === "texture-3d") {
572
- return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
573
- }
574
- if (forcedBackend === "array-texture") {
575
- return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
576
- }
577
- const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
578
- const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
579
- if (tileCount > maxLayers) {
580
- return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
581
- }
582
- return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
583
- }
584
- function storeTerrainField(storage, ix, iy, tileIndex, value) {
585
- if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
586
- return tsl.textureStore(
587
- storage.texture,
588
- tsl.uvec3(tsl.int(ix), tsl.int(iy), tsl.int(tileIndex)),
589
- value
590
- );
591
- }
592
- return tsl.textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
593
- }
594
- function loadTerrainField(storage, ix, iy, tileIndex) {
595
- if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
596
- return tsl.textureLoad(storage.texture, tsl.ivec2(tsl.int(ix), tsl.int(iy)), tsl.int(0)).depth(
597
- tsl.int(tileIndex)
598
- );
599
- }
600
- return tsl.textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), tsl.int(0));
601
- }
602
- function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
603
- return loadTerrainField(storage, ix, iy, tileIndex).r;
604
- }
605
- function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
606
- const raw = loadTerrainField(storage, ix, iy, tileIndex);
607
- return tsl.vec3(raw.g, raw.b, raw.a);
608
- }
609
- function sampleTerrainField(storage, u, v, tileIndex) {
610
- return storage.sample(u, v, tileIndex);
611
- }
612
- function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
613
- return sampleTerrainField(storage, u, v, tileIndex).r;
614
- }
615
- function packTerrainFieldSample(height, normal) {
616
- return tsl.vec4(height, normal.x, normal.y, normal.z);
617
- }
618
-
619
- const createElevation = (tile, uniforms, elevationFn) => {
620
- return function perVertexElevation(nodeIndex, localCoordinates) {
621
- const ix = tsl.int(localCoordinates.x);
622
- const iy = tsl.int(localCoordinates.y);
623
- const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
624
- const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
625
- const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
626
- const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
627
- const rootSize = uniforms.uRootSize.toVar();
628
- return elevationFn({
629
- worldPosition,
630
- rootSize,
631
- rootUV,
632
- tileOriginVec2: tile.tileOriginVec2(nodeIndex),
633
- tileSize: tile.tileSize(nodeIndex),
634
- tileLevel: tile.tileLevel(nodeIndex),
635
- nodeIndex: tsl.int(nodeIndex),
636
- tileUV
637
- });
638
- };
639
- };
640
-
641
- const HALF_PI = Math.PI * 0.5;
642
- const FIELD_INNER_TEXEL_OFFSET = 1.5;
643
- const FIELD_EDGE_EXTRA_TEXELS = 3;
644
- function sphereTileArcLength(radius, levelDivisor) {
645
- return radius * HALF_PI / levelDivisor;
646
- }
647
- function decodeLeafTile(leafStorage, nodeIndex) {
648
- const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
649
- return {
650
- level: leafStorage.node.element(nodeOffset).toInt(),
651
- x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
652
- y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
653
- face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
654
- };
655
- }
656
- function faceUVFromTileLocal(tile, localU, localV, baseU = tsl.float(1), baseV = tsl.float(1)) {
657
- const levelScale = tsl.pow(tsl.float(2), tile.level.toFloat());
658
- const nU = baseU.mul(levelScale);
659
- const nV = baseV.mul(levelScale);
660
- return tsl.vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
661
- }
662
- function createTileCompute(leafStorage, uniforms, projection) {
663
- const baseU = tsl.float(projection.baseResolution?.u ?? 1);
664
- const baseV = tsl.float(projection.baseResolution?.v ?? 1);
665
- const tileLevel = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
666
- const tileFace = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
667
- const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
668
- const tile = decodeLeafTile(leafStorage, nodeIndex);
669
- return tsl.vec2(tile.x, tile.y);
670
- });
671
- const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
672
- const tile = decodeLeafTile(leafStorage, nodeIndex);
673
- const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
674
- const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
675
- const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
676
- return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
677
- });
678
- const shared = {
679
- tileLevel: (nodeIndex) => tileLevel(nodeIndex),
680
- tileFace: (nodeIndex) => tileFace(nodeIndex),
681
- tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
682
- tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
683
- };
684
- const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
685
- return {
686
- ...shared,
687
- tileSize: parts.tileSize,
688
- rootUVCompute: parts.rootUV,
689
- tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
690
- };
691
- }
692
- function tileLocalToFieldUV(localCoord, innerSegments) {
693
- const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
694
- return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
695
- }
696
- function tileLocalToFieldUVNumber(localCoord, innerSegments) {
697
- const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
698
- return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
699
- }
700
-
701
435
  function createLeafStorage(maxNodes) {
702
436
  const data = new Int32Array(maxNodes * 4);
703
437
  const attribute = new webgpu.StorageBufferAttribute(data, 4);
@@ -1178,97 +912,373 @@ function buildSeams2to1(topology, leaves, outSeams, outIndex) {
1178
912
  if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
1179
913
  }
1180
914
  }
1181
- return outSeams;
915
+ return outSeams;
916
+ }
917
+
918
+ function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
919
+ return tsl.Fn(
920
+ ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
921
+ const iEdge = tsl.int(edgeVertexCount);
922
+ const verticesPerNode = iEdge.mul(iEdge);
923
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
924
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
925
+ const xRight = tsl.int(ix).add(tsl.int(1));
926
+ const yUp = tsl.int(iy).sub(tsl.int(1));
927
+ const yDown = tsl.int(iy).add(tsl.int(1));
928
+ const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
929
+ const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
930
+ const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
931
+ const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
932
+ const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
933
+ const stepWorld = tileSize.div(innerSegments);
934
+ const inv2Step = tsl.float(0.5).div(stepWorld);
935
+ const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
936
+ const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
937
+ return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
938
+ }
939
+ );
940
+ }
941
+ function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
942
+ return tsl.Fn(([nodeIndex, ix, iy, elevationScale]) => {
943
+ const iEdge = tsl.int(edgeVertexCount);
944
+ const verticesPerNode = iEdge.mul(iEdge);
945
+ const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
946
+ const xLeft = tsl.int(ix).sub(tsl.int(1));
947
+ const xRight = tsl.int(ix).add(tsl.int(1));
948
+ const yUp = tsl.int(iy).sub(tsl.int(1));
949
+ const yDown = tsl.int(iy).add(tsl.int(1));
950
+ const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
951
+ const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
952
+ const pLeft = positionAt(xLeft, tsl.int(iy), heightAt(xLeft, tsl.int(iy)));
953
+ const pRight = positionAt(xRight, tsl.int(iy), heightAt(xRight, tsl.int(iy)));
954
+ const pUp = positionAt(tsl.int(ix), yUp, heightAt(tsl.int(ix), yUp));
955
+ const pDown = positionAt(tsl.int(ix), yDown, heightAt(tsl.int(ix), yDown));
956
+ const tangentU = pRight.sub(pLeft);
957
+ const tangentV = pDown.sub(pUp);
958
+ const normal = tsl.cross(tangentU, tangentV).normalize();
959
+ const dir = dirAt(tsl.int(ix), tsl.int(iy));
960
+ return normal.mul(normal.dot(dir).sign());
961
+ });
962
+ }
963
+
964
+ const isSkirtVertex = tsl.Fn(([segments]) => {
965
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
966
+ const vIndex = tsl.int(tsl.vertexIndex);
967
+ const segmentEdges = tsl.int(segmentsNode.add(3));
968
+ const vx = vIndex.mod(segmentEdges);
969
+ const vy = vIndex.div(segmentEdges);
970
+ const last = segmentEdges.sub(tsl.int(1));
971
+ return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
972
+ });
973
+ const isSkirtUV = tsl.Fn(([segments]) => {
974
+ const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
975
+ const ux = tsl.uv().x;
976
+ const uy = tsl.uv().y;
977
+ const segmentCount = segmentsNode.add(2);
978
+ const segmentStep = tsl.float(1).div(segmentCount);
979
+ const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
980
+ const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
981
+ return innerX.and(innerY).not();
982
+ });
983
+
984
+ const TILE_BOUNDS_FLOATS_PER_TILE = 4;
985
+ const TILE_BOUNDS_LOD_MIN_OFFSET = 0;
986
+ const TILE_BOUNDS_LOD_MAX_OFFSET = 1;
987
+ const TILE_BOUNDS_PACK_MIN_OFFSET = 2;
988
+ const TILE_BOUNDS_PACK_MAX_OFFSET = 3;
989
+ const TERRAIN_FIELD_PACK_EPSILON = 1e-4;
990
+ function resolveType(format) {
991
+ return format === "rgba16float" ? three.HalfFloatType : three.FloatType;
992
+ }
993
+ function resolveFilter(mode) {
994
+ return mode === "linear" ? three.LinearFilter : three.NearestFilter;
995
+ }
996
+ function configureStorageTexture(texture2, format, filter) {
997
+ texture2.format = three.RGBAFormat;
998
+ texture2.type = resolveType(format);
999
+ texture2.magFilter = resolveFilter(filter);
1000
+ texture2.minFilter = resolveFilter(filter);
1001
+ texture2.wrapS = three.ClampToEdgeWrapping;
1002
+ texture2.wrapT = three.ClampToEdgeWrapping;
1003
+ texture2.generateMipmaps = false;
1004
+ texture2.needsUpdate = true;
1005
+ }
1006
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
1007
+ let currentEdgeVertexCount = edgeVertexCount;
1008
+ let currentTileCount = tileCount;
1009
+ const tex = new webgpu.StorageArrayTexture(
1010
+ edgeVertexCount,
1011
+ edgeVertexCount,
1012
+ tileCount
1013
+ );
1014
+ tex.name = "terrainField";
1015
+ configureStorageTexture(tex, options.format, options.filter);
1016
+ return {
1017
+ backendType: "array-texture",
1018
+ get edgeVertexCount() {
1019
+ return currentEdgeVertexCount;
1020
+ },
1021
+ get tileCount() {
1022
+ return currentTileCount;
1023
+ },
1024
+ texture: tex,
1025
+ uv(ix, iy, _tileIndex) {
1026
+ return tsl.vec2(ix.toFloat(), iy.toFloat());
1027
+ },
1028
+ texel(ix, iy, tileIndex) {
1029
+ return tsl.ivec3(ix, iy, tileIndex);
1030
+ },
1031
+ sample(u, v, tileIndex) {
1032
+ return tsl.texture(tex, tsl.vec2(u, v)).depth(tsl.int(tileIndex));
1033
+ },
1034
+ resize(width, height, nextTileCount) {
1035
+ currentEdgeVertexCount = width;
1036
+ currentTileCount = nextTileCount;
1037
+ tex.setSize(width, height, nextTileCount);
1038
+ tex.needsUpdate = true;
1039
+ }
1040
+ };
1041
+ }
1042
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
1043
+ const tilesPerRowNode = tsl.int(tilesPerRow);
1044
+ const edge = tsl.int(edgeVertexCount);
1045
+ const tile = tsl.int(tileIndex);
1046
+ const col = tile.mod(tilesPerRowNode);
1047
+ const row = tile.div(tilesPerRowNode);
1048
+ const atlasX = col.mul(edge).add(tsl.int(ix));
1049
+ const atlasY = row.mul(edge).add(tsl.int(iy));
1050
+ return { atlasX, atlasY };
1051
+ }
1052
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
1053
+ let currentEdgeVertexCount = edgeVertexCount;
1054
+ let currentTileCount = tileCount;
1055
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
1056
+ const atlasSize = tilesPerRow * edgeVertexCount;
1057
+ const tex = new webgpu.StorageTexture(atlasSize, atlasSize);
1058
+ tex.name = "terrainFieldAtlas";
1059
+ configureStorageTexture(tex, options.format, options.filter);
1060
+ return {
1061
+ backendType: "atlas",
1062
+ get edgeVertexCount() {
1063
+ return currentEdgeVertexCount;
1064
+ },
1065
+ get tileCount() {
1066
+ return currentTileCount;
1067
+ },
1068
+ texture: tex,
1069
+ uv(ix, iy, tileIndex) {
1070
+ const { atlasX, atlasY } = atlasCoord(
1071
+ tilesPerRow,
1072
+ currentEdgeVertexCount,
1073
+ ix,
1074
+ iy,
1075
+ tileIndex
1076
+ );
1077
+ const currentAtlasSize = tsl.float(tilesPerRow * currentEdgeVertexCount);
1078
+ return tsl.vec2(
1079
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
1080
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
1081
+ );
1082
+ },
1083
+ texel(ix, iy, tileIndex) {
1084
+ const { atlasX, atlasY } = atlasCoord(
1085
+ tilesPerRow,
1086
+ currentEdgeVertexCount,
1087
+ ix,
1088
+ iy,
1089
+ tileIndex
1090
+ );
1091
+ return tsl.ivec2(atlasX, atlasY);
1092
+ },
1093
+ sample(u, v, tileIndex) {
1094
+ const tile = tsl.int(tileIndex);
1095
+ const tilesPerRowNode = tsl.int(tilesPerRow);
1096
+ const col = tile.mod(tilesPerRowNode);
1097
+ const row = tile.div(tilesPerRowNode);
1098
+ const invTilesPerRow = tsl.float(1 / tilesPerRow);
1099
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
1100
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
1101
+ return tsl.texture(tex, tsl.vec2(atlasU, atlasV));
1102
+ },
1103
+ resize(width, height, nextTileCount) {
1104
+ currentEdgeVertexCount = width;
1105
+ currentTileCount = nextTileCount;
1106
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
1107
+ const nextAtlasSize = tilesPerRow * width;
1108
+ const image = tex.image;
1109
+ image.width = nextAtlasSize;
1110
+ image.height = nextAtlasSize;
1111
+ tex.needsUpdate = true;
1112
+ }
1113
+ };
1114
+ }
1115
+ function texture3DBackend(edgeVertexCount, tileCount, options) {
1116
+ const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
1117
+ return { ...storage, backendType: "texture-3d" };
1118
+ }
1119
+ function tryGetDeviceLimits(renderer) {
1120
+ const backend = renderer;
1121
+ return backend.backend?.device?.limits ?? {};
1122
+ }
1123
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
1124
+ const filter = options.filter ?? "linear";
1125
+ const format = options.format ?? "rgba16float";
1126
+ const forcedBackend = options.backend;
1127
+ if (forcedBackend === "atlas") {
1128
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
1129
+ }
1130
+ if (forcedBackend === "texture-3d") {
1131
+ return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
1132
+ }
1133
+ if (forcedBackend === "array-texture") {
1134
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
1135
+ }
1136
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
1137
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
1138
+ if (tileCount > maxLayers) {
1139
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
1140
+ }
1141
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
1142
+ }
1143
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
1144
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
1145
+ return tsl.textureStore(
1146
+ storage.texture,
1147
+ tsl.uvec3(tsl.int(ix), tsl.int(iy), tsl.int(tileIndex)),
1148
+ value
1149
+ );
1150
+ }
1151
+ return tsl.textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
1152
+ }
1153
+ function loadTerrainField(storage, ix, iy, tileIndex) {
1154
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
1155
+ return tsl.textureLoad(storage.texture, tsl.ivec2(tsl.int(ix), tsl.int(iy)), tsl.int(0)).depth(
1156
+ tsl.int(tileIndex)
1157
+ );
1158
+ }
1159
+ return tsl.textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), tsl.int(0));
1160
+ }
1161
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
1162
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
1163
+ }
1164
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
1165
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
1166
+ return tsl.vec3(raw.g, raw.b, raw.a);
1167
+ }
1168
+ function sampleTerrainField(storage, u, v, tileIndex) {
1169
+ return storage.sample(u, v, tileIndex);
1170
+ }
1171
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
1172
+ return sampleTerrainField(storage, u, v, tileIndex).r;
1173
+ }
1174
+ function packTerrainFieldSample(height, normal) {
1175
+ return tsl.vec4(height, normal.x, normal.y, normal.z);
1176
+ }
1177
+ function loadTilePackBounds(boundsNode, tileIndex) {
1178
+ const base = tsl.int(tileIndex).mul(tsl.int(TILE_BOUNDS_FLOATS_PER_TILE));
1179
+ return {
1180
+ packMin: boundsNode.element(base.add(tsl.int(TILE_BOUNDS_PACK_MIN_OFFSET))),
1181
+ packMax: boundsNode.element(base.add(tsl.int(TILE_BOUNDS_PACK_MAX_OFFSET)))
1182
+ };
1183
+ }
1184
+ function packNormalizedTerrainFieldSample(height, normal, packMin, packMax) {
1185
+ const span = tsl.max(packMax.sub(packMin), tsl.float(TERRAIN_FIELD_PACK_EPSILON));
1186
+ const normalized = height.sub(packMin).div(span);
1187
+ return tsl.vec4(normalized, normal.x, normal.y, normal.z);
1188
+ }
1189
+ function denormalizeTerrainFieldElevation(normalized, packMin, packMax) {
1190
+ const span = tsl.max(packMax.sub(packMin), tsl.float(TERRAIN_FIELD_PACK_EPSILON));
1191
+ return packMin.add(normalized.mul(span));
1182
1192
  }
1183
1193
 
1184
- function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1185
- return tsl.Fn(
1186
- ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
1187
- const iEdge = tsl.int(edgeVertexCount);
1188
- const verticesPerNode = iEdge.mul(iEdge);
1189
- const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1190
- const xLeft = tsl.int(ix).sub(tsl.int(1));
1191
- const xRight = tsl.int(ix).add(tsl.int(1));
1192
- const yUp = tsl.int(iy).sub(tsl.int(1));
1193
- const yDown = tsl.int(iy).add(tsl.int(1));
1194
- const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
1195
- const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
1196
- const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1197
- const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
1198
- const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
1199
- const stepWorld = tileSize.div(innerSegments);
1200
- const inv2Step = tsl.float(0.5).div(stepWorld);
1201
- const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
1202
- const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
1203
- return tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
1204
- }
1205
- );
1194
+ const HALF_PI = Math.PI * 0.5;
1195
+ const FIELD_INNER_TEXEL_OFFSET = 1.5;
1196
+ const FIELD_EDGE_EXTRA_TEXELS = 3;
1197
+ function sphereTileArcLength(radius, levelDivisor) {
1198
+ return radius * HALF_PI / levelDivisor;
1206
1199
  }
1207
- function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
1208
- return tsl.Fn(([nodeIndex, ix, iy, elevationScale]) => {
1209
- const iEdge = tsl.int(edgeVertexCount);
1210
- const verticesPerNode = iEdge.mul(iEdge);
1211
- const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
1212
- const xLeft = tsl.int(ix).sub(tsl.int(1));
1213
- const xRight = tsl.int(ix).add(tsl.int(1));
1214
- const yUp = tsl.int(iy).sub(tsl.int(1));
1215
- const yDown = tsl.int(iy).add(tsl.int(1));
1216
- const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
1217
- const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
1218
- const pLeft = positionAt(xLeft, tsl.int(iy), heightAt(xLeft, tsl.int(iy)));
1219
- const pRight = positionAt(xRight, tsl.int(iy), heightAt(xRight, tsl.int(iy)));
1220
- const pUp = positionAt(tsl.int(ix), yUp, heightAt(tsl.int(ix), yUp));
1221
- const pDown = positionAt(tsl.int(ix), yDown, heightAt(tsl.int(ix), yDown));
1222
- const tangentU = pRight.sub(pLeft);
1223
- const tangentV = pDown.sub(pUp);
1224
- const normal = tsl.cross(tangentU, tangentV).normalize();
1225
- const dir = dirAt(tsl.int(ix), tsl.int(iy));
1226
- return normal.mul(normal.dot(dir).sign());
1200
+ function decodeLeafTile(leafStorage, nodeIndex) {
1201
+ const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
1202
+ return {
1203
+ level: leafStorage.node.element(nodeOffset).toInt(),
1204
+ x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
1205
+ y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
1206
+ face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
1207
+ };
1208
+ }
1209
+ function faceUVFromTileLocal(tile, localU, localV, baseU = tsl.float(1), baseV = tsl.float(1)) {
1210
+ const levelScale = tsl.pow(tsl.float(2), tile.level.toFloat());
1211
+ const nU = baseU.mul(levelScale);
1212
+ const nV = baseV.mul(levelScale);
1213
+ return tsl.vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
1214
+ }
1215
+ function createTileCompute(leafStorage, uniforms, projection) {
1216
+ const baseU = tsl.float(projection.baseResolution?.u ?? 1);
1217
+ const baseV = tsl.float(projection.baseResolution?.v ?? 1);
1218
+ const tileLevel = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
1219
+ const tileFace = tsl.Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
1220
+ const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
1221
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
1222
+ return tsl.vec2(tile.x, tile.y);
1223
+ });
1224
+ const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
1225
+ const tile = decodeLeafTile(leafStorage, nodeIndex);
1226
+ const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
1227
+ const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
1228
+ const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
1229
+ return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
1227
1230
  });
1231
+ const shared = {
1232
+ tileLevel: (nodeIndex) => tileLevel(nodeIndex),
1233
+ tileFace: (nodeIndex) => tileFace(nodeIndex),
1234
+ tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
1235
+ tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
1236
+ };
1237
+ const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
1238
+ return {
1239
+ ...shared,
1240
+ tileSize: parts.tileSize,
1241
+ rootUVCompute: parts.rootUV,
1242
+ tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
1243
+ };
1244
+ }
1245
+ function tileLocalToFieldUV(localCoord, innerSegments) {
1246
+ const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
1247
+ return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
1248
+ }
1249
+ function tileLocalToFieldUVNumber(localCoord, innerSegments) {
1250
+ const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
1251
+ return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
1228
1252
  }
1229
1253
 
1230
- const isSkirtVertex = tsl.Fn(([segments]) => {
1231
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1232
- const vIndex = tsl.int(tsl.vertexIndex);
1233
- const segmentEdges = tsl.int(segmentsNode.add(3));
1234
- const vx = vIndex.mod(segmentEdges);
1235
- const vy = vIndex.div(segmentEdges);
1236
- const last = segmentEdges.sub(tsl.int(1));
1237
- return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
1238
- });
1239
- const isSkirtUV = tsl.Fn(([segments]) => {
1240
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
1241
- const ux = tsl.uv().x;
1242
- const uy = tsl.uv().y;
1243
- const segmentCount = segmentsNode.add(2);
1244
- const segmentStep = tsl.float(1).div(segmentCount);
1245
- const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
1246
- const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
1247
- return innerX.and(innerY).not();
1248
- });
1249
-
1250
- function createTileElevation(terrainUniforms, terrainFieldStorage) {
1251
- if (!terrainFieldStorage) return tsl.float(0);
1254
+ function createTileElevation(terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1255
+ if (!terrainFieldStorage || !tileBoundsNode) return tsl.float(0);
1252
1256
  const innerSegs = terrainUniforms.uInnerTileSegments;
1253
1257
  const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1254
1258
  const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1255
- return sampleTerrainFieldElevation(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex)).mul(
1259
+ const normalized = sampleTerrainFieldElevation(
1260
+ terrainFieldStorage,
1261
+ u,
1262
+ v,
1263
+ tsl.int(tsl.instanceIndex)
1264
+ );
1265
+ const { packMin, packMax } = loadTilePackBounds(tileBoundsNode, tsl.int(tsl.instanceIndex));
1266
+ return denormalizeTerrainFieldElevation(normalized, packMin, packMax).mul(
1256
1267
  terrainUniforms.uElevationScale
1257
1268
  );
1258
1269
  }
1259
1270
  function loadWorldNormal(terrainUniforms, terrainFieldStorage) {
1260
- const nodeIndex = tsl.int(tsl.instanceIndex);
1261
- const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
1262
- const localVertexIndex = tsl.int(tsl.vertexIndex);
1263
- const ix = localVertexIndex.mod(edgeVertexCount);
1264
- const iy = localVertexIndex.div(edgeVertexCount);
1265
- return loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1271
+ const innerSegs = terrainUniforms.uInnerTileSegments;
1272
+ const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
1273
+ const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
1274
+ const raw = sampleTerrainField(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex));
1275
+ return tsl.vec3(raw.g, raw.b, raw.a).normalize();
1266
1276
  }
1267
1277
  function assignWorldNormal(terrainUniforms, terrainFieldStorage) {
1268
1278
  if (!terrainFieldStorage) return;
1269
1279
  tsl.normalLocal.assign(tsl.Fn(() => loadWorldNormal(terrainUniforms, terrainFieldStorage))());
1270
1280
  }
1271
- function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1281
+ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, tileBoundsNode) {
1272
1282
  return tsl.Fn(() => {
1273
1283
  const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
1274
1284
  const rootSize = terrainUniforms.uRootSize.toVar();
@@ -1282,7 +1292,11 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1282
1292
  const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
1283
1293
  const worldX = centerX.add(clampedX.mul(size));
1284
1294
  const worldZ = centerZ.add(clampedZ.mul(size));
1285
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1295
+ const yElevation = createTileElevation(
1296
+ terrainUniforms,
1297
+ terrainFieldStorage,
1298
+ tileBoundsNode
1299
+ );
1286
1300
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1287
1301
  const baseY = rootOrigin.y.add(yElevation);
1288
1302
  const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
@@ -1291,7 +1305,7 @@ function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFie
1291
1305
  return tsl.vec3(worldX, worldY, worldZ);
1292
1306
  })();
1293
1307
  }
1294
- function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, baseU = 1, baseV = 1) {
1308
+ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, tileBoundsNode, baseU = 1, baseV = 1) {
1295
1309
  const fBaseU = tsl.float(baseU);
1296
1310
  const fBaseV = tsl.float(baseV);
1297
1311
  return tsl.Fn(() => {
@@ -1300,7 +1314,11 @@ function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainF
1300
1314
  const localU = tsl.positionLocal.x.max(half.negate()).min(half).add(half);
1301
1315
  const localV = tsl.positionLocal.z.max(half.negate()).min(half).add(half);
1302
1316
  const faceUV = faceUVFromTileLocal(tile, localU, localV, fBaseU, fBaseV);
1303
- const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1317
+ const yElevation = createTileElevation(
1318
+ terrainUniforms,
1319
+ terrainFieldStorage,
1320
+ tileBoundsNode
1321
+ );
1304
1322
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1305
1323
  const displacement = tsl.select(
1306
1324
  skirtVertex,
@@ -1717,7 +1735,12 @@ function createFlatProjection() {
1717
1735
  faceOutward: false,
1718
1736
  gpu: {
1719
1737
  renderVertexPosition(ctx) {
1720
- return createFlatRenderVertexPosition(ctx.leafStorage, ctx.uniforms, ctx.terrainFieldStorage);
1738
+ return createFlatRenderVertexPosition(
1739
+ ctx.leafStorage,
1740
+ ctx.uniforms,
1741
+ ctx.terrainFieldStorage,
1742
+ ctx.tileBoundsNode
1743
+ );
1721
1744
  },
1722
1745
  createTileComputeParts: createFlatTileComputeParts,
1723
1746
  createFieldNormal(ctx) {
@@ -2280,9 +2303,11 @@ function packedSampleFromTileResult(params, tileResult) {
2280
2303
  fieldV,
2281
2304
  safeTileIndex
2282
2305
  ).toVar();
2306
+ const { packMin, packMax } = loadTilePackBounds(params.tileBoundsNode, safeTileIndex);
2307
+ const elevation = denormalizeTerrainFieldElevation(sampled.r, packMin, packMax);
2283
2308
  const normal = tsl.vec3(sampled.g, sampled.b, sampled.a);
2284
2309
  const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
2285
- return tsl.vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
2310
+ return tsl.vec4(elevation, normal.x, normal.y, normal.z).mul(valid);
2286
2311
  }
2287
2312
  function createTerrainSampleNode(params) {
2288
2313
  const tileLookup = createTileIndexFromWorldPosition(
@@ -2510,7 +2535,8 @@ function createCubeSphereProjection(config) {
2510
2535
  const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
2511
2536
  const r = invert ? ctx.uniforms.uRadius.toVar().sub(displacement) : ctx.uniforms.uRadius.toVar().add(displacement);
2512
2537
  return ctx.uniforms.uRootOrigin.toVar().add(dir.mul(r));
2513
- }
2538
+ },
2539
+ ctx.tileBoundsNode
2514
2540
  );
2515
2541
  },
2516
2542
  createTileComputeParts: createSphereTileComputeParts,
@@ -2858,6 +2884,7 @@ function createTorusProjection(config) {
2858
2884
  ctx.uniforms,
2859
2885
  ctx.terrainFieldStorage,
2860
2886
  (_tile, faceUV, displacement) => torusPosition(geometry, faceUV.x, faceUV.y, displacement),
2887
+ ctx.tileBoundsNode,
2861
2888
  baseU,
2862
2889
  baseV
2863
2890
  );
@@ -3084,8 +3111,8 @@ function buildTileElevationPyramid(pyramid, index, tileBounds, leafCount) {
3084
3111
  const level = index.keysLevel[slot];
3085
3112
  const x = index.keysX[slot];
3086
3113
  const y = index.keysY[slot];
3087
- const rawMin = tileBounds[leafIndex * 2];
3088
- const rawMax = tileBounds[leafIndex * 2 + 1];
3114
+ const rawMin = tileBounds[leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3115
+ const rawMax = tileBounds[leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3089
3116
  for (let ancestorLevel = level; ancestorLevel >= 0; ancestorLevel--) {
3090
3117
  const shift = level - ancestorLevel;
3091
3118
  mergeRange(
@@ -3239,8 +3266,8 @@ function createTerrainSnapshotState(maxNodes, maxLevel, totalElements) {
3239
3266
  backElevation: new Float32Array(totalElements),
3240
3267
  frontIndex: createSpatialIndex(maxNodes),
3241
3268
  backIndex: createSpatialIndex(maxNodes),
3242
- frontTileBounds: new Float32Array(maxNodes * 2),
3243
- backTileBounds: new Float32Array(maxNodes * 2),
3269
+ frontTileBounds: new Float32Array(maxNodes * TILE_BOUNDS_FLOATS_PER_TILE),
3270
+ backTileBounds: new Float32Array(maxNodes * TILE_BOUNDS_FLOATS_PER_TILE),
3244
3271
  frontLeafCount: 0,
3245
3272
  globalRange: null,
3246
3273
  hasSnapshot: false,
@@ -3281,7 +3308,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3281
3308
  let boundsValid = activeLeafCount === 0;
3282
3309
  if (boundsFilled) {
3283
3310
  for (let i = 0; i < activeLeafCount; i += 1) {
3284
- if ((state.backTileBounds[i * 2 + 1] ?? 0) !== 0) {
3311
+ if ((state.backTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET] ?? 0) !== 0) {
3285
3312
  boundsValid = true;
3286
3313
  break;
3287
3314
  }
@@ -3303,8 +3330,8 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3303
3330
  let gMin = Infinity;
3304
3331
  let gMax = -Infinity;
3305
3332
  for (let i = 0; i < activeLeafCount; i++) {
3306
- const rawMin = state.frontTileBounds[i * 2];
3307
- const rawMax = state.frontTileBounds[i * 2 + 1];
3333
+ const rawMin = state.frontTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3334
+ const rawMax = state.frontTileBounds[i * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3308
3335
  const a = originY + rawMin * elevationScale;
3309
3336
  const b = originY + rawMax * elevationScale;
3310
3337
  gMin = Math.min(gMin, a, b);
@@ -3340,7 +3367,7 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3340
3367
  boundsAttribute,
3341
3368
  state.boundsReadback,
3342
3369
  state.backTileBounds,
3343
- activeLeafCount * 2,
3370
+ activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE,
3344
3371
  "terrainBoundsReadback"
3345
3372
  );
3346
3373
  }
@@ -3359,7 +3386,9 @@ function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, bound
3359
3386
  if (boundsResult) {
3360
3387
  const rawBounds = new Float32Array(boundsResult);
3361
3388
  state.backTileBounds.fill(0);
3362
- state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
3389
+ state.backTileBounds.set(
3390
+ rawBounds.subarray(0, activeLeafCount * TILE_BOUNDS_FLOATS_PER_TILE)
3391
+ );
3363
3392
  boundsFilled = true;
3364
3393
  }
3365
3394
  applySnapshot(boundsFilled);
@@ -3473,8 +3502,8 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3473
3502
  };
3474
3503
  const tileBoundsFromLookup = (lookup, elevationBase) => {
3475
3504
  if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
3476
- const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
3477
- const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
3505
+ const rawMin = state.frontTileBounds[lookup.leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MIN_OFFSET];
3506
+ const rawMax = state.frontTileBounds[lookup.leafIndex * TILE_BOUNDS_FLOATS_PER_TILE + TILE_BOUNDS_LOD_MAX_OFFSET];
3478
3507
  const a = elevationBase + rawMin * config.elevationScale;
3479
3508
  const b = elevationBase + rawMax * config.elevationScale;
3480
3509
  return {
@@ -3702,84 +3731,132 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3702
3731
  return lookupTileElevationRange(state.elevationPyramid, space, level, x, y, out);
3703
3732
  }
3704
3733
  };
3705
- return api;
3706
- }
3734
+ return api;
3735
+ }
3736
+
3737
+ const createElevation = (tile, uniforms, elevationFn) => {
3738
+ return function perVertexElevation(nodeIndex, localCoordinates) {
3739
+ const ix = tsl.int(localCoordinates.x);
3740
+ const iy = tsl.int(localCoordinates.y);
3741
+ const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
3742
+ const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
3743
+ const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
3744
+ const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
3745
+ const rootSize = uniforms.uRootSize.toVar();
3746
+ return elevationFn({
3747
+ worldPosition,
3748
+ rootSize,
3749
+ rootUV,
3750
+ tileOriginVec2: tile.tileOriginVec2(nodeIndex),
3751
+ tileSize: tile.tileSize(nodeIndex),
3752
+ tileLevel: tile.tileLevel(nodeIndex),
3753
+ nodeIndex: tsl.int(nodeIndex),
3754
+ tileUV
3755
+ });
3756
+ };
3757
+ };
3758
+
3759
+ function createTerrainUniforms(params) {
3760
+ const sanitizedId = params.instanceId?.replace(/-/g, "_");
3761
+ const suffix = sanitizedId ? `_${sanitizedId}` : "";
3762
+ const uRootOrigin = tsl.uniform(
3763
+ new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
3764
+ ).setName(`uRootOrigin${suffix}`);
3765
+ const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
3766
+ const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
3767
+ `uInnerTileSegments${suffix}`
3768
+ );
3769
+ const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
3770
+ const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
3771
+ const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
3772
+ return {
3773
+ uRootOrigin,
3774
+ uRootSize,
3775
+ uInnerTileSegments,
3776
+ uSkirtScale,
3777
+ uElevationScale,
3778
+ uRadius
3779
+ };
3780
+ }
3781
+
3782
+ const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
3783
+
3784
+ const scratchVector3 = new three.Vector3();
3785
+ const createUniformsTask = work.task((get, work) => {
3786
+ const uniformParams = {
3787
+ rootOrigin: get(origin),
3788
+ rootSize: get(rootSize),
3789
+ innerTileSegments: get(innerTileSegments),
3790
+ skirtScale: get(skirtScale),
3791
+ elevationScale: get(elevationScale),
3792
+ radius: get(radius),
3793
+ instanceId: get(instanceIdTask)
3794
+ };
3795
+ return work(() => createTerrainUniforms(uniformParams));
3796
+ }).displayName("createUniformsTask").cache("once");
3797
+ const updateUniformsTask = work.task((get, work) => {
3798
+ const terrainUniformsContext = get(createUniformsTask);
3799
+ const rootSizeVal = get(rootSize);
3800
+ const rootOrigin = get(origin);
3801
+ const innerTileSegmentsVal = get(innerTileSegments);
3802
+ const skirtScaleVal = get(skirtScale);
3803
+ const elevationScaleVal = get(elevationScale);
3804
+ const radiusVal = get(radius);
3805
+ return work(() => {
3806
+ terrainUniformsContext.uRootSize.value = rootSizeVal;
3807
+ terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
3808
+ rootOrigin.x,
3809
+ rootOrigin.y,
3810
+ rootOrigin.z
3811
+ );
3812
+ terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
3813
+ terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
3814
+ terrainUniformsContext.uElevationScale.value = elevationScaleVal;
3815
+ terrainUniformsContext.uRadius.value = radiusVal;
3816
+ return terrainUniformsContext;
3817
+ });
3818
+ }).displayName("updateUniformsTask");
3707
3819
 
3708
- const WGSIZE = 64;
3709
- function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
3710
- const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
3711
- return tsl.Fn(() => {
3712
- const sharedMin = tsl.workgroupArray("float", WGSIZE);
3713
- const sharedMax = tsl.workgroupArray("float", WGSIZE);
3714
- const tid = tsl.int(tsl.localId.x);
3715
- const tileIdx = tsl.int(tsl.workgroupId.z);
3716
- const baseOffset = tileIdx.mul(tsl.int(verticesPerNode));
3717
- const start = tid.mul(tsl.int(elemsPerThread));
3718
- const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
3719
- const localMin = tsl.float(1e10).toVar("localMin");
3720
- const localMax = tsl.float(-1e10).toVar("localMax");
3721
- const edge = tsl.int(edgeVertexCount);
3722
- const lastEdge = tsl.int(edgeVertexCount - 1);
3723
- tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
3724
- const ix = tsl.int(i).mod(edge);
3725
- const iy = tsl.int(i).div(edge);
3726
- const isSkirt = ix.equal(tsl.int(0)).or(ix.equal(lastEdge)).or(iy.equal(tsl.int(0))).or(iy.equal(lastEdge));
3727
- tsl.If(isSkirt.not(), () => {
3728
- const h = elevationFieldNode.element(baseOffset.add(i));
3729
- localMin.assign(tsl.min(localMin, h));
3730
- localMax.assign(tsl.max(localMax, h));
3731
- });
3732
- });
3733
- sharedMin.element(tid).assign(localMin);
3734
- sharedMax.element(tid).assign(localMax);
3735
- tsl.workgroupBarrier();
3736
- tsl.If(tid.equal(tsl.int(0)), () => {
3737
- const finalMin = tsl.float(1e10).toVar("finalMin");
3738
- const finalMax = tsl.float(-1e10).toVar("finalMax");
3739
- tsl.Loop(WGSIZE, ({ i }) => {
3740
- finalMin.assign(tsl.min(finalMin, sharedMin.element(i)));
3741
- finalMax.assign(tsl.max(finalMax, sharedMax.element(i)));
3742
- });
3743
- const outIdx = tileIdx.mul(tsl.int(2));
3744
- boundsNode.element(outIdx).assign(finalMin);
3745
- boundsNode.element(outIdx.add(tsl.int(1))).assign(finalMax);
3746
- });
3747
- })().computeKernel([WGSIZE, 1, 1]);
3748
- }
3749
- const tileBoundsContextTask = work.task((get, work) => {
3750
- const elevationFieldContext = get(createElevationFieldContextTask);
3751
- const maxNodesValue = get(maxNodes);
3820
+ const createElevationFieldContextTask = work.task((get, work) => {
3752
3821
  const edgeVertexCount = get(innerTileSegments) + 3;
3822
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
3823
+ const totalElements = get(maxNodes) * verticesPerNode;
3753
3824
  return work(() => {
3754
- const data = new Float32Array(maxNodesValue * 2);
3825
+ const data = new Float32Array(totalElements);
3755
3826
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
3756
- attribute.name = "tileBounds";
3757
- const node = tsl.storage(attribute, "float", maxNodesValue * 2).setName(
3758
- "tileBounds"
3759
- );
3760
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
3761
- const kernel = buildReductionKernel(
3762
- elevationFieldContext.node,
3763
- node,
3764
- verticesPerNode,
3765
- edgeVertexCount
3766
- );
3767
- return { data, attribute, node, kernel };
3827
+ attribute.name = "elevationField";
3828
+ const node = tsl.storage(attribute, "float", totalElements).setName("elevationField");
3829
+ return {
3830
+ data,
3831
+ attribute,
3832
+ node
3833
+ };
3768
3834
  });
3769
- }).displayName("tileBoundsContextTask");
3770
- const tileBoundsReductionTask = work.task(
3771
- (get, work, { resources }) => {
3772
- get(executeComputeTask);
3773
- const boundsContext = get(tileBoundsContextTask);
3774
- const leafState = get(leafGpuBufferTask);
3775
- return work(() => {
3776
- if (resources?.renderer && leafState.count > 0) {
3777
- resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
3835
+ }).displayName("createElevationFieldContextTask");
3836
+ const tileNodesTask = work.task((get, work) => {
3837
+ const leafStorage = get(leafStorageTask);
3838
+ const uniforms = get(updateUniformsTask);
3839
+ const topology = get(topologyTask);
3840
+ return work(() => {
3841
+ return createTileCompute(leafStorage, uniforms, topology.projection);
3842
+ });
3843
+ }).displayName("tileNodesTask");
3844
+ const elevationFieldStageTask = work.task((get, work) => {
3845
+ const tile = get(tileNodesTask);
3846
+ const uniforms = get(updateUniformsTask);
3847
+ const elevationFieldContext = get(createElevationFieldContextTask);
3848
+ const userElevationFn = get(elevationFn);
3849
+ return work(() => {
3850
+ const heightFn = createElevationFunction(userElevationFn);
3851
+ const heightWriteFn = createElevation(tile, uniforms, heightFn);
3852
+ return [
3853
+ (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
3854
+ const height = heightWriteFn(nodeIndex, localCoordinates);
3855
+ elevationFieldContext.node.element(globalVertexIndex).assign(height);
3778
3856
  }
3779
- return boundsContext;
3780
- });
3781
- }
3782
- ).displayName("tileBoundsReductionTask").lane("gpu");
3857
+ ];
3858
+ });
3859
+ }).displayName("elevationFieldStageTask");
3783
3860
 
3784
3861
  const terrainQueryTask = work.task((get, work) => {
3785
3862
  const maxNodesValue = get(maxNodes);
@@ -3922,107 +3999,88 @@ const leafGpuBufferTask = work.task((get, work) => {
3922
3999
  });
3923
4000
  }).displayName("leafGpuBufferTask");
3924
4001
 
3925
- function createTerrainUniforms(params) {
3926
- const sanitizedId = params.instanceId?.replace(/-/g, "_");
3927
- const suffix = sanitizedId ? `_${sanitizedId}` : "";
3928
- const uRootOrigin = tsl.uniform(
3929
- new webgpu.Vector3(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
3930
- ).setName(`uRootOrigin${suffix}`);
3931
- const uRootSize = tsl.uniform(tsl.float(params.rootSize)).setName(`uRootSize${suffix}`);
3932
- const uInnerTileSegments = tsl.uniform(tsl.int(params.innerTileSegments)).setName(
3933
- `uInnerTileSegments${suffix}`
3934
- );
3935
- const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
3936
- const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
3937
- const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
3938
- return {
3939
- uRootOrigin,
3940
- uRootSize,
3941
- uInnerTileSegments,
3942
- uSkirtScale,
3943
- uElevationScale,
3944
- uRadius
3945
- };
4002
+ const WGSIZE = 64;
4003
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
4004
+ const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
4005
+ return tsl.Fn(() => {
4006
+ const sharedLodMin = tsl.workgroupArray("float", WGSIZE);
4007
+ const sharedLodMax = tsl.workgroupArray("float", WGSIZE);
4008
+ const sharedPackMin = tsl.workgroupArray("float", WGSIZE);
4009
+ const sharedPackMax = tsl.workgroupArray("float", WGSIZE);
4010
+ const tid = tsl.int(tsl.localId.x);
4011
+ const tileIdx = tsl.int(tsl.workgroupId.z);
4012
+ const baseOffset = tileIdx.mul(tsl.int(verticesPerNode));
4013
+ const start = tid.mul(tsl.int(elemsPerThread));
4014
+ const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
4015
+ const localLodMin = tsl.float(1e10).toVar("localLodMin");
4016
+ const localLodMax = tsl.float(-1e10).toVar("localLodMax");
4017
+ const localPackMin = tsl.float(1e10).toVar("localPackMin");
4018
+ const localPackMax = tsl.float(-1e10).toVar("localPackMax");
4019
+ const edge = tsl.int(edgeVertexCount);
4020
+ const lastEdge = tsl.int(edgeVertexCount - 1);
4021
+ tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
4022
+ const ix = tsl.int(i).mod(edge);
4023
+ const iy = tsl.int(i).div(edge);
4024
+ const isSkirt = ix.equal(tsl.int(0)).or(ix.equal(lastEdge)).or(iy.equal(tsl.int(0))).or(iy.equal(lastEdge));
4025
+ const h = elevationFieldNode.element(baseOffset.add(i));
4026
+ localPackMin.assign(tsl.min(localPackMin, h));
4027
+ localPackMax.assign(tsl.max(localPackMax, h));
4028
+ tsl.If(isSkirt.not(), () => {
4029
+ localLodMin.assign(tsl.min(localLodMin, h));
4030
+ localLodMax.assign(tsl.max(localLodMax, h));
4031
+ });
4032
+ });
4033
+ sharedLodMin.element(tid).assign(localLodMin);
4034
+ sharedLodMax.element(tid).assign(localLodMax);
4035
+ sharedPackMin.element(tid).assign(localPackMin);
4036
+ sharedPackMax.element(tid).assign(localPackMax);
4037
+ tsl.workgroupBarrier();
4038
+ tsl.If(tid.equal(tsl.int(0)), () => {
4039
+ const finalLodMin = tsl.float(1e10).toVar("finalLodMin");
4040
+ const finalLodMax = tsl.float(-1e10).toVar("finalLodMax");
4041
+ const finalPackMin = tsl.float(1e10).toVar("finalPackMin");
4042
+ const finalPackMax = tsl.float(-1e10).toVar("finalPackMax");
4043
+ tsl.Loop(WGSIZE, ({ i }) => {
4044
+ finalLodMin.assign(tsl.min(finalLodMin, sharedLodMin.element(i)));
4045
+ finalLodMax.assign(tsl.max(finalLodMax, sharedLodMax.element(i)));
4046
+ finalPackMin.assign(tsl.min(finalPackMin, sharedPackMin.element(i)));
4047
+ finalPackMax.assign(tsl.max(finalPackMax, sharedPackMax.element(i)));
4048
+ });
4049
+ const outIdx = tileIdx.mul(tsl.int(TILE_BOUNDS_FLOATS_PER_TILE));
4050
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_LOD_MIN_OFFSET))).assign(finalLodMin);
4051
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_LOD_MAX_OFFSET))).assign(finalLodMax);
4052
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_PACK_MIN_OFFSET))).assign(finalPackMin);
4053
+ boundsNode.element(outIdx.add(tsl.int(TILE_BOUNDS_PACK_MAX_OFFSET))).assign(finalPackMax);
4054
+ });
4055
+ })().computeKernel([WGSIZE, 1, 1]);
3946
4056
  }
3947
-
3948
- const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
3949
-
3950
- const scratchVector3 = new three.Vector3();
3951
- const createUniformsTask = work.task((get, work) => {
3952
- const uniformParams = {
3953
- rootOrigin: get(origin),
3954
- rootSize: get(rootSize),
3955
- innerTileSegments: get(innerTileSegments),
3956
- skirtScale: get(skirtScale),
3957
- elevationScale: get(elevationScale),
3958
- radius: get(radius),
3959
- instanceId: get(instanceIdTask)
3960
- };
3961
- return work(() => createTerrainUniforms(uniformParams));
3962
- }).displayName("createUniformsTask").cache("once");
3963
- const updateUniformsTask = work.task((get, work) => {
3964
- const terrainUniformsContext = get(createUniformsTask);
3965
- const rootSizeVal = get(rootSize);
3966
- const rootOrigin = get(origin);
3967
- const innerTileSegmentsVal = get(innerTileSegments);
3968
- const skirtScaleVal = get(skirtScale);
3969
- const elevationScaleVal = get(elevationScale);
3970
- const radiusVal = get(radius);
3971
- return work(() => {
3972
- terrainUniformsContext.uRootSize.value = rootSizeVal;
3973
- terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
3974
- rootOrigin.x,
3975
- rootOrigin.y,
3976
- rootOrigin.z
3977
- );
3978
- terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
3979
- terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
3980
- terrainUniformsContext.uElevationScale.value = elevationScaleVal;
3981
- terrainUniformsContext.uRadius.value = radiusVal;
3982
- return terrainUniformsContext;
3983
- });
3984
- }).displayName("updateUniformsTask");
3985
-
3986
- const createElevationFieldContextTask = work.task((get, work) => {
4057
+ function runTileBoundsReduction(renderer, boundsContext, leafCount) {
4058
+ if (leafCount > 0) {
4059
+ renderer.compute(boundsContext.kernel, [1, 1, leafCount]);
4060
+ }
4061
+ }
4062
+ const tileBoundsContextTask = work.task((get, work) => {
4063
+ const elevationFieldContext = get(createElevationFieldContextTask);
4064
+ const maxNodesValue = get(maxNodes);
3987
4065
  const edgeVertexCount = get(innerTileSegments) + 3;
3988
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
3989
- const totalElements = get(maxNodes) * verticesPerNode;
3990
4066
  return work(() => {
3991
- const data = new Float32Array(totalElements);
4067
+ const floatCount = maxNodesValue * TILE_BOUNDS_FLOATS_PER_TILE;
4068
+ const data = new Float32Array(floatCount);
3992
4069
  const attribute = new webgpu.StorageBufferAttribute(data, 1);
3993
- attribute.name = "elevationField";
3994
- const node = tsl.storage(attribute, "float", totalElements).setName("elevationField");
3995
- return {
3996
- data,
3997
- attribute,
3998
- node
3999
- };
4000
- });
4001
- }).displayName("createElevationFieldContextTask");
4002
- const tileNodesTask = work.task((get, work) => {
4003
- const leafStorage = get(leafStorageTask);
4004
- const uniforms = get(updateUniformsTask);
4005
- const topology = get(topologyTask);
4006
- return work(() => {
4007
- return createTileCompute(leafStorage, uniforms, topology.projection);
4008
- });
4009
- }).displayName("tileNodesTask");
4010
- const elevationFieldStageTask = work.task((get, work) => {
4011
- const tile = get(tileNodesTask);
4012
- const uniforms = get(updateUniformsTask);
4013
- const elevationFieldContext = get(createElevationFieldContextTask);
4014
- const userElevationFn = get(elevationFn);
4015
- return work(() => {
4016
- const heightFn = createElevationFunction(userElevationFn);
4017
- const heightWriteFn = createElevation(tile, uniforms, heightFn);
4018
- return [
4019
- (nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
4020
- const height = heightWriteFn(nodeIndex, localCoordinates);
4021
- elevationFieldContext.node.element(globalVertexIndex).assign(height);
4022
- }
4023
- ];
4070
+ attribute.name = "tileBounds";
4071
+ const node = tsl.storage(attribute, "float", floatCount).setName(
4072
+ "tileBounds"
4073
+ );
4074
+ const verticesPerNode = edgeVertexCount * edgeVertexCount;
4075
+ const kernel = buildReductionKernel(
4076
+ elevationFieldContext.node,
4077
+ node,
4078
+ verticesPerNode,
4079
+ edgeVertexCount
4080
+ );
4081
+ return { data, attribute, node, kernel };
4024
4082
  });
4025
- }).displayName("elevationFieldStageTask");
4083
+ }).displayName("tileBoundsContextTask");
4026
4084
 
4027
4085
  const createTerrainFieldTextureTask = work.task(
4028
4086
  (get, work, { resources }) => {
@@ -4047,6 +4105,7 @@ const terrainFieldStageTask = work.task((get, work) => {
4047
4105
  const tile = get(tileNodesTask);
4048
4106
  const uniforms = get(updateUniformsTask);
4049
4107
  const topology = get(topologyTask);
4108
+ const boundsContext = get(tileBoundsContextTask);
4050
4109
  return work(() => {
4051
4110
  const computeNormal = topology.projection.gpu.createFieldNormal({
4052
4111
  elevationFieldNode: elevationFieldContext.node,
@@ -4061,12 +4120,13 @@ const terrainFieldStageTask = work.task((get, work) => {
4061
4120
  const iy = tsl.int(localCoordinates.y);
4062
4121
  const height = elevationFieldContext.node.element(globalVertexIndex);
4063
4122
  const normal = computeNormal(nodeIndex, ix, iy);
4123
+ const { packMin, packMax } = loadTilePackBounds(boundsContext.node, nodeIndex);
4064
4124
  storeTerrainField(
4065
4125
  terrainFieldStorage,
4066
4126
  ix,
4067
4127
  iy,
4068
4128
  nodeIndex,
4069
- packTerrainFieldSample(height, normal)
4129
+ packNormalizedTerrainFieldSample(height, normal, packMin, packMax)
4070
4130
  );
4071
4131
  }
4072
4132
  ];
@@ -4078,23 +4138,28 @@ function createComputePipelineTasks(leafStageTask) {
4078
4138
  const compile = work.task((get, work) => {
4079
4139
  const pipeline = get(leafStageTask);
4080
4140
  const edgeVertexCount = get(innerTileSegments) + 3;
4141
+ const boundsContext = get(tileBoundsContextTask);
4081
4142
  return work(
4082
4143
  () => compileComputePipeline(pipeline, edgeVertexCount, {
4083
- })
4144
+ midPipelineExecute: (renderer, instanceCount) => {
4145
+ runTileBoundsReduction(renderer, boundsContext, instanceCount);
4146
+ }
4147
+ })
4084
4148
  );
4085
4149
  }).displayName("compileComputeTask");
4086
- const execute = work.task(
4087
- (get, work, { resources }) => {
4088
- const { execute: run } = get(compile);
4089
- const leafState = get(leafGpuBufferTask);
4090
- return work(
4091
- () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4092
- }
4093
- );
4094
- }
4095
- ).displayName("executeComputeTask").lane("gpu");
4150
+ const execute = work.task((get, work, { resources }) => {
4151
+ const { execute: run } = get(compile);
4152
+ const leafState = get(leafGpuBufferTask);
4153
+ return work(() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
4154
+ });
4155
+ }).displayName("executeComputeTask").lane("gpu");
4096
4156
  return { compile, execute };
4097
4157
  }
4158
+ const tileBoundsReductionTask = work.task((get, work) => {
4159
+ get(executeComputeTask);
4160
+ const boundsContext = get(tileBoundsContextTask);
4161
+ return work(() => boundsContext);
4162
+ }).displayName("tileBoundsReductionTask").lane("gpu");
4098
4163
 
4099
4164
  const gpuSpatialIndexStorageTask = work.task((get, work) => {
4100
4165
  const maxNodesValue = get(maxNodes);
@@ -4112,6 +4177,7 @@ const gpuSpatialIndexUploadTask = work.task((get, work) => {
4112
4177
 
4113
4178
  const createTerrainSamplerTask = work.task((get, work) => {
4114
4179
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4180
+ const tileBoundsContext = get(tileBoundsContextTask);
4115
4181
  const spatialIndex = get(gpuSpatialIndexStorageTask);
4116
4182
  const uniforms = get(updateUniformsTask);
4117
4183
  const elevationCallback = get(elevationFn);
@@ -4120,6 +4186,7 @@ const createTerrainSamplerTask = work.task((get, work) => {
4120
4186
  return work(
4121
4187
  () => createTerrainSampler({
4122
4188
  terrainFieldStorage,
4189
+ tileBoundsNode: tileBoundsContext.node,
4123
4190
  spatialIndex,
4124
4191
  uniforms,
4125
4192
  elevationCallback,
@@ -4133,12 +4200,14 @@ const positionNodeTask = work.task((get, work) => {
4133
4200
  const leafStorage = get(leafStorageTask);
4134
4201
  const terrainUniforms = get(updateUniformsTask);
4135
4202
  const terrainFieldStorage = get(createTerrainFieldTextureTask);
4203
+ const tileBoundsContext = get(tileBoundsContextTask);
4136
4204
  const topology = get(topologyTask);
4137
4205
  return work(
4138
4206
  () => topology.projection.gpu.renderVertexPosition({
4139
4207
  leafStorage,
4140
4208
  uniforms: terrainUniforms,
4141
- terrainFieldStorage
4209
+ terrainFieldStorage,
4210
+ tileBoundsNode: tileBoundsContext.node
4142
4211
  })
4143
4212
  );
4144
4213
  }).displayName("positionNodeTask");
@@ -4259,6 +4328,17 @@ function terrainGraph() {
4259
4328
  return g;
4260
4329
  }
4261
4330
 
4331
+ const decodeUint16RG = tsl.Fn(
4332
+ ([sample]) => sample.r.mul(tsl.float(256)).add(sample.g).div(tsl.float(257))
4333
+ );
4334
+ const sampleHeightmapMeters = tsl.Fn(
4335
+ ([heightmapTexture, uv, minM, _maxM, rangeM]) => {
4336
+ const sample = tsl.texture(heightmapTexture, uv);
4337
+ const normalized = decodeUint16RG(sample);
4338
+ return minM.add(normalized.mul(rangeM));
4339
+ }
4340
+ );
4341
+
4262
4342
  const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
4263
4343
  return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
4264
4344
  });
@@ -4316,6 +4396,12 @@ exports.AtlasBackend = AtlasBackend;
4316
4396
  exports.CUBE_FACES = CUBE_FACES;
4317
4397
  exports.CUBE_FACE_COUNT = CUBE_FACE_COUNT;
4318
4398
  exports.Dir = Dir;
4399
+ exports.TERRAIN_FIELD_PACK_EPSILON = TERRAIN_FIELD_PACK_EPSILON;
4400
+ exports.TILE_BOUNDS_FLOATS_PER_TILE = TILE_BOUNDS_FLOATS_PER_TILE;
4401
+ exports.TILE_BOUNDS_LOD_MAX_OFFSET = TILE_BOUNDS_LOD_MAX_OFFSET;
4402
+ exports.TILE_BOUNDS_LOD_MIN_OFFSET = TILE_BOUNDS_LOD_MIN_OFFSET;
4403
+ exports.TILE_BOUNDS_PACK_MAX_OFFSET = TILE_BOUNDS_PACK_MAX_OFFSET;
4404
+ exports.TILE_BOUNDS_PACK_MIN_OFFSET = TILE_BOUNDS_PACK_MIN_OFFSET;
4319
4405
  exports.TerrainGeometry = TerrainGeometry;
4320
4406
  exports.TerrainMesh = TerrainMesh;
4321
4407
  exports.U32_EMPTY = U32_EMPTY;
@@ -4352,6 +4438,8 @@ exports.cubeFaceDirection = cubeFaceDirection;
4352
4438
  exports.cubeFaceFromDirection = cubeFaceFromDirection;
4353
4439
  exports.cubeFacePoint = cubeFacePoint;
4354
4440
  exports.cubeFaceUVFromDirection = cubeFaceUVFromDirection;
4441
+ exports.decodeUint16RG = decodeUint16RG;
4442
+ exports.denormalizeTerrainFieldElevation = denormalizeTerrainFieldElevation;
4355
4443
  exports.deriveNormalZ = deriveNormalZ;
4356
4444
  exports.directionToFace = directionToFace;
4357
4445
  exports.directionToFaceUV = directionToFaceUV;
@@ -4374,9 +4462,11 @@ exports.leafStorageTask = leafStorageTask;
4374
4462
  exports.loadTerrainField = loadTerrainField;
4375
4463
  exports.loadTerrainFieldElevation = loadTerrainFieldElevation;
4376
4464
  exports.loadTerrainFieldNormal = loadTerrainFieldNormal;
4465
+ exports.loadTilePackBounds = loadTilePackBounds;
4377
4466
  exports.maxLevel = maxLevel;
4378
4467
  exports.maxNodes = maxNodes;
4379
4468
  exports.origin = origin;
4469
+ exports.packNormalizedTerrainFieldSample = packNormalizedTerrainFieldSample;
4380
4470
  exports.packTerrainFieldSample = packTerrainFieldSample;
4381
4471
  exports.positionNodeTask = positionNodeTask;
4382
4472
  exports.positionToTorusParams = positionToTorusParams;
@@ -4387,6 +4477,8 @@ exports.radius = radius;
4387
4477
  exports.resetLeafSet = resetLeafSet;
4388
4478
  exports.resetSeamTable = resetSeamTable;
4389
4479
  exports.rootSize = rootSize;
4480
+ exports.runTileBoundsReduction = runTileBoundsReduction;
4481
+ exports.sampleHeightmapMeters = sampleHeightmapMeters;
4390
4482
  exports.sampleTerrainField = sampleTerrainField;
4391
4483
  exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
4392
4484
  exports.skirtScale = skirtScale;
@@ -4401,6 +4493,8 @@ exports.terrainRaycastTask = terrainRaycastTask;
4401
4493
  exports.terrainReadbackTask = terrainReadbackTask;
4402
4494
  exports.terrainTasks = terrainTasks;
4403
4495
  exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
4496
+ exports.tileBoundsContextTask = tileBoundsContextTask;
4497
+ exports.tileBoundsReductionTask = tileBoundsReductionTask;
4404
4498
  exports.tileNodesTask = tileNodesTask;
4405
4499
  exports.topology = topology;
4406
4500
  exports.topologyTask = topologyTask;