react-native-cesium 1.2.1 → 1.2.2

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/README.md CHANGED
@@ -399,11 +399,11 @@ Avoid assuming **main-thread-only** vs **JS-thread-only** labels beyond the abov
399
399
 
400
400
  #### Smoothing of incoming camera updates
401
401
 
402
- Every value you pass to `setCamera` / `setCameraQuaternion` is treated as a **demand** target — what you would *like* the camera to be — rather than a per-frame command. The native render thread runs an exponential filter every frame that converges the displayed camera onto your demand target, which means a coarse 1 Hz position feed (e.g. external GPS) does **not** teleport the view once a second. Instead it eases between samples.
402
+ Every value you pass to `setCamera` / `setCameraQuaternion` is treated as a **demand** target — what you would *like* the camera to be — rather than a per-frame command. The native render thread interpolates the displayed camera toward your demand target every frame, which means a coarse 1 Hz position feed (e.g. external GPS) does **not** teleport the view once a second. Instead it moves at constant speed — exactly like looking out the window of the vehicle being tracked.
403
403
 
404
404
  | DoF | Smoothing |
405
405
  | --- | --- |
406
- | `latitude` / `longitude` | **Adaptive** exponential filter. Time constant `τ = clamp(α · ewmaInterval, τmin, τmax)` where `ewmaInterval` tracks the cadence of your `setCamera` calls. ~60 Hz gestures collapse `τ` to the floor (≤1 frame of trail, indistinguishable from the previous "snap" behaviour); ~1 Hz GPS feeds expand `τ` so each sample is ~95 % converged before the next arrives. Cross-planet jumps (Δ > ~0.5°) bypass the smoother and snap, so a deliberate teleport stays instant. Antimeridian crossings always rotate the short way. |
406
+ | `latitude` / `longitude` | **Linear interpolation** at constant speed between position fixes. When a new lat/lon arrives, the native engine records the previous target as the "from" position and linearly lerps toward the new target over the EWMA inter-arrival interval (e.g. a 1 Hz GPS feed gives a 1 s travel time, so the camera moves at exactly the vehicle's speed between fixes). At 60 Hz gestures the interval collapses to ~16 ms making the interpolation indistinguishable from an instant snap. Cross-planet jumps (Δ > ~0.5°) bypass interpolation so deliberate teleports remain instant. Antimeridian crossings always rotate the short way. |
407
407
  | `altitude` | Exponential, fixed `τ ≈ 40 ms`. |
408
408
  | `heading` / `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20–33 ms`. |
409
409
  | `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected so it always takes the short rotation. |
@@ -244,8 +244,7 @@ void CesiumBridgeAndroid::renderFrame(double dt) {
244
244
 
245
245
  const auto cur = engine_->camera().getParams();
246
246
  const auto tgt = target_.snapshot();
247
- auto smooth = reactnativecesium::CameraSmoother::step(
248
- cur, tgt, dt, target_.recentIntervalSec());
247
+ auto smooth = reactnativecesium::CameraSmoother::step(cur, tgt, dt);
249
248
 
250
249
  // ── Terrain floor clamp ───────────────────────────────────────────────
251
250
  const float minAbove = engine_->getConfig().minAltitudeAboveTerrain;
@@ -2,7 +2,6 @@
2
2
 
3
3
  #include "EngineTunables.hpp"
4
4
 
5
- #include <algorithm>
6
5
  #include <cmath>
7
6
 
8
7
  namespace reactnativecesium {
@@ -24,40 +23,18 @@ inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
24
23
  return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
25
24
  }
26
25
 
27
- inline double wrapLonDeltaDeg(double a, double b) {
28
- return std::fmod(b - a + 540.0, 360.0) - 180.0;
29
- }
30
-
31
26
  } // namespace
32
27
 
33
28
  CameraParams CameraSmoother::step(
34
- const CameraParams& current, const CameraParams& target, double dt,
35
- double recentIntervalSec) {
29
+ const CameraParams& current, const CameraParams& target, double dt) {
36
30
  CameraParams next = current;
37
31
 
38
- // ── lat/lon: adaptive exponential smoothing ─────────────────────────────
39
- // τ scales with the cadence of incoming setCamera calls so gestures feel
40
- // instant (τ at the floor) while sparse GPS-rate updates ease across each
41
- // interval (τ near the ceiling).
42
- const double tau = std::clamp(
43
- tunables::kSmoothPositionAlpha * recentIntervalSec,
44
- tunables::kSmoothPositionTauMin,
45
- tunables::kSmoothPositionTauMax);
46
- const double aPos = 1.0 - std::exp(-dt / tau);
47
-
48
- const double dLat = target.latitude - current.latitude;
49
- const double dLon = wrapLonDeltaDeg(current.longitude, target.longitude);
50
- const bool snapThrough =
51
- std::abs(dLat) > tunables::kPosSnapThroughDeg ||
52
- std::abs(dLon) > tunables::kPosSnapThroughDeg;
53
-
54
- if (snapThrough) {
55
- next.latitude = target.latitude;
56
- next.longitude = target.longitude;
57
- } else {
58
- next.latitude = current.latitude + aPos * dLat;
59
- next.longitude = lerpAngleDeg(current.longitude, target.longitude, aPos);
60
- }
32
+ // lat/lon: snap directly to the target.
33
+ // CameraTargetState::snapshot() already produces a linearly-interpolated
34
+ // position that advances at constant speed between position fixes, so
35
+ // snapping here gives smooth, constant-velocity motion with no ease-in/out.
36
+ next.latitude = target.latitude;
37
+ next.longitude = target.longitude;
61
38
 
62
39
  const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
63
40
  const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
@@ -76,11 +53,6 @@ CameraParams CameraSmoother::step(
76
53
  next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
77
54
 
78
55
  // Eventually-exact convergence: snap once the residual is sub-epsilon.
79
- if (std::abs(target.latitude - next.latitude) <= tunables::kEpsLatLon)
80
- next.latitude = target.latitude;
81
- if (std::abs(wrapLonDeltaDeg(next.longitude, target.longitude))
82
- <= tunables::kEpsLatLon)
83
- next.longitude = target.longitude;
84
56
  if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
85
57
  next.altitude = target.altitude;
86
58
  if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
@@ -8,12 +8,9 @@ namespace reactnativecesium {
8
8
  // returns the next camera state for a frame of duration `dt` seconds.
9
9
  //
10
10
  // Per-DoF smoothing rules:
11
- // - lat / lon: adaptive exponential smoothing `1 - exp(-dt/τ)` where
12
- // τ = clamp(α · recentIntervalSec, τmin, τmax). High update rates (60 Hz
13
- // gestures) drive τ to the floor so the camera tracks the finger without
14
- // perceptible lag; sparse updates (1 Hz GPS) widen τ so each sample
15
- // interpolates smoothly across its interval. Snap-through guard for
16
- // genuine teleports keeps cross-planet `setCamera` jumps instant.
11
+ // - lat / lon: snapped directly to target. The target itself is already a
12
+ // linearly-interpolated position produced by CameraTargetState::snapshot(),
13
+ // so snapping here gives constant-velocity motion between position fixes.
17
14
  // - altitude / heading / pitch / roll / viewCorrection: exponential decay
18
15
  // `1 - exp(-k * dt)` per DoF with k taken from EngineTunables.
19
16
  // - When the delta drops below the matching epsilon, the value is snapped
@@ -22,8 +19,7 @@ class CameraSmoother {
22
19
  public:
23
20
  static CameraParams step(const CameraParams& current,
24
21
  const CameraParams& target,
25
- double dt,
26
- double recentIntervalSec);
22
+ double dt);
27
23
  };
28
24
 
29
25
  } // namespace reactnativecesium
@@ -1,18 +1,130 @@
1
1
  #include "CameraTargetState.hpp"
2
2
 
3
+ #include <algorithm>
3
4
  #include <cmath>
4
5
 
5
6
  namespace reactnativecesium {
6
7
 
7
8
  namespace {
9
+
8
10
  inline double angleDeltaAbsDeg(double a, double b) {
9
11
  return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
10
12
  }
13
+
11
14
  inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
12
15
  return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
13
16
  }
17
+
18
+ // Shortest signed angular delta for longitude, wrapped to [-180, 180].
19
+ inline double lonDeltaDeg(double from, double to) {
20
+ return std::fmod(to - from + 540.0, 360.0) - 180.0;
21
+ }
22
+
14
23
  } // namespace
15
24
 
25
+ // ── Public writers ────────────────────────────────────────────────────────────
26
+
27
+ void CameraTargetState::setAll(const CameraParams& target) {
28
+ {
29
+ std::lock_guard<std::mutex> g(mutex_);
30
+ target_ = target;
31
+ // Deliberately do NOT touch interpolation state: setAll is engine-init only
32
+ // and the first frame must snap to the requested position immediately.
33
+ }
34
+ dirty_.store(true, std::memory_order_release);
35
+ forceRender_.store(true, std::memory_order_release);
36
+ }
37
+
38
+ void CameraTargetState::setHpr(double lat, double lon, double alt,
39
+ double heading, double pitch, double roll) {
40
+ {
41
+ std::lock_guard<std::mutex> g(mutex_);
42
+
43
+ const bool posChanged =
44
+ std::abs(lat - target_.latitude) > tunables::kEpsLatLon ||
45
+ std::abs(lon - target_.longitude) > tunables::kEpsLatLon;
46
+
47
+ if (posChanged) {
48
+ const auto now = std::chrono::steady_clock::now();
49
+
50
+ if (!hasInterpolation_) {
51
+ // First runtime lat/lon update: seed posFrom = destination so that
52
+ // lerp(dest, dest, t) = dest for all t — clean snap, no startup glide.
53
+ posFromLat_ = lat;
54
+ posFromLon_ = lon;
55
+ ewmaIntervalSec_ = tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha;
56
+ posUpdateTime_ = now;
57
+ hasInterpolation_ = true;
58
+ } else {
59
+ // Subsequent update: record where we're moving FROM (the previous
60
+ // target), update the EWMA interval, and reset the clock.
61
+ posFromLat_ = target_.latitude;
62
+ posFromLon_ = target_.longitude;
63
+
64
+ const double dt = std::chrono::duration<double>(now - posUpdateTime_).count();
65
+ if (dt > 0.0) {
66
+ ewmaIntervalSec_ = ewmaIntervalSec_ +
67
+ tunables::kEwmaIntervalAlpha * (dt - ewmaIntervalSec_);
68
+ }
69
+ posUpdateTime_ = now;
70
+ }
71
+ }
72
+
73
+ target_.latitude = lat;
74
+ target_.longitude = lon;
75
+ target_.altitude = alt;
76
+ target_.heading = heading;
77
+ target_.pitch = pitch;
78
+ target_.roll = roll;
79
+ }
80
+ dirty_.store(true, std::memory_order_release);
81
+ forceRender_.store(true, std::memory_order_release);
82
+ }
83
+
84
+ void CameraTargetState::setViewCorrection(const glm::dquat& q) {
85
+ {
86
+ std::lock_guard<std::mutex> g(mutex_);
87
+ target_.viewCorrection = q;
88
+ // Position cadence intentionally NOT updated — viewCorrection changes
89
+ // carry no lat/lon information and must not affect the interpolation window.
90
+ }
91
+ dirty_.store(true, std::memory_order_release);
92
+ forceRender_.store(true, std::memory_order_release);
93
+ }
94
+
95
+ // ── Snapshot (render-thread read) ─────────────────────────────────────────────
96
+
97
+ CameraParams CameraTargetState::snapshot() const {
98
+ const auto now = std::chrono::steady_clock::now();
99
+ std::lock_guard<std::mutex> g(mutex_);
100
+
101
+ CameraParams result = target_;
102
+
103
+ if (hasInterpolation_ && ewmaIntervalSec_ > 0.0) {
104
+ const double dLat = target_.latitude - posFromLat_;
105
+ const double dLon = lonDeltaDeg(posFromLon_, target_.longitude);
106
+
107
+ // Snap-through guard: genuine teleports (setCamera(NewYork → Tokyo)) bypass
108
+ // the interpolation so they remain instant rather than gliding across the planet.
109
+ const bool snapThrough =
110
+ std::abs(dLat) > tunables::kPosSnapThroughDeg ||
111
+ std::abs(dLon) > tunables::kPosSnapThroughDeg;
112
+
113
+ if (!snapThrough) {
114
+ const double elapsed =
115
+ std::chrono::duration<double>(now - posUpdateTime_).count();
116
+ const double t = std::min(elapsed / ewmaIntervalSec_, 1.0);
117
+
118
+ result.latitude = posFromLat_ + t * dLat;
119
+ result.longitude = posFromLon_ + t * dLon;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ // ── Epsilon check (shared) ────────────────────────────────────────────────────
127
+
16
128
  bool CameraTargetState::deltaExceedsEpsilon(
17
129
  const CameraParams& a, const CameraParams& b) {
18
130
  return std::abs(a.latitude - b.latitude) > tunables::kEpsLatLon
@@ -25,25 +137,4 @@ bool CameraTargetState::deltaExceedsEpsilon(
25
137
  > tunables::kEpsQuatDot;
26
138
  }
27
139
 
28
- void CameraTargetState::noteUpdateCadence() noexcept {
29
- const auto now = std::chrono::steady_clock::now();
30
- if (!hasLastUpdate_) {
31
- // Seed the EWMA with τmax so the first sample after a quiet period uses
32
- // the full smoothing budget rather than the floor (5 ms ≈ snap).
33
- ewmaIntervalSec_.store(tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha,
34
- std::memory_order_release);
35
- lastUpdateTime_ = now;
36
- hasLastUpdate_ = true;
37
- return;
38
- }
39
-
40
- const double dt = std::chrono::duration<double>(now - lastUpdateTime_).count();
41
- lastUpdateTime_ = now;
42
- if (dt <= 0.0) return; // back-to-back writes from the same frame: ignore.
43
-
44
- const double prev = ewmaIntervalSec_.load(std::memory_order_relaxed);
45
- const double next = prev + tunables::kEwmaIntervalAlpha * (dt - prev);
46
- ewmaIntervalSec_.store(next, std::memory_order_release);
47
- }
48
-
49
140
  } // namespace reactnativecesium
@@ -21,54 +21,43 @@ namespace reactnativecesium {
21
21
  //
22
22
  // `dirty_` and `forceRender_` are independent atomic flags so the read side can
23
23
  // poll without taking the lock when the camera has not moved.
24
+ //
25
+ // Lat/lon interpolation
26
+ // ─────────────────────
27
+ // When setHpr is called with a new lat/lon, `posFrom` (old target) and
28
+ // `posUpdateTime` are recorded inside the mutex. snapshot() then linearly
29
+ // interpolates posFrom → target over `ewmaIntervalSec` (EWMA of inter-arrival
30
+ // times). This produces constant-velocity motion between position fixes —
31
+ // realistic for any vehicle/GPS source — without any changes to the public API.
32
+ //
33
+ // Special cases handled in snapshot():
34
+ // - t >= 1 : camera has reached the target, returns target_ directly.
35
+ // - Snap-through guard: deltas > kPosSnapThroughDeg bypass interpolation so
36
+ // deliberate teleports (setCamera(NewYork → Tokyo)) remain instant.
37
+ // - First setHpr for a given lat/lon: posFrom is set to the *destination*
38
+ // so lerp(dest, dest, t) = dest — no startup glide.
39
+ // - setAll (initialisation only): does not touch interpolation state; always
40
+ // snaps directly.
24
41
  class CameraTargetState {
25
42
  public:
26
43
  CameraTargetState() = default;
27
44
 
28
- // Replace the entire target (does not touch viewCorrection if you want to
29
- // preserve it — use setHpr instead).
30
- void setAll(const CameraParams& target) {
31
- {
32
- std::lock_guard<std::mutex> g(mutex_);
33
- target_ = target;
34
- }
35
- noteUpdateCadence();
36
- dirty_.store(true, std::memory_order_release);
37
- forceRender_.store(true, std::memory_order_release);
38
- }
45
+ // Replace the entire target. Used only for engine initialisation; does NOT
46
+ // update position-interpolation state so the first rendered frame snaps.
47
+ void setAll(const CameraParams& target);
39
48
 
40
49
  // Update lat/lon/alt + heading/pitch/roll, leaving viewCorrection unchanged.
50
+ // When lat/lon actually changes, updates the linear-interpolation bookkeeping.
41
51
  void setHpr(double lat, double lon, double alt,
42
- double heading, double pitch, double roll) {
43
- {
44
- std::lock_guard<std::mutex> g(mutex_);
45
- target_.latitude = lat;
46
- target_.longitude = lon;
47
- target_.altitude = alt;
48
- target_.heading = heading;
49
- target_.pitch = pitch;
50
- target_.roll = roll;
51
- }
52
- noteUpdateCadence();
53
- dirty_.store(true, std::memory_order_release);
54
- forceRender_.store(true, std::memory_order_release);
55
- }
52
+ double heading, double pitch, double roll);
56
53
 
57
- void setViewCorrection(const glm::dquat& q) {
58
- {
59
- std::lock_guard<std::mutex> g(mutex_);
60
- target_.viewCorrection = q;
61
- }
62
- noteUpdateCadence();
63
- dirty_.store(true, std::memory_order_release);
64
- forceRender_.store(true, std::memory_order_release);
65
- }
54
+ void setViewCorrection(const glm::dquat& q);
66
55
 
67
- // Snapshot the demand target (read side; one mutex per frame).
68
- CameraParams snapshot() const {
69
- std::lock_guard<std::mutex> g(mutex_);
70
- return target_;
71
- }
56
+ // Snapshot the demand target for this render frame. Lat/lon are linearly
57
+ // interpolated between the previous target (posFrom) and the current target
58
+ // over the EWMA inter-arrival interval. All other DoFs are returned as-is
59
+ // (the smoother handles their exponential curves).
60
+ CameraParams snapshot() const;
72
61
 
73
62
  // Lightweight, lock-free check used by the render thread to decide whether
74
63
  // it should bother taking a full snapshot at all.
@@ -91,31 +80,24 @@ public:
91
80
  // whether to render this frame.
92
81
  static bool deltaExceedsEpsilon(const CameraParams& a, const CameraParams& b);
93
82
 
94
- // EWMA of the inter-arrival interval (seconds) of demand updates. Used by
95
- // CameraSmoother to scale the lat/lon time constant: high update rate ⇒
96
- // tiny τ (snappy gestures); low update rate ⇒ larger τ (smooth GPS catch-up).
97
- // Lock-free read on the render-thread hot path.
98
- double recentIntervalSec() const noexcept {
99
- return ewmaIntervalSec_.load(std::memory_order_acquire);
100
- }
101
-
102
83
  private:
103
- // Folds the time since the previous demand update into ewmaIntervalSec_.
104
- // Called under (or after) the target_ mutex; the EWMA itself is atomic so
105
- // the read side never blocks. Writes are uncontended (only JS-thread).
106
- void noteUpdateCadence() noexcept;
107
-
108
84
  mutable std::mutex mutex_;
109
85
  CameraParams target_;
110
86
  std::atomic<bool> dirty_{false};
111
87
  std::atomic<bool> forceRender_{true};
112
88
 
113
- // Cadence tracker. lastUpdateTime_ is only touched on the writer (JS)
114
- // thread, so it does not need synchronisation; ewmaIntervalSec_ is read
115
- // by the render thread without taking the mutex.
116
- std::chrono::steady_clock::time_point lastUpdateTime_{};
117
- bool hasLastUpdate_{false};
118
- std::atomic<double> ewmaIntervalSec_{0.0};
89
+ // ── Lat/lon linear interpolation state (all protected by mutex_) ──────────
90
+ // posFromLat_/posFromLon_: the target position when the last update arrived
91
+ // (i.e. where we are smoothly moving FROM).
92
+ // posUpdateTime_: wall-clock time of the last lat/lon change.
93
+ // ewmaIntervalSec_: EWMA of inter-arrival times of lat/lon changes.
94
+ // hasInterpolation_: true once we have valid posFrom + posUpdateTime
95
+ // data (false until the second distinct lat/lon value).
96
+ double posFromLat_{0.0};
97
+ double posFromLon_{0.0};
98
+ std::chrono::steady_clock::time_point posUpdateTime_{};
99
+ double ewmaIntervalSec_{0.0};
100
+ bool hasInterpolation_{false};
119
101
  };
120
102
 
121
103
  } // namespace reactnativecesium
@@ -277,8 +277,7 @@
277
277
 
278
278
  const auto cur = _engine->camera().getParams();
279
279
  const auto tgt = _target.snapshot();
280
- auto smooth = reactnativecesium::CameraSmoother::step(
281
- cur, tgt, dt, _target.recentIntervalSec());
280
+ auto smooth = reactnativecesium::CameraSmoother::step(cur, tgt, dt);
282
281
 
283
282
  // ── Terrain floor clamp ───────────────────────────────────────────────
284
283
  // Uses terrain floor from the previous frame (one-frame lag is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro (iOS/Metal today; Android planned)",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",