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