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 *
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"
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 )
79  , mOwnsFactory( ownsFactory )
80  , mPrimitivesBudget( primitiveBudget )
81 {
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
93  Q_ASSERT( mActiveJobs.isEmpty() );
94
95  // clean up any pending load requests
97  {
99  QgsChunkNode *node = entry->chunk;
100
101  if ( node->state() == QgsChunkNode::QueuedForLoad )
103  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
104  node->cancelQueuedForUpdate();
105  else
106  Q_ASSERT( false ); // impossible!
107  }
108
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  {
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
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() )
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  {
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 );
237  }
238
239  // trigger update
240  startJobs();
241 }
242
243 int QgsChunkedEntity::pendingJobsCount() const
244 {
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
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  {
388  {
391  }
392  }
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
403  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
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
423  {
426
427  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), 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  {
438
439  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
440
441  if ( mPickingEnabled )
442  {
443  Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( node->entity() );
445  connect( picker, &Qt3DRender::QObjectPicker::clicked, this, &QgsChunkedEntity::onPickEvent );
446  }
447
448  emit newEntityCreated( entity );
449  }
450  else
451  {
452  node->setHasData( false );
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  {
482  return;
483
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  {
499  QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), node->tileId().text() );
500
502  connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
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
531
532  QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "Load " ) + node->tileId().text(), 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() );
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