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

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
@@ -935,20 +935,15 @@ function beginUpdate(state, topology, params) {
935
935
 
936
936
  function shouldSplit(bounds, level, maxLevel, params) {
937
937
  if (level >= maxLevel) return false;
938
- const mode = params.mode ?? "distance";
939
938
  const cx = bounds.cx;
940
939
  const cy = bounds.cy;
941
940
  const cz = bounds.cz;
942
941
  const distSq = cx * cx + cy * cy + cz * cz;
943
942
  const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
944
- if (mode === "screen") {
945
- const proj = params.projectionFactor ?? 0;
946
- const target = params.targetPixels ?? 0;
947
- if (proj <= 0 || target <= 0) {
948
- const f2 = params.distanceFactor ?? 2;
949
- const threshold2 = bounds.r * f2;
950
- return safeDistSq < threshold2 * threshold2;
951
- }
943
+ if (params.mode === "screen") {
944
+ const proj = params.projectionFactor;
945
+ const target = params.targetPixels;
946
+ if (proj <= 0 || target <= 0) return false;
952
947
  const left = bounds.r * bounds.r * proj * proj;
953
948
  const right = safeDistSq * target * target;
954
949
  return left > right;
@@ -982,7 +977,7 @@ function refineLeaves(state, topology, params, outLeaves) {
982
977
  let elevationRange;
983
978
  if (params.tileElevationRange) {
984
979
  const range = state.scratchElevationRange;
985
- if (params.tileElevationRange(space, level, x, y, range)) {
980
+ if (params.tileElevationRange(tile, range)) {
986
981
  elevationRange = range;
987
982
  }
988
983
  }
@@ -1085,19 +1080,9 @@ function balance2to1(state, topology, params, leaves) {
1085
1080
  }
1086
1081
 
1087
1082
  function update(state, topology, params, outLeaves) {
1088
- const cam = params.cameraOrigin;
1089
- const elevation = params.elevationAtCameraXZ ?? 0;
1090
- const origX = cam.x;
1091
- const origY = cam.y;
1092
- const origZ = cam.z;
1093
- topology.projection.cpu.cameraSurfaceOffset(cam, elevation);
1094
1083
  beginUpdate(state, topology, params);
1095
1084
  const leaves = refineLeaves(state, topology, params, outLeaves);
1096
- const result = balance2to1(state, topology, params, leaves);
1097
- cam.x = origX;
1098
- cam.y = origY;
1099
- cam.z = origZ;
1100
- return result;
1085
+ return balance2to1(state, topology, params, leaves);
1101
1086
  }
1102
1087
 
1103
1088
  const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
@@ -1498,26 +1483,6 @@ function cpuRaycast(query, ray, config, options) {
1498
1483
  distance: ray.origin.distanceTo(point)
1499
1484
  };
1500
1485
  }
1501
- function cpuRaycastBoundsOnly(ray, config, options) {
1502
- const bounds = getTerrainBounds(config);
1503
- const planeY = (config.minY + config.maxY) * 0.5;
1504
- const dirY = ray.direction.y;
1505
- if (Math.abs(dirY) < 1e-8) return null;
1506
- const t = (planeY - ray.origin.y) / dirY;
1507
- if (t < 0) return null;
1508
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1509
- if (t > maxDistance) return null;
1510
- const point = new Vector3();
1511
- ray.at(t, point);
1512
- if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
1513
- return null;
1514
- }
1515
- return {
1516
- position: point,
1517
- normal: new Vector3(0, 1, 0),
1518
- distance: ray.origin.distanceTo(point)
1519
- };
1520
- }
1521
1486
  function intersectRaySphere(ray, cx, cy, cz, radius) {
1522
1487
  const ox = ray.origin.x - cx;
1523
1488
  const oy = ray.origin.y - cy;
@@ -1583,22 +1548,6 @@ function cubeSphereRaycast(query, ray, params, options) {
1583
1548
  distance: ray.origin.distanceTo(sample.position)
1584
1549
  };
1585
1550
  }
1586
- function cubeSphereRaycastBoundsOnly(ray, params, options) {
1587
- const shell = intersectRaySphere(ray, params.centerX, params.centerY, params.centerZ, params.radius);
1588
- if (!shell) return null;
1589
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1590
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1591
- if (t < 0 || t > maxDistance) return null;
1592
- const point = new Vector3();
1593
- ray.at(t, point);
1594
- const normal = new Vector3(
1595
- point.x - params.centerX,
1596
- point.y - params.centerY,
1597
- point.z - params.centerZ
1598
- ).normalize();
1599
- if (params.invert) normal.negate();
1600
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1601
- }
1602
1551
  function torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams) {
1603
1552
  positionToTorusParams(
1604
1553
  px,
@@ -1653,28 +1602,6 @@ function torusRaycast(query, ray, params, options) {
1653
1602
  distance: ray.origin.distanceTo(sample.position)
1654
1603
  };
1655
1604
  }
1656
- function torusRaycastBoundsOnly(ray, params, options) {
1657
- const shell = intersectRaySphere(
1658
- ray,
1659
- params.centerX,
1660
- params.centerY,
1661
- params.centerZ,
1662
- params.outerRadius
1663
- );
1664
- if (!shell) return null;
1665
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1666
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1667
- if (t < 0 || t > maxDistance) return null;
1668
- const point = new Vector3();
1669
- ray.at(t, point);
1670
- const normal = new Vector3(
1671
- point.x - params.centerX,
1672
- point.y - params.centerY,
1673
- point.z - params.centerZ
1674
- ).normalize();
1675
- if (params.invert) normal.negate();
1676
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1677
- }
1678
1605
 
1679
1606
  function createTerrainQuery(cache) {
1680
1607
  return {
@@ -1800,9 +1727,6 @@ function createFlatProjection() {
1800
1727
  }
1801
1728
  },
1802
1729
  cpu: {
1803
- cameraSurfaceOffset(cam, elevation) {
1804
- cam.y -= elevation;
1805
- },
1806
1730
  createSurfaceOps() {
1807
1731
  return null;
1808
1732
  },
@@ -1811,26 +1735,44 @@ function createFlatProjection() {
1811
1735
  },
1812
1736
  raycast(ctx) {
1813
1737
  const { ray, options, terrainQuery, config } = ctx;
1814
- if (terrainQuery) {
1815
- const precise = cpuRaycast(terrainQuery, ray, config, options);
1816
- if (precise) return precise;
1817
- }
1818
- const coarse = cpuRaycastBoundsOnly(ray, config, options);
1819
- if (coarse && terrainQuery) {
1820
- const sample = terrainQuery.sampleTerrain(coarse.position.x, coarse.position.z);
1821
- if (sample.valid) {
1822
- coarse.position.y = sample.elevation;
1823
- coarse.normal.copy(sample.normal);
1824
- }
1825
- }
1826
- return coarse;
1738
+ if (!terrainQuery) return null;
1739
+ return cpuRaycast(terrainQuery, ray, config, options);
1827
1740
  }
1828
1741
  }
1829
1742
  };
1830
1743
  }
1831
1744
 
1745
+ function boundingSphereFromPoints(px, py, pz, count, cameraOrigin, out) {
1746
+ let sumX = 0;
1747
+ let sumY = 0;
1748
+ let sumZ = 0;
1749
+ for (let i = 0; i < count; i++) {
1750
+ sumX += px[i];
1751
+ sumY += py[i];
1752
+ sumZ += pz[i];
1753
+ }
1754
+ const cX = sumX / count;
1755
+ const cY = sumY / count;
1756
+ const cZ = sumZ / count;
1757
+ let maxDistSq = 0;
1758
+ for (let i = 0; i < count; i++) {
1759
+ const dx = px[i] - cX;
1760
+ const dy = py[i] - cY;
1761
+ const dz = pz[i] - cZ;
1762
+ const dSq = dx * dx + dy * dy + dz * dz;
1763
+ if (dSq > maxDistSq) maxDistSq = dSq;
1764
+ }
1765
+ out.cx = cX - cameraOrigin.x;
1766
+ out.cy = cY - cameraOrigin.y;
1767
+ out.cz = cZ - cameraOrigin.z;
1768
+ out.r = Math.sqrt(maxDistSq);
1769
+ }
1770
+
1832
1771
  function createFlatTopology(cfg) {
1833
1772
  const halfRoot = 0.5 * cfg.rootSize;
1773
+ const px = new Float64Array(8);
1774
+ const py = new Float64Array(8);
1775
+ const pz = new Float64Array(8);
1834
1776
  const topology = {
1835
1777
  spaceCount: 1,
1836
1778
  maxRootCount: 1,
@@ -1870,15 +1812,26 @@ function createFlatTopology(cfg) {
1870
1812
  const size = cfg.rootSize * scale;
1871
1813
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1872
1814
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1873
- const centerX = minX + 0.5 * size;
1874
- const centerZ = minZ + 0.5 * size;
1875
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1876
- out.cx = centerX - cameraOrigin.x;
1877
- out.cy = centerY - cameraOrigin.y;
1878
- out.cz = centerZ - cameraOrigin.z;
1879
- const halfDiag = 0.7071067811865476 * size;
1880
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1881
- out.r = halfDiag + vertExtent;
1815
+ const maxX = minX + size;
1816
+ const maxZ = minZ + size;
1817
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1818
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1819
+ let pointCount = 0;
1820
+ for (let i = 0; i < 4; i++) {
1821
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1822
+ const cornerZ = i < 2 ? minZ : maxZ;
1823
+ px[pointCount] = cornerX;
1824
+ py[pointCount] = yLo;
1825
+ pz[pointCount] = cornerZ;
1826
+ pointCount += 1;
1827
+ if (elevationRange) {
1828
+ px[pointCount] = cornerX;
1829
+ py[pointCount] = yHi;
1830
+ pz[pointCount] = cornerZ;
1831
+ pointCount += 1;
1832
+ }
1833
+ }
1834
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1882
1835
  },
1883
1836
  rootTiles(_cameraOrigin, out) {
1884
1837
  const root = out[0];
@@ -1896,6 +1849,9 @@ function createInfiniteFlatTopology(cfg) {
1896
1849
  const halfRoot = 0.5 * cfg.rootSize;
1897
1850
  const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1898
1851
  const rootWidth = rootGridRadius * 2 + 1;
1852
+ const px = new Float64Array(8);
1853
+ const py = new Float64Array(8);
1854
+ const pz = new Float64Array(8);
1899
1855
  return {
1900
1856
  spaceCount: 1,
1901
1857
  maxRootCount: rootWidth * rootWidth,
@@ -1929,15 +1885,26 @@ function createInfiniteFlatTopology(cfg) {
1929
1885
  const size = cfg.rootSize * scale;
1930
1886
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1931
1887
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1932
- const centerX = minX + 0.5 * size;
1933
- const centerZ = minZ + 0.5 * size;
1934
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1935
- out.cx = centerX - cameraOrigin.x;
1936
- out.cy = centerY - cameraOrigin.y;
1937
- out.cz = centerZ - cameraOrigin.z;
1938
- const halfDiag = 0.7071067811865476 * size;
1939
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1940
- out.r = halfDiag + vertExtent;
1888
+ const maxX = minX + size;
1889
+ const maxZ = minZ + size;
1890
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1891
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1892
+ let pointCount = 0;
1893
+ for (let i = 0; i < 4; i++) {
1894
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1895
+ const cornerZ = i < 2 ? minZ : maxZ;
1896
+ px[pointCount] = cornerX;
1897
+ py[pointCount] = yLo;
1898
+ pz[pointCount] = cornerZ;
1899
+ pointCount += 1;
1900
+ if (elevationRange) {
1901
+ px[pointCount] = cornerX;
1902
+ py[pointCount] = yHi;
1903
+ pz[pointCount] = cornerZ;
1904
+ pointCount += 1;
1905
+ }
1906
+ }
1907
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1941
1908
  },
1942
1909
  rootTiles(cameraOrigin, out) {
1943
1910
  const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
@@ -2473,7 +2440,6 @@ function createCubeSphereProjection(config) {
2473
2440
  const nx = dx / len;
2474
2441
  const ny = dy / len;
2475
2442
  const nz = dz / len;
2476
- const dirSign = invert ? -1 : 1;
2477
2443
  dirScratch[0] = nx;
2478
2444
  dirScratch[1] = ny;
2479
2445
  dirScratch[2] = nz;
@@ -2482,9 +2448,9 @@ function createCubeSphereProjection(config) {
2482
2448
  out.space = face;
2483
2449
  out.u = uvScratch[0];
2484
2450
  out.v = uvScratch[1];
2485
- out.dirX = nx * dirSign;
2486
- out.dirY = ny * dirSign;
2487
- out.dirZ = nz * dirSign;
2451
+ out.dirX = nx;
2452
+ out.dirY = ny;
2453
+ out.dirZ = nz;
2488
2454
  return true;
2489
2455
  },
2490
2456
  surfacePosition(key, elevation, outVec) {
@@ -2517,7 +2483,8 @@ function createCubeSphereProjection(config) {
2517
2483
  let nx = tuy * tvz - tuz * tvy;
2518
2484
  let ny = tuz * tvx - tux * tvz;
2519
2485
  let nz = tux * tvy - tuy * tvx;
2520
- if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
2486
+ const outwardSign = invert ? -1 : 1;
2487
+ if ((nx * key.dirX + ny * key.dirY + nz * key.dirZ) * outwardSign < 0) {
2521
2488
  nx = -nx;
2522
2489
  ny = -ny;
2523
2490
  nz = -nz;
@@ -2571,19 +2538,6 @@ function createCubeSphereProjection(config) {
2571
2538
  augmentSampler: augmentCubeSphereSampler
2572
2539
  },
2573
2540
  cpu: {
2574
- cameraSurfaceOffset(cam, elevation) {
2575
- const dx = cam.x - center.x;
2576
- const dy = cam.y - center.y;
2577
- const dz = cam.z - center.z;
2578
- const len = Math.hypot(dx, dy, dz);
2579
- if (len > 1e-12) {
2580
- const sign = invert ? 1 : -1;
2581
- const inv = sign * elevation / len;
2582
- cam.x += dx * inv;
2583
- cam.y += dy * inv;
2584
- cam.z += dz * inv;
2585
- }
2586
- },
2587
2541
  createSurfaceOps() {
2588
2542
  return surfaceOps;
2589
2543
  },
@@ -2594,6 +2548,7 @@ function createCubeSphereProjection(config) {
2594
2548
  return { query, surfaceQuery, sphereQuery };
2595
2549
  },
2596
2550
  raycast(ctx) {
2551
+ if (!ctx.sphereQuery) return null;
2597
2552
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2598
2553
  const dispMax = range ? Math.max(0, range.max - center.y) : radius * 0.1;
2599
2554
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING$1;
@@ -2605,11 +2560,7 @@ function createCubeSphereProjection(config) {
2605
2560
  maxRadius: radius + outerPadding,
2606
2561
  invert
2607
2562
  };
2608
- if (ctx.sphereQuery) {
2609
- const precise = cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2610
- if (precise) return precise;
2611
- }
2612
- return cubeSphereRaycastBoundsOnly(ctx.ray, params, ctx.options);
2563
+ return cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2613
2564
  }
2614
2565
  }
2615
2566
  };
@@ -2700,8 +2651,6 @@ function createCubeSphereTopology(cfg) {
2700
2651
  spaceCount: 6,
2701
2652
  maxRootCount: 6,
2702
2653
  projection: createCubeSphereProjection({ radius, center, invert: cfg.invert }),
2703
- radius,
2704
- center,
2705
2654
  neighborSameLevel(tile, dir, out) {
2706
2655
  const level = tile.level;
2707
2656
  const n = 1 << level;
@@ -2738,48 +2687,29 @@ function createCubeSphereTopology(cfg) {
2738
2687
  const u1 = (tile.x + 1) / n;
2739
2688
  const v0 = tile.y / n;
2740
2689
  const v1 = (tile.y + 1) / n;
2741
- const cornersU = [u0, u1, u0, u1];
2742
- const cornersV = [v0, v0, v1, v1];
2743
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2690
+ const shellLo = radius + (elevationRange ? elevationRange.min : 0);
2691
+ const shellHi = elevationRange ? radius + elevationRange.max : 0;
2744
2692
  let pointCount = 0;
2745
- let sumX = 0;
2746
- let sumY = 0;
2747
- let sumZ = 0;
2748
2693
  for (let i = 0; i < 4; i++) {
2749
- faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
2694
+ const u = (i & 1) === 0 ? u0 : u1;
2695
+ const v = i < 2 ? v0 : v1;
2696
+ faceUVToCube(tile.space, u, v, cube);
2750
2697
  const len = Math.hypot(cube[0], cube[1], cube[2]);
2751
2698
  const dirX = cube[0] / len;
2752
2699
  const dirY = cube[1] / len;
2753
2700
  const dirZ = cube[2] / len;
2754
- for (let di = 0; di < disps.length; di++) {
2755
- const shellRadius = radius + disps[di];
2756
- const sx = center.x + dirX * shellRadius;
2757
- const sy = center.y + dirY * shellRadius;
2758
- const sz = center.z + dirZ * shellRadius;
2759
- px[pointCount] = sx;
2760
- py[pointCount] = sy;
2761
- pz[pointCount] = sz;
2762
- sumX += sx;
2763
- sumY += sy;
2764
- sumZ += sz;
2701
+ px[pointCount] = center.x + dirX * shellLo;
2702
+ py[pointCount] = center.y + dirY * shellLo;
2703
+ pz[pointCount] = center.z + dirZ * shellLo;
2704
+ pointCount += 1;
2705
+ if (elevationRange) {
2706
+ px[pointCount] = center.x + dirX * shellHi;
2707
+ py[pointCount] = center.y + dirY * shellHi;
2708
+ pz[pointCount] = center.z + dirZ * shellHi;
2765
2709
  pointCount += 1;
2766
2710
  }
2767
2711
  }
2768
- const cX = sumX / pointCount;
2769
- const cY = sumY / pointCount;
2770
- const cZ = sumZ / pointCount;
2771
- let maxDistSq = 0;
2772
- for (let i = 0; i < pointCount; i++) {
2773
- const dx = px[i] - cX;
2774
- const dy = py[i] - cY;
2775
- const dz = pz[i] - cZ;
2776
- const dSq = dx * dx + dy * dy + dz * dz;
2777
- if (dSq > maxDistSq) maxDistSq = dSq;
2778
- }
2779
- out.cx = cX - cameraOrigin.x;
2780
- out.cy = cY - cameraOrigin.y;
2781
- out.cz = cZ - cameraOrigin.z;
2782
- out.r = Math.sqrt(maxDistSq);
2712
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
2783
2713
  },
2784
2714
  rootTiles(_cameraOrigin, out) {
2785
2715
  for (let s = 0; s < 6; s++) {
@@ -2950,13 +2880,6 @@ function createTorusProjection(config) {
2950
2880
  }
2951
2881
  },
2952
2882
  cpu: {
2953
- cameraSurfaceOffset(cam, elevation) {
2954
- positionToTorusParams(cam.x, cam.y, cam.z, majorRadius, center, params);
2955
- torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
2956
- cam.x -= normalScratch[0] * elevation;
2957
- cam.y -= normalScratch[1] * elevation;
2958
- cam.z -= normalScratch[2] * elevation;
2959
- },
2960
2883
  createSurfaceOps() {
2961
2884
  return surfaceOps;
2962
2885
  },
@@ -2966,6 +2889,7 @@ function createTorusProjection(config) {
2966
2889
  return { query, surfaceQuery, sphereQuery: null };
2967
2890
  },
2968
2891
  raycast(ctx) {
2892
+ if (!ctx.surfaceQuery) return null;
2969
2893
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2970
2894
  const dispMax = range ? Math.max(0, range.max - ctx.config.originY) : minorRadius * 0.5;
2971
2895
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING;
@@ -2978,11 +2902,7 @@ function createTorusProjection(config) {
2978
2902
  outerRadius: majorRadius + minorRadius + outerPadding,
2979
2903
  invert
2980
2904
  };
2981
- if (ctx.surfaceQuery) {
2982
- const precise = torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2983
- if (precise) return precise;
2984
- }
2985
- return torusRaycastBoundsOnly(ctx.ray, raycastParams, ctx.options);
2905
+ return torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2986
2906
  }
2987
2907
  }
2988
2908
  };
@@ -3015,8 +2935,6 @@ function createTorusTopology(cfg) {
3015
2935
  baseU,
3016
2936
  baseV
3017
2937
  }),
3018
- radius: majorRadius + minorRadius,
3019
- center,
3020
2938
  neighborSameLevel(tile, dir, out) {
3021
2939
  const { nU, nV } = levelResolution(tile.level);
3022
2940
  let nx = tile.x;
@@ -3047,42 +2965,28 @@ function createTorusTopology(cfg) {
3047
2965
  const v0 = tile.y / nV;
3048
2966
  const stepU = 1 / nU;
3049
2967
  const stepV = 1 / nV;
3050
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2968
+ const dispLo = elevationRange ? elevationRange.min : 0;
2969
+ const dispHi = elevationRange ? elevationRange.max : 0;
3051
2970
  let pointCount = 0;
3052
- let sumX = 0;
3053
- let sumY = 0;
3054
- let sumZ = 0;
3055
2971
  for (let sj = 0; sj <= 2; sj++) {
3056
2972
  for (let si = 0; si <= 2; si++) {
3057
2973
  const u = u0 + si * stepU / 2;
3058
2974
  const v = v0 + sj * stepV / 2;
3059
- for (let di = 0; di < disps.length; di++) {
3060
- torusUVToPoint(u, v, majorRadius, minorRadius, disps[di], center, corner, invert);
2975
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispLo, center, corner, invert);
2976
+ px[pointCount] = corner[0];
2977
+ py[pointCount] = corner[1];
2978
+ pz[pointCount] = corner[2];
2979
+ pointCount += 1;
2980
+ if (elevationRange) {
2981
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispHi, center, corner, invert);
3061
2982
  px[pointCount] = corner[0];
3062
2983
  py[pointCount] = corner[1];
3063
2984
  pz[pointCount] = corner[2];
3064
- sumX += corner[0];
3065
- sumY += corner[1];
3066
- sumZ += corner[2];
3067
2985
  pointCount += 1;
3068
2986
  }
3069
2987
  }
3070
2988
  }
3071
- const cX = sumX / pointCount;
3072
- const cY = sumY / pointCount;
3073
- const cZ = sumZ / pointCount;
3074
- let maxDistSq = 0;
3075
- for (let i = 0; i < pointCount; i++) {
3076
- const dx = px[i] - cX;
3077
- const dy = py[i] - cY;
3078
- const dz = pz[i] - cZ;
3079
- const dSq = dx * dx + dy * dy + dz * dz;
3080
- if (dSq > maxDistSq) maxDistSq = dSq;
3081
- }
3082
- out.cx = cX - cameraOrigin.x;
3083
- out.cy = cY - cameraOrigin.y;
3084
- out.cz = cZ - cameraOrigin.z;
3085
- out.r = Math.sqrt(maxDistSq);
2989
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
3086
2990
  },
3087
2991
  rootTiles(_cameraOrigin, out) {
3088
2992
  let count = 0;
@@ -3646,6 +3550,9 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3646
3550
  get hasSurface() {
3647
3551
  return surfaceOps !== null;
3648
3552
  },
3553
+ setSurfaceOps(nextSurfaceOps) {
3554
+ surfaceOps = nextSurfaceOps;
3555
+ },
3649
3556
  updateConfig(nextConfig) {
3650
3557
  config = nextConfig;
3651
3558
  shape.edgeVertexCount = config.innerTileSegments + 3;
@@ -3797,7 +3704,7 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3797
3704
  }
3798
3705
 
3799
3706
  const WGSIZE = 64;
3800
- function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
3707
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
3801
3708
  const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
3802
3709
  return Fn(() => {
3803
3710
  const sharedMin = workgroupArray("float", WGSIZE);
@@ -3809,10 +3716,17 @@ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
3809
3716
  const end = min(start.add(int(elemsPerThread)), int(verticesPerNode));
3810
3717
  const localMin = float(1e10).toVar("localMin");
3811
3718
  const localMax = float(-1e10).toVar("localMax");
3719
+ const edge = int(edgeVertexCount);
3720
+ const lastEdge = int(edgeVertexCount - 1);
3812
3721
  Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
3813
- const h = elevationFieldNode.element(baseOffset.add(i));
3814
- localMin.assign(min(localMin, h));
3815
- localMax.assign(max(localMax, h));
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
+ });
3816
3730
  });
3817
3731
  sharedMin.element(tid).assign(localMin);
3818
3732
  sharedMax.element(tid).assign(localMax);
@@ -3842,7 +3756,12 @@ const tileBoundsContextTask = task((get, work) => {
3842
3756
  "tileBounds"
3843
3757
  );
3844
3758
  const verticesPerNode = edgeVertexCount * edgeVertexCount;
3845
- const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
3759
+ const kernel = buildReductionKernel(
3760
+ elevationFieldContext.node,
3761
+ node,
3762
+ verticesPerNode,
3763
+ edgeVertexCount
3764
+ );
3846
3765
  return { data, attribute, node, kernel };
3847
3766
  });
3848
3767
  }).displayName("tileBoundsContextTask");
@@ -3872,6 +3791,7 @@ const terrainQueryTask = task((get, work) => {
3872
3791
  const projection = topologyValue.projection;
3873
3792
  return work((prev) => {
3874
3793
  const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projection.kind}`;
3794
+ const resolvedRadius = projection.radius ?? radiusValue;
3875
3795
  const configValues = {
3876
3796
  rootSize: rootSizeValue,
3877
3797
  originX: originValue.x,
@@ -3880,7 +3800,7 @@ const terrainQueryTask = task((get, work) => {
3880
3800
  innerTileSegments: innerTileSegmentsValue,
3881
3801
  elevationScale: elevationScaleValue,
3882
3802
  maxLevel: maxLevelValue,
3883
- radius: topologyValue.radius ?? radiusValue,
3803
+ radius: resolvedRadius,
3884
3804
  baseU: projection.baseResolution?.u ?? 1,
3885
3805
  baseV: projection.baseResolution?.v ?? 1
3886
3806
  };
@@ -3895,9 +3815,15 @@ const terrainQueryTask = task((get, work) => {
3895
3815
  query = runtime.query;
3896
3816
  surfaceQuery = runtime.surfaceQuery;
3897
3817
  sphereQuery = runtime.sphereQuery;
3818
+ } else if (prev?.projection !== projection) {
3819
+ cache.setSurfaceOps(projection.cpu.createSurfaceOps());
3820
+ const runtime = projection.cpu.createRuntimeQueries(cache);
3821
+ query = runtime.query;
3822
+ surfaceQuery = runtime.surfaceQuery;
3823
+ sphereQuery = runtime.sphereQuery;
3898
3824
  }
3899
3825
  cache.updateConfig(configValues);
3900
- return { cache, query, surfaceQuery, sphereQuery, shapeKey };
3826
+ return { cache, query, surfaceQuery, sphereQuery, shapeKey, projection };
3901
3827
  });
3902
3828
  }).displayName("terrainQueryTask");
3903
3829
  const terrainReadbackTask = task(
@@ -3944,27 +3870,19 @@ const quadtreeConfigTask = task((get, work) => {
3944
3870
  const quadtreeUpdateTask = task((get, work) => {
3945
3871
  const quadtreeConfig = get(quadtreeConfigTask);
3946
3872
  const quadtreeUpdateConfig = get(quadtreeUpdate);
3947
- const { query: terrainQuery, surfaceQuery, cache } = get(terrainQueryTask);
3873
+ const { cache } = get(terrainQueryTask);
3948
3874
  const elevationScaleValue = get(elevationScale);
3949
3875
  let outLeaves = void 0;
3950
- const cameraPosition = new Vector3();
3951
3876
  const elevationRangeScratch = { min: 0, max: 0 };
3952
- return work(() => {
3953
- const cam = quadtreeUpdateConfig.cameraOrigin;
3954
- if (surfaceQuery) {
3955
- cameraPosition.set(cam.x, cam.y, cam.z);
3956
- quadtreeUpdateConfig.elevationAtCameraXZ = surfaceQuery.getElevationByPosition(cameraPosition) ?? 0;
3957
- } else {
3958
- quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
3877
+ quadtreeUpdateConfig.tileElevationRange = (tile, out) => {
3878
+ if (!cache.getTileElevationRange(tile.space, tile.level, tile.x, tile.y, elevationRangeScratch)) {
3879
+ return false;
3959
3880
  }
3960
- quadtreeUpdateConfig.tileElevationRange = (space, level, x, y, out) => {
3961
- if (!cache.getTileElevationRange(space, level, x, y, elevationRangeScratch)) {
3962
- return false;
3963
- }
3964
- out.min = elevationRangeScratch.min * elevationScaleValue;
3965
- out.max = elevationRangeScratch.max * elevationScaleValue;
3966
- return true;
3967
- };
3881
+ out.min = elevationRangeScratch.min * elevationScaleValue;
3882
+ out.max = elevationRangeScratch.max * elevationScaleValue;
3883
+ return true;
3884
+ };
3885
+ return work(() => {
3968
3886
  outLeaves = update(
3969
3887
  quadtreeConfig.state,
3970
3888
  quadtreeConfig.topology,
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/kenjinp/hello-terrain.git"
8
8
  },
9
- "version": "0.0.0-alpha.12",
9
+ "version": "0.0.0-alpha.13",
10
10
  "type": "module",
11
11
  "main": "./dist/index.mjs",
12
12
  "module": "./dist/index.mjs",
@@ -29,9 +29,9 @@
29
29
  "mitata": "^1.0.34",
30
30
  "unbuild": "^3.5.0",
31
31
  "vitest": "^4.0.16",
32
+ "@config/oxfmt": "0.1.0",
32
33
  "@config/oxlint": "0.1.0",
33
- "@config/typescript": "0.1.0",
34
- "@config/oxfmt": "0.1.0"
34
+ "@config/typescript": "0.1.0"
35
35
  },
36
36
  "dependencies": {
37
37
  "@hello-terrain/work": "0.3.0"