QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgschunkedentity_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgschunkedentity_p.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_p.h"
17
18#include <QElapsedTimer>
19#include <QVector4D>
20#include <Qt3DRender/QObjectPicker>
21#include <Qt3DRender/QPickTriangleEvent>
22#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
23#include <Qt3DRender/QBuffer>
24typedef Qt3DRender::QBuffer Qt3DQBuffer;
25#else
26#include <Qt3DCore/QBuffer>
27typedef Qt3DCore::QBuffer Qt3DQBuffer;
28#endif
29
30#include "qgs3dutils.h"
32#include "qgschunklist_p.h"
33#include "qgschunkloader_p.h"
34#include "qgschunknode_p.h"
36
37#include "qgseventtracing.h"
38
39#include <queue>
40
42
43static float screenSpaceError( float epsilon, float distance, float screenSize, float fov )
44{
45 /* This routine approximately calculates how an error (epsilon) of an object in world coordinates
46 * at given distance (between camera and the object) will look like in screen coordinates.
47 *
48 * the math below simply uses triangle similarity:
49 *
50 * epsilon phi
51 * ----------------------------- = ----------------
52 * [ frustum width at distance ] [ screen width ]
53 *
54 * Then we solve for phi, substituting [frustum width at distance] = 2 * distance * tan(fov / 2)
55 *
56 * ________xxx__ xxx = real world error (epsilon)
57 * \ | / x = screen space error (phi)
58 * \ | /
59 * \___|_x_/ near plane (screen space)
60 * \ | /
61 * \ | /
62 * \|/ angle = field of view
63 * camera
64 */
65 float phi = epsilon * screenSize / ( 2 * distance * tan( fov * M_PI / ( 2 * 180 ) ) );
66 return phi;
67}
68
69static float screenSpaceError( QgsChunkNode *node, const QgsChunkedEntity::SceneState &state )
70{
71 if ( node->error() <= 0 ) //it happens for meshes
72 return 0;
73
74 float dist = node->bbox().distanceFromPoint( state.cameraPos );
75
76 // TODO: what to do when distance == 0 ?
77
78 float sse = screenSpaceError( node->error(), dist, state.screenSizePx, state.cameraFov );
79 return sse;
80}
81
82QgsChunkedEntity::QgsChunkedEntity( float tau, QgsChunkLoaderFactory *loaderFactory, bool ownsFactory, int primitiveBudget, Qt3DCore::QNode *parent )
83 : Qt3DCore::QEntity( parent )
84 , mTau( tau )
85 , mChunkLoaderFactory( loaderFactory )
86 , mOwnsFactory( ownsFactory )
87 , mPrimitivesBudget( primitiveBudget )
88{
89 mRootNode = loaderFactory->createRootNode();
90 mChunkLoaderQueue = new QgsChunkList;
91 mReplacementQueue = new QgsChunkList;
92}
93
94
95QgsChunkedEntity::~QgsChunkedEntity()
96{
97 // derived classes have to make sure that any pending active job has finished / been canceled
98 // before getting to this destructor - here it would be too late to cancel them
99 // (e.g. objects required for loading/updating have been deleted already)
100 Q_ASSERT( mActiveJobs.isEmpty() );
101
102 // clean up any pending load requests
103 while ( !mChunkLoaderQueue->isEmpty() )
104 {
105 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
106 QgsChunkNode *node = entry->chunk;
107
108 if ( node->state() == QgsChunkNode::QueuedForLoad )
109 node->cancelQueuedForLoad();
110 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
111 node->cancelQueuedForUpdate();
112 else
113 Q_ASSERT( false ); // impossible!
114 }
115
116 delete mChunkLoaderQueue;
117
118 while ( !mReplacementQueue->isEmpty() )
119 {
120 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
121
122 // remove loaded data from node
123 entry->chunk->unloadChunk(); // also deletes the entry
124 }
125
126 delete mReplacementQueue;
127 delete mRootNode;
128
129 if ( mOwnsFactory )
130 {
131 delete mChunkLoaderFactory;
132 }
133}
134
135
136void QgsChunkedEntity::update( const SceneState &state )
137{
138 if ( !mIsValid )
139 return;
140
141 // Let's start the update by removing from loader queue chunks that
142 // would get frustum culled if loaded (outside of the current view
143 // of the camera). Removing them keeps the loading queue shorter,
144 // and we avoid loading chunks that we only wanted for a short period
145 // of time when camera was moving.
146 pruneLoaderQueue( state );
147
148 QElapsedTimer t;
149 t.start();
150
151 int oldJobsCount = pendingJobsCount();
152
153 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
154 mActiveNodes.clear();
155 mFrustumCulled = 0;
156 mCurrentTime = QTime::currentTime();
157
158 update( mRootNode, state );
159
160 int enabled = 0, disabled = 0, unloaded = 0;
161
162 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
163 {
164 if ( activeBefore.contains( node ) )
165 {
166 activeBefore.remove( node );
167 }
168 else
169 {
170 if ( !node->entity() )
171 {
172 QgsDebugMsg( "Active node has null entity - this should never happen!" );
173 continue;
174 }
175 node->entity()->setEnabled( true );
176 ++enabled;
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 QgsDebugMsg( "Active node has null entity - this should never happen!" );
186 continue;
187 }
188 node->entity()->setEnabled( false );
189 ++disabled;
190 }
191
192 double usedGpuMemory = QgsChunkedEntity::calculateEntityGpuMemorySize( this );
193
194 // unload those that are over the limit for replacement
195 // TODO: what to do when our cache is too small and nodes are being constantly evicted + loaded again
196 while ( usedGpuMemory > mGpuMemoryLimit )
197 {
198 QgsChunkListEntry *entry = mReplacementQueue->takeLast();
199 usedGpuMemory -= QgsChunkedEntity::calculateEntityGpuMemorySize( entry->chunk->entity() );
200 entry->chunk->unloadChunk(); // also deletes the entry
201 mActiveNodes.removeOne( entry->chunk );
202 ++unloaded;
203 }
204
205 if ( mBboxesEntity )
206 {
207 QList<QgsAABB> bboxes;
208 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
209 bboxes << n->bbox();
210 mBboxesEntity->setBoxes( bboxes );
211 }
212
213 // start a job from queue if there is anything waiting
214 startJobs();
215
216 mNeedsUpdate = false; // just updated
217
218 if ( pendingJobsCount() != oldJobsCount )
219 emit pendingJobsCountChanged();
220
221 QgsDebugMsgLevel( QStringLiteral( "update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms" ).arg( mActiveNodes.count() )
222 .arg( enabled )
223 .arg( disabled )
224 .arg( mFrustumCulled )
225 .arg( mReplacementQueue->count() )
226 .arg( unloaded )
227 .arg( t.elapsed() ), 2 );
228}
229
230void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
231{
232 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
233 return;
234
235 if ( enabled )
236 {
237 mBboxesEntity = new QgsChunkBoundsEntity( this );
238 }
239 else
240 {
241 mBboxesEntity->deleteLater();
242 mBboxesEntity = nullptr;
243 }
244}
245
246void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
247{
248 for ( QgsChunkNode *node : nodes )
249 {
250 if ( node->state() == QgsChunkNode::QueuedForUpdate )
251 {
252 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
253 node->cancelQueuedForUpdate();
254 }
255 else if ( node->state() == QgsChunkNode::Updating )
256 {
257 cancelActiveJob( node->updater() );
258 }
259
260 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
261
262 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
263 node->setQueuedForUpdate( entry, updateJobFactory );
264 mChunkLoaderQueue->insertLast( entry );
265 }
266
267 // trigger update
268 startJobs();
269}
270
271void QgsChunkedEntity::pruneLoaderQueue( const SceneState &state )
272{
273 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
274
275 // Step 1: collect all entries from chunk loader queue that would get frustum culled
276 // (i.e. they are outside of the current view of the camera) and therefore loading
277 // such chunks would be probably waste of time.
278 QgsChunkListEntry *e = mChunkLoaderQueue->first();
279 while ( e )
280 {
281 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
282 if ( Qgs3DUtils::isCullable( e->chunk->bbox(), state.viewProjectionMatrix ) )
283 {
284 toRemoveFromLoaderQueue.append( e->chunk );
285 }
286 e = e->next;
287 }
288
289 // Step 2: remove collected chunks from the loading queue
290 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
291 {
292 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
293 if ( n->state() == QgsChunkNode::QueuedForLoad )
294 {
295 n->cancelQueuedForLoad();
296 }
297 else // queued for update
298 {
299 n->cancelQueuedForUpdate();
300 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
301 n->unloadChunk();
302 }
303 }
304
305 if ( !toRemoveFromLoaderQueue.isEmpty() )
306 {
307 QgsDebugMsgLevel( QStringLiteral( "Pruned %1 chunks in loading queue" ).arg( toRemoveFromLoaderQueue.count() ), 2 );
308 }
309}
310
311
312int QgsChunkedEntity::pendingJobsCount() const
313{
314 return mChunkLoaderQueue->count() + mActiveJobs.count();
315}
316
317struct ResidencyRequest
318{
319 QgsChunkNode *node = nullptr;
320 float dist = 0.0;
321 int level = -1;
322 ResidencyRequest() = default;
323 ResidencyRequest(
324 QgsChunkNode *n,
325 float d,
326 int l )
327 : node( n )
328 , dist( d )
329 , level( l )
330 {}
331};
332
333struct
334{
335 bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
336 {
337 if ( request.level == otherRequest.level )
338 return request.dist > otherRequest.dist;
339 return request.level > otherRequest.level;
340 }
341} ResidencyRequestSorter;
342
343void QgsChunkedEntity::update( QgsChunkNode *root, const SceneState &state )
344{
345 QSet<QgsChunkNode *> nodes;
346 QVector<ResidencyRequest> residencyRequests;
347
348 using slotItem = std::pair<QgsChunkNode *, float>;
349 auto cmp_funct = []( const slotItem & p1, const slotItem & p2 )
350 {
351 return p1.second <= p2.second;
352 };
353 int renderedCount = 0;
354 std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
355 pq.push( std::make_pair( root, screenSpaceError( root, state ) ) );
356 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
357 {
358 slotItem s = pq.top();
359 pq.pop();
360 QgsChunkNode *node = s.first;
361
362 if ( Qgs3DUtils::isCullable( node->bbox(), state.viewProjectionMatrix ) )
363 {
364 ++mFrustumCulled;
365 continue;
366 }
367
368 // ensure we have child nodes (at least skeletons) available, if any
369 if ( node->childCount() == -1 )
370 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
371
372 // make sure all nodes leading to children are always loaded
373 // so that zooming out does not create issues
374 double dist = node->bbox().center().distanceToPoint( state.cameraPos );
375 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
376
377 if ( !node->entity() )
378 {
379 // this happens initially when root node is not ready yet
380 continue;
381 }
382 bool becomesActive = false;
383
384 // QgsDebugMsgLevel( QStringLiteral( "%1|%2|%3 %4 %5" ).arg( node->tileId().x ).arg( node->tileId().y ).arg( node->tileId().z ).arg( mTau ).arg( screenSpaceError( node, state ) ), 2 );
385 if ( node->childCount() == 0 )
386 {
387 // there's no children available for this node, so regardless of whether it has an acceptable error
388 // or not, it's the best we'll ever get...
389 becomesActive = true;
390 }
391 else if ( mTau > 0 && screenSpaceError( node, state ) <= mTau )
392 {
393 // acceptable error for the current chunk - let's render it
394 becomesActive = true;
395 }
396 else
397 {
398 // This chunk does not have acceptable error (it does not provide enough detail)
399 // so we'll try to use its children. The exact logic depends on whether the entity
400 // has additive strategy. With additive strategy, child nodes should be rendered
401 // in addition to the parent nodes (rather than child nodes replacing parent entirely)
402
403 if ( mAdditiveStrategy )
404 {
405 // Logic of the additive strategy:
406 // - children that are not loaded will get requested to be loaded
407 // - children that are already loaded get recursively visited
408 becomesActive = true;
409
410 QgsChunkNode *const *children = node->children();
411 for ( int i = 0; i < node->childCount(); ++i )
412 {
413 if ( children[i]->entity() || !children[i]->hasData() )
414 {
415 // chunk is resident - let's visit it recursively
416 pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
417 }
418 else
419 {
420 // chunk is not yet resident - let's try to load it
421 if ( Qgs3DUtils::isCullable( children[i]->bbox(), state.viewProjectionMatrix ) )
422 continue;
423
424 double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
425 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
426 }
427 }
428 }
429 else
430 {
431 // Logic of the replace strategy:
432 // - if we have all children loaded, we use them instead of the parent node
433 // - if we do not have all children loaded, we request to load them and keep using the parent for the time being
434 if ( node->allChildChunksResident( mCurrentTime ) )
435 {
436 QgsChunkNode *const *children = node->children();
437 for ( int i = 0; i < node->childCount(); ++i )
438 pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
439 }
440 else
441 {
442 becomesActive = true;
443
444 QgsChunkNode *const *children = node->children();
445 for ( int i = 0; i < node->childCount(); ++i )
446 {
447 double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
448 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
449 }
450 }
451 }
452 }
453
454 if ( becomesActive )
455 {
456 mActiveNodes << node;
457 // if we are not using additive strategy we need to make sure the parent primitives are not counted
458 if ( !mAdditiveStrategy && node->parent() && nodes.contains( node->parent() ) )
459 {
460 nodes.remove( node->parent() );
461 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
462 }
463 renderedCount += mChunkLoaderFactory->primitivesCount( node );
464 nodes.insert( node );
465 }
466 }
467
468 // sort nodes by their level and their distance from the camera
469 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
470 for ( const auto &request : residencyRequests )
471 requestResidency( request.node );
472}
473
474void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
475{
476 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
477 {
478 Q_ASSERT( node->replacementQueueEntry() );
479 Q_ASSERT( node->entity() );
480 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
481 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
482 }
483 else if ( node->state() == QgsChunkNode::QueuedForLoad )
484 {
485 // move to the front of loading queue
486 Q_ASSERT( node->loaderQueueEntry() );
487 Q_ASSERT( !node->loader() );
488 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
489 {
490 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
491 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
492 }
493 }
494 else if ( node->state() == QgsChunkNode::Loading )
495 {
496 // the entry is being currently processed - nothing to do really
497 }
498 else if ( node->state() == QgsChunkNode::Skeleton )
499 {
500 if ( !node->hasData() )
501 return; // no need to load (we already tried but got nothing back)
502
503 // add to the loading queue
504 QgsChunkListEntry *entry = new QgsChunkListEntry( node );
505 node->setQueuedForLoad( entry );
506 mChunkLoaderQueue->insertFirst( entry );
507 }
508 else
509 Q_ASSERT( false && "impossible!" );
510}
511
512
513void QgsChunkedEntity::onActiveJobFinished()
514{
515 int oldJobsCount = pendingJobsCount();
516
517 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
518 Q_ASSERT( job );
519 Q_ASSERT( mActiveJobs.contains( job ) );
520
521 QgsChunkNode *node = job->chunk();
522
523 if ( QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job ) )
524 {
525 Q_ASSERT( node->state() == QgsChunkNode::Loading );
526 Q_ASSERT( node->loader() == loader );
527
528 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
529 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
530
531 QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
532 // mark as loaded + create entity
533 Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
534
535 if ( entity )
536 {
537 // The returned QEntity is initially enabled, so let's add it to active nodes too.
538 // Soon afterwards updateScene() will be called, which would remove it from the scene
539 // if the node should not be shown anymore. Ideally entities should be initially disabled,
540 // but there seems to be a bug in Qt3D - if entity is disabled initially, showing it
541 // by setting setEnabled(true) is not reliable (entity eventually gets shown, but only after
542 // some more changes in the scene) - see https://github.com/qgis/QGIS/issues/48334
543 mActiveNodes << node;
544
545 // load into node (should be in main thread again)
546 node->setLoaded( entity );
547
548 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
549
550 if ( mPickingEnabled )
551 {
552 Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
553 node->entity()->addComponent( picker );
554 connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
555 }
556
557 emit newEntityCreated( entity );
558 }
559 else
560 {
561 node->setHasData( false );
562 node->cancelLoading();
563 }
564
565 // now we need an update!
566 mNeedsUpdate = true;
567 }
568 else
569 {
570 Q_ASSERT( node->state() == QgsChunkNode::Updating );
571 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
572 node->setUpdated();
573 }
574
575 // cleanup the job that has just finished
576 mActiveJobs.removeOne( job );
577 job->deleteLater();
578
579 // start another job - if any
580 startJobs();
581
582 if ( pendingJobsCount() != oldJobsCount )
583 emit pendingJobsCountChanged();
584}
585
586void QgsChunkedEntity::startJobs()
587{
588 while ( mActiveJobs.count() < 4 )
589 {
590 if ( mChunkLoaderQueue->isEmpty() )
591 return;
592
593 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
594 Q_ASSERT( entry );
595 QgsChunkNode *node = entry->chunk;
596 delete entry;
597
598 QgsChunkQueueJob *job = startJob( node );
599 mActiveJobs.append( job );
600 }
601}
602
603QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
604{
605 if ( node->state() == QgsChunkNode::QueuedForLoad )
606 {
607 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
608 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
609
610 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
611 connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
612 node->setLoading( loader );
613 return loader;
614 }
615 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
616 {
617 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
618
619 node->setUpdating();
620 connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
621 return node->updater();
622 }
623 else
624 {
625 Q_ASSERT( false ); // not possible
626 return nullptr;
627 }
628}
629
630void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
631{
632 Q_ASSERT( job );
633
634 QgsChunkNode *node = job->chunk();
635
636 if ( qobject_cast<QgsChunkLoader *>( job ) )
637 {
638 // return node back to skeleton
639 node->cancelLoading();
640
641 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
642 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
643 }
644 else
645 {
646 // return node back to loaded state
647 node->cancelUpdating();
648
649 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
650 }
651
652 job->cancel();
653 mActiveJobs.removeOne( job );
654 job->deleteLater();
655}
656
657void QgsChunkedEntity::cancelActiveJobs()
658{
659 while ( !mActiveJobs.isEmpty() )
660 {
661 cancelActiveJob( mActiveJobs.takeFirst() );
662 }
663}
664
665
666void QgsChunkedEntity::setPickingEnabled( bool enabled )
667{
668 if ( mPickingEnabled == enabled )
669 return;
670
671 mPickingEnabled = enabled;
672
673 if ( enabled )
674 {
675 QgsChunkListEntry *entry = mReplacementQueue->first();
676 while ( entry )
677 {
678 QgsChunkNode *node = entry->chunk;
679 Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
680 node->entity()->addComponent( picker );
681 connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
682
683 entry = entry->next;
684 }
685 }
686 else
687 {
688 for ( Qt3DRender::QObjectPicker *picker : findChildren<Qt3DRender::QObjectPicker *>() )
689 picker->deleteLater();
690 }
691}
692
693void QgsChunkedEntity::onPickEvent( Qt3DRender::QPickEvent *event )
694{
695 Qt3DRender::QPickTriangleEvent *triangleEvent = qobject_cast<Qt3DRender::QPickTriangleEvent *>( event );
696 if ( !triangleEvent )
697 return;
698
699 Qt3DRender::QObjectPicker *picker = qobject_cast<Qt3DRender::QObjectPicker *>( sender() );
700 if ( !picker )
701 return;
702
703 Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( picker->parent() );
704 if ( !entity )
705 return;
706
707 // go figure out feature ID from the triangle index
709 for ( Qt3DRender::QGeometryRenderer *geomRenderer : entity->findChildren<Qt3DRender::QGeometryRenderer *>() )
710 {
711 // unfortunately we can't access which sub-entity triggered the pick event
712 // so as a temporary workaround let's just ignore the entity with selection
713 // and hope the event was the main entity (QTBUG-58206)
714 if ( geomRenderer->objectName() != QLatin1String( "main" ) )
715 continue;
716
717 if ( QgsTessellatedPolygonGeometry *g = qobject_cast<QgsTessellatedPolygonGeometry *>( geomRenderer->geometry() ) )
718 {
719 fid = g->triangleIndexToFeatureId( triangleEvent->triangleIndex() );
720 if ( !FID_IS_NULL( fid ) )
721 break;
722 }
723 }
724
725 if ( !FID_IS_NULL( fid ) )
726 {
727 emit pickedObject( event, fid );
728 }
729}
730
731double QgsChunkedEntity::calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity )
732{
733 long long usedGpuMemory = 0;
734 for ( Qt3DQBuffer *buffer : entity->findChildren<Qt3DQBuffer *>() )
735 {
736 usedGpuMemory += buffer->data().size();
737 }
738 for ( Qt3DRender::QTexture2D *tex : entity->findChildren<Qt3DRender::QTexture2D *>() )
739 {
740 // TODO : lift the assumption that the texture is RGBA
741 usedGpuMemory += tex->width() * tex->height() * 4;
742 }
743 return usedGpuMemory / 1024.0 / 1024.0;
744}
745
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
Definition: qgs3dutils.cpp:525
Qt3DCore::QBuffer Qt3DQBuffer
Definition: qgs3daxis.cpp:30
Qt3DCore::QBuffer Qt3DQBuffer
#define FID_NULL
Definition: qgsfeatureid.h:29
#define FID_IS_NULL(fid)
Definition: qgsfeatureid.h:30
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38