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