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