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