QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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 "qgsraycastingutils_p.h"
18 #include "qgsterrainentity_p.h"
19 #include "qgsvector3d.h"
20 
21 #include "qgis.h"
22 
23 #include <QDomDocument>
24 #include <Qt3DRender/QCamera>
25 #include <Qt3DRender/QObjectPicker>
26 #include <Qt3DRender/QPickEvent>
27 #include <Qt3DInput>
28 
29 
30 QgsCameraController::QgsCameraController( Qt3DCore::QNode *parent )
31  : Qt3DCore::QEntity( parent )
32  , mMouseDevice( new Qt3DInput::QMouseDevice() )
33  , mKeyboardDevice( new Qt3DInput::QKeyboardDevice() )
34  , mMouseHandler( new Qt3DInput::QMouseHandler )
35  , mKeyboardHandler( new Qt3DInput::QKeyboardHandler )
36 {
37 
38  mMouseHandler->setSourceDevice( mMouseDevice );
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( mKeyboardDevice );
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::QMouseHandler::setEnabled );
61 }
62 
63 void QgsCameraController::setTerrainEntity( QgsTerrainEntity *te )
64 {
65  mTerrainEntity = te;
66 
67  // object picker for terrain for correct map panning
68  connect( te->terrainPicker(), &Qt3DRender::QObjectPicker::pressed, this, &QgsCameraController::onPickerMousePressed );
69 }
70 
71 void QgsCameraController::setCamera( Qt3DRender::QCamera *camera )
72 {
73  if ( mCamera == camera )
74  return;
75  mCamera = camera;
76 
77  mCameraPose.updateCamera( mCamera ); // initial setup
78 
79  // TODO: set camera's parent if not set already?
80  // TODO: registerDestructionHelper (?)
81  emit cameraChanged();
82 }
83 
85 {
86  if ( mViewport == viewport )
87  return;
88 
89  mViewport = viewport;
90  emit viewportChanged();
91 }
92 
93 
94 static QVector3D unproject( QVector3D v, const QMatrix4x4 &modelView, const QMatrix4x4 &projection, QRect viewport )
95 {
96  // Reimplementation of QVector3D::unproject() - see qtbase/src/gui/math3d/qvector3d.cpp
97  // The only difference is that the original implementation uses tolerance 1e-5
98  // (see qFuzzyIsNull()) as a protection against division by zero. For us it is however
99  // common to get lower values (e.g. as low as 1e-8 when zoomed out to the whole Earth with web mercator).
100 
101  QMatrix4x4 inverse = QMatrix4x4( projection * modelView ).inverted();
102 
103  QVector4D tmp( v, 1.0f );
104  tmp.setX( ( tmp.x() - float( viewport.x() ) ) / float( viewport.width() ) );
105  tmp.setY( ( tmp.y() - float( viewport.y() ) ) / float( viewport.height() ) );
106  tmp = tmp * 2.0f - QVector4D( 1.0f, 1.0f, 1.0f, 1.0f );
107 
108  QVector4D obj = inverse * tmp;
109  if ( qgsDoubleNear( obj.w(), 0, 1e-10 ) )
110  obj.setW( 1.0f );
111  obj /= obj.w();
112  return obj.toVector3D();
113 }
114 
115 
116 float find_x_on_line( float x0, float y0, float x1, float y1, float y )
117 {
118  float d_x = x1 - x0;
119  float d_y = y1 - y0;
120  float k = ( y - y0 ) / d_y; // TODO: can we have d_y == 0 ?
121  return x0 + k * d_x;
122 }
123 
124 QPointF screen_point_to_point_on_plane( QPointF pt, QRect viewport, Qt3DRender::QCamera *camera, float y )
125 {
126  // get two points of the ray
127  QVector3D l0 = unproject( QVector3D( pt.x(), viewport.height() - pt.y(), 0 ), camera->viewMatrix(), camera->projectionMatrix(), viewport );
128  QVector3D l1 = unproject( QVector3D( pt.x(), viewport.height() - pt.y(), 1 ), camera->viewMatrix(), camera->projectionMatrix(), viewport );
129 
130  QVector3D p0( 0, y, 0 ); // a point on the plane
131  QVector3D n( 0, 1, 0 ); // normal of the plane
132  QVector3D l = l1 - l0; // vector in the direction of the line
133  float d = QVector3D::dotProduct( p0 - l0, n ) / QVector3D::dotProduct( l, n );
134  QVector3D p = d * l + l0;
135 
136  return QPointF( p.x(), p.z() );
137 }
138 
139 
140 void QgsCameraController::rotateCamera( float diffPitch, float diffYaw )
141 {
142  float pitch = mCameraPose.pitchAngle();
143  float yaw = mCameraPose.headingAngle();
144 
145  if ( pitch + diffPitch > 180 )
146  diffPitch = 180 - pitch; // prevent going over the head
147  if ( pitch + diffPitch < 0 )
148  diffPitch = 0 - pitch; // prevent going over the head
149 
150  // Is it always going to be love/hate relationship with quaternions???
151  // This quaternion combines two rotations:
152  // - first it undoes the previously applied rotation so we have do not have any rotation compared to world coords
153  // - then it applies new rotation
154  // (We can't just apply our euler angles difference because the camera may be already rotated)
155  QQuaternion q = QQuaternion::fromEulerAngles( pitch + diffPitch, yaw + diffYaw, 0 ) *
156  QQuaternion::fromEulerAngles( pitch, yaw, 0 ).conjugated();
157 
158  // get camera's view vector, rotate it to get new view center
159  QVector3D position = mCamera->position();
160  QVector3D viewCenter = mCamera->viewCenter();
161  QVector3D viewVector = viewCenter - position;
162  QVector3D cameraToCenter = q * viewVector;
163  viewCenter = position + cameraToCenter;
164 
165  mCameraPose.setCenterPoint( viewCenter );
166  mCameraPose.setPitchAngle( pitch + diffPitch );
167  mCameraPose.setHeadingAngle( yaw + diffYaw );
168 }
169 
170 
172 {
173  Q_UNUSED( dt )
174 }
175 
177 {
178  setViewFromTop( 0, 0, distance );
179 }
180 
181 void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
182 {
183  QgsCameraPose camPose;
184  camPose.setCenterPoint( QgsVector3D( worldX, 0, worldY ) );
185  camPose.setDistanceFromCenterPoint( distance );
186  camPose.setHeadingAngle( yaw );
187 
188  // a basic setup to make frustum depth range long enough that it does not cull everything
189  mCamera->setNearPlane( distance / 2 );
190  mCamera->setFarPlane( distance * 2 );
191 
192  setCameraPose( camPose );
193 }
194 
196 {
197  return mCameraPose.centerPoint();
198 }
199 
200 void QgsCameraController::setLookingAtPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
201 {
202  QgsCameraPose camPose;
203  camPose.setCenterPoint( point );
204  camPose.setDistanceFromCenterPoint( distance );
205  camPose.setPitchAngle( pitch );
206  camPose.setHeadingAngle( yaw );
207  setCameraPose( camPose );
208 }
209 
211 {
212  if ( camPose == mCameraPose )
213  return;
214 
215  mCameraPose = camPose;
216 
217  if ( mCamera )
218  mCameraPose.updateCamera( mCamera );
219 
220  emit cameraChanged();
221 }
222 
223 QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
224 {
225  QDomElement elemCamera = doc.createElement( QStringLiteral( "camera" ) );
226  elemCamera.setAttribute( QStringLiteral( "x" ), mCameraPose.centerPoint().x() );
227  elemCamera.setAttribute( QStringLiteral( "y" ), mCameraPose.centerPoint().z() );
228  elemCamera.setAttribute( QStringLiteral( "elev" ), mCameraPose.centerPoint().y() );
229  elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraPose.distanceFromCenterPoint() );
230  elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraPose.pitchAngle() );
231  elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraPose.headingAngle() );
232  return elemCamera;
233 }
234 
235 void QgsCameraController::readXml( const QDomElement &elem )
236 {
237  float x = elem.attribute( QStringLiteral( "x" ) ).toFloat();
238  float y = elem.attribute( QStringLiteral( "y" ) ).toFloat();
239  float elev = elem.attribute( QStringLiteral( "elev" ) ).toFloat();
240  float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
241  float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
242  float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
243  setLookingAtPoint( QgsVector3D( x, elev, y ), dist, pitch, yaw );
244 }
245 
246 void QgsCameraController::updateCameraFromPose( bool centerPointChanged )
247 {
248  if ( std::isnan( mCameraPose.centerPoint().x() ) || std::isnan( mCameraPose.centerPoint().y() ) || std::isnan( mCameraPose.centerPoint().z() ) )
249  {
250  // something went horribly wrong but we need to at least try to fix it somehow
251  qDebug() << "camera position got NaN!";
252  mCameraPose.setCenterPoint( QgsVector3D( 0, 0, 0 ) );
253  }
254 
255  if ( mCameraPose.pitchAngle() > 180 )
256  mCameraPose.setPitchAngle( 180 ); // prevent going over the head
257  if ( mCameraPose.pitchAngle() < 0 )
258  mCameraPose.setPitchAngle( 0 ); // prevent going over the head
259  if ( mCameraPose.distanceFromCenterPoint() < 10 )
260  mCameraPose.setDistanceFromCenterPoint( 10 );
261 
262  if ( mCamera )
263  mCameraPose.updateCamera( mCamera );
264 
265  if ( mCamera && mTerrainEntity && centerPointChanged )
266  {
267  // figure out our distance from terrain and update the camera's view center
268  // so that camera tilting and rotation is around a point on terrain, not an point at fixed elevation
269  QVector3D intersectionPoint;
270  QgsRayCastingUtils::Ray3D ray = QgsRayCastingUtils::rayForCameraCenter( mCamera );
271  if ( mTerrainEntity->rayIntersection( ray, intersectionPoint ) )
272  {
273  float dist = ( intersectionPoint - mCamera->position() ).length();
274  mCameraPose.setDistanceFromCenterPoint( dist );
275  mCameraPose.setCenterPoint( QgsVector3D( intersectionPoint ) );
276  mCameraPose.updateCamera( mCamera );
277  }
278  }
279 
280  emit cameraChanged();
281 }
282 
283 void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
284 {
285  int dx = mouse->x() - mMousePos.x();
286  int dy = mouse->y() - mMousePos.y();
287 
288  bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
289  bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
290  bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
291  bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
292  bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
293 
294  if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
295  {
296  // rotate/tilt using mouse (camera moves as it rotates around its view center)
297  float pitch = mCameraPose.pitchAngle();
298  float yaw = mCameraPose.headingAngle();
299  pitch += dy;
300  yaw -= dx / 2;
301  mCameraPose.setPitchAngle( pitch );
302  mCameraPose.setHeadingAngle( yaw );
303  updateCameraFromPose();
304  }
305  else if ( hasLeftButton && hasCtrl && !hasShift )
306  {
307  // rotate/tilt using mouse (camera stays at one position as it rotates)
308  float diffPitch = 0.2f * dy;
309  float diffYaw = 0.2f * -dx / 2;
310  rotateCamera( diffPitch, diffYaw );
311  updateCameraFromPose( true );
312  }
313  else if ( hasLeftButton && !hasShift && !hasCtrl )
314  {
315  // translation works as if one grabbed a point on the plane and dragged it
316  // i.e. find out x,z of the previous mouse point, find out x,z of the current mouse point
317  // and use the difference
318 
319  float z = mLastPressedHeight;
320  QPointF p1 = screen_point_to_point_on_plane( QPointF( mMousePos.x(), mMousePos.y() ), mViewport, mCamera, z );
321  QPointF p2 = screen_point_to_point_on_plane( QPointF( mouse->x(), mouse->y() ), mViewport, mCamera, z );
322 
323  QgsVector3D center = mCameraPose.centerPoint();
324  center.set( center.x() - ( p2.x() - p1.x() ), center.y(), center.z() - ( p2.y() - p1.y() ) );
325  mCameraPose.setCenterPoint( center );
326  updateCameraFromPose( true );
327  }
328  else if ( hasRightButton && !hasShift && !hasCtrl )
329  {
330  zoom( dy );
331  }
332 
333  mMousePos = QPoint( mouse->x(), mouse->y() );
334 }
335 
336 void QgsCameraController::zoom( float factor )
337 {
338  // zoom in/out
339  float dist = mCameraPose.distanceFromCenterPoint();
340  dist -= dist * factor * 0.01f;
341  mCameraPose.setDistanceFromCenterPoint( dist );
342  updateCameraFromPose();
343 }
344 
345 void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
346 {
347  float scaling = ( ( wheel->modifiers() & Qt::ControlModifier ) ? 0.1f : 1.0f ) / 1000.f;
348  float dist = mCameraPose.distanceFromCenterPoint();
349  dist -= dist * scaling * wheel->angleDelta().y();
350  mCameraPose.setDistanceFromCenterPoint( dist );
351  updateCameraFromPose();
352 }
353 
354 void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
355 {
356  Q_UNUSED( mouse )
357  mKeyboardHandler->setFocus( true );
358 }
359 
360 void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
361 {
362  Q_UNUSED( mouse )
363 }
364 
365 void QgsCameraController::onKeyPressed( Qt3DInput::QKeyEvent *event )
366 {
367  bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
368  bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
369 
370  int tx = 0, ty = 0, tElev = 0;
371  switch ( event->key() )
372  {
373  case Qt::Key_Left:
374  tx -= 1;
375  break;
376  case Qt::Key_Right:
377  tx += 1;
378  break;
379 
380  case Qt::Key_Up:
381  ty += 1;
382  break;
383  case Qt::Key_Down:
384  ty -= 1;
385  break;
386 
387  case Qt::Key_PageDown:
388  tElev -= 1;
389  break;
390  case Qt::Key_PageUp:
391  tElev += 1;
392  break;
393  }
394 
395  if ( tx || ty )
396  {
397  if ( !hasShift && !hasCtrl )
398  {
399  moveView( tx, ty );
400  }
401  else if ( hasShift && !hasCtrl )
402  {
403  // rotate/tilt using keyboard (camera moves as it rotates around its view center)
406  }
407  else if ( hasCtrl && !hasShift )
408  {
409  // rotate/tilt using keyboard (camera stays at one position as it rotates)
410  float diffPitch = ty; // down key = rotating camera down
411  float diffYaw = -tx; // right key = rotating camera to the right
412  rotateCamera( diffPitch, diffYaw );
413  updateCameraFromPose( true );
414  }
415  }
416 
417  if ( tElev )
418  {
419  QgsVector3D center = mCameraPose.centerPoint();
420  center.set( center.x(), center.y() + tElev * 10, center.z() );
421  mCameraPose.setCenterPoint( center );
422  updateCameraFromPose( true );
423  }
424 }
425 
426 void QgsCameraController::onKeyReleased( Qt3DInput::QKeyEvent *event )
427 {
428  Q_UNUSED( event )
429 }
430 
431 void QgsCameraController::onPickerMousePressed( Qt3DRender::QPickEvent *pick )
432 {
433  mLastPressedHeight = pick->worldIntersection().y();
434 }
435 
437 {
438  // Tilt up the view by deltaPitch around the view center (camera moves)
439  float pitch = mCameraPose.pitchAngle();
440  pitch -= deltaPitch; // down key = moving camera toward terrain
441  mCameraPose.setPitchAngle( pitch );
442  updateCameraFromPose();
443 }
444 
446 {
447  // Rotate clockwise the view by deltaYaw around the view center (camera moves)
448  float yaw = mCameraPose.headingAngle();
449  yaw -= deltaYaw; // right key = moving camera clockwise
450  mCameraPose.setHeadingAngle( yaw );
451  updateCameraFromPose();
452  qInfo() << "Delta yaw: " << deltaYaw;
453  qInfo() << "Yaw: " << yaw;
454 }
455 
457 {
458  mCameraPose.setHeadingAngle( angle );
459  updateCameraFromPose();
460 }
461 
462 void QgsCameraController::moveView( float tx, float ty )
463 {
464  float yaw = mCameraPose.headingAngle();
465  float dist = mCameraPose.distanceFromCenterPoint();
466  float x = tx * dist * 0.02f;
467  float y = -ty * dist * 0.02f;
468 
469  // moving with keyboard - take into account yaw of camera
470  float t = sqrt( x * x + y * y );
471  float a = atan2( y, x ) - yaw * M_PI / 180;
472  float dx = cos( a ) * t;
473  float dy = sin( a ) * t;
474 
475  QgsVector3D center = mCameraPose.centerPoint();
476  center.set( center.x() + dx, center.y(), center.z() + dy );
477  mCameraPose.setCenterPoint( center );
478  updateCameraFromPose( true );
479 }
3 Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double preci...
Definition: qgsvector3d.h:31
void cameraChanged()
Emitted when camera has been updated.
QgsCameraController(Qt3DCore::QNode *parent=nullptr)
Constructs the camera controller with optional parent node that will take ownership.
Qt3DRender::QCamera * camera() const
Returns camera that is being controlled.
void frameTriggered(float dt)
Called internally from 3D scene when a new frame is generated. Updates camera according to keyboard/m...
void setCameraHeadingAngle(float angle)
Set camera heading to angle (used for rotating the view)
float pitch() const
Returns pitch angle in degrees (0 = looking from the top, 90 = looking from the side).
void tiltUpAroundViewCenter(float deltaPitch)
Tilt up the view by deltaPitch around the view center (camera moves)
void setViewport(QRect viewport)
Sets viewport rectangle. Called internally from 3D canvas. Allows conversion of mouse coordinates...
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:280
QgsVector3D centerPoint() const
Returns center point (towards which point the camera is looking)
Definition: qgscamerapose.h:46
QRect viewport() const
Returns viewport rectangle.
void resetView(float distance)
Move camera back to the initial position (looking down towards origin of world&#39;s coordinates) ...
double y() const
Returns Y coordinate.
Definition: qgsvector3d.h:51
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:786
void set(double x, double y, double z)
Sets vector coordinates.
Definition: qgsvector3d.h:56
3 Class that encapsulates camera pose in a 3D scene.
Definition: qgscamerapose.h:41
float yaw() const
Returns yaw angle in degrees.
double z() const
Returns Z coordinate.
Definition: qgsvector3d.h:53
float find_x_on_line(float x0, float y0, float x1, float y1, float y)
void viewportChanged()
Emitted when viewport rectangle has been updated.
void setPitchAngle(float pitch)
Sets pitch angle in degrees.
Definition: qgscamerapose.h:58
float distanceFromCenterPoint() const
Returns distance of the camera from the center point.
Definition: qgscamerapose.h:51
void readXml(const QDomElement &elem)
Reads camera configuration from the given DOM element.
float headingAngle() const
Returns heading (yaw) angle in degrees.
Definition: qgscamerapose.h:61
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...
QDomElement writeXml(QDomDocument &doc) const
Writes camera configuration to the given DOM element.
void setCamera(Qt3DRender::QCamera *camera)
Assigns camera that should be controlled by this class. Called internally from 3D scene...
void updateCamera(Qt3DRender::QCamera *camera)
Update Qt3D camera view matrix based on the pose.
QgsVector3D lookingAtPoint() const
Returns the point in the world coordinates towards which the camera is looking.
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 moveView(float tx, float ty)
Move the map by tx and ty.
void setHeadingAngle(float heading)
Sets heading (yaw) angle in degrees.
Definition: qgscamerapose.h:63
QPointF screen_point_to_point_on_plane(QPointF pt, QRect viewport, Qt3DRender::QCamera *camera, float y)
void setCenterPoint(const QgsVector3D &point)
Sets center point (towards which point the camera is looking)
Definition: qgscamerapose.h:48
float pitchAngle() const
Returns pitch angle in degrees.
Definition: qgscamerapose.h:56
void setDistanceFromCenterPoint(float distance)
Sets distance of the camera from the center point.
Definition: qgscamerapose.h:53
void setTerrainEntity(QgsTerrainEntity *te)
Connects to object picker attached to terrain entity.
void setCameraPose(const QgsCameraPose &camPose)
Sets camera pose.
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)...
float distance() const
Returns distance of the camera from the point it is looking at.
double x() const
Returns X coordinate.
Definition: qgsvector3d.h:49