QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 in/out
331  float dist = mCameraPose.distanceFromCenterPoint();
332  dist -= dist * dy * 0.01f;
333  mCameraPose.setDistanceFromCenterPoint( dist );
334  updateCameraFromPose();
335  }
336 
337  mMousePos = QPoint( mouse->x(), mouse->y() );
338 }
339 
340 void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
341 {
342  float scaling = ( ( wheel->modifiers() & Qt::ControlModifier ) ? 0.1f : 1.0f ) / 1000.f;
343  float dist = mCameraPose.distanceFromCenterPoint();
344  dist -= dist * scaling * wheel->angleDelta().y();
345  mCameraPose.setDistanceFromCenterPoint( dist );
346  updateCameraFromPose();
347 }
348 
349 void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
350 {
351  Q_UNUSED( mouse )
352  mKeyboardHandler->setFocus( true );
353 }
354 
355 void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
356 {
357  Q_UNUSED( mouse )
358 }
359 
360 void QgsCameraController::onKeyPressed( Qt3DInput::QKeyEvent *event )
361 {
362  bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
363  bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
364 
365  int tx = 0, ty = 0, tElev = 0;
366  switch ( event->key() )
367  {
368  case Qt::Key_Left:
369  tx -= 1;
370  break;
371  case Qt::Key_Right:
372  tx += 1;
373  break;
374 
375  case Qt::Key_Up:
376  ty += 1;
377  break;
378  case Qt::Key_Down:
379  ty -= 1;
380  break;
381 
382  case Qt::Key_PageDown:
383  tElev -= 1;
384  break;
385  case Qt::Key_PageUp:
386  tElev += 1;
387  break;
388  }
389 
390  if ( tx || ty )
391  {
392  if ( !hasShift && !hasCtrl )
393  {
394  float yaw = mCameraPose.headingAngle();
395  float dist = mCameraPose.distanceFromCenterPoint();
396  float x = tx * dist * 0.02f;
397  float y = -ty * dist * 0.02f;
398 
399  // moving with keyboard - take into account yaw of camera
400  float t = sqrt( x * x + y * y );
401  float a = atan2( y, x ) - yaw * M_PI / 180;
402  float dx = cos( a ) * t;
403  float dy = sin( a ) * t;
404 
405  QgsVector3D center = mCameraPose.centerPoint();
406  center.set( center.x() + dx, center.y(), center.z() + dy );
407  mCameraPose.setCenterPoint( center );
408  updateCameraFromPose( true );
409  }
410  else if ( hasShift && !hasCtrl )
411  {
412  // rotate/tilt using keyboard (camera moves as it rotates around its view center)
413  float pitch = mCameraPose.pitchAngle();
414  float yaw = mCameraPose.headingAngle();
415  pitch -= ty; // down key = moving camera toward terrain
416  yaw -= tx; // right key = moving camera clockwise
417  mCameraPose.setPitchAngle( pitch );
418  mCameraPose.setHeadingAngle( yaw );
419  updateCameraFromPose();
420  }
421  else if ( hasCtrl && !hasShift )
422  {
423  // rotate/tilt using keyboard (camera stays at one position as it rotates)
424  float diffPitch = ty; // down key = rotating camera down
425  float diffYaw = -tx; // right key = rotating camera to the right
426  rotateCamera( diffPitch, diffYaw );
427  updateCameraFromPose( true );
428  }
429  }
430 
431  if ( tElev )
432  {
433  QgsVector3D center = mCameraPose.centerPoint();
434  center.set( center.x(), center.y() + tElev * 10, center.z() );
435  mCameraPose.setCenterPoint( center );
436  updateCameraFromPose( true );
437  }
438 }
439 
440 void QgsCameraController::onKeyReleased( Qt3DInput::QKeyEvent *event )
441 {
442  Q_UNUSED( event )
443 }
444 
445 void QgsCameraController::onPickerMousePressed( Qt3DRender::QPickEvent *pick )
446 {
447  mLastPressedHeight = pick->worldIntersection().y();
448 }
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...
float pitch() const
Returns pitch angle in degrees (0 = looking from the top, 90 = looking from the side).
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:265
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
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 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