QGIS API Documentation 3.99.0-Master (26c88405ac0)
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 "qgs3dutils.h"
20#include "qgschunklist_p.h"
21#include "qgschunkloader.h"
22#include "qgschunknode.h"
23#include "qgseventtracing.h"
24#include "qgsgeotransform.h"
25
26#include <QElapsedTimer>
27#include <QVector4D>
28#include <queue>
29
30#include "moc_qgschunkedentity.cpp"
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 setNeedsUpdate( true );
77 emit pendingJobsCountChanged();
78 } );
79}
80
81
82QgsChunkedEntity::~QgsChunkedEntity()
83{
84 // derived classes have to make sure that any pending active job has finished / been canceled
85 // before getting to this destructor - here it would be too late to cancel them
86 // (e.g. objects required for loading/updating have been deleted already)
87 Q_ASSERT( mActiveJobs.isEmpty() );
88
89 // clean up any pending load requests
90 while ( !mChunkLoaderQueue->isEmpty() )
91 {
92 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
93 QgsChunkNode *node = entry->chunk;
94
95 if ( node->state() == QgsChunkNode::QueuedForLoad )
96 node->cancelQueuedForLoad();
97 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
98 node->cancelQueuedForUpdate();
99 else
100 Q_ASSERT( false ); // impossible!
101 }
102
103 delete mChunkLoaderQueue;
104
105 while ( !mReplacementQueue->isEmpty() )
106 {
107 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
108
109 // remove loaded data from node
110 entry->chunk->unloadChunk(); // also deletes the entry
111 }
112
113 delete mReplacementQueue;
114 delete mRootNode;
115
116 if ( mOwnsFactory )
117 {
118 delete mChunkLoaderFactory;
119 }
120}
121
122
123void QgsChunkedEntity::handleSceneUpdate( const SceneContext &sceneContext )
124{
125 if ( !mIsValid )
126 return;
127
128 // Let's start the update by removing from loader queue chunks that
129 // would get frustum culled if loaded (outside of the current view
130 // of the camera). Removing them keeps the loading queue shorter,
131 // and we avoid loading chunks that we only wanted for a short period
132 // of time when camera was moving.
133 pruneLoaderQueue( sceneContext );
134
135 QElapsedTimer t;
136 t.start();
137
138 int oldJobsCount = pendingJobsCount();
139
140 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
141 mActiveNodes.clear();
142 mFrustumCulled = 0;
143 mCurrentTime = QTime::currentTime();
144
145 update( mRootNode, sceneContext );
146
147#ifdef QGISDEBUG
148 int enabled = 0, disabled = 0, unloaded = 0;
149#endif
150
151 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
152 {
153 if ( activeBefore.contains( node ) )
154 {
155 activeBefore.remove( node );
156 }
157 else
158 {
159 if ( !node->entity() )
160 {
161 QgsDebugError( "Active node has null entity - this should never happen!" );
162 continue;
163 }
164 node->entity()->setEnabled( true );
165
166 // let's make sure that any entity we're about to show has the right scene origin set
167 const QList<QgsGeoTransform *> transforms = node->entity()->findChildren<QgsGeoTransform *>();
168 for ( QgsGeoTransform *transform : transforms )
169 {
170 transform->setOrigin( mMapSettings->origin() );
171 }
172
173#ifdef QGISDEBUG
174 ++enabled;
175#endif
176 }
177 }
178
179 // disable those that were active but will not be anymore
180 for ( QgsChunkNode *node : activeBefore )
181 {
182 if ( !node->entity() )
183 {
184 QgsDebugError( "Active node has null entity - this should never happen!" );
185 continue;
186 }
187 node->entity()->setEnabled( false );
188#ifdef QGISDEBUG
189 ++disabled;
190#endif
191 }
192
193 // if this entity's loaded nodes are using more GPU memory than allowed,
194 // let's try to unload those that are not needed right now
195#ifdef QGISDEBUG
196 unloaded = unloadNodes();
197#else
198 unloadNodes();
199#endif
200
201 if ( mBboxesEntity )
202 {
203 QList<QgsBox3D> bboxes;
204 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
205 bboxes << n->box3D();
206 mBboxesEntity->setBoxes( bboxes );
207 }
208
209 // start a job from queue if there is anything waiting
210 startJobs();
211
212 mNeedsUpdate = false; // just updated
213
214 if ( pendingJobsCount() != oldJobsCount )
215 emit pendingJobsCountChanged();
216
217 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 );
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( mRootNode->box3D().center(), 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 else if ( node->state() == QgsChunkNode::Skeleton || node->state() == QgsChunkNode::QueuedForLoad )
326 {
327 // there is not much to update yet
328 continue;
329 }
330 else if ( node->state() == QgsChunkNode::Loading )
331 {
332 // let's cancel the current loading job and queue for loading again
333 cancelActiveJob( node->loader() );
334 requestResidency( node );
335 continue;
336 }
337
338 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
339
340 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
341 node->setQueuedForUpdate( entry, updateJobFactory );
342 mChunkLoaderQueue->insertLast( entry );
343 }
344
345 // trigger update
346 startJobs();
347}
348
349void QgsChunkedEntity::pruneLoaderQueue( const SceneContext &sceneContext )
350{
351 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
352
353 // Step 1: collect all entries from chunk loader queue that would get frustum culled
354 // (i.e. they are outside of the current view of the camera) and therefore loading
355 // such chunks would be probably waste of time.
356 QgsChunkListEntry *e = mChunkLoaderQueue->first();
357 while ( e )
358 {
359 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
360 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( e->chunk->box3D(), mMapSettings->origin() );
361 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
362 {
363 toRemoveFromLoaderQueue.append( e->chunk );
364 }
365 e = e->next;
366 }
367
368 // Step 2: remove collected chunks from the loading queue
369 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
370 {
371 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
372 if ( n->state() == QgsChunkNode::QueuedForLoad )
373 {
374 n->cancelQueuedForLoad();
375 }
376 else // queued for update
377 {
378 n->cancelQueuedForUpdate();
379 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
380 n->unloadChunk();
381 }
382 }
383
384 if ( !toRemoveFromLoaderQueue.isEmpty() )
385 {
386 QgsDebugMsgLevel( QStringLiteral( "Pruned %1 chunks in loading queue" ).arg( toRemoveFromLoaderQueue.count() ), 2 );
387 }
388}
389
390
391int QgsChunkedEntity::pendingJobsCount() const
392{
393 return mChunkLoaderQueue->count() + mActiveJobs.count();
394}
395
396struct ResidencyRequest
397{
398 QgsChunkNode *node = nullptr;
399 float dist = 0.0;
400 int level = -1;
401 ResidencyRequest() = default;
402 ResidencyRequest(
403 QgsChunkNode *n,
404 float d,
405 int l
406 )
407 : node( n )
408 , dist( d )
409 , level( l )
410 {}
411};
412
413struct
414{
415 bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
416 {
417 if ( request.level == otherRequest.level )
418 return request.dist > otherRequest.dist;
419 return request.level > otherRequest.level;
420 }
421} ResidencyRequestSorter;
422
423void QgsChunkedEntity::update( QgsChunkNode *root, const SceneContext &sceneContext )
424{
425 QSet<QgsChunkNode *> nodes;
426 QVector<ResidencyRequest> residencyRequests;
427
428 using slotItem = std::pair<QgsChunkNode *, float>;
429 auto cmp_funct = []( const slotItem &p1, const slotItem &p2 ) {
430 return p1.second <= p2.second;
431 };
432 int renderedCount = 0;
433 std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
434 const QgsAABB rootBbox = Qgs3DUtils::mapToWorldExtent( root->box3D(), mMapSettings->origin() );
435 pq.push( std::make_pair( root, screenSpaceError( rootBbox, root->error(), sceneContext ) ) );
436 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
437 {
438 slotItem s = pq.top();
439 pq.pop();
440 QgsChunkNode *node = s.first;
441
442 const QgsAABB bbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
443 if ( Qgs3DUtils::isCullable( bbox, sceneContext.viewProjectionMatrix ) )
444 {
445 ++mFrustumCulled;
446 continue;
447 }
448
449 // ensure we have child nodes (at least skeletons) available, if any
450 if ( !node->hasChildrenPopulated() )
451 {
452 // Some chunked entities (e.g. tiled scene) may not know the full node hierarchy in advance
453 // and need to fetch it from a remote server. Having a blocking network request
454 // in createChildren() is not wanted because this code runs on the main thread and thus
455 // would cause GUI freezes. Here is a mechanism to first check whether there are any
456 // network requests needed (with canCreateChildren()), and if that's the case,
457 // prepareChildren() will start those requests in the background and immediately returns.
458 // The factory will emit a signal when hierarchy fetching is done to force another update
459 // of this entity to create children of this node.
460 if ( mChunkLoaderFactory->canCreateChildren( node ) )
461 {
462 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
463 }
464 else
465 {
466 mChunkLoaderFactory->prepareChildren( node );
467 }
468 }
469
470 // make sure all nodes leading to children are always loaded
471 // so that zooming out does not create issues
472 double dist = bbox.center().distanceToPoint( sceneContext.cameraPos );
473 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
474
475 if ( !node->entity() && node->hasData() )
476 {
477 // this happens initially when root node is not ready yet
478 continue;
479 }
480 bool becomesActive = false;
481
482 // 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 );
483 if ( node->childCount() == 0 )
484 {
485 // there's no children available for this node, so regardless of whether it has an acceptable error
486 // or not, it's the best we'll ever get...
487 becomesActive = true;
488 }
489 else if ( mTau > 0 && screenSpaceError( bbox, node->error(), sceneContext ) <= mTau && node->hasData() )
490 {
491 // acceptable error for the current chunk - let's render it
492 becomesActive = true;
493 }
494 else
495 {
496 // This chunk does not have acceptable error (it does not provide enough detail)
497 // so we'll try to use its children. The exact logic depends on whether the entity
498 // has additive strategy. With additive strategy, child nodes should be rendered
499 // in addition to the parent nodes (rather than child nodes replacing parent entirely)
500
501 if ( node->refinementProcess() == Qgis::TileRefinementProcess::Additive )
502 {
503 // Logic of the additive strategy:
504 // - children that are not loaded will get requested to be loaded
505 // - children that are already loaded get recursively visited
506 becomesActive = true;
507
508 QgsChunkNode *const *children = node->children();
509 for ( int i = 0; i < node->childCount(); ++i )
510 {
511 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
512 if ( children[i]->entity() || !children[i]->hasData() )
513 {
514 // chunk is resident - let's visit it recursively
515 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
516 }
517 else
518 {
519 // chunk is not yet resident - let's try to load it
520 if ( Qgs3DUtils::isCullable( childBbox, sceneContext.viewProjectionMatrix ) )
521 continue;
522
523 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
524 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
525 }
526 }
527 }
528 else
529 {
530 // Logic of the replace strategy:
531 // - if we have all children loaded, we use them instead of the parent node
532 // - if we do not have all children loaded, we request to load them and keep using the parent for the time being
533 if ( node->allChildChunksResident( mCurrentTime ) )
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 pq.push( std::make_pair( children[i], screenSpaceError( childBbox, children[i]->error(), sceneContext ) ) );
540 }
541 }
542 else
543 {
544 becomesActive = true;
545
546 QgsChunkNode *const *children = node->children();
547 for ( int i = 0; i < node->childCount(); ++i )
548 {
549 const QgsAABB childBbox = Qgs3DUtils::mapToWorldExtent( children[i]->box3D(), mMapSettings->origin() );
550 double dist = childBbox.center().distanceToPoint( sceneContext.cameraPos );
551 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
552 }
553 }
554 }
555 }
556
557 if ( becomesActive && node->entity() )
558 {
559 mActiveNodes << node;
560 // if we are not using additive strategy we need to make sure the parent primitives are not counted
561 if ( node->refinementProcess() != Qgis::TileRefinementProcess::Additive && node->parent() && nodes.contains( node->parent() ) )
562 {
563 nodes.remove( node->parent() );
564 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
565 }
566 renderedCount += mChunkLoaderFactory->primitivesCount( node );
567 nodes.insert( node );
568 }
569 }
570
571 // sort nodes by their level and their distance from the camera
572 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
573 for ( const auto &request : residencyRequests )
574 requestResidency( request.node );
575}
576
577void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
578{
579 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
580 {
581 Q_ASSERT( node->replacementQueueEntry() );
582 Q_ASSERT( node->entity() );
583 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
584 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
585 }
586 else if ( node->state() == QgsChunkNode::QueuedForLoad )
587 {
588 // move to the front of loading queue
589 Q_ASSERT( node->loaderQueueEntry() );
590 Q_ASSERT( !node->loader() );
591 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
592 {
593 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
594 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
595 }
596 }
597 else if ( node->state() == QgsChunkNode::Loading )
598 {
599 // the entry is being currently processed - nothing to do really
600 }
601 else if ( node->state() == QgsChunkNode::Skeleton )
602 {
603 if ( !node->hasData() )
604 return; // no need to load (we already tried but got nothing back)
605
606 // add to the loading queue
607 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
608 node->setQueuedForLoad( entry );
609 mChunkLoaderQueue->insertFirst( entry );
610 }
611 else
612 Q_ASSERT( false && "impossible!" );
613}
614
615
616void QgsChunkedEntity::onActiveJobFinished()
617{
618 int oldJobsCount = pendingJobsCount();
619
620 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
621 Q_ASSERT( job );
622 Q_ASSERT( mActiveJobs.contains( job ) );
623
624 QgsChunkNode *node = job->chunk();
625
626 if ( node->state() == QgsChunkNode::Loading )
627 {
628 QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job );
629 Q_ASSERT( loader );
630 Q_ASSERT( node->loader() == loader );
631
632 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
633 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
634
635 QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
636 // mark as loaded + create entity
637 Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
638
639 if ( entity )
640 {
641 // The returned QEntity is initially enabled, so let's add it to active nodes too.
642 // Soon afterwards updateScene() will be called, which would remove it from the scene
643 // if the node should not be shown anymore. Ideally entities should be initially disabled,
644 // but there seems to be a bug in Qt3D - if entity is disabled initially, showing it
645 // by setting setEnabled(true) is not reliable (entity eventually gets shown, but only after
646 // some more changes in the scene) - see https://github.com/qgis/QGIS/issues/48334
647 mActiveNodes << node;
648
649 // load into node (should be in main thread again)
650 node->setLoaded( entity );
651
652 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
653
654 emit newEntityCreated( entity );
655 }
656 else
657 {
658 node->setHasData( false );
659 node->cancelLoading();
660 }
661
662 // now we need an update!
663 mNeedsUpdate = true;
664 }
665 else
666 {
667 Q_ASSERT( node->state() == QgsChunkNode::Updating );
668
669 // This is a special case when we're replacing the node's entity
670 // with QgsChunkUpdaterFactory passed to updatedNodes(). The returned
671 // updater is actually a chunk loader that will give us a completely
672 // new QEntity, so we just delete the old one and use the new one
673 if ( QgsChunkLoader *nodeUpdater = qobject_cast<QgsChunkLoader *>( node->updater() ) )
674 {
675 Qt3DCore::QEntity *newEntity = nodeUpdater->createEntity( this );
676 node->replaceEntity( newEntity );
677 emit newEntityCreated( newEntity );
678 }
679
680 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
681 node->setUpdated();
682 }
683
684 // cleanup the job that has just finished
685 mActiveJobs.removeOne( job );
686 job->deleteLater();
687
688 // start another job - if any
689 startJobs();
690
691 if ( pendingJobsCount() != oldJobsCount )
692 emit pendingJobsCountChanged();
693}
694
695void QgsChunkedEntity::startJobs()
696{
697 while ( mActiveJobs.count() < 4 && !mChunkLoaderQueue->isEmpty() )
698 {
699 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
700 Q_ASSERT( entry );
701 QgsChunkNode *node = entry->chunk;
702 delete entry;
703
704 QgsChunkQueueJob *job = startJob( node );
705 if ( !job->isFinished() )
706 mActiveJobs.append( job );
707 }
708}
709
710QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
711{
712 if ( node->state() == QgsChunkNode::QueuedForLoad )
713 {
714 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
715 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
716
717 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
718 connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
719 loader->start();
720 node->setLoading( loader );
721 return loader;
722 }
723 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
724 {
725 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
726
727 node->setUpdating();
728 connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
729 node->updater()->start();
730 return node->updater();
731 }
732 else
733 {
734 Q_ASSERT( false ); // not possible
735 return nullptr;
736 }
737}
738
739void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
740{
741 Q_ASSERT( job );
742
743 QgsChunkNode *node = job->chunk();
744 disconnect( job, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
745
746 if ( node->state() == QgsChunkNode::Loading )
747 {
748 // return node back to skeleton
749 node->cancelLoading();
750
751 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
752 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
753 }
754 else if ( node->state() == QgsChunkNode::Updating )
755 {
756 // return node back to loaded state
757 node->cancelUpdating();
758
759 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
760 }
761 else
762 {
763 Q_ASSERT( false );
764 }
765
766 job->cancel();
767 mActiveJobs.removeOne( job );
768 job->deleteLater();
769}
770
771void QgsChunkedEntity::cancelActiveJobs()
772{
773 while ( !mActiveJobs.isEmpty() )
774 {
775 cancelActiveJob( mActiveJobs.takeFirst() );
776 }
777}
778
779QList<QgsRayCastHit> QgsChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
780{
781 Q_UNUSED( ray )
782 Q_UNUSED( context )
783 return {};
784}
785
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
Definition qgis.h:5658
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:35
QVector3D center() const
Returns coordinates of the center of the box.
Definition qgsaabb.h:68
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
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:61
#define QgsDebugError(str)
Definition qgslogger.h:57