QGIS API Documentation  3.23.0-Master (dd0cd13a00)
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  for ( QgsChunkNode *n : std::as_const( 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  for ( 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 struct ResidencyRequest
249 {
250  QgsChunkNode *node = nullptr;
251  float dist = 0.0;
252  int level = -1;
253  ResidencyRequest() = default;
254  ResidencyRequest(
255  QgsChunkNode *n,
256  float d,
257  int l )
258  : node( n )
259  , dist( d )
260  , level( l )
261  {}
262 };
263 
264 struct
265 {
266  bool operator()( const ResidencyRequest &request, const ResidencyRequest &otherRequest ) const
267  {
268  if ( request.level == otherRequest.level )
269  return request.dist > otherRequest.dist;
270  return request.level > otherRequest.level;
271  }
272 } ResidencyRequestSorter;
273 
274 void QgsChunkedEntity::update( QgsChunkNode *root, const SceneState &state )
275 {
276  QSet<QgsChunkNode *> nodes;
277  QVector<ResidencyRequest> residencyRequests;
278 
279  using slotItem = std::pair<QgsChunkNode *, float>;
280  auto cmp_funct = []( slotItem & p1, slotItem & p2 )
281  {
282  return p1.second <= p2.second;
283  };
284  int renderedCount = 0;
285  std::priority_queue<slotItem, std::vector<slotItem>, decltype( cmp_funct )> pq( cmp_funct );
286  pq.push( std::make_pair( root, screenSpaceError( root, state ) ) );
287  while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
288  {
289  slotItem s = pq.top();
290  pq.pop();
291  QgsChunkNode *node = s.first;
292 
293  if ( Qgs3DUtils::isCullable( node->bbox(), state.viewProjectionMatrix ) )
294  {
295  ++mFrustumCulled;
296  continue;
297  }
298 
299  // ensure we have child nodes (at least skeletons) available, if any
300  if ( node->childCount() == -1 )
301  node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
302 
303  // make sure all nodes leading to children are always loaded
304  // so that zooming out does not create issues
305  double dist = node->bbox().center().distanceToPoint( state.cameraPos );
306  residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
307 
308  if ( !node->entity() )
309  {
310  // this happens initially when root node is not ready yet
311  continue;
312  }
313  bool becomesActive = false;
314 
315  // 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 );
316  if ( node->childCount() == 0 )
317  {
318  // there's no children available for this node, so regardless of whether it has an acceptable error
319  // or not, it's the best we'll ever get...
320  becomesActive = true;
321  }
322  else if ( mTau > 0 && screenSpaceError( node, state ) <= mTau )
323  {
324  // acceptable error for the current chunk - let's render it
325  becomesActive = true;
326  }
327  else if ( node->allChildChunksResident( mCurrentTime ) )
328  {
329  // error is not acceptable and children are ready to be used - recursive descent
330  if ( mAdditiveStrategy )
331  {
332  // With additive strategy enabled, also all parent nodes are added to active nodes.
333  // This is desired when child nodes add more detailed data rather than just replace
334  // coarser data in parents. We use this e.g. with point cloud data.
335  becomesActive = true;
336  }
337  QgsChunkNode *const *children = node->children();
338  for ( int i = 0; i < node->childCount(); ++i )
339  pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
340  }
341  else
342  {
343  // error is not acceptable but children are not ready either - still use parent but request children
344  becomesActive = true;
345 
346  QgsChunkNode *const *children = node->children();
347  for ( int i = 0; i < node->childCount(); ++i )
348  {
349  double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
350  residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
351  }
352  }
353  if ( becomesActive )
354  {
355  mActiveNodes << node;
356  // if we are not using additive strategy we need to make sure the parent primitives are not counted
357  if ( !mAdditiveStrategy && node->parent() && nodes.contains( node->parent() ) )
358  {
359  nodes.remove( node->parent() );
360  renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
361  }
362  renderedCount += mChunkLoaderFactory->primitivesCount( node );
363  nodes.insert( node );
364  }
365  }
366 
367  // sort nodes by their level and their distance from the camera
368  std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
369  for ( const auto &request : residencyRequests )
370  requestResidency( request.node );
371 }
372 
373 void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
374 {
375  if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
376  {
377  Q_ASSERT( node->replacementQueueEntry() );
378  Q_ASSERT( node->entity() );
379  mReplacementQueue->takeEntry( node->replacementQueueEntry() );
380  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
381  }
382  else if ( node->state() == QgsChunkNode::QueuedForLoad )
383  {
384  // move to the front of loading queue
385  Q_ASSERT( node->loaderQueueEntry() );
386  Q_ASSERT( !node->loader() );
387  if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
388  {
389  mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
390  mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
391  }
392  }
393  else if ( node->state() == QgsChunkNode::Loading )
394  {
395  // the entry is being currently processed - nothing to do really
396  }
397  else if ( node->state() == QgsChunkNode::Skeleton )
398  {
399  if ( !node->hasData() )
400  return; // no need to load (we already tried but got nothing back)
401 
402  // add to the loading queue
403  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
404  node->setQueuedForLoad( entry );
405  mChunkLoaderQueue->insertFirst( entry );
406  }
407  else
408  Q_ASSERT( false && "impossible!" );
409 }
410 
411 
412 void QgsChunkedEntity::onActiveJobFinished()
413 {
414  int oldJobsCount = pendingJobsCount();
415 
416  QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
417  Q_ASSERT( job );
418  Q_ASSERT( mActiveJobs.contains( job ) );
419 
420  QgsChunkNode *node = job->chunk();
421 
422  if ( QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job ) )
423  {
424  Q_ASSERT( node->state() == QgsChunkNode::Loading );
425  Q_ASSERT( node->loader() == loader );
426 
427  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
428  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
429 
430  QgsEventTracing::ScopedEvent e( "3D", QString( "create" ) );
431  // mark as loaded + create entity
432  Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
433 
434  if ( entity )
435  {
436  // load into node (should be in main thread again)
437  node->setLoaded( entity );
438 
439  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
440 
441  if ( mPickingEnabled )
442  {
443  Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
444  node->entity()->addComponent( picker );
445  connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
446  }
447 
448  emit newEntityCreated( entity );
449  }
450  else
451  {
452  node->setHasData( false );
453  node->cancelLoading();
454  }
455 
456  // now we need an update!
457  mNeedsUpdate = true;
458  }
459  else
460  {
461  Q_ASSERT( node->state() == QgsChunkNode::Updating );
462  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
463  node->setUpdated();
464  }
465 
466  // cleanup the job that has just finished
467  mActiveJobs.removeOne( job );
468  job->deleteLater();
469 
470  // start another job - if any
471  startJobs();
472 
473  if ( pendingJobsCount() != oldJobsCount )
474  emit pendingJobsCountChanged();
475 }
476 
477 void QgsChunkedEntity::startJobs()
478 {
479  while ( mActiveJobs.count() < 4 )
480  {
481  if ( mChunkLoaderQueue->isEmpty() )
482  return;
483 
484  QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
485  Q_ASSERT( entry );
486  QgsChunkNode *node = entry->chunk;
487  delete entry;
488 
489  QgsChunkQueueJob *job = startJob( node );
490  mActiveJobs.append( job );
491  }
492 }
493 
494 QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
495 {
496  if ( node->state() == QgsChunkNode::QueuedForLoad )
497  {
498  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
499  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
500 
501  QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
502  connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
503  node->setLoading( loader );
504  return loader;
505  }
506  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
507  {
508  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
509 
510  node->setUpdating();
511  connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
512  return node->updater();
513  }
514  else
515  {
516  Q_ASSERT( false ); // not possible
517  return nullptr;
518  }
519 }
520 
521 void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
522 {
523  Q_ASSERT( job );
524 
525  QgsChunkNode *node = job->chunk();
526 
527  if ( qobject_cast<QgsChunkLoader *>( job ) )
528  {
529  // return node back to skeleton
530  node->cancelLoading();
531 
532  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
533  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load" ), node->tileId().text() );
534  }
535  else
536  {
537  // return node back to loaded state
538  node->cancelUpdating();
539 
540  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Update" ), node->tileId().text() );
541  }
542 
543  job->cancel();
544  mActiveJobs.removeOne( job );
545  job->deleteLater();
546 }
547 
548 void QgsChunkedEntity::cancelActiveJobs()
549 {
550  while ( !mActiveJobs.isEmpty() )
551  {
552  cancelActiveJob( mActiveJobs.takeFirst() );
553  }
554 }
555 
556 
557 void QgsChunkedEntity::setPickingEnabled( bool enabled )
558 {
559  if ( mPickingEnabled == enabled )
560  return;
561 
562  mPickingEnabled = enabled;
563 
564  if ( enabled )
565  {
566  QgsChunkListEntry *entry = mReplacementQueue->first();
567  while ( entry )
568  {
569  QgsChunkNode *node = entry->chunk;
570  Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
571  node->entity()->addComponent( picker );
572  connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
573 
574  entry = entry->next;
575  }
576  }
577  else
578  {
579  for ( Qt3DRender::QObjectPicker *picker : findChildren<Qt3DRender::QObjectPicker *>() )
580  picker->deleteLater();
581  }
582 }
583 
584 void QgsChunkedEntity::onPickEvent( Qt3DRender::QPickEvent *event )
585 {
586  Qt3DRender::QPickTriangleEvent *triangleEvent = qobject_cast<Qt3DRender::QPickTriangleEvent *>( event );
587  if ( !triangleEvent )
588  return;
589 
590  Qt3DRender::QObjectPicker *picker = qobject_cast<Qt3DRender::QObjectPicker *>( sender() );
591  if ( !picker )
592  return;
593 
594  Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( picker->parent() );
595  if ( !entity )
596  return;
597 
598  // go figure out feature ID from the triangle index
599  QgsFeatureId fid = FID_NULL;
600  for ( Qt3DRender::QGeometryRenderer *geomRenderer : entity->findChildren<Qt3DRender::QGeometryRenderer *>() )
601  {
602  // unfortunately we can't access which sub-entity triggered the pick event
603  // so as a temporary workaround let's just ignore the entity with selection
604  // and hope the event was the main entity (QTBUG-58206)
605  if ( geomRenderer->objectName() != QLatin1String( "main" ) )
606  continue;
607 
608  if ( QgsTessellatedPolygonGeometry *g = qobject_cast<QgsTessellatedPolygonGeometry *>( geomRenderer->geometry() ) )
609  {
610  fid = g->triangleIndexToFeatureId( triangleEvent->triangleIndex() );
611  if ( !FID_IS_NULL( fid ) )
612  break;
613  }
614  }
615 
616  if ( !FID_IS_NULL( fid ) )
617  {
618  emit pickedObject( event, fid );
619  }
620 }
621 
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