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