react-native-cesium 1.1.0 → 1.2.0
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 +16 -0
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +2 -1
- package/cpp/engine/CameraSmoother.cpp +35 -4
- package/cpp/engine/CameraSmoother.hpp +9 -4
- package/cpp/engine/CameraTargetState.cpp +21 -0
- package/cpp/engine/CameraTargetState.hpp +24 -0
- package/cpp/engine/EngineTunables.hpp +15 -0
- package/ios/CesiumBridge.mm +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -397,6 +397,22 @@ Avoid assuming **main-thread-only** vs **JS-thread-only** labels beyond the abov
|
|
|
397
397
|
- You may **mix** the two: calling `setCamera` updates position and HPR but leaves the last view-correction target unchanged, so a calibration quaternion set earlier continues to apply until you change it with `setCameraQuaternion`.
|
|
398
398
|
- Prefer **`setCameraQuaternion`** on frames where you need to drive both the globe camera and the correction together so the native target stays consistent.
|
|
399
399
|
|
|
400
|
+
#### Smoothing of incoming camera updates
|
|
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.
|
|
403
|
+
|
|
404
|
+
| DoF | Smoothing |
|
|
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. |
|
|
407
|
+
| `altitude` | Exponential, fixed `τ ≈ 40 ms`. |
|
|
408
|
+
| `heading` / `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20–33 ms`. |
|
|
409
|
+
| `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected so it always takes the short rotation. |
|
|
410
|
+
| `verticalFovDeg` | Applied directly (not smoothed). |
|
|
411
|
+
|
|
412
|
+
You don't have to do anything special to take advantage of this — the smoother already runs, it's the same code path whether your driver is a Reanimated worklet at 60 Hz, a `setInterval` push at 10 Hz, or a 1 Hz GPS subscription. Just call `setCamera` whenever you have a new value.
|
|
413
|
+
|
|
414
|
+
If the smoothing ever feels off for your data source, the relevant constants live in [`cpp/engine/EngineTunables.hpp`](cpp/engine/EngineTunables.hpp) (`kSmoothPositionAlpha`, `kSmoothPositionTauMin`, `kSmoothPositionTauMax`, `kPosSnapThroughDeg`, `kEwmaIntervalAlpha`, plus the per-DoF `kSmooth*` rates).
|
|
415
|
+
|
|
400
416
|
### `CesiumMetrics`
|
|
401
417
|
|
|
402
418
|
| Field | Type | Description |
|
|
@@ -239,7 +239,8 @@ void CesiumBridgeAndroid::renderFrame(double dt) {
|
|
|
239
239
|
|
|
240
240
|
const auto cur = engine_->camera().getParams();
|
|
241
241
|
const auto tgt = target_.snapshot();
|
|
242
|
-
const auto smooth = reactnativecesium::CameraSmoother::step(
|
|
242
|
+
const auto smooth = reactnativecesium::CameraSmoother::step(
|
|
243
|
+
cur, tgt, dt, target_.recentIntervalSec());
|
|
243
244
|
engine_->camera().setParams(smooth);
|
|
244
245
|
|
|
245
246
|
// If we converged, clear the dirty flag so the next idle vsync can early-out.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include "EngineTunables.hpp"
|
|
4
4
|
|
|
5
|
+
#include <algorithm>
|
|
5
6
|
#include <cmath>
|
|
6
7
|
|
|
7
8
|
namespace reactnativecesium {
|
|
@@ -23,15 +24,40 @@ inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
|
|
|
23
24
|
return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
inline double wrapLonDeltaDeg(double a, double b) {
|
|
28
|
+
return std::fmod(b - a + 540.0, 360.0) - 180.0;
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
} // namespace
|
|
27
32
|
|
|
28
33
|
CameraParams CameraSmoother::step(
|
|
29
|
-
const CameraParams& current, const CameraParams& target, double dt
|
|
34
|
+
const CameraParams& current, const CameraParams& target, double dt,
|
|
35
|
+
double recentIntervalSec) {
|
|
30
36
|
CameraParams next = current;
|
|
31
37
|
|
|
32
|
-
// lat/lon:
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
35
61
|
|
|
36
62
|
const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
|
|
37
63
|
const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
|
|
@@ -50,6 +76,11 @@ CameraParams CameraSmoother::step(
|
|
|
50
76
|
next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
|
|
51
77
|
|
|
52
78
|
// 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;
|
|
53
84
|
if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
|
|
54
85
|
next.altitude = target.altitude;
|
|
55
86
|
if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
|
|
@@ -7,9 +7,13 @@ namespace reactnativecesium {
|
|
|
7
7
|
// Stateless helper. Given a current camera state and a target camera state,
|
|
8
8
|
// returns the next camera state for a frame of duration `dt` seconds.
|
|
9
9
|
//
|
|
10
|
-
// Per-DoF smoothing rules
|
|
11
|
-
// - lat / lon:
|
|
12
|
-
//
|
|
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.
|
|
13
17
|
// - altitude / heading / pitch / roll / viewCorrection: exponential decay
|
|
14
18
|
// `1 - exp(-k * dt)` per DoF with k taken from EngineTunables.
|
|
15
19
|
// - When the delta drops below the matching epsilon, the value is snapped
|
|
@@ -18,7 +22,8 @@ class CameraSmoother {
|
|
|
18
22
|
public:
|
|
19
23
|
static CameraParams step(const CameraParams& current,
|
|
20
24
|
const CameraParams& target,
|
|
21
|
-
double dt
|
|
25
|
+
double dt,
|
|
26
|
+
double recentIntervalSec);
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
} // namespace reactnativecesium
|
|
@@ -25,4 +25,25 @@ bool CameraTargetState::deltaExceedsEpsilon(
|
|
|
25
25
|
> tunables::kEpsQuatDot;
|
|
26
26
|
}
|
|
27
27
|
|
|
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
|
+
|
|
28
49
|
} // namespace reactnativecesium
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "GlobeCamera.hpp"
|
|
5
5
|
|
|
6
6
|
#include <atomic>
|
|
7
|
+
#include <chrono>
|
|
7
8
|
#include <mutex>
|
|
8
9
|
|
|
9
10
|
namespace reactnativecesium {
|
|
@@ -31,6 +32,7 @@ public:
|
|
|
31
32
|
std::lock_guard<std::mutex> g(mutex_);
|
|
32
33
|
target_ = target;
|
|
33
34
|
}
|
|
35
|
+
noteUpdateCadence();
|
|
34
36
|
dirty_.store(true, std::memory_order_release);
|
|
35
37
|
forceRender_.store(true, std::memory_order_release);
|
|
36
38
|
}
|
|
@@ -47,6 +49,7 @@ public:
|
|
|
47
49
|
target_.pitch = pitch;
|
|
48
50
|
target_.roll = roll;
|
|
49
51
|
}
|
|
52
|
+
noteUpdateCadence();
|
|
50
53
|
dirty_.store(true, std::memory_order_release);
|
|
51
54
|
forceRender_.store(true, std::memory_order_release);
|
|
52
55
|
}
|
|
@@ -56,6 +59,7 @@ public:
|
|
|
56
59
|
std::lock_guard<std::mutex> g(mutex_);
|
|
57
60
|
target_.viewCorrection = q;
|
|
58
61
|
}
|
|
62
|
+
noteUpdateCadence();
|
|
59
63
|
dirty_.store(true, std::memory_order_release);
|
|
60
64
|
forceRender_.store(true, std::memory_order_release);
|
|
61
65
|
}
|
|
@@ -87,11 +91,31 @@ public:
|
|
|
87
91
|
// whether to render this frame.
|
|
88
92
|
static bool deltaExceedsEpsilon(const CameraParams& a, const CameraParams& b);
|
|
89
93
|
|
|
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
|
+
|
|
90
102
|
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
|
+
|
|
91
108
|
mutable std::mutex mutex_;
|
|
92
109
|
CameraParams target_;
|
|
93
110
|
std::atomic<bool> dirty_{false};
|
|
94
111
|
std::atomic<bool> forceRender_{true};
|
|
112
|
+
|
|
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};
|
|
95
119
|
};
|
|
96
120
|
|
|
97
121
|
} // namespace reactnativecesium
|
|
@@ -53,6 +53,21 @@ inline constexpr double kEpsAltitudeMeters = 0.1;
|
|
|
53
53
|
inline constexpr double kEpsAngleDeg = 0.05;
|
|
54
54
|
inline constexpr double kEpsQuatDot = 1e-8;
|
|
55
55
|
|
|
56
|
+
// Lat/lon are smoothed adaptively: τ = clamp(α · ewmaInterval, τmin, τmax),
|
|
57
|
+
// then per frame we apply 1 - exp(-dt/τ). α<1 means the camera converges
|
|
58
|
+
// before the next sample lands. With ~60 Hz gestures the EWMA interval is
|
|
59
|
+
// ~16 ms ⇒ τ at the floor (5 ms) ⇒ ≤1-frame trail (indistinguishable from
|
|
60
|
+
// the previous snap). With a 1 Hz GPS feed τ ≈ 450 ms ⇒ ~95 % converged
|
|
61
|
+
// before the next sample arrives, removing the per-second teleport.
|
|
62
|
+
inline constexpr double kSmoothPositionAlpha = 0.45;
|
|
63
|
+
inline constexpr double kSmoothPositionTauMin = 0.005;
|
|
64
|
+
inline constexpr double kSmoothPositionTauMax = 0.6;
|
|
65
|
+
// Genuine teleports (e.g. setCamera(NewYork) → setCamera(Tokyo)) skip the
|
|
66
|
+
// smoother — easing across the planet over 600 ms looks worse than a snap.
|
|
67
|
+
inline constexpr double kPosSnapThroughDeg = 0.5;
|
|
68
|
+
// EWMA mixing factor for the inter-arrival interval of setCamera calls.
|
|
69
|
+
inline constexpr double kEwmaIntervalAlpha = 0.3;
|
|
70
|
+
|
|
56
71
|
// Metrics throttle: emit telemetry every N rendered frames.
|
|
57
72
|
inline constexpr int kMetricsEmitEveryFrames = 20;
|
|
58
73
|
|
package/ios/CesiumBridge.mm
CHANGED
|
@@ -271,7 +271,8 @@
|
|
|
271
271
|
|
|
272
272
|
const auto cur = _engine->camera().getParams();
|
|
273
273
|
const auto tgt = _target.snapshot();
|
|
274
|
-
const auto smooth = reactnativecesium::CameraSmoother::step(
|
|
274
|
+
const auto smooth = reactnativecesium::CameraSmoother::step(
|
|
275
|
+
cur, tgt, dt, _target.recentIntervalSec());
|
|
275
276
|
_engine->camera().setParams(smooth);
|
|
276
277
|
|
|
277
278
|
if (!reactnativecesium::CameraTargetState::deltaExceedsEpsilon(smooth, tgt)) {
|
package/package.json
CHANGED