QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 
21 #include "qgs3dutils.h"
22 #include "qgschunkboundsentity_p.h"
23 #include "qgschunklist_p.h"
24 #include "qgschunkloader_p.h"
25 #include "qgschunknode_p.h"
26 
28 
29 static float screenSpaceError( float epsilon, float distance, float screenSize, float fov )
30 {
31  /* This routine approximately calculates how an error (epsilon) of an object in world coordinates
32  * at given distance (between camera and the object) will look like in screen coordinates.
33  *
34  * the math below simply uses triangle similarity:
35  *
36  * epsilon phi
37  * ----------------------------- = ----------------
38  * [ frustum width at distance ] [ screen width ]
39  *
40  * Then we solve for phi, substituting [frustum width at distance] = 2 * distance * tan(fov / 2)
41  *
42  * ________xxx__ xxx = real world error (epsilon)
43  * \ | / x = screen space error (phi)
44  * \ | /
45  * \___|_x_/ near plane (screen space)
46  * \ | /
47  * \ | /
48  * \|/ angle = field of view
49  * camera
50  */
51  float phi = epsilon * screenSize / ( 2 * distance * tan( fov * M_PI / ( 2 * 180 ) ) );
52  return phi;
53 }
54 
55 static float screenSpaceError( QgsChunkNode *node, const QgsChunkedEntity::SceneState &state )
56 {
57  float dist = node->bbox().distanceFromPoint( state.cameraPos );
58 
59  // TODO: what to do when distance == 0 ?
60 
61  float sse = screenSpaceError( node->error(), dist, state.screenSizePx, state.cameraFov );
62  return sse;
63 }
64 
65 QgsChunkedEntity::QgsChunkedEntity( const QgsAABB &rootBbox, float rootError, float tau, int maxLevel, QgsChunkLoaderFactory *loaderFactory, Qt3DCore::QNode *parent )
66  : Qt3DCore::QEntity( parent )
67  , mTau( tau )
68  , mMaxLevel( maxLevel )
69  , mChunkLoaderFactory( loaderFactory )
70 {
71  mRootNode = new QgsChunkNode( 0, 0, 0, rootBbox, rootError );
72  mChunkLoaderQueue = new QgsChunkList;
73  mReplacementQueue = new QgsChunkList;
74 }
75 
76 
77 QgsChunkedEntity::~QgsChunkedEntity()
78 {
79  // derived classes have to make sure that any pending active job has finished / been canceled
80  // before getting to this destructor - here it would be too late to cancel them
81  // (e.g. objects required for loading/updating have been deleted already)
82  Q_ASSERT( !mActiveJob );
83 
84  // clean up any pending load requests
85  while ( !mChunkLoaderQueue->isEmpty() )
86  {
87  QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
88  QgsChunkNode *node = entry->chunk;
89 
90  if ( node->state() == QgsChunkNode::QueuedForLoad )
91  node->cancelQueuedForLoad();
92  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
93  node->cancelQueuedForUpdate();
94  else
95  Q_ASSERT( false ); // impossible!
96  }
97 
98  delete mChunkLoaderQueue;
99 
100  while ( !mReplacementQueue->isEmpty() )
101  {
102  QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
103 
104  // remove loaded data from node
105  entry->chunk->unloadChunk(); // also deletes the entry
106  }
107 
108  delete mReplacementQueue;
109  delete mRootNode;
110 
111  // TODO: shall we own the factory or not?
112  //delete chunkLoaderFactory;
113 }
114 
115 
116 void QgsChunkedEntity::update( const SceneState &state )
117 {
118  QElapsedTimer t;
119  t.start();
120 
121  int oldJobsCount = pendingJobsCount();
122 
123  QSet<QgsChunkNode *> activeBefore = QSet<QgsChunkNode *>::fromList( mActiveNodes );
124  mActiveNodes.clear();
125  mFrustumCulled = 0;
126  mCurrentTime = QTime::currentTime();
127 
128  update( mRootNode, state );
129 
130  int enabled = 0, disabled = 0, unloaded = 0;
131 
132  Q_FOREACH ( QgsChunkNode *node, mActiveNodes )
133  {
134  if ( activeBefore.contains( node ) )
135  activeBefore.remove( node );
136  else
137  {
138  node->entity()->setEnabled( true );
139  ++enabled;
140  }
141  }
142 
143  // disable those that were active but will not be anymore
144  Q_FOREACH ( QgsChunkNode *node, activeBefore )
145  {
146  node->entity()->setEnabled( false );
147  ++disabled;
148  }
149 
150  // unload those that are over the limit for replacement
151  // TODO: what to do when our cache is too small and nodes are being constantly evicted + loaded again
152  while ( mReplacementQueue->count() > mMaxLoadedChunks )
153  {
154  QgsChunkListEntry *entry = mReplacementQueue->takeLast();
155  entry->chunk->unloadChunk(); // also deletes the entry
156  ++unloaded;
157  }
158 
159  if ( mBboxesEntity )
160  {
161  QList<QgsAABB> bboxes;
162  Q_FOREACH ( QgsChunkNode *n, mActiveNodes )
163  bboxes << n->bbox();
164  mBboxesEntity->setBoxes( bboxes );
165  }
166 
167  // start a job from queue if there is anything waiting
168  if ( !mActiveJob )
169  startJob();
170 
171  mNeedsUpdate = false; // just updated
172 
173  if ( pendingJobsCount() != oldJobsCount )
174  emit pendingJobsCountChanged();
175 
176  qDebug() << "update: active " << mActiveNodes.count() << " enabled " << enabled << " disabled " << disabled << " | culled " << mFrustumCulled << " | loading " << mChunkLoaderQueue->count() << " loaded " << mReplacementQueue->count() << " | unloaded " << unloaded << " elapsed " << t.elapsed() << "ms";
177 }
178 
179 void QgsChunkedEntity::setShowBoundingBoxes( bool enabled )
180 {
181  if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
182  return;
183 
184  if ( enabled )
185  {
186  mBboxesEntity = new QgsChunkBoundsEntity( this );
187  }
188  else
189  {
190  mBboxesEntity->deleteLater();
191  mBboxesEntity = nullptr;
192  }
193 }
194 
195 void QgsChunkedEntity::updateNodes( const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
196 {
197  Q_FOREACH ( QgsChunkNode *node, nodes )
198  {
199  if ( node->state() == QgsChunkNode::QueuedForUpdate )
200  {
201  mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
202  node->cancelQueuedForUpdate();
203  }
204  else if ( node->state() == QgsChunkNode::Updating )
205  {
206  cancelActiveJob(); // we have currently just one active job so that must be it
207  }
208 
209  Q_ASSERT( node->state() == QgsChunkNode::Loaded );
210 
211  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
212  node->setQueuedForUpdate( entry, updateJobFactory );
213  mChunkLoaderQueue->insertLast( entry );
214  }
215 
216  // trigger update
217  if ( !mActiveJob )
218  startJob();
219 }
220 
221 int QgsChunkedEntity::pendingJobsCount() const
222 {
223  return mChunkLoaderQueue->count() + ( mActiveJob ? 1 : 0 );
224 }
225 
226 
227 void QgsChunkedEntity::update( QgsChunkNode *node, const SceneState &state )
228 {
229  if ( Qgs3DUtils::isCullable( node->bbox(), state.viewProjectionMatrix ) )
230  {
231  ++mFrustumCulled;
232  return;
233  }
234 
235  node->ensureAllChildrenExist();
236 
237  // make sure all nodes leading to children are always loaded
238  // so that zooming out does not create issues
239  requestResidency( node );
240 
241  if ( !node->entity() )
242  {
243  // this happens initially when root node is not ready yet
244  return;
245  }
246 
247  //qDebug() << node->x << "|" << node->y << "|" << node->z << " " << tau << " " << screenSpaceError(node, state);
248 
249  if ( screenSpaceError( node, state ) <= mTau )
250  {
251  // acceptable error for the current chunk - let's render it
252 
253  mActiveNodes << node;
254  }
255  else if ( node->allChildChunksResident( mCurrentTime ) )
256  {
257  // error is not acceptable and children are ready to be used - recursive descent
258 
259  QgsChunkNode *const *children = node->children();
260  for ( int i = 0; i < 4; ++i )
261  update( children[i], state );
262  }
263  else
264  {
265  // error is not acceptable but children are not ready either - still use parent but request children
266 
267  mActiveNodes << node;
268 
269  if ( node->level() < mMaxLevel )
270  {
271  QgsChunkNode *const *children = node->children();
272  for ( int i = 0; i < 4; ++i )
273  requestResidency( children[i] );
274  }
275  }
276 }
277 
278 
279 void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
280 {
281  if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
282  {
283  Q_ASSERT( node->replacementQueueEntry() );
284  Q_ASSERT( node->entity() );
285  mReplacementQueue->takeEntry( node->replacementQueueEntry() );
286  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
287  }
288  else if ( node->state() == QgsChunkNode::QueuedForLoad )
289  {
290  // move to the front of loading queue
291  Q_ASSERT( node->loaderQueueEntry() );
292  Q_ASSERT( !node->loader() );
293  if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
294  {
295  mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
296  mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
297  }
298  }
299  else if ( node->state() == QgsChunkNode::Loading )
300  {
301  // the entry is being currently processed - nothing to do really
302  }
303  else if ( node->state() == QgsChunkNode::Skeleton )
304  {
305  if ( !node->hasData() )
306  return; // no need to load (we already tried but got nothing back)
307 
308  // add to the loading queue
309  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
310  node->setQueuedForLoad( entry );
311  mChunkLoaderQueue->insertFirst( entry );
312  }
313  else
314  Q_ASSERT( false && "impossible!" );
315 }
316 
317 
318 void QgsChunkedEntity::onActiveJobFinished()
319 {
320  int oldJobsCount = pendingJobsCount();
321 
322  QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
323  Q_ASSERT( job );
324  Q_ASSERT( job == mActiveJob );
325 
326  QgsChunkNode *node = job->chunk();
327 
328  if ( QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job ) )
329  {
330  Q_ASSERT( node->state() == QgsChunkNode::Loading );
331  Q_ASSERT( node->loader() == loader );
332 
333  // mark as loaded + create entity
334  Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
335 
336  if ( entity )
337  {
338  // load into node (should be in main thread again)
339  node->setLoaded( entity );
340 
341  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
342  }
343  else
344  {
345  node->setHasData( false );
346  node->cancelLoading();
347  }
348 
349  // now we need an update!
350  mNeedsUpdate = true;
351  }
352  else
353  {
354  Q_ASSERT( node->state() == QgsChunkNode::Updating );
355  node->setUpdated();
356  }
357 
358  // cleanup the job that has just finished
359  mActiveJob->deleteLater();
360  mActiveJob = nullptr;
361 
362  // start another job - if any
363  startJob();
364 
365  if ( pendingJobsCount() != oldJobsCount )
366  emit pendingJobsCountChanged();
367 }
368 
369 void QgsChunkedEntity::startJob()
370 {
371  Q_ASSERT( !mActiveJob );
372  if ( mChunkLoaderQueue->isEmpty() )
373  return;
374 
375  QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
376  Q_ASSERT( entry );
377  QgsChunkNode *node = entry->chunk;
378  delete entry;
379 
380  if ( node->state() == QgsChunkNode::QueuedForLoad )
381  {
382  QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
383  connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
384  node->setLoading( loader );
385  mActiveJob = loader;
386  }
387  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
388  {
389  node->setUpdating();
390  connect( node->updater(), &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
391  mActiveJob = node->updater();
392  }
393  else
394  Q_ASSERT( false ); // not possible
395 }
396 
397 void QgsChunkedEntity::cancelActiveJob()
398 {
399  Q_ASSERT( mActiveJob );
400 
401  QgsChunkNode *node = mActiveJob->chunk();
402 
403  if ( qobject_cast<QgsChunkLoader *>( mActiveJob ) )
404  {
405  // return node back to skeleton
406  node->cancelLoading();
407  }
408  else
409  {
410  // return node back to loaded state
411  node->cancelUpdating();
412  }
413 
414  mActiveJob->cancel();
415  mActiveJob->deleteLater();
416  mActiveJob = nullptr;
417 }
418 
3 Axis-aligned bounding box - in world coords.
Definition: qgsaabb.h:30
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
Definition: qgs3dutils.cpp:412