react-native-cesium 1.2.11 → 1.2.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.
@@ -341,9 +341,20 @@ CameraParams CameraIntegrator::step() {
341
341
  }
342
342
 
343
343
  void CameraIntegrator::clampActualAltitude(double newAltMsl) {
344
+ const auto now = Clock::now();
344
345
  std::lock_guard<std::mutex> g(mutex_);
345
- alt_.value = newAltMsl;
346
- alt_.velocity = 0.0;
346
+
347
+ // Instead of snapping, feed the clamped altitude through the alpha-beta
348
+ // tracker for smooth interpolation. This matches how heading/pitch/roll work.
349
+ updateAlphaBeta(alt_, newAltMsl, now,
350
+ tunables::kAlphaAlt, tunables::kBetaAlt, false);
351
+
352
+ // Only clamp DOWNWARD velocity to prevent descending through terrain.
353
+ // Allow upward velocity for smooth ascents over rising terrain.
354
+ if (alt_.velocity < 0.0) {
355
+ alt_.velocity = 0.0;
356
+ }
357
+
347
358
  // Do not touch demand_.altitude — getDemand() should still report what the
348
359
  // user asked for.
349
360
  }
@@ -4,7 +4,9 @@
4
4
  #endif
5
5
 
6
6
  #include "CesiumEngine.hpp"
7
+ #include "GeoidConverter.hpp"
7
8
 
9
+ #include <iostream>
8
10
 
9
11
  #include <Cesium3DTilesContent/registerAllTileContentTypes.h>
10
12
  #include <Cesium3DTilesSelection/BoundingVolume.h>
@@ -35,6 +37,7 @@
35
37
  #include <algorithm>
36
38
  #include <cmath>
37
39
  #include <cstring>
40
+ #include <limits>
38
41
  #include <thread>
39
42
 
40
43
 
@@ -608,17 +611,21 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
608
611
  const CameraParams camParams = trackTerrainFloor ? camera_.getParams() : CameraParams{};
609
612
  const double camLatRad = glm::radians(camParams.latitude);
610
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
+
611
621
  float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
612
622
  double bestHorizDistSq = std::numeric_limits<double>::max();
613
623
  bool floorScanFound = false;
614
624
 
615
- // Horizontal search radius for floor sampling (50m = reasonable for urban terrain).
616
- // Vertices within this radius are candidates; we pick the lowest (ground level).
617
- constexpr double kFloorSearchRadiusM = 50.0;
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;
618
628
  constexpr double kFloorSearchRadiusSq = kFloorSearchRadiusM * kFloorSearchRadiusM;
619
- // 3D fallback: if no vertices within horizontal radius, use closest 3D vertex.
620
- double bestDist3D = std::numeric_limits<double>::max();
621
- float bestDist3DFloor = 0.0f;
622
629
 
623
630
  // ── floor-scan cache lookup ─────────────────────────────────────────
624
631
  // Skip the per-vertex scan when the camera has moved less than ~10 cm in
@@ -644,6 +651,10 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
644
651
  // ENU basis at the camera — horizontal distance to each terrain vertex.
645
652
  glm::dvec3 floorEast(1.0, 0.0, 0.0);
646
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
+
647
658
  if (runFloorScan) {
648
659
  const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
649
660
  const glm::dmat4 enuToEcef =
@@ -651,6 +662,58 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
651
662
  cameraPos, ellipsoid);
652
663
  floorEast = glm::normalize(glm::dvec3(enuToEcef[0]));
653
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;
716
+ }
654
717
  }
655
718
 
656
719
  // ── fast path ────────────────────────────────────────────────────────
@@ -669,48 +732,83 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
669
732
 
670
733
  // Floor scan (run once per res so multi-primitive tiles aren't scanned
671
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.
672
737
  if (runFloorScan && c.res != lastScanRes) {
673
738
  lastScanRes = c.res;
674
- const bool inside =
675
- (c.wmTileBounds.x <= static_cast<float>(camLonRad) &&
676
- static_cast<float>(camLonRad) <= c.wmTileBounds.z) &&
677
- (c.wmTileBounds.y <= static_cast<float>(camLatRad) &&
678
- static_cast<float>(camLatRad) <= c.wmTileBounds.w);
679
- if (inside) {
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
+
680
761
  for (const auto& prim : c.res->primitives) {
681
762
  if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
682
763
  const size_t vc = prim.localPositions.size();
764
+
683
765
  for (size_t vi = 0; vi < vc; ++vi) {
684
- const glm::dvec3 vECEF =
685
- glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
686
- const glm::dvec3 delta = vECEF - cameraPos;
687
- const double de = glm::dot(delta, floorEast);
688
- const double dn = glm::dot(delta, floorNorth);
689
- const double horizSq = de * de + dn * dn;
690
- const float vAlt = prim.altitudes[vi];
691
-
692
- // Primary: horizontal search within radius, pick minimum altitude.
693
- if (horizSq <= kFloorSearchRadiusSq) {
694
- if (!floorScanFound || vAlt < bestTerrainFloor) {
695
- bestTerrainFloor = vAlt;
696
- bestHorizDistSq = horizSq;
697
- floorScanFound = true;
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
+ }
698
781
  }
699
782
  }
700
-
701
- // Fallback: track closest 3D vertex for sparse/ocean scenarios.
702
- const double dist3D = glm::length(delta);
703
- if (dist3D < bestDist3D) {
704
- bestDist3D = dist3D;
705
- bestDist3DFloor = vAlt;
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
+ }
706
797
  }
707
798
  }
708
799
  }
709
- }
710
- // If no vertices within horizontal radius, use 3D fallback.
711
- if (!floorScanFound && bestDist3D < std::numeric_limits<double>::max()) {
712
- bestTerrainFloor = bestDist3DFloor;
713
- floorScanFound = true;
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
+ }
714
812
  }
715
813
  }
716
814
 
@@ -797,44 +895,71 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
797
895
  static_cast<float>(rect.getEast()),
798
896
  static_cast<float>(rect.getNorth()));
799
897
 
800
- // ── Terrain floor: find nearest vertex to camera nadir ────────────
801
- // Suppressed when the cache returned a usable value for the
802
- // current (signature, camLon, camLat) tuple.
803
- if (runFloorScan &&
804
- rect.contains(CesiumGeospatial::Cartographic(camLonRad, camLatRad, 0.0))) {
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
+
805
913
  for (const auto& prim : res->primitives) {
806
914
  if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
807
915
  const size_t vertexCount = prim.localPositions.size();
916
+
808
917
  for (size_t vi = 0; vi < vertexCount; ++vi) {
809
- const glm::dvec3 vECEF =
810
- glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
811
- const glm::dvec3 delta = vECEF - cameraPos;
812
- const double de = glm::dot(delta, floorEast);
813
- const double dn = glm::dot(delta, floorNorth);
814
- const double horizSq = de * de + dn * dn;
815
- const float vAlt = prim.altitudes[vi];
816
-
817
- // Primary: horizontal search within radius, pick minimum altitude.
818
- if (horizSq <= kFloorSearchRadiusSq) {
819
- if (!floorScanFound || vAlt < bestTerrainFloor) {
820
- bestTerrainFloor = vAlt;
821
- bestHorizDistSq = horizSq;
822
- floorScanFound = true;
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
+ }
823
933
  }
824
934
  }
825
-
826
- // Fallback: track closest 3D vertex for sparse/ocean scenarios.
827
- const double dist3D = glm::length(delta);
828
- if (dist3D < bestDist3D) {
829
- bestDist3D = dist3D;
830
- bestDist3DFloor = vAlt;
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
+ }
831
949
  }
832
950
  }
833
951
  }
834
- // If no vertices within horizontal radius, use 3D fallback.
835
- if (!floorScanFound && bestDist3D < std::numeric_limits<double>::max()) {
836
- bestTerrainFloor = bestDist3DFloor;
837
- floorScanFound = true;
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;
838
963
  }
839
964
  }
840
965
  }
@@ -384,17 +384,18 @@
384
384
  auto actual = _integrator->step();
385
385
 
386
386
  // ── Terrain floor clamp ───────────────────────────────────────────────
387
- // Uses terrain floor from the previous frame (one-frame lag is
388
- // imperceptible at 60 fps). Skipped when minAltitudeAboveTerrain == 0.
387
+ // Simple collision detection: if camera goes below terrain + offset, snap it up.
388
+ // Equivalent to CesiumJS's preRender terrain clamping.
389
389
  const float minAbove = _engine->getConfig().minAltitudeAboveTerrain;
390
390
  if (minAbove > 0.0f && _engine->terrainFloorKnown()) {
391
391
  const double geoidN =
392
392
  reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, 0.0);
393
- const double floorMsl =
394
- static_cast<double>(_engine->terrainFloorEllipsoidMeters()) - geoidN;
393
+ const double floorEllipsoid = static_cast<double>(_engine->terrainFloorEllipsoidMeters());
394
+ const double floorMsl = floorEllipsoid - geoidN;
395
395
  const double minMsl = floorMsl + static_cast<double>(minAbove);
396
+
397
+ // Simple clamp: if below minimum, snap to minimum
396
398
  if (actual.altitude < minMsl) {
397
- _integrator->clampActualAltitude(minMsl);
398
399
  actual.altitude = minMsl;
399
400
  }
400
401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",