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