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