QGIS API Documentation  3.2.0-Bonn (bc43194)
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 *
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"
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 )
70 {
71  mRootNode = new QgsChunkNode( 0, 0, 0, rootBbox, rootError );
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
82  Q_ASSERT( !mActiveJob );
83
84  // clean up any pending load requests
86  {
88  QgsChunkNode *node = entry->chunk;
89
90  if ( node->state() == QgsChunkNode::QueuedForLoad )
92  else if ( node->state() == QgsChunkNode::QueuedForUpdate )
93  node->cancelQueuedForUpdate();
94  else
95  Q_ASSERT( false ); // impossible!
96  }
97
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?
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
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  {
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 );
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  {
294  {
297  }
298  }
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
309  QgsChunkListEntry *entry = new QgsChunkListEntry( node );
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
329  {
332
333  // mark as loaded + create entity
334  Qt3DCore::QEntity *entity = node->loader()->createEntity( this );
335
336  if ( entity )
337  {
340
341  mReplacementQueue->insertFirst( node->replacementQueueEntry() );
342  }
343  else
344  {
345  node->setHasData( false );
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 );
373  return;
374
376  Q_ASSERT( entry );
377  QgsChunkNode *node = entry->chunk;
378  delete entry;
379
380  if ( node->state() == QgsChunkNode::QueuedForLoad )
381  {
383  connect( loader, &QgsChunkQueueJob::finished, this, &QgsChunkedEntity::onActiveJobFinished );
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
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:264