QGIS API Documentation 4.3.0-Master (bf28115e945)
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
66static void addTileTraceEvent( QObject &self, QgsChunkNode &node, QgsEventTracing::EventType eventType, QString name )
67{
68 QgsEventTracing::addEvent( eventType, u"3D"_s, name + u" "_s + node.tileId().text(), u"%1 %2"_s.arg( self.objectName(), node.tileId().text() ) );
69}
70
71QgsChunkedEntity::QgsChunkedEntity( Qgs3DMapSettings *mapSettings, float tau, QgsChunkLoaderFactory *loaderFactory, bool ownsFactory, int primitiveBudget, Qt3DCore::QNode *parent )
72 : Qgs3DMapSceneEntity( mapSettings, parent )
73 , mTau( tau )
74 , mChunkLoaderFactory( loaderFactory )
75 , mOwnsFactory( ownsFactory )
76 , mPrimitivesBudget( primitiveBudget )
77{
78 mRootNode.reset( loaderFactory->createRootNode() );
79 mChunkLoaderQueue = std::make_unique<QgsChunkList>();
80 mReplacementQueue = std::make_unique<QgsChunkList>();
81
82 // in case the chunk loader factory supports fetching of hierarchy in background (to avoid GUI freezes)
83 connect( loaderFactory, &QgsChunkLoaderFactory::childrenPrepared, this, [this] {
84 setNeedsUpdate( true );
85 emit pendingJobsCountChanged();
86 } );
87}
88
89
90QgsChunkedEntity::~QgsChunkedEntity()
91{
92 // derived classes have to make sure that any pending active job has finished / been canceled
93 // before getting to this destructor - here it would be too late to cancel them
94 // (e.g. objects required for loading/updating have been deleted already)
95 Q_ASSERT( mActiveJobs.isEmpty() );
96
97 // clean up any pending load requests
98 while ( !mChunkLoaderQueue->isEmpty() )
99 {
100 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
101 QgsChunkNode *node = entry->chunk;
102
103 if ( node->state() == QgsChunkNode::QueuedForLoad )
104 node->cancelQueuedForLoad();
105 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
106 node->cancelQueuedForUpdate();
107 else
108 Q_ASSERT( false ); // impossible!
109 }
110
111 while ( !mReplacementQueue->isEmpty() )
112 {
113 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
114
115 // remove loaded data from node
116 entry->chunk->unloadChunk(); // also deletes the entry
117 }
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.get(), 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#ifdef QGISDEBUG
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#endif
233}
234
235
236int QgsChunkedEntity::unloadNodes()
237{
238 double usedGpuMemory = Qgs3DUtils::calculateEntityGpuMemorySize( this );
239 if ( usedGpuMemory <= mGpuMemoryLimit )
240 {
241 setHasReachedGpuMemoryLimit( false );
242 return 0;
243 }
244
245 QgsDebugMsgLevel( u"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
246
247 int unloaded = 0;
248
249 // unload nodes starting from the back of the queue with currently loaded
250 // nodes - i.e. those that have been least recently used
251 QgsChunkListEntry *entry = mReplacementQueue->last();
252 while ( entry && usedGpuMemory > mGpuMemoryLimit )
253 {
254 // not all nodes are safe to unload: we do not want to unload nodes
255 // that are currently active, or have their descendants active or their
256 // siblings or their descendants are active (because in the next scene
257 // update, these would be very likely loaded again, making the unload worthless)
258 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
259 {
260 QgsChunkListEntry *entryPrev = entry->prev;
261 mReplacementQueue->takeEntry( entry );
262 usedGpuMemory -= Qgs3DUtils::calculateEntityGpuMemorySize( entry->chunk->entity() );
263 mActiveNodes.removeOne( entry->chunk );
264 entry->chunk->unloadChunk(); // also deletes the entry
265 ++unloaded;
266 entry = entryPrev;
267 }
268 else
269 {
270 entry = entry->prev;
271 }
272 }
273
274 if ( usedGpuMemory > mGpuMemoryLimit )
275 {
276 setHasReachedGpuMemoryLimit( true );
277 QgsDebugMsgLevel( u"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
278 }
279
280 return unloaded;
281}
282
283
284QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange( const QMatrix4x4 &viewMatrix ) const
285{
286 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
287
288 // it could be that there are no active nodes - they could be all culled or because root node
289 // is not yet loaded - we still need at least something to understand bounds of our scene
290 // so lets use the root node
291 if ( activeEntityNodes.empty() )
292 activeEntityNodes << rootNode();
293
294 float fnear = 1e9;
295 float ffar = 0;
296
297 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
298 {
299 // project each corner of bbox to camera coordinates
300 // and determine closest and farthest point.
301 QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
302 float bboxfnear;
303 float bboxffar;
304 Qgs3DUtils::computeBoundingBoxNearFarPlanes( bbox, viewMatrix, bboxfnear, bboxffar );
305 fnear = std::min( fnear, bboxfnear );
306 ffar = std::max( ffar, bboxffar );
307 }
308 return QgsRange<float>( fnear, ffar );
309}
310
311void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
312{
313 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
314 return;
315
316 if ( enabled )
317 {
318 mBboxesEntity = new QgsChunkBoundsEntity( mRootNode->box3D().center(), this );
319 }
320 else
321 {
322 mBboxesEntity->deleteLater();
323 mBboxesEntity = nullptr;
324 }
325}
326
327void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
328{
329 for ( QgsChunkNode *node : nodes )
330 {
331 if ( node->state() == QgsChunkNode::QueuedForUpdate )
332 {
333 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
334 node->cancelQueuedForUpdate();
335 }
336 else if ( node->state() == QgsChunkNode::Updating )
337 {
338 cancelActiveJob( node->updater() );
339 }
340 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
341 {
342 // there is not much to update yet
343 continue;
344 }
345 else if ( node->state() == QgsChunkNode::Loading )
346 {
347 // let's cancel the current loading job and queue for loading again
348 cancelActiveJob( node->loader() );
349 requestResidency( node );
350 continue;
351 }
352
353 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
354
355 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
356 node->setQueuedForUpdate( entry, updateJobFactory );
357 mChunkLoaderQueue->insertLast( entry );
358 }
359
360 // trigger update
361 startJobs();
362}
363
364void QgsChunkedEntity::pruneLoaderQueue( const SceneContext &sceneContext )
365{
366 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
367
368 // Step 1: collect all entries from chunk loader queue that would get frustum culled
369 // (i.e. they are outside of the current view of the camera) and therefore loading
370 // such chunks would be probably waste of time.
371 QgsChunkListEntry *e = mChunkLoaderQueue->first();
372 while ( e )
373 {
374 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
375 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( e->chunk->box3D(), mMapSettings->origin() );
376 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
377 {
378 toRemoveFromLoaderQueue.append( e->chunk );
379 }
380 e = e->next;
381 }
382
383 // Step 2: remove collected chunks from the loading queue
384 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
385 {
386 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
387 if ( n->state() == QgsChunkNode::QueuedForLoad )
388 {
389 n->cancelQueuedForLoad();
390 }
391 else // queued for update
392 {
393 n->cancelQueuedForUpdate();
394 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
395 n->unloadChunk();
396 }
397 }
398
399 if ( !toRemoveFromLoaderQueue.isEmpty() )
400 {
401 QgsDebugMsgLevel( u"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
402 }
403}
404
405
406int QgsChunkedEntity::pendingJobsCount() const
407{
408 return mChunkLoaderQueue->count() + mActiveJobs.count();
409}
410
411struct ResidencyRequest
412{
413 QgsChunkNode *node = nullptr;
414 float dist = 0.0;
415 int level = -1;
416 ResidencyRequest() = default;
417 ResidencyRequest( QgsChunkNode *n, float d, int l )
418 : node( n )
419 , dist( d )
420 , level( l )
421 {}
422};
423
424struct
425{
426 bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
427 {
428 if ( request.level == otherRequest.level )
429 return request.dist > otherRequest.dist;
430 return request.level > otherRequest.level;
431 }
432} ResidencyRequestSorter;
433
434void QgsChunkedEntity::update( QgsChunkNode *root, const SceneContext &sceneContext )
435{
436 QSet<QgsChunkNode *> nodes;
437 QVector<ResidencyRequest> residencyRequests;
438
439 using slotItem = std::pair<QgsChunkNode *, float>;
440 auto cmp_funct = []( const slotItem &p1, const slotItem &p2 ) { return p1.second <= p2.second; };
441 int renderedCount = 0;
442 std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
443 const QgsAABB rootBbox = Qgs3DUtils::mapToWorldExtent( root->box3D(), mMapSettings->origin() );
444 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
445 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
446 {
447 slotItem s = pq.top();
448 pq.pop();
449 QgsChunkNode *node = s.first;
450
451 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
452 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
453 {
454 ++mFrustumCulled;
455 continue;
456 }
457
458 // ensure we have child nodes (at least skeletons) available, if any
459 if ( !node->hasChildrenPopulated() )
460 {
461 // Some chunked entities (e.g. tiled scene) may not know the full node hierarchy in advance
462 // and need to fetch it from a remote server. Having a blocking network request
463 // in createChildren() is not wanted because this code runs on the main thread and thus
464 // would cause GUI freezes. Here is a mechanism to first check whether there are any
465 // network requests needed (with canCreateChildren()), and if that's the case,
466 // prepareChildren() will start those requests in the background and immediately returns.
467 // The factory will emit a signal when hierarchy fetching is done to force another update
468 // of this entity to create children of this node.
469 if ( mChunkLoaderFactory->canCreateChildren( node ) )
470 {
471 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
472 }
473 else
474 {
475 mChunkLoaderFactory->prepareChildren( node );
476 }
477 }
478
479 // make sure all nodes leading to children are always loaded
480 // so that zooming out does not create issues
481 double dist = bbox.center().distanceToPoint( sceneContext.cameraPos );
482 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
483
484 if ( !node->entity() && node->hasData() )
485 {
486 // this happens initially when root node is not ready yet
487 continue;
488 }
489 bool becomesActive = false;
490
491 // 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 );
492 if ( node->childCount() == 0 )
493 {
494 // there's no children available for this node, so regardless of whether it has an acceptable error
495 // or not, it's the best we'll ever get...
496 becomesActive = true;
497 }
498 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
499 {
500 // acceptable error for the current chunk - let's render it
501 becomesActive = true;
502 }
503 else
504 {
505 // This chunk does not have acceptable error (it does not provide enough detail)
506 // so we'll try to use its children. The exact logic depends on whether the entity
507 // has additive strategy. With additive strategy, child nodes should be rendered
508 // in addition to the parent nodes (rather than child nodes replacing parent entirely)
509
510 if ( node->refinementProcess() == Qgis::TileRefinementProcess::Additive )
511 {
512 // Logic of the additive strategy:
513 // - children that are not loaded will get requested to be loaded
514 // - children that are already loaded get recursively visited
515 becomesActive = true;
516
517 QgsChunkNode *const *children = node->children();
518 for ( int i = 0; i < node->childCount(); ++i )
519 {
520 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
521 if ( children[i]->entity() || !children[i]->hasData() )
522 {
523 // chunk is resident - let's visit it recursively
524 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
525 }
526 else
527 {
528 // chunk is not yet resident - let's try to load it
529 if ( Qgs3DUtils::isCullable( childBbox, sceneContext.viewProjectionMatrix ) )
530 continue;
531
532 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
533 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
534 }
535 }
536 }
537 else
538 {
539 // Logic of the replace strategy:
540 // - if we have all children loaded, we use them instead of the parent node
541 // - if we do not have all children loaded, we request to load them and keep using the parent for the time being
542 if ( node->allChildChunksResident( mCurrentTime ) )
543 {
544 QgsChunkNode *const *children = node->children();
545 for ( int i = 0; i < node->childCount(); ++i )
546 {
547 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
548 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
549 }
550 }
551 else
552 {
553 becomesActive = true;
554
555 QgsChunkNode *const *children = node->children();
556 for ( int i = 0; i < node->childCount(); ++i )
557 {
558 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
559 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
560 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
561 }
562 }
563 }
564 }
565
566 if ( becomesActive && node->entity() )
567 {
568 mActiveNodes << node;
569 // if we are not using additive strategy we need to make sure the parent primitives are not counted
570 if ( node->refinementProcess() != Qgis::TileRefinementProcess::Additive && node->parent() && nodes.contains( node->parent() ) )
571 {
572 nodes.remove( node->parent() );
573 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
574 }
575 renderedCount += mChunkLoaderFactory->primitivesCount( node );
576 nodes.insert( node );
577 }
578 }
579
580 // sort nodes by their level and their distance from the camera
581 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
582 for ( const auto &request : residencyRequests )
583 requestResidency( request.node );
584}
585
586void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
587{
588 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
589 {
590 Q_ASSERT( node->replacementQueueEntry() );
591 Q_ASSERT( node->entity() );
592 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
593 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
594 }
595 else if ( node->state() == QgsChunkNode::QueuedForLoad )
596 {
597 // move to the front of loading queue
598 Q_ASSERT( node->loaderQueueEntry() );
599 Q_ASSERT( !node->loader() );
600 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
601 {
602 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
603 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
604 }
605 }
606 else if ( node->state() == QgsChunkNode::Loading )
607 {
608 // the entry is being currently processed - nothing to do really
609 }
610 else if ( node->state() == QgsChunkNode::Skeleton )
611 {
612 if ( !node->hasData() )
613 return; // no need to load (we already tried but got nothing back)
614
615 // add to the loading queue
616 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
617 node->setQueuedForLoad( entry );
618 mChunkLoaderQueue->insertFirst( entry );
619 }
620 else
621 Q_ASSERT( false && "impossible!" );
622}
623
624
625void QgsChunkedEntity::onActiveJobFinished()
626{
627 int oldJobsCount = pendingJobsCount();
628
629 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
630 Q_ASSERT( job );
631 Q_ASSERT( mActiveJobs.contains( job ) );
632
633 QgsChunkNode *node = job->chunk();
634
635 if ( node->state() == QgsChunkNode::Loading )
636 {
637 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
638 Q_ASSERT( loader );
639 Q_ASSERT( node->loader() == loader );
640
641 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncEnd, u"Load"_s );
642
643 QgsScopedEvent 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 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncEnd, u"Update"_s );
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 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncBegin, u"Load"_s );
723
724 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
725 connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
726 loader->start();
727 node->setLoading( loader );
728 return loader;
729 }
730 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
731 {
732 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncBegin, u"Update"_s );
733
734 node->setUpdating();
735 connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
736 node->updater()->start();
737 return node->updater();
738 }
739 else
740 {
741 Q_ASSERT( false ); // not possible
742 return nullptr;
743 }
744}
745
746void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
747{
748 Q_ASSERT( job );
749
750 QgsChunkNode *node = job->chunk();
751 disconnect( job, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
752
753 if ( node->state() == QgsChunkNode::Loading )
754 {
755 // return node back to skeleton
756 node->cancelLoading();
757
758 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncEnd, u"Load"_s );
759 }
760 else if ( node->state() == QgsChunkNode::Updating )
761 {
762 // return node back to loaded state
763 node->cancelUpdating();
764
765 addTileTraceEvent( *this, *node, QgsEventTracing::AsyncEnd, u"Update"_s );
766 }
767 else
768 {
769 Q_ASSERT( false );
770 }
771
772 job->cancel();
773 mActiveJobs.removeOne( job );
774 job->deleteLater();
775}
776
777void QgsChunkedEntity::cancelActiveJobs()
778{
779 while ( !mActiveJobs.isEmpty() )
780 {
781 cancelActiveJob( mActiveJobs.takeFirst() );
782 }
783}
784
785QList<QgsRayCastHit> QgsChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
786{
787 Q_UNUSED( ray )
788 Q_UNUSED( context )
789 return {};
790}
791
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
Definition qgis.h:6308
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:80
#define QgsDebugError(str)
Definition qgslogger.h:71