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