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