QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
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
221 u"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms"_s.arg( mActiveNodes.count() )
222 .arg( enabled )
223 .arg( disabled )
224 .arg( mFrustumCulled )
225 .arg( mChunkLoaderQueue->count() )
226 .arg( mReplacementQueue->count() )
227 .arg( unloaded )
228 .arg( t.elapsed() ),
229 2
230 );
231}
232
233
234int QgsChunkedEntity::unloadNodes()
235{
236 double usedGpuMemory = Qgs3DUtils::calculateEntityGpuMemorySize( this );
237 if ( usedGpuMemory <= mGpuMemoryLimit )
238 {
239 setHasReachedGpuMemoryLimit( false );
240 return 0;
241 }
242
243 QgsDebugMsgLevel( u"Going to unload nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
244
245 int unloaded = 0;
246
247 // unload nodes starting from the back of the queue with currently loaded
248 // nodes - i.e. those that have been least recently used
249 QgsChunkListEntry *entry = mReplacementQueue->last();
250 while ( entry && usedGpuMemory > mGpuMemoryLimit )
251 {
252 // not all nodes are safe to unload: we do not want to unload nodes
253 // that are currently active, or have their descendants active or their
254 // siblings or their descendants are active (because in the next scene
255 // update, these would be very likely loaded again, making the unload worthless)
256 if ( entry->chunk->parent() && !hasAnyActiveChildren( entry->chunk->parent(), mActiveNodes ) )
257 {
258 QgsChunkListEntry *entryPrev = entry->prev;
259 mReplacementQueue->takeEntry( entry );
260 usedGpuMemory -= Qgs3DUtils::calculateEntityGpuMemorySize( entry->chunk->entity() );
261 mActiveNodes.removeOne( entry->chunk );
262 entry->chunk->unloadChunk(); // also deletes the entry
263 ++unloaded;
264 entry = entryPrev;
265 }
266 else
267 {
268 entry = entry->prev;
269 }
270 }
271
272 if ( usedGpuMemory > mGpuMemoryLimit )
273 {
274 setHasReachedGpuMemoryLimit( true );
275 QgsDebugMsgLevel( u"Unable to unload enough nodes to free GPU memory (used: %1 MB, limit: %2 MB)"_s.arg( usedGpuMemory ).arg( mGpuMemoryLimit ), 2 );
276 }
277
278 return unloaded;
279}
280
281
282QgsRange<float> QgsChunkedEntity::getNearFarPlaneRange( const QMatrix4x4 &viewMatrix ) const
283{
284 QList<QgsChunkNode *> activeEntityNodes = activeNodes();
285
286 // it could be that there are no active nodes - they could be all culled or because root node
287 // is not yet loaded - we still need at least something to understand bounds of our scene
288 // so lets use the root node
289 if ( activeEntityNodes.empty() )
290 activeEntityNodes << rootNode();
291
292 float fnear = 1e9;
293 float ffar = 0;
294
295 for ( QgsChunkNode *node : std::as_const( activeEntityNodes ) )
296 {
297 // project each corner of bbox to camera coordinates
298 // and determine closest and farthest point.
299 QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
300 float bboxfnear;
301 float bboxffar;
302 Qgs3DUtils::computeBoundingBoxNearFarPlanes( bbox, viewMatrix, bboxfnear, bboxffar );
303 fnear = std::min( fnear, bboxfnear );
304 ffar = std::max( ffar, bboxffar );
305 }
306 return QgsRange<float>( fnear, ffar );
307}
308
309void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
310{
311 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
312 return;
313
314 if ( enabled )
315 {
316 mBboxesEntity = new QgsChunkBoundsEntity( mRootNode->box3D().center(), this );
317 }
318 else
319 {
320 mBboxesEntity->deleteLater();
321 mBboxesEntity = nullptr;
322 }
323}
324
325void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
326{
327 for ( QgsChunkNode *node : nodes )
328 {
329 if ( node->state() == QgsChunkNode::QueuedForUpdate )
330 {
331 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
332 node->cancelQueuedForUpdate();
333 }
334 else if ( node->state() == QgsChunkNode::Updating )
335 {
336 cancelActiveJob( node->updater() );
337 }
338 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
339 {
340 // there is not much to update yet
341 continue;
342 }
343 else if ( node->state() == QgsChunkNode::Loading )
344 {
345 // let's cancel the current loading job and queue for loading again
346 cancelActiveJob( node->loader() );
347 requestResidency( node );
348 continue;
349 }
350
351 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
352
353 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
354 node->setQueuedForUpdate( entry, updateJobFactory );
355 mChunkLoaderQueue->insertLast( entry );
356 }
357
358 // trigger update
359 startJobs();
360}
361
362void QgsChunkedEntity::pruneLoaderQueue( const SceneContext &sceneContext )
363{
364 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
365
366 // Step 1: collect all entries from chunk loader queue that would get frustum culled
367 // (i.e. they are outside of the current view of the camera) and therefore loading
368 // such chunks would be probably waste of time.
369 QgsChunkListEntry *e = mChunkLoaderQueue->first();
370 while ( e )
371 {
372 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
373 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( e->chunk->box3D(), mMapSettings->origin() );
374 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
375 {
376 toRemoveFromLoaderQueue.append( e->chunk );
377 }
378 e = e->next;
379 }
380
381 // Step 2: remove collected chunks from the loading queue
382 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
383 {
384 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
385 if ( n->state() == QgsChunkNode::QueuedForLoad )
386 {
387 n->cancelQueuedForLoad();
388 }
389 else // queued for update
390 {
391 n->cancelQueuedForUpdate();
392 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
393 n->unloadChunk();
394 }
395 }
396
397 if ( !toRemoveFromLoaderQueue.isEmpty() )
398 {
399 QgsDebugMsgLevel( u"Pruned %1 chunks in loading queue"_s.arg( toRemoveFromLoaderQueue.count() ), 2 );
400 }
401}
402
403
404int QgsChunkedEntity::pendingJobsCount() const
405{
406 return mChunkLoaderQueue->count() + mActiveJobs.count();
407}
408
409struct ResidencyRequest
410{
411 QgsChunkNode *node = nullptr;
412 float dist = 0.0;
413 int level = -1;
414 ResidencyRequest() = default;
415 ResidencyRequest( QgsChunkNode *n, float d, int l )
416 : node( n )
417 , dist( d )
418 , level( l )
419 {}
420};
421
422struct
423{
424 bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
425 {
426 if ( request.level == otherRequest.level )
427 return request.dist > otherRequest.dist;
428 return request.level > otherRequest.level;
429 }
430} ResidencyRequestSorter;
431
432void QgsChunkedEntity::update( QgsChunkNode *root, const SceneContext &sceneContext )
433{
434 QSet<QgsChunkNode *> nodes;
435 QVector<ResidencyRequest> residencyRequests;
436
437 using slotItem = std::pair<QgsChunkNode *, float>;
438 auto cmp_funct = []( const slotItem &p1, const slotItem &p2 ) { return p1.second <= p2.second; };
439 int renderedCount = 0;
440 std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
441 const QgsAABB rootBbox = Qgs3DUtils::mapToWorldExtent( root->box3D(), mMapSettings->origin() );
442 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
443 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
444 {
445 slotItem s = pq.top();
446 pq.pop();
447 QgsChunkNode *node = s.first;
448
449 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
450 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
451 {
452 ++mFrustumCulled;
453 continue;
454 }
455
456 // ensure we have child nodes (at least skeletons) available, if any
457 if ( !node->hasChildrenPopulated() )
458 {
459 // Some chunked entities (e.g. tiled scene) may not know the full node hierarchy in advance
460 // and need to fetch it from a remote server. Having a blocking network request
461 // in createChildren() is not wanted because this code runs on the main thread and thus
462 // would cause GUI freezes. Here is a mechanism to first check whether there are any
463 // network requests needed (with canCreateChildren()), and if that's the case,
464 // prepareChildren() will start those requests in the background and immediately returns.
465 // The factory will emit a signal when hierarchy fetching is done to force another update
466 // of this entity to create children of this node.
467 if ( mChunkLoaderFactory->canCreateChildren( node ) )
468 {
469 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
470 }
471 else
472 {
473 mChunkLoaderFactory->prepareChildren( node );
474 }
475 }
476
477 // make sure all nodes leading to children are always loaded
478 // so that zooming out does not create issues
479 double dist = bbox.center().distanceToPoint( sceneContext.cameraPos );
480 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
481
482 if ( !node->entity() && node->hasData() )
483 {
484 // this happens initially when root node is not ready yet
485 continue;
486 }
487 bool becomesActive = false;
488
489 // 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 );
490 if ( node->childCount() == 0 )
491 {
492 // there's no children available for this node, so regardless of whether it has an acceptable error
493 // or not, it's the best we'll ever get...
494 becomesActive = true;
495 }
496 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
497 {
498 // acceptable error for the current chunk - let's render it
499 becomesActive = true;
500 }
501 else
502 {
503 // This chunk does not have acceptable error (it does not provide enough detail)
504 // so we'll try to use its children. The exact logic depends on whether the entity
505 // has additive strategy. With additive strategy, child nodes should be rendered
506 // in addition to the parent nodes (rather than child nodes replacing parent entirely)
507
508 if ( node->refinementProcess() == Qgis::TileRefinementProcess::Additive )
509 {
510 // Logic of the additive strategy:
511 // - children that are not loaded will get requested to be loaded
512 // - children that are already loaded get recursively visited
513 becomesActive = true;
514
515 QgsChunkNode *const *children = node->children();
516 for ( int i = 0; i < node->childCount(); ++i )
517 {
518 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
519 if ( children[i]->entity() || !children[i]->hasData() )
520 {
521 // chunk is resident - let's visit it recursively
522 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
523 }
524 else
525 {
526 // chunk is not yet resident - let's try to load it
527 if ( Qgs3DUtils::isCullable( childBbox, sceneContext.viewProjectionMatrix ) )
528 continue;
529
530 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
531 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
532 }
533 }
534 }
535 else
536 {
537 // Logic of the replace strategy:
538 // - if we have all children loaded, we use them instead of the parent node
539 // - if we do not have all children loaded, we request to load them and keep using the parent for the time being
540 if ( node->allChildChunksResident( mCurrentTime ) )
541 {
542 QgsChunkNode *const *children = node->children();
543 for ( int i = 0; i < node->childCount(); ++i )
544 {
545 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
546 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
547 }
548 }
549 else
550 {
551 becomesActive = true;
552
553 QgsChunkNode *const *children = node->children();
554 for ( int i = 0; i < node->childCount(); ++i )
555 {
556 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
557 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
558 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
559 }
560 }
561 }
562 }
563
564 if ( becomesActive && node->entity() )
565 {
566 mActiveNodes << node;
567 // if we are not using additive strategy we need to make sure the parent primitives are not counted
568 if ( node->refinementProcess() != Qgis::TileRefinementProcess::Additive && node->parent() && nodes.contains( node->parent() ) )
569 {
570 nodes.remove( node->parent() );
571 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
572 }
573 renderedCount += mChunkLoaderFactory->primitivesCount( node );
574 nodes.insert( node );
575 }
576 }
577
578 // sort nodes by their level and their distance from the camera
579 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
580 for ( const auto &request : residencyRequests )
581 requestResidency( request.node );
582}
583
584void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
585{
586 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
587 {
588 Q_ASSERT( node->replacementQueueEntry() );
589 Q_ASSERT( node->entity() );
590 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
591 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
592 }
593 else if ( node->state() == QgsChunkNode::QueuedForLoad )
594 {
595 // move to the front of loading queue
596 Q_ASSERT( node->loaderQueueEntry() );
597 Q_ASSERT( !node->loader() );
598 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
599 {
600 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
601 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
602 }
603 }
604 else if ( node->state() == QgsChunkNode::Loading )
605 {
606 // the entry is being currently processed - nothing to do really
607 }
608 else if ( node->state() == QgsChunkNode::Skeleton )
609 {
610 if ( !node->hasData() )
611 return; // no need to load (we already tried but got nothing back)
612
613 // add to the loading queue
614 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
615 node->setQueuedForLoad( entry );
616 mChunkLoaderQueue->insertFirst( entry );
617 }
618 else
619 Q_ASSERT( false && "impossible!" );
620}
621
622
623void QgsChunkedEntity::onActiveJobFinished()
624{
625 int oldJobsCount = pendingJobsCount();
626
627 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
628 Q_ASSERT( job );
629 Q_ASSERT( mActiveJobs.contains( job ) );
630
631 QgsChunkNode *node = job->chunk();
632
633 if ( node->state() == QgsChunkNode::Loading )
634 {
635 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
636 Q_ASSERT( loader );
637 Q_ASSERT( node->loader() == loader );
638
639 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Load "_s + node->tileId().text(), node->tileId().text() );
640 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Load"_s, node->tileId().text() );
641
642 QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
643 // mark as loaded + create entity
644 Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
645
646 if ( entity )
647 {
648 // The returned QEntity is initially enabled, so let's add it to active nodes too.
649 // Soon afterwards updateScene() will be called, which would remove it from the scene
650 // if the node should not be shown anymore. Ideally entities should be initially disabled,
651 // but there seems to be a bug in Qt3D - if entity is disabled initially, showing it
652 // by setting setEnabled(true) is not reliable (entity eventually gets shown, but only after
653 // some more changes in the scene) - see https://github.com/qgis/QGIS/issues/48334
654 mActiveNodes << node;
655
656 // load into node (should be in main thread again)
657 node->setLoaded( entity );
658
659 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
660
661 emit newEntityCreated( entity );
662 }
663 else
664 {
665 node->setHasData( false );
666 node->cancelLoading();
667 }
668
669 // now we need an update!
670 mNeedsUpdate = true;
671 }
672 else
673 {
674 Q_ASSERT( node->state() == QgsChunkNode::Updating );
675
676 // This is a special case when we're replacing the node's entity
677 // with QgsChunkUpdaterFactory passed to updatedNodes(). The returned
678 // updater is actually a chunk loader that will give us a completely
679 // new QEntity, so we just delete the old one and use the new one
680 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
681 {
682 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity( this );
683 node->replaceEntity( newEntity );
684 emit newEntityCreated( newEntity );
685 }
686
687 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Update"_s, node->tileId().text() );
688 node->setUpdated();
689 }
690
691 // cleanup the job that has just finished
692 mActiveJobs.removeOne( job );
693 job->deleteLater();
694
695 // start another job - if any
696 startJobs();
697
698 if ( pendingJobsCount() != oldJobsCount )
699 emit pendingJobsCountChanged();
700}
701
702void QgsChunkedEntity::startJobs()
703{
704 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
705 {
706 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
707 Q_ASSERT( entry );
708 QgsChunkNode *node = entry->chunk;
709 delete entry;
710
711 QgsChunkQueueJob *job = startJob( node );
712 if ( !job->isFinished() )
713 mActiveJobs.append( job );
714 }
715}
716
717QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
718{
719 if ( node->state() == QgsChunkNode::QueuedForLoad )
720 {
721 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u"3D"_s, u"Load"_s, node->tileId().text() );
722 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u"3D"_s, u"Load "_s + node->tileId().text(), node->tileId().text() );
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 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u"3D"_s, u"Update"_s, node->tileId().text() );
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 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Load "_s + node->tileId().text(), node->tileId().text() );
759 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Load"_s, node->tileId().text() );
760 }
761 else if ( node->state() == QgsChunkNode::Updating )
762 {
763 // return node back to loaded state
764 node->cancelUpdating();
765
766 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"Update"_s, node->tileId().text() );
767 }
768 else
769 {
770 Q_ASSERT( false );
771 }
772
773 job->cancel();
774 mActiveJobs.removeOne( job );
775 job->deleteLater();
776}
777
778void QgsChunkedEntity::cancelActiveJobs()
779{
780 while ( !mActiveJobs.isEmpty() )
781 {
782 cancelActiveJob( mActiveJobs.takeFirst() );
783 }
784}
785
786QList<QgsRayCastHit> QgsChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
787{
788 Q_UNUSED( ray )
789 Q_UNUSED( context )
790 return {};
791}
792
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
Definition qgis.h:6014
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:66
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