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