QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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  node->entity()->setEnabled( true );
157  ++enabled;
158  }
159  }
160 
161  // disable those that were active but will not be anymore
162  for ( QgsChunkNode *node : activeBefore )
163  {
164  node->entity()->setEnabled( false );
165  ++disabled;
166  }
167 
168  // unload those that are over the limit for replacement
169  // TODO: what to do when our cache is too small and nodes are being constantly evicted + loaded again
170  while ( mReplacementQueue->count() > mMaxLoadedChunks )
171  {
172  QgsChunkListEntry *entry = mReplacementQueue->takeLast();
173  entry->chunk->unloadChunk(); // also deletes the entry
174  ++unloaded;
175  }
176 
177  if ( mBboxesEntity )
178  {
179  QList<QgsAABB> bboxes;
180  Q_FOREACH ( QgsChunkNode *n, mActiveNodes )
181  bboxes << n->bbox();
182  mBboxesEntity->setBoxes( bboxes );
183  }
184 
185  // start a job from queue if there is anything waiting
186  startJobs();
187 
188  mNeedsUpdate = false; // just updated
189 
190  if ( pendingJobsCount() != oldJobsCount )
191  emit pendingJobsCountChanged();
192 
193  QgsDebugMsgLevel( QStringLiteral( "update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms" ).arg( mActiveNodes.count() )
194  .arg( enabled )
195  .arg( disabled )
196  .arg( mFrustumCulled )
197  .arg( mReplacementQueue->count() )
198  .arg( unloaded )
199  .arg( t.elapsed() ), 2 );
200 }
201 
202 void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
203 {
204  if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
205  return;
206 
207  if ( enabled )
208  {
209  mBboxesEntity = new QgsChunkBoundsEntity( this );
210  }
211  else
212  {
213  mBboxesEntity->deleteLater();
214  mBboxesEntity = nullptr;
215  }
216 }
217 
218 void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
219 {
220  Q_FOREACH ( QgsChunkNode *node, nodes )
221  {
222  if ( node->state() == QgsChunkNode::QueuedForUpdate )
223  {
224  mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
225  node->cancelQueuedForUpdate();
226  }
227  else if ( node->state() == QgsChunkNode::Updating )
228  {
229  cancelActiveJob( node->updater() );
230  }
231 
232  Q_ASSERT( node->state() == QgsChunkNode::Loaded );
233 
234  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
235  node->setQueuedForUpdate( entry, updateJobFactory );
236  mChunkLoaderQueue->insertLast( entry );
237  }
238 
239  // trigger update
240  startJobs();
241 }
242 
243 int QgsChunkedEntity::pendingJobsCount() const
244 {
245  return mChunkLoaderQueue->count() + mActiveJobs.count();
246 }
247 
248 void QgsChunkedEntity::update( QgsChunkNode *root, const SceneState &state )
249 {
250  QSet<QgsChunkNode *> nodes;
251  QVector<std::tuple<QgsChunkNode *, float, int>> residencyRequests;
252 
253  using slot = std::pair<QgsChunkNode *, float>;
254  auto cmp_funct = []( slot & p1, slot & p2 )
255  {
256  return p1.second <= p2.second;
257  };
258  int renderedCount = 0;
259  std::priority_queue<slot, std::vector<slot>, decltype( cmp_funct )> pq( cmp_funct );
260  pq.push( std::make_pair( root, screenSpaceError( root, state ) ) );
261  while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
262  {
263  slot s = pq.top();
264  pq.pop();
265  QgsChunkNode *node = s.first;
266 
267  if ( Qgs3DUtils::isCullable( node->bbox(), state.viewProjectionMatrix ) )
268  {
269  ++mFrustumCulled;
270  continue;
271  }
272 
273  // ensure we have child nodes (at least skeletons) available, if any
274  if ( node->childCount() == -1 )
275  node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
276 
277  // make sure all nodes leading to children are always loaded
278  // so that zooming out does not create issues
279  double dist = node->bbox().center().distanceToPoint( state.cameraPos );
280  residencyRequests.push_back( std::make_tuple( node, dist, node->level() ) );
281 
282  if ( !node->entity() )
283  {
284  // this happens initially when root node is not ready yet
285  continue;
286  }
287  bool becomesActive = false;
288 
289  //QgsDebugMsgLevel( QStringLiteral( "%1|%2|%3 %4 %5" ).arg( node->tileX() ).arg( node->tileY() ).arg( node->tileZ() ).arg( mTau ).arg( screenSpaceError( node, state ) ), 2 );
290  if ( node->childCount() == 0 )
291  {
292  // there's no children available for this node, so regardless of whether it has an acceptable error
293  // or not, it's the best we'll ever get...
294  becomesActive = true;
295  }
296  else if ( mTau > 0 && screenSpaceError( node, state ) <= mTau )
297  {
298  // acceptable error for the current chunk - let's render it
299  becomesActive = true;
300  }
301  else if ( node->allChildChunksResident( mCurrentTime ) )
302  {
303  // error is not acceptable and children are ready to be used - recursive descent
304  if ( mAdditiveStrategy )
305  {
306  // With additive strategy enabled, also all parent nodes are added to active nodes.
307  // This is desired when child nodes add more detailed data rather than just replace
308  // coarser data in parents. We use this e.g. with point cloud data.
309  becomesActive = true;
310  }
311  QgsChunkNode *const *children = node->children();
312  for ( int i = 0; i < node->childCount(); ++i )
313  pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
314  }
315  else
316  {
317  // error is not acceptable but children are not ready either - still use parent but request children
318  becomesActive = true;
319 
320  QgsChunkNode *const *children = node->children();
321  for ( int i = 0; i < node->childCount(); ++i )
322  {
323  double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
324  residencyRequests.push_back( std::make_tuple( children[i], dist, children[i]->level() ) );
325  }
326  }
327  if ( becomesActive )
328  {
329  mActiveNodes << node;
330  // if we are not using additive strategy we need to make sure the parent primitives are not counted
331  if ( !mAdditiveStrategy && node->parent() && nodes.contains( node->parent() ) )
332  {
333  nodes.remove( node->parent() );
334  renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
335  }
336  renderedCount += mChunkLoaderFactory->primitivesCount( node );
337  nodes.insert( node );
338  }
339  }
340  // sort nodes by their level and their distance from the camera
341  std::sort( residencyRequests.begin(), residencyRequests.end(), [&]( std::tuple<QgsChunkNode *, float, int> &n1, std::tuple<QgsChunkNode *, float, int> &n2 )
342  {
343  if ( std::get<2>( n1 ) == std::get<2>( n2 ) )
344  return std::get<1>( n1 ) >= std::get<1>( n1 );
345  return std::get<2>( n1 ) >= std::get<2>( n2 );
346  } );
347  for ( std::tuple<QgsChunkNode *, float, int> n : residencyRequests )
348  requestResidency( std::get<0>( n ) );
349 }
350 
351 void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
352 {
353  if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
354  {
355  Q_ASSERT( node->replacementQueueEntry() );
356  Q_ASSERT( node->entity() );
357  mReplacementQueue->takeEntry( node->replacementQueueEntry() );
358  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
359  }
360  else if ( node->state() == QgsChunkNode::QueuedForLoad )
361  {
362  // move to the front of loading queue
363  Q_ASSERT( node->loaderQueueEntry() );
364  Q_ASSERT( !node->loader() );
365  if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
366  {
367  mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
368  mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
369  }
370  }
371  else if ( node->state() == QgsChunkNode::Loading )
372  {
373  // the entry is being currently processed - nothing to do really
374  }
375  else if ( node->state() == QgsChunkNode::Skeleton )
376  {
377  if ( !node->hasData() )
378  return; // no need to load (we already tried but got nothing back)
379 
380  // add to the loading queue
381  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
382  node->setQueuedForLoad( entry );
383  mChunkLoaderQueue->insertFirst( entry );
384  }
385  else
386  Q_ASSERT( false && "impossible!" );
387 }
388 
389 
390 void QgsChunkedEntity::onActiveJobFinished()
391 {
392  int oldJobsCount = pendingJobsCount();
393 
394  QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
395  Q_ASSERT( job );
396  Q_ASSERT( mActiveJobs.contains( job ) );
397 
398  QgsChunkNode *node = job->chunk();
399 
400  if ( QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job ) )
401  {
402  Q_ASSERT( node->state() == QgsChunkNode::Loading );
403  Q_ASSERT( node->loader() == loader );
404 
405  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
406  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
407 
408  QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
409  // mark as loaded + create entity
410  Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
411 
412  if ( entity )
413  {
414  // load into node (should be in main thread again)
415  node->setLoaded( entity );
416 
417  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
418 
419  if ( mPickingEnabled )
420  {
421  Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
422  node->entity()->addComponent( picker );
423  connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
424  }
425 
426  emit newEntityCreated( entity );
427  }
428  else
429  {
430  node->setHasData( false );
431  node->cancelLoading();
432  }
433 
434  // now we need an update!
435  mNeedsUpdate = true;
436  }
437  else
438  {
439  Q_ASSERT( node->state() == QgsChunkNode::Updating );
440  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
441  node->setUpdated();
442  }
443 
444  // cleanup the job that has just finished
445  mActiveJobs.removeOne( job );
446  job->deleteLater();
447 
448  // start another job - if any
449  startJobs();
450 
451  if ( pendingJobsCount() != oldJobsCount )
452  emit pendingJobsCountChanged();
453 }
454 
455 void QgsChunkedEntity::startJobs()
456 {
457  while ( mActiveJobs.count() < 4 )
458  {
459  if ( mChunkLoaderQueue->isEmpty() )
460  return;
461 
462  QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
463  Q_ASSERT( entry );
464  QgsChunkNode *node = entry->chunk;
465  delete entry;
466 
467  QgsChunkQueueJob *job = startJob( node );
468  mActiveJobs.append( job );
469  }
470 }
471 
472 QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
473 {
474  if ( node->state() == QgsChunkNode::QueuedForLoad )
475  {
476  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
477  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
478 
479  QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
480  connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
481  node->setLoading( loader );
482  return loader;
483  }
484  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
485  {
486  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
487 
488  node->setUpdating();
489  connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
490  return node->updater();
491  }
492  else
493  {
494  Q_ASSERT( false ); // not possible
495  return nullptr;
496  }
497 }
498 
499 void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
500 {
501  Q_ASSERT( job );
502 
503  QgsChunkNode *node = job->chunk();
504 
505  if ( qobject_cast<QgsChunkLoader *>( job ) )
506  {
507  // return node back to skeleton
508  node->cancelLoading();
509 
510  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
511  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
512  }
513  else
514  {
515  // return node back to loaded state
516  node->cancelUpdating();
517 
518  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
519  }
520 
521  job->cancel();
522  mActiveJobs.removeOne( job );
523  job->deleteLater();
524 }
525 
526 void QgsChunkedEntity::cancelActiveJobs()
527 {
528  while ( !mActiveJobs.isEmpty() )
529  {
530  cancelActiveJob( mActiveJobs.takeFirst() );
531  }
532 }
533 
534 
535 void QgsChunkedEntity::setPickingEnabled( bool enabled )
536 {
537  if ( mPickingEnabled == enabled )
538  return;
539 
540  mPickingEnabled = enabled;
541 
542  if ( enabled )
543  {
544  QgsChunkListEntry *entry = mReplacementQueue->first();
545  while ( entry )
546  {
547  QgsChunkNode *node = entry->chunk;
548  Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
549  node->entity()->addComponent( picker );
550  connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
551 
552  entry = entry->next;
553  }
554  }
555  else
556  {
557  for ( Qt3DRender::QObjectPicker *picker : findChildren<Qt3DRender::QObjectPicker *>() )
558  picker->deleteLater();
559  }
560 }
561 
562 void QgsChunkedEntity::onPickEvent( Qt3DRender::QPickEvent *event )
563 {
564  Qt3DRender::QPickTriangleEvent *triangleEvent = qobject_cast<Qt3DRender::QPickTriangleEvent *>( event );
565  if ( !triangleEvent )
566  return;
567 
568  Qt3DRender::QObjectPicker *picker = qobject_cast<Qt3DRender::QObjectPicker *>( sender() );
569  if ( !picker )
570  return;
571 
572  Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( picker->parent() );
573  if ( !entity )
574  return;
575 
576  // go figure out feature ID from the triangle index
577  QgsFeatureId fid = FID_NULL;
578  for ( Qt3DRender::QGeometryRenderer *geomRenderer : entity->findChildren<Qt3DRender::QGeometryRenderer *>() )
579  {
580  // unfortunately we can't access which sub-entity triggered the pick event
581  // so as a temporary workaround let's just ignore the entity with selection
582  // and hope the event was the main entity (QTBUG-58206)
583  if ( geomRenderer->objectName() != QLatin1String( "main" ) )
584  continue;
585 
586  if ( QgsTessellatedPolygonGeometry *g = qobject_cast<QgsTessellatedPolygonGeometry *>( geomRenderer->geometry() ) )
587  {
588  fid = g->triangleIndexToFeatureId( triangleEvent->triangleIndex() );
589  if ( !FID_IS_NULL( fid ) )
590  break;
591  }
592  }
593 
594  if ( !FID_IS_NULL( fid ) )
595  {
596  emit pickedObject( event, fid );
597  }
598 }
599 
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
Definition: qgs3dutils.cpp:408
#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