QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgstiledscenechunkloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstiledscenechunkloader_p.cpp
3 --------------------------------------
4 Date : July 2023
5 Copyright : (C) 2023 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgs3dmapsettings.h"
19#include "qgsapplication.h"
20#include "qgscesiumutils.h"
21#include "qgsgltf3dutils.h"
24#include "qgstiledscenetile.h"
25
26#include <QtConcurrentRun>
27
28
30
31size_t qHash( const QgsChunkNodeId &n )
32{
33 return n.uniqueId;
34}
35
36static bool hasLargeBounds( const QgsTiledSceneTile &t )
37{
38 if ( t.geometricError() > 1e6 )
39 return true;
40
41 if ( t.boundingVolume().box().isNull() )
42 return true;
43
44 const QgsVector3D size = t.boundingVolume().box().size();
45 return size.x() > 1e5 || size.y() > 1e5 || size.z() > 1e5;
46}
47
49
50QgsTiledSceneChunkLoader::QgsTiledSceneChunkLoader( QgsChunkNode *node, const QgsTiledSceneIndex &index, const QgsTiledSceneChunkLoaderFactory &factory, double zValueScale, double zValueOffset )
51 : QgsChunkLoader( node )
52 , mFactory( factory )
53 , mIndex( index )
54{
55 mFutureWatcher = new QFutureWatcher<void>( this );
56 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
57
58 const QgsChunkNodeId tileId = node->tileId();
59 const QFuture<void> future = QtConcurrent::run( [this, tileId, zValueScale, zValueOffset]
60 {
61 const QgsTiledSceneTile tile = mIndex.getTile( tileId.uniqueId );
62
63 // we do not load tiles that are too big - at least for the time being
64 // the problem is that their 3D bounding boxes with ECEF coordinates are huge
65 // and we are unable to turn them into planar bounding boxes
66 if ( hasLargeBounds( tile ) )
67 return;
68
69 QString uri = tile.resources().value( QStringLiteral( "content" ) ).toString();
70 if ( uri.isEmpty() )
71 {
72 // nothing to show for this tile
73 // TODO: can we skip loading it at all?
74 return;
75 }
76
77 uri = tile.baseUrl().resolved( uri ).toString();
78 QByteArray content = mFactory.mIndex.retrieveContent( uri );
79 if ( content.isEmpty() )
80 {
81 // the request probably failed
82 // TODO: how can we report it?
83 return;
84 }
85
87 if ( tileContent.gltf.isEmpty() )
88 {
89 // unsupported tile content type
90 return;
91 }
92
93 QgsGltf3DUtils::EntityTransform entityTransform;
94 entityTransform.tileTransform = ( tile.transform() ? *tile.transform() : QgsMatrix4x4() );
95 entityTransform.tileTransform.translate( tileContent.rtcCenter );
96 entityTransform.sceneOriginTargetCrs = mFactory.mMap.origin();
97 entityTransform.ecefToTargetCrs = &mFactory.mBoundsTransform;
98 entityTransform.zValueScale = zValueScale;
99 entityTransform.zValueOffset = zValueOffset;
100 entityTransform.gltfUpAxis = static_cast< Qgis::Axis >( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Y ) ).toInt() );
101
102 QStringList errors;
103 mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
104
105 if ( mEntity )
106 mEntity->moveToThread( QgsApplication::instance()->thread() );
107
108 // TODO: report errors somewhere?
109 if ( !errors.isEmpty() )
110 {
111 QgsDebugError( "gltf load errors: " + errors.join( '\n' ) );
112 }
113 } );
114
115 // emit finished() as soon as the handler is populated with features
116 mFutureWatcher->setFuture( future );
117}
118
119QgsTiledSceneChunkLoader::~QgsTiledSceneChunkLoader()
120{
121 if ( !mFutureWatcher->isFinished() )
122 {
123 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
124 mFutureWatcher->waitForFinished();
125 }
126}
127
128Qt3DCore::QEntity *QgsTiledSceneChunkLoader::createEntity( Qt3DCore::QEntity *parent )
129{
130 if ( !mEntity )
131 return new Qt3DCore::QEntity( parent );
132
133 mEntity->setParent( parent );
134 return mEntity;
135}
136
138
139QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory( const Qgs3DMapSettings &map, const QgsTiledSceneIndex &index, double zValueScale, double zValueOffset )
140 : mMap( map )
141 , mIndex( index )
142 , mZValueScale( zValueScale )
143 , mZValueOffset( zValueOffset )
144{
145 mBoundsTransform = QgsCoordinateTransform( QgsCoordinateReferenceSystem( "EPSG:4978" ), mMap.crs(), mMap.transformContext() );
146}
147
148QgsChunkLoader *QgsTiledSceneChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
149{
150 return new QgsTiledSceneChunkLoader( node, mIndex, *this, mZValueScale, mZValueOffset );
151}
152
153// converts box from map coordinates to world coords (also flips [X,Y] to [X,-Z])
154static QgsAABB aabbConvert( const QgsBox3D &b0, const QgsVector3D &sceneOriginTargetCrs )
155{
156 const QgsBox3D b = b0 - sceneOriginTargetCrs;
157 return QgsAABB( b.xMinimum(), b.zMinimum(), -b.yMaximum(), b.xMaximum(), b.zMaximum(), -b.yMinimum() );
158}
159
160QgsChunkNode *QgsTiledSceneChunkLoaderFactory::nodeForTile( const QgsTiledSceneTile &t, const QgsChunkNodeId &nodeId, QgsChunkNode *parent ) const
161{
162 QgsChunkNode *node = nullptr;
163 if ( hasLargeBounds( t ) )
164 {
165 // use the full extent of the scene
166 QgsVector3D v0 = mMap.mapToWorldCoordinates( QgsVector3D( mMap.extent().xMinimum(), mMap.extent().yMinimum(), -100 ) );
167 QgsVector3D v1 = mMap.mapToWorldCoordinates( QgsVector3D( mMap.extent().xMaximum(), mMap.extent().yMaximum(), +100 ) );
168 QgsAABB aabb( v0.x(), v0.y(), v0.z(), v1.x(), v1.y(), v1.z() );
169 float err = std::min( 1e6, t.geometricError() );
170 node = new QgsChunkNode( nodeId, aabb, err, parent );
171 }
172 else
173 {
174 QgsBox3D box = t.boundingVolume().bounds( mBoundsTransform );
175 box.setZMinimum( box.zMinimum() * mZValueScale + mZValueOffset );
176 box.setZMaximum( box.zMaximum() * mZValueScale + mZValueOffset );
177 const QgsAABB aabb = aabbConvert( box, mMap.origin() );
178 node = new QgsChunkNode( nodeId, aabb, t.geometricError(), parent );
179 }
180
181 node->setRefinementProcess( t.refinementProcess() );
182 return node;
183}
184
185
186QgsChunkNode *QgsTiledSceneChunkLoaderFactory::createRootNode() const
187{
188 const QgsTiledSceneTile t = mIndex.rootTile();
189 return nodeForTile( t, QgsChunkNodeId( t.id() ), nullptr );
190}
191
192
193QVector<QgsChunkNode *> QgsTiledSceneChunkLoaderFactory::createChildren( QgsChunkNode *node ) const
194{
195 QVector<QgsChunkNode *> children;
196 const long long indexTileId = node->tileId().uniqueId;
197
198 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
199 Q_ASSERT( mIndex.childAvailability( indexTileId ) != Qgis::TileChildrenAvailability::NeedFetching );
200
201 const QVector< long long > childIds = mIndex.childTileIds( indexTileId );
202 for ( long long childId : childIds )
203 {
204 const QgsChunkNodeId chId( childId );
205 QgsTiledSceneTile t = mIndex.getTile( childId );
206
207 // first check if this node should be even considered
208 if ( hasLargeBounds( t ) )
209 {
210 // if the tile is huge, let's try to see if our scene is actually inside
211 // (if not, let' skip this child altogether!)
212 // TODO: make OBB of our scene in ECEF rather than just using center of the scene?
213 const QgsOrientedBox3D obb = t.boundingVolume().box();
214
215 const QgsPointXY c = mMap.extent().center();
216 const QgsVector3D cEcef = mBoundsTransform.transform( QgsVector3D( c.x(), c.y(), 0 ), Qgis::TransformDirection::Reverse );
217 const QgsVector3D ecef2 = cEcef - obb.center();
218
219 const double *half = obb.halfAxes();
220
221 // this is an approximate check anyway, no need for double precision matrix/vector
222 QMatrix4x4 rot(
223 half[0], half[3], half[6], 0,
224 half[1], half[4], half[7], 0,
225 half[2], half[5], half[8], 0,
226 0, 0, 0, 1 );
227 QVector3D aaa = rot.inverted().map( ecef2.toVector3D() );
228
229 if ( aaa.x() > 1 || aaa.y() > 1 || aaa.z() > 1 ||
230 aaa.x() < -1 || aaa.y() < -1 || aaa.z() < -1 )
231 {
232 continue;
233 }
234 }
235
236 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
237 Q_ASSERT( mIndex.childAvailability( childId ) != Qgis::TileChildrenAvailability::NeedFetching );
238
239 QgsChunkNode *nChild = nodeForTile( t, chId, node );
240 children.append( nChild );
241 }
242 return children;
243}
244
245bool QgsTiledSceneChunkLoaderFactory::canCreateChildren( QgsChunkNode *node )
246{
247 long long nodeId = node->tileId().uniqueId;
248 if ( mFutureHierarchyFetches.contains( nodeId ) || mPendingHierarchyFetches.contains( nodeId ) )
249 return false;
250
251 if ( mIndex.childAvailability( nodeId ) == Qgis::TileChildrenAvailability::NeedFetching )
252 {
253 mFutureHierarchyFetches.insert( nodeId );
254 return false;
255 }
256
257 // we need to make sure that if a child tile's content references another tileset JSON,
258 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
259 // end up trying to load tileset JSON file instead of the actual content
260
261 const QVector< long long > childIds = mIndex.childTileIds( nodeId );
262 for ( long long childId : childIds )
263 {
264 if ( mFutureHierarchyFetches.contains( childId ) || mPendingHierarchyFetches.contains( childId ) )
265 return false;
266
267 if ( mIndex.childAvailability( childId ) == Qgis::TileChildrenAvailability::NeedFetching )
268 {
269 mFutureHierarchyFetches.insert( childId );
270 return false;
271 }
272 }
273 return true;
274}
275
276void QgsTiledSceneChunkLoaderFactory::fetchHierarchyForNode( long long nodeId, QgsChunkNode *origNode )
277{
278 Q_ASSERT( !mPendingHierarchyFetches.contains( nodeId ) );
279 mFutureHierarchyFetches.remove( nodeId );
280 mPendingHierarchyFetches.insert( nodeId );
281
282 QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>( this );
283 connect( futureWatcher, &QFutureWatcher<void>::finished, this, [this, origNode, nodeId, futureWatcher]
284 {
285 mPendingHierarchyFetches.remove( nodeId );
286 emit childrenPrepared( origNode );
287 futureWatcher->deleteLater();
288 } );
289 futureWatcher->setFuture( QtConcurrent::run( [this, nodeId]
290 {
291 mIndex.fetchHierarchy( nodeId );
292 } ) );
293}
294
295void QgsTiledSceneChunkLoaderFactory::prepareChildren( QgsChunkNode *node )
296{
297 long long nodeId = node->tileId().uniqueId;
298 if ( mFutureHierarchyFetches.contains( nodeId ) )
299 {
300 fetchHierarchyForNode( nodeId, node );
301 return;
302 }
303
304 // we need to make sure that if a child tile's content references another tileset JSON,
305 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
306 // end up trying to load tileset JSON file instead of the actual content
307
308 const QVector< long long > childIds = mIndex.childTileIds( nodeId );
309 for ( long long childId : childIds )
310 {
311 if ( mFutureHierarchyFetches.contains( childId ) )
312 {
313 fetchHierarchyForNode( childId, node );
314 }
315 }
316}
317
318
320
321QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity( const Qgs3DMapSettings &map, const QgsTiledSceneIndex &index, double maximumScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset )
322 : QgsChunkedEntity( maximumScreenError, new QgsTiledSceneChunkLoaderFactory( map, index, zValueScale, zValueOffset ), true )
323 , mIndex( index )
324{
325 setShowBoundingBoxes( showBoundingBoxes );
326}
327
328QgsTiledSceneLayerChunkedEntity::~QgsTiledSceneLayerChunkedEntity()
329{
330 // cancel / wait for jobs
331 cancelActiveJobs();
332}
333
334int QgsTiledSceneLayerChunkedEntity::pendingJobsCount() const
335{
336 return QgsChunkedEntity::pendingJobsCount() + static_cast<QgsTiledSceneChunkLoaderFactory *>( mChunkLoaderFactory )->mPendingHierarchyFetches.count();
337}
338
339QVector<QgsRayCastingUtils::RayHit> QgsTiledSceneLayerChunkedEntity::rayIntersection( const QgsRayCastingUtils::Ray3D &ray, const QgsRayCastingUtils::RayCastContext &context ) const
340{
341 Q_UNUSED( context );
342 QgsDebugMsgLevel( QStringLiteral( "Ray cast on tiled scene layer" ), 2 );
343#ifdef QGISDEBUG
344 int nodeUsed = 0;
345 int nodesAll = 0;
346 int hits = 0;
347#endif
348
349 QVector<QgsRayCastingUtils::RayHit> result;
350 float minDist = -1;
351 QVector3D intersectionPoint;
352 QgsChunkNode *minNode = nullptr;
353 int minTriangleIndex = -1;
354
355 const QList<QgsChunkNode *> active = activeNodes();
356 for ( QgsChunkNode *node : active )
357 {
358#ifdef QGISDEBUG
359 nodesAll++;
360#endif
361 if ( node->entity() &&
362 ( minDist < 0 || node->bbox().distanceFromPoint( ray.origin() ) < minDist ) &&
363 QgsRayCastingUtils::rayBoxIntersection( ray, node->bbox() ) )
364 {
365#ifdef QGISDEBUG
366 nodeUsed++;
367#endif
368 const QList<Qt3DRender::QGeometryRenderer *> rendLst = node->entity()->findChildren<Qt3DRender::QGeometryRenderer *>();
369 for ( const auto &rend : rendLst )
370 {
371 QVector3D nodeIntPoint;
372 int triangleIndex = -1;
373 bool success = QgsRayCastingUtils::rayMeshIntersection( rend, ray, QMatrix4x4(), nodeIntPoint, triangleIndex );
374 if ( success )
375 {
376#ifdef QGISDEBUG
377 hits++;
378#endif
379 float dist = ( ray.origin() - nodeIntPoint ).length();
380 if ( minDist < 0 || dist < minDist )
381 {
382 minDist = dist;
383 minNode = node;
384 minTriangleIndex = triangleIndex;
385 intersectionPoint = nodeIntPoint;
386 }
387 }
388 }
389 }
390 }
391
392 if ( minDist >= 0 )
393 {
394 QVariantMap vm;
395 QgsTiledSceneTile tile = mIndex.getTile( minNode->tileId().uniqueId );
396 // at this point this is mostly for debugging - we may want to change/rename what's returned here
397 vm["node_id"] = tile.id();
398 vm["node_error"] = tile.geometricError();
399 vm["node_content"] = tile.resources().value( QStringLiteral( "content" ) );
400 vm["triangle_index"] = minTriangleIndex;
401 QgsRayCastingUtils::RayHit hit( minDist, intersectionPoint, FID_NULL, vm );
402 result.append( hit );
403 }
404
405 QgsDebugMsgLevel( QStringLiteral( "Active Nodes: %1, checked nodes: %2, hits found: %3" ).arg( nodesAll ).arg( nodeUsed ).arg( hits ), 2 );
406 return result;
407}
408
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Axis
Cartesian axes.
Definition: qgis.h:1989
@ Y
Y-axis.
@ Reverse
Reverse/inverse transform (from destination to source)
3
Definition: qgsaabb.h:33
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
A 3-dimensional box composed of x, y, z coordinates.
Definition: qgsbox3d.h:43
double yMaximum() const
Returns the maximum y value.
Definition: qgsbox3d.h:197
void setZMinimum(double z)
Sets the minimum z value.
Definition: qgsbox3d.cpp:76
void setZMaximum(double z)
Sets the maximum z value.
Definition: qgsbox3d.cpp:81
double xMinimum() const
Returns the minimum x value.
Definition: qgsbox3d.h:162
double zMaximum() const
Returns the maximum z value.
Definition: qgsbox3d.h:225
double xMaximum() const
Returns the maximum x value.
Definition: qgsbox3d.h:169
double zMinimum() const
Returns the minimum z value.
Definition: qgsbox3d.h:218
double yMinimum() const
Returns the minimum y value.
Definition: qgsbox3d.h:190
static TileContents extractGltfFromTileContent(const QByteArray &tileContent)
Parses tile content.
This class represents a coordinate reference system (CRS).
Class for doing transforms between two map coordinate systems.
A simple 4x4 matrix implementation useful for transformation in 3D space.
Definition: qgsmatrix4x4.h:40
Represents a oriented (rotated) box in 3 dimensions.
const double * halfAxes() const
Returns the half axes matrix;.
bool isNull() const
Returns true if the box is a null box.
QgsVector3D center() const
Returns the vector to the center of the box.
QgsVector3D size() const
Returns size of sides of the box.
A class to represent a 2D point.
Definition: qgspointxy.h:60
QgsOrientedBox3D box() const
Returns the volume's oriented box.
QgsBox3D bounds(const QgsCoordinateTransform &transform=QgsCoordinateTransform(), Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Returns the axis aligned bounding box of the volume.
An index for tiled scene data providers.
Represents an individual tile from a tiled scene data source.
Qgis::TileRefinementProcess refinementProcess() const
Returns the tile's refinement process.
QVariantMap resources() const
Returns the resources attached to the tile.
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
QVariantMap metadata() const
Returns additional metadata attached to the tile.
long long id() const
Returns the tile's unique ID.
const QgsMatrix4x4 * transform() const
Returns the tile's transform.
double geometricError() const
Returns the tile's geometric error, which is the error, in scene CRS units, of the tile's simplified ...
QUrl baseUrl() const
Returns the tile's base URL.
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition: qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition: qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition: qgsvector3d.h:52
QVector3D toVector3D() const
Converts the current object to QVector3D.
Definition: qgsvector3d.h:186
double x() const
Returns X coordinate.
Definition: qgsvector3d.h:48
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
uint qHash(const QVariant &variant)
Hash for QVariant.
Definition: qgis.cpp:198
#define FID_NULL
Definition: qgsfeatureid.h:29
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38
Encapsulates the contents of a 3D tile.