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