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