QGIS API Documentation 3.43.0-Master (3ee7834ace6)
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#include "moc_qgstiledscenechunkloader_p.cpp"
18
19#include "qgs3dmapsettings.h"
20#include "qgs3dutils.h"
21#include "qgsapplication.h"
22#include "qgscesiumutils.h"
24#include "qgsgeotransform.h"
25#include "qgsgltf3dutils.h"
29#include "qgstiledscenetile.h"
30
31#include <QtConcurrentRun>
32
33
35
36size_t qHash( const QgsChunkNodeId &n )
37{
38 return n.uniqueId;
39}
40
41static bool hasLargeBounds( const QgsTiledSceneTile &t, const QgsCoordinateTransform &boundsTransform )
42{
43 if ( t.geometricError() > 1e6 )
44 return true;
45
46 if ( t.boundingVolume().box().isNull() )
47 return true;
48
49 Q_ASSERT( boundsTransform.destinationCrs().mapUnits() == Qgis::DistanceUnit::Meters );
50 QgsBox3D bounds = t.boundingVolume().bounds( boundsTransform );
51 return bounds.width() > 1e5 || bounds.height() > 1e5 || bounds.depth() > 1e5;
52}
53
55
56QgsTiledSceneChunkLoader::QgsTiledSceneChunkLoader( QgsChunkNode *node, const QgsTiledSceneIndex &index, const QgsTiledSceneChunkLoaderFactory &factory, double zValueScale, double zValueOffset )
57 : QgsChunkLoader( node )
58 , mFactory( factory )
59 , mIndex( index )
60{
61 mFutureWatcher = new QFutureWatcher<void>( this );
62 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
63
64 const QgsCoordinateTransform &boundsTransform = factory.mBoundsTransform;
65
66 const QgsChunkNodeId tileId = node->tileId();
67 const QgsVector3D chunkOrigin = node->box3D().center();
68 const bool isGlobe = factory.mRenderContext.crs().type() == Qgis::CrsType::Geocentric;
69 const QFuture<void> future = QtConcurrent::run( [this, tileId, zValueScale, zValueOffset, boundsTransform, chunkOrigin, isGlobe] {
70 const QgsTiledSceneTile tile = mIndex.getTile( tileId.uniqueId );
71
72 // we do not load tiles that are too big when not in globe scene mode...
73 // the problem is that their 3D bounding boxes with ECEF coordinates are huge
74 // and we are unable to turn them into planar bounding boxes
75 if ( !isGlobe && hasLargeBounds( tile, boundsTransform ) )
76 return;
77
78 QString uri = tile.resources().value( QStringLiteral( "content" ) ).toString();
79 if ( uri.isEmpty() )
80 {
81 // nothing to show for this tile
82 // TODO: can we skip loading it at all?
83 return;
84 }
85
86 uri = tile.baseUrl().resolved( uri ).toString();
87 QByteArray content = mFactory.mIndex.retrieveContent( uri );
88 if ( content.isEmpty() )
89 {
90 // the request probably failed
91 // TODO: how can we report it?
92 return;
93 }
94
95 QgsGltf3DUtils::EntityTransform entityTransform;
96 entityTransform.tileTransform = ( tile.transform() ? *tile.transform() : QgsMatrix4x4() );
97 entityTransform.chunkOriginTargetCrs = chunkOrigin;
98 entityTransform.ecefToTargetCrs = &mFactory.mBoundsTransform;
99 entityTransform.zValueScale = zValueScale;
100 entityTransform.zValueOffset = zValueOffset;
101 entityTransform.gltfUpAxis = static_cast<Qgis::Axis>( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast<int>( Qgis::Axis::Y ) ).toInt() );
102
103 const QString &format = tile.metadata().value( QStringLiteral( "contentFormat" ) ).value<QString>();
104 QStringList errors;
105 if ( format == QLatin1String( "quantizedmesh" ) )
106 {
107 try
108 {
109 QgsQuantizedMeshTile qmTile( content );
110 qmTile.removeDegenerateTriangles();
111 tinygltf::Model model = qmTile.toGltf( true, 100 );
112 mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, uri, &errors );
113 }
115 {
116 errors.append( QStringLiteral( "Failed to parse tile from '%1'" ).arg( uri ) );
117 }
118 }
119 else if ( format == "cesiumtiles" )
120 {
122 if ( tileContent.gltf.isEmpty() )
123 return;
124 entityTransform.tileTransform.translate( tileContent.rtcCenter );
125 mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
126 }
127 else
128 return; // unsupported tile content type
129
130 // TODO: report errors somewhere?
131 if ( !errors.isEmpty() )
132 {
133 QgsDebugError( "gltf load errors: " + errors.join( '\n' ) );
134 }
135
136 if ( mEntity )
137 {
138 QgsGeoTransform *transform = new QgsGeoTransform;
139 transform->setGeoTranslation( chunkOrigin );
140 mEntity->addComponent( transform );
141
142 mEntity->moveToThread( QgsApplication::instance()->thread() );
143 }
144 } );
145
146 // emit finished() as soon as the handler is populated with features
147 mFutureWatcher->setFuture( future );
148}
149
150QgsTiledSceneChunkLoader::~QgsTiledSceneChunkLoader()
151{
152 if ( !mFutureWatcher->isFinished() )
153 {
154 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
155 mFutureWatcher->waitForFinished();
156 }
157}
158
159Qt3DCore::QEntity *QgsTiledSceneChunkLoader::createEntity( Qt3DCore::QEntity *parent )
160{
161 if ( mEntity )
162 mEntity->setParent( parent );
163 return mEntity;
164}
165
167
168QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory( const Qgs3DRenderContext &context, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, double zValueScale, double zValueOffset )
169 : mRenderContext( context )
170 , mIndex( index )
171 , mZValueScale( zValueScale )
172 , mZValueOffset( zValueOffset )
173{
174 mBoundsTransform = QgsCoordinateTransform( tileCrs, context.crs(), context.transformContext() );
175}
176
177QgsChunkLoader *QgsTiledSceneChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
178{
179 return new QgsTiledSceneChunkLoader( node, mIndex, *this, mZValueScale, mZValueOffset );
180}
181
182QgsChunkNode *QgsTiledSceneChunkLoaderFactory::nodeForTile( const QgsTiledSceneTile &t, const QgsChunkNodeId &nodeId, QgsChunkNode *parent ) const
183{
184 QgsChunkNode *node = nullptr;
185 if ( mRenderContext.crs().type() != Qgis::CrsType::Geocentric && hasLargeBounds( t, mBoundsTransform ) )
186 {
187 // use the full extent of the scene
188 QgsVector3D v0( mRenderContext.extent().xMinimum(), mRenderContext.extent().yMinimum(), -100 );
189 QgsVector3D v1( mRenderContext.extent().xMaximum(), mRenderContext.extent().yMaximum(), +100 );
190 QgsBox3D box3D( v0, v1 );
191 float err = std::min( 1e6, t.geometricError() );
192 node = new QgsChunkNode( nodeId, box3D, err, parent );
193 }
194 else
195 {
196 QgsBox3D box = t.boundingVolume().bounds( mBoundsTransform );
197 box.setZMinimum( box.zMinimum() * mZValueScale + mZValueOffset );
198 box.setZMaximum( box.zMaximum() * mZValueScale + mZValueOffset );
199 node = new QgsChunkNode( nodeId, box, t.geometricError(), parent );
200 }
201
202 node->setRefinementProcess( t.refinementProcess() );
203 return node;
204}
205
206
207QgsChunkNode *QgsTiledSceneChunkLoaderFactory::createRootNode() const
208{
209 const QgsTiledSceneTile t = mIndex.rootTile();
210 return nodeForTile( t, QgsChunkNodeId( t.id() ), nullptr );
211}
212
213
214QVector<QgsChunkNode *> QgsTiledSceneChunkLoaderFactory::createChildren( QgsChunkNode *node ) const
215{
216 QVector<QgsChunkNode *> children;
217 const long long indexTileId = node->tileId().uniqueId;
218
219 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
220 Q_ASSERT( mIndex.childAvailability( indexTileId ) != Qgis::TileChildrenAvailability::NeedFetching );
221
222 const QVector<long long> childIds = mIndex.childTileIds( indexTileId );
223 for ( long long childId : childIds )
224 {
225 const QgsChunkNodeId chId( childId );
226 QgsTiledSceneTile t = mIndex.getTile( childId );
227
228 // first check if this node should be even considered
229 // XXX: This check doesn't work for Quantized Mesh layers and possibly some
230 // Cesium 3D tiles as well. For now this hack is in place to make sure both
231 // work in practice.
232 if ( t.metadata()["contentFormat"] == QStringLiteral( "cesiumtiles" )
233 && mRenderContext.crs().type() != Qgis::CrsType::Geocentric
234 && hasLargeBounds( t, mBoundsTransform ) )
235 {
236 // if the tile is huge, let's try to see if our scene is actually inside
237 // (if not, let' skip this child altogether!)
238 // TODO: make OBB of our scene in ECEF rather than just using center of the scene?
239 const QgsOrientedBox3D obb = t.boundingVolume().box();
240 const QgsPointXY c = mRenderContext.extent().center();
241 const QgsVector3D cEcef = mBoundsTransform.transform( QgsVector3D( c.x(), c.y(), 0 ), Qgis::TransformDirection::Reverse );
242 const QgsVector3D ecef2 = cEcef - obb.center();
243 const double *half = obb.halfAxes();
244 // this is an approximate check anyway, no need for double precision matrix/vector
245 QMatrix4x4 rot(
246 half[0], half[3], half[6], 0,
247 half[1], half[4], half[7], 0,
248 half[2], half[5], half[8], 0,
249 0, 0, 0, 1
250 );
251 QVector3D aaa = rot.inverted().map( ecef2.toVector3D() );
252 if ( aaa.x() > 1 || aaa.y() > 1 || aaa.z() > 1 || aaa.x() < -1 || aaa.y() < -1 || aaa.z() < -1 )
253 {
254 continue;
255 }
256 }
257
258 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
259 Q_ASSERT( mIndex.childAvailability( childId ) != Qgis::TileChildrenAvailability::NeedFetching );
260
261 QgsChunkNode *nChild = nodeForTile( t, chId, node );
262 children.append( nChild );
263 }
264 return children;
265}
266
267bool QgsTiledSceneChunkLoaderFactory::canCreateChildren( QgsChunkNode *node )
268{
269 long long nodeId = node->tileId().uniqueId;
270 if ( mFutureHierarchyFetches.contains( nodeId ) || mPendingHierarchyFetches.contains( nodeId ) )
271 return false;
272
273 if ( mIndex.childAvailability( nodeId ) == Qgis::TileChildrenAvailability::NeedFetching )
274 {
275 mFutureHierarchyFetches.insert( nodeId );
276 return false;
277 }
278
279 // we need to make sure that if a child tile's content references another tileset JSON,
280 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
281 // end up trying to load tileset JSON file instead of the actual content
282
283 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
284 for ( long long childId : childIds )
285 {
286 if ( mFutureHierarchyFetches.contains( childId ) || mPendingHierarchyFetches.contains( childId ) )
287 return false;
288
289 if ( mIndex.childAvailability( childId ) == Qgis::TileChildrenAvailability::NeedFetching )
290 {
291 mFutureHierarchyFetches.insert( childId );
292 return false;
293 }
294 }
295 return true;
296}
297
298void QgsTiledSceneChunkLoaderFactory::fetchHierarchyForNode( long long nodeId, QgsChunkNode *origNode )
299{
300 Q_ASSERT( !mPendingHierarchyFetches.contains( nodeId ) );
301 mFutureHierarchyFetches.remove( nodeId );
302 mPendingHierarchyFetches.insert( nodeId );
303
304 QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>( this );
305 connect( futureWatcher, &QFutureWatcher<void>::finished, this, [this, origNode, nodeId, futureWatcher] {
306 mPendingHierarchyFetches.remove( nodeId );
307 emit childrenPrepared( origNode );
308 futureWatcher->deleteLater();
309 } );
310 futureWatcher->setFuture( QtConcurrent::run( [this, nodeId] {
311 mIndex.fetchHierarchy( nodeId );
312 } ) );
313}
314
315void QgsTiledSceneChunkLoaderFactory::prepareChildren( QgsChunkNode *node )
316{
317 long long nodeId = node->tileId().uniqueId;
318 if ( mFutureHierarchyFetches.contains( nodeId ) )
319 {
320 fetchHierarchyForNode( nodeId, node );
321 return;
322 }
323
324 // we need to make sure that if a child tile's content references another tileset JSON,
325 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
326 // end up trying to load tileset JSON file instead of the actual content
327
328 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
329 for ( long long childId : childIds )
330 {
331 if ( mFutureHierarchyFetches.contains( childId ) )
332 {
333 fetchHierarchyForNode( childId, node );
334 }
335 }
336}
337
338
340
341QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity( Qgs3DMapSettings *map, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, double maximumScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset )
342 : QgsChunkedEntity( map, maximumScreenError, new QgsTiledSceneChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), index, tileCrs, zValueScale, zValueOffset ), true )
343 , mIndex( index )
344{
345 setShowBoundingBoxes( showBoundingBoxes );
346}
347
348QgsTiledSceneLayerChunkedEntity::~QgsTiledSceneLayerChunkedEntity()
349{
350 // cancel / wait for jobs
351 cancelActiveJobs();
352}
353
354int QgsTiledSceneLayerChunkedEntity::pendingJobsCount() const
355{
356 return QgsChunkedEntity::pendingJobsCount() + static_cast<QgsTiledSceneChunkLoaderFactory *>( mChunkLoaderFactory )->mPendingHierarchyFetches.count();
357}
358
359QVector<QgsRayCastingUtils::RayHit> QgsTiledSceneLayerChunkedEntity::rayIntersection( const QgsRayCastingUtils::Ray3D &ray, const QgsRayCastingUtils::RayCastContext &context ) const
360{
361 Q_UNUSED( context );
362 QgsDebugMsgLevel( QStringLiteral( "Ray cast on tiled scene layer" ), 2 );
363#ifdef QGISDEBUG
364 int nodeUsed = 0;
365 int nodesAll = 0;
366 int hits = 0;
367#endif
368
369 QVector<QgsRayCastingUtils::RayHit> result;
370 float minDist = -1;
371 QVector3D intersectionPoint;
372 QgsChunkNode *minNode = nullptr;
373 int minTriangleIndex = -1;
374
375 const QList<QgsChunkNode *> active = activeNodes();
376 for ( QgsChunkNode *node : active )
377 {
378#ifdef QGISDEBUG
379 nodesAll++;
380#endif
381
382 QgsAABB nodeBbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
383
384 if ( node->entity() && ( minDist < 0 || nodeBbox.distanceFromPoint( ray.origin() ) < minDist ) && QgsRayCastingUtils::rayBoxIntersection( ray, nodeBbox ) )
385 {
386#ifdef QGISDEBUG
387 nodeUsed++;
388#endif
389 const QList<Qt3DRender::QGeometryRenderer *> rendLst = node->entity()->findChildren<Qt3DRender::QGeometryRenderer *>();
390 for ( Qt3DRender::QGeometryRenderer *rend : rendLst )
391 {
392 QVector3D nodeIntPoint;
393 int triangleIndex = -1;
394 bool success = QgsRayCastingUtils::rayMeshIntersection( rend, ray, QMatrix4x4(), nodeIntPoint, triangleIndex );
395 if ( success )
396 {
397#ifdef QGISDEBUG
398 hits++;
399#endif
400 float dist = ( ray.origin() - nodeIntPoint ).length();
401 if ( minDist < 0 || dist < minDist )
402 {
403 minDist = dist;
404 minNode = node;
405 minTriangleIndex = triangleIndex;
406 intersectionPoint = nodeIntPoint;
407 }
408 }
409 }
410 }
411 }
412
413 if ( minDist >= 0 )
414 {
415 QVariantMap vm;
416 QgsTiledSceneTile tile = mIndex.getTile( minNode->tileId().uniqueId );
417 // at this point this is mostly for debugging - we may want to change/rename what's returned here
418 vm[QStringLiteral( "node_id" )] = tile.id();
419 vm[QStringLiteral( "node_error" )] = tile.geometricError();
420 vm[QStringLiteral( "node_content" )] = tile.resources().value( QStringLiteral( "content" ) );
421 vm[QStringLiteral( "triangle_index" )] = minTriangleIndex;
422 QgsRayCastingUtils::RayHit hit( minDist, intersectionPoint, FID_NULL, vm );
423 result.append( hit );
424 }
425
426 QgsDebugMsgLevel( QStringLiteral( "Active Nodes: %1, checked nodes: %2, hits found: %3" ).arg( nodesAll ).arg( nodeUsed ).arg( hits ), 2 );
427 return result;
428}
429
@ Geocentric
Geocentric CRS.
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Axis
Cartesian axes.
Definition qgis.h:2399
@ Y
Y-axis.
@ Reverse
Reverse/inverse transform (from destination to source)
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system used in the 3D scene.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which stores various information regarding which datum tran...
static QgsAABB mapToWorldExtent(const QgsRectangle &extent, double zMin, double zMax, const QgsVector3D &mapOrigin)
Converts map extent to axis aligned bounding box in 3D world coordinates.
float distanceFromPoint(float x, float y, float z) const
Returns shortest distance from the box to a point.
Definition qgsaabb.cpp:46
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:43
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:88
double depth() const
Returns the depth of the box.
Definition qgsbox3d.h:292
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:93
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:259
double width() const
Returns the width of the box.
Definition qgsbox3d.h:278
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:252
double height() const
Returns the height of the box.
Definition qgsbox3d.h:285
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.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
A simple 4x4 matrix implementation useful for transformation in 3D space.
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.
A class to represent a 2D point.
Definition qgspointxy.h:60
Exception thrown on failure to parse Quantized Mesh tile (malformed data)
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 meters, of the tile's simplified represent...
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
QVector3D toVector3D() const
Converts the current object to QVector3D.
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:199
#define FID_NULL
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40
Encapsulates the contents of a 3D tile.
QgsVector3D rtcCenter
Center position of relative-to-center coordinates (when used)
QByteArray gltf
GLTF binary content.
Helper struct to store ray casting parameters.
Helper struct to store ray casting results.