react-native-cesium 1.2.1 → 1.2.3
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 +6 -5
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +1 -2
- package/cpp/engine/CameraSmoother.cpp +16 -47
- package/cpp/engine/CameraSmoother.hpp +12 -13
- package/cpp/engine/CameraTargetState.cpp +177 -21
- package/cpp/engine/CameraTargetState.hpp +63 -58
- package/cpp/engine/CesiumEngine.cpp +3 -1
- package/cpp/engine/EngineTunables.hpp +25 -13
- package/ios/CesiumBridge.mm +1 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -399,14 +399,15 @@ 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
|
|
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` | **
|
|
407
|
-
| `altitude` |
|
|
408
|
-
| `heading`
|
|
409
|
-
| `
|
|
406
|
+
| `latitude` / `longitude` | **Linear interpolation** — own independent EWMA. Constant-velocity glide at GPS cadence; near-instant at gesture rate. Cross-planet jumps (Δ > ~0.5°) bypass interpolation. Antimeridian crossings always rotate the short way. |
|
|
407
|
+
| `altitude` | **Linear interpolation** — own independent EWMA. Self-tunes whether altitude comes from GPS (1 Hz → 1 s glide) or a barometer (50 Hz → ~20 ms, near-instant). Jumps > 500 m bypass interpolation. |
|
|
408
|
+
| `heading` | **Linear interpolation** — own independent EWMA. Constant yaw-rate between GPS course-over-ground fixes; near-instant when heading comes from a magnetometer at high rate. Jumps > 90° bypass interpolation. |
|
|
409
|
+
| `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20 ms`. Almost always attitude-sensor driven; ease-out feel is appropriate. |
|
|
410
|
+
| `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected. |
|
|
410
411
|
| `verticalFovDeg` | Applied directly (not smoothed). |
|
|
411
412
|
|
|
412
413
|
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.
|
|
@@ -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,67 +23,37 @@ 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
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
}
|
|
61
|
-
|
|
62
|
-
const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
|
|
63
|
-
const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
|
|
32
|
+
// lat / lon / altitude / heading: snap directly to the (already linearly-
|
|
33
|
+
// interpolated) target from CameraTargetState::snapshot(). The constant-
|
|
34
|
+
// velocity motion is fully handled there; no additional smoothing is applied
|
|
35
|
+
// here so the movement stays linear rather than decelerating.
|
|
36
|
+
next.latitude = target.latitude;
|
|
37
|
+
next.longitude = target.longitude;
|
|
38
|
+
next.altitude = target.altitude;
|
|
39
|
+
next.heading = target.heading;
|
|
40
|
+
|
|
41
|
+
// pitch / roll / viewCorrection: exponential ease-out. These are almost
|
|
42
|
+
// always driven by high-rate attitude sensors (accelerometer, gyroscope)
|
|
43
|
+
// where the ease-out feel is appropriate and desired.
|
|
64
44
|
const double aPitch = 1.0 - std::exp(-tunables::kSmoothPitch * dt);
|
|
65
45
|
const double aRoll = 1.0 - std::exp(-tunables::kSmoothRoll * dt);
|
|
66
46
|
const double aViewQ = 1.0 - std::exp(-tunables::kSmoothViewCorr * dt);
|
|
67
47
|
|
|
68
|
-
next.
|
|
69
|
-
next.
|
|
70
|
-
next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
|
|
71
|
-
next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
|
|
48
|
+
next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
|
|
49
|
+
next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
|
|
72
50
|
|
|
73
51
|
glm::dquat cq = glm::normalize(current.viewCorrection);
|
|
74
52
|
glm::dquat tq = glm::normalize(target.viewCorrection);
|
|
75
53
|
if (glm::dot(cq, tq) < 0.0) tq = -tq;
|
|
76
54
|
next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
|
|
77
55
|
|
|
78
|
-
// Eventually-exact convergence: snap once
|
|
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
|
-
if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
|
|
85
|
-
next.altitude = target.altitude;
|
|
86
|
-
if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
|
|
87
|
-
next.heading = target.heading;
|
|
56
|
+
// Eventually-exact convergence for exponential DoFs: snap once sub-epsilon.
|
|
88
57
|
if (angleDeltaAbsDeg(next.pitch, target.pitch) <= tunables::kEpsAngleDeg)
|
|
89
58
|
next.pitch = target.pitch;
|
|
90
59
|
if (angleDeltaAbsDeg(next.roll, target.roll) <= tunables::kEpsAngleDeg)
|
|
@@ -7,23 +7,22 @@ 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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
10
|
+
// lat / lon / altitude / heading are snapped directly to the target.
|
|
11
|
+
// CameraTargetState::snapshot() has already linearly interpolated those DoFs
|
|
12
|
+
// to their "current" position based on each DoF's own EWMA inter-arrival
|
|
13
|
+
// interval, so snapping here gives constant-velocity motion between fixes
|
|
14
|
+
// with no ease-in/ease-out. At gesture rates (~60 Hz) the EWMA collapses to
|
|
15
|
+
// ~16 ms so the snap is indistinguishable from the previous behaviour.
|
|
16
|
+
//
|
|
17
|
+
// pitch / roll / viewCorrection use fixed-rate exponential decay
|
|
18
|
+
// `1 - exp(-k * dt)` with k from EngineTunables — appropriate for high-rate
|
|
19
|
+
// attitude sensors where the ease-out feel is desirable.
|
|
20
|
+
// Once a DoF drops below its matching epsilon it is snapped to exact equality.
|
|
21
21
|
class CameraSmoother {
|
|
22
22
|
public:
|
|
23
23
|
static CameraParams step(const CameraParams& current,
|
|
24
24
|
const CameraParams& target,
|
|
25
|
-
double dt
|
|
26
|
-
double recentIntervalSec);
|
|
25
|
+
double dt);
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
} // namespace reactnativecesium
|
|
@@ -1,18 +1,195 @@
|
|
|
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 wrapped to [-180, 180].
|
|
19
|
+
inline double shortAngleDeltaDeg(double from, double to) {
|
|
20
|
+
return std::fmod(to - from + 540.0, 360.0) - 180.0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Advance an EWMA with a new sample, or seed it on first use.
|
|
24
|
+
inline void updateEwma(double& ewma, bool& active,
|
|
25
|
+
std::chrono::steady_clock::time_point& lastTime,
|
|
26
|
+
const std::chrono::steady_clock::time_point& now,
|
|
27
|
+
double seedSec) {
|
|
28
|
+
if (!active) {
|
|
29
|
+
ewma = seedSec;
|
|
30
|
+
lastTime = now;
|
|
31
|
+
active = true;
|
|
32
|
+
} else {
|
|
33
|
+
const double dt = std::chrono::duration<double>(now - lastTime).count();
|
|
34
|
+
if (dt > 0.0) {
|
|
35
|
+
ewma = ewma + tunables::kEwmaIntervalAlpha * (dt - ewma);
|
|
36
|
+
}
|
|
37
|
+
lastTime = now;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
} // namespace
|
|
15
42
|
|
|
43
|
+
// ── Public writers ────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
void CameraTargetState::setAll(const CameraParams& target) {
|
|
46
|
+
{
|
|
47
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
48
|
+
target_ = target;
|
|
49
|
+
// Deliberately do NOT touch any interpolation state: setAll is init-only
|
|
50
|
+
// and the first rendered frame must snap to the requested position.
|
|
51
|
+
}
|
|
52
|
+
dirty_.store(true, std::memory_order_release);
|
|
53
|
+
forceRender_.store(true, std::memory_order_release);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void CameraTargetState::setHpr(double lat, double lon, double alt,
|
|
57
|
+
double heading, double pitch, double roll) {
|
|
58
|
+
{
|
|
59
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
60
|
+
|
|
61
|
+
const auto now = std::chrono::steady_clock::now();
|
|
62
|
+
|
|
63
|
+
// ── lat / lon ─────────────────────────────────────────────────────────
|
|
64
|
+
const bool posChanged =
|
|
65
|
+
std::abs(lat - target_.latitude) > tunables::kEpsLatLon ||
|
|
66
|
+
std::abs(lon - target_.longitude) > tunables::kEpsLatLon;
|
|
67
|
+
|
|
68
|
+
if (posChanged) {
|
|
69
|
+
if (!hasPosInterp_) {
|
|
70
|
+
// First runtime change: seed "from" = destination for a clean snap,
|
|
71
|
+
// then enable interpolation for subsequent updates.
|
|
72
|
+
posFromLat_ = lat;
|
|
73
|
+
posFromLon_ = lon;
|
|
74
|
+
updateEwma(posEwmaIntervalSec_, hasPosInterp_, posUpdateTime_, now,
|
|
75
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
76
|
+
} else {
|
|
77
|
+
posFromLat_ = target_.latitude;
|
|
78
|
+
posFromLon_ = target_.longitude;
|
|
79
|
+
updateEwma(posEwmaIntervalSec_, hasPosInterp_, posUpdateTime_, now,
|
|
80
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── altitude ──────────────────────────────────────────────────────────
|
|
85
|
+
const bool altChanged =
|
|
86
|
+
std::abs(alt - target_.altitude) > tunables::kEpsAltitudeMeters;
|
|
87
|
+
|
|
88
|
+
if (altChanged) {
|
|
89
|
+
if (!hasAltInterp_) {
|
|
90
|
+
altFrom_ = alt;
|
|
91
|
+
updateEwma(altEwmaIntervalSec_, hasAltInterp_, altUpdateTime_, now,
|
|
92
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
93
|
+
} else {
|
|
94
|
+
altFrom_ = target_.altitude;
|
|
95
|
+
updateEwma(altEwmaIntervalSec_, hasAltInterp_, altUpdateTime_, now,
|
|
96
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── heading ───────────────────────────────────────────────────────────
|
|
101
|
+
const bool hdgChanged =
|
|
102
|
+
angleDeltaAbsDeg(heading, target_.heading) > tunables::kEpsAngleDeg;
|
|
103
|
+
|
|
104
|
+
if (hdgChanged) {
|
|
105
|
+
if (!hasHdgInterp_) {
|
|
106
|
+
hdgFrom_ = heading;
|
|
107
|
+
updateEwma(hdgEwmaIntervalSec_, hasHdgInterp_, hdgUpdateTime_, now,
|
|
108
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
109
|
+
} else {
|
|
110
|
+
hdgFrom_ = target_.heading;
|
|
111
|
+
updateEwma(hdgEwmaIntervalSec_, hasHdgInterp_, hdgUpdateTime_, now,
|
|
112
|
+
tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
target_.latitude = lat;
|
|
117
|
+
target_.longitude = lon;
|
|
118
|
+
target_.altitude = alt;
|
|
119
|
+
target_.heading = heading;
|
|
120
|
+
target_.pitch = pitch;
|
|
121
|
+
target_.roll = roll;
|
|
122
|
+
}
|
|
123
|
+
dirty_.store(true, std::memory_order_release);
|
|
124
|
+
forceRender_.store(true, std::memory_order_release);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void CameraTargetState::setViewCorrection(const glm::dquat& q) {
|
|
128
|
+
{
|
|
129
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
130
|
+
target_.viewCorrection = q;
|
|
131
|
+
// Interpolation state is not touched — viewCorrection carries no
|
|
132
|
+
// positional information and must not affect any of the three EWMAs.
|
|
133
|
+
}
|
|
134
|
+
dirty_.store(true, std::memory_order_release);
|
|
135
|
+
forceRender_.store(true, std::memory_order_release);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Snapshot (render-thread read) ─────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
CameraParams CameraTargetState::snapshot() const {
|
|
141
|
+
const auto now = std::chrono::steady_clock::now();
|
|
142
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
143
|
+
|
|
144
|
+
CameraParams result = target_;
|
|
145
|
+
|
|
146
|
+
// ── lat / lon ─────────────────────────────────────────────────────────────
|
|
147
|
+
if (hasPosInterp_ && posEwmaIntervalSec_ > 0.0) {
|
|
148
|
+
const double dLat = target_.latitude - posFromLat_;
|
|
149
|
+
const double dLon = shortAngleDeltaDeg(posFromLon_, target_.longitude);
|
|
150
|
+
|
|
151
|
+
const bool snapThrough =
|
|
152
|
+
std::abs(dLat) > tunables::kPosSnapThroughDeg ||
|
|
153
|
+
std::abs(dLon) > tunables::kPosSnapThroughDeg;
|
|
154
|
+
|
|
155
|
+
if (!snapThrough) {
|
|
156
|
+
const double elapsed =
|
|
157
|
+
std::chrono::duration<double>(now - posUpdateTime_).count();
|
|
158
|
+
const double t = std::min(elapsed / posEwmaIntervalSec_, 1.0);
|
|
159
|
+
result.latitude = posFromLat_ + t * dLat;
|
|
160
|
+
result.longitude = posFromLon_ + t * dLon;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── altitude ──────────────────────────────────────────────────────────────
|
|
165
|
+
if (hasAltInterp_ && altEwmaIntervalSec_ > 0.0) {
|
|
166
|
+
const double dAlt = target_.altitude - altFrom_;
|
|
167
|
+
|
|
168
|
+
if (std::abs(dAlt) <= tunables::kAltSnapThroughMeters) {
|
|
169
|
+
const double elapsed =
|
|
170
|
+
std::chrono::duration<double>(now - altUpdateTime_).count();
|
|
171
|
+
const double t = std::min(elapsed / altEwmaIntervalSec_, 1.0);
|
|
172
|
+
result.altitude = altFrom_ + t * dAlt;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── heading ───────────────────────────────────────────────────────────────
|
|
177
|
+
if (hasHdgInterp_ && hdgEwmaIntervalSec_ > 0.0) {
|
|
178
|
+
const double dHdg = shortAngleDeltaDeg(hdgFrom_, target_.heading);
|
|
179
|
+
|
|
180
|
+
if (std::abs(dHdg) <= tunables::kHdgSnapThroughDeg) {
|
|
181
|
+
const double elapsed =
|
|
182
|
+
std::chrono::duration<double>(now - hdgUpdateTime_).count();
|
|
183
|
+
const double t = std::min(elapsed / hdgEwmaIntervalSec_, 1.0);
|
|
184
|
+
result.heading = hdgFrom_ + t * dHdg;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Epsilon check (shared) ────────────────────────────────────────────────────
|
|
192
|
+
|
|
16
193
|
bool CameraTargetState::deltaExceedsEpsilon(
|
|
17
194
|
const CameraParams& a, const CameraParams& b) {
|
|
18
195
|
return std::abs(a.latitude - b.latitude) > tunables::kEpsLatLon
|
|
@@ -25,25 +202,4 @@ bool CameraTargetState::deltaExceedsEpsilon(
|
|
|
25
202
|
> tunables::kEpsQuatDot;
|
|
26
203
|
}
|
|
27
204
|
|
|
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
205
|
} // namespace reactnativecesium
|
|
@@ -21,54 +21,51 @@ 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
|
+
// Per-DoF linear interpolation (lat/lon, altitude, heading)
|
|
26
|
+
// ──────────────────────────────────────────────────────────
|
|
27
|
+
// Each of lat/lon, altitude, and heading maintains its own independent EWMA
|
|
28
|
+
// of inter-arrival times and only updates that EWMA when *its own value*
|
|
29
|
+
// actually changes. snapshot() then linearly interpolates each DoF from its
|
|
30
|
+
// previous value to its current target over its own EWMA interval.
|
|
31
|
+
//
|
|
32
|
+
// This means lat/lon, altitude, and heading each self-tune independently:
|
|
33
|
+
// - 60 Hz gesture input → EWMA ~16 ms → interpolation near-instant (snap)
|
|
34
|
+
// - 1 Hz GPS → EWMA ~1 s → constant-velocity glide between fixes
|
|
35
|
+
// - 50 Hz barometer for altitude + 1 Hz GPS for lat/lon → altitude behaves
|
|
36
|
+
// near-instant, lat/lon glides — with no special-casing needed.
|
|
37
|
+
//
|
|
38
|
+
// Special cases in snapshot():
|
|
39
|
+
// - t >= 1: camera has reached the target, returns target_ directly.
|
|
40
|
+
// - Snap-through guard: large jumps bypass interpolation so deliberate
|
|
41
|
+
// teleports remain instant.
|
|
42
|
+
// - First change per DoF: "from" = destination, so lerp(dest, dest, t) = dest
|
|
43
|
+
// for all t — clean snap, no startup glide.
|
|
44
|
+
// - setAll (init-only): never touches any interpolation state.
|
|
45
|
+
//
|
|
46
|
+
// pitch / roll / viewCorrection are NOT interpolated here; CameraSmoother
|
|
47
|
+
// applies fixed-rate exponential curves to them (appropriate for high-rate
|
|
48
|
+
// attitude sensors).
|
|
24
49
|
class CameraTargetState {
|
|
25
50
|
public:
|
|
26
51
|
CameraTargetState() = default;
|
|
27
52
|
|
|
28
|
-
// Replace the entire target
|
|
29
|
-
//
|
|
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
|
-
}
|
|
53
|
+
// Replace the entire target. Used only for engine initialisation; does NOT
|
|
54
|
+
// update any interpolation state so the first rendered frame snaps.
|
|
55
|
+
void setAll(const CameraParams& target);
|
|
39
56
|
|
|
40
57
|
// Update lat/lon/alt + heading/pitch/roll, leaving viewCorrection unchanged.
|
|
58
|
+
// Independently tracks cadence for lat/lon, altitude, and heading.
|
|
41
59
|
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
|
-
}
|
|
60
|
+
double heading, double pitch, double roll);
|
|
56
61
|
|
|
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
|
-
}
|
|
62
|
+
void setViewCorrection(const glm::dquat& q);
|
|
66
63
|
|
|
67
|
-
// Snapshot the demand target
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
// Snapshot the demand target for this render frame. lat/lon, altitude, and
|
|
65
|
+
// heading are linearly interpolated to their "current" position based on
|
|
66
|
+
// each DoF's own EWMA interval. pitch/roll/viewCorrection are returned as-is
|
|
67
|
+
// (CameraSmoother handles their exponential curves).
|
|
68
|
+
CameraParams snapshot() const;
|
|
72
69
|
|
|
73
70
|
// Lightweight, lock-free check used by the render thread to decide whether
|
|
74
71
|
// it should bother taking a full snapshot at all.
|
|
@@ -91,31 +88,39 @@ public:
|
|
|
91
88
|
// whether to render this frame.
|
|
92
89
|
static bool deltaExceedsEpsilon(const CameraParams& a, const CameraParams& b);
|
|
93
90
|
|
|
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
91
|
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
92
|
mutable std::mutex mutex_;
|
|
109
93
|
CameraParams target_;
|
|
110
94
|
std::atomic<bool> dirty_{false};
|
|
111
95
|
std::atomic<bool> forceRender_{true};
|
|
112
96
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
97
|
+
// ── Per-DoF linear interpolation state (all protected by mutex_) ─────────
|
|
98
|
+
// Each DoF has: a "from" value, a wall-clock timestamp of the last change,
|
|
99
|
+
// an EWMA of inter-arrival times, and an "active" flag.
|
|
100
|
+
//
|
|
101
|
+
// Pattern per DoF:
|
|
102
|
+
// First change: from = destination (snap), seed EWMA, set active.
|
|
103
|
+
// Subsequent change: from = old target, update EWMA, reset clock.
|
|
104
|
+
// snapshot(): t = clamp(elapsed/ewma, 0, 1); result = from + t*delta.
|
|
105
|
+
|
|
106
|
+
// lat / lon
|
|
107
|
+
double posFromLat_{0.0};
|
|
108
|
+
double posFromLon_{0.0};
|
|
109
|
+
std::chrono::steady_clock::time_point posUpdateTime_{};
|
|
110
|
+
double posEwmaIntervalSec_{0.0};
|
|
111
|
+
bool hasPosInterp_{false};
|
|
112
|
+
|
|
113
|
+
// altitude
|
|
114
|
+
double altFrom_{0.0};
|
|
115
|
+
std::chrono::steady_clock::time_point altUpdateTime_{};
|
|
116
|
+
double altEwmaIntervalSec_{0.0};
|
|
117
|
+
bool hasAltInterp_{false};
|
|
118
|
+
|
|
119
|
+
// heading
|
|
120
|
+
double hdgFrom_{0.0};
|
|
121
|
+
std::chrono::steady_clock::time_point hdgUpdateTime_{};
|
|
122
|
+
double hdgEwmaIntervalSec_{0.0};
|
|
123
|
+
bool hasHdgInterp_{false};
|
|
119
124
|
};
|
|
120
125
|
|
|
121
126
|
} // namespace reactnativecesium
|
|
@@ -434,7 +434,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
|
|
|
434
434
|
asyncSystem_.dispatchMainThreadTasks();
|
|
435
435
|
|
|
436
436
|
const auto viewState = camera_.computeViewState(w, h);
|
|
437
|
-
const auto& updateResult =
|
|
437
|
+
const auto& updateResult =
|
|
438
|
+
tileset_->updateViewGroup(tileset_->getDefaultViewGroup(), {viewState});
|
|
439
|
+
tileset_->loadTiles();
|
|
438
440
|
|
|
439
441
|
// Only emit the fallback ellipsoid when no real terrain is being rendered
|
|
440
442
|
// this frame. Otherwise it would be over-drawn anyway (~8k vertices and
|
|
@@ -43,31 +43,43 @@ inline constexpr int32_t kDefaultLoadingDescendantLimit = 20;
|
|
|
43
43
|
|
|
44
44
|
// Camera smoothing constants — shared between platform bridges so jank /
|
|
45
45
|
// motion feel is identical on iOS and Android.
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
//
|
|
47
|
+
// lat / lon / altitude / heading use per-DoF linear interpolation inside
|
|
48
|
+
// CameraTargetState::snapshot(): each DoF tracks its own EWMA of inter-arrival
|
|
49
|
+
// times and linearly lerps from its previous value to its current target over
|
|
50
|
+
// that interval. At ~60 Hz (gestures) the EWMA collapses to ~16 ms so the
|
|
51
|
+
// motion is indistinguishable from a snap; at 1 Hz (GPS) the DoF glides at
|
|
52
|
+
// constant velocity between fixes — no teleports or ease-out deceleration.
|
|
53
|
+
// pitch / roll / viewCorrection remain on their fixed exponential curves
|
|
54
|
+
// because they are almost always driven by high-rate attitude sensors.
|
|
55
|
+
|
|
56
|
+
// Exponential smoothing for pitch / roll / viewCorrection (k = 1/τ).
|
|
48
57
|
inline constexpr double kSmoothPitch = 50.0;
|
|
49
58
|
inline constexpr double kSmoothRoll = 50.0;
|
|
50
59
|
inline constexpr double kSmoothViewCorr = 50.0;
|
|
60
|
+
|
|
61
|
+
// Convergence epsilons (used by deltaExceedsEpsilon and snap-to-exact).
|
|
51
62
|
inline constexpr double kEpsLatLon = 1e-7;
|
|
52
63
|
inline constexpr double kEpsAltitudeMeters = 0.1;
|
|
53
64
|
inline constexpr double kEpsAngleDeg = 0.05;
|
|
54
65
|
inline constexpr double kEpsQuatDot = 1e-8;
|
|
55
66
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
67
|
+
// Linear interpolation parameters shared by lat/lon, altitude, and heading.
|
|
68
|
+
//
|
|
69
|
+
// kSmoothPositionTauMax / kSmoothPositionAlpha is the seed value for the EWMA
|
|
70
|
+
// on the first update (gives a generous interpolation window before the cadence
|
|
71
|
+
// is known). kEwmaIntervalAlpha controls how quickly the EWMA adapts to a rate
|
|
72
|
+
// change (0.3 = ~3-5 samples to converge to a new cadence).
|
|
62
73
|
inline constexpr double kSmoothPositionAlpha = 0.45;
|
|
63
|
-
inline constexpr double kSmoothPositionTauMin = 0.005;
|
|
64
74
|
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
75
|
inline constexpr double kEwmaIntervalAlpha = 0.3;
|
|
70
76
|
|
|
77
|
+
// Snap-through guards: deltas larger than these skip interpolation so a
|
|
78
|
+
// deliberate teleport (e.g. setCamera(NewYork → Tokyo)) remains instant.
|
|
79
|
+
inline constexpr double kPosSnapThroughDeg = 0.5; // lat or lon, ~55 km
|
|
80
|
+
inline constexpr double kAltSnapThroughMeters = 500.0; // altitude
|
|
81
|
+
inline constexpr double kHdgSnapThroughDeg = 90.0; // heading
|
|
82
|
+
|
|
71
83
|
// Metrics throttle: emit telemetry every N rendered frames.
|
|
72
84
|
inline constexpr int kMetricsEmitEveryFrames = 20;
|
|
73
85
|
|
package/ios/CesiumBridge.mm
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.2.3",
|
|
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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
80
80
|
"nitrogen": "0.35.6",
|
|
81
81
|
"react": "19.2.3",
|
|
82
|
-
"react-native": "0.85.
|
|
82
|
+
"react-native": "0.85.3",
|
|
83
83
|
"react-native-builder-bob": "^0.41.0",
|
|
84
84
|
"semantic-release": "^25.0.3",
|
|
85
85
|
"typescript": "^5.9.3"
|