react-native-cesium 1.2.17 → 1.2.19

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/android/CMakeLists.txt +1 -0
  3. package/android/build.gradle +1 -1
  4. package/android/gradle.properties +4 -4
  5. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +5 -15
  6. package/android/src/main/cpp/CesiumBridgeAndroid.h +2 -0
  7. package/android/src/main/cpp/vulkan/VulkanBackend.cpp +40 -4
  8. package/cpp/engine/CameraIntegrator.cpp +82 -47
  9. package/cpp/engine/CameraIntegrator.hpp +27 -8
  10. package/cpp/engine/CesiumEngine.cpp +152 -304
  11. package/cpp/engine/CesiumEngine.hpp +38 -28
  12. package/cpp/engine/EngineTunables.hpp +6 -5
  13. package/cpp/engine/GltfToMesh.cpp +14 -5
  14. package/cpp/engine/ResourcePreparer.cpp +3 -3
  15. package/cpp/engine/ResourcePreparer.hpp +1 -1
  16. package/cpp/engine/TerrainFloor.hpp +70 -0
  17. package/cpp/engine/TerrainHeight.hpp +113 -0
  18. package/cpp/engine/test/CameraIntegratorTest.cpp +264 -0
  19. package/ios/CesiumBridge.mm +7 -27
  20. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +7 -6
  21. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  22. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +1 -1
  23. package/nitrogen/generated/android/c++/JCameraState.hpp +1 -1
  24. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +1 -1
  25. package/nitrogen/generated/android/c++/JQuaternion.hpp +1 -1
  26. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +177 -131
  27. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.hpp +6 -1
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +1 -1
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +1 -1
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +0 -2
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CesiumMetrics.kt +0 -2
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +2 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +1 -1
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewManager.kt +26 -10
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewStateUpdater.kt +2 -2
  36. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +211 -161
  37. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +34 -335
  38. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +107 -77
  39. package/package.json +12 -12
  40. package/scripts/cesium/config.mjs +1 -1
  41. package/scripts/cesium/vcpkg-triplets/arm64-ios-simulator.cmake +3 -0
  42. package/scripts/cesium/vcpkg-triplets/arm64-ios.cmake +4 -0
  43. package/src/specs/cesium-view.nitro.ts +7 -6
@@ -4,7 +4,7 @@
4
4
  #endif
5
5
 
6
6
  #include "CesiumEngine.hpp"
7
- #include "GeoidConverter.hpp"
7
+ #include "TerrainHeight.hpp"
8
8
 
9
9
  #include <iostream>
10
10
 
@@ -37,7 +37,6 @@
37
37
  #include <algorithm>
38
38
  #include <cmath>
39
39
  #include <cstring>
40
- #include <limits>
41
40
  #include <thread>
42
41
 
43
42
 
@@ -404,9 +403,7 @@ void CesiumEngine::applyImageryOverlay(int64_t targetAssetId) {
404
403
  lastDrawSignature_ = 0;
405
404
  stableSigFrames_ = 0;
406
405
  cachedDraws_.clear();
407
- // floor cache is keyed on signature — drop it too so the next frame
408
- // re-scans before establishing a new entry.
409
- floorCacheValid_ = false;
406
+ heightCache_.valid = false;
410
407
  }
411
408
 
412
409
  void CesiumEngine::destroyTileset() {
@@ -422,8 +419,7 @@ void CesiumEngine::destroyTileset() {
422
419
  lastDrawSignature_ = 0;
423
420
  stableSigFrames_ = 0;
424
421
  cachedDraws_.clear();
425
- // P5: the cached value was keyed on TileGPUResources we just freed.
426
- floorCacheValid_ = false;
422
+ clearResidentTerrain();
427
423
  }
428
424
 
429
425
  void CesiumEngine::setImageryAssetId(int64_t assetId) {
@@ -445,6 +441,126 @@ inline uint64_t fnv1a64Mix(uint64_t h, uint64_t v) {
445
441
  }
446
442
  } // namespace
447
443
 
444
+ void CesiumEngine::clearResidentTerrain() {
445
+ residentTiles_.clear();
446
+ residentSignature_ = 0;
447
+ heightCache_.valid = false;
448
+ }
449
+
450
+ bool CesiumEngine::sampleTerrainEllipsoidMeters(double latitudeDeg,
451
+ double longitudeDeg,
452
+ float& outEllipsoidMeters) {
453
+ if (residentTiles_.empty() || !std::isfinite(latitudeDeg) || !std::isfinite(longitudeDeg)) {
454
+ return false;
455
+ }
456
+
457
+ // ~0.5 m cell. A hover reuses the sample; moving to the next cell rescans.
458
+ constexpr double kCellDeg = 4.5e-6;
459
+ const int latCell = static_cast<int>(std::floor(latitudeDeg / kCellDeg));
460
+ const int lonCell = static_cast<int>(std::floor(longitudeDeg / kCellDeg));
461
+ if (heightCache_.valid &&
462
+ heightCache_.signature == residentSignature_ &&
463
+ heightCache_.latCell == latCell &&
464
+ heightCache_.lonCell == lonCell) {
465
+ outEllipsoidMeters = heightCache_.height;
466
+ return true;
467
+ }
468
+
469
+ const double latRad = glm::radians(latitudeDeg);
470
+ const double lonRad = glm::radians(longitudeDeg);
471
+
472
+ const ResidentTerrainTile* tried[8] = {};
473
+ int triedCount = 0;
474
+
475
+ const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
476
+ const glm::dvec3 origin = ellipsoid.cartographicToCartesian(
477
+ CesiumGeospatial::Cartographic(lonRad, latRad, 0.0));
478
+ const glm::dmat4 enuToEcef =
479
+ CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(origin, ellipsoid);
480
+ const glm::dvec3 east = glm::normalize(glm::dvec3(enuToEcef[0]));
481
+ const glm::dvec3 north = glm::normalize(glm::dvec3(enuToEcef[1]));
482
+
483
+ auto contains = [&](const ResidentTerrainTile& tile) {
484
+ if (latRad < tile.south || latRad > tile.north) return false;
485
+ if (tile.west <= tile.east) return lonRad >= tile.west && lonRad <= tile.east;
486
+ return lonRad >= tile.west || lonRad <= tile.east;
487
+ };
488
+
489
+ auto sampleTile = [&](const ResidentTerrainTile& tile, float& outHeight) {
490
+ if (!tile.res) return false;
491
+ bool found = false;
492
+ float best = 0.0f;
493
+ for (const auto& prim : tile.res->primitives) {
494
+ const size_t nVerts = prim.localPositions.size();
495
+ if (nVerts == 0 || prim.altitudes.size() != nVerts) continue;
496
+ const size_t nTri = prim.indices.size() / 3;
497
+ for (size_t t = 0; t < nTri; ++t) {
498
+ const uint32_t i0 = prim.indices[t * 3];
499
+ const uint32_t i1 = prim.indices[t * 3 + 1];
500
+ const uint32_t i2 = prim.indices[t * 3 + 2];
501
+ if (i0 >= nVerts || i1 >= nVerts || i2 >= nVerts) continue;
502
+
503
+ auto project = [&](uint32_t i, double& e, double& n) {
504
+ const glm::dvec3 world = glm::dvec3(prim.localPositions[i]) + prim.rtcCenter;
505
+ const glm::dvec3 delta = world - origin;
506
+ e = glm::dot(delta, east);
507
+ n = glm::dot(delta, north);
508
+ };
509
+ double e0, n0, e1, n1, e2, n2;
510
+ project(i0, e0, n0);
511
+ project(i1, e1, n1);
512
+ project(i2, e2, n2);
513
+
514
+ float h = 0.0f;
515
+ if (!horizontalTriangleHeight(0.0, 0.0,
516
+ e0, n0, prim.altitudes[i0],
517
+ e1, n1, prim.altitudes[i1],
518
+ e2, n2, prim.altitudes[i2],
519
+ h)) {
520
+ continue;
521
+ }
522
+ if (!found || h > best) {
523
+ best = h;
524
+ found = true;
525
+ }
526
+ }
527
+ }
528
+ if (!found) return false;
529
+ outHeight = best;
530
+ return true;
531
+ };
532
+
533
+ // Finest containing tile first. If its mesh misses the point (a crack
534
+ // between tiles), try the next-finest. At most a handful of LODs overlap.
535
+ for (int attempt = 0; attempt < 8; ++attempt) {
536
+ const ResidentTerrainTile* best = nullptr;
537
+ for (const auto& tile : residentTiles_) {
538
+ if (!contains(tile)) continue;
539
+ bool skipped = false;
540
+ for (int i = 0; i < triedCount; ++i) {
541
+ if (tried[i] == &tile) { skipped = true; break; }
542
+ }
543
+ if (skipped) continue;
544
+ if (!best || tile.geometricError < best->geometricError) best = &tile;
545
+ }
546
+ if (!best) break;
547
+
548
+ float height = 0.0f;
549
+ if (sampleTile(*best, height)) {
550
+ heightCache_.valid = true;
551
+ heightCache_.tile = best->res;
552
+ heightCache_.latCell = latCell;
553
+ heightCache_.lonCell = lonCell;
554
+ heightCache_.signature = residentSignature_;
555
+ heightCache_.height = height;
556
+ outEllipsoidMeters = height;
557
+ return true;
558
+ }
559
+ if (triedCount < 8) tried[triedCount++] = best;
560
+ }
561
+ return false;
562
+ }
563
+
448
564
  void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
449
565
  result.localPositions.clear();
450
566
  result.altitudes.clear();
@@ -475,6 +591,7 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
475
591
  lastDrawSignature_ = 0;
476
592
  stableSigFrames_ = 0;
477
593
  cachedDraws_.clear();
594
+ clearResidentTerrain();
478
595
  lifecycle_.advanceFrame();
479
596
  return;
480
597
  }
@@ -604,116 +721,35 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
604
721
  stableSigFrames_ > tunables::kMaxFramesInFlight &&
605
722
  !cachedDraws_.empty();
606
723
 
607
- // ── Terrain floor tracking (for minAltitudeAboveTerrain clamp) ───────────
608
- // Computed once before the tile loop and updated per-tile inside it.
609
- // Only active when the feature is enabled to avoid any overhead otherwise.
610
- const bool trackTerrainFloor = (config_.minAltitudeAboveTerrain > 0.0f);
611
- const CameraParams camParams = trackTerrainFloor ? camera_.getParams() : CameraParams{};
612
- const double camLatRad = glm::radians(camParams.latitude);
613
- const double camLonRad = glm::radians(camParams.longitude);
614
-
615
- // Expected ground ellipsoid height at camera location (from geoid).
616
- // Used to filter out elevated structures (buildings) during floor sampling.
617
- const double expectedGroundEllipsoid = trackTerrainFloor
618
- ? mslToEllipsoidMeters(camParams.latitude, camParams.longitude, 0.0)
619
- : 0.0;
620
-
621
- float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
622
- double bestHorizDistSq = std::numeric_limits<double>::max();
623
- bool floorScanFound = false;
624
-
625
- // Horizontal search radius for floor sampling.
626
- // Increased to 200m to ensure robust sampling across terrain LODs and camera speeds.
627
- constexpr double kFloorSearchRadiusM = 200.0;
628
- constexpr double kFloorSearchRadiusSq = kFloorSearchRadiusM * kFloorSearchRadiusM;
629
-
630
- // ── floor-scan cache lookup ─────────────────────────────────────────
631
- // Skip the per-vertex scan when the camera has moved less than ~10 cm in
632
- // both lat and lon AND the visible primitive set is unchanged (same
633
- // geometry signature). The cached value is only trusted for non-zero
634
- // signatures — sig==0 means a fade or rebuild is in flight, in which case
635
- // we always re-scan and refresh the cache afterwards.
636
- bool floorScanFromCache = false;
637
- if (trackTerrainFloor && floorCacheValid_ && signature != 0ULL &&
638
- signature == floorCacheSignature_) {
639
- const double dLon = std::abs(camParams.longitude - floorCacheCamLonDeg_);
640
- const double dLat = std::abs(camParams.latitude - floorCacheCamLatDeg_);
641
- if (dLon < 1e-6 && dLat < 1e-6) {
642
- bestTerrainFloor = floorCacheValue_;
643
- floorScanFromCache = true;
644
- }
645
- }
646
- // When tracking is off, suppress the per-tile scan branches (legacy
647
- // behaviour already gated on trackTerrainFloor; this just keeps the cache
648
- // semantics aligned).
649
- const bool runFloorScan = trackTerrainFloor && !floorScanFromCache;
650
-
651
- // ENU basis at the camera — horizontal distance to each terrain vertex.
652
- glm::dvec3 floorEast(1.0, 0.0, 0.0);
653
- glm::dvec3 floorNorth(0.0, 1.0, 0.0);
654
- glm::dvec3 floorScanPos = cameraPos; // Always scan directly below camera
655
- glm::dvec3 floorScanAheadPos = cameraPos; // Also scan ahead if moving
656
- bool useLookAhead = false;
657
-
658
- if (runFloorScan) {
659
- const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
660
- const glm::dmat4 enuToEcef =
661
- CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(
662
- cameraPos, ellipsoid);
663
- floorEast = glm::normalize(glm::dvec3(enuToEcef[0]));
664
- floorNorth = glm::normalize(glm::dvec3(enuToEcef[1]));
665
-
666
- // ── Look-ahead terrain sampling ──────────────────────────────────────
667
- // Sample terrain BOTH below camera AND ahead. Use the maximum height to
668
- // prevent clipping through terrain between current position and look-ahead.
669
- if (floorCacheValid_) {
670
- // Convert previous lat/lon to ECEF to get movement vector
671
- const double prevLonRad = floorCacheCamLonDeg_ * M_PI / 180.0;
672
- const double prevLatRad = floorCacheCamLatDeg_ * M_PI / 180.0;
673
- const glm::dvec3 prevCamPos = ellipsoid.cartographicToCartesian(
674
- CesiumGeospatial::Cartographic(prevLonRad, prevLatRad, camParams.altitude));
675
-
676
- // Calculate horizontal movement since last frame
677
- const glm::dvec3 movement3D = cameraPos - prevCamPos;
678
- const double moveEast = glm::dot(movement3D, floorEast);
679
- const double moveNorth = glm::dot(movement3D, floorNorth);
680
- const double horizMovement = std::sqrt(moveEast * moveEast + moveNorth * moveNorth);
681
-
682
- // Look ahead by 5x recent movement (minimum 10m, max 100m)
683
- constexpr double kMinLookAhead = 10.0; // meters
684
- constexpr double kMaxLookAhead = 100.0; // meters
685
- constexpr double kLookAheadMultiplier = 5.0;
686
-
687
- double lookAheadDist = std::clamp(
688
- horizMovement * kLookAheadMultiplier,
689
- kMinLookAhead,
690
- kMaxLookAhead);
691
-
692
- if (horizMovement > 0.1) { // Only look ahead if actually moving
693
- // Project sampling point forward in direction of movement
694
- const glm::dvec3 moveDir = glm::normalize(
695
- floorEast * moveEast + floorNorth * moveNorth);
696
- floorScanAheadPos = cameraPos + moveDir * lookAheadDist;
697
- useLookAhead = true;
698
- }
699
- }
700
- }
701
-
702
- // Convert floor scan positions to lat/lon for tile bounds checking
703
- // Camera position (always used)
704
- double floorScanLonRad = camLonRad;
705
- double floorScanLatRad = camLatRad;
706
-
707
- // Look-ahead position (used when moving)
708
- double floorScanAheadLonRad = camLonRad;
709
- double floorScanAheadLatRad = camLatRad;
710
- if (runFloorScan && useLookAhead) {
711
- const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
712
- const auto scanCartographic = ellipsoid.cartesianToCartographic(floorScanAheadPos);
713
- if (scanCartographic) {
714
- floorScanAheadLonRad = scanCartographic->longitude;
715
- floorScanAheadLatRad = scanCartographic->latitude;
724
+ // Capture the tiles this frame actually drew. The next render samples
725
+ // height from this list before updateViewGroup can release it. Bounds and
726
+ // geometric error only — no per-vertex work on the draw path.
727
+ if (noVisibleTerrain) {
728
+ clearResidentTerrain();
729
+ } else {
730
+ residentTiles_.clear();
731
+ residentTiles_.reserve(updateResult.tilesToRenderThisFrame.size());
732
+ for (const auto& tile : updateResult.tilesToRenderThisFrame) {
733
+ if (!tile) continue;
734
+ const auto* rc = tile->getContent().getRenderContent();
735
+ if (!rc) continue;
736
+ const auto* res = static_cast<const TileGPUResources*>(rc->getRenderResources());
737
+ if (!res || res->primitives.empty()) continue;
738
+ const CesiumGeospatial::BoundingRegion* region =
739
+ Cesium3DTilesSelection::getBoundingRegionFromBoundingVolume(tile->getBoundingVolume());
740
+ if (!region) continue;
741
+ const auto& rect = region->getRectangle();
742
+ ResidentTerrainTile entry;
743
+ entry.res = res;
744
+ entry.geometricError = tile->getGeometricError();
745
+ entry.west = rect.getWest();
746
+ entry.south = rect.getSouth();
747
+ entry.east = rect.getEast();
748
+ entry.north = rect.getNorth();
749
+ residentTiles_.push_back(entry);
716
750
  }
751
+ residentSignature_ = signature;
752
+ if (heightCache_.signature != residentSignature_) heightCache_.valid = false;
717
753
  }
718
754
 
719
755
  // ── fast path ────────────────────────────────────────────────────────
@@ -726,92 +762,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
726
762
  // detect this via result.geometrySignature and use their cached slot data.
727
763
  if (canFastPath) {
728
764
  result.draws.reserve(cachedDraws_.size());
729
- const TileGPUResources* lastScanRes = nullptr;
730
765
  for (const auto& c : cachedDraws_) {
731
766
  lifecycle_.stampTileUsed(const_cast<TileGPUResources*>(c.res));
732
767
 
733
- // Floor scan (run once per res so multi-primitive tiles aren't scanned
734
- // twice). Skipped when the P5 cache hit above produced a usable value.
735
- // Sample terrain at BOTH camera position and look-ahead position.
736
- // Find lowest terrain at each position, then use the MAXIMUM of the two.
737
- if (runFloorScan && c.res != lastScanRes) {
738
- lastScanRes = c.res;
739
-
740
- // Check if camera position is in tile bounds
741
- const bool camInside =
742
- (c.wmTileBounds.x <= static_cast<float>(floorScanLonRad) &&
743
- static_cast<float>(floorScanLonRad) <= c.wmTileBounds.z) &&
744
- (c.wmTileBounds.y <= static_cast<float>(floorScanLatRad) &&
745
- static_cast<float>(floorScanLatRad) <= c.wmTileBounds.w);
746
-
747
- // Check if look-ahead position is in tile bounds (when moving)
748
- const bool aheadInside = useLookAhead &&
749
- (c.wmTileBounds.x <= static_cast<float>(floorScanAheadLonRad) &&
750
- static_cast<float>(floorScanAheadLonRad) <= c.wmTileBounds.z) &&
751
- (c.wmTileBounds.y <= static_cast<float>(floorScanAheadLatRad) &&
752
- static_cast<float>(floorScanAheadLatRad) <= c.wmTileBounds.w);
753
-
754
- if (camInside || aheadInside) {
755
- // Track lowest terrain at each position separately
756
- float terrainAtCam = std::numeric_limits<float>::max();
757
- float terrainAhead = std::numeric_limits<float>::max();
758
- bool foundAtCam = false;
759
- bool foundAhead = false;
760
-
761
- for (const auto& prim : c.res->primitives) {
762
- if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
763
- const size_t vc = prim.localPositions.size();
764
-
765
- for (size_t vi = 0; vi < vc; ++vi) {
766
- const glm::dvec3 vECEF = glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
767
- const float vAlt = prim.altitudes[vi];
768
-
769
- // Sample terrain at camera position - find LOWEST
770
- if (camInside) {
771
- const glm::dvec3 deltaCam = vECEF - floorScanPos;
772
- const double deCam = glm::dot(deltaCam, floorEast);
773
- const double dnCam = glm::dot(deltaCam, floorNorth);
774
- const double horizSqCam = deCam * deCam + dnCam * dnCam;
775
-
776
- if (horizSqCam <= kFloorSearchRadiusSq) {
777
- if (!foundAtCam || vAlt < terrainAtCam) {
778
- terrainAtCam = vAlt;
779
- foundAtCam = true;
780
- }
781
- }
782
- }
783
-
784
- // Sample terrain at look-ahead position - find LOWEST
785
- if (aheadInside) {
786
- const glm::dvec3 deltaAhead = vECEF - floorScanAheadPos;
787
- const double deAhead = glm::dot(deltaAhead, floorEast);
788
- const double dnAhead = glm::dot(deltaAhead, floorNorth);
789
- const double horizSqAhead = deAhead * deAhead + dnAhead * dnAhead;
790
-
791
- if (horizSqAhead <= kFloorSearchRadiusSq) {
792
- if (!foundAhead || vAlt < terrainAhead) {
793
- terrainAhead = vAlt;
794
- foundAhead = true;
795
- }
796
- }
797
- }
798
- }
799
- }
800
-
801
- // Use the MAXIMUM of the two terrain samples (prevents clipping through either)
802
- if (foundAtCam && foundAhead) {
803
- bestTerrainFloor = std::max(terrainAtCam, terrainAhead);
804
- floorScanFound = true;
805
- } else if (foundAtCam) {
806
- bestTerrainFloor = terrainAtCam;
807
- floorScanFound = true;
808
- } else if (foundAhead) {
809
- bestTerrainFloor = terrainAhead;
810
- floorScanFound = true;
811
- }
812
- }
813
- }
814
-
815
768
  const glm::dvec3 rtcCamRel = c.rtcCenter - glm::dvec3(cameraPos);
816
769
  const glm::dmat4 translateD = glm::translate(glm::dmat4(1.0), rtcCamRel);
817
770
 
@@ -845,20 +798,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
845
798
  result.cameraLon = paramsFP.longitude;
846
799
  result.cameraAlt = paramsFP.altitude;
847
800
 
848
- if (trackTerrainFloor && (floorScanFound || floorScanFromCache)) {
849
- terrainFloorEllipsoidM_ = bestTerrainFloor;
850
- terrainFloorValid_ = true;
851
- // P5: refresh the floor cache on a fresh scan; skip when we hit the
852
- // cache (the stored value already reflects this signature).
853
- if (runFloorScan && signature != 0ULL) {
854
- floorCacheValid_ = true;
855
- floorCacheSignature_ = signature;
856
- floorCacheCamLonDeg_ = camParams.longitude;
857
- floorCacheCamLatDeg_ = camParams.latitude;
858
- floorCacheValue_ = bestTerrainFloor;
859
- }
860
- }
861
-
862
801
  lifecycle_.advanceFrame();
863
802
  return;
864
803
  }
@@ -894,74 +833,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
894
833
  static_cast<float>(rect.getSouth()),
895
834
  static_cast<float>(rect.getEast()),
896
835
  static_cast<float>(rect.getNorth()));
897
-
898
- // ── Terrain floor: sample at both camera and look-ahead positions ────
899
- // Find lowest terrain at each position, then use the MAXIMUM of the two.
900
- // Suppressed when the cache returned a usable value.
901
- const bool camInside = runFloorScan &&
902
- rect.contains(CesiumGeospatial::Cartographic(floorScanLonRad, floorScanLatRad, 0.0));
903
- const bool aheadInside = runFloorScan && useLookAhead &&
904
- rect.contains(CesiumGeospatial::Cartographic(floorScanAheadLonRad, floorScanAheadLatRad, 0.0));
905
-
906
- if (camInside || aheadInside) {
907
- // Track lowest terrain at each position separately
908
- float terrainAtCam = std::numeric_limits<float>::max();
909
- float terrainAhead = std::numeric_limits<float>::max();
910
- bool foundAtCam = false;
911
- bool foundAhead = false;
912
-
913
- for (const auto& prim : res->primitives) {
914
- if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
915
- const size_t vertexCount = prim.localPositions.size();
916
-
917
- for (size_t vi = 0; vi < vertexCount; ++vi) {
918
- const glm::dvec3 vECEF = glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
919
- const float vAlt = prim.altitudes[vi];
920
-
921
- // Sample terrain at camera position - find LOWEST
922
- if (camInside) {
923
- const glm::dvec3 deltaCam = vECEF - floorScanPos;
924
- const double deCam = glm::dot(deltaCam, floorEast);
925
- const double dnCam = glm::dot(deltaCam, floorNorth);
926
- const double horizSqCam = deCam * deCam + dnCam * dnCam;
927
-
928
- if (horizSqCam <= kFloorSearchRadiusSq) {
929
- if (!foundAtCam || vAlt < terrainAtCam) {
930
- terrainAtCam = vAlt;
931
- foundAtCam = true;
932
- }
933
- }
934
- }
935
-
936
- // Sample terrain at look-ahead position - find LOWEST
937
- if (aheadInside) {
938
- const glm::dvec3 deltaAhead = vECEF - floorScanAheadPos;
939
- const double deAhead = glm::dot(deltaAhead, floorEast);
940
- const double dnAhead = glm::dot(deltaAhead, floorNorth);
941
- const double horizSqAhead = deAhead * deAhead + dnAhead * dnAhead;
942
-
943
- if (horizSqAhead <= kFloorSearchRadiusSq) {
944
- if (!foundAhead || vAlt < terrainAhead) {
945
- terrainAhead = vAlt;
946
- foundAhead = true;
947
- }
948
- }
949
- }
950
- }
951
- }
952
-
953
- // Use the MAXIMUM of the two terrain samples (prevents clipping through either)
954
- if (foundAtCam && foundAhead) {
955
- bestTerrainFloor = std::max(terrainAtCam, terrainAhead);
956
- floorScanFound = true;
957
- } else if (foundAtCam) {
958
- bestTerrainFloor = terrainAtCam;
959
- floorScanFound = true;
960
- } else if (foundAhead) {
961
- bestTerrainFloor = terrainAhead;
962
- floorScanFound = true;
963
- }
964
- }
965
836
  }
966
837
 
967
838
  for (const auto& prim : res->primitives) {
@@ -1209,29 +1080,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
1209
1080
  result.cameraLon = params.longitude;
1210
1081
  result.cameraAlt = params.altitude;
1211
1082
 
1212
- if (trackTerrainFloor && (floorScanFound || floorScanFromCache)) {
1213
- terrainFloorEllipsoidM_ = bestTerrainFloor;
1214
- terrainFloorValid_ = true;
1215
- // refresh the floor cache on a fresh scan (slow path always runs a
1216
- // scan unless the cache short-circuit fired above). Only memoise when
1217
- // the geometry signature is stable enough to key on.
1218
- if (runFloorScan && signature != 0ULL) {
1219
- floorCacheValid_ = true;
1220
- floorCacheSignature_ = signature;
1221
- floorCacheCamLonDeg_ = camParams.longitude;
1222
- floorCacheCamLatDeg_ = camParams.latitude;
1223
- floorCacheValue_ = bestTerrainFloor;
1224
- } else if (!runFloorScan) {
1225
- // Cache hit on the slow path is unusual but possible (e.g. fade
1226
- // started this frame after a stable cache was warm). Nothing to do
1227
- // — terrainFloorEllipsoidM_ is already the cached value.
1228
- } else {
1229
- // Cannot key on signature=0; invalidate so the next stable frame
1230
- // re-populates the cache deterministically.
1231
- floorCacheValid_ = false;
1232
- }
1233
- }
1234
-
1235
1083
  lifecycle_.advanceFrame();
1236
1084
  }
1237
1085
 
@@ -12,8 +12,10 @@
12
12
  #include <CesiumUtility/IntrusivePointer.h>
13
13
 
14
14
  #include <chrono>
15
+ #include <cstdint>
15
16
  #include <memory>
16
17
  #include <string>
18
+ #include <vector>
17
19
 
18
20
  namespace Cesium3DTilesSelection {
19
21
  class Tileset;
@@ -103,8 +105,9 @@ struct EngineConfig {
103
105
  int32_t taskProcessorThreads = 0;
104
106
 
105
107
  // ── Camera constraints ───────────────────────────────────────────────
106
- // Minimum altitude above the loaded terrain surface (metres MSL).
107
- // 0 = no constraint (default). Typical use: 2.0 to prevent going underground.
108
+ // Clearance above the rendered terrain surface, in metres. The floor is
109
+ // always enforced. 0 means the camera may sit on the surface and may not
110
+ // go below it.
108
111
  float minAltitudeAboveTerrain = 0.0f;
109
112
  };
110
113
 
@@ -142,11 +145,13 @@ public:
142
145
 
143
146
  const EngineConfig& getConfig() const { return config_; }
144
147
 
145
- // Ellipsoid height (metres) of the terrain surface under the camera nadir,
146
- // sampled from loaded tile meshes. Meaningless until terrainFloorKnown().
147
- float terrainFloorEllipsoidMeters() const { return terrainFloorEllipsoidM_; }
148
- // True after at least one frame has sampled the floor from real terrain tiles.
149
- bool terrainFloorKnown() const { return terrainFloorValid_; }
148
+ // Ellipsoid height (metres) of the rendered terrain surface at lat/lon,
149
+ // interpolated from the finest resident tile. False when no triangle
150
+ // contains the point (tiles not loaded yet, or the point is off the mesh).
151
+ // Uses the tiles captured by the previous updateFrame.
152
+ bool sampleTerrainEllipsoidMeters(double latitudeDeg,
153
+ double longitudeDeg,
154
+ float& outEllipsoidMeters);
150
155
 
151
156
  private:
152
157
  void createTileset();
@@ -185,11 +190,6 @@ private:
185
190
  GlobeCamera camera_;
186
191
  TileLifecycleManager lifecycle_;
187
192
 
188
- // Ellipsoid height of the terrain surface at the camera nadir (horizontal
189
- // nearest-neighbour sample). Only written after a successful tile scan.
190
- float terrainFloorEllipsoidM_ = 0.0f;
191
- bool terrainFloorValid_ = false;
192
-
193
193
  // Wall-clock timestamp of the previous updateFrame call. Used to compute
194
194
  // deltaTime for Tileset::updateViewGroup, which drives LOD transition fading.
195
195
  // Zero-initialised "not yet set" state — first frame passes 0 deltaTime.
@@ -224,22 +224,32 @@ private:
224
224
  // is guaranteed to contain the right bytes — fast path becomes legal.
225
225
  int stableSigFrames_ = 0;
226
226
 
227
- // ── terrain-floor scan cache ──────────────────────────────────────
228
- // The minAltitudeAboveTerrain clamp scans every vertex of every tile whose
229
- // bounding region contains the camera lon/lat and picks the altitude of
230
- // the vertex with the smallest horizontal (ENU) distance to the camera
231
- // nadir (tie-break: lower ellipsoid height). That's an O(NV)
232
- // scan over the visible primitives — dominant CPU cost when the camera
233
- // sits over a dense LOD chain. Result depends only on the camera lon/lat
234
- // and the set of visible primitives, so we cache it keyed on
235
- // (camLon, camLat, geometrySignature). Threshold of ~1e-6 degrees (~10 cm)
236
- // is far below the inter-vertex spacing of the densest tiles served by
237
- // Cesium Ion world terrain, so the cached nearest vertex stays correct.
238
- bool floorCacheValid_ = false;
239
- double floorCacheCamLonDeg_ = 0.0;
240
- double floorCacheCamLatDeg_ = 0.0;
241
- uint64_t floorCacheSignature_ = 0;
242
- float floorCacheValue_ = 0.0f;
227
+ // Tiles drawn by the last updateFrame. The next frame's terrain query
228
+ // reads this list before updateViewGroup can free the previous selection.
229
+ struct ResidentTerrainTile {
230
+ const TileGPUResources* res = nullptr;
231
+ double geometricError = 0.0;
232
+ double west = 0.0;
233
+ double south = 0.0;
234
+ double east = 0.0;
235
+ double north = 0.0;
236
+ };
237
+ std::vector<ResidentTerrainTile> residentTiles_;
238
+ uint64_t residentSignature_ = 0;
239
+
240
+ // One-entry hover cache. Keyed by resident signature, finest-tile identity,
241
+ // and a sub-metre lat/lon cell. A miss scans that one tile's triangles.
242
+ struct TerrainHeightCache {
243
+ bool valid = false;
244
+ const TileGPUResources* tile = nullptr;
245
+ int latCell = 0;
246
+ int lonCell = 0;
247
+ uint64_t signature = 0;
248
+ float height = 0.0f;
249
+ };
250
+ TerrainHeightCache heightCache_;
251
+
252
+ void clearResidentTerrain();
243
253
  };
244
254
 
245
255
  } // namespace reactnativecesium
@@ -63,16 +63,17 @@ inline constexpr int32_t kDefaultLoadingDescendantLimit = 20;
63
63
  // steady-state lag at constant motion) while remaining instant under a
64
64
  // 60 Hz gesture-driven worklet.
65
65
  //
66
- // `viewCorrection` (a unit quaternion) uses a single-coefficient SLERP toward
67
- // the latest demand each frame — there is no useful velocity concept for it.
68
- // `verticalFovDeg` is a 1-D scalar with α only (no velocity term).
66
+ // `viewCorrection` (a unit quaternion) slerps toward the latest demand with a
67
+ // time constant, so a single long frame does not close half the error.
68
+ // `verticalFovDeg` eases toward its demand with the same kind of time constant
69
+ // (no velocity term).
69
70
  inline constexpr double kAlphaPos = 0.6; inline constexpr double kBetaPos = 0.05;
70
71
  inline constexpr double kAlphaAlt = 0.6; inline constexpr double kBetaAlt = 0.05;
71
72
  inline constexpr double kAlphaHdg = 0.55; inline constexpr double kBetaHdg = 0.05;
72
73
  inline constexpr double kAlphaPitch = 0.75; inline constexpr double kBetaPitch = 0.05;
73
74
  inline constexpr double kAlphaRoll = 0.75; inline constexpr double kBetaRoll = 0.05;
74
- inline constexpr double kAlphaVfov = 0.4; // no velocity term
75
- inline constexpr double kAlphaViewCorr = 0.5; // SLERP toward target
75
+ inline constexpr double kVfovTauSec = 0.12; // seconds, dt-based blend
76
+ inline constexpr double kViewCorrTauSec = 0.08; // seconds, dt-based slerp
76
77
 
77
78
  // Convergence epsilons (used to decide when the integrator has settled and
78
79
  // the render loop can drop back to idle).