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