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