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