diff --git a/src/3d/qgs3dmapscene.cpp b/src/3d/qgs3dmapscene.cpp index 1845df0759c2..baa60913ef0c 100644 --- a/src/3d/qgs3dmapscene.cpp +++ b/src/3d/qgs3dmapscene.cpp @@ -334,8 +334,7 @@ void Qgs3DMapScene::updateScene( bool forceUpdate ) return; } - if ( forceUpdate ) - QgsEventTracing::addEvent( QgsEventTracing::Instant, QStringLiteral( "3D" ), QStringLiteral( "Update Scene" ) ); + QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), forceUpdate ? QStringLiteral( "Force update scene" ) : QStringLiteral( "Update scene" ) ); Qgs3DMapSceneEntity::SceneContext sceneContext; Qt3DRender::QCamera *camera = mEngine->camera(); @@ -421,6 +420,8 @@ bool Qgs3DMapScene::updateCameraNearFarPlanes() void Qgs3DMapScene::onFrameTriggered( float dt ) { + QgsEventTracing::addEvent( QgsEventTracing::EventType::Instant, QStringLiteral( "3D" ), QStringLiteral( "Frame begins" ) ); + mCameraController->frameTriggered( dt ); updateScene(); diff --git a/src/3d/qgs3dutils.cpp b/src/3d/qgs3dutils.cpp index db6010839948..c5141af3f6ea 100644 --- a/src/3d/qgs3dutils.cpp +++ b/src/3d/qgs3dutils.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) @@ -58,6 +59,24 @@ typedef Qt3DCore::QBuffer Qt3DQBuffer; // declared here as Qgs3DTypes has no cpp file const char *Qgs3DTypes::PROP_NAME_3D_RENDERER_FLAG = "PROP_NAME_3D_RENDERER_FLAG"; +void Qgs3DUtils::waitForFrame( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene ) +{ + // Set policy to always render frame, so we don't wait forever. + Qt3DRender::QRenderSettings::RenderPolicy oldPolicy = engine.renderSettings()->renderPolicy(); + engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::Always ); + + // Wait for at least one frame to render + Qt3DLogic::QFrameAction *frameAction = new Qt3DLogic::QFrameAction(); + scene->addComponent( frameAction ); + QEventLoop evLoop; + QObject::connect( frameAction, &Qt3DLogic::QFrameAction::triggered, &evLoop, &QEventLoop::quit ); + evLoop.exec(); + scene->removeComponent( frameAction ); + frameAction->deleteLater(); + + engine.renderSettings()->setRenderPolicy( oldPolicy ); +} + QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene ) { QImage resImage; @@ -66,13 +85,7 @@ QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene // We need to change render policy to RenderPolicy::Always, since otherwise render capture node won't work engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::Always ); - auto requestImageFcn = [&engine, scene] { - if ( scene->sceneState() == Qgs3DMapScene::Ready ) - { - engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::OnDemand ); - engine.requestCaptureImage(); - } - }; + waitForFrame( engine, scene ); auto saveImageFcn = [&evLoop, &resImage]( const QImage &img ) { resImage = img; @@ -82,6 +95,14 @@ QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene const QMetaObject::Connection conn1 = QObject::connect( &engine, &QgsAbstract3DEngine::imageCaptured, saveImageFcn ); QMetaObject::Connection conn2; + auto requestImageFcn = [&engine, scene] { + if ( scene->sceneState() == Qgs3DMapScene::Ready ) + { + engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::OnDemand ); + engine.requestCaptureImage(); + } + }; + if ( scene->sceneState() == Qgs3DMapScene::Ready ) { requestImageFcn(); @@ -126,6 +147,8 @@ QImage Qgs3DUtils::captureSceneDepthBuffer( QgsAbstract3DEngine &engine, Qgs3DMa QMetaObject::Connection conn1 = QObject::connect( &engine, &QgsAbstract3DEngine::depthBufferCaptured, saveImageFcn ); QMetaObject::Connection conn2; + // Make sure once-per-frame functions run + waitForFrame( engine, scene ); if ( scene->sceneState() == Qgs3DMapScene::Ready ) { requestImageFcn(); diff --git a/src/3d/qgs3dutils.h b/src/3d/qgs3dutils.h index e2689ecb01ab..bda8d4d9ab64 100644 --- a/src/3d/qgs3dutils.h +++ b/src/3d/qgs3dutils.h @@ -65,6 +65,12 @@ class _3D_EXPORT Qgs3DUtils */ static QImage captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene ); + /** + * Waits for a frame to be rendered. Useful to trigger once-per-frame updates + * \since QGIS 3.42 + */ + static void waitForFrame( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene ); + /** * Captures the depth buffer of the current 3D scene of a 3D engine. The function waits * until the scene is not fully loaded/updated before capturing the image. diff --git a/src/3d/qgscameracontroller.cpp b/src/3d/qgscameracontroller.cpp index b889a069a4e3..13e650f515be 100644 --- a/src/3d/qgscameracontroller.cpp +++ b/src/3d/qgscameracontroller.cpp @@ -15,6 +15,7 @@ #include "qgscameracontroller.h" #include "moc_qgscameracontroller.cpp" +#include "qgseventtracing.h" #include "qgsvector3d.h" #include "qgswindow3dengine.h" #include "qgs3dmapscene.h" @@ -161,6 +162,12 @@ void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosit void QgsCameraController::frameTriggered( float dt ) { Q_UNUSED( dt ) + + if ( mCameraChanged ) + { + emit cameraChanged(); + mCameraChanged = false; + } } void QgsCameraController::resetView( float distance ) @@ -233,7 +240,7 @@ void QgsCameraController::readXml( const QDomElement &elem ) setLookingAtPoint( QgsVector3D( x, elev, y ), dist, pitch, yaw ); } -double QgsCameraController::sampleDepthBuffer( const QImage &buffer, int px, int py ) +double QgsCameraController::sampleDepthBuffer( int px, int py ) { double depth = 1; @@ -242,9 +249,9 @@ double QgsCameraController::sampleDepthBuffer( const QImage &buffer, int px, int { for ( int y = py - 3; y <= py + 3; ++y ) { - if ( buffer.valid( x, y ) ) + if ( mDepthBufferImage.valid( x, y ) ) { - depth = std::min( depth, Qgs3DUtils::decodeDepth( buffer.pixel( x, y ) ) ); + depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) ); } } } @@ -252,14 +259,21 @@ double QgsCameraController::sampleDepthBuffer( const QImage &buffer, int px, int if ( depth < 1 ) return depth; + // Cache the computed depth, since averaging over all pixels can be expensive + if ( mDepthBufferNonVoidAverage != -1 ) + return mDepthBufferNonVoidAverage; + // Returns the average of depth values that are not 1 (void area) depth = 0; int samplesCount = 0; - for ( int x = 0; x < buffer.width(); ++x ) + // Make sure we can do the cast + Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 ); + for ( int y = 0; y < mDepthBufferImage.height(); ++y ) { - for ( int y = 0; y < buffer.height(); ++y ) + const QRgb *line = reinterpret_cast( mDepthBufferImage.constScanLine( y ) ); + for ( int x = 0; x < mDepthBufferImage.width(); ++x ) { - double d = Qgs3DUtils::decodeDepth( buffer.pixel( x, y ) ); + double d = Qgs3DUtils::decodeDepth( line[x] ); if ( d < 1 ) { depth += d; @@ -274,6 +288,8 @@ double QgsCameraController::sampleDepthBuffer( const QImage &buffer, int px, int else depth /= samplesCount; + mDepthBufferNonVoidAverage = depth; + return depth; } @@ -282,7 +298,7 @@ void QgsCameraController::updateCameraFromPose() if ( mCamera ) mCameraPose.updateCamera( mCamera ); - emit cameraChanged(); + mCameraChanged = true; } void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff ) @@ -296,6 +312,8 @@ void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse ) if ( !mInputHandlersEnabled ) return; + QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), QStringLiteral( "QgsCameraController::onPositionChanged" ) ); + switch ( mCameraNavigationMode ) { case Qgis::NavigationMode::TerrainBased: @@ -310,7 +328,7 @@ void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse ) bool QgsCameraController::screenPointToWorldPos( QPoint position, Qt3DRender::QCamera *mCameraBefore, double &depth, QVector3D &worldPosition ) { - depth = sampleDepthBuffer( mDepthBufferImage, position.x(), position.y() ); + depth = sampleDepthBuffer( position.x(), position.y() ); if ( !std::isfinite( depth ) ) { QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: depth is NaN or Inf. This should not happen." ), 2 ); @@ -557,6 +575,7 @@ void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel ) if ( mCurrentOperation != MouseOperation::ZoomWheel ) { setMouseParameters( MouseOperation::ZoomWheel ); + // The actual zooming will happen after we get a new depth buffer } else { @@ -1017,6 +1036,7 @@ void QgsCameraController::depthBufferCaptured( const QImage &depthImage ) { mDepthBufferImage = depthImage; mDepthBufferIsReady = true; + mDepthBufferNonVoidAverage = -1; if ( mCurrentOperation == MouseOperation::ZoomWheel ) { diff --git a/src/3d/qgscameracontroller.h b/src/3d/qgscameracontroller.h index 6bb158e7831f..e1b2e7304b6b 100644 --- a/src/3d/qgscameracontroller.h +++ b/src/3d/qgscameracontroller.h @@ -326,7 +326,7 @@ class _3D_EXPORT QgsCameraController : public QObject * Returns the minimum depth value in the square [px - 3, px + 3] * [py - 3, py + 3] * If the value is 1, the average depth of all non void pixels is returned instead. */ - double sampleDepthBuffer( const QImage &buffer, int px, int py ); + double sampleDepthBuffer( int px, int py ); #ifndef SIP_RUN //! Converts screen point to world position @@ -350,6 +350,9 @@ class _3D_EXPORT QgsCameraController : public QObject bool mDepthBufferIsReady = false; QImage mDepthBufferImage; + // -1 when unset + // TODO: Change to std::optional + double mDepthBufferNonVoidAverage = -1; std::unique_ptr mCameraBefore; @@ -385,6 +388,9 @@ class _3D_EXPORT QgsCameraController : public QObject // 3D world's origin in map coordinates QgsVector3D mOrigin; + //! Did camera change since last frame? Need to know if we should emit cameraChanged(). + bool mCameraChanged = false; + // To test the cameracontroller friend class TestQgs3DRendering; friend class TestQgs3DCameraController; diff --git a/tests/src/3d/testqgs3dcameracontroller.cpp b/tests/src/3d/testqgs3dcameracontroller.cpp index 0b61369c09e7..7e8f76d161df 100644 --- a/tests/src/3d/testqgs3dcameracontroller.cpp +++ b/tests/src/3d/testqgs3dcameracontroller.cpp @@ -52,6 +52,8 @@ class TestQgs3DCameraController : public QgsTest void testRotationCenterRotationCameraRotationCenter(); private: + void waitForNearPlane( QgsOffscreen3DEngine &engine, Qgs3DMapScene *scene, float atLeast ); //#spellok + QgsRasterLayer *mLayerRgb = nullptr; QgsVectorLayer *mLayerBuildings = nullptr; }; @@ -455,14 +457,13 @@ void TestQgs3DCameraController::testRotationCenterZoomWheelRotationCenter() // look from the top scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 0, 0 ); + waitForNearPlane( engine, scene, 1000 ); + QVector3D initialCamViewCenter = scene->cameraController()->camera()->viewCenter(); QVector3D initialCamPosition = scene->cameraController()->camera()->position(); float initialPitch = scene->cameraController()->pitch(); float initialYaw = scene->cameraController()->yaw(); - // this call is not used but ensures to synchronize the scene - Qgs3DUtils::captureSceneImage( engine, scene ); - QMouseEvent mousePressEvent( QEvent::MouseButtonPress, midPos, Qt::LeftButton, Qt::LeftButton, Qt::ShiftModifier ); scene->cameraController()->onMousePressed( new Qt3DInput::QMouseEvent( mousePressEvent ) ); @@ -522,9 +523,9 @@ void TestQgs3DCameraController::testRotationCenterZoomWheelRotationCenter() depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); scene->cameraController()->depthBufferCaptured( depthImage ); - QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( 283.2, -923.1, -27.0 ), 1.5 ); - QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( 99.4, -319.9, -8.8 ), 2.0 ); - QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1631.9, 2.0 ); + QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( 312.936, -950.772, -125.381 ), 3.0 ); + QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( 109.8, -329.4, -43.3 ), 3.0 ); + QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1631.9, 3.0 ); QCOMPARE( scene->cameraController()->pitch(), initialPitch ); QCOMPARE( scene->cameraController()->yaw(), initialYaw ); QCOMPARE( scene->cameraController()->mCumulatedWheelY, 0 ); @@ -540,6 +541,7 @@ void TestQgs3DCameraController::testRotationCenterZoomWheelRotationCenter() initialPitch = scene->cameraController()->pitch(); initialYaw = scene->cameraController()->yaw(); + Qgs3DUtils::waitForFrame( engine, scene ); // the first mouse event only updates the mouse position // the second one will update the camera QMouseEvent mouseMoveEvent3( QEvent::MouseMove, midPos + movement1 + movement2, Qt::LeftButton, Qt::LeftButton, Qt::ShiftModifier ); @@ -558,9 +560,9 @@ void TestQgs3DCameraController::testRotationCenterZoomWheelRotationCenter() QCOMPARE( scene->cameraController()->mCurrentOperation, QgsCameraController::MouseOperation::RotationCenter ); diffViewCenter = scene->cameraController()->camera()->viewCenter() - initialCamViewCenter; - QGSCOMPARENEARVECTOR3D( diffViewCenter, QVector3D( 25.9, 7.1, 5.2 ), 1.0 ); + QGSCOMPARENEARVECTOR3D( diffViewCenter, QVector3D( 26.9, 7.3, 5.4 ), 2.0 ); diffPosition = scene->cameraController()->camera()->position() - initialCamPosition; - QGSCOMPARENEARVECTOR3D( diffPosition, QVector3D( -44.3, -9.1, -11.7 ), 1.0 ); + QGSCOMPARENEARVECTOR3D( diffPosition, QVector3D( -43.2, -9.1, -11.1 ), 1.0 ); diffPitch = scene->cameraController()->pitch() - initialPitch; diffYaw = scene->cameraController()->yaw() - initialYaw; QGSCOMPARENEAR( diffPitch, 2.5, 0.1 ); @@ -598,14 +600,13 @@ void TestQgs3DCameraController::testTranslateRotationCenterTranslate() // look from the top scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 0, 0 ); + waitForNearPlane( engine, scene, 1000 ); + QVector3D initialCamViewCenter = scene->cameraController()->camera()->viewCenter(); QVector3D initialCamPosition = scene->cameraController()->camera()->position(); float initialPitch = scene->cameraController()->pitch(); float initialYaw = scene->cameraController()->yaw(); - // this call is not used but ensures to synchronize the scene - Qgs3DUtils::captureSceneImage( engine, scene ); - // // 1. Translate // @@ -751,14 +752,12 @@ void TestQgs3DCameraController::testTranslateZoomWheelTranslate() // look from the top scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 0, 0 ); + waitForNearPlane( engine, scene, 1000 ); QVector3D initialCamViewCenter = scene->cameraController()->camera()->viewCenter(); QVector3D initialCamPosition = scene->cameraController()->camera()->position(); float initialPitch = scene->cameraController()->pitch(); float initialYaw = scene->cameraController()->yaw(); - // this call is not used but ensures to synchronize the scene - Qgs3DUtils::captureSceneImage( engine, scene ); - // // 1. Translate // @@ -857,9 +856,9 @@ void TestQgs3DCameraController::testTranslateZoomWheelTranslate() QCOMPARE( scene->cameraController()->mCurrentOperation, QgsCameraController::MouseOperation::Translation ); diffViewCenter = scene->cameraController()->camera()->viewCenter() - initialCamViewCenter; - QGSCOMPARENEARVECTOR3D( diffViewCenter, QVector3D( -11.3, 11.3, 0.0 ), 1.0 ); + QGSCOMPARENEARVECTOR3D( diffViewCenter, QVector3D( -17.2, 17.2, 0.0 ), 1.0 ); diffPosition = scene->cameraController()->camera()->position() - initialCamPosition; - QGSCOMPARENEARVECTOR3D( diffPosition, QVector3D( -11.3, 11.3, 0.0 ), 1.0 ); + QGSCOMPARENEARVECTOR3D( diffPosition, QVector3D( -17.2, 17.2, 0.0 ), 1.0 ); QCOMPARE( scene->cameraController()->pitch(), initialPitch ); QCOMPARE( scene->cameraController()->yaw(), initialYaw ); @@ -1173,5 +1172,23 @@ void TestQgs3DCameraController::testRotationCenterRotationCameraRotationCenter() mapSettings->setLayers( {} ); } +void TestQgs3DCameraController::waitForNearPlane( QgsOffscreen3DEngine &engine, Qgs3DMapScene *scene, float atLeast ) //#spellok +{ + // XXX: Sometimes the near/far planes aren't calculated correctly, so they're + // left at the too-deep default. This causes the rest of the test to fail in + // weird ways every once in a while, so loop until we get good values. + size_t i = 0; + do + { + QVERIFY2( i++ < 10, "Near plane not set properly even after multiple tries" ); + + // Force recalcualtion of near/far planes. + scene->cameraController()->mCameraChanged = true; + + // this call is not used but ensures to synchronize the scene + Qgs3DUtils::captureSceneImage( engine, scene ); + } while ( scene->cameraController()->camera()->nearPlane() < atLeast ); //#spellok +} + QGSTEST_MAIN( TestQgs3DCameraController ) #include "testqgs3dcameracontroller.moc" diff --git a/tests/src/3d/testqgs3drendering.cpp b/tests/src/3d/testqgs3drendering.cpp index f129886a477f..583f4e637939 100644 --- a/tests/src/3d/testqgs3drendering.cpp +++ b/tests/src/3d/testqgs3drendering.cpp @@ -2114,6 +2114,7 @@ void TestQgs3DRendering::testDepthBuffer() QGSVERIFYIMAGECHECK( "depth_wheel_action_1", "depth_wheel_action_1", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); + Qgs3DUtils::waitForFrame( engine, scene ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( -32.7, -185.5, 224.6 ), 1.0 ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -6.8, -38.6, 46.7 ), 1.0 ); @@ -2131,6 +2132,7 @@ void TestQgs3DRendering::testDepthBuffer() QGSVERIFYIMAGECHECK( "depth_wheel_action_2", "depth_wheel_action_2", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); + Qgs3DUtils::waitForFrame( engine, scene ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( -32.5, -184.7, 223.5 ), 1.0 ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -12.1, -69.0, 83.5 ), 1.0 ); @@ -2148,6 +2150,7 @@ void TestQgs3DRendering::testDepthBuffer() QGSVERIFYIMAGECHECK( "depth_wheel_action_3", "depth_wheel_action_3", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); + Qgs3DUtils::waitForFrame( engine, scene ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( -32.4, -184.1, 222.8 ), 1.0 ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -29.0, -164.9, 199.6 ), 1.0 );