18#include <QElapsedTimer>
20#include <Qt3DRender/QObjectPicker>
21#include <Qt3DRender/QPickTriangleEvent>
22#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
23#include <Qt3DRender/QBuffer>
26#include <Qt3DCore/QBuffer>
43static float screenSpaceError(
float epsilon,
float distance,
float screenSize,
float fov )
65 float phi = epsilon * screenSize / ( 2 * distance * tan( fov * M_PI / ( 2 * 180 ) ) );
69static float screenSpaceError( QgsChunkNode *node,
const QgsChunkedEntity::SceneState &state )
71 if ( node->error() <= 0 )
74 float dist = node->bbox().distanceFromPoint( state.cameraPos );
78 float sse = screenSpaceError( node->error(), dist, state.screenSizePx, state.cameraFov );
82QgsChunkedEntity::QgsChunkedEntity(
float tau, QgsChunkLoaderFactory *loaderFactory,
bool ownsFactory,
int primitiveBudget, Qt3DCore::QNode *parent )
85 , mChunkLoaderFactory( loaderFactory )
86 , mOwnsFactory( ownsFactory )
87 , mPrimitivesBudget( primitiveBudget )
89 mRootNode = loaderFactory->createRootNode();
90 mChunkLoaderQueue =
new QgsChunkList;
91 mReplacementQueue =
new QgsChunkList;
95QgsChunkedEntity::~QgsChunkedEntity()
100 Q_ASSERT( mActiveJobs.isEmpty() );
103 while ( !mChunkLoaderQueue->isEmpty() )
105 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
106 QgsChunkNode *node = entry->chunk;
108 if ( node->state() == QgsChunkNode::QueuedForLoad )
109 node->cancelQueuedForLoad();
110 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
111 node->cancelQueuedForUpdate();
116 delete mChunkLoaderQueue;
118 while ( !mReplacementQueue->isEmpty() )
120 QgsChunkListEntry *entry = mReplacementQueue->takeFirst();
123 entry->chunk->unloadChunk();
126 delete mReplacementQueue;
131 delete mChunkLoaderFactory;
136void QgsChunkedEntity::update(
const SceneState &state )
146 pruneLoaderQueue( state );
151 int oldJobsCount = pendingJobsCount();
153 QSet<QgsChunkNode *> activeBefore = qgis::listToSet( mActiveNodes );
154 mActiveNodes.clear();
156 mCurrentTime = QTime::currentTime();
158 update( mRootNode, state );
160 int enabled = 0, disabled = 0, unloaded = 0;
162 for ( QgsChunkNode *node : std::as_const( mActiveNodes ) )
164 if ( activeBefore.contains( node ) )
166 activeBefore.remove( node );
170 if ( !node->entity() )
172 QgsDebugMsg(
"Active node has null entity - this should never happen!" );
175 node->entity()->setEnabled(
true );
181 for ( QgsChunkNode *node : activeBefore )
183 if ( !node->entity() )
185 QgsDebugMsg(
"Active node has null entity - this should never happen!" );
188 node->entity()->setEnabled(
false );
192 double usedGpuMemory = QgsChunkedEntity::calculateEntityGpuMemorySize(
this );
196 while ( usedGpuMemory > mGpuMemoryLimit )
198 QgsChunkListEntry *entry = mReplacementQueue->takeLast();
199 usedGpuMemory -= QgsChunkedEntity::calculateEntityGpuMemorySize( entry->chunk->entity() );
200 entry->chunk->unloadChunk();
201 mActiveNodes.removeOne( entry->chunk );
207 QList<QgsAABB> bboxes;
208 for ( QgsChunkNode *n : std::as_const( mActiveNodes ) )
210 mBboxesEntity->setBoxes( bboxes );
216 mNeedsUpdate =
false;
218 if ( pendingJobsCount() != oldJobsCount )
219 emit pendingJobsCountChanged();
221 QgsDebugMsgLevel( QStringLiteral(
"update: active %1 enabled %2 disabled %3 | culled %4 | loading %5 loaded %6 | unloaded %7 elapsed %8ms" ).arg( mActiveNodes.count() )
224 .arg( mFrustumCulled )
225 .arg( mReplacementQueue->count() )
227 .arg( t.elapsed() ), 2 );
230void QgsChunkedEntity::setShowBoundingBoxes(
bool enabled )
232 if ( ( enabled && mBboxesEntity ) || ( !enabled && !mBboxesEntity ) )
237 mBboxesEntity =
new QgsChunkBoundsEntity(
this );
241 mBboxesEntity->deleteLater();
242 mBboxesEntity =
nullptr;
246void QgsChunkedEntity::updateNodes(
const QList<QgsChunkNode *> &nodes, QgsChunkQueueJobFactory *updateJobFactory )
248 for ( QgsChunkNode *node : nodes )
250 if ( node->state() == QgsChunkNode::QueuedForUpdate )
252 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
253 node->cancelQueuedForUpdate();
255 else if ( node->state() == QgsChunkNode::Updating )
257 cancelActiveJob( node->updater() );
260 Q_ASSERT( node->state() == QgsChunkNode::Loaded );
262 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
263 node->setQueuedForUpdate( entry, updateJobFactory );
264 mChunkLoaderQueue->insertLast( entry );
271void QgsChunkedEntity::pruneLoaderQueue(
const SceneState &state )
273 QList<QgsChunkNode *> toRemoveFromLoaderQueue;
278 QgsChunkListEntry *e = mChunkLoaderQueue->first();
281 Q_ASSERT( e->chunk->state() == QgsChunkNode::QueuedForLoad || e->chunk->state() == QgsChunkNode::QueuedForUpdate );
284 toRemoveFromLoaderQueue.append( e->chunk );
290 for ( QgsChunkNode *n : toRemoveFromLoaderQueue )
292 mChunkLoaderQueue->takeEntry( n->loaderQueueEntry() );
293 if ( n->state() == QgsChunkNode::QueuedForLoad )
295 n->cancelQueuedForLoad();
299 n->cancelQueuedForUpdate();
300 mReplacementQueue->takeEntry( n->replacementQueueEntry() );
305 if ( !toRemoveFromLoaderQueue.isEmpty() )
307 QgsDebugMsgLevel( QStringLiteral(
"Pruned %1 chunks in loading queue" ).arg( toRemoveFromLoaderQueue.count() ), 2 );
312int QgsChunkedEntity::pendingJobsCount()
const
314 return mChunkLoaderQueue->count() + mActiveJobs.count();
317struct ResidencyRequest
319 QgsChunkNode *node =
nullptr;
322 ResidencyRequest() =
default;
335 bool operator()(
const ResidencyRequest &request,
const ResidencyRequest &otherRequest )
const
337 if ( request.level == otherRequest.level )
338 return request.dist > otherRequest.dist;
339 return request.level > otherRequest.level;
341} ResidencyRequestSorter;
343void QgsChunkedEntity::update( QgsChunkNode *root,
const SceneState &state )
345 QSet<QgsChunkNode *> nodes;
346 QVector<ResidencyRequest> residencyRequests;
348 using slotItem = std::pair<QgsChunkNode *, float>;
349 auto cmp_funct = [](
const slotItem & p1,
const slotItem & p2 )
351 return p1.second <= p2.second;
353 int renderedCount = 0;
354 std::priority_queue<slotItem, std::vector<slotItem>,
decltype( cmp_funct )> pq( cmp_funct );
355 pq.push( std::make_pair( root, screenSpaceError( root, state ) ) );
356 while ( !pq.empty() && renderedCount <= mPrimitivesBudget )
358 slotItem s = pq.top();
360 QgsChunkNode *node = s.first;
369 if ( node->childCount() == -1 )
370 node->populateChildren( mChunkLoaderFactory->createChildren( node ) );
374 double dist = node->bbox().center().distanceToPoint( state.cameraPos );
375 residencyRequests.push_back( ResidencyRequest( node, dist, node->level() ) );
377 if ( !node->entity() )
382 bool becomesActive =
false;
385 if ( node->childCount() == 0 )
389 becomesActive =
true;
391 else if ( mTau > 0 && screenSpaceError( node, state ) <= mTau )
394 becomesActive =
true;
403 if ( mAdditiveStrategy )
408 becomesActive =
true;
410 QgsChunkNode *
const *children = node->children();
411 for (
int i = 0; i < node->childCount(); ++i )
413 if ( children[i]->entity() || !children[i]->hasData() )
416 pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
424 double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
425 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
434 if ( node->allChildChunksResident( mCurrentTime ) )
436 QgsChunkNode *
const *children = node->children();
437 for (
int i = 0; i < node->childCount(); ++i )
438 pq.push( std::make_pair( children[i], screenSpaceError( children[i], state ) ) );
442 becomesActive =
true;
444 QgsChunkNode *
const *children = node->children();
445 for (
int i = 0; i < node->childCount(); ++i )
447 double dist = children[i]->bbox().center().distanceToPoint( state.cameraPos );
448 residencyRequests.push_back( ResidencyRequest( children[i], dist, children[i]->level() ) );
456 mActiveNodes << node;
458 if ( !mAdditiveStrategy && node->parent() && nodes.contains( node->parent() ) )
460 nodes.remove( node->parent() );
461 renderedCount -= mChunkLoaderFactory->primitivesCount( node->parent() );
463 renderedCount += mChunkLoaderFactory->primitivesCount( node );
464 nodes.insert( node );
469 std::sort( residencyRequests.begin(), residencyRequests.end(), ResidencyRequestSorter );
470 for (
const auto &request : residencyRequests )
471 requestResidency( request.node );
474void QgsChunkedEntity::requestResidency( QgsChunkNode *node )
476 if ( node->state() == QgsChunkNode::Loaded || node->state() == QgsChunkNode::QueuedForUpdate || node->state() == QgsChunkNode::Updating )
478 Q_ASSERT( node->replacementQueueEntry() );
479 Q_ASSERT( node->entity() );
480 mReplacementQueue->takeEntry( node->replacementQueueEntry() );
481 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
483 else if ( node->state() == QgsChunkNode::QueuedForLoad )
486 Q_ASSERT( node->loaderQueueEntry() );
487 Q_ASSERT( !node->loader() );
488 if ( node->loaderQueueEntry()->prev || node->loaderQueueEntry()->next )
490 mChunkLoaderQueue->takeEntry( node->loaderQueueEntry() );
491 mChunkLoaderQueue->insertFirst( node->loaderQueueEntry() );
494 else if ( node->state() == QgsChunkNode::Loading )
498 else if ( node->state() == QgsChunkNode::Skeleton )
500 if ( !node->hasData() )
504 QgsChunkListEntry *entry =
new QgsChunkListEntry( node );
505 node->setQueuedForLoad( entry );
506 mChunkLoaderQueue->insertFirst( entry );
509 Q_ASSERT(
false &&
"impossible!" );
513void QgsChunkedEntity::onActiveJobFinished()
515 int oldJobsCount = pendingJobsCount();
517 QgsChunkQueueJob *job = qobject_cast<QgsChunkQueueJob *>( sender() );
519 Q_ASSERT( mActiveJobs.contains( job ) );
521 QgsChunkNode *node = job->chunk();
523 if ( QgsChunkLoader *loader = qobject_cast<QgsChunkLoader *>( job ) )
525 Q_ASSERT( node->state() == QgsChunkNode::Loading );
526 Q_ASSERT( node->loader() == loader );
528 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Load " ) + node->tileId().text(), node->tileId().text() );
529 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Load" ), node->tileId().text() );
531 QgsEventTracing::ScopedEvent e(
"3D", QString(
"create" ) );
533 Qt3DCore::QEntity *entity = node->loader()->createEntity(
this );
543 mActiveNodes << node;
546 node->setLoaded( entity );
548 mReplacementQueue->insertFirst( node->replacementQueueEntry() );
550 if ( mPickingEnabled )
552 Qt3DRender::QObjectPicker *picker =
new Qt3DRender::QObjectPicker( node->entity() );
553 node->entity()->addComponent( picker );
554 connect( picker, &Qt3DRender::QObjectPicker::clicked,
this, &QgsChunkedEntity::onPickEvent );
557 emit newEntityCreated( entity );
561 node->setHasData(
false );
562 node->cancelLoading();
570 Q_ASSERT( node->state() == QgsChunkNode::Updating );
571 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Update" ), node->tileId().text() );
576 mActiveJobs.removeOne( job );
582 if ( pendingJobsCount() != oldJobsCount )
583 emit pendingJobsCountChanged();
586void QgsChunkedEntity::startJobs()
588 while ( mActiveJobs.count() < 4 )
590 if ( mChunkLoaderQueue->isEmpty() )
593 QgsChunkListEntry *entry = mChunkLoaderQueue->takeFirst();
595 QgsChunkNode *node = entry->chunk;
598 QgsChunkQueueJob *job = startJob( node );
599 mActiveJobs.append( job );
603QgsChunkQueueJob *QgsChunkedEntity::startJob( QgsChunkNode *node )
605 if ( node->state() == QgsChunkNode::QueuedForLoad )
607 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral(
"3D" ), QStringLiteral(
"Load" ), node->tileId().text() );
608 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral(
"3D" ), QStringLiteral(
"Load " ) + node->tileId().text(), node->tileId().text() );
610 QgsChunkLoader *loader = mChunkLoaderFactory->createChunkLoader( node );
611 connect( loader, &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
612 node->setLoading( loader );
615 else if ( node->state() == QgsChunkNode::QueuedForUpdate )
617 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral(
"3D" ), QStringLiteral(
"Update" ), node->tileId().text() );
620 connect( node->updater(), &QgsChunkQueueJob::finished,
this, &QgsChunkedEntity::onActiveJobFinished );
621 return node->updater();
630void QgsChunkedEntity::cancelActiveJob( QgsChunkQueueJob *job )
634 QgsChunkNode *node = job->chunk();
636 if ( qobject_cast<QgsChunkLoader *>( job ) )
639 node->cancelLoading();
641 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Load " ) + node->tileId().text(), node->tileId().text() );
642 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Load" ), node->tileId().text() );
647 node->cancelUpdating();
649 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral(
"3D" ), QStringLiteral(
"Update" ), node->tileId().text() );
653 mActiveJobs.removeOne( job );
657void QgsChunkedEntity::cancelActiveJobs()
659 while ( !mActiveJobs.isEmpty() )
661 cancelActiveJob( mActiveJobs.takeFirst() );
666void QgsChunkedEntity::setPickingEnabled(
bool enabled )
668 if ( mPickingEnabled == enabled )
671 mPickingEnabled = enabled;
675 QgsChunkListEntry *entry = mReplacementQueue->first();
678 QgsChunkNode *node = entry->chunk;
679 Qt3DRender::QObjectPicker *picker =
new Qt3DRender::QObjectPicker( node->entity() );
680 node->entity()->addComponent( picker );
681 connect( picker, &Qt3DRender::QObjectPicker::clicked,
this, &QgsChunkedEntity::onPickEvent );
688 for ( Qt3DRender::QObjectPicker *picker : findChildren<Qt3DRender::QObjectPicker *>() )
689 picker->deleteLater();
693void QgsChunkedEntity::onPickEvent( Qt3DRender::QPickEvent *event )
695 Qt3DRender::QPickTriangleEvent *triangleEvent = qobject_cast<Qt3DRender::QPickTriangleEvent *>( event );
696 if ( !triangleEvent )
699 Qt3DRender::QObjectPicker *picker = qobject_cast<Qt3DRender::QObjectPicker *>( sender() );
703 Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( picker->parent() );
709 for ( Qt3DRender::QGeometryRenderer *geomRenderer : entity->findChildren<Qt3DRender::QGeometryRenderer *>() )
714 if ( geomRenderer->objectName() != QLatin1String(
"main" ) )
719 fid = g->triangleIndexToFeatureId( triangleEvent->triangleIndex() );
727 emit pickedObject( event, fid );
731double QgsChunkedEntity::calculateEntityGpuMemorySize( Qt3DCore::QEntity *entity )
733 long long usedGpuMemory = 0;
736 usedGpuMemory += buffer->data().size();
738 for ( Qt3DRender::QTexture2D *tex : entity->findChildren<Qt3DRender::QTexture2D *>() )
741 usedGpuMemory += tex->width() * tex->height() * 4;
743 return usedGpuMemory / 1024.0 / 1024.0;
static bool isCullable(const QgsAABB &bbox, const QMatrix4x4 &viewProjectionMatrix)
Returns true if bbox is completely outside the current viewing volume.
Qt3DCore::QBuffer Qt3DQBuffer
Qt3DCore::QBuffer Qt3DQBuffer
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsgLevel(str, level)