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