QGIS API Documentation 3.99.0-Master (a26b91b364d)
qgscameracontroller.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscameracontroller.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 "qgscameracontroller.h"
17#include "moc_qgscameracontroller.cpp"
18#include "qgseventtracing.h"
20#include "qgsvector3d.h"
21#include "qgswindow3dengine.h"
22#include "qgs3dmapscene.h"
23#include "qgsterrainentity.h"
24#include "qgis.h"
25#include "qgs3dutils.h"
26
27#include <QDomDocument>
28#include <Qt3DRender/QCamera>
29#include <Qt3DInput>
30#include <QStringLiteral>
31#include <QQuaternion>
32#include <cmath>
33
34#include "qgslogger.h"
35
37 : Qt3DCore::QEntity( scene )
38 , mScene( scene )
39 , mCamera( scene->engine()->camera() )
40 , mCameraBefore( new Qt3DRender::QCamera )
41 , mMouseHandler( new Qt3DInput::QMouseHandler )
42 , mKeyboardHandler( new Qt3DInput::QKeyboardHandler )
43 , mOrigin( scene->mapSettings()->origin() )
44{
45 mMouseHandler->setSourceDevice( new Qt3DInput::QMouseDevice() );
46 connect( mMouseHandler, &Qt3DInput::QMouseHandler::positionChanged, this, &QgsCameraController::onPositionChanged );
47 connect( mMouseHandler, &Qt3DInput::QMouseHandler::wheel, this, &QgsCameraController::onWheel );
48 connect( mMouseHandler, &Qt3DInput::QMouseHandler::pressed, this, &QgsCameraController::onMousePressed );
49 connect( mMouseHandler, &Qt3DInput::QMouseHandler::released, this, &QgsCameraController::onMouseReleased );
50 addComponent( mMouseHandler );
51
52 // Disable the handlers when the entity is disabled
53 connect( this, &Qt3DCore::QEntity::enabledChanged, mMouseHandler, &Qt3DInput::QMouseHandler::setEnabled );
54 connect( this, &Qt3DCore::QEntity::enabledChanged, mKeyboardHandler, &Qt3DInput::QKeyboardHandler::setEnabled );
55
56 mFpsNavTimer = new QTimer( this );
57 mFpsNavTimer->setInterval( 10 );
58 connect( mFpsNavTimer, &QTimer::timeout, this, &QgsCameraController::applyFlyModeKeyMovements );
59 mFpsNavTimer->start();
60
61 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
62 {
64 mGlobeCrsToLatLon = QgsCoordinateTransform( mScene->mapSettings()->crs(), mScene->mapSettings()->crs().toGeographicCrs(), mScene->mapSettings()->transformContext() );
65 }
66}
67
69
70QWindow *QgsCameraController::window() const
71{
72 QgsWindow3DEngine *windowEngine = qobject_cast<QgsWindow3DEngine *>( mScene->engine() );
73 return windowEngine ? windowEngine->window() : nullptr;
74}
75
77{
78 if ( navigationMode == mCameraNavigationMode )
79 return;
80
81 mCameraNavigationMode = navigationMode;
82 mIgnoreNextMouseMove = true;
83 emit navigationModeChanged( mCameraNavigationMode );
84}
85
87{
88 if ( movementSpeed == mCameraMovementSpeed )
89 return;
90
91 // If the speed becomes 0, navigation does not work anymore
92 // If the speed becomes too important, only one walk can move the view far from the scene.
93 mCameraMovementSpeed = std::clamp( movementSpeed, 0.05, 150.0 );
94 emit cameraMovementSpeedChanged( mCameraMovementSpeed );
95}
96
98{
99 mVerticalAxisInversion = inversion;
100}
101
102void QgsCameraController::rotateCamera( float diffPitch, float diffHeading )
103{
104 float newPitch = mCameraPose.pitchAngle() + diffPitch;
105 float newHeading = mCameraPose.headingAngle() + diffHeading;
106
107 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
108
109 switch ( mScene->mapSettings()->sceneMode() )
110 {
112 {
113 // When on a globe, we need to calculate "where is up" (the normal of a tangent plane).
114 // Also it uses different axes than the standard plane-based view.
115 // See QgsCameraPose::updateCameraGlobe(), we basically want to make sure
116 // that after an update, the camera stays in the same spot.
117 QgsVector3D viewCenterLatLon;
118 try
119 {
120 viewCenterLatLon = mGlobeCrsToLatLon.transform( mCameraPose.centerPoint() + mOrigin );
121 }
122 catch ( const QgsCsException & )
123 {
124 QgsDebugError( QStringLiteral( "rotateCamera: ECEF -> lat,lon transform failed!" ) );
125 return;
126 }
127 QQuaternion qLatLon = QQuaternion::fromAxisAndAngle( QVector3D( 0, 0, 1 ), static_cast<float>( viewCenterLatLon.x() ) )
128 * QQuaternion::fromAxisAndAngle( QVector3D( 0, -1, 0 ), static_cast<float>( viewCenterLatLon.y() ) );
129 QQuaternion qPitchHeading = QQuaternion::fromAxisAndAngle( QVector3D( 1, 0, 0 ), newHeading )
130 * QQuaternion::fromAxisAndAngle( QVector3D( 0, 1, 0 ), newPitch );
131 QVector3D newCameraToCenter = ( qLatLon * qPitchHeading * QVector3D( -1, 0, 0 ) ) * mCameraPose.distanceFromCenterPoint();
132
133 mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
134 mCameraPose.setPitchAngle( newPitch );
135 mCameraPose.setHeadingAngle( newHeading );
136 updateCameraFromPose();
137 return;
138 }
139
141 {
142 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
143 QVector3D newCameraToCenter = q * QVector3D( 0, 0, -mCameraPose.distanceFromCenterPoint() );
144 mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
145 mCameraPose.setPitchAngle( newPitch );
146 mCameraPose.setHeadingAngle( newHeading );
147 updateCameraFromPose();
148 return;
149 }
150 }
151}
152
153void QgsCameraController::rotateCameraAroundPivot( float newPitch, float newHeading, const QVector3D &pivotPoint )
154{
155 const float oldPitch = mCameraPose.pitchAngle();
156 const float oldHeading = mCameraPose.headingAngle();
157
158 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
159
160 // First undo the previously applied rotation, then apply the new rotation
161 // (We can't just apply our euler angles difference because the camera may be already rotated)
162 const QQuaternion qNew = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
163 const QQuaternion qOld = Qgs3DUtils::rotationFromPitchHeadingAngles( oldPitch, oldHeading );
164 const QQuaternion q = qNew * qOld.conjugated();
165
166 const QVector3D newViewCenter = q * ( mCamera->viewCenter() - pivotPoint ) + pivotPoint;
167
168 mCameraPose.setCenterPoint( newViewCenter );
169 mCameraPose.setPitchAngle( newPitch );
170 mCameraPose.setHeadingAngle( newHeading );
171 updateCameraFromPose();
172}
173
174void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint )
175{
176 // step 1: move camera along the line connecting reference camera position and our pivot point
177 QVector3D newCamPosition = pivotPoint + ( oldCameraPosition - pivotPoint ) * zoomFactor;
178 double newDistance = mCameraPose.distanceFromCenterPoint() * zoomFactor;
179
180 // step 2: using the new camera position and distance from center, calculate new view center
181 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( mCameraPose.pitchAngle(), mCameraPose.headingAngle() );
182 QVector3D cameraToCenter = q * QVector3D( 0, 0, -newDistance );
183 QVector3D newViewCenter = newCamPosition + cameraToCenter;
184
185 mCameraPose.setDistanceFromCenterPoint( newDistance );
186 mCameraPose.setCenterPoint( newViewCenter );
187 updateCameraFromPose();
188}
189
191{
192 Q_UNUSED( dt )
193
194 if ( mCameraChanged )
195 {
196 emit cameraChanged();
197 mCameraChanged = false;
198 }
199}
200
201void QgsCameraController::resetView( float distance )
202{
203 QgsPointXY extentCenter = mScene->mapSettings()->extent().center();
204 QgsVector3D origin = mScene->mapSettings()->origin();
205 setViewFromTop( extentCenter.x() - origin.x(), extentCenter.y() - origin.y(), distance );
206}
207
208void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
209{
210 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
211 {
212 QgsDebugError( QStringLiteral( "setViewFromTop() should not be used with globe!" ) );
213 return;
214 }
215
216 QgsCameraPose camPose;
217 QgsTerrainEntity *terrain = mScene->terrainEntity();
218 const float terrainElevationOffset = terrain ? terrain->terrainElevationOffset() : 0.0f;
219 camPose.setCenterPoint( QgsVector3D( worldX, worldY, terrainElevationOffset - mScene->mapSettings()->origin().z() ) );
221 camPose.setHeadingAngle( yaw );
222
223 // a basic setup to make frustum depth range long enough that it does not cull everything
224 mCamera->setNearPlane( distance / 2 );
225 mCamera->setFarPlane( distance * 2 );
226 // we force the updateCameraNearFarPlanes() in Qgs3DMapScene to properly set the planes
227 setCameraPose( camPose, true );
228}
229
231{
232 return mCameraPose.centerPoint();
233}
234
235void QgsCameraController::setLookingAtPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
236{
237 QgsCameraPose camPose;
238 camPose.setCenterPoint( point );
240 camPose.setPitchAngle( pitch );
241 camPose.setHeadingAngle( yaw );
242 setCameraPose( camPose );
243}
244
246{
247 return lookingAtPoint() + mOrigin;
248}
249
250void QgsCameraController::setLookingAtMapPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
251{
252 setLookingAtPoint( point - mOrigin, distance, pitch, yaw );
253}
254
255void QgsCameraController::setCameraPose( const QgsCameraPose &camPose, bool force )
256{
257 if ( camPose == mCameraPose && !force )
258 return;
259
260 mCameraPose = camPose;
261 updateCameraFromPose();
262}
263
264QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
265{
266 QDomElement elemCamera = doc.createElement( QStringLiteral( "camera" ) );
267 // Save center point in map coordinates, since our world origin won't be
268 // the same on loading
269 QgsVector3D centerPoint = mCameraPose.centerPoint() + mOrigin;
270 elemCamera.setAttribute( QStringLiteral( "xMap" ), centerPoint.x() );
271 elemCamera.setAttribute( QStringLiteral( "yMap" ), centerPoint.y() );
272 elemCamera.setAttribute( QStringLiteral( "zMap" ), centerPoint.z() );
273 elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraPose.distanceFromCenterPoint() );
274 elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraPose.pitchAngle() );
275 elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraPose.headingAngle() );
276 return elemCamera;
277}
278
279void QgsCameraController::readXml( const QDomElement &elem, QgsVector3D savedOrigin )
280{
281 const float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
282 const float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
283 const float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
284
285 QgsVector3D centerPoint;
286 if ( elem.hasAttribute( "xMap" ) )
287 {
288 // Prefer newer point saved in map coordinates ...
289 const double x = elem.attribute( QStringLiteral( "xMap" ) ).toDouble();
290 const double y = elem.attribute( QStringLiteral( "yMap" ) ).toDouble();
291 const double z = elem.attribute( QStringLiteral( "zMap" ) ).toDouble();
292 centerPoint = QgsVector3D( x, y, z ) - mOrigin;
293 }
294 else
295 {
296 // ... but allow use of older origin-relative coordinates.
297 const double x = elem.attribute( QStringLiteral( "x" ) ).toDouble();
298 const double y = elem.attribute( QStringLiteral( "y" ) ).toDouble();
299 const double elev = elem.attribute( QStringLiteral( "elev" ) ).toDouble();
300 centerPoint = QgsVector3D( x, elev, y ) - savedOrigin + mOrigin;
301 }
302 setLookingAtPoint( centerPoint, dist, pitch, yaw );
303}
304
305double QgsCameraController::sampleDepthBuffer( int px, int py )
306{
307 if ( !mDepthBufferIsReady )
308 {
309 QgsDebugError( QStringLiteral( "Asked to sample depth buffer, but depth buffer not ready!" ) );
310 }
311
312 double depth = 1;
313
314 if ( QWindow *win = window() )
315 {
316 // on high DPI screens, the mouse position is in device-independent pixels,
317 // but the depth buffer is in physical pixels...
318 px = static_cast<int>( px * win->devicePixelRatio() );
319 py = static_cast<int>( py * win->devicePixelRatio() );
320 }
321
322 // Sample the neighbouring pixels for the closest point to the camera
323 for ( int x = px - 3; x <= px + 3; ++x )
324 {
325 for ( int y = py - 3; y <= py + 3; ++y )
326 {
327 if ( mDepthBufferImage.valid( x, y ) )
328 {
329 depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) );
330 }
331 }
332 }
333 return depth;
334}
335
336double QgsCameraController::depthBufferNonVoidAverage()
337{
338 // Cache the computed depth, since averaging over all pixels can be expensive
339 if ( mDepthBufferNonVoidAverage != -1 )
340 return mDepthBufferNonVoidAverage;
341
342 // Returns the average of depth values that are not 1 (void area)
343 double depth = 0;
344 int samplesCount = 0;
345 // Make sure we can do the cast
346 Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 );
347 for ( int y = 0; y < mDepthBufferImage.height(); ++y )
348 {
349 const QRgb *line = reinterpret_cast<const QRgb *>( mDepthBufferImage.constScanLine( y ) );
350 for ( int x = 0; x < mDepthBufferImage.width(); ++x )
351 {
352 double d = Qgs3DUtils::decodeDepth( line[x] );
353 if ( d < 1 )
354 {
355 depth += d;
356 samplesCount += 1;
357 }
358 }
359 }
360
361 // if the whole buffer is white, a depth cannot be computed
362 if ( samplesCount == 0 )
363 depth = 1.0;
364 else
365 depth /= samplesCount;
366
367 mDepthBufferNonVoidAverage = depth;
368
369 return depth;
370}
371
372QgsVector3D QgsCameraController::moveGeocentricPoint( const QgsVector3D &point, double latDiff, double lonDiff )
373{
374 try
375 {
376 QgsVector3D pointLatLon = mGlobeCrsToLatLon.transform( point );
377 pointLatLon.setX( pointLatLon.x() + lonDiff );
378 pointLatLon.setY( std::clamp( pointLatLon.y() + latDiff, -90., 90. ) );
379
380 return mGlobeCrsToLatLon.transform( pointLatLon, Qgis::TransformDirection::Reverse );
381 }
382 catch ( const QgsCsException & )
383 {
384 QgsDebugError( QStringLiteral( "moveGeocentricPoint: transform failed!" ) );
385 return point;
386 }
387}
388
389void QgsCameraController::globeMoveCenterPoint( double latDiff, double lonDiff )
390{
391 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
392 const QgsVector3D newViewCenter = moveGeocentricPoint( viewCenter, latDiff, lonDiff );
393 mCameraPose.setCenterPoint( newViewCenter - mOrigin );
394 updateCameraFromPose();
395}
396
398{
399 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * factor );
400 updateCameraFromPose();
401}
402
404{
405 mCameraPose.setPitchAngle( std::clamp( mCameraPose.pitchAngle() + angleDiff, 0.f, 90.f ) );
406 updateCameraFromPose();
407}
408
410{
411 mCameraPose.setHeadingAngle( mCameraPose.headingAngle() + angleDiff );
412 updateCameraFromPose();
413}
414
415void QgsCameraController::resetGlobe( float distance, double lat, double lon )
416{
417 QgsVector3D mapPoint;
418 try
419 {
420 mapPoint = mGlobeCrsToLatLon.transform( QgsVector3D( lon, lat, 0 ), Qgis::TransformDirection::Reverse );
421 }
422 catch ( const QgsCsException & )
423 {
424 QgsDebugError( QStringLiteral( "resetGlobe: transform failed!" ) );
425 return;
426 }
427
428 QgsCameraPose cp;
429 cp.setCenterPoint( mapPoint - mOrigin );
431 setCameraPose( cp );
432}
433
434void QgsCameraController::updateCameraFromPose()
435{
436 if ( mCamera )
437 {
438 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
439 {
440 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
441
442 QgsVector3D viewCenterLatLon;
443 try
444 {
445 viewCenterLatLon = mGlobeCrsToLatLon.transform( viewCenter );
446 }
447 catch ( const QgsCsException & )
448 {
449 QgsDebugError( QStringLiteral( "updateCameraFromPose: transform failed!" ) );
450 return;
451 }
452
453 mCameraPose.updateCameraGlobe( mCamera, viewCenterLatLon.y(), viewCenterLatLon.x() );
454 }
455 else
456 {
457 mCameraPose.updateCamera( mCamera );
458 }
459 mCameraChanged = true;
460 }
461}
462
463void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff )
464{
465 mCameraPose.setCenterPoint( mCameraPose.centerPoint() + posDiff );
466 updateCameraFromPose();
467}
468
469void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
470{
471 if ( !mInputHandlersEnabled )
472 return;
473
474 QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), QStringLiteral( "QgsCameraController::onPositionChanged" ) );
475
476 switch ( mCameraNavigationMode )
477 {
479 onPositionChangedTerrainNavigation( mouse );
480 break;
481
483 onPositionChangedFlyNavigation( mouse );
484 break;
485
487 onPositionChangedGlobeTerrainNavigation( mouse );
488 break;
489 }
490}
491
492bool QgsCameraController::screenPointToWorldPos( QPoint position, double &depth, QVector3D &worldPosition )
493{
494 depth = sampleDepthBuffer( position.x(), position.y() );
495
496 // if there's nothing around the given position, try to get just any depth
497 // from the scene as a coarse approximation...
498 if ( depth == 1 )
499 {
500 depth = depthBufferNonVoidAverage();
501 }
502
503 worldPosition = Qgs3DUtils::screenPointToWorldPos( position, depth, mScene->engine()->size(), mDepthBufferCamera.get() );
504 if ( !std::isfinite( worldPosition.x() ) || !std::isfinite( worldPosition.y() ) || !std::isfinite( worldPosition.z() ) )
505 {
506 QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: position is NaN or Inf. This should not happen." ), 2 );
507 return false;
508 }
509
510 return true;
511}
512
513void QgsCameraController::onPositionChangedTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
514{
515 if ( mIgnoreNextMouseMove )
516 {
517 mIgnoreNextMouseMove = false;
518 mMousePos = QPoint( mouse->x(), mouse->y() );
519 return;
520 }
521
522 const int dx = mouse->x() - mMousePos.x();
523 const int dy = mouse->y() - mMousePos.y();
524
525 const bool hasShift = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier );
526 const bool hasCtrl = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier );
527 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
528 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
529 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
530
531 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
532 {
533 // rotate/tilt using mouse (camera moves as it rotates around the clicked point)
534 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
535
536 float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
537 float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
538 float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
539
540 if ( !mDepthBufferIsReady )
541 return;
542
543 if ( !mRotationCenterCalculated )
544 {
545 double depth;
546 QVector3D worldPosition;
547 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
548 {
549 mRotationCenter = worldPosition;
550 mRotationDistanceFromCenter = ( mRotationCenter - mCameraBefore->position() ).length();
551 emit cameraRotationCenterChanged( mRotationCenter );
552 mRotationCenterCalculated = true;
553 }
554 }
555
556 rotateCameraAroundPivot( mRotationPitch + pitchDiff, mRotationYaw + yawDiff, mRotationCenter );
557 }
558 else if ( hasLeftButton && hasCtrl && !hasShift )
559 {
560 setMouseParameters( MouseOperation::RotationCamera );
561 // rotate/tilt using mouse (camera stays at one position as it rotates)
562 const float diffPitch = 0.2f * dy;
563 const float diffYaw = -0.2f * dx;
564 rotateCamera( diffPitch, diffYaw );
565 }
566 else if ( hasLeftButton && !hasShift && !hasCtrl )
567 {
568 // translation works as if one grabbed a point on the 3D viewer and dragged it
569 setMouseParameters( MouseOperation::Translation, mMousePos );
570
571 if ( !mDepthBufferIsReady )
572 return;
573
574 if ( !mDragPointCalculated )
575 {
576 double depth;
577 QVector3D worldPosition;
578 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
579 {
580 mDragDepth = depth;
581 mDragPoint = worldPosition;
582 mDragPointCalculated = true;
583 }
584 }
585
586 QVector3D cameraBeforeDragPos = mCameraBefore->position();
587
588 QVector3D moveToPosition = Qgs3DUtils::screenPointToWorldPos( mMousePos, mDragDepth, mScene->engine()->size(), mCameraBefore.get() );
589 QVector3D cameraBeforeToMoveToPos = ( moveToPosition - mCameraBefore->position() ).normalized();
590 QVector3D cameraBeforeToDragPointPos = ( mDragPoint - mCameraBefore->position() ).normalized();
591
592 // Make sure the rays are not horizontal (add small z shift if it is)
593 if ( cameraBeforeToMoveToPos.z() == 0 )
594 {
595 cameraBeforeToMoveToPos.setZ( 0.01 );
596 cameraBeforeToMoveToPos = cameraBeforeToMoveToPos.normalized();
597 }
598
599 if ( cameraBeforeToDragPointPos.z() == 0 )
600 {
601 cameraBeforeToDragPointPos.setZ( 0.01 );
602 cameraBeforeToDragPointPos = cameraBeforeToDragPointPos.normalized();
603 }
604
605 double d1 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToMoveToPos.z();
606 double d2 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToDragPointPos.z();
607
608 QVector3D from = cameraBeforeDragPos + d1 * cameraBeforeToMoveToPos;
609 QVector3D to = cameraBeforeDragPos + d2 * cameraBeforeToDragPointPos;
610
611 QVector3D shiftVector = to - from;
612
613 mCameraPose.setCenterPoint( mCameraBefore->viewCenter() + shiftVector );
614 updateCameraFromPose();
615 }
616 else if ( hasLeftButton && hasShift && hasCtrl )
617 {
618 // change the camera elevation, similar to pageUp/pageDown
619 QgsVector3D center = mCameraPose.centerPoint();
620 double tElev = mMousePos.y() - mouse->y();
621 center.set( center.x(), center.y(), center.z() + tElev * 0.5 );
622 mCameraPose.setCenterPoint( center );
623 updateCameraFromPose();
624 }
625 else if ( hasRightButton && !hasShift && !hasCtrl )
626 {
627 setMouseParameters( MouseOperation::Zoom, mMousePos );
628 if ( !mDepthBufferIsReady )
629 return;
630
631 if ( !mDragPointCalculated )
632 {
633 double depth;
634 QVector3D worldPosition;
635 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
636 {
637 mDragPoint = worldPosition;
638 mDragPointCalculated = true;
639 }
640 }
641
642 float oldDist = ( mCameraBefore->position() - mDragPoint ).length();
643 float newDist = oldDist;
644
645 int yOffset = 0;
646 int screenHeight = mScene->engine()->size().height();
647 QWindow *win = window();
648 if ( win )
649 {
650 yOffset = win->mapToGlobal( QPoint( 0, 0 ) ).y();
651 screenHeight = win->screen()->size().height();
652 }
653
654 // Applies smoothing
655 if ( mMousePos.y() > mClickPoint.y() ) // zoom in
656 {
657 double f = ( double ) ( mMousePos.y() - mClickPoint.y() ) / ( double ) ( screenHeight - mClickPoint.y() - yOffset );
658 f = std::max( 0.0, std::min( 1.0, f ) );
659 f = 1 - ( std::expm1( -2 * f ) ) / ( std::expm1( -2 ) );
660 newDist = newDist * f;
661 }
662 else // zoom out
663 {
664 double f = 1 - ( double ) ( mMousePos.y() + yOffset ) / ( double ) ( mClickPoint.y() + yOffset );
665 f = std::max( 0.0, std::min( 1.0, f ) );
666 f = ( std::expm1( 2 * f ) ) / ( std::expm1( 2 ) );
667 newDist = newDist + 2 * newDist * f;
668 }
669
670 double zoomFactor = newDist / oldDist;
671 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mDragPoint );
672 }
673
674 mMousePos = QPoint( mouse->x(), mouse->y() );
675}
676
677void QgsCameraController::onPositionChangedGlobeTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
678{
679 const bool hasShift = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier );
680 const bool hasCtrl = ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier );
681 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
682 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
683
684 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
685 {
686 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
687
688 const float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
689 const float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
690 const float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
691
692 mCameraPose.setPitchAngle( mRotationPitch + pitchDiff );
693 mCameraPose.setHeadingAngle( mRotationYaw + yawDiff );
694 updateCameraFromPose();
695 return;
696 }
697
698 if ( !( mouse->buttons() & Qt::LeftButton ) )
699 return;
700
701 // translation works as if one grabbed a point on the 3D viewer and dragged it
702 setMouseParameters( MouseOperation::Translation, mMousePos );
703
704 if ( !mDepthBufferIsReady )
705 return;
706
707 if ( !mDragPointCalculated )
708 {
709 double depth;
710 QVector3D worldPosition;
711 if ( !screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
712 return;
713
714 mDragDepth = depth;
715 mDragPoint = worldPosition;
716 mDragPointCalculated = true;
717 }
718
719 // Approximate the globe as a sphere with a center in (0,0,0) map coords and
720 // of radius the same as at startPosMap.
721 const QgsVector3D startPosMap = QgsVector3D( mDragPoint ) + mOrigin;
722 const double sphereRadiusMap = startPosMap.length();
723 // Find the intersection of this sphere and the ray from the current clicked point.
724 const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( QPoint( mouse->x(), mouse->y() ), mScene->engine()->size(), mCameraBefore.get() );
725 const QgsVector3D rayOriginMap = QgsVector3D( ray.origin() ) + mOrigin;
726 // From equations of ray and sphere
727 const double quadA = QVector3D::dotProduct( ray.direction(), ray.direction() );
728 const double quadB = 2 * QgsVector3D::dotProduct( ray.direction(), rayOriginMap );
729 const double quadC = QgsVector3D::dotProduct( rayOriginMap, rayOriginMap ) - sphereRadiusMap * sphereRadiusMap;
730 const double disc = quadB * quadB - 4 * quadA * quadC;
731 if ( disc < 0 )
732 // Ray misses sphere
733 return;
734 // Distance to intersection along ray (take smaller root, closer to camera)
735 const double rayDistMap = ( -quadB - sqrt( disc ) ) / ( 2 * quadA );
736 if ( rayDistMap < 0 )
737 {
738 QgsDebugError( QStringLiteral( "Sphere intersection result negative, cancelling move" ) );
739 return;
740 }
741 const QgsVector3D newPosMap = rayOriginMap + QgsVector3D( ray.direction() ) * rayDistMap;
742
743 // now that we have old and new mouse position in ECEF coordinates,
744 // let's figure out the difference in lat/lon angles and update the center point
745
746 QgsVector3D oldLatLon, newLatLon;
747 try
748 {
749 oldLatLon = mGlobeCrsToLatLon.transform( startPosMap );
750 newLatLon = mGlobeCrsToLatLon.transform( newPosMap );
751 }
752 catch ( const QgsCsException & )
753 {
754 QgsDebugError( QStringLiteral( "onPositionChangedGlobeTerrainNavigation: transform failed!" ) );
755 return;
756 }
757
758 const double latDiff = oldLatLon.y() - newLatLon.y();
759 const double lonDiff = oldLatLon.x() - newLatLon.x();
760
761 const QgsVector3D newVC = moveGeocentricPoint( mMousePressViewCenter, latDiff, lonDiff );
762 const QgsVector3D newVCWorld = newVC - mOrigin;
763
764 mCameraPose.setCenterPoint( newVCWorld );
765 updateCameraFromPose();
766}
767
768
769void QgsCameraController::zoom( float factor )
770{
771 // zoom in/out
772 float dist = mCameraPose.distanceFromCenterPoint();
773 dist -= dist * factor * 0.01f;
774 mCameraPose.setDistanceFromCenterPoint( dist );
775 updateCameraFromPose();
776}
777
778void QgsCameraController::handleTerrainNavigationWheelZoom()
779{
780 if ( !mDepthBufferIsReady )
781 return;
782
783 if ( !mZoomPointCalculated )
784 {
785 double depth;
786 QVector3D worldPosition;
787 if ( screenPointToWorldPos( mMousePos, depth, worldPosition ) )
788 {
789 mZoomPoint = worldPosition;
790 mZoomPointCalculated = true;
791 }
792 }
793
794 double oldDist = ( mZoomPoint - mCameraBefore->position() ).length();
795 // Each step of the scroll wheel decreases distance by 20%
796 double newDist = std::pow( 0.8, mCumulatedWheelY ) * oldDist;
797 // Make sure we don't clip the thing we're zooming to.
798 newDist = std::max( newDist, 2.0 );
799 double zoomFactor = newDist / oldDist;
800 // Don't change the distance too suddenly to hopefully prevent numerical instability
801 zoomFactor = std::clamp( zoomFactor, 0.01, 100.0 );
802
803 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mZoomPoint );
804
805 mCumulatedWheelY = 0;
806 setMouseParameters( MouseOperation::None );
807}
808
809void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
810{
811 if ( !mInputHandlersEnabled )
812 return;
813
814 switch ( mCameraNavigationMode )
815 {
817 {
818 const float scaling = ( ( wheel->modifiers() & Qt3DInput::QWheelEvent::Modifiers::ControlModifier ) != 0 ? 0.1f : 1.0f ) / 1000.f;
819 setCameraMovementSpeed( mCameraMovementSpeed + mCameraMovementSpeed * scaling * wheel->angleDelta().y() );
820 break;
821 }
822
824 {
825 // Scale our variable to roughly "number of normal steps", with Ctrl
826 // increasing granularity 10x
827 const double scaling = ( 1.0 / 120.0 ) * ( ( wheel->modifiers() & Qt3DInput::QWheelEvent::Modifiers::ControlModifier ) != 0 ? 0.1 : 1.0 );
828
829 // Apparently angleDelta needs to be accumulated
830 // see: https://doc.qt.io/qt-5/qwheelevent.html#angleDelta
831 mCumulatedWheelY += scaling * wheel->angleDelta().y();
832
833 if ( mCurrentOperation != MouseOperation::ZoomWheel )
834 {
835 setMouseParameters( MouseOperation::ZoomWheel );
836 // The actual zooming will happen after we get a new depth buffer
837 }
838 else
839 {
840 handleTerrainNavigationWheelZoom();
841 }
842 break;
843 }
844
846 {
847 float wheelAmount = static_cast<float>( wheel->angleDelta().y() );
848 float factor = abs( wheelAmount ) / 1000.f;
849 float mulFactor = wheelAmount > 0 ? ( 1 - factor ) : ( 1 + factor );
850 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * mulFactor );
851 updateCameraFromPose();
852 break;
853 }
854 }
855}
856
857void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
858{
859 if ( !mInputHandlersEnabled )
860 return;
861
862 mKeyboardHandler->setFocus( true );
863
864 if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton || ( ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) || ( ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) )
865 {
866 mMousePos = QPoint( mouse->x(), mouse->y() );
867
868 if ( mCaptureFpsMouseMovements )
869 mIgnoreNextMouseMove = true;
870
871 const MouseOperation operation {
872 ( mouse->modifiers() & Qt3DInput::QMouseEvent::Modifiers::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ? MouseOperation::RotationCamera : MouseOperation::RotationCenter
873 };
874 setMouseParameters( operation, mMousePos );
875 }
876
877 else if ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton || mouse->button() == Qt3DInput::QMouseEvent::RightButton )
878 {
879 mMousePos = QPoint( mouse->x(), mouse->y() );
880
881 if ( mCaptureFpsMouseMovements )
882 mIgnoreNextMouseMove = true;
883
884 const MouseOperation operation = ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ? MouseOperation::Translation : MouseOperation::Zoom;
885 setMouseParameters( operation, mMousePos );
886 }
887}
888
889void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
890{
891 Q_UNUSED( mouse )
892 if ( !mInputHandlersEnabled )
893 return;
894
895
896 setMouseParameters( MouseOperation::None );
897}
898
899bool QgsCameraController::onKeyPressedTerrainNavigation( QKeyEvent *event )
900{
901 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
902 const bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
903
904 int tx = 0, ty = 0, tElev = 0;
905 switch ( event->key() )
906 {
907 case Qt::Key_Left:
908 tx -= 1;
909 break;
910 case Qt::Key_Right:
911 tx += 1;
912 break;
913
914 case Qt::Key_Up:
915 ty += 1;
916 break;
917 case Qt::Key_Down:
918 ty -= 1;
919 break;
920
921 case Qt::Key_PageDown:
922 tElev -= 1;
923 break;
924 case Qt::Key_PageUp:
925 tElev += 1;
926 break;
927 default:
928 break;
929 }
930
931 if ( tx || ty )
932 {
933 if ( !hasShift && !hasCtrl )
934 {
935 moveView( tx, ty );
936 }
937 else if ( hasShift && !hasCtrl )
938 {
939 // rotate/tilt using keyboard (camera moves as it rotates around its view center)
942 }
943 else if ( hasCtrl && !hasShift )
944 {
945 // rotate/tilt using keyboard (camera stays at one position as it rotates)
946 const float diffPitch = ty; // down key = rotating camera down
947 const float diffYaw = -tx; // right key = rotating camera to the right
948 rotateCamera( diffPitch, diffYaw );
949 }
950 return true;
951 }
952
953 if ( tElev )
954 {
955 QgsVector3D center = mCameraPose.centerPoint();
956 center.set( center.x(), center.y(), center.z() + tElev * 10 );
957 mCameraPose.setCenterPoint( center );
958 return true;
959 updateCameraFromPose();
960 }
961
962 return false;
963}
964
965bool QgsCameraController::onKeyPressedGlobeTerrainNavigation( QKeyEvent *event )
966{
967 // both move factor and zoom factor are just empirically picked numbers
968 // that seem to work well (providing steps that are not too big / not too small)
969 constexpr float MOVE_FACTOR = 0.000001f; // multiplied by distance to get angle
970 constexpr float ZOOM_FACTOR = 0.9f;
971
972 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
973
974 switch ( event->key() )
975 {
976 case Qt::Key_Left:
977 if ( hasShift )
979 else
980 globeMoveCenterPoint( 0, -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
981 return true;
982 case Qt::Key_Right:
983 if ( hasShift )
985 else
986 globeMoveCenterPoint( 0, MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
987 return true;
988 case Qt::Key_Up:
989 if ( hasShift )
991 else
992 globeMoveCenterPoint( MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
993 return true;
994 case Qt::Key_Down:
995 if ( hasShift )
997 else
998 globeMoveCenterPoint( -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
999 return true;
1000 case Qt::Key_PageDown:
1001 globeZoom( ZOOM_FACTOR );
1002 return true;
1003 case Qt::Key_PageUp:
1004 globeZoom( 1 / ZOOM_FACTOR );
1005 return true;
1006 default:
1007 break;
1008 }
1009 return false;
1010}
1011
1012static const QSet<int> walkNavigationSavedKeys = {
1013 Qt::Key_Left,
1014 Qt::Key_A,
1015 Qt::Key_Right,
1016 Qt::Key_D,
1017 Qt::Key_Up,
1018 Qt::Key_W,
1019 Qt::Key_Down,
1020 Qt::Key_S,
1021 Qt::Key_PageUp,
1022 Qt::Key_E,
1023 Qt::Key_PageDown,
1024 Qt::Key_Q,
1025};
1026
1027bool QgsCameraController::onKeyPressedFlyNavigation( QKeyEvent *event )
1028{
1029 switch ( event->key() )
1030 {
1031 case Qt::Key_QuoteLeft:
1032 {
1033 // toggle mouse lock mode
1034 mCaptureFpsMouseMovements = !mCaptureFpsMouseMovements;
1035 mIgnoreNextMouseMove = true;
1036 if ( mCaptureFpsMouseMovements )
1037 {
1038 qApp->setOverrideCursor( QCursor( Qt::BlankCursor ) );
1039 }
1040 else
1041 {
1042 qApp->restoreOverrideCursor();
1043 }
1044 event->accept();
1045 return true;
1046 }
1047
1048 case Qt::Key_Escape:
1049 {
1050 // always exit mouse lock mode
1051 if ( mCaptureFpsMouseMovements )
1052 {
1053 mCaptureFpsMouseMovements = false;
1054 mIgnoreNextMouseMove = true;
1055 qApp->restoreOverrideCursor();
1056 event->accept();
1057 return true;
1058 }
1059 break;
1060 }
1061 default:
1062 break;
1063 }
1064
1065 if ( walkNavigationSavedKeys.contains( event->key() ) )
1066 {
1067 if ( !event->isAutoRepeat() )
1068 {
1069 mDepressedKeys.insert( event->key() );
1070 }
1071 event->accept();
1072 return true;
1073 }
1074 return false;
1075}
1076
1077void QgsCameraController::walkView( double tx, double ty, double tz )
1078{
1079 const QVector3D cameraUp = mCamera->upVector().normalized();
1080 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1081 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1082
1083 QVector3D cameraPosDiff( 0.0f, 0.0f, 0.0f );
1084
1085 if ( tx != 0.0 )
1086 {
1087 cameraPosDiff += static_cast<float>( tx ) * cameraFront;
1088 }
1089 if ( ty != 0.0 )
1090 {
1091 cameraPosDiff += static_cast<float>( ty ) * cameraLeft;
1092 }
1093 if ( tz != 0.0 )
1094 {
1095 cameraPosDiff += static_cast<float>( tz ) * QVector3D( 0.0f, 0.0f, 1.0f );
1096 }
1097
1098 moveCameraPositionBy( cameraPosDiff );
1099}
1100
1101void QgsCameraController::applyFlyModeKeyMovements()
1102{
1103 if ( mCameraNavigationMode != Qgis::NavigationMode::Walk )
1104 return;
1105
1106 // shift = "run", ctrl = "slow walk"
1107 const bool shiftPressed = mDepressedKeys.contains( Qt::Key_Shift );
1108 const bool ctrlPressed = mDepressedKeys.contains( Qt::Key_Control );
1109
1110 const double movementSpeed = mCameraMovementSpeed * ( shiftPressed ? 2 : 1 ) * ( ctrlPressed ? 0.1 : 1 );
1111
1112 bool changed = false;
1113 double x = 0.0;
1114 double y = 0.0;
1115 double z = 0.0;
1116 if ( mDepressedKeys.contains( Qt::Key_Left ) || mDepressedKeys.contains( Qt::Key_A ) )
1117 {
1118 changed = true;
1119 y += movementSpeed;
1120 }
1121
1122 if ( mDepressedKeys.contains( Qt::Key_Right ) || mDepressedKeys.contains( Qt::Key_D ) )
1123 {
1124 changed = true;
1125 y -= movementSpeed;
1126 }
1127
1128 if ( mDepressedKeys.contains( Qt::Key_Up ) || mDepressedKeys.contains( Qt::Key_W ) )
1129 {
1130 changed = true;
1131 x += movementSpeed;
1132 }
1133
1134 if ( mDepressedKeys.contains( Qt::Key_Down ) || mDepressedKeys.contains( Qt::Key_S ) )
1135 {
1136 changed = true;
1137 x -= movementSpeed;
1138 }
1139
1140 // note -- vertical axis movements are slower by default then horizontal ones, as GIS projects
1141 // tend to have much more limited elevation range vs ground range
1142 static constexpr double ELEVATION_MOVEMENT_SCALE = 0.5;
1143 if ( mDepressedKeys.contains( Qt::Key_PageUp ) || mDepressedKeys.contains( Qt::Key_E ) )
1144 {
1145 changed = true;
1146 z += ELEVATION_MOVEMENT_SCALE * movementSpeed;
1147 }
1148
1149 if ( mDepressedKeys.contains( Qt::Key_PageDown ) || mDepressedKeys.contains( Qt::Key_Q ) )
1150 {
1151 changed = true;
1152 z -= ELEVATION_MOVEMENT_SCALE * movementSpeed;
1153 }
1154
1155 if ( changed )
1156 walkView( x, y, z );
1157}
1158
1159void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent *mouse )
1160{
1161 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
1162 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
1163
1164 const double dx = mCaptureFpsMouseMovements ? QCursor::pos().x() - mMousePos.x() : mouse->x() - mMousePos.x();
1165 const double dy = mCaptureFpsMouseMovements ? QCursor::pos().y() - mMousePos.y() : mouse->y() - mMousePos.y();
1166 mMousePos = mCaptureFpsMouseMovements ? QCursor::pos() : QPoint( mouse->x(), mouse->y() );
1167
1168 if ( mIgnoreNextMouseMove )
1169 {
1170 mIgnoreNextMouseMove = false;
1171 return;
1172 }
1173
1174 if ( hasMiddleButton )
1175 {
1176 // middle button drag = pan camera in place (strafe)
1177 const QVector3D cameraUp = mCamera->upVector().normalized();
1178 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1179 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1180 const QVector3D cameraPosDiff = -dx * cameraLeft - dy * cameraUp;
1181 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 10.0 );
1182 }
1183 else if ( hasRightButton )
1184 {
1185 // right button drag = camera dolly
1186 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1187 const QVector3D cameraPosDiff = dy * cameraFront;
1188 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 5.0 );
1189 }
1190 else
1191 {
1192 if ( mCaptureFpsMouseMovements )
1193 {
1194 float diffPitch = -0.2f * dy;
1195 switch ( mVerticalAxisInversion )
1196 {
1198 diffPitch *= -1;
1199 break;
1200
1203 break;
1204 }
1205
1206 const float diffYaw = -0.2f * dx;
1207 rotateCamera( diffPitch, diffYaw );
1208 }
1209 else if ( mouse->buttons() & Qt::LeftButton )
1210 {
1211 float diffPitch = -0.2f * dy;
1212 switch ( mVerticalAxisInversion )
1213 {
1216 diffPitch *= -1;
1217 break;
1218
1220 break;
1221 }
1222 const float diffYaw = -0.2f * dx;
1223 rotateCamera( diffPitch, diffYaw );
1224 }
1225 }
1226
1227 if ( mCaptureFpsMouseMovements )
1228 {
1229 mIgnoreNextMouseMove = true;
1230
1231 // reset cursor back to center of map widget
1232 emit setCursorPosition( QPoint( mScene->engine()->size().width() / 2, mScene->engine()->size().height() / 2 ) );
1233 }
1234}
1235
1237{
1238 // Tilt up the view by deltaPitch around the view center (camera moves)
1239 float pitch = mCameraPose.pitchAngle();
1240 pitch -= deltaPitch; // down key = moving camera toward terrain
1241 mCameraPose.setPitchAngle( pitch );
1242 updateCameraFromPose();
1243}
1244
1246{
1247 // Rotate clockwise the view by deltaYaw around the view center (camera moves)
1248 float yaw = mCameraPose.headingAngle();
1249 yaw -= deltaYaw; // right key = moving camera clockwise
1250 mCameraPose.setHeadingAngle( yaw );
1251 updateCameraFromPose();
1252}
1253
1255{
1256 mCameraPose.setHeadingAngle( angle );
1257 updateCameraFromPose();
1258}
1259
1260void QgsCameraController::moveView( float tx, float ty )
1261{
1262 const float yaw = mCameraPose.headingAngle();
1263 const float dist = mCameraPose.distanceFromCenterPoint();
1264 const float x = tx * dist * 0.02f;
1265 const float y = -ty * dist * 0.02f;
1266
1267 // moving with keyboard - take into account yaw of camera
1268 const float t = sqrt( x * x + y * y );
1269 const float a = atan2( y, x ) - yaw * M_PI / 180;
1270 const float dx = cos( a ) * t;
1271 const float dy = sin( a ) * t;
1272
1273 QgsVector3D center = mCameraPose.centerPoint();
1274 center.set( center.x() + dx, center.y() - dy, center.z() );
1275 mCameraPose.setCenterPoint( center );
1276 updateCameraFromPose();
1277}
1278
1280{
1281 if ( !mInputHandlersEnabled )
1282 return false;
1283
1284 if ( event->type() == QKeyEvent::Type::KeyRelease )
1285 {
1286 if ( !event->isAutoRepeat() && mDepressedKeys.contains( event->key() ) )
1287 {
1288 mDepressedKeys.remove( event->key() );
1289 return true;
1290 }
1291 }
1292 else if ( event->type() == QEvent::ShortcutOverride )
1293 {
1294 if ( event->modifiers() & Qt::ControlModifier )
1295 {
1296 switch ( event->key() )
1297 {
1298 case Qt::Key_QuoteLeft:
1299 {
1300 // switch navigation mode
1301 switch ( mCameraNavigationMode )
1302 {
1308 );
1309 break;
1313 break;
1314 }
1315 event->accept();
1316 return true;
1317 }
1318
1319 // Make sure to sync the key combinations with strings in Qgs3DAxis::createMenu()!
1320 case Qt::Key_8:
1322 return true;
1323 case Qt::Key_6:
1325 return true;
1326 case Qt::Key_2:
1328 return true;
1329 case Qt::Key_4:
1331 return true;
1332 case Qt::Key_9:
1334 return true;
1335 case Qt::Key_3:
1337 return true;
1338 case Qt::Key_5:
1340 return true;
1341
1342 default:
1343 break;
1344 }
1345 }
1346
1347 switch ( mCameraNavigationMode )
1348 {
1350 return onKeyPressedFlyNavigation( event );
1351
1353 return onKeyPressedTerrainNavigation( event );
1354
1356 return onKeyPressedGlobeTerrainNavigation( event );
1357 }
1358 }
1359 return false;
1360}
1361
1362void QgsCameraController::depthBufferCaptured( const QImage &depthImage )
1363{
1364 mDepthBufferImage = depthImage;
1365 mDepthBufferIsReady = true;
1366 mDepthBufferNonVoidAverage = -1;
1367
1368 // To read distances from the captured depth buffer, we need to know the
1369 // camera parameters it was rendered with. This seems like the closest
1370 // place to save them, though I have no idea if they can't be changed
1371 // between the rendering and now anyway...
1372 mDepthBufferCamera = Qgs3DUtils::copyCamera( mCamera );
1373
1374 if ( mCurrentOperation == MouseOperation::ZoomWheel )
1375 {
1376 handleTerrainNavigationWheelZoom();
1377 }
1378}
1379
1380bool QgsCameraController::isATranslationRotationSequence( MouseOperation newOperation ) const
1381{
1382 return std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), newOperation ) != std::end( mTranslateOrRotate ) && std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), mCurrentOperation ) != std::end( mTranslateOrRotate );
1383}
1384
1385void QgsCameraController::setMouseParameters( const MouseOperation &newOperation, const QPoint &clickPoint )
1386{
1387 if ( newOperation == mCurrentOperation )
1388 {
1389 return;
1390 }
1391
1392 if ( newOperation == MouseOperation::None )
1393 {
1394 mClickPoint = QPoint();
1395 }
1396 // click point and rotation angles are updated if:
1397 // - it has never been computed
1398 // - the current and new operations are both rotation and translation
1399 // Indeed, if the sequence such as rotation -> zoom -> rotation updating mClickPoint on
1400 // the click point does not need to be updated because the relative mouse position is kept
1401 // during a zoom operation
1402 else if ( mClickPoint.isNull() || isATranslationRotationSequence( newOperation ) )
1403 {
1404 mClickPoint = clickPoint;
1405 mRotationPitch = mCameraPose.pitchAngle();
1406 mRotationYaw = mCameraPose.headingAngle();
1407 }
1408 mCurrentOperation = newOperation;
1409 mDepthBufferIsReady = false;
1410 mRotationCenterCalculated = false;
1411 mDragPointCalculated = false;
1412 mZoomPointCalculated = false;
1413
1414 if ( mCurrentOperation != MouseOperation::None && mCurrentOperation != MouseOperation::RotationCamera )
1415 {
1416 mMousePressViewCenter = mCameraPose.centerPoint() + mOrigin;
1417 mCameraBefore = Qgs3DUtils::copyCamera( mCamera );
1418
1420 }
1421}
1422
1424{
1425 QgsVector3D diff = origin - mOrigin;
1426 mCameraPose.setCenterPoint( mCameraPose.centerPoint() - diff );
1427
1428 // update other members that depend on world coordinates
1429 mCameraBefore->setPosition( ( QgsVector3D( mCameraBefore->position() ) - diff ).toVector3D() );
1430 mCameraBefore->setViewCenter( ( QgsVector3D( mCameraBefore->viewCenter() ) - diff ).toVector3D() );
1431 mDragPoint = ( QgsVector3D( mDragPoint ) - diff ).toVector3D();
1432 mRotationCenter = ( QgsVector3D( mRotationCenter ) - diff ).toVector3D();
1433
1434 mOrigin = origin;
1435
1436 updateCameraFromPose();
1437}
1438
1439void QgsCameraController::rotateToRespectingTerrain( float pitch, float yaw )
1440{
1442 double elevation = 0.0;
1443 if ( mScene->mapSettings()->terrainRenderingEnabled() )
1444 {
1445 QgsDebugMsgLevel( "Checking elevation from terrain...", 2 );
1446 QVector3D camPos = mCamera->position();
1447 QgsRayCastingUtils::Ray3D ray( camPos, pos.toVector3D() - camPos, mCamera->farPlane() );
1448 const QVector<QgsRayCastingUtils::RayHit> hits = mScene->terrainEntity()->rayIntersection( ray, QgsRayCastingUtils::RayCastContext() );
1449 if ( !hits.isEmpty() )
1450 {
1451 elevation = hits.at( 0 ).pos.z();
1452 QgsDebugMsgLevel( QString( "Computed elevation from terrain: %1" ).arg( elevation ), 2 );
1453 }
1454 else
1455 {
1456 QgsDebugMsgLevel( "Unable to obtain elevation from terrain", 2 );
1457 }
1458 }
1459 pos.set( pos.x(), pos.y(), elevation + mScene->terrainEntity()->terrainElevationOffset() );
1460
1461 setLookingAtPoint( pos, ( mCamera->position() - pos.toVector3D() ).length(), pitch, yaw );
1462}
VerticalAxisInversion
Vertical axis inversion options for 3D views.
Definition qgis.h:4081
@ Always
Always invert vertical axis movements.
@ Never
Never invert vertical axis movements.
@ WhenDragging
Invert vertical axis movements when dragging in first person modes.
NavigationMode
The navigation mode used by 3D cameras.
Definition qgis.h:4056
@ TerrainBased
The default navigation based on the terrain.
@ Walk
Uses WASD keys or arrows to navigate in walking (first person) manner.
@ GlobeTerrainBased
Navigation similar to TerrainBased, but for use with globe.
@ Globe
Scene is represented as a globe using a geocentric CRS.
@ Local
Local scene based on a projected CRS.
@ Reverse
Reverse/inverse transform (from destination to source)
Entity that encapsulates our 3D scene - contains all other entities (such as terrain) as children.
Qgs3DMapSettings * mapSettings() const
Returns the 3D map settings.
QgsAbstract3DEngine * engine() const
Returns the abstract 3D engine.
QgsTerrainEntity * terrainEntity()
Returns terrain entity (may be nullptr if using globe scene, terrain rendering is disabled or when te...
Qgis::NavigationMode cameraNavigationMode() const
Returns the navigation mode used by the camera.
Qgis::SceneMode sceneMode() const
Returns mode of the 3D scene - whether it is represented as a globe (when using Geocentric CRS such a...
QgsRectangle extent() const
Returns the 3D scene's 2D extent in the 3D scene's CRS.
bool terrainRenderingEnabled() const
Returns whether the 2D terrain surface will be rendered.
QgsCoordinateReferenceSystem crs() const
Returns coordinate reference system used in the 3D scene.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which stores various information regarding which datum tran...
QgsVector3D origin() const
Returns coordinates in map CRS at which 3D scene has origin (0,0,0).
static QQuaternion rotationFromPitchHeadingAngles(float pitchAngle, float headingAngle)
Returns rotation quaternion that performs rotation around X axis by pitchAngle, followed by rotation ...
static std::unique_ptr< Qt3DRender::QCamera > copyCamera(Qt3DRender::QCamera *cam)
Returns new camera object with copied properties.
static double decodeDepth(const QRgb &pixel)
Decodes the depth value from the pixel's color value The depth value is encoded from OpenGL side (the...
Definition qgs3dutils.h:243
static QVector3D screenPointToWorldPos(const QPoint &screenPoint, double depth, const QSize &screenSize, Qt3DRender::QCamera *camera)
Converts the clicked mouse position to the corresponding 3D world coordinates.
static QgsRay3D rayFromScreenPoint(const QPoint &point, const QSize &windowSize, Qt3DRender::QCamera *camera)
Convert from clicked point on the screen to a ray in world coordinates.
virtual QSize size() const =0
Returns size of the engine's rendering area in pixels.
void navigationModeChanged(Qgis::NavigationMode mode)
Emitted when the navigation mode is changed using the hotkey ctrl + ~.
void rotateCameraToBottom()
Rotate to bottom-up view.
void setLookingAtMapPoint(const QgsVector3D &point, float distance, float pitch, float yaw)
Sets camera configuration like setLookingAtPoint(), but the point is given in map coordinates.
float pitch() const
Returns pitch angle in degrees (0 = looking from the top, 90 = looking from the side).
~QgsCameraController() override
float yaw() const
Returns yaw angle in degrees.
void requestDepthBufferCapture()
Emitted to ask for the depth buffer image.
void rotateCameraToTop()
Rotate to top-down view.
void tiltUpAroundViewCenter(float deltaPitch)
Tilt up the view by deltaPitch around the view center (camera moves)
void globeMoveCenterPoint(double latDiff, double lonDiff)
Orbits camera around the globe by the specified amount given as the difference in latitude/longitude ...
void rotateCameraToEast()
Rotate to view from the east.
void setVerticalAxisInversion(Qgis::VerticalAxisInversion inversion)
Sets the vertical axis inversion behavior.
const QgsVector3D origin() const
Returns the origin of the scene in map coordinates.
void rotateCameraToHome()
Rotate to diagonal view.
void rotateCameraToNorth()
Rotate to view from the north.
float distance() const
Returns distance of the camera from the point it is looking at.
void globeUpdatePitchAngle(float angleDiff)
Updates pitch angle by the specified amount given as the angular difference in degrees.
void zoomCameraAroundPivot(const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint)
Zooms camera by given zoom factor (>1 one means zoom in) while keeping the pivot point (given in worl...
void globeZoom(float factor)
Moves camera closer or further away from the globe.
void rotateCamera(float diffPitch, float diffYaw)
Rotates the camera on itself.
void rotateCameraToWest()
Rotate to view from the west.
void setCameraNavigationMode(Qgis::NavigationMode navigationMode)
Sets the navigation mode used by the camera controller.
void cameraChanged()
Emitted when camera has been updated.
void cameraMovementSpeedChanged(double speed)
Emitted whenever the camera movement speed is changed by the controller.
QgsCameraController(Qgs3DMapScene *scene)
Constructs the camera controller with optional parent node that will take ownership.
void frameTriggered(float dt)
Called internally from 3D scene when a new frame is generated. Updates camera according to keyboard/m...
void resetView(float distance)
Move camera back to the initial position (looking down towards origin of world's coordinates)
void setLookingAtPoint(const QgsVector3D &point, float distance, float pitch, float yaw)
Sets the complete camera configuration: the point towards it is looking (in 3D world coordinates),...
QgsVector3D lookingAtPoint() const
Returns the point in the world coordinates towards which the camera is looking.
QgsVector3D lookingAtMapPoint() const
Returns the point in the map coordinates towards which the camera is looking.
void rotateCameraToSouth()
Rotate to view from the south.
QDomElement writeXml(QDomDocument &doc) const
Writes camera configuration to the given DOM element.
void setViewFromTop(float worldX, float worldY, float distance, float yaw=0)
Sets camera to look down towards given point in world coordinate, in given distance from plane with z...
bool keyboardEventFilter(QKeyEvent *event)
If the event is relevant, handles the event and returns true, otherwise false.
void resetGlobe(float distance, double lat=0, double lon=0)
Resets view of the globe to look at a particular location given as latitude and longitude (in degrees...
void readXml(const QDomElement &elem, QgsVector3D savedOrigin)
Reads camera configuration from the given DOM element.
void zoom(float factor)
Zoom the map by factor.
void globeUpdateHeadingAngle(float angleDiff)
Updates heading angle by the specified amount given as the angular difference in degrees.
void rotateAroundViewCenter(float deltaYaw)
Rotate clockwise the view by deltaYaw around the view center (camera moves)
void walkView(double tx, double ty, double tz)
Walks into the map by tx, ty, and tz.
void setCameraPose(const QgsCameraPose &camPose, bool force=false)
Sets camera pose.
void setOrigin(const QgsVector3D &origin)
Reacts to the shift of origin of the scene, updating camera pose and any other member variables so th...
void setCameraHeadingAngle(float angle)
Set camera heading to angle (used for rotating the view)
void setCameraMovementSpeed(double movementSpeed)
Sets the camera movement speed.
void rotateCameraAroundPivot(float newPitch, float newHeading, const QVector3D &pivotPoint)
Rotates the camera around the pivot point (in world coordinates) to the given new pitch and heading a...
void depthBufferCaptured(const QImage &depthImage)
Sets the depth buffer image used by the camera controller to calculate world position from a pixel's ...
void cameraRotationCenterChanged(QVector3D position)
Emitted when the camera rotation center changes.
void setCursorPosition(QPoint point)
Emitted when the mouse cursor position should be moved to the specified point on the map viewport.
void moveView(float tx, float ty)
Move the map by tx and ty.
Encapsulates camera pose in a 3D scene.
float headingAngle() const
Returns heading (yaw) angle in degrees.
QgsVector3D centerPoint() const
Returns center point (towards which point the camera is looking)
float pitchAngle() const
Returns pitch angle in degrees.
void updateCameraGlobe(Qt3DRender::QCamera *camera, double lat, double lon)
Updates camera when using a globe scene.
float distanceFromCenterPoint() const
Returns distance of the camera from the center point.
void setPitchAngle(float pitch)
Sets pitch angle in degrees.
void setCenterPoint(const QgsVector3D &point)
Sets center point (towards which point the camera is looking)
void setHeadingAngle(float heading)
Sets heading (yaw) angle in degrees.
void setDistanceFromCenterPoint(float distance)
Sets distance of the camera from the center point.
void updateCamera(Qt3DRender::QCamera *camera)
Update Qt3D camera view matrix based on the pose.
QgsCoordinateReferenceSystem toGeographicCrs() const
Returns the geographic CRS associated with this CRS object.
Handles coordinate transforms between two coordinate systems.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Represents a 2D point.
Definition qgspointxy.h:60
double y
Definition qgspointxy.h:64
double x
Definition qgspointxy.h:63
A representation of a ray in 3D.
Definition qgsray3d.h:31
QVector3D origin() const
Returns the origin of the ray.
Definition qgsray3d.h:44
QVector3D direction() const
Returns the direction of the ray see setDirection()
Definition qgsray3d.h:50
QgsPointXY center
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:30
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:49
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:51
QVector3D toVector3D() const
Converts the current object to QVector3D.
static double dotProduct(const QgsVector3D &v1, const QgsVector3D &v2)
Returns the dot product of two vectors.
double x() const
Returns X coordinate.
Definition qgsvector3d.h:47
void setX(double x)
Sets X coordinate.
Definition qgsvector3d.h:57
void set(double x, double y, double z)
Sets vector coordinates.
Definition qgsvector3d.h:72
void setY(double y)
Sets Y coordinate.
Definition qgsvector3d.h:63
double length() const
Returns the length of the vector.
On-screen 3D engine: it creates an OpenGL window (QWindow) and displays rendered 3D scenes there.
QWindow * window()
Returns the internal 3D window where all the rendered output is displayed.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40
Helper struct to store ray casting parameters.