QGIS API Documentation 3.43.0-Master (2a27c31701b)
qgschunkedentity.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgschunkedentity.cpp
3 --------------------------------------
4 Date : July 2017
5 Copyright : (C) 2017 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
16#include "qgschunkedentity.h"
17#include "moc_qgschunkedentity.cpp"
18
19#include <QElapsedTimer>
20#include <QVector4D>
21
22#include "qgs3dutils.h"
24#include "qgschunklist_p.h"
25#include "qgschunkloader.h"
26#include "qgschunknode.h"
27#include "qgsgeotransform.h"
28
29#include "qgseventtracing.h"
30
31#include <queue>
32
34
35
36static float screenSpaceError( const QgsAABB &nodeBbox, float nodeError, const QgsChunkedEntity::SceneContext &sceneContext )
37{
38 if ( nodeError <= 0 ) //it happens for meshes
39 return 0;
40
41 float dist = nodeBbox.distanceFromPoint( sceneContext.cameraPos );
42
43 // TODO: what to do when distance == 0 ?
44
45 float sse = Qgs3DUtils::screenSpaceError( nodeError, dist, sceneContext.screenSizePx, sceneContext.cameraFov );
46 return sse;
47}
48
49
50static bool hasAnyActiveChildren( QgsChunkNode *node, QList<QgsChunkNode *> &activeNodes )
51{
52 for ( int i = 0; i < node->childCount(); ++i )
53 {
54 QgsChunkNode *child = node->children()[i];
55 if ( child->entity() && activeNodes.contains( child ) )
56 return true;
57 if ( hasAnyActiveChildren( child, activeNodes ) )
58 return true;
59 }
60 return false;
61}
62
63
64QgsChunkedEntity::QgsChunkedEntity( Qgs3DMapSettings *mapSettings, float tau, QgsChunkLoaderFactory *loaderFactory, bool ownsFactory, int primitiveBudget, Qt3DCore::QNode *parent )
65 : Qgs3DMapSceneEntity( mapSettings, parent )
66 , mTau( tau )
67 , mChunkLoaderFactory( loaderFactory )
68 , mOwnsFactory( ownsFactory )
69 , mPrimitivesBudget( primitiveBudget )
70{
71 mRootNode = loaderFactory->createRootNode();
72 mChunkLoaderQueue = new QgsChunkList;
73 mReplacementQueue = new QgsChunkList;
74
75 // in case the chunk loader factory supports fetching of hierarchy in background (to avoid GUI freezes)
76 connect( loaderFactory, &QgsChunkLoaderFactory::childrenPrepared, this, [this] {
77 setNeedsUpdate( true );
78 emit pendingJobsCountChanged();
79 } );
80}
81
82
83QgsChunkedEntity::~QgsChunkedEntity()
84{
85 // derived classes have to make sure that any pending active job has finished / been canceled
86 // before getting to this destructor - here it would be too late to cancel them
87 // (e.g. objects required for loading/updating have been deleted already)
88 Q_ASSERT( mActiveJobs.isEmpty() );
89
90 // clean up any pending load requests
91 while ( !mChunkLoaderQueue->isEmpty() )
92 {
93 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
94 QgsChunkNode *node = entry->chunk;
95
96 if ( node->state() == QgsChunkNode::QueuedForLoad )
97 node->cancelQueuedForLoad();
98 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
99 node->cancelQueuedForUpdate();
100 else
101 Q_ASSERT( false ); // impossible!
102 }
103
104 delete mChunkLoaderQueue;
105
106 while ( !mReplacementQueue->isEmpty() )
107 {
108 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
109
110 // remove loaded data from node
111 entry->chunk->unloadChunk(); // also deletes the entry
112 }
113
114 delete mReplacementQueue;
115 delete mRootNode;
116
117 if ( mOwnsFactory )
118 {
119 delete mChunkLoaderFactory;
120 }
121}
122
123
124void QgsChunkedEntity::handleSceneUpdate( const SceneContext &sceneContext )
125{
126 if ( !mIsValid )
127 return;
128
129 // Let's start the update by removing from loader queue chunks that
130 // would get frustum culled if loaded (outside of the current view
131 // of the camera). Removing them keeps the loading queue shorter,
132 // and we avoid loading chunks that we only wanted for a short period
133 // of time when camera was moving.
134 pruneLoaderQueue( sceneContext );
135
136 QElapsedTimer t;
137 t.start();
138
139 int oldJobsCount = pendingJobsCount();
140
141 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
142 mActiveNodes.clear();
143 mFrustumCulled = 0;
144 mCurrentTime = QTime::currentTime();
145
146 update( mRootNode, sceneContext );
147
148#ifdef QGISDEBUG
149 int enabled = 0, disabled = 0, unloaded = 0;
150#endif
151
152 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
153 {
154 if ( activeBefore.contains( node ) )
155 {
156 activeBefore.remove( node );
157 }
158 else
159 {
160 if ( !node->entity() )
161 {
162 QgsDebugError( "Active node has null entity - this should never happen!" );
163 continue;
164 }
165 node->entity()->setEnabled( true );
166
167 // let's make sure that any entity we're about to show has the right scene origin set
168 const QList<QgsGeoTransform *> transforms = node->entity()->findChildren<QgsGeoTransform *>();
169 for ( QgsGeoTransform *transform : transforms )
170 {
171 transform->setOrigin( mMapSettings->origin() );
172 }
173
174#ifdef QGISDEBUG
175 ++enabled;
176#endif
177 }
178 }
179
180 // disable those that were active but will not be anymore
181 for ( QgsChunkNode *node : activeBefore )
182 {
183 if ( !node->entity() )
184 {
185 QgsDebugError( "Active node has null entity - this should never happen!" );
186 continue;
187 }
188 node->entity()->setEnabled( false );
189#ifdef QGISDEBUG
190 ++disabled;
191#endif
192 }
193
194 // if this entity's loaded nodes are using more GPU memory than allowed,
195 // let's try to unload those that are not needed right now
196#ifdef QGISDEBUG
197 unloaded = unloadNodes();
198#else
199 unloadNodes();
200#endif
201
202 if ( mBboxesEntity )
203 {
204 QList<QgsBox3D> bboxes;
205 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
206 bboxes << n->box3D();
207 mBboxesEntity->setBoxes( bboxes );
208 }
209
210 // start a job from queue if there is anything waiting
211 startJobs();
212
213 mNeedsUpdate = false; // just updated
214
215 if ( pendingJobsCount() != oldJobsCount )
216 emit pendingJobsCountChanged();
217
218 QgsDebugMsgLevel( QStringLiteral( "update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms" ).arg( mActiveNodes.count() ).arg( enabled ).arg( disabled ).arg( mFrustumCulled ).arg( mChunkLoaderQueue->count() ).arg( mReplacementQueue->count() ).arg( unloaded ).arg( t.elapsed() ), 2 );
219}
220
221
222int QgsChunkedEntity::unloadNodes()
223{
224 double usedGpuMemory = Qgs3DUtils::calculateEntityGpuMemorySize( this );
225 if ( usedGpuMemory <= mGpuMemoryLimit )
226 {
227 setHasReachedGpuMemoryLimit( false );
228 return 0;
229 }
230
231 QgsDebugMsgLevel( QStringLiteral( "Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
232
233 int unloaded = 0;
234
235 // unload nodes starting from the back of the queue with currently loaded
236 // nodes - i.e. those that have been least recently used
237 QgsChunkListEntry *entry = mReplacementQueue->last();
238 while ( entry && usedGpuMemory > mGpuMemoryLimit )
239 {
240 // not all nodes are safe to unload: we do not want to unload nodes
241 // that are currently active, or have their descendants active or their
242 // siblings or their descendants are active (because in the next scene
243 // update, these would be very likely loaded again, making the unload worthless)
244 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
245 {
246 QgsChunkListEntry *entryPrev = entry->prev;
247 mReplacementQueue->takeEntry( entry );
248 usedGpuMemory -= Qgs3DUtils::calculateEntityGpuMemorySize( entry->chunk->entity() );
249 mActiveNodes.removeOne( entry->chunk );
250 entry->chunk->unloadChunk(); // also deletes the entry
251 ++unloaded;
252 entry = entryPrev;
253 }
254 else
255 {
256 entry = entry->prev;
257 }
258 }
259
260 if ( usedGpuMemory > mGpuMemoryLimit )
261 {
262 setHasReachedGpuMemoryLimit( true );
263 QgsDebugMsgLevel( QStringLiteral( "Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)" ).arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
264 }
265
266 return unloaded;
267}
268
269
270QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange( const QMatrix4x4 &viewMatrix ) const
271{
272 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
273
274 // it could be that there are no active nodes - they could be all culled or because root node
275 // is not yet loaded - we still need at least something to understand bounds of our scene
276 // so lets use the root node
277 if ( activeEntityNodes.empty() )
278 activeEntityNodes << rootNode();
279
280 float fnear = 1e9;
281 float ffar = 0;
282
283 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
284 {
285 // project each corner of bbox to camera coordinates
286 // and determine closest and farthest point.
287 QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
288 float bboxfnear;
289 float bboxffar;
290 Qgs3DUtils::computeBoundingBoxNearFarPlanes( bbox, viewMatrix, bboxfnear, bboxffar );
291 fnear = std::min( fnear, bboxfnear );
292 ffar = std::max( ffar, bboxffar );
293 }
294 return QgsRange<float>( fnear, ffar );
295}
296
297void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
298{
299 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
300 return;
301
302 if ( enabled )
303 {
304 mBboxesEntity = new QgsChunkBoundsEntity( mRootNode->box3D().center(), this );
305 }
306 else
307 {
308 mBboxesEntity->deleteLater();
309 mBboxesEntity = nullptr;
310 }
311}
312
313void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
314{
315 for ( QgsChunkNode *node : nodes )
316 {
317 if ( node->state() == QgsChunkNode::QueuedForUpdate )
318 {
319 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
320 node->cancelQueuedForUpdate();
321 }
322 else if ( node->state() == QgsChunkNode::Updating )
323 {
324 cancelActiveJob( node->updater() );
325 }
326 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
327 {
328 // there is not much to update yet
329 continue;
330 }
331 else if ( node->state() == QgsChunkNode::Loading )
332 {
333 // let's cancel the current loading job and queue for loading again
334 cancelActiveJob( node->loader() );
335 requestResidency( node );
336 continue;
337 }
338
339 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
340
341 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
342 node->setQueuedForUpdate( entry, updateJobFactory );
343 mChunkLoaderQueue->insertLast( entry );
344 }
345
346 // trigger update
347 startJobs();
348}
349
350void QgsChunkedEntity::pruneLoaderQueue( const SceneContext &sceneContext )
351{
352 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
353
354 // Step 1: collect all entries from chunk loader queue that would get frustum culled
355 // (i.e. they are outside of the current view of the camera) and therefore loading
356 // such chunks would be probably waste of time.
357 QgsChunkListEntry *e = mChunkLoaderQueue->first();
358 while ( e )
359 {
360 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
361 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( e->chunk->box3D(), mMapSettings->origin() );
362 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
363 {
364 toRemoveFromLoaderQueue.append( e->chunk );
365 }
366 e = e->next;
367 }
368
369 // Step 2: remove collected chunks from the loading queue
370 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
371 {
372 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
373 if ( n->state() == QgsChunkNode::QueuedForLoad )
374 {
375 n->cancelQueuedForLoad();
376 }
377 else // queued for update
378 {
379 n->cancelQueuedForUpdate();
380 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
381 n->unloadChunk();
382 }
383 }
384
385 if ( !toRemoveFromLoaderQueue.isEmpty() )
386 {
387 QgsDebugMsgLevel( QStringLiteral( "Pruned %1 chunks in loading queue" ).arg( toRemoveFromLoaderQueue.count() ), 2 );
388 }
389}
390
391
392int QgsChunkedEntity::pendingJobsCount() const
393{
394 return mChunkLoaderQueue->count() + mActiveJobs.count();
395}
396
397struct ResidencyRequest
398{
399 QgsChunkNode *node = nullptr;
400 float dist = 0.0;
401 int level = -1;
402 ResidencyRequest() = default;
403 ResidencyRequest(
404 QgsChunkNode *n,
405 float d,
406 int l
407 )
408 : node( n )
409 , dist( d )
410 , level( l )
411 {}
412};
413
414struct
415{
416 bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
417 {
418 if ( request.level == otherRequest.level )
419 return request.dist > otherRequest.dist;
420 return request.level > otherRequest.level;
421 }
422} ResidencyRequestSorter;
423
424void QgsChunkedEntity::update( QgsChunkNode *root, const SceneContext &sceneContext )
425{
426 QSet<QgsChunkNode *> nodes;
427 QVector<ResidencyRequest> residencyRequests;
428
429 using slotItem = std::pair<QgsChunkNode *, float>;
430 auto cmp_funct = []( const slotItem &p1, const slotItem &p2 ) {
431 return p1.second <= p2.second;
432 };
433 int renderedCount = 0;
434 std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
435 const QgsAABB rootBbox = Qgs3DUtils::mapToWorldExtent( root->box3D(), mMapSettings->origin() );
436 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
437 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
438 {
439 slotItem s = pq.top();
440 pq.pop();
441 QgsChunkNode *node = s.first;
442
443 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
444 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
445 {
446 ++mFrustumCulled;
447 continue;
448 }
449
450 // ensure we have child nodes (at least skeletons) available, if any
451 if ( !node->hasChildrenPopulated() )
452 {
453 // Some chunked entities (e.g. tiled scene) may not know the full node hierarchy in advance
454 // and need to fetch it from a remote server. Having a blocking network request
455 // in createChildren() is not wanted because this code runs on the main thread and thus
456 // would cause GUI freezes. Here is a mechanism to first check whether there are any
457 // network requests needed (with canCreateChildren()), and if that's the case,
458 // prepareChildren() will start those requests in the background and immediately returns.
459 // The factory will emit a signal when hierarchy fetching is done to force another update
460 // of this entity to create children of this node.
461 if ( mChunkLoaderFactory->canCreateChildren( node ) )
462 {
463 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
464 }
465 else
466 {
467 mChunkLoaderFactory->prepareChildren( node );
468 }
469 }
470
471 // make sure all nodes leading to children are always loaded
472 // so that zooming out does not create issues
473 double dist = bbox.center().distanceToPoint( sceneContext.cameraPos );
474 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
475
476 if ( !node->entity() && node->hasData() )
477 {
478 // this happens initially when root node is not ready yet
479 continue;
480 }
481 bool becomesActive = false;
482
483 // QgsDebugMsgLevel( QStringLiteral( "%1|%2|%3 %4 %5" ).arg( node->tileId().x ).arg( node->tileId().y ).arg( node->tileId().z ).arg( mTau ).arg( screenSpaceError( node, sceneContext ) ), 2 );
484 if ( node->childCount() == 0 )
485 {
486 // there's no children available for this node, so regardless of whether it has an acceptable error
487 // or not, it's the best we'll ever get...
488 becomesActive = true;
489 }
490 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
491 {
492 // acceptable error for the current chunk - let's render it
493 becomesActive = true;
494 }
495 else
496 {
497 // This chunk does not have acceptable error (it does not provide enough detail)
498 // so we'll try to use its children. The exact logic depends on whether the entity
499 // has additive strategy. With additive strategy, child nodes should be rendered
500 // in addition to the parent nodes (rather than child nodes replacing parent entirely)
501
502 if ( node->refinementProcess() == Qgis::TileRefinementProcess::Additive )
503 {
504 // Logic of the additive strategy:
505 // - children that are not loaded will get requested to be loaded
506 // - children that are already loaded get recursively visited
507 becomesActive = true;
508
509 QgsChunkNode *const *children = node->children();
510 for ( int i = 0; i < node->childCount(); ++i )
511 {
512 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
513 if ( children[i]->entity() || !children[i]->hasData() )
514 {
515 // chunk is resident - let's visit it recursively
516 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
517 }
518 else
519 {
520 // chunk is not yet resident - let's try to load it
521 if ( Qgs3DUtils::isCullable( childBbox, sceneContext.viewProjectionMatrix ) )
522 continue;
523
524 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
525 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
526 }
527 }
528 }
529 else
530 {
531 // Logic of the replace strategy:
532 // - if we have all children loaded, we use them instead of the parent node
533 // - if we do not have all children loaded, we request to load them and keep using the parent for the time being
534 if ( node->allChildChunksResident( mCurrentTime ) )
535 {
536 QgsChunkNode *const *children = node->children();
537 for ( int i = 0; i < node->childCount(); ++i )
538 {
539 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
540 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
541 }
542 }
543 else
544 {
545 becomesActive = true;
546
547 QgsChunkNode *const *children = node->children();
548 for ( int i = 0; i < node->childCount(); ++i )
549 {
550 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
551 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
552 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
553 }
554 }
555 }
556 }
557
558 if ( becomesActive && node->entity() )
559 {
560 mActiveNodes << node;
561 // if we are not using additive strategy we need to make sure the parent primitives are not counted
562 if ( node->refinementProcess() != Qgis::TileRefinementProcess::Additive && node->parent() && nodes.contains( node->parent() ) )
563 {
564 nodes.remove( node->parent() );
565 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
566 }
567 renderedCount += mChunkLoaderFactory->primitivesCount( node );
568 nodes.insert( node );
569 }
570 }
571
572 // sort nodes by their level and their distance from the camera
573 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
574 for ( const auto &request : residencyRequests )
575 requestResidency( request.node );
576}
577
578void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
579{
580 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
581 {
582 Q_ASSERT( node->replacementQueueEntry() );
583 Q_ASSERT( node->entity() );
584 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
585 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
586 }
587 else if ( node->state() == QgsChunkNode::QueuedForLoad )
588 {
589 // move to the front of loading queue
590 Q_ASSERT( node->loaderQueueEntry() );
591 Q_ASSERT( !node->loader() );
592 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
593 {
594 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
595 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
596 }
597 }
598 else if ( node->state() == QgsChunkNode::Loading )
599 {
600 // the entry is being currently processed - nothing to do really
601 }
602 else if ( node->state() == QgsChunkNode::Skeleton )
603 {
604 if ( !node->hasData() )
605 return; // no need to load (we already tried but got nothing back)
606
607 // add to the loading queue
608 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
609 node->setQueuedForLoad( entry );
610 mChunkLoaderQueue->insertFirst( entry );
611 }
612 else
613 Q_ASSERT( false && "impossible!" );
614}
615
616
617void QgsChunkedEntity::onActiveJobFinished()
618{
619 int oldJobsCount = pendingJobsCount();
620
621 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
622 Q_ASSERT( job );
623 Q_ASSERT( mActiveJobs.contains( job ) );
624
625 QgsChunkNode *node = job->chunk();
626
627 if ( node->state() == QgsChunkNode::Loading )
628 {
629 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
630 Q_ASSERT( loader );
631 Q_ASSERT( node->loader() == loader );
632
633 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
634 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
635
636 QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
637 // mark as loaded + create entity
638 Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
639
640 if ( entity )
641 {
642 // The returned QEntity is initially enabled, so let's add it to active nodes too.
643 // Soon afterwards updateScene() will be called, which would remove it from the scene
644 // if the node should not be shown anymore. Ideally entities should be initially disabled,
645 // but there seems to be a bug in Qt3D - if entity is disabled initially, showing it
646 // by setting setEnabled(true) is not reliable (entity eventually gets shown, but only after
647 // some more changes in the scene) - see https://github.com/qgis/QGIS/issues/48334
648 mActiveNodes << node;
649
650 // load into node (should be in main thread again)
651 node->setLoaded( entity );
652
653 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
654
655 emit newEntityCreated( entity );
656 }
657 else
658 {
659 node->setHasData( false );
660 node->cancelLoading();
661 }
662
663 // now we need an update!
664 mNeedsUpdate = true;
665 }
666 else
667 {
668 Q_ASSERT( node->state() == QgsChunkNode::Updating );
669
670 // This is a special case when we're replacing the node's entity
671 // with QgsChunkUpdaterFactory passed to updatedNodes(). The returned
672 // updater is actually a chunk loader that will give us a completely
673 // new QEntity, so we just delete the old one and use the new one
674 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
675 {
676 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity( this );
677 node->replaceEntity( newEntity );
678 emit newEntityCreated( newEntity );
679 }
680
681 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
682 node->setUpdated();
683 }
684
685 // cleanup the job that has just finished
686 mActiveJobs.removeOne( job );
687 job->deleteLater();
688
689 // start another job - if any
690 startJobs();
691
692 if ( pendingJobsCount() != oldJobsCount )
693 emit pendingJobsCountChanged();
694}
695
696void QgsChunkedEntity::startJobs()
697{
698 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
699 {
700 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
701 Q_ASSERT( entry );
702 QgsChunkNode *node = entry->chunk;
703 delete entry;
704
705 QgsChunkQueueJob *job = startJob( node );
706 mActiveJobs.append( job );
707 }
708}
709
710QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
711{
712 if ( node->state() == QgsChunkNode::QueuedForLoad )
713 {
714 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
715 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
716
717 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
718 connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
719 node->setLoading( loader );
720 return loader;
721 }
722 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
723 {
724 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
725
726 node->setUpdating();
727 connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
728 return node->updater();
729 }
730 else
731 {
732 Q_ASSERT( false ); // not possible
733 return nullptr;
734 }
735}
736
737void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
738{
739 Q_ASSERT( job );
740
741 QgsChunkNode *node = job->chunk();
742 disconnect( job, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
743
744 if ( node->state() == QgsChunkNode::Loading )
745 {
746 // return node back to skeleton
747 node->cancelLoading();
748
749 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
750 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
751 }
752 else if ( node->state() == QgsChunkNode::Updating )
753 {
754 // return node back to loaded state
755 node->cancelUpdating();
756
757 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
758 }
759 else
760 {
761 Q_ASSERT( false );
762 }
763
764 job->cancel();
765 mActiveJobs.removeOne( job );
766 job->deleteLater();
767}
768
769void QgsChunkedEntity::cancelActiveJobs()
770{
771 while ( !mActiveJobs.isEmpty() )
772 {
773 cancelActiveJob( mActiveJobs.takeFirst() );
774 }
775}
776
777
778QVector<QgsRayCastingUtils::RayHit> QgsChunkedEntity::rayIntersection( const QgsRayCastingUtils::Ray3D &ray, const QgsRayCastingUtils::RayCastContext &context ) const
779{
780 Q_UNUSED( ray )
781 Q_UNUSED( context )
782 return QVector<QgsRayCastingUtils::RayHit>();
783}
784
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
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.
static double calculateEntityGpuMemorySize(Qt3DCore::QEntity *entity)
Calculates approximate usage of GPU memory by an entity.
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
static float screenSpaceError(float epsilon, float distance, int screenSize, float fov)
This routine approximately calculates how an error (epsilon) of an object in world coordinates at giv...
static void computeBoundingBoxNearFarPlanes(const QgsAABB &bbox, const QMatrix4x4 &viewMatrix, float &fnear, float &ffar)
This routine computes nearPlane farPlane from the closest and farthest corners point of bounding box ...
QVector3D center() const
Returns coordinates of the center of the box.
Definition qgsaabb.h:68
float distanceFromPoint(float x, float y, float z) const
Returns shortest distance from the box to a point.
Definition qgsaabb.cpp:46
A template based class for storing ranges (lower to upper values).
Definition qgsrange.h:46
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40
Helper struct to store ray casting parameters.