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