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