QGIS API Documentation 3.99.0-Master (e69e8341d6a)
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 QgsVector3D centerPoint;
268 switch ( mScene->mapSettings()->sceneMode() )
269 {
271 centerPoint = mCameraPose.centerPoint();
272 break;
274 // Save center point in map coordinates, since our world origin won't be
275 // the same on loading
276 centerPoint = mCameraPose.centerPoint() + mOrigin;
277 break;
278 }
279 elemCamera.setAttribute( QStringLiteral( "x" ), centerPoint.x() );
280 elemCamera.setAttribute( QStringLiteral( "y" ), centerPoint.z() );
281 elemCamera.setAttribute( QStringLiteral( "elev" ), centerPoint.y() );
282 elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraPose.distanceFromCenterPoint() );
283 elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraPose.pitchAngle() );
284 elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraPose.headingAngle() );
285 return elemCamera;
286}
287
288void QgsCameraController::readXml( const QDomElement &elem )
289{
290 const float x = elem.attribute( QStringLiteral( "x" ) ).toFloat();
291 const float y = elem.attribute( QStringLiteral( "y" ) ).toFloat();
292 const float elev = elem.attribute( QStringLiteral( "elev" ) ).toFloat();
293 const float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
294 const float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
295 const float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
296 QgsVector3D centerPoint( x, elev, y );
297 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
298 centerPoint = centerPoint - mOrigin;
299 setLookingAtPoint( centerPoint, dist, pitch, yaw );
300}
301
302double QgsCameraController::sampleDepthBuffer( int px, int py )
303{
304 if ( !mDepthBufferIsReady )
305 {
306 QgsDebugError( QStringLiteral( "Asked to sample depth buffer, but depth buffer not ready!" ) );
307 }
308
309 double depth = 1;
310
311 if ( QWindow *win = window() )
312 {
313 // on high DPI screens, the mouse position is in device-independent pixels,
314 // but the depth buffer is in physical pixels...
315 px = static_cast<int>( px * win->devicePixelRatio() );
316 py = static_cast<int>( py * win->devicePixelRatio() );
317 }
318
319 // Sample the neighbouring pixels for the closest point to the camera
320 for ( int x = px - 3; x <= px + 3; ++x )
321 {
322 for ( int y = py - 3; y <= py + 3; ++y )
323 {
324 if ( mDepthBufferImage.valid( x, y ) )
325 {
326 depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) );
327 }
328 }
329 }
330 return depth;
331}
332
333double QgsCameraController::depthBufferNonVoidAverage()
334{
335 // Cache the computed depth, since averaging over all pixels can be expensive
336 if ( mDepthBufferNonVoidAverage != -1 )
337 return mDepthBufferNonVoidAverage;
338
339 // Returns the average of depth values that are not 1 (void area)
340 double depth = 0;
341 int samplesCount = 0;
342 // Make sure we can do the cast
343 Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 );
344 for ( int y = 0; y < mDepthBufferImage.height(); ++y )
345 {
346 const QRgb *line = reinterpret_cast<const QRgb *>( mDepthBufferImage.constScanLine( y ) );
347 for ( int x = 0; x < mDepthBufferImage.width(); ++x )
348 {
349 double d = Qgs3DUtils::decodeDepth( line[x] );
350 if ( d < 1 )
351 {
352 depth += d;
353 samplesCount += 1;
354 }
355 }
356 }
357
358 // if the whole buffer is white, a depth cannot be computed
359 if ( samplesCount == 0 )
360 depth = 1.0;
361 else
362 depth /= samplesCount;
363
364 mDepthBufferNonVoidAverage = depth;
365
366 return depth;
367}
368
369QgsVector3D QgsCameraController::moveGeocentricPoint( const QgsVector3D &point, double latDiff, double lonDiff )
370{
371 try
372 {
373 QgsVector3D pointLatLon = mGlobeCrsToLatLon.transform( point );
374 pointLatLon.setX( pointLatLon.x() + lonDiff );
375 pointLatLon.setY( std::clamp( pointLatLon.y() + latDiff, -90., 90. ) );
376
377 return mGlobeCrsToLatLon.transform( pointLatLon, Qgis::TransformDirection::Reverse );
378 }
379 catch ( const QgsCsException & )
380 {
381 QgsDebugError( QStringLiteral( "moveGeocentricPoint: transform failed!" ) );
382 return point;
383 }
384}
385
386void QgsCameraController::globeMoveCenterPoint( double latDiff, double lonDiff )
387{
388 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
389 const QgsVector3D newViewCenter = moveGeocentricPoint( viewCenter, latDiff, lonDiff );
390 mCameraPose.setCenterPoint( newViewCenter - mOrigin );
391 updateCameraFromPose();
392}
393
395{
396 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * factor );
397 updateCameraFromPose();
398}
399
401{
402 mCameraPose.setPitchAngle( std::clamp( mCameraPose.pitchAngle() + angleDiff, 0.f, 90.f ) );
403 updateCameraFromPose();
404}
405
407{
408 mCameraPose.setHeadingAngle( mCameraPose.headingAngle() + angleDiff );
409 updateCameraFromPose();
410}
411
412void QgsCameraController::resetGlobe( float distance, double lat, double lon )
413{
414 QgsVector3D mapPoint;
415 try
416 {
417 mapPoint = mGlobeCrsToLatLon.transform( QgsVector3D( lon, lat, 0 ), Qgis::TransformDirection::Reverse );
418 }
419 catch ( const QgsCsException & )
420 {
421 QgsDebugError( QStringLiteral( "resetGlobe: transform failed!" ) );
422 return;
423 }
424
425 QgsCameraPose cp;
426 cp.setCenterPoint( mapPoint - mOrigin );
428 setCameraPose( cp );
429}
430
431void QgsCameraController::updateCameraFromPose()
432{
433 if ( mCamera )
434 {
435 if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
436 {
437 const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
438
439 QgsVector3D viewCenterLatLon;
440 try
441 {
442 viewCenterLatLon = mGlobeCrsToLatLon.transform( viewCenter );
443 }
444 catch ( const QgsCsException & )
445 {
446 QgsDebugError( QStringLiteral( "updateCameraFromPose: transform failed!" ) );
447 return;
448 }
449
450 mCameraPose.updateCameraGlobe( mCamera, viewCenterLatLon.y(), viewCenterLatLon.x() );
451 }
452 else
453 {
454 mCameraPose.updateCamera( mCamera );
455 }
456 mCameraChanged = true;
457 }
458}
459
460void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff )
461{
462 mCameraPose.setCenterPoint( mCameraPose.centerPoint() + posDiff );
463 updateCameraFromPose();
464}
465
466void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
467{
468 if ( !mInputHandlersEnabled )
469 return;
470
471 QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), QStringLiteral( "QgsCameraController::onPositionChanged" ) );
472
473 switch ( mCameraNavigationMode )
474 {
476 onPositionChangedTerrainNavigation( mouse );
477 break;
478
480 onPositionChangedFlyNavigation( mouse );
481 break;
482
484 onPositionChangedGlobeTerrainNavigation( mouse );
485 break;
486 }
487}
488
489bool QgsCameraController::screenPointToWorldPos( QPoint position, double &depth, QVector3D &worldPosition )
490{
491 depth = sampleDepthBuffer( position.x(), position.y() );
492
493 // if there's nothing around the given position, try to get just any depth
494 // from the scene as a coarse approximation...
495 if ( depth == 1 )
496 {
497 depth = depthBufferNonVoidAverage();
498 }
499
500 worldPosition = Qgs3DUtils::screenPointToWorldPos( position, depth, mScene->engine()->size(), mDepthBufferCamera.get() );
501 if ( !std::isfinite( worldPosition.x() ) || !std::isfinite( worldPosition.y() ) || !std::isfinite( worldPosition.z() ) )
502 {
503 QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: position is NaN or Inf. This should not happen." ), 2 );
504 return false;
505 }
506
507 return true;
508}
509
510void QgsCameraController::onPositionChangedTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
511{
512 if ( mIgnoreNextMouseMove )
513 {
514 mIgnoreNextMouseMove = false;
515 mMousePos = QPoint( mouse->x(), mouse->y() );
516 return;
517 }
518
519 const int dx = mouse->x() - mMousePos.x();
520 const int dy = mouse->y() - mMousePos.y();
521
522 const bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
523 const bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
524 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
525 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
526 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
527
528 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
529 {
530 // rotate/tilt using mouse (camera moves as it rotates around the clicked point)
531 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
532
533 float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
534 float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
535 float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
536
537 if ( !mDepthBufferIsReady )
538 return;
539
540 if ( !mRotationCenterCalculated )
541 {
542 double depth;
543 QVector3D worldPosition;
544 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
545 {
546 mRotationCenter = worldPosition;
547 mRotationDistanceFromCenter = ( mRotationCenter - mCameraBefore->position() ).length();
548 emit cameraRotationCenterChanged( mRotationCenter );
549 mRotationCenterCalculated = true;
550 }
551 }
552
553 rotateCameraAroundPivot( mRotationPitch + pitchDiff, mRotationYaw + yawDiff, mRotationCenter );
554 }
555 else if ( hasLeftButton && hasCtrl && !hasShift )
556 {
557 setMouseParameters( MouseOperation::RotationCamera );
558 // rotate/tilt using mouse (camera stays at one position as it rotates)
559 const float diffPitch = 0.2f * dy;
560 const float diffYaw = -0.2f * dx;
561 rotateCamera( diffPitch, diffYaw );
562 }
563 else if ( hasLeftButton && !hasShift && !hasCtrl )
564 {
565 // translation works as if one grabbed a point on the 3D viewer and dragged it
566 setMouseParameters( MouseOperation::Translation, mMousePos );
567
568 if ( !mDepthBufferIsReady )
569 return;
570
571 if ( !mDragPointCalculated )
572 {
573 double depth;
574 QVector3D worldPosition;
575 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
576 {
577 mDragDepth = depth;
578 mDragPoint = worldPosition;
579 mDragPointCalculated = true;
580 }
581 }
582
583 QVector3D cameraBeforeDragPos = mCameraBefore->position();
584
585 QVector3D moveToPosition = Qgs3DUtils::screenPointToWorldPos( mMousePos, mDragDepth, mScene->engine()->size(), mCameraBefore.get() );
586 QVector3D cameraBeforeToMoveToPos = ( moveToPosition - mCameraBefore->position() ).normalized();
587 QVector3D cameraBeforeToDragPointPos = ( mDragPoint - mCameraBefore->position() ).normalized();
588
589 // Make sure the rays are not horizontal (add small z shift if it is)
590 if ( cameraBeforeToMoveToPos.z() == 0 )
591 {
592 cameraBeforeToMoveToPos.setZ( 0.01 );
593 cameraBeforeToMoveToPos = cameraBeforeToMoveToPos.normalized();
594 }
595
596 if ( cameraBeforeToDragPointPos.z() == 0 )
597 {
598 cameraBeforeToDragPointPos.setZ( 0.01 );
599 cameraBeforeToDragPointPos = cameraBeforeToDragPointPos.normalized();
600 }
601
602 double d1 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToMoveToPos.z();
603 double d2 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToDragPointPos.z();
604
605 QVector3D from = cameraBeforeDragPos + d1 * cameraBeforeToMoveToPos;
606 QVector3D to = cameraBeforeDragPos + d2 * cameraBeforeToDragPointPos;
607
608 QVector3D shiftVector = to - from;
609
610 mCameraPose.setCenterPoint( mCameraBefore->viewCenter() + shiftVector );
611 updateCameraFromPose();
612 }
613 else if ( hasLeftButton && hasShift && hasCtrl )
614 {
615 // change the camera elevation, similar to pageUp/pageDown
616 QgsVector3D center = mCameraPose.centerPoint();
617 double tElev = mMousePos.y() - mouse->y();
618 center.set( center.x(), center.y(), center.z() + tElev * 0.5 );
619 mCameraPose.setCenterPoint( center );
620 updateCameraFromPose();
621 }
622 else if ( hasRightButton && !hasShift && !hasCtrl )
623 {
624 setMouseParameters( MouseOperation::Zoom, mMousePos );
625 if ( !mDepthBufferIsReady )
626 return;
627
628 if ( !mDragPointCalculated )
629 {
630 double depth;
631 QVector3D worldPosition;
632 if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
633 {
634 mDragPoint = worldPosition;
635 mDragPointCalculated = true;
636 }
637 }
638
639 float oldDist = ( mCameraBefore->position() - mDragPoint ).length();
640 float newDist = oldDist;
641
642 int yOffset = 0;
643 int screenHeight = mScene->engine()->size().height();
644 QWindow *win = window();
645 if ( win )
646 {
647 yOffset = win->mapToGlobal( QPoint( 0, 0 ) ).y();
648 screenHeight = win->screen()->size().height();
649 }
650
651 // Applies smoothing
652 if ( mMousePos.y() > mClickPoint.y() ) // zoom in
653 {
654 double f = ( double ) ( mMousePos.y() - mClickPoint.y() ) / ( double ) ( screenHeight - mClickPoint.y() - yOffset );
655 f = std::max( 0.0, std::min( 1.0, f ) );
656 f = 1 - ( std::expm1( -2 * f ) ) / ( std::expm1( -2 ) );
657 newDist = newDist * f;
658 }
659 else // zoom out
660 {
661 double f = 1 - ( double ) ( mMousePos.y() + yOffset ) / ( double ) ( mClickPoint.y() + yOffset );
662 f = std::max( 0.0, std::min( 1.0, f ) );
663 f = ( std::expm1( 2 * f ) ) / ( std::expm1( 2 ) );
664 newDist = newDist + 2 * newDist * f;
665 }
666
667 double zoomFactor = newDist / oldDist;
668 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mDragPoint );
669 }
670
671 mMousePos = QPoint( mouse->x(), mouse->y() );
672}
673
674void QgsCameraController::onPositionChangedGlobeTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
675{
676 const bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
677 const bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
678 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
679 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
680
681 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
682 {
683 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
684
685 const float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
686 const float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
687 const float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
688
689 mCameraPose.setPitchAngle( mRotationPitch + pitchDiff );
690 mCameraPose.setHeadingAngle( mRotationYaw + yawDiff );
691 updateCameraFromPose();
692 return;
693 }
694
695 if ( !( mouse->buttons() & Qt::LeftButton ) )
696 return;
697
698 // translation works as if one grabbed a point on the 3D viewer and dragged it
699 setMouseParameters( MouseOperation::Translation, mMousePos );
700
701 if ( !mDepthBufferIsReady )
702 return;
703
704 if ( !mDragPointCalculated )
705 {
706 double depth;
707 QVector3D worldPosition;
708 if ( !screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
709 return;
710
711 mDragDepth = depth;
712 mDragPoint = worldPosition;
713 mDragPointCalculated = true;
714 }
715
716 // Approximate the globe as a sphere with a center in (0,0,0) map coords and
717 // of radius the same as at startPosMap.
718 const QgsVector3D startPosMap = QgsVector3D( mDragPoint ) + mOrigin;
719 const double sphereRadiusMap = startPosMap.length();
720 // Find the intersection of this sphere and the ray from the current clicked point.
721 const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( QPoint( mouse->x(), mouse->y() ), mScene->engine()->size(), mCameraBefore.get() );
722 const QgsVector3D rayOriginMap = QgsVector3D( ray.origin() ) + mOrigin;
723 // From equations of ray and sphere
724 const double quadA = QVector3D::dotProduct( ray.direction(), ray.direction() );
725 const double quadB = 2 * QgsVector3D::dotProduct( ray.direction(), rayOriginMap );
726 const double quadC = QgsVector3D::dotProduct( rayOriginMap, rayOriginMap ) - sphereRadiusMap * sphereRadiusMap;
727 const double disc = quadB * quadB - 4 * quadA * quadC;
728 if ( disc < 0 )
729 // Ray misses sphere
730 return;
731 // Distance to intersection along ray (take smaller root, closer to camera)
732 const double rayDistMap = ( -quadB - sqrt( disc ) ) / ( 2 * quadA );
733 if ( rayDistMap < 0 )
734 {
735 QgsDebugError( QStringLiteral( "Sphere intersection result negative, cancelling move" ) );
736 return;
737 }
738 const QgsVector3D newPosMap = rayOriginMap + QgsVector3D( ray.direction() ) * rayDistMap;
739
740 // now that we have old and new mouse position in ECEF coordinates,
741 // let's figure out the difference in lat/lon angles and update the center point
742
743 QgsVector3D oldLatLon, newLatLon;
744 try
745 {
746 oldLatLon = mGlobeCrsToLatLon.transform( startPosMap );
747 newLatLon = mGlobeCrsToLatLon.transform( newPosMap );
748 }
749 catch ( const QgsCsException & )
750 {
751 QgsDebugError( QStringLiteral( "onPositionChangedGlobeTerrainNavigation: transform failed!" ) );
752 return;
753 }
754
755 const double latDiff = oldLatLon.y() - newLatLon.y();
756 const double lonDiff = oldLatLon.x() - newLatLon.x();
757
758 const QgsVector3D newVC = moveGeocentricPoint( mMousePressViewCenter, latDiff, lonDiff );
759 const QgsVector3D newVCWorld = newVC - mOrigin;
760
761 mCameraPose.setCenterPoint( newVCWorld );
762 updateCameraFromPose();
763}
764
765
766void QgsCameraController::zoom( float factor )
767{
768 // zoom in/out
769 float dist = mCameraPose.distanceFromCenterPoint();
770 dist -= dist * factor * 0.01f;
771 mCameraPose.setDistanceFromCenterPoint( dist );
772 updateCameraFromPose();
773}
774
775void QgsCameraController::handleTerrainNavigationWheelZoom()
776{
777 if ( !mDepthBufferIsReady )
778 return;
779
780 if ( !mZoomPointCalculated )
781 {
782 double depth;
783 QVector3D worldPosition;
784 if ( screenPointToWorldPos( mMousePos, depth, worldPosition ) )
785 {
786 mZoomPoint = worldPosition;
787 mZoomPointCalculated = true;
788 }
789 }
790
791 double oldDist = ( mZoomPoint - mCameraBefore->position() ).length();
792 // Each step of the scroll wheel decreases distance by 20%
793 double newDist = std::pow( 0.8, mCumulatedWheelY ) * oldDist;
794 // Make sure we don't clip the thing we're zooming to.
795 newDist = std::max( newDist, 2.0 );
796 double zoomFactor = newDist / oldDist;
797 // Don't change the distance too suddenly to hopefully prevent numerical instability
798 zoomFactor = std::clamp( zoomFactor, 0.01, 100.0 );
799
800 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mZoomPoint );
801
802 mCumulatedWheelY = 0;
803 setMouseParameters( MouseOperation::None );
804}
805
806void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
807{
808 if ( !mInputHandlersEnabled )
809 return;
810
811 switch ( mCameraNavigationMode )
812 {
814 {
815 const float scaling = ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1f : 1.0f ) / 1000.f;
816 setCameraMovementSpeed( mCameraMovementSpeed + mCameraMovementSpeed * scaling * wheel->angleDelta().y() );
817 break;
818 }
819
821 {
822 // Scale our variable to roughly "number of normal steps", with Ctrl
823 // increasing granularity 10x
824 const double scaling = ( 1.0 / 120.0 ) * ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1 : 1.0 );
825
826 // Apparently angleDelta needs to be accumulated
827 // see: https://doc.qt.io/qt-5/qwheelevent.html#angleDelta
828 mCumulatedWheelY += scaling * wheel->angleDelta().y();
829
830 if ( mCurrentOperation != MouseOperation::ZoomWheel )
831 {
832 setMouseParameters( MouseOperation::ZoomWheel );
833 // The actual zooming will happen after we get a new depth buffer
834 }
835 else
836 {
837 handleTerrainNavigationWheelZoom();
838 }
839 break;
840 }
841
843 {
844 float wheelAmount = static_cast<float>( wheel->angleDelta().y() );
845 float factor = abs( wheelAmount ) / 1000.f;
846 float mulFactor = wheelAmount > 0 ? ( 1 - factor ) : ( 1 + factor );
847 mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * mulFactor );
848 updateCameraFromPose();
849 break;
850 }
851 }
852}
853
854void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
855{
856 if ( !mInputHandlersEnabled )
857 return;
858
859 mKeyboardHandler->setFocus( true );
860
861 if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton || ( ( mouse->modifiers() & Qt::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) || ( ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) )
862 {
863 mMousePos = QPoint( mouse->x(), mouse->y() );
864
865 if ( mCaptureFpsMouseMovements )
866 mIgnoreNextMouseMove = true;
867
868 const MouseOperation operation {
869 ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ? MouseOperation::RotationCamera : MouseOperation::RotationCenter
870 };
871 setMouseParameters( operation, mMousePos );
872 }
873
874 else if ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton || mouse->button() == Qt3DInput::QMouseEvent::RightButton )
875 {
876 mMousePos = QPoint( mouse->x(), mouse->y() );
877
878 if ( mCaptureFpsMouseMovements )
879 mIgnoreNextMouseMove = true;
880
881 const MouseOperation operation = ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ? MouseOperation::Translation : MouseOperation::Zoom;
882 setMouseParameters( operation, mMousePos );
883 }
884}
885
886void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
887{
888 Q_UNUSED( mouse )
889 if ( !mInputHandlersEnabled )
890 return;
891
892
893 setMouseParameters( MouseOperation::None );
894}
895
896bool QgsCameraController::onKeyPressedTerrainNavigation( QKeyEvent *event )
897{
898 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
899 const bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
900
901 int tx = 0, ty = 0, tElev = 0;
902 switch ( event->key() )
903 {
904 case Qt::Key_Left:
905 tx -= 1;
906 break;
907 case Qt::Key_Right:
908 tx += 1;
909 break;
910
911 case Qt::Key_Up:
912 ty += 1;
913 break;
914 case Qt::Key_Down:
915 ty -= 1;
916 break;
917
918 case Qt::Key_PageDown:
919 tElev -= 1;
920 break;
921 case Qt::Key_PageUp:
922 tElev += 1;
923 break;
924 default:
925 break;
926 }
927
928 if ( tx || ty )
929 {
930 if ( !hasShift && !hasCtrl )
931 {
932 moveView( tx, ty );
933 }
934 else if ( hasShift && !hasCtrl )
935 {
936 // rotate/tilt using keyboard (camera moves as it rotates around its view center)
939 }
940 else if ( hasCtrl && !hasShift )
941 {
942 // rotate/tilt using keyboard (camera stays at one position as it rotates)
943 const float diffPitch = ty; // down key = rotating camera down
944 const float diffYaw = -tx; // right key = rotating camera to the right
945 rotateCamera( diffPitch, diffYaw );
946 }
947 return true;
948 }
949
950 if ( tElev )
951 {
952 QgsVector3D center = mCameraPose.centerPoint();
953 center.set( center.x(), center.y(), center.z() + tElev * 10 );
954 mCameraPose.setCenterPoint( center );
955 return true;
956 updateCameraFromPose();
957 }
958
959 return false;
960}
961
962bool QgsCameraController::onKeyPressedGlobeTerrainNavigation( QKeyEvent *event )
963{
964 // both move factor and zoom factor are just empirically picked numbers
965 // that seem to work well (providing steps that are not too big / not too small)
966 constexpr float MOVE_FACTOR = 0.000001f; // multiplied by distance to get angle
967 constexpr float ZOOM_FACTOR = 0.9f;
968
969 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
970
971 switch ( event->key() )
972 {
973 case Qt::Key_Left:
974 if ( hasShift )
976 else
977 globeMoveCenterPoint( 0, -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
978 return true;
979 case Qt::Key_Right:
980 if ( hasShift )
982 else
983 globeMoveCenterPoint( 0, MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
984 return true;
985 case Qt::Key_Up:
986 if ( hasShift )
988 else
989 globeMoveCenterPoint( MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
990 return true;
991 case Qt::Key_Down:
992 if ( hasShift )
994 else
995 globeMoveCenterPoint( -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
996 return true;
997 case Qt::Key_PageDown:
998 globeZoom( ZOOM_FACTOR );
999 return true;
1000 case Qt::Key_PageUp:
1001 globeZoom( 1 / ZOOM_FACTOR );
1002 return true;
1003 default:
1004 break;
1005 }
1006 return false;
1007}
1008
1009static const QSet<int> walkNavigationSavedKeys = {
1010 Qt::Key_Left,
1011 Qt::Key_A,
1012 Qt::Key_Right,
1013 Qt::Key_D,
1014 Qt::Key_Up,
1015 Qt::Key_W,
1016 Qt::Key_Down,
1017 Qt::Key_S,
1018 Qt::Key_PageUp,
1019 Qt::Key_E,
1020 Qt::Key_PageDown,
1021 Qt::Key_Q,
1022};
1023
1024bool QgsCameraController::onKeyPressedFlyNavigation( QKeyEvent *event )
1025{
1026 switch ( event->key() )
1027 {
1028 case Qt::Key_QuoteLeft:
1029 {
1030 // toggle mouse lock mode
1031 mCaptureFpsMouseMovements = !mCaptureFpsMouseMovements;
1032 mIgnoreNextMouseMove = true;
1033 if ( mCaptureFpsMouseMovements )
1034 {
1035 qApp->setOverrideCursor( QCursor( Qt::BlankCursor ) );
1036 }
1037 else
1038 {
1039 qApp->restoreOverrideCursor();
1040 }
1041 event->accept();
1042 return true;
1043 }
1044
1045 case Qt::Key_Escape:
1046 {
1047 // always exit mouse lock mode
1048 if ( mCaptureFpsMouseMovements )
1049 {
1050 mCaptureFpsMouseMovements = false;
1051 mIgnoreNextMouseMove = true;
1052 qApp->restoreOverrideCursor();
1053 event->accept();
1054 return true;
1055 }
1056 break;
1057 }
1058 default:
1059 break;
1060 }
1061
1062 if ( walkNavigationSavedKeys.contains( event->key() ) )
1063 {
1064 if ( !event->isAutoRepeat() )
1065 {
1066 mDepressedKeys.insert( event->key() );
1067 }
1068 event->accept();
1069 return true;
1070 }
1071 return false;
1072}
1073
1074void QgsCameraController::walkView( double tx, double ty, double tz )
1075{
1076 const QVector3D cameraUp = mCamera->upVector().normalized();
1077 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1078 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1079
1080 QVector3D cameraPosDiff( 0.0f, 0.0f, 0.0f );
1081
1082 if ( tx != 0.0 )
1083 {
1084 cameraPosDiff += static_cast<float>( tx ) * cameraFront;
1085 }
1086 if ( ty != 0.0 )
1087 {
1088 cameraPosDiff += static_cast<float>( ty ) * cameraLeft;
1089 }
1090 if ( tz != 0.0 )
1091 {
1092 cameraPosDiff += static_cast<float>( tz ) * QVector3D( 0.0f, 0.0f, 1.0f );
1093 }
1094
1095 moveCameraPositionBy( cameraPosDiff );
1096}
1097
1098void QgsCameraController::applyFlyModeKeyMovements()
1099{
1100 if ( mCameraNavigationMode != Qgis::NavigationMode::Walk )
1101 return;
1102
1103 // shift = "run", ctrl = "slow walk"
1104 const bool shiftPressed = mDepressedKeys.contains( Qt::Key_Shift );
1105 const bool ctrlPressed = mDepressedKeys.contains( Qt::Key_Control );
1106
1107 const double movementSpeed = mCameraMovementSpeed * ( shiftPressed ? 2 : 1 ) * ( ctrlPressed ? 0.1 : 1 );
1108
1109 bool changed = false;
1110 double x = 0.0;
1111 double y = 0.0;
1112 double z = 0.0;
1113 if ( mDepressedKeys.contains( Qt::Key_Left ) || mDepressedKeys.contains( Qt::Key_A ) )
1114 {
1115 changed = true;
1116 y += movementSpeed;
1117 }
1118
1119 if ( mDepressedKeys.contains( Qt::Key_Right ) || mDepressedKeys.contains( Qt::Key_D ) )
1120 {
1121 changed = true;
1122 y -= movementSpeed;
1123 }
1124
1125 if ( mDepressedKeys.contains( Qt::Key_Up ) || mDepressedKeys.contains( Qt::Key_W ) )
1126 {
1127 changed = true;
1128 x += movementSpeed;
1129 }
1130
1131 if ( mDepressedKeys.contains( Qt::Key_Down ) || mDepressedKeys.contains( Qt::Key_S ) )
1132 {
1133 changed = true;
1134 x -= movementSpeed;
1135 }
1136
1137 // note -- vertical axis movements are slower by default then horizontal ones, as GIS projects
1138 // tend to have much more limited elevation range vs ground range
1139 static constexpr double ELEVATION_MOVEMENT_SCALE = 0.5;
1140 if ( mDepressedKeys.contains( Qt::Key_PageUp ) || mDepressedKeys.contains( Qt::Key_E ) )
1141 {
1142 changed = true;
1143 z += ELEVATION_MOVEMENT_SCALE * movementSpeed;
1144 }
1145
1146 if ( mDepressedKeys.contains( Qt::Key_PageDown ) || mDepressedKeys.contains( Qt::Key_Q ) )
1147 {
1148 changed = true;
1149 z -= ELEVATION_MOVEMENT_SCALE * movementSpeed;
1150 }
1151
1152 if ( changed )
1153 walkView( x, y, z );
1154}
1155
1156void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent *mouse )
1157{
1158 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
1159 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
1160
1161 const double dx = mCaptureFpsMouseMovements ? QCursor::pos().x() - mMousePos.x() : mouse->x() - mMousePos.x();
1162 const double dy = mCaptureFpsMouseMovements ? QCursor::pos().y() - mMousePos.y() : mouse->y() - mMousePos.y();
1163 mMousePos = mCaptureFpsMouseMovements ? QCursor::pos() : QPoint( mouse->x(), mouse->y() );
1164
1165 if ( mIgnoreNextMouseMove )
1166 {
1167 mIgnoreNextMouseMove = false;
1168 return;
1169 }
1170
1171 if ( hasMiddleButton )
1172 {
1173 // middle button drag = pan camera in place (strafe)
1174 const QVector3D cameraUp = mCamera->upVector().normalized();
1175 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1176 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
1177 const QVector3D cameraPosDiff = -dx * cameraLeft - dy * cameraUp;
1178 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 10.0 );
1179 }
1180 else if ( hasRightButton )
1181 {
1182 // right button drag = camera dolly
1183 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
1184 const QVector3D cameraPosDiff = dy * cameraFront;
1185 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 5.0 );
1186 }
1187 else
1188 {
1189 if ( mCaptureFpsMouseMovements )
1190 {
1191 float diffPitch = -0.2f * dy;
1192 switch ( mVerticalAxisInversion )
1193 {
1195 diffPitch *= -1;
1196 break;
1197
1200 break;
1201 }
1202
1203 const float diffYaw = -0.2f * dx;
1204 rotateCamera( diffPitch, diffYaw );
1205 }
1206 else if ( mouse->buttons() & Qt::LeftButton )
1207 {
1208 float diffPitch = -0.2f * dy;
1209 switch ( mVerticalAxisInversion )
1210 {
1213 diffPitch *= -1;
1214 break;
1215
1217 break;
1218 }
1219 const float diffYaw = -0.2f * dx;
1220 rotateCamera( diffPitch, diffYaw );
1221 }
1222 }
1223
1224 if ( mCaptureFpsMouseMovements )
1225 {
1226 mIgnoreNextMouseMove = true;
1227
1228 // reset cursor back to center of map widget
1229 emit setCursorPosition( QPoint( mScene->engine()->size().width() / 2, mScene->engine()->size().height() / 2 ) );
1230 }
1231}
1232
1234{
1235 // Tilt up the view by deltaPitch around the view center (camera moves)
1236 float pitch = mCameraPose.pitchAngle();
1237 pitch -= deltaPitch; // down key = moving camera toward terrain
1238 mCameraPose.setPitchAngle( pitch );
1239 updateCameraFromPose();
1240}
1241
1243{
1244 // Rotate clockwise the view by deltaYaw around the view center (camera moves)
1245 float yaw = mCameraPose.headingAngle();
1246 yaw -= deltaYaw; // right key = moving camera clockwise
1247 mCameraPose.setHeadingAngle( yaw );
1248 updateCameraFromPose();
1249}
1250
1252{
1253 mCameraPose.setHeadingAngle( angle );
1254 updateCameraFromPose();
1255}
1256
1257void QgsCameraController::moveView( float tx, float ty )
1258{
1259 const float yaw = mCameraPose.headingAngle();
1260 const float dist = mCameraPose.distanceFromCenterPoint();
1261 const float x = tx * dist * 0.02f;
1262 const float y = -ty * dist * 0.02f;
1263
1264 // moving with keyboard - take into account yaw of camera
1265 const float t = sqrt( x * x + y * y );
1266 const float a = atan2( y, x ) - yaw * M_PI / 180;
1267 const float dx = cos( a ) * t;
1268 const float dy = sin( a ) * t;
1269
1270 QgsVector3D center = mCameraPose.centerPoint();
1271 center.set( center.x() + dx, center.y() - dy, center.z() );
1272 mCameraPose.setCenterPoint( center );
1273 updateCameraFromPose();
1274}
1275
1277{
1278 if ( !mInputHandlersEnabled )
1279 return false;
1280
1281 if ( event->type() == QKeyEvent::Type::KeyRelease )
1282 {
1283 if ( !event->isAutoRepeat() && mDepressedKeys.contains( event->key() ) )
1284 {
1285 mDepressedKeys.remove( event->key() );
1286 return true;
1287 }
1288 }
1289 else if ( event->type() == QEvent::ShortcutOverride )
1290 {
1291 if ( event->modifiers() & Qt::ControlModifier )
1292 {
1293 switch ( event->key() )
1294 {
1295 case Qt::Key_QuoteLeft:
1296 {
1297 // switch navigation mode
1298 switch ( mCameraNavigationMode )
1299 {
1305 );
1306 break;
1310 break;
1311 }
1312 event->accept();
1313 return true;
1314 }
1315
1316 // Make sure to sync the key combinations with strings in Qgs3DAxis::createMenu()!
1317 case Qt::Key_8:
1319 return true;
1320 case Qt::Key_6:
1322 return true;
1323 case Qt::Key_2:
1325 return true;
1326 case Qt::Key_4:
1328 return true;
1329 case Qt::Key_9:
1331 return true;
1332 case Qt::Key_3:
1334 return true;
1335 case Qt::Key_5:
1337 return true;
1338
1339 default:
1340 break;
1341 }
1342 }
1343
1344 switch ( mCameraNavigationMode )
1345 {
1347 return onKeyPressedFlyNavigation( event );
1348
1350 return onKeyPressedTerrainNavigation( event );
1351
1353 return onKeyPressedGlobeTerrainNavigation( event );
1354 }
1355 }
1356 return false;
1357}
1358
1359void QgsCameraController::depthBufferCaptured( const QImage &depthImage )
1360{
1361 mDepthBufferImage = depthImage;
1362 mDepthBufferIsReady = true;
1363 mDepthBufferNonVoidAverage = -1;
1364
1365 // To read distances from the captured depth buffer, we need to know the
1366 // camera parameters it was rendered with. This seems like the closest
1367 // place to save them, though I have no idea if they can't be changed
1368 // between the rendering and now anyway...
1369 mDepthBufferCamera = Qgs3DUtils::copyCamera( mCamera );
1370
1371 if ( mCurrentOperation == MouseOperation::ZoomWheel )
1372 {
1373 handleTerrainNavigationWheelZoom();
1374 }
1375}
1376
1377bool QgsCameraController::isATranslationRotationSequence( MouseOperation newOperation ) const
1378{
1379 return std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), newOperation ) != std::end( mTranslateOrRotate ) && std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), mCurrentOperation ) != std::end( mTranslateOrRotate );
1380}
1381
1382void QgsCameraController::setMouseParameters( const MouseOperation &newOperation, const QPoint &clickPoint )
1383{
1384 if ( newOperation == mCurrentOperation )
1385 {
1386 return;
1387 }
1388
1389 if ( newOperation == MouseOperation::None )
1390 {
1391 mClickPoint = QPoint();
1392 }
1393 // click point and rotation angles are updated if:
1394 // - it has never been computed
1395 // - the current and new operations are both rotation and translation
1396 // Indeed, if the sequence such as rotation -> zoom -> rotation updating mClickPoint on
1397 // the click point does not need to be updated because the relative mouse position is kept
1398 // during a zoom operation
1399 else if ( mClickPoint.isNull() || isATranslationRotationSequence( newOperation ) )
1400 {
1401 mClickPoint = clickPoint;
1402 mRotationPitch = mCameraPose.pitchAngle();
1403 mRotationYaw = mCameraPose.headingAngle();
1404 }
1405 mCurrentOperation = newOperation;
1406 mDepthBufferIsReady = false;
1407 mRotationCenterCalculated = false;
1408 mDragPointCalculated = false;
1409 mZoomPointCalculated = false;
1410
1411 if ( mCurrentOperation != MouseOperation::None && mCurrentOperation != MouseOperation::RotationCamera )
1412 {
1413 mMousePressViewCenter = mCameraPose.centerPoint() + mOrigin;
1414 mCameraBefore = Qgs3DUtils::copyCamera( mCamera );
1415
1417 }
1418}
1419
1421{
1422 QgsVector3D diff = origin - mOrigin;
1423 mCameraPose.setCenterPoint( mCameraPose.centerPoint() - diff );
1424
1425 // update other members that depend on world coordinates
1426 mCameraBefore->setPosition( ( QgsVector3D( mCameraBefore->position() ) - diff ).toVector3D() );
1427 mCameraBefore->setViewCenter( ( QgsVector3D( mCameraBefore->viewCenter() ) - diff ).toVector3D() );
1428 mDragPoint = ( QgsVector3D( mDragPoint ) - diff ).toVector3D();
1429 mRotationCenter = ( QgsVector3D( mRotationCenter ) - diff ).toVector3D();
1430
1431 mOrigin = origin;
1432
1433 updateCameraFromPose();
1434}
1435
1436void QgsCameraController::rotateToRespectingTerrain( float pitch, float yaw )
1437{
1439 double elevation = 0.0;
1440 if ( mScene->mapSettings()->terrainRenderingEnabled() )
1441 {
1442 QgsDebugMsgLevel( "Checking elevation from terrain...", 2 );
1443 QVector3D camPos = mCamera->position();
1444 QgsRayCastingUtils::Ray3D ray( camPos, pos.toVector3D() - camPos, mCamera->farPlane() );
1445 const QVector<QgsRayCastingUtils::RayHit> hits = mScene->terrainEntity()->rayIntersection( ray, QgsRayCastingUtils::RayCastContext() );
1446 if ( !hits.isEmpty() )
1447 {
1448 elevation = hits.at( 0 ).pos.z();
1449 QgsDebugMsgLevel( QString( "Computed elevation from terrain: %1" ).arg( elevation ), 2 );
1450 }
1451 else
1452 {
1453 QgsDebugMsgLevel( "Unable to obtain elevation from terrain", 2 );
1454 }
1455 }
1456 pos.set( pos.x(), pos.y(), elevation + mScene->terrainEntity()->terrainElevationOffset() );
1457
1458 setLookingAtPoint( pos, ( mCamera->position() - pos.toVector3D() ).length(), pitch, yaw );
1459}
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.
void readXml(const QDomElement &elem)
Reads camera configuration from the given DOM element.
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 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.