QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PatchGeometry.cc
Go to the documentation of this file.
1#include "PatchGeometry.h"
2
3#include <QtGui/QVector3D>
4
5#include <algorithm>
6#include <bit>
7
8#include "HeightField.h"
10
11QGC_LOGGING_CATEGORY(GeoMapPatchGeometryLog, "GeoMap.PatchGeometry")
12
13PatchGeometry::PatchGeometry(QQuick3DObject* parent) : QQuick3DGeometry(parent)
14{
15 _rebuild();
16}
17
19{
20 QQuick3DGeometry::componentComplete();
21 _rebuild(); // one build for the whole batch of initial property assignments
22}
23
24void PatchGeometry::_requestRebuild()
25{
26 if (isComponentComplete()) {
27 _rebuild();
28 }
29}
30
32{
33 const int clamped = std::clamp(gridSize, kMinGridSize, kMaxGridSize);
34 if (clamped == _gridSize) {
35 return;
36 }
37 _gridSize = clamped;
38 for (const int delta : _lodDelta) {
39 if ((delta > 0) && ((_gridSize % (1 << delta)) != 0)) {
40 qCWarning(GeoMapPatchGeometryLog) << "setGridSize reset edge LOD deltas: delta" << delta
41 << "has no coincident vertices at gridSize" << _gridSize;
42 _lodDelta.fill(0);
44 break;
45 }
46 }
47 emit gridSizeChanged();
48 _requestRebuild();
49}
50
51void PatchGeometry::setSpan(qreal span)
52{
53 if (qFuzzyCompare(span, _span) || (span <= 0.0)) {
54 return;
55 }
56 _span = span;
57 emit spanChanged();
58 _requestRebuild();
59}
60
61void PatchGeometry::setHeights(const QList<float>& heights)
62{
63 if (heights == _heights) {
64 return;
65 }
66 _heights = heights;
67 emit heightsChanged();
68 _requestRebuild();
69}
70
72{
73 if (!_heightField) {
74 qCWarning(GeoMapPatchGeometryLog) << "sampleFromField rejected: no height field set, key" << key;
75 return false;
76 }
77 const QList<float> sampled = _heightField->samplePatch(key, _gridSize);
78 if (sampled.isEmpty()) {
79 qCWarning(GeoMapPatchGeometryLog)
80 << "sampleFromField rejected: field returned no samples, key" << key << "gridSize" << _gridSize;
81 return false;
82 }
83 if (sampled != _heights) {
84 _heights = sampled;
85 emit heightsChanged();
86 }
87 _rebuild();
88 return true;
89}
90
92{
93 if (heightField == _heightField) {
94 return;
95 }
96 if (_heightField) {
97 disconnect(_heightField, nullptr, this, nullptr);
98 }
99 _heightField = heightField;
100 if (_heightField) {
101 // Match setHeightField(nullptr) semantics; null first, the dying
102 // object needs no disconnect
103 connect(_heightField, &QObject::destroyed, this, [this] {
104 _heightField = nullptr;
105 emit heightFieldChanged();
106 });
107 }
108 emit heightFieldChanged();
109}
110
111void PatchGeometry::setEdgeLodDeltas(int north, int south, int west, int east)
112{
113 // Bounding delta first keeps the shift below well-defined
114 static_assert(std::has_single_bit(static_cast<unsigned>(kMaxGridSize)), "kMaxLodDelta assumes a power of two");
115 constexpr int kMaxLodDelta = std::countr_zero(static_cast<unsigned>(kMaxGridSize));
116 for (const int delta : {north, south, west, east}) {
117 if ((delta < 0) || (delta > kMaxLodDelta) || ((delta > 0) && ((_gridSize % (1 << delta)) != 0))) {
118 qCWarning(GeoMapPatchGeometryLog)
119 << "setEdgeLodDeltas rejected: delta" << delta << "has no coincident vertices at gridSize" << _gridSize;
120 return;
121 }
122 }
123 if ((north == _lodDelta[kNorth]) && (south == _lodDelta[kSouth]) && (west == _lodDelta[kWest]) &&
124 (east == _lodDelta[kEast])) {
125 return;
126 }
127 _lodDelta[kNorth] = north;
128 _lodDelta[kSouth] = south;
129 _lodDelta[kWest] = west;
130 _lodDelta[kEast] = east;
132 _requestRebuild();
133}
134
135void PatchGeometry::setEdgeLodDeltas(const QList<int>& deltas)
136{
137 if (deltas.count() != kEdgeCount) {
138 qCWarning(GeoMapPatchGeometryLog)
139 << "setEdgeLodDeltas rejected: expected {N,S,W,E}, got" << deltas.count() << "deltas";
140 return;
141 }
142 setEdgeLodDeltas(deltas[0], deltas[1], deltas[2], deltas[3]);
143}
144
145float PatchGeometry::_rawHeightAt(int row, int col) const
146{
147 const int verticesPerEdge = _gridSize + 1;
148 if (_heights.count() != (verticesPerEdge * verticesPerEdge)) {
149 return 0.0f; // missing or mismatched grid renders flat
150 }
151 return _heights.at((row * verticesPerEdge) + col);
152}
153
154float PatchGeometry::_heightAt(int row, int col) const
155{
156 const int r = std::clamp(row, 0, _gridSize);
157 const int c = std::clamp(col, 0, _gridSize);
158
159 // T-junction fix: on an edge with a coarser neighbor, non-coincident
160 // vertices are collapsed onto the segment between the coincident ones.
161 // The coincident vertices need no correction: HeightField::samplePatch's
162 // canonical boundary resolution makes them bit-identical to the coarse
163 // neighbor's rendered edge (plain setHeights callers must provide heights
164 // with the same property). A corner on two constrained edges takes the
165 // first match (N,S,W,E precedence); skirts hide the residual three-LOD
166 // corner mismatch.
167 Edge edge = kNorth;
168 bool matched = false;
169 bool alongCol = false; // lerp runs along the edge direction
170 if ((r == 0) && (_lodDelta[kNorth] > 0)) {
171 edge = kNorth;
172 alongCol = true;
173 matched = true;
174 } else if ((r == _gridSize) && (_lodDelta[kSouth] > 0)) {
175 edge = kSouth;
176 alongCol = true;
177 matched = true;
178 } else if ((c == 0) && (_lodDelta[kWest] > 0)) {
179 edge = kWest;
180 matched = true;
181 } else if ((c == _gridSize) && (_lodDelta[kEast] > 0)) {
182 edge = kEast;
183 matched = true;
184 }
185 if (matched) {
186 const int step = 1 << _lodDelta[edge];
187 const int idx = alongCol ? c : r;
188 const int base = (idx / step) * step;
189 if (idx == base) {
190 return _rawHeightAt(r, c);
191 }
192 const float t = float(idx - base) / step;
193 const float a = alongCol ? _rawHeightAt(r, base) : _rawHeightAt(base, c);
194 const float b = alongCol ? _rawHeightAt(r, base + step) : _rawHeightAt(base + step, c);
195 return a + ((b - a) * t);
196 }
197 return _rawHeightAt(r, c);
198}
199
200void PatchGeometry::_rebuild()
201{
202 const int verticesPerEdge = _gridSize + 1;
203 const int gridVertexCount = verticesPerEdge * verticesPerEdge;
204 const int skirtVertexCount = 4 * verticesPerEdge;
205 const int vertexCount = gridVertexCount + skirtVertexCount;
206
207 const float span = static_cast<float>(_span);
208 const float half = span / 2.0f;
209 const float step = span / _gridSize;
210 // Skirt hides the seam against the coarsest constraining neighbor; that
211 // seam doubles with each level the neighbor is coarser, so scale the base
212 // depth by 2^maxDelta (the coarser level's geometric error halves per
213 // level). maxDelta is in
214 // [0, kMaxLodDelta] (setEdgeLodDeltas validates), so the shift is in
215 // range; 0 = base depth.
216 const int maxDelta = *std::max_element(_lodDelta.cbegin(), _lodDelta.cend());
217 const float skirtDepth = span * static_cast<float>(kSkirtDepthFraction) * static_cast<float>(1 << maxDelta);
218
219 // Interleaved layout: position (3f) + normal (3f) + uv (2f)
220 constexpr int kFloatsPerVertex = 8;
221 constexpr int kStride = kFloatsPerVertex * sizeof(float);
222
223 QByteArray vertexData(vertexCount * kStride, Qt::Uninitialized);
224 float* v = reinterpret_cast<float*>(vertexData.data());
225
226 float minZ = 0.0f;
227 float maxZ = 0.0f;
228
229 const auto writeVertex = [&](float x, float y, float z, const QVector3D& normal, float u, float texV) {
230 *v++ = x;
231 *v++ = y;
232 *v++ = z;
233 *v++ = normal.x();
234 *v++ = normal.y();
235 *v++ = normal.z();
236 *v++ = u;
237 *v++ = texV;
238 minZ = std::min(minZ, z);
239 maxZ = std::max(maxZ, z);
240 };
241
242 // Per-vertex normal from central differences of the height grid
243 const auto normalAt = [&](int row, int col) {
244 const float dzdx = (_heightAt(row, col + 1) - _heightAt(row, col - 1)) / (2.0f * step);
245 const float dzdy =
246 (_heightAt(row - 1, col) - _heightAt(row + 1, col)) / (2.0f * step); // row 0 = north (max y)
247 QVector3D normal(-dzdx, -dzdy, 1.0f);
248 normal.normalize();
249 return normal;
250 };
251
252 // Grid vertices, row-major from the north-west corner
253 for (int row = 0; row < verticesPerEdge; row++) {
254 const float y = half - (row * step);
255 const float texV = static_cast<float>(row) / _gridSize;
256 for (int col = 0; col < verticesPerEdge; col++) {
257 const float x = -half + (col * step);
258 const float u = static_cast<float>(col) / _gridSize;
259 writeVertex(x, y, _heightAt(row, col), normalAt(row, col), u, texV);
260 }
261 }
262
263 // Skirt vertices: boundary vertices dropped by skirtDepth, same normal/uv
264 // to avoid lighting/texture seams. Edge order: north, south, west, east.
265 const auto writeSkirtVertex = [&](int row, int col) {
266 const float x = -half + (col * step);
267 const float y = half - (row * step);
268 const float u = static_cast<float>(col) / _gridSize;
269 const float texV = static_cast<float>(row) / _gridSize;
270 writeVertex(x, y, _heightAt(row, col) - skirtDepth, normalAt(row, col), u, texV);
271 };
272 for (int col = 0; col < verticesPerEdge; col++) {
273 writeSkirtVertex(0, col);
274 }
275 for (int col = 0; col < verticesPerEdge; col++) {
276 writeSkirtVertex(_gridSize, col);
277 }
278 for (int row = 0; row < verticesPerEdge; row++) {
279 writeSkirtVertex(row, 0);
280 }
281 for (int row = 0; row < verticesPerEdge; row++) {
282 writeSkirtVertex(row, _gridSize);
283 }
284
285 // Indices: grid cells (2 triangles each, CCW seen from +z) + skirt quads
286 const int gridIndexCount = _gridSize * _gridSize * 6;
287 const int skirtIndexCount = 4 * _gridSize * 6;
288 QByteArray indexData((gridIndexCount + skirtIndexCount) * sizeof(quint32), Qt::Uninitialized);
289 quint32* idx = reinterpret_cast<quint32*>(indexData.data());
290
291 const auto gridIndex = [&](int row, int col) { return static_cast<quint32>((row * verticesPerEdge) + col); };
292 for (int row = 0; row < _gridSize; row++) {
293 for (int col = 0; col < _gridSize; col++) {
294 *idx++ = gridIndex(row, col);
295 *idx++ = gridIndex(row + 1, col);
296 *idx++ = gridIndex(row, col + 1);
297 *idx++ = gridIndex(row, col + 1);
298 *idx++ = gridIndex(row + 1, col);
299 *idx++ = gridIndex(row + 1, col + 1);
300 }
301 }
302
303 // Skirt quads between each boundary edge segment and its dropped copy.
304 // Rendered with culling disabled, so winding is not significant.
305 const auto writeSkirtQuads = [&](quint32 skirtBase, const auto& topIndex) {
306 for (int i = 0; i < _gridSize; i++) {
307 const quint32 topA = topIndex(i);
308 const quint32 topB = topIndex(i + 1);
309 const quint32 skirtA = skirtBase + i;
310 const quint32 skirtB = skirtBase + i + 1;
311 *idx++ = topA;
312 *idx++ = skirtA;
313 *idx++ = topB;
314 *idx++ = topB;
315 *idx++ = skirtA;
316 *idx++ = skirtB;
317 }
318 };
319 const quint32 skirtStart = static_cast<quint32>(gridVertexCount);
320 writeSkirtQuads(skirtStart, [&](int i) { return gridIndex(0, i); });
321 writeSkirtQuads(skirtStart + verticesPerEdge, [&](int i) { return gridIndex(_gridSize, i); });
322 writeSkirtQuads(skirtStart + (2 * verticesPerEdge), [&](int i) { return gridIndex(i, 0); });
323 writeSkirtQuads(skirtStart + (3 * verticesPerEdge), [&](int i) { return gridIndex(i, _gridSize); });
324
325 clear();
326 setStride(kStride);
327 setVertexData(vertexData);
328 setIndexData(indexData);
329 setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
330 addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type);
331 addAttribute(QQuick3DGeometry::Attribute::NormalSemantic, 3 * sizeof(float), QQuick3DGeometry::Attribute::F32Type);
332 addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic, 6 * sizeof(float),
333 QQuick3DGeometry::Attribute::F32Type);
334 addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0, QQuick3DGeometry::Attribute::U32Type);
335 setBounds(QVector3D(-half, -half, minZ), QVector3D(half, half, maxZ));
336 update();
337}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QList< float > samplePatch(const TileMath::TileKey &key, int gridSize) const
static constexpr int kMinGridSize
void setEdgeLodDeltas(int north, int south, int west, int east)
void setHeightField(HeightField *heightField)
void spanChanged()
HeightField * heightField() const
Field for sampleFromField to sample from; not owned, may be null.
void heightsChanged()
QList< float > heights() const
(gridSize+1)^2 heights row-major from the north-west corner; empty = flat
void componentComplete() override
bool sampleFromField(const TileMath::TileKey &key)
void gridSizeChanged()
static constexpr int kMaxGridSize
static constexpr double kSkirtDepthFraction
base skirt depth (fraction of span); scaled by 2^maxEdgeDelta
void setSpan(qreal span)
void setGridSize(int gridSize)
void setHeights(const QList< float > &heights)
qreal span() const
int gridSize() const
void edgeLodDeltasChanged()
void heightFieldChanged()