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