QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
SurfaceModel.cc
Go to the documentation of this file.
1#include "SurfaceModel.h"
2
3#include <QtCore/QElapsedTimer>
4#include <QtCore/QSet>
5#include <QtCore/QTimer>
6#include <QtCore/QVarLengthArray>
7#include <QtCore/QtMath>
8#include <queue>
9
10#include <algorithm>
11#include <cmath>
12#include <utility>
13#include <vector>
14
15#include "GeoMapCamera.h"
16#include "HeightField.h"
17#include "HeightSource.h"
18#include "QGCLoggingCategory.h"
19
20QGC_LOGGING_CATEGORY(GeoMapSurfaceModelLog, "GeoMap.SurfaceModel")
21QGC_LOGGING_CATEGORY(GeoMapSurfaceModelVerboseLog, "GeoMap.SurfaceModel.Verbose")
22
23namespace {
24
25QRectF patchRect(const TileMath::TileKey& key)
26{
27 const double span = TileMath::tileSpanAtZoom(key.zoom);
28 const QPointF minCorner = TileMath::tileMinCorner(key);
29 return QRectF(minCorner.x(), minCorner.y(), span, span);
30}
31
35bool patchTouchesRegion(const TileMath::TileKey& key, const QRectF& region)
36{
37 const QRectF rect = patchRect(key);
38 const double margin = rect.width() * 1e-6;
39 return rect.marginsAdded(QMarginsF(margin, margin, margin, margin)).intersects(region);
40}
41
42float maxHeightOf(const QList<float>& heights)
43{
44 float maxHeight = 0.0f;
45 for (const float height : heights) {
46 if (std::isfinite(height)) {
47 maxHeight = std::max(maxHeight, height);
48 }
49 }
50 return maxHeight;
51}
52
53} // namespace
54
55SurfaceModel::SurfaceModel(GeoMapCamera* camera, HeightSource* heightSource, HeightField* field, QObject* parent)
56 : QObject(parent), _camera(camera), _heightSource(heightSource), _field(field)
57{
58 qRegisterMetaType<TileMath::TileKey>();
59
60 connect(_field, &HeightField::regionChanged, this, &SurfaceModel::_fieldRegionChanged);
61
62 // Coalesce: camera signals fire per input event (up to 120/s during a
63 // gesture); one queued pass per event-loop iteration bounds the cost
64 connect(_camera, &GeoMapCamera::centerChanged, this, &SurfaceModel::_scheduleUpdate);
65 connect(_camera, &GeoMapCamera::headingChanged, this, &SurfaceModel::_scheduleUpdate);
66 connect(_camera, &GeoMapCamera::tiltChanged, this, &SurfaceModel::_scheduleUpdate);
67 connect(_camera, &GeoMapCamera::distanceChanged, this, &SurfaceModel::_scheduleUpdate);
68 connect(_camera, &GeoMapCamera::centerElevationChanged, this, &SurfaceModel::_scheduleUpdate);
69 connect(_camera, &GeoMapCamera::viewportSizeChanged, this, &SurfaceModel::_scheduleUpdate);
70 connect(_camera, &GeoMapCamera::fieldOfViewChanged, this, &SurfaceModel::_scheduleUpdate);
71}
72
73void SurfaceModel::_scheduleUpdate()
74{
75 if (_updatePending) {
76 return;
77 }
78 _updatePending = true;
79 QTimer::singleShot(0, this, [this] {
80 if (!_updatePending) {
81 return; // absorbed by a direct update() call meanwhile
82 }
83 update();
84 });
85}
86
88{
89 // Any direct pass supersedes a queued coalesced one (its singleShot
90 // becomes a no-op), so updateSettled() is truthful right after a call
91 _updatePending = false;
92 if (!_camera->isPositioned()) {
93 return; // the default pose is null island; building patches there fires doomed terrain fetches
94 }
95 if (_camera->viewportSize().isEmpty()) {
96 return; // keep the last valid set; transient 0-size viewports occur during layout
97 }
98
99 QElapsedTimer updateTimer;
100 updateTimer.start();
101
102 // Max terrain height scans every resident vertex: compute once per update
103 const double terrainZ = _maxTerrainZ();
104 const QRectF visible = _visibleGroundRect(terrainZ);
105 _culledTerrainZ = terrainZ;
106 const QPointF cameraGround = _camera->cameraGroundPosition();
107 const double cameraHeight = _camera->distance() * std::cos(qDegreesToRadians(_camera->tilt()));
108
109 const QList<TileMath::TileKey> desired = _desiredPatches(visible, cameraGround, cameraHeight);
110
111 QSet<TileMath::TileKey> desiredSet(desired.cbegin(), desired.cend());
112
113 // Add newly desired patches, capped per pass: each add synchronously
114 // builds a render delegate downstream, and uncapped bursts (300+ adds/s
115 // while zooming) blow the frame budget. A new patch meshes immediately
116 // from the field's best estimate; tile coverage is requested so the
117 // estimate refines when data arrives.
118 QVarLengthArray<QRectF, 8> churnRects; // added/removed extents: neighbors there re-stitch
119 int adds = 0;
120 _addsDeferred = false;
121 for (const TileMath::TileKey& key : desired) {
122 if (_patches.contains(key)) {
123 continue;
124 }
125 if (adds >= kMaxPatchAddsPerUpdate) {
126 _addsDeferred = true;
127 continue;
128 }
129 PatchData data;
130 data.heights = _field->samplePatch(key, kGridSize);
131 data.maxHeight = maxHeightOf(data.heights);
132 _patches.insert(key, std::move(data));
133 churnRects.append(patchRect(key));
134 _heightSource->requestTile(key);
135 emit patchAdded(key);
136 adds++;
137 }
138
139 // Rects of desired patches still not resident after the capped adds: a
140 // no-longer-desired patch overlapping one may not be removed yet, or the
141 // surface would show a hole until the adds catch up. Computed after the
142 // adds so a replaced patch drops in the same pass its last replacement
143 // arrives, instead of both rendering (and z-fighting) one extra pass.
144 QVarLengthArray<QRectF, 16> missingRects;
145 for (const TileMath::TileKey& key : desired) {
146 if (!_patches.contains(key)) {
147 missingRects.append(patchRect(key));
148 }
149 }
150 const auto overlapsMissing = [&missingRects](const TileMath::TileKey& key) {
151 const QRectF rect = patchRect(key);
152 for (const QRectF& missing : missingRects) {
153 if (rect.intersects(missing)) {
154 return true;
155 }
156 }
157 return false;
158 };
159
160 // Drop patches no longer desired. Removals are capped like adds: each
161 // erase synchronously destroys a render delegate, and pose jumps
162 // (high-tilt orbits) can invalidate hundreds of patches at once. Patches
163 // whose replacements are not resident yet stay for a follow-up pass.
164 _removalsThisPass = 0;
165 _removalsDeferred = false;
166 for (auto it = _patches.begin(); it != _patches.end();) {
167 if (desiredSet.contains(it.key())) {
168 ++it;
169 continue;
170 }
171 if ((_removalsThisPass >= kMaxPatchRemovalsPerUpdate) || overlapsMissing(it.key())) {
172 _removalsDeferred = true; // stays resident one more pass; follow-up finishes the cull
173 ++it;
174 continue;
175 }
176 const TileMath::TileKey removedKey = it.key();
177 it = _patches.erase(it);
178 churnRects.append(patchRect(removedKey));
179 emit patchRemoved(removedKey);
180 _removalsThisPass++;
181 }
182
183 // Added/removed patches change their neighbors' edge LOD deltas: notify
184 // every resident patch touching a churned extent so it re-stitches
185 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
186 for (const QRectF& rect : churnRects) {
187 if (patchTouchesRegion(it.key(), rect)) {
188 emit patchEdgeDeltasChanged(it.key());
189 break;
190 }
191 }
192 }
193
194 // Pin every resident patch's key and its full ancestor chain: whatever
195 // tile the field resolves a patch sample to is in that chain, and an
196 // eviction there would silently coarsen a rendered mesh
197 if ((adds > 0) || (_removalsThisPass > 0)) {
198 QSet<TileMath::TileKey> pinned;
199 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
200 TileMath::TileKey key = it.key();
201 while (true) {
202 pinned.insert(key);
203 if (key.zoom == TileMath::kMinZoom) {
204 break;
205 }
206 key = TileMath::TileKey{key.x >> 1, key.y >> 1, key.zoom - 1};
207 }
208 }
209 _field->setPinnedKeys(std::move(pinned));
210 }
211
212 // The caps guarantee every deferred pass makes progress, so follow-ups
213 // always converge
214 if (_addsDeferred || _removalsDeferred) {
215 _scheduleUpdate();
216 }
217
218 const qint64 elapsedUs = updateTimer.nsecsElapsed() / 1000;
219 qCDebug(GeoMapSurfaceModelVerboseLog)
220 << "update pass: desired" << desired.count() << "resident" << _patches.count() << "adds" << adds << "removals"
221 << _removalsThisPass << "deferred adds" << _addsDeferred << "deferred removals" << _removalsDeferred
222 << "elapsedUs" << elapsedUs;
223 _updateStats.updates++;
224 _updateStats.totalUs += elapsedUs;
225 _updateStats.maxUs = std::max(_updateStats.maxUs, elapsedUs);
226}
227
229{
230 const UpdateStats stats = _updateStats;
231 _updateStats = UpdateStats{};
232 return stats;
233}
234
235QList<SurfaceModel::Patch> SurfaceModel::patches() const
236{
237 QList<Patch> result;
238 result.reserve(_patches.count());
239 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
240 result.append(Patch{it.key(), it.value().heights, true, false});
241 }
242 return result;
243}
244
245std::optional<SurfaceModel::Patch> SurfaceModel::patch(const TileMath::TileKey& key) const
246{
247 const auto it = _patches.constFind(key);
248 if (it == _patches.cend()) {
249 return std::nullopt;
250 }
251 return Patch{key, it.value().heights, true, false};
252}
253
255{
256 // {N,S,W,E} neighbor offsets; slippy y grows south, so north is y-1
257 static constexpr int kOffsets[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
258 QList<int> deltas;
259 deltas.reserve(4);
260 for (const auto& offset : kOffsets) {
261 deltas.append(_edgeDelta(key, offset[0], offset[1]));
262 }
263 return deltas;
264}
265
266int SurfaceModel::_edgeDelta(const TileMath::TileKey& key, int dx, int dy) const
267{
268 const TileMath::TileKey neighbor{key.x + dx, key.y + dy, key.zoom};
269 if (!TileMath::isValidKey(neighbor) || _patches.contains(neighbor)) {
270 return 0; // world edge or same-zoom neighbor: unconstrained
271 }
272 // First resident ancestor of the missing same-zoom neighbor is the coarse
273 // patch rendering across this edge; finer neighbors constrain themselves
274 TileMath::TileKey ancestor = neighbor;
275 while (ancestor.zoom > TileMath::kMinZoom) {
276 ancestor = TileMath::TileKey{ancestor.x >> 1, ancestor.y >> 1, ancestor.zoom - 1};
277 if (_patches.contains(ancestor)) {
278 const int delta = key.zoom - ancestor.zoom;
279 // No coincident vertices beyond this: leave unconstrained (skirts cover)
280 return ((kGridSize % (1 << delta)) == 0) ? delta : 0;
281 }
282 }
283 return 0;
284}
285
286QRectF SurfaceModel::_visibleGroundRect(double terrainZ) const
287{
288 const QSizeF viewport = _camera->viewportSize();
289 const double maxRange = _camera->distance() * kMaxRangeMultiplier;
290
291 // Terrain-aware near boundary: rays that hit the ground plane far ahead
292 // cross the terrain-top plane much closer to (or behind) the camera, so
293 // tall terrain there is visible even though its flat-ground point is not
294 const double eyeZ =
295 _camera->centerElevation() + (_camera->distance() * std::cos(qDegreesToRadians(_camera->tilt())));
296 const QPointF cameraGround = _camera->cameraGroundPosition();
297 const double terrainT = ((terrainZ > 0.0) && (eyeZ > terrainZ)) ? (1.0 - (terrainZ / eyeZ)) : 0.0;
298
299 // Sample a screen grid; horizon misses are capped at maxRange. Accumulate
300 // extremes manually (zero-size QRectFs are "null" and united() ignores them).
301 double minX = 0;
302 double minY = 0;
303 double maxX = 0;
304 double maxY = 0;
305 bool first = true;
306 const auto include = [&](const QPointF& point) {
307 if (first) {
308 minX = maxX = point.x();
309 minY = maxY = point.y();
310 first = false;
311 } else {
312 minX = std::min(minX, point.x());
313 maxX = std::max(maxX, point.x());
314 minY = std::min(minY, point.y());
315 maxY = std::max(maxY, point.y());
316 }
317 };
318 for (int row = 0; row < kVisibleSampleGrid; row++) {
319 for (int col = 0; col < kVisibleSampleGrid; col++) {
320 const QPointF screenPos((viewport.width() * col) / (kVisibleSampleGrid - 1),
321 (viewport.height() * row) / (kVisibleSampleGrid - 1));
322 const auto ground = _camera->groundPointCapped(screenPos, maxRange);
323 if (!ground) {
324 continue;
325 }
326 include(*ground);
327 if (terrainT > 0.0) {
328 include(cameraGround + ((*ground - cameraGround) * terrainT));
329 }
330 }
331 }
332 if (first) {
333 return QRectF();
334 }
335 // Always keep the camera's tile in range: the terrain ceiling above only
336 // knows resident patches, so terrain living solely in never-requested
337 // tiles near the camera could otherwise never be discovered
338 include(cameraGround);
339
340 const double half = TileMath::worldSize() / 2.0;
341 return QRectF(QPointF(minX, minY), QPointF(maxX, maxY))
342 .intersected(QRectF(-half, -half, TileMath::worldSize(), TileMath::worldSize()));
343}
344
347double SurfaceModel::_maxTerrainZ() const
348{
349 float maxHeight = 0.0f;
350 for (const PatchData& data : _patches) {
351 maxHeight = std::max(maxHeight, data.maxHeight);
352 }
353 if (maxHeight <= 0.0f) {
354 return 0.0;
355 }
356 return maxHeight * TileMath::mercatorScale(_camera->center().latitude());
357}
358
359double SurfaceModel::_projectedPixels(const TileMath::TileKey& key, const QPointF& cameraGround,
360 double cameraHeight) const
361{
362 const QRectF rect = patchRect(key);
363 const double span = rect.width();
364
365 // Distance from the camera to the closest point of the patch (heights ignored for LOD)
366 const double dx = std::max({rect.left() - cameraGround.x(), cameraGround.x() - rect.right(), 0.0});
367 const double dy = std::max({rect.top() - cameraGround.y(), cameraGround.y() - rect.bottom(), 0.0});
368 const double distance = std::hypot(std::hypot(dx, dy), cameraHeight);
369
370 const double metersPerPixel =
371 (2.0 * distance * std::tan(qDegreesToRadians(_camera->fieldOfView()) / 2.0)) / _camera->viewportSize().height();
372 return span / metersPerPixel;
373}
374
375QList<TileMath::TileKey> SurfaceModel::_desiredPatches(const QRectF& visible, const QPointF& cameraGround,
376 double cameraHeight) const
377{
378 // Coverage-preserving refinement: repeatedly split the visible patch with
379 // the largest projected size while the budget allows. A split only ever
380 // replaces a patch with its visible children, so hitting kMaxPatches leaves
381 // the coarsest acceptable coverage instead of punching holes in it.
382 struct Entry
383 {
384 double pixels;
386 };
387
388 const auto lessPixels = [](const Entry& a, const Entry& b) { return a.pixels < b.pixels; };
389 std::priority_queue<Entry, std::vector<Entry>, decltype(lessPixels)> queue(lessPixels);
390
391 QList<TileMath::TileKey> desired;
392
393 const TileMath::TileKey root{0, 0, 0};
394 if (visible.intersects(patchRect(root))) {
395 queue.push(Entry{_projectedPixels(root, cameraGround, cameraHeight), root});
396 }
397
398 while (!queue.empty()) {
399 const Entry entry = queue.top();
400 if (entry.pixels <= kRefinePixelThreshold) {
401 break; // largest is small enough, so every remaining patch is too
402 }
403 if (entry.key.zoom >= TileMath::kMaxZoom) {
404 queue.pop();
405 desired.append(entry.key); // cannot refine further; keep as-is
406 continue;
407 }
408
409 QVarLengthArray<Entry, 4> children;
410 for (int childY = 0; childY < 2; childY++) {
411 for (int childX = 0; childX < 2; childX++) {
412 const TileMath::TileKey childKey{(entry.key.x * 2) + childX, (entry.key.y * 2) + childY,
413 entry.key.zoom + 1};
414 if (visible.intersects(patchRect(childKey))) {
415 children.append(Entry{_projectedPixels(childKey, cameraGround, cameraHeight), childKey});
416 }
417 }
418 }
419 const int totalAfterSplit = static_cast<int>(queue.size()) + desired.count() - 1 + children.count();
420 if (totalAfterSplit > kMaxPatches) {
421 break; // budget exhausted: keep remaining patches coarse
422 }
423 queue.pop();
424 for (const Entry& child : children) {
425 queue.push(child);
426 }
427 }
428
429 while (!queue.empty()) {
430 desired.append(queue.top().key);
431 queue.pop();
432 }
433 return desired;
434}
435
436void SurfaceModel::_fieldRegionChanged(const QRectF& worldRect)
437{
438 // Re-mesh exactly the patches touching the changed region: their field
439 // samples (and skirt/normal data downstream) may have changed. Patch edge
440 // vertices exactly on the region boundary sample the new data too, hence
441 // the inflated-rect contact test.
442 int remeshed = 0;
443 for (auto it = _patches.begin(); it != _patches.end(); ++it) {
444 if (!patchTouchesRegion(it.key(), worldRect)) {
445 continue;
446 }
447 PatchData& data = it.value();
448 data.heights = _field->samplePatch(it.key(), kGridSize);
449 data.maxHeight = maxHeightOf(data.heights);
450 emit patchMeshChanged(it.key());
451 remeshed++;
452 }
453 qCDebug(GeoMapSurfaceModelVerboseLog)
454 << "regionChanged" << worldRect << "re-meshed" << remeshed << "of" << _patches.count() << "patches";
455
456 // Terrain taller than the last cull assumed may be visible below/behind
457 // the camera: re-cull terrain-aware
458 if (_maxTerrainZ() > (_culledTerrainZ + kRecullHeightMargin)) {
459 _scheduleUpdate();
460 }
461}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QSizeF viewportSize() const
void distanceChanged()
void centerElevationChanged()
qreal distance() const
qreal fieldOfView() const
void tiltChanged()
QPointF cameraGroundPosition() const
void headingChanged()
qreal centerElevation() const
void fieldOfViewChanged()
bool isPositioned() const
void viewportSizeChanged()
std::optional< QPointF > groundPointCapped(const QPointF &screenPos, double maxRange) const
void centerChanged()
qreal tilt() const
QGeoCoordinate center() const
void setPinnedKeys(QSet< TileMath::TileKey > keys)
Definition HeightField.h:54
QList< float > samplePatch(const TileMath::TileKey &key, int gridSize) const
void regionChanged(const QRectF &worldRect)
virtual bool requestTile(const TileMath::TileKey &key)
static constexpr double kRecullHeightMargin
SurfaceModel(GeoMapCamera *camera, HeightSource *heightSource, HeightField *field, QObject *parent=nullptr)
static constexpr int kMaxPatchAddsPerUpdate
static constexpr double kMaxRangeMultiplier
visible-range cap in camera distances
std::optional< Patch > patch(const TileMath::TileKey &key) const
Single-patch lookup; std::nullopt when the key is not resident.
static constexpr int kMaxPatchRemovalsPerUpdate
static constexpr int kGridSize
QList< Patch > patches() const
void patchEdgeDeltasChanged(const TileMath::TileKey &key)
QList< int > edgeLodDeltas(const TileMath::TileKey &key) const
void patchRemoved(const TileMath::TileKey &key)
static constexpr int kVisibleSampleGrid
void patchAdded(const TileMath::TileKey &key)
static constexpr double kRefinePixelThreshold
subdivide above this projected size
void patchMeshChanged(const TileMath::TileKey &key)
The patch's mesh content changed (heights re-sampled): consumers must re-pull heights.
UpdateStats takeUpdateStats()
Returns the counters accumulated since the previous call and resets them.
static constexpr int kMaxPatches
double worldSize()
Full mercator world extent (2*pi*R) in world meters.
Definition TileMath.cc:18
QPointF tileMinCorner(const TileKey &key)
South-west (minimum x/y) corner of a tile in world meters.
Definition TileMath.cc:58
constexpr int kMaxZoom
Definition TileMath.h:31
bool isValidKey(const TileKey &key)
True if zoom is within [kMinZoom, kMaxZoom] and x/y address a tile at that zoom.
Definition TileMath.cc:44
constexpr int kMinZoom
Definition TileMath.h:30
double mercatorScale(double latitude)
Definition TileMath.cc:38
double tileSpanAtZoom(int zoom)
Edge length of one tile at zoom, in world meters.
Definition TileMath.cc:53
TileMath::TileKey key
update() call/duration counters for the perf overlay