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