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