QGIS API Documentation 3.43.0-Master (ebb4087afc0)
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#include "moc_qgscameracontroller.cpp"
18#include "qgseventtracing.h"
19#include "qgsvector3d.h"
20#include "qgswindow3dengine.h"
21#include "qgs3dmapscene.h"
22#include "qgsterrainentity.h"
23#include "qgis.h"
24#include "qgs3dutils.h"
25
26#include <QDomDocument>
27#include <Qt3DRender/QCamera>
28#include <Qt3DInput>
29#include <cmath>
30
31#include "qgslogger.h"
32
34 : Qt3DCore::QEntity( scene )
35 , mScene( scene )
36 , mCamera( scene->engine()->camera() )
37 , mCameraBefore( new Qt3DRender::QCamera )
38 , mMouseHandler( new Qt3DInput::QMouseHandler )
39 , mKeyboardHandler( new Qt3DInput::QKeyboardHandler )
40 , mOrigin( scene->mapSettings()->origin() )
41{
42 mMouseHandler->setSourceDevice( new Qt3DInput::QMouseDevice() );
43 connect( mMouseHandler, &Qt3DInput::QMouseHandler::positionChanged, this, &QgsCameraController::onPositionChanged );
44 connect( mMouseHandler, &Qt3DInput::QMouseHandler::wheel, this, &QgsCameraController::onWheel );
45 connect( mMouseHandler, &Qt3DInput::QMouseHandler::pressed, this, &QgsCameraController::onMousePressed );
46 connect( mMouseHandler, &Qt3DInput::QMouseHandler::released, this, &QgsCameraController::onMouseReleased );
47 addComponent( mMouseHandler );
48
49 mKeyboardHandler->setSourceDevice( new Qt3DInput::QKeyboardDevice() );
50 connect( mKeyboardHandler, &Qt3DInput::QKeyboardHandler::pressed, this, &QgsCameraController::onKeyPressed );
51 connect( mKeyboardHandler, &Qt3DInput::QKeyboardHandler::released, this, &QgsCameraController::onKeyReleased );
52 addComponent( mKeyboardHandler );
53
54 // Disable the handlers when the entity is disabled
55 connect( this, &Qt3DCore::QEntity::enabledChanged, mMouseHandler, &Qt3DInput::QMouseHandler::setEnabled );
56 connect( this, &Qt3DCore::QEntity::enabledChanged, mKeyboardHandler, &Qt3DInput::QKeyboardHandler::setEnabled );
57
58 mFpsNavTimer = new QTimer( this );
59 mFpsNavTimer->setInterval( 10 );
60 connect( mFpsNavTimer, &QTimer::timeout, this, &QgsCameraController::applyFlyModeKeyMovements );
61 mFpsNavTimer->start();
62}
63
65
66QWindow *QgsCameraController::window() const
67{
68 QgsWindow3DEngine *windowEngine = qobject_cast<QgsWindow3DEngine *>( mScene->engine() );
69 return windowEngine ? windowEngine->window() : nullptr;
70}
71
73{
74 if ( navigationMode == mCameraNavigationMode )
75 return;
76
77 mCameraNavigationMode = navigationMode;
78 mIgnoreNextMouseMove = true;
79 emit navigationModeChanged( mCameraNavigationMode );
80}
81
83{
84 if ( movementSpeed == mCameraMovementSpeed )
85 return;
86
87 // If the speed becomes 0, navigation does not work anymore
88 // If the speed becomes too important, only one walk can move the view far from the scene.
89 mCameraMovementSpeed = std::clamp( movementSpeed, 0.05, 150.0 );
90 emit cameraMovementSpeedChanged( mCameraMovementSpeed );
91}
92
94{
95 mVerticalAxisInversion = inversion;
96}
97
98void QgsCameraController::rotateCamera( float diffPitch, float diffHeading )
99{
100 const float oldPitch = mCameraPose.pitchAngle();
101 const float oldHeading = mCameraPose.headingAngle();
102 float newPitch = oldPitch + diffPitch;
103 float newHeading = oldHeading + diffHeading;
104
105 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
106
107 // First undo the previously applied rotation, then apply the new rotation
108 // (We can't just apply our euler angles difference because the camera may be already rotated)
109 const QQuaternion qNew = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
110 const QQuaternion qOld = Qgs3DUtils::rotationFromPitchHeadingAngles( oldPitch, oldHeading );
111 const QQuaternion q = qNew * qOld.conjugated();
112
113 // get camera's view vector, rotate it to get new view center
114 const QVector3D position = mCamera->position();
115 QVector3D viewCenter = mCamera->viewCenter();
116 const QVector3D viewVector = viewCenter - position;
117 const QVector3D cameraToCenter = q * viewVector;
118 viewCenter = position + cameraToCenter;
119
120 mCameraPose.setCenterPoint( viewCenter );
121 mCameraPose.setPitchAngle( newPitch );
122 mCameraPose.setHeadingAngle( newHeading );
123 updateCameraFromPose();
124}
125
126void QgsCameraController::rotateCameraAroundPivot( float newPitch, float newHeading, const QVector3D &pivotPoint )
127{
128 const float oldPitch = mCameraPose.pitchAngle();
129 const float oldHeading = mCameraPose.headingAngle();
130
131 newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
132
133 // First undo the previously applied rotation, then apply the new rotation
134 // (We can't just apply our euler angles difference because the camera may be already rotated)
135 const QQuaternion qNew = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
136 const QQuaternion qOld = Qgs3DUtils::rotationFromPitchHeadingAngles( oldPitch, oldHeading );
137 const QQuaternion q = qNew * qOld.conjugated();
138
139 const QVector3D newViewCenter = q * ( mCamera->viewCenter() - pivotPoint ) + pivotPoint;
140
141 mCameraPose.setCenterPoint( newViewCenter );
142 mCameraPose.setPitchAngle( newPitch );
143 mCameraPose.setHeadingAngle( newHeading );
144 updateCameraFromPose();
145}
146
147void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint )
148{
149 // step 1: move camera along the line connecting reference camera position and our pivot point
150 QVector3D newCamPosition = pivotPoint + ( oldCameraPosition - pivotPoint ) * zoomFactor;
151 double newDistance = mCameraPose.distanceFromCenterPoint() * zoomFactor;
152
153 // step 2: using the new camera position and distance from center, calculate new view center
154 QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( mCameraPose.pitchAngle(), mCameraPose.headingAngle() );
155 QVector3D cameraToCenter = q * QVector3D( 0, 0, -newDistance );
156 QVector3D newViewCenter = newCamPosition + cameraToCenter;
157
158 mCameraPose.setDistanceFromCenterPoint( newDistance );
159 mCameraPose.setCenterPoint( newViewCenter );
160 updateCameraFromPose();
161}
162
164{
165 Q_UNUSED( dt )
166
167 if ( mCameraChanged )
168 {
169 emit cameraChanged();
170 mCameraChanged = false;
171 }
172}
173
174void QgsCameraController::resetView( float distance )
175{
176 setViewFromTop( 0, 0, distance );
177}
178
179void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
180{
181 QgsCameraPose camPose;
182 QgsTerrainEntity *terrain = mScene->terrainEntity();
183 if ( terrain )
184 camPose.setCenterPoint( QgsVector3D( worldX, worldY, terrain->terrainElevationOffset() ) );
185 else
186 camPose.setCenterPoint( QgsVector3D( worldX, worldY, 0.0f ) );
188 camPose.setHeadingAngle( yaw );
189
190 // a basic setup to make frustum depth range long enough that it does not cull everything
191 mCamera->setNearPlane( distance / 2 );
192 mCamera->setFarPlane( distance * 2 );
193 // we force the updateCameraNearFarPlanes() in Qgs3DMapScene to properly set the planes
194 setCameraPose( camPose, true );
195}
196
198{
199 return mCameraPose.centerPoint();
200}
201
202void QgsCameraController::setLookingAtPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
203{
204 QgsCameraPose camPose;
205 camPose.setCenterPoint( point );
207 camPose.setPitchAngle( pitch );
208 camPose.setHeadingAngle( yaw );
209 setCameraPose( camPose );
210}
211
212void QgsCameraController::setCameraPose( const QgsCameraPose &camPose, bool force )
213{
214 if ( camPose == mCameraPose && !force )
215 return;
216
217 mCameraPose = camPose;
218 updateCameraFromPose();
219}
220
221QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
222{
223 QDomElement elemCamera = doc.createElement( QStringLiteral( "camera" ) );
224 elemCamera.setAttribute( QStringLiteral( "x" ), mCameraPose.centerPoint().x() );
225 elemCamera.setAttribute( QStringLiteral( "y" ), mCameraPose.centerPoint().z() );
226 elemCamera.setAttribute( QStringLiteral( "elev" ), mCameraPose.centerPoint().y() );
227 elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraPose.distanceFromCenterPoint() );
228 elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraPose.pitchAngle() );
229 elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraPose.headingAngle() );
230 return elemCamera;
231}
232
233void QgsCameraController::readXml( const QDomElement &elem )
234{
235 const float x = elem.attribute( QStringLiteral( "x" ) ).toFloat();
236 const float y = elem.attribute( QStringLiteral( "y" ) ).toFloat();
237 const float elev = elem.attribute( QStringLiteral( "elev" ) ).toFloat();
238 const float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
239 const float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
240 const float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
241 setLookingAtPoint( QgsVector3D( x, elev, y ), dist, pitch, yaw );
242}
243
244double QgsCameraController::sampleDepthBuffer( int px, int py )
245{
246 double depth = 1;
247
248 // Sample the neighbouring pixels for the closest point to the camera
249 for ( int x = px - 3; x <= px + 3; ++x )
250 {
251 for ( int y = py - 3; y <= py + 3; ++y )
252 {
253 if ( mDepthBufferImage.valid( x, y ) )
254 {
255 depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) );
256 }
257 }
258 }
259
260 if ( depth < 1 )
261 return depth;
262
263 // Cache the computed depth, since averaging over all pixels can be expensive
264 if ( mDepthBufferNonVoidAverage != -1 )
265 return mDepthBufferNonVoidAverage;
266
267 // Returns the average of depth values that are not 1 (void area)
268 depth = 0;
269 int samplesCount = 0;
270 // Make sure we can do the cast
271 Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 );
272 for ( int y = 0; y < mDepthBufferImage.height(); ++y )
273 {
274 const QRgb *line = reinterpret_cast<const QRgb *>( mDepthBufferImage.constScanLine( y ) );
275 for ( int x = 0; x < mDepthBufferImage.width(); ++x )
276 {
277 double d = Qgs3DUtils::decodeDepth( line[x] );
278 if ( d < 1 )
279 {
280 depth += d;
281 samplesCount += 1;
282 }
283 }
284 }
285
286 // if the whole buffer is white, a depth cannot be computed
287 if ( samplesCount == 0 )
288 depth = 1.0;
289 else
290 depth /= samplesCount;
291
292 mDepthBufferNonVoidAverage = depth;
293
294 return depth;
295}
296
297void QgsCameraController::updateCameraFromPose()
298{
299 if ( mCamera )
300 {
301 mCameraPose.updateCamera( mCamera );
302 mCameraChanged = true;
303 }
304}
305
306void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff )
307{
308 mCameraPose.setCenterPoint( mCameraPose.centerPoint() + posDiff );
309 updateCameraFromPose();
310}
311
312void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
313{
314 if ( !mInputHandlersEnabled )
315 return;
316
317 QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), QStringLiteral( "QgsCameraController::onPositionChanged" ) );
318
319 switch ( mCameraNavigationMode )
320 {
322 onPositionChangedTerrainNavigation( mouse );
323 break;
324
326 onPositionChangedFlyNavigation( mouse );
327 break;
328 }
329}
330
331bool QgsCameraController::screenPointToWorldPos( QPoint position, Qt3DRender::QCamera *mCameraBefore, double &depth, QVector3D &worldPosition )
332{
333 depth = sampleDepthBuffer( position.x(), position.y() );
334 if ( !std::isfinite( depth ) )
335 {
336 QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: depth is NaN or Inf. This should not happen." ), 2 );
337 return false;
338 }
339 else
340 {
341 worldPosition = Qgs3DUtils::screenPointToWorldPos( position, depth, mScene->engine()->size(), mCameraBefore );
342 if ( !std::isfinite( worldPosition.x() ) || !std::isfinite( worldPosition.y() ) || !std::isfinite( worldPosition.z() ) )
343 {
344 QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: position is NaN or Inf. This should not happen." ), 2 );
345 return false;
346 }
347 else
348 {
349 return true;
350 }
351 }
352}
353
354void QgsCameraController::onPositionChangedTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
355{
356 if ( mIgnoreNextMouseMove )
357 {
358 mIgnoreNextMouseMove = false;
359 mMousePos = QPoint( mouse->x(), mouse->y() );
360 return;
361 }
362
363 const int dx = mouse->x() - mMousePos.x();
364 const int dy = mouse->y() - mMousePos.y();
365
366 const bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
367 const bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
368 const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
369 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
370 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
371
372 if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
373 {
374 // rotate/tilt using mouse (camera moves as it rotates around the clicked point)
375 setMouseParameters( MouseOperation::RotationCenter, mMousePos );
376
377 float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
378 float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
379 float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
380
381 if ( !mDepthBufferIsReady )
382 return;
383
384 if ( !mRotationCenterCalculated )
385 {
386 double depth;
387 QVector3D worldPosition;
388 if ( screenPointToWorldPos( mClickPoint, mCameraBefore.get(), depth, worldPosition ) )
389 {
390 mRotationCenter = worldPosition;
391 mRotationDistanceFromCenter = ( mRotationCenter - mCameraBefore->position() ).length();
392 emit cameraRotationCenterChanged( mRotationCenter );
393 mRotationCenterCalculated = true;
394 }
395 }
396
397 rotateCameraAroundPivot( mRotationPitch + pitchDiff, mRotationYaw + yawDiff, mRotationCenter );
398 }
399 else if ( hasLeftButton && hasCtrl && !hasShift )
400 {
401 setMouseParameters( MouseOperation::RotationCamera );
402 // rotate/tilt using mouse (camera stays at one position as it rotates)
403 const float diffPitch = 0.2f * dy;
404 const float diffYaw = -0.2f * dx;
405 rotateCamera( diffPitch, diffYaw );
406 }
407 else if ( hasLeftButton && !hasShift && !hasCtrl )
408 {
409 // translation works as if one grabbed a point on the 3D viewer and dragged it
410 setMouseParameters( MouseOperation::Translation, mMousePos );
411
412 if ( !mDepthBufferIsReady )
413 return;
414
415 if ( !mDragPointCalculated )
416 {
417 double depth;
418 QVector3D worldPosition;
419 if ( screenPointToWorldPos( mClickPoint, mCameraBefore.get(), depth, worldPosition ) )
420 {
421 mDragDepth = depth;
422 mDragPoint = worldPosition;
423 mDragPointCalculated = true;
424 }
425 }
426
427 QVector3D cameraBeforeDragPos = mCameraBefore->position();
428
429 QVector3D moveToPosition = Qgs3DUtils::screenPointToWorldPos( mMousePos, mDragDepth, mScene->engine()->size(), mCameraBefore.get() );
430 QVector3D cameraBeforeToMoveToPos = ( moveToPosition - mCameraBefore->position() ).normalized();
431 QVector3D cameraBeforeToDragPointPos = ( mDragPoint - mCameraBefore->position() ).normalized();
432
433 // Make sure the rays are not horizontal (add small z shift if it is)
434 if ( cameraBeforeToMoveToPos.z() == 0 )
435 {
436 cameraBeforeToMoveToPos.setZ( 0.01 );
437 cameraBeforeToMoveToPos = cameraBeforeToMoveToPos.normalized();
438 }
439
440 if ( cameraBeforeToDragPointPos.z() == 0 )
441 {
442 cameraBeforeToDragPointPos.setZ( 0.01 );
443 cameraBeforeToDragPointPos = cameraBeforeToDragPointPos.normalized();
444 }
445
446 double d1 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToMoveToPos.z();
447 double d2 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToDragPointPos.z();
448
449 QVector3D from = cameraBeforeDragPos + d1 * cameraBeforeToMoveToPos;
450 QVector3D to = cameraBeforeDragPos + d2 * cameraBeforeToDragPointPos;
451
452 QVector3D shiftVector = to - from;
453
454 mCameraPose.setCenterPoint( mCameraBefore->viewCenter() + shiftVector );
455 updateCameraFromPose();
456 }
457 else if ( hasLeftButton && hasShift && hasCtrl )
458 {
459 // change the camera elevation, similar to pageUp/pageDown
460 QgsVector3D center = mCameraPose.centerPoint();
461 double tElev = mMousePos.y() - mouse->y();
462 center.set( center.x(), center.y(), center.z() + tElev * 0.5 );
463 mCameraPose.setCenterPoint( center );
464 updateCameraFromPose();
465 }
466 else if ( hasRightButton && !hasShift && !hasCtrl )
467 {
468 setMouseParameters( MouseOperation::Zoom, mMousePos );
469 if ( !mDepthBufferIsReady )
470 return;
471
472 if ( !mDragPointCalculated )
473 {
474 double depth;
475 QVector3D worldPosition;
476 if ( screenPointToWorldPos( mClickPoint, mCameraBefore.get(), depth, worldPosition ) )
477 {
478 mDragPoint = worldPosition;
479 mDragPointCalculated = true;
480 }
481 }
482
483 float oldDist = ( mCameraBefore->position() - mDragPoint ).length();
484 float newDist = oldDist;
485
486 int yOffset = 0;
487 int screenHeight = mScene->engine()->size().height();
488 QWindow *win = window();
489 if ( win )
490 {
491 yOffset = win->mapToGlobal( QPoint( 0, 0 ) ).y();
492 screenHeight = win->screen()->size().height();
493 }
494
495 // Applies smoothing
496 if ( mMousePos.y() > mClickPoint.y() ) // zoom in
497 {
498 double f = ( double ) ( mMousePos.y() - mClickPoint.y() ) / ( double ) ( screenHeight - mClickPoint.y() - yOffset );
499 f = std::max( 0.0, std::min( 1.0, f ) );
500 f = 1 - ( std::expm1( -2 * f ) ) / ( std::expm1( -2 ) );
501 newDist = newDist * f;
502 }
503 else // zoom out
504 {
505 double f = 1 - ( double ) ( mMousePos.y() + yOffset ) / ( double ) ( mClickPoint.y() + yOffset );
506 f = std::max( 0.0, std::min( 1.0, f ) );
507 f = ( std::expm1( 2 * f ) ) / ( std::expm1( 2 ) );
508 newDist = newDist + 2 * newDist * f;
509 }
510
511 double zoomFactor = newDist / oldDist;
512 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mDragPoint );
513 }
514
515 mMousePos = QPoint( mouse->x(), mouse->y() );
516}
517
518void QgsCameraController::zoom( float factor )
519{
520 // zoom in/out
521 float dist = mCameraPose.distanceFromCenterPoint();
522 dist -= dist * factor * 0.01f;
523 mCameraPose.setDistanceFromCenterPoint( dist );
524 updateCameraFromPose();
525}
526
527void QgsCameraController::handleTerrainNavigationWheelZoom()
528{
529 if ( !mDepthBufferIsReady )
530 return;
531
532 if ( !mZoomPointCalculated )
533 {
534 double depth;
535 QVector3D worldPosition;
536 if ( screenPointToWorldPos( mMousePos, mCameraBefore.get(), depth, worldPosition ) )
537 {
538 mZoomPoint = worldPosition;
539 mZoomPointCalculated = true;
540 }
541 }
542
543 double oldDist = ( mZoomPoint - mCameraBefore->position() ).length();
544 // Each step of the scroll wheel decreases distance by 20%
545 double newDist = std::pow( 0.8, mCumulatedWheelY ) * oldDist;
546 // Make sure we don't clip the thing we're zooming to.
547 newDist = std::max( newDist, 2.0 );
548 double zoomFactor = newDist / oldDist;
549 // Don't change the distance too suddenly to hopefully prevent numerical instability
550 zoomFactor = std::clamp( zoomFactor, 0.01, 100.0 );
551
552 zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mZoomPoint );
553
554 mCumulatedWheelY = 0;
555 setMouseParameters( MouseOperation::None );
556}
557
558void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
559{
560 if ( !mInputHandlersEnabled )
561 return;
562
563 switch ( mCameraNavigationMode )
564 {
566 {
567 const float scaling = ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1f : 1.0f ) / 1000.f;
568 setCameraMovementSpeed( mCameraMovementSpeed + mCameraMovementSpeed * scaling * wheel->angleDelta().y() );
569 break;
570 }
571
573 {
574 // Scale our variable to roughly "number of normal steps", with Ctrl
575 // increasing granularity 10x
576 const double scaling = ( 1.0 / 120.0 ) * ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1 : 1.0 );
577
578 // Apparently angleDelta needs to be accumulated
579 // see: https://doc.qt.io/qt-5/qwheelevent.html#angleDelta
580 mCumulatedWheelY += scaling * wheel->angleDelta().y();
581
582 if ( mCurrentOperation != MouseOperation::ZoomWheel )
583 {
584 setMouseParameters( MouseOperation::ZoomWheel );
585 // The actual zooming will happen after we get a new depth buffer
586 }
587 else
588 {
589 handleTerrainNavigationWheelZoom();
590 }
591 break;
592 }
593 }
594}
595
596void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
597{
598 if ( !mInputHandlersEnabled )
599 return;
600
601 mKeyboardHandler->setFocus( true );
602
603 if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton || ( ( mouse->modifiers() & Qt::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) || ( ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) )
604 {
605 mMousePos = QPoint( mouse->x(), mouse->y() );
606
607 if ( mCaptureFpsMouseMovements )
608 mIgnoreNextMouseMove = true;
609
610 const MouseOperation operation {
611 ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ? MouseOperation::RotationCamera : MouseOperation::RotationCenter
612 };
613 setMouseParameters( operation, mMousePos );
614 }
615
616 else if ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton || mouse->button() == Qt3DInput::QMouseEvent::RightButton )
617 {
618 mMousePos = QPoint( mouse->x(), mouse->y() );
619
620 if ( mCaptureFpsMouseMovements )
621 mIgnoreNextMouseMove = true;
622
623 const MouseOperation operation = ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ? MouseOperation::Translation : MouseOperation::Zoom;
624 setMouseParameters( operation, mMousePos );
625 }
626}
627
628void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
629{
630 Q_UNUSED( mouse )
631 if ( !mInputHandlersEnabled )
632 return;
633
634
635 setMouseParameters( MouseOperation::None );
636}
637
638void QgsCameraController::onKeyPressed( Qt3DInput::QKeyEvent *event )
639{
640 if ( !mInputHandlersEnabled )
641 return;
642
643 if ( event->modifiers() & Qt::ControlModifier && event->key() == Qt::Key_QuoteLeft )
644 {
645 // switch navigation mode
646 switch ( mCameraNavigationMode )
647 {
650 break;
653 break;
654 }
655 return;
656 }
657
658 switch ( mCameraNavigationMode )
659 {
661 {
662 onKeyPressedFlyNavigation( event );
663 break;
664 }
665
667 {
668 onKeyPressedTerrainNavigation( event );
669 break;
670 }
671 }
672}
673
674void QgsCameraController::onKeyPressedTerrainNavigation( Qt3DInput::QKeyEvent *event )
675{
676 const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
677 const bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
678
679 int tx = 0, ty = 0, tElev = 0;
680 switch ( event->key() )
681 {
682 case Qt::Key_Left:
683 tx -= 1;
684 break;
685 case Qt::Key_Right:
686 tx += 1;
687 break;
688
689 case Qt::Key_Up:
690 ty += 1;
691 break;
692 case Qt::Key_Down:
693 ty -= 1;
694 break;
695
696 case Qt::Key_PageDown:
697 tElev -= 1;
698 break;
699 case Qt::Key_PageUp:
700 tElev += 1;
701 break;
702 }
703
704 if ( tx || ty )
705 {
706 if ( !hasShift && !hasCtrl )
707 {
708 moveView( tx, ty );
709 }
710 else if ( hasShift && !hasCtrl )
711 {
712 // rotate/tilt using keyboard (camera moves as it rotates around its view center)
715 }
716 else if ( hasCtrl && !hasShift )
717 {
718 // rotate/tilt using keyboard (camera stays at one position as it rotates)
719 const float diffPitch = ty; // down key = rotating camera down
720 const float diffYaw = -tx; // right key = rotating camera to the right
721 rotateCamera( diffPitch, diffYaw );
722 }
723 }
724
725 if ( tElev )
726 {
727 QgsVector3D center = mCameraPose.centerPoint();
728 center.set( center.x(), center.y(), center.z() + tElev * 10 );
729 mCameraPose.setCenterPoint( center );
730 updateCameraFromPose();
731 }
732}
733
734void QgsCameraController::onKeyPressedFlyNavigation( Qt3DInput::QKeyEvent *event )
735{
736 switch ( event->key() )
737 {
738 case Qt::Key_QuoteLeft:
739 {
740 // toggle mouse lock mode
741 mCaptureFpsMouseMovements = !mCaptureFpsMouseMovements;
742 mIgnoreNextMouseMove = true;
743 if ( mCaptureFpsMouseMovements )
744 {
745 qApp->setOverrideCursor( QCursor( Qt::BlankCursor ) );
746 }
747 else
748 {
749 qApp->restoreOverrideCursor();
750 }
751 return;
752 }
753
754 case Qt::Key_Escape:
755 {
756 // always exit mouse lock mode
757 if ( mCaptureFpsMouseMovements )
758 {
759 mCaptureFpsMouseMovements = false;
760 mIgnoreNextMouseMove = true;
761 qApp->restoreOverrideCursor();
762 return;
763 }
764 break;
765 }
766
767 default:
768 break;
769 }
770
771 if ( event->isAutoRepeat() )
772 return;
773
774 mDepressedKeys.insert( event->key() );
775}
776
777void QgsCameraController::walkView( double tx, double ty, double tz )
778{
779 const QVector3D cameraUp = mCamera->upVector().normalized();
780 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
781 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
782
783 QVector3D cameraPosDiff( 0.0f, 0.0f, 0.0f );
784
785 if ( tx != 0.0 )
786 {
787 cameraPosDiff += static_cast<float>( tx ) * cameraFront;
788 }
789 if ( ty != 0.0 )
790 {
791 cameraPosDiff += static_cast<float>( ty ) * cameraLeft;
792 }
793 if ( tz != 0.0 )
794 {
795 cameraPosDiff += static_cast<float>( tz ) * QVector3D( 0.0f, 0.0f, 1.0f );
796 }
797
798 moveCameraPositionBy( cameraPosDiff );
799}
800
801void QgsCameraController::applyFlyModeKeyMovements()
802{
803 // shift = "run", ctrl = "slow walk"
804 const bool shiftPressed = mDepressedKeys.contains( Qt::Key_Shift );
805 const bool ctrlPressed = mDepressedKeys.contains( Qt::Key_Control );
806
807 const double movementSpeed = mCameraMovementSpeed * ( shiftPressed ? 2 : 1 ) * ( ctrlPressed ? 0.1 : 1 );
808
809 bool changed = false;
810 double x = 0.0;
811 double y = 0.0;
812 double z = 0.0;
813 if ( mDepressedKeys.contains( Qt::Key_Left ) || mDepressedKeys.contains( Qt::Key_A ) )
814 {
815 changed = true;
816 y += movementSpeed;
817 }
818
819 if ( mDepressedKeys.contains( Qt::Key_Right ) || mDepressedKeys.contains( Qt::Key_D ) )
820 {
821 changed = true;
822 y -= movementSpeed;
823 }
824
825 if ( mDepressedKeys.contains( Qt::Key_Up ) || mDepressedKeys.contains( Qt::Key_W ) )
826 {
827 changed = true;
828 x += movementSpeed;
829 }
830
831 if ( mDepressedKeys.contains( Qt::Key_Down ) || mDepressedKeys.contains( Qt::Key_S ) )
832 {
833 changed = true;
834 x -= movementSpeed;
835 }
836
837 // note -- vertical axis movements are slower by default then horizontal ones, as GIS projects
838 // tend to have much more limited elevation range vs ground range
839 static constexpr double ELEVATION_MOVEMENT_SCALE = 0.5;
840 if ( mDepressedKeys.contains( Qt::Key_PageUp ) || mDepressedKeys.contains( Qt::Key_E ) )
841 {
842 changed = true;
843 z += ELEVATION_MOVEMENT_SCALE * movementSpeed;
844 }
845
846 if ( mDepressedKeys.contains( Qt::Key_PageDown ) || mDepressedKeys.contains( Qt::Key_Q ) )
847 {
848 changed = true;
849 z -= ELEVATION_MOVEMENT_SCALE * movementSpeed;
850 }
851
852 if ( changed )
853 walkView( x, y, z );
854}
855
856void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent *mouse )
857{
858 const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
859 const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
860
861 const double dx = mCaptureFpsMouseMovements ? QCursor::pos().x() - mMousePos.x() : mouse->x() - mMousePos.x();
862 const double dy = mCaptureFpsMouseMovements ? QCursor::pos().y() - mMousePos.y() : mouse->y() - mMousePos.y();
863 mMousePos = mCaptureFpsMouseMovements ? QCursor::pos() : QPoint( mouse->x(), mouse->y() );
864
865 if ( mIgnoreNextMouseMove )
866 {
867 mIgnoreNextMouseMove = false;
868 return;
869 }
870
871 if ( hasMiddleButton )
872 {
873 // middle button drag = pan camera in place (strafe)
874 const QVector3D cameraUp = mCamera->upVector().normalized();
875 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
876 const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
877 const QVector3D cameraPosDiff = -dx * cameraLeft - dy * cameraUp;
878 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 10.0 );
879 }
880 else if ( hasRightButton )
881 {
882 // right button drag = camera dolly
883 const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
884 const QVector3D cameraPosDiff = dy * cameraFront;
885 moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 5.0 );
886 }
887 else
888 {
889 if ( mCaptureFpsMouseMovements )
890 {
891 float diffPitch = -0.2f * dy;
892 switch ( mVerticalAxisInversion )
893 {
895 diffPitch *= -1;
896 break;
897
900 break;
901 }
902
903 const float diffYaw = -0.2f * dx;
904 rotateCamera( diffPitch, diffYaw );
905 }
906 else if ( mouse->buttons() & Qt::LeftButton )
907 {
908 float diffPitch = -0.2f * dy;
909 switch ( mVerticalAxisInversion )
910 {
913 diffPitch *= -1;
914 break;
915
917 break;
918 }
919 const float diffYaw = -0.2f * dx;
920 rotateCamera( diffPitch, diffYaw );
921 }
922 }
923
924 if ( mCaptureFpsMouseMovements )
925 {
926 mIgnoreNextMouseMove = true;
927
928 // reset cursor back to center of map widget
929 emit setCursorPosition( QPoint( mScene->engine()->size().width() / 2, mScene->engine()->size().height() / 2 ) );
930 }
931}
932
933void QgsCameraController::onKeyReleased( Qt3DInput::QKeyEvent *event )
934{
935 if ( !mInputHandlersEnabled )
936 return;
937
938 if ( event->isAutoRepeat() )
939 return;
940
941 mDepressedKeys.remove( event->key() );
942}
943
945{
946 // Tilt up the view by deltaPitch around the view center (camera moves)
947 float pitch = mCameraPose.pitchAngle();
948 pitch -= deltaPitch; // down key = moving camera toward terrain
949 mCameraPose.setPitchAngle( pitch );
950 updateCameraFromPose();
951}
952
954{
955 // Rotate clockwise the view by deltaYaw around the view center (camera moves)
956 float yaw = mCameraPose.headingAngle();
957 yaw -= deltaYaw; // right key = moving camera clockwise
958 mCameraPose.setHeadingAngle( yaw );
959 updateCameraFromPose();
960}
961
963{
964 mCameraPose.setHeadingAngle( angle );
965 updateCameraFromPose();
966}
967
968void QgsCameraController::moveView( float tx, float ty )
969{
970 const float yaw = mCameraPose.headingAngle();
971 const float dist = mCameraPose.distanceFromCenterPoint();
972 const float x = tx * dist * 0.02f;
973 const float y = -ty * dist * 0.02f;
974
975 // moving with keyboard - take into account yaw of camera
976 const float t = sqrt( x * x + y * y );
977 const float a = atan2( y, x ) - yaw * M_PI / 180;
978 const float dx = cos( a ) * t;
979 const float dy = sin( a ) * t;
980
981 QgsVector3D center = mCameraPose.centerPoint();
982 center.set( center.x() + dx, center.y() - dy, center.z() );
983 mCameraPose.setCenterPoint( center );
984 updateCameraFromPose();
985}
986
988{
989 if ( event->key() == Qt::Key_QuoteLeft )
990 return true;
991
992 switch ( mCameraNavigationMode )
993 {
995 {
996 switch ( event->key() )
997 {
998 case Qt::Key_Left:
999 case Qt::Key_A:
1000 case Qt::Key_Right:
1001 case Qt::Key_D:
1002 case Qt::Key_Up:
1003 case Qt::Key_W:
1004 case Qt::Key_Down:
1005 case Qt::Key_S:
1006 case Qt::Key_PageUp:
1007 case Qt::Key_E:
1008 case Qt::Key_PageDown:
1009 case Qt::Key_Q:
1010 return true;
1011
1012 case Qt::Key_Escape:
1013 if ( mCaptureFpsMouseMovements )
1014 return true;
1015 break;
1016
1017 default:
1018 break;
1019 }
1020 break;
1021 }
1022
1024 {
1025 switch ( event->key() )
1026 {
1027 case Qt::Key_Left:
1028 case Qt::Key_Right:
1029 case Qt::Key_PageUp:
1030 case Qt::Key_PageDown:
1031 return true;
1032
1033 default:
1034 break;
1035 }
1036 break;
1037 }
1038 }
1039 return false;
1040}
1041
1042void QgsCameraController::depthBufferCaptured( const QImage &depthImage )
1043{
1044 mDepthBufferImage = depthImage;
1045 mDepthBufferIsReady = true;
1046 mDepthBufferNonVoidAverage = -1;
1047
1048 if ( mCurrentOperation == MouseOperation::ZoomWheel )
1049 {
1050 handleTerrainNavigationWheelZoom();
1051 }
1052}
1053
1054bool QgsCameraController::isATranslationRotationSequence( MouseOperation newOperation ) const
1055{
1056 return std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), newOperation ) != std::end( mTranslateOrRotate ) && std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), mCurrentOperation ) != std::end( mTranslateOrRotate );
1057}
1058
1059void QgsCameraController::setMouseParameters( const MouseOperation &newOperation, const QPoint &clickPoint )
1060{
1061 if ( newOperation == mCurrentOperation )
1062 {
1063 return;
1064 }
1065
1066 if ( newOperation == MouseOperation::None )
1067 {
1068 mClickPoint = QPoint();
1069 }
1070 // click point and rotation angles are updated if:
1071 // - it has never been computed
1072 // - the current and new operations are both rotation and translation
1073 // Indeed, if the sequence such as rotation -> zoom -> rotation updating mClickPoint on
1074 // the click point does not need to be updated because the relative mouse position is kept
1075 // during a zoom operation
1076 else if ( mClickPoint.isNull() || isATranslationRotationSequence( newOperation ) )
1077 {
1078 mClickPoint = clickPoint;
1079 mRotationPitch = mCameraPose.pitchAngle();
1080 mRotationYaw = mCameraPose.headingAngle();
1081 }
1082 mCurrentOperation = newOperation;
1083 mDepthBufferIsReady = false;
1084 mRotationCenterCalculated = false;
1085 mDragPointCalculated = false;
1086 mZoomPointCalculated = false;
1087
1088 if ( mCurrentOperation != MouseOperation::None && mCurrentOperation != MouseOperation::RotationCamera )
1089 {
1091
1092 mCameraBefore->setProjectionMatrix( mCamera->projectionMatrix() );
1093 mCameraBefore->setNearPlane( mCamera->nearPlane() );
1094 mCameraBefore->setFarPlane( mCamera->farPlane() );
1095 mCameraBefore->setAspectRatio( mCamera->aspectRatio() );
1096 mCameraBefore->setFieldOfView( mCamera->fieldOfView() );
1097 mCameraPose.updateCamera( mCameraBefore.get() );
1098 }
1099}
1100
1102{
1103 QgsVector3D diff = origin - mOrigin;
1104 mCameraPose.setCenterPoint( mCameraPose.centerPoint() - diff );
1105
1106 // update other members that depend on world coordinates
1107 mCameraBefore->setPosition( ( QgsVector3D( mCameraBefore->position() ) - diff ).toVector3D() );
1108 mCameraBefore->setViewCenter( ( QgsVector3D( mCameraBefore->viewCenter() ) - diff ).toVector3D() );
1109 mDragPoint = ( QgsVector3D( mDragPoint ) - diff ).toVector3D();
1110 mRotationCenter = ( QgsVector3D( mRotationCenter ) - diff ).toVector3D();
1111
1112 mOrigin = origin;
1113
1114 updateCameraFromPose();
1115}
VerticalAxisInversion
Vertical axis inversion options for 3D views.
Definition qgis.h:4009
@ Always
Always invert vertical axis movements.
@ Never
Never invert vertical axis movements.
@ WhenDragging
Invert vertical axis movements when dragging in first person modes.
NavigationMode
The navigation mode used by 3D cameras.
Definition qgis.h:3997
@ TerrainBased
The default navigation based on the terrain.
@ Walk
Uses WASD keys or arrows to navigate in walking (first person) manner.
QgsAbstract3DEngine * engine() const
Returns the abstract 3D engine.
QgsTerrainEntity * terrainEntity()
Returns terrain entity (may be temporarily nullptr)
static QQuaternion rotationFromPitchHeadingAngles(float pitchAngle, float headingAngle)
Returns rotation quaternion that performs rotation around X axis by pitchAngle, followed by rotation ...
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:241
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.
virtual QSize size() const =0
Returns size of the engine's rendering area in pixels.
void navigationModeChanged(Qgis::NavigationMode mode)
Emitted when the navigation mode is changed using the hotkey ctrl + ~.
void readXml(const QDomElement &elem)
Reads camera configuration from the given DOM element.
float pitch() const
Returns pitch angle in degrees (0 = looking from the top, 90 = looking from the side).
~QgsCameraController() override
float yaw() const
Returns yaw angle in degrees.
void requestDepthBufferCapture()
Emitted to ask for the depth buffer image.
void tiltUpAroundViewCenter(float deltaPitch)
Tilt up the view by deltaPitch around the view center (camera moves)
void setVerticalAxisInversion(Qgis::VerticalAxisInversion inversion)
Sets the vertical axis inversion behavior.
bool willHandleKeyEvent(QKeyEvent *event)
Returns true if the camera controller will handle the specified key event, preventing it from being i...
float distance() const
Returns distance of the camera from the point it is looking at.
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 rotateCamera(float diffPitch, float diffYaw)
Rotates the camera on itself.
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.
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...
void zoom(float factor)
Zoom the map by factor.
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.
float headingAngle() const
Returns heading (yaw) angle in degrees.
QgsVector3D centerPoint() const
Returns center point (towards which point the camera is looking)
float pitchAngle() const
Returns pitch angle in degrees.
float distanceFromCenterPoint() const
Returns distance of the camera from the center point.
void setPitchAngle(float pitch)
Sets pitch angle in degrees.
void setCenterPoint(const QgsVector3D &point)
Sets center point (towards which point the camera is looking)
void setHeadingAngle(float heading)
Sets heading (yaw) angle in degrees.
void setDistanceFromCenterPoint(float distance)
Sets distance of the camera from the center point.
void updateCamera(Qt3DRender::QCamera *camera)
Update Qt3D camera view matrix based on the pose.
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:52
double x() const
Returns X coordinate.
Definition qgsvector3d.h:48
void set(double x, double y, double z)
Sets vector coordinates.
Definition qgsvector3d.h:73
QWindow * window()
Returns the internal 3D window where all the rendered output is displayed.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41