react-native-cesium 1.2.2 → 1.2.4
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 +100 -26
- package/android/CMakeLists.txt +1 -2
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +264 -130
- package/android/src/main/cpp/CesiumBridgeAndroid.h +70 -27
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +68 -44
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +160 -187
- package/cpp/engine/CameraIntegrator.cpp +374 -0
- package/cpp/engine/CameraIntegrator.hpp +146 -0
- package/cpp/engine/CesiumEngine.cpp +3 -1
- package/cpp/engine/EngineTunables.hpp +80 -21
- package/cpp/engine/GlobeCamera.cpp +17 -6
- package/cpp/engine/GlobeCamera.hpp +11 -8
- package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
- package/cpp/engine/test/build_and_run.sh +27 -0
- package/example/README.md +1 -1
- package/ios/CesiumBridge.h +53 -28
- package/ios/CesiumBridge.mm +172 -86
- package/ios/HybridCesiumView.swift +213 -145
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
- package/package.json +2 -2
- package/src/specs/cesium-view.nitro.ts +93 -14
- package/cpp/engine/CameraSmoother.cpp +0 -71
- package/cpp/engine/CameraSmoother.hpp +0 -25
- package/cpp/engine/CameraTargetState.cpp +0 -140
- package/cpp/engine/CameraTargetState.hpp +0 -103
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#include "CameraIntegrator.hpp"
|
|
2
|
+
|
|
3
|
+
#include "EngineTunables.hpp"
|
|
4
|
+
|
|
5
|
+
#include <glm/gtc/quaternion.hpp>
|
|
6
|
+
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <cmath>
|
|
9
|
+
|
|
10
|
+
namespace reactnativecesium {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
// WGS84 mean radius (m). Hard-coded so the integrator can be unit-tested in
|
|
15
|
+
// isolation without linking CesiumGeospatial. Matches Ellipsoid::WGS84.
|
|
16
|
+
inline constexpr double kEarthMeanRadiusM = 6371008.7714150595;
|
|
17
|
+
|
|
18
|
+
inline double shortAngleDeltaDeg(double from, double to) {
|
|
19
|
+
return std::fmod(to - from + 540.0, 360.0) - 180.0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
inline double angleAbsDeltaDeg(double a, double b) {
|
|
23
|
+
return std::abs(shortAngleDeltaDeg(a, b));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
inline double wrap360(double deg) {
|
|
27
|
+
double v = std::fmod(deg, 360.0);
|
|
28
|
+
if (v < 0.0) v += 360.0;
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
inline double clampAbs(double v, double maxAbs) {
|
|
33
|
+
if (maxAbs <= 0.0) return v;
|
|
34
|
+
if (v > maxAbs) return maxAbs;
|
|
35
|
+
if (v < -maxAbs) return -maxAbs;
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
|
|
40
|
+
return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve a runtime rate cap (caps.X) against the compile-time default.
|
|
44
|
+
// 0.0 means "no override" so we fall back to the EngineTunables value, which
|
|
45
|
+
// may itself be 0.0 (i.e. uncapped). Negative values are treated as 0.
|
|
46
|
+
inline double resolveCap(double rt, double dflt) {
|
|
47
|
+
return rt > 0.0 ? rt : (dflt > 0.0 ? dflt : 0.0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} // namespace
|
|
51
|
+
|
|
52
|
+
CameraIntegrator::CameraIntegrator(const CameraParams& initial) {
|
|
53
|
+
teleport(initial);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── α-β core ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
void CameraIntegrator::updateAlphaBeta(
|
|
59
|
+
AlphaBeta& s, double z, TimePoint tz,
|
|
60
|
+
double alpha, double beta, bool wrapAngle) {
|
|
61
|
+
// Predict the value at the time the measurement arrived. On the very first
|
|
62
|
+
// call (tState default-constructed = epoch) the predicted value is the
|
|
63
|
+
// current state (velocity == 0), so the result is identical to a snap.
|
|
64
|
+
const double dt = std::chrono::duration<double>(tz - s.tState).count();
|
|
65
|
+
const double pred = s.value + s.velocity * std::max(dt, 0.0);
|
|
66
|
+
|
|
67
|
+
const double residual =
|
|
68
|
+
wrapAngle ? shortAngleDeltaDeg(pred, z) : (z - pred);
|
|
69
|
+
|
|
70
|
+
s.value = pred + alpha * residual;
|
|
71
|
+
|
|
72
|
+
// Velocity update is gated on a meaningful time gap. Two reasons:
|
|
73
|
+
// - Bootstrap: a teleport(...) followed immediately by a setter in the
|
|
74
|
+
// same microtask has dt ≈ 0. Dividing β by that produces an absurd
|
|
75
|
+
// velocity estimate ("you climbed 100 m in zero seconds").
|
|
76
|
+
// - Same-tick burst writes (e.g. setAltitude + setHeading inside a
|
|
77
|
+
// single useAnimatedReaction tick): treat them as coincident updates
|
|
78
|
+
// to two independent DoFs, not as a sample of the rate of change.
|
|
79
|
+
//
|
|
80
|
+
// 5 ms is well below any plausible sensor sample interval (max useful
|
|
81
|
+
// rate ~200 Hz) but comfortably above the coincident-call window. For
|
|
82
|
+
// longer gaps we also floor dt to 20 ms when computing β/dt so a single
|
|
83
|
+
// sub-frame measurement does not blow up the velocity estimate.
|
|
84
|
+
constexpr double kVelocityMinDtSec = 0.005;
|
|
85
|
+
constexpr double kVelocityFloorDtSec = 0.020;
|
|
86
|
+
if (dt > kVelocityMinDtSec) {
|
|
87
|
+
const double dtForVel = std::max(dt, kVelocityFloorDtSec);
|
|
88
|
+
s.velocity += (beta / dtForVel) * residual;
|
|
89
|
+
}
|
|
90
|
+
s.tState = tz;
|
|
91
|
+
|
|
92
|
+
// Inter-arrival EWMA — used by the silence-aware velocity bleed in
|
|
93
|
+
// extrapolate(). The first setter after a teleport seeds tLastMeas
|
|
94
|
+
// without contributing to the EWMA (sentinel-epoch check). Sub-millisecond
|
|
95
|
+
// gaps are also skipped so a same-tick burst of writes does not pull the
|
|
96
|
+
// mean toward zero.
|
|
97
|
+
if (s.tLastMeas.time_since_epoch().count() > 0) {
|
|
98
|
+
const double interval = std::chrono::duration<double>(tz - s.tLastMeas).count();
|
|
99
|
+
if (interval > 1e-3) {
|
|
100
|
+
s.meanIntervalSec =
|
|
101
|
+
(1.0 - tunables::kIntervalEwmaAlpha) * s.meanIntervalSec
|
|
102
|
+
+ tunables::kIntervalEwmaAlpha * interval;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
s.tLastMeas = tz;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void CameraIntegrator::extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs) {
|
|
109
|
+
double dt = std::chrono::duration<double>(now - s.tState).count();
|
|
110
|
+
if (dt <= 0.0) {
|
|
111
|
+
s.tState = now;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (dt > tunables::kMaxFrameDtSec) {
|
|
115
|
+
dt = tunables::kMaxFrameDtSec;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Silence-aware velocity bleed. Once measurements have stopped arriving
|
|
119
|
+
// for this DoF (e.g. the user released a pan gesture, or the joystick
|
|
120
|
+
// returned to centre), bleed the learned velocity exponentially. Both the
|
|
121
|
+
// grace window and the decay τ scale with the EWMA of inter-arrival times
|
|
122
|
+
// so a 60 Hz worklet bleeds in ~50 ms while a 1 Hz GPS glide is left
|
|
123
|
+
// alone between fixes.
|
|
124
|
+
if (s.tLastMeas.time_since_epoch().count() > 0) {
|
|
125
|
+
const double silence = std::chrono::duration<double>(now - s.tLastMeas).count();
|
|
126
|
+
const double grace = tunables::kVelocitySilenceGraceFactor * s.meanIntervalSec;
|
|
127
|
+
if (silence > grace) {
|
|
128
|
+
const double tau = tunables::kVelocityBleedTauFactor * s.meanIntervalSec;
|
|
129
|
+
if (tau > 1.0e-6) {
|
|
130
|
+
s.velocity *= std::exp(-dt / tau);
|
|
131
|
+
} else {
|
|
132
|
+
s.velocity = 0.0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (maxRateAbs > 0.0) {
|
|
138
|
+
s.velocity = clampAbs(s.velocity, maxRateAbs);
|
|
139
|
+
}
|
|
140
|
+
s.value += s.velocity * dt;
|
|
141
|
+
s.tState = now;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Demand setters ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
void CameraIntegrator::setPosition(double latitudeDeg, double longitudeDeg) {
|
|
147
|
+
const auto now = Clock::now();
|
|
148
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
149
|
+
|
|
150
|
+
if (tunables::kOutlierLatLonDeg > 0.0) {
|
|
151
|
+
if (std::abs(latitudeDeg - lat_.value) > tunables::kOutlierLatLonDeg ||
|
|
152
|
+
std::abs(shortAngleDeltaDeg(lon_.value, longitudeDeg))
|
|
153
|
+
> tunables::kOutlierLatLonDeg) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
updateAlphaBeta(lat_, latitudeDeg, now,
|
|
159
|
+
tunables::kAlphaPos, tunables::kBetaPos, false);
|
|
160
|
+
updateAlphaBeta(lon_, longitudeDeg, now,
|
|
161
|
+
tunables::kAlphaPos, tunables::kBetaPos, true);
|
|
162
|
+
demand_.latitude = latitudeDeg;
|
|
163
|
+
demand_.longitude = longitudeDeg;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void CameraIntegrator::setAltitude(double altitudeMeters) {
|
|
167
|
+
const auto now = Clock::now();
|
|
168
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
169
|
+
|
|
170
|
+
if (tunables::kOutlierAltMeters > 0.0 &&
|
|
171
|
+
std::abs(altitudeMeters - alt_.value) > tunables::kOutlierAltMeters) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateAlphaBeta(alt_, altitudeMeters, now,
|
|
176
|
+
tunables::kAlphaAlt, tunables::kBetaAlt, false);
|
|
177
|
+
demand_.altitude = altitudeMeters;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void CameraIntegrator::setHeading(double headingDeg) {
|
|
181
|
+
const auto now = Clock::now();
|
|
182
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
183
|
+
|
|
184
|
+
if (tunables::kOutlierHeadingDeg > 0.0 &&
|
|
185
|
+
angleAbsDeltaDeg(headingDeg, hdg_.value) > tunables::kOutlierHeadingDeg) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
updateAlphaBeta(hdg_, headingDeg, now,
|
|
190
|
+
tunables::kAlphaHdg, tunables::kBetaHdg, true);
|
|
191
|
+
hdg_.value = wrap360(hdg_.value);
|
|
192
|
+
demand_.heading = headingDeg;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void CameraIntegrator::setAttitude(double pitchDeg, double rollDeg) {
|
|
196
|
+
const auto now = Clock::now();
|
|
197
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
198
|
+
|
|
199
|
+
updateAlphaBeta(pitch_, pitchDeg, now,
|
|
200
|
+
tunables::kAlphaPitch, tunables::kBetaPitch, true);
|
|
201
|
+
updateAlphaBeta(roll_, rollDeg, now,
|
|
202
|
+
tunables::kAlphaRoll, tunables::kBetaRoll, true);
|
|
203
|
+
demand_.pitch = pitchDeg;
|
|
204
|
+
demand_.roll = rollDeg;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void CameraIntegrator::setViewCorrection(const glm::dquat& q) {
|
|
208
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
209
|
+
|
|
210
|
+
const double ql2 = q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z;
|
|
211
|
+
if (ql2 < 1.0e-20) {
|
|
212
|
+
viewCorrTarget_ = glm::dquat(1.0, 0.0, 0.0, 0.0);
|
|
213
|
+
} else {
|
|
214
|
+
const double inv = 1.0 / std::sqrt(ql2);
|
|
215
|
+
viewCorrTarget_ = glm::dquat(q.w * inv, q.x * inv, q.y * inv, q.z * inv);
|
|
216
|
+
}
|
|
217
|
+
demand_.viewCorrection = viewCorrTarget_;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
void CameraIntegrator::setVerticalFov(double degrees) {
|
|
221
|
+
const auto now = Clock::now();
|
|
222
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
223
|
+
|
|
224
|
+
const double residual = degrees - vfov_.value;
|
|
225
|
+
vfov_.value += tunables::kAlphaVfov * residual;
|
|
226
|
+
vfov_.velocity = 0.0;
|
|
227
|
+
vfov_.tState = now;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
void CameraIntegrator::teleport(const CameraParams& target) {
|
|
231
|
+
const auto now = Clock::now();
|
|
232
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
233
|
+
|
|
234
|
+
auto seed = [now](AlphaBeta& s, double v) {
|
|
235
|
+
s.value = v;
|
|
236
|
+
s.velocity = 0.0;
|
|
237
|
+
s.tState = now;
|
|
238
|
+
s.tLastMeas = TimePoint{}; // sentinel: bleed inactive until first setter
|
|
239
|
+
s.meanIntervalSec = 0.1;
|
|
240
|
+
};
|
|
241
|
+
seed(lat_, target.latitude);
|
|
242
|
+
seed(lon_, target.longitude);
|
|
243
|
+
seed(alt_, target.altitude);
|
|
244
|
+
seed(hdg_, wrap360(target.heading));
|
|
245
|
+
seed(pitch_, target.pitch);
|
|
246
|
+
seed(roll_, target.roll);
|
|
247
|
+
|
|
248
|
+
const glm::dquat& qIn = target.viewCorrection;
|
|
249
|
+
const double ql2 = qIn.w*qIn.w + qIn.x*qIn.x + qIn.y*qIn.y + qIn.z*qIn.z;
|
|
250
|
+
if (ql2 < 1.0e-20) {
|
|
251
|
+
viewCorrTarget_ = viewCorrActual_ = glm::dquat(1.0, 0.0, 0.0, 0.0);
|
|
252
|
+
} else {
|
|
253
|
+
const double inv = 1.0 / std::sqrt(ql2);
|
|
254
|
+
viewCorrTarget_ = viewCorrActual_ =
|
|
255
|
+
glm::dquat(qIn.w * inv, qIn.x * inv, qIn.y * inv, qIn.z * inv);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
seed(vfov_, target.verticalFov > 0.0 ? target.verticalFov : 0.0);
|
|
259
|
+
|
|
260
|
+
demand_ = target;
|
|
261
|
+
demand_.viewCorrection = viewCorrTarget_;
|
|
262
|
+
if (demand_.verticalFov <= 0.0) {
|
|
263
|
+
demand_.verticalFov = vfov_.value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
CameraParams CameraIntegrator::step() {
|
|
268
|
+
const auto now = Clock::now();
|
|
269
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
270
|
+
|
|
271
|
+
// Resolve effective rate caps once per frame; runtime override beats default.
|
|
272
|
+
const double yawCap = resolveCap(caps_.maxYawRateDegSec, tunables::kMaxYawRateDegSec);
|
|
273
|
+
const double pitchCap = resolveCap(caps_.maxPitchRateDegSec, tunables::kMaxPitchRateDegSec);
|
|
274
|
+
const double rollCap = resolveCap(caps_.maxRollRateDegSec, tunables::kMaxRollRateDegSec);
|
|
275
|
+
const double climbCap = resolveCap(caps_.maxClimbRateMps, tunables::kMaxClimbRateMps);
|
|
276
|
+
|
|
277
|
+
// Convert ground-speed cap (m/s) to a per-DoF deg/s cap on latitude /
|
|
278
|
+
// longitude. We use the WGS84 mean radius and ignore the cos(lat)
|
|
279
|
+
// correction for the longitude term; this is a first-order approximation
|
|
280
|
+
// but fine for a flight-sim rate cap, and avoids tangling the rate cap
|
|
281
|
+
// with the current latitude (which would otherwise feed back into itself).
|
|
282
|
+
double latLonCapDegSec = 0.0;
|
|
283
|
+
const double groundCap = resolveCap(caps_.maxGroundSpeedMps, tunables::kMaxGroundSpeedMps);
|
|
284
|
+
if (groundCap > 0.0) {
|
|
285
|
+
latLonCapDegSec = glm::degrees(groundCap / kEarthMeanRadiusM);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
extrapolate(lat_, now, latLonCapDegSec);
|
|
289
|
+
extrapolate(lon_, now, latLonCapDegSec);
|
|
290
|
+
extrapolate(alt_, now, climbCap);
|
|
291
|
+
extrapolate(hdg_, now, yawCap);
|
|
292
|
+
extrapolate(pitch_, now, pitchCap);
|
|
293
|
+
extrapolate(roll_, now, rollCap);
|
|
294
|
+
extrapolate(vfov_, now, 0.0); // no velocity term anyway
|
|
295
|
+
|
|
296
|
+
hdg_.value = wrap360(hdg_.value);
|
|
297
|
+
lat_.value = std::clamp(lat_.value, -90.0, 90.0);
|
|
298
|
+
// Longitude is allowed to drift over ±180; consumers usually want the raw
|
|
299
|
+
// continuous value for short-arc maths and the camera transform doesn't
|
|
300
|
+
// care.
|
|
301
|
+
|
|
302
|
+
// viewCorrection: SLERP toward the latest target.
|
|
303
|
+
glm::dquat cur = glm::normalize(viewCorrActual_);
|
|
304
|
+
glm::dquat tgt = glm::normalize(viewCorrTarget_);
|
|
305
|
+
if (glm::dot(cur, tgt) < 0.0) tgt = -tgt;
|
|
306
|
+
viewCorrActual_ = glm::normalize(
|
|
307
|
+
glm::slerp(cur, tgt, tunables::kAlphaViewCorr));
|
|
308
|
+
|
|
309
|
+
CameraParams out;
|
|
310
|
+
out.latitude = lat_.value;
|
|
311
|
+
out.longitude = lon_.value;
|
|
312
|
+
out.altitude = alt_.value;
|
|
313
|
+
out.heading = hdg_.value;
|
|
314
|
+
out.pitch = pitch_.value;
|
|
315
|
+
out.roll = roll_.value;
|
|
316
|
+
out.verticalFov = vfov_.value;
|
|
317
|
+
out.viewCorrection = viewCorrActual_;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
void CameraIntegrator::clampActualAltitude(double newAltMsl) {
|
|
322
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
323
|
+
alt_.value = newAltMsl;
|
|
324
|
+
alt_.velocity = 0.0;
|
|
325
|
+
// Do not touch demand_.altitude — getDemand() should still report what the
|
|
326
|
+
// user asked for.
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
void CameraIntegrator::setRateCaps(const CameraRateCaps& caps) {
|
|
330
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
331
|
+
caps_ = caps;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
CameraParams CameraIntegrator::getDemand() const {
|
|
335
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
336
|
+
return demand_;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
CameraParams CameraIntegrator::getActual() const {
|
|
340
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
341
|
+
CameraParams out;
|
|
342
|
+
out.latitude = lat_.value;
|
|
343
|
+
out.longitude = lon_.value;
|
|
344
|
+
out.altitude = alt_.value;
|
|
345
|
+
out.heading = hdg_.value;
|
|
346
|
+
out.pitch = pitch_.value;
|
|
347
|
+
out.roll = roll_.value;
|
|
348
|
+
out.verticalFov = vfov_.value;
|
|
349
|
+
out.viewCorrection = viewCorrActual_;
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
bool CameraIntegrator::isActive() const {
|
|
354
|
+
std::lock_guard<std::mutex> g(mutex_);
|
|
355
|
+
if (std::abs(demand_.latitude - lat_.value) > tunables::kEpsLatLon) return true;
|
|
356
|
+
if (std::abs(shortAngleDeltaDeg(lon_.value, demand_.longitude))
|
|
357
|
+
> tunables::kEpsLatLon) return true;
|
|
358
|
+
if (std::abs(demand_.altitude - alt_.value) > tunables::kEpsAltitudeMeters) return true;
|
|
359
|
+
if (angleAbsDeltaDeg(demand_.heading, hdg_.value) > tunables::kEpsAngleDeg) return true;
|
|
360
|
+
if (angleAbsDeltaDeg(demand_.pitch, pitch_.value) > tunables::kEpsAngleDeg) return true;
|
|
361
|
+
if (angleAbsDeltaDeg(demand_.roll, roll_.value) > tunables::kEpsAngleDeg) return true;
|
|
362
|
+
if (std::abs(demand_.verticalFov - vfov_.value) > tunables::kEpsAngleDeg) return true;
|
|
363
|
+
if (std::abs(lat_.velocity) > tunables::kEpsLatLon * 60.0) return true;
|
|
364
|
+
if (std::abs(lon_.velocity) > tunables::kEpsLatLon * 60.0) return true;
|
|
365
|
+
if (std::abs(alt_.velocity) > tunables::kEpsAltitudeMeters * 60.0) return true;
|
|
366
|
+
if (std::abs(hdg_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
|
|
367
|
+
if (std::abs(pitch_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
|
|
368
|
+
if (std::abs(roll_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
|
|
369
|
+
if ((1.0 - quatDotAbs(viewCorrActual_, viewCorrTarget_))
|
|
370
|
+
> tunables::kEpsQuatDot) return true;
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
} // namespace reactnativecesium
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "GlobeCamera.hpp"
|
|
4
|
+
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <mutex>
|
|
7
|
+
|
|
8
|
+
namespace reactnativecesium {
|
|
9
|
+
|
|
10
|
+
// Runtime-tunable per-DoF rate caps and outlier thresholds. Mirrors the
|
|
11
|
+
// compile-time defaults in EngineTunables but exposed as a struct so a view
|
|
12
|
+
// prop can override them at runtime without recompiling.
|
|
13
|
+
struct CameraRateCaps {
|
|
14
|
+
double maxYawRateDegSec = 0.0;
|
|
15
|
+
double maxPitchRateDegSec = 0.0;
|
|
16
|
+
double maxRollRateDegSec = 0.0;
|
|
17
|
+
double maxClimbRateMps = 0.0;
|
|
18
|
+
double maxGroundSpeedMps = 0.0;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Single owner of the camera's per-DoF α-β tracker state. Measurements are
|
|
22
|
+
// written from any thread (typically a Reanimated worklet on the UI thread or
|
|
23
|
+
// a GPS / IMU callback on the JS thread); the render thread ticks step() once
|
|
24
|
+
// per frame and reads the new actual camera back out.
|
|
25
|
+
//
|
|
26
|
+
// Internally every scalar DoF (latitude, longitude, altitude, heading, pitch,
|
|
27
|
+
// roll, verticalFov) has independent (value, velocity, t_state) state. On a
|
|
28
|
+
// measurement the residual is computed against the predicted value at the
|
|
29
|
+
// arrival time and split between value (scaled by α) and velocity
|
|
30
|
+
// (scaled by β / dt_arrival). On step() the value is extrapolated forward
|
|
31
|
+
// using the running velocity, clamped against the rate cap for that DoF.
|
|
32
|
+
//
|
|
33
|
+
// viewCorrection is a unit quaternion with no useful velocity model; it uses
|
|
34
|
+
// a single-coefficient SLERP toward the latest demand each frame.
|
|
35
|
+
//
|
|
36
|
+
// All public methods take `mutex_` briefly — contention is uncontested in
|
|
37
|
+
// practice (writers are sub-microsecond and the render thread reads once per
|
|
38
|
+
// frame). The integrator owns its own clock (Clock::now() inside both the
|
|
39
|
+
// setters and step()), so no platform time-base needs to be threaded in.
|
|
40
|
+
class CameraIntegrator {
|
|
41
|
+
public:
|
|
42
|
+
using Clock = std::chrono::steady_clock;
|
|
43
|
+
using TimePoint = Clock::time_point;
|
|
44
|
+
|
|
45
|
+
explicit CameraIntegrator(const CameraParams& initial = {});
|
|
46
|
+
|
|
47
|
+
// ── Per-DoF demand setters ──────────────────────────────────────────────
|
|
48
|
+
// Each call stamps the measurement with the current steady_clock now() and
|
|
49
|
+
// performs one α-β update against the predicted value at that time.
|
|
50
|
+
void setPosition(double latitudeDeg, double longitudeDeg);
|
|
51
|
+
void setAltitude(double altitudeMeters);
|
|
52
|
+
void setHeading(double headingDeg);
|
|
53
|
+
void setAttitude(double pitchDeg, double rollDeg);
|
|
54
|
+
void setViewCorrection(const glm::dquat& q);
|
|
55
|
+
void setVerticalFov(double degrees);
|
|
56
|
+
|
|
57
|
+
// Atomically reset every DoF to `target` — value, velocity (0), demand,
|
|
58
|
+
// all timestamps. Used for initial seeding and for explicit scripted
|
|
59
|
+
// scene jumps; bypasses the trackers entirely.
|
|
60
|
+
void teleport(const CameraParams& target);
|
|
61
|
+
|
|
62
|
+
// Render-thread tick. Extrapolates every DoF forward using its current
|
|
63
|
+
// velocity, applies rate caps, then returns the new actual camera. dt is
|
|
64
|
+
// clamped internally to kMaxFrameDtSec so a long sleep can't catapult the
|
|
65
|
+
// camera along a stale velocity vector.
|
|
66
|
+
//
|
|
67
|
+
// The wall-clock used to compute dt is Clock::now() (steady_clock) — the
|
|
68
|
+
// same clock the setters stamp with. This is deliberate: the integrator
|
|
69
|
+
// is the single owner of the time base. Earlier versions accepted an
|
|
70
|
+
// externally supplied `now` from the platform render loop, but on iOS
|
|
71
|
+
// Simulator `CACurrentMediaTime()` and `std::chrono::steady_clock` do
|
|
72
|
+
// not share an epoch, so mixing them produced multi-thousand-second dt
|
|
73
|
+
// values and an instant runaway.
|
|
74
|
+
CameraParams step();
|
|
75
|
+
|
|
76
|
+
// Terrain-floor write-back hook. Replaces the *actual* altitude value with
|
|
77
|
+
// `newAltMsl` and zeroes vertical velocity so the integrator does not
|
|
78
|
+
// immediately try to descend back through the floor. The *demand* altitude
|
|
79
|
+
// is left untouched — getDemand().altitude continues to reflect what the
|
|
80
|
+
// consumer asked for.
|
|
81
|
+
void clampActualAltitude(double newAltMsl);
|
|
82
|
+
|
|
83
|
+
// Runtime override of the rate caps (driven by view props). 0 = uncapped
|
|
84
|
+
// for a given axis. The defaults match tunables::kMax*.
|
|
85
|
+
void setRateCaps(const CameraRateCaps& caps);
|
|
86
|
+
|
|
87
|
+
// ── Readback ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
// What the consumer last asked for (each DoF independently). This is the
|
|
90
|
+
// unfiltered demand: pre-α-β. Useful for HUDs that want to display the
|
|
91
|
+
// requested camera rather than the rendered one.
|
|
92
|
+
CameraParams getDemand() const;
|
|
93
|
+
|
|
94
|
+
// The most recent value of step() (or the initial seed if step() has not
|
|
95
|
+
// run yet). Equivalent to what is currently being rendered.
|
|
96
|
+
CameraParams getActual() const;
|
|
97
|
+
|
|
98
|
+
// True when there is meaningful work to do: any residual between actual
|
|
99
|
+
// and demand exceeds its epsilon, or any DoF has non-zero velocity. The
|
|
100
|
+
// render loop can use this to drop back to idle when the camera is at
|
|
101
|
+
// rest.
|
|
102
|
+
bool isActive() const;
|
|
103
|
+
|
|
104
|
+
private:
|
|
105
|
+
// Per-DoF α-β state. `value` and `velocity` are the running estimates,
|
|
106
|
+
// `tState` the time of the last value update. `tLastMeas` is the time of
|
|
107
|
+
// the most recent setter call for this DoF (sentinel epoch == "no
|
|
108
|
+
// measurement seen yet"); `meanIntervalSec` is an EWMA of inter-arrival
|
|
109
|
+
// times used to drive the silence-aware velocity bleed.
|
|
110
|
+
struct AlphaBeta {
|
|
111
|
+
double value = 0.0;
|
|
112
|
+
double velocity = 0.0;
|
|
113
|
+
TimePoint tState{};
|
|
114
|
+
TimePoint tLastMeas{};
|
|
115
|
+
double meanIntervalSec = 0.1; // seed: ~10 Hz, irrelevant after a few setters
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Apply one α-β measurement update. `wrapAngle` is true for the angular
|
|
119
|
+
// DoFs (heading, pitch, roll, longitude) so the residual is computed on
|
|
120
|
+
// the shortest signed arc.
|
|
121
|
+
static void updateAlphaBeta(AlphaBeta& s, double z, TimePoint tz,
|
|
122
|
+
double alpha, double beta, bool wrapAngle);
|
|
123
|
+
|
|
124
|
+
// Advance the running estimate to `now`, clamp velocity to `maxRateAbs`
|
|
125
|
+
// (per second; 0 = uncapped). For angles, the resulting value is also
|
|
126
|
+
// normalised to its canonical range by the caller as needed.
|
|
127
|
+
static void extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs);
|
|
128
|
+
|
|
129
|
+
mutable std::mutex mutex_;
|
|
130
|
+
|
|
131
|
+
AlphaBeta lat_, lon_, alt_;
|
|
132
|
+
AlphaBeta hdg_, pitch_, roll_;
|
|
133
|
+
AlphaBeta vfov_; // velocity always zero — α-only
|
|
134
|
+
|
|
135
|
+
// Quaternion SLERP target + most recent actual (rendered) value.
|
|
136
|
+
glm::dquat viewCorrTarget_{1.0, 0.0, 0.0, 0.0};
|
|
137
|
+
glm::dquat viewCorrActual_{1.0, 0.0, 0.0, 0.0};
|
|
138
|
+
|
|
139
|
+
// Demand mirrors (raw last-set values, no α-β filter applied).
|
|
140
|
+
CameraParams demand_{};
|
|
141
|
+
|
|
142
|
+
// Runtime overrides; 0 = use compile-time defaults from EngineTunables.
|
|
143
|
+
CameraRateCaps caps_{};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
} // 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
|
|
@@ -41,32 +41,91 @@ inline constexpr double kDefaultMaximumScreenSpaceError = 32.0;
|
|
|
41
41
|
inline constexpr int32_t kDefaultMaximumSimultaneousTileLoads = 12;
|
|
42
42
|
inline constexpr int32_t kDefaultLoadingDescendantLimit = 20;
|
|
43
43
|
|
|
44
|
-
// Camera smoothing constants — shared between platform bridges so
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Camera smoothing constants — shared between platform bridges so motion
|
|
45
|
+
// feel is identical on iOS and Android.
|
|
46
|
+
//
|
|
47
|
+
// Every scalar DoF (lat, lon, alt, heading, pitch, roll) is driven by an
|
|
48
|
+
// α-β (constant-velocity predictor) tracker inside CameraIntegrator:
|
|
49
|
+
//
|
|
50
|
+
// on measurement z at time t_z:
|
|
51
|
+
// pred = value + velocity * (t_z - t_state)
|
|
52
|
+
// residual = wrap(z - pred) // wrap only for angles
|
|
53
|
+
// value += α * residual
|
|
54
|
+
// velocity += (β / max(t_z - t_state, ε)) * residual
|
|
55
|
+
//
|
|
56
|
+
// on render frame at time t_now:
|
|
57
|
+
// value += velocity * dt // extrapolate
|
|
58
|
+
// // optional per-DoF rate cap applied here
|
|
59
|
+
//
|
|
60
|
+
// α controls how hard each measurement pulls the value (responsiveness).
|
|
61
|
+
// β controls how aggressively velocity is corrected (anticipation). With
|
|
62
|
+
// α ≈ 0.5–0.6 / β ≈ 0.05 every DoF stays smooth on a 1 Hz GPS feed (zero
|
|
63
|
+
// steady-state lag at constant motion) while remaining instant under a
|
|
64
|
+
// 60 Hz gesture-driven worklet.
|
|
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).
|
|
69
|
+
inline constexpr double kAlphaPos = 0.6; inline constexpr double kBetaPos = 0.05;
|
|
70
|
+
inline constexpr double kAlphaAlt = 0.6; inline constexpr double kBetaAlt = 0.05;
|
|
71
|
+
inline constexpr double kAlphaHdg = 0.55; inline constexpr double kBetaHdg = 0.05;
|
|
72
|
+
inline constexpr double kAlphaPitch = 0.5; inline constexpr double kBetaPitch = 0.05;
|
|
73
|
+
inline constexpr double kAlphaRoll = 0.5; 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
|
|
76
|
+
|
|
77
|
+
// Convergence epsilons (used to decide when the integrator has settled and
|
|
78
|
+
// the render loop can drop back to idle).
|
|
51
79
|
inline constexpr double kEpsLatLon = 1e-7;
|
|
52
80
|
inline constexpr double kEpsAltitudeMeters = 0.1;
|
|
53
81
|
inline constexpr double kEpsAngleDeg = 0.05;
|
|
54
82
|
inline constexpr double kEpsQuatDot = 1e-8;
|
|
55
83
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
inline constexpr double
|
|
63
|
-
inline constexpr double
|
|
64
|
-
inline constexpr double
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
inline constexpr double
|
|
84
|
+
// Per-DoF rate caps (per second). 0 = uncapped. Hard ceilings; the integrator
|
|
85
|
+
// clamps the extrapolated velocity to these on every render tick. Useful to
|
|
86
|
+
// reject a single noisy IMU sample without making the camera spin or pitch up
|
|
87
|
+
// at hundreds of degrees per second. Default values are deliberately generous.
|
|
88
|
+
inline constexpr double kMaxYawRateDegSec = 360.0;
|
|
89
|
+
inline constexpr double kMaxPitchRateDegSec = 360.0;
|
|
90
|
+
inline constexpr double kMaxRollRateDegSec = 720.0;
|
|
91
|
+
inline constexpr double kMaxClimbRateMps = 0.0;
|
|
92
|
+
inline constexpr double kMaxGroundSpeedMps = 0.0;
|
|
93
|
+
|
|
94
|
+
// Render-step dt clamp (catch-up after a long pause / app foreground). Without
|
|
95
|
+
// this, the first render after a multi-second sleep would extrapolate a long
|
|
96
|
+
// way along the last known velocity vector and visibly jump.
|
|
97
|
+
inline constexpr double kMaxFrameDtSec = 0.1;
|
|
98
|
+
|
|
99
|
+
// Velocity bleed for bursty inputs. Without this, a gesture-driven feed (a
|
|
100
|
+
// worklet that emits at 60 Hz during the pan and stops emitting on release,
|
|
101
|
+
// or a joystick that pushes rates only while held) would leave the
|
|
102
|
+
// integrator extrapolating along the last learned velocity forever — there
|
|
103
|
+
// is no measurement-end signal in the per-DoF API.
|
|
104
|
+
//
|
|
105
|
+
// Each scalar DoF tracks an EWMA of inter-arrival times between its
|
|
106
|
+
// measurements. While measurements keep arriving on schedule, silence is
|
|
107
|
+
// small relative to the mean and velocity is extrapolated normally. Once
|
|
108
|
+
// silence exceeds the grace window the velocity bleeds exponentially with a
|
|
109
|
+
// time-constant proportional to the same mean interval — fast for gestures
|
|
110
|
+
// (mean ≈ 16 ms → τ ≈ 32 ms), slow for 1 Hz GPS (mean ≈ 1 s → τ ≈ 2 s, but
|
|
111
|
+
// silence rarely exceeds grace before the next fix arrives so the glide is
|
|
112
|
+
// untouched).
|
|
113
|
+
//
|
|
114
|
+
// silence > kVelocitySilenceGraceFactor * meanIntervalSec → bleed active.
|
|
115
|
+
// velocity *= exp(-frameDt / (kVelocityBleedTauFactor * meanIntervalSec))
|
|
116
|
+
inline constexpr double kVelocitySilenceGraceFactor = 1.5;
|
|
117
|
+
inline constexpr double kVelocityBleedTauFactor = 2.0;
|
|
118
|
+
// EWMA blend coefficient for the inter-arrival mean. Higher = faster adapt
|
|
119
|
+
// to cadence changes (e.g. user switches from gesture to sparse GPS feed).
|
|
120
|
+
inline constexpr double kIntervalEwmaAlpha = 0.3;
|
|
121
|
+
|
|
122
|
+
// Optional outlier rejection (off if 0). When > 0, a single measurement whose
|
|
123
|
+
// residual against the predicted state exceeds the threshold is dropped (and
|
|
124
|
+
// logged in debug builds). For deliberate jumps the consumer should call
|
|
125
|
+
// teleport() instead. Disabled by default — easy to enable per-deployment.
|
|
126
|
+
inline constexpr double kOutlierLatLonDeg = 0.0;
|
|
127
|
+
inline constexpr double kOutlierAltMeters = 0.0;
|
|
128
|
+
inline constexpr double kOutlierHeadingDeg = 0.0;
|
|
70
129
|
|
|
71
130
|
// Metrics throttle: emit telemetry every N rendered frames.
|
|
72
131
|
inline constexpr int kMetricsEmitEveryFrames = 20;
|