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