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