QGIS API Documentation  3.2.0-Bonn (bc43194)
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 
19 #include "qgis.h"
20 
21 #include <QDomDocument>
22 #include <Qt3DRender/QObjectPicker>
23 #include <Qt3DRender/QPickEvent>
24 
25 
26 QgsCameraController::QgsCameraController( Qt3DCore::QNode *parent )
27  : Qt3DCore::QEntity( parent )
28  , mMouseDevice( new Qt3DInput::QMouseDevice() )
29  , mKeyboardDevice( new Qt3DInput::QKeyboardDevice() )
30  , mMouseHandler( new Qt3DInput::QMouseHandler )
31  , mLogicalDevice( new Qt3DInput::QLogicalDevice() )
32  , mLeftMouseButtonAction( new Qt3DInput::QAction() )
33  , mLeftMouseButtonInput( new Qt3DInput::QActionInput() )
34  , mMiddleMouseButtonAction( new Qt3DInput::QAction() )
35  , mMiddleMouseButtonInput( new Qt3DInput::QActionInput() )
36  , mRightMouseButtonAction( new Qt3DInput::QAction() )
37  , mRightMouseButtonInput( new Qt3DInput::QActionInput() )
38  , mShiftAction( new Qt3DInput::QAction() )
39  , mShiftInput( new Qt3DInput::QActionInput() )
40  , mCtrlAction( new Qt3DInput::QAction() )
41  , mCtrlInput( new Qt3DInput::QActionInput() )
42  , mWheelAxis( new Qt3DInput::QAxis() )
43  , mMouseWheelInput( new Qt3DInput::QAnalogAxisInput() )
44  , mTxAxis( new Qt3DInput::QAxis() )
45  , mTyAxis( new Qt3DInput::QAxis() )
46  , mKeyboardTxPosInput( new Qt3DInput::QButtonAxisInput() )
47  , mKeyboardTyPosInput( new Qt3DInput::QButtonAxisInput() )
48  , mKeyboardTxNegInput( new Qt3DInput::QButtonAxisInput() )
49  , mKeyboardTyNegInput( new Qt3DInput::QButtonAxisInput() )
50 {
51 
52  // not using QAxis + QAnalogAxisInput for mouse X,Y because
53  // it is only in action when a mouse button is pressed.
54  mMouseHandler->setSourceDevice( mMouseDevice );
55  connect( mMouseHandler, &Qt3DInput::QMouseHandler::positionChanged,
56  this, &QgsCameraController::onPositionChanged );
57  addComponent( mMouseHandler );
58 
59  // TODO: keep using QAxis and QAction approach or just switch to using QMouseHandler / QKeyboardHandler?
60  // it does not feel like the former approach makes anything much simpler
61 
62  // left mouse button
63  mLeftMouseButtonInput->setButtons( QVector<int>() << Qt::LeftButton );
64  mLeftMouseButtonInput->setSourceDevice( mMouseDevice );
65  mLeftMouseButtonAction->addInput( mLeftMouseButtonInput );
66 
67  // middle mouse button
68  mMiddleMouseButtonInput->setButtons( QVector<int>() << Qt::MiddleButton );
69  mMiddleMouseButtonInput->setSourceDevice( mMouseDevice );
70  mMiddleMouseButtonAction->addInput( mMiddleMouseButtonInput );
71 
72  // right mouse button
73  mRightMouseButtonInput->setButtons( QVector<int>() << Qt::RightButton );
74  mRightMouseButtonInput->setSourceDevice( mMouseDevice );
75  mRightMouseButtonAction->addInput( mRightMouseButtonInput );
76 
77  // Mouse Wheel (Y)
78  mMouseWheelInput->setAxis( Qt3DInput::QMouseDevice::WheelY );
79  mMouseWheelInput->setSourceDevice( mMouseDevice );
80  mWheelAxis->addInput( mMouseWheelInput );
81 
82  // Keyboard shift
83  mShiftInput->setButtons( QVector<int>() << Qt::Key_Shift );
84  mShiftInput->setSourceDevice( mKeyboardDevice );
85  mShiftAction->addInput( mShiftInput );
86 
87  // Keyboard ctrl
88  mCtrlInput->setButtons( QVector<int>() << Qt::Key_Control );
89  mCtrlInput->setSourceDevice( mKeyboardDevice );
90  mCtrlAction->addInput( mCtrlInput );
91 
92  // Keyboard Pos Tx
93  mKeyboardTxPosInput->setButtons( QVector<int>() << Qt::Key_Right );
94  mKeyboardTxPosInput->setScale( 1.0f );
95  mKeyboardTxPosInput->setSourceDevice( mKeyboardDevice );
96  mTxAxis->addInput( mKeyboardTxPosInput );
97 
98  // Keyboard Pos Ty
99  mKeyboardTyPosInput->setButtons( QVector<int>() << Qt::Key_Up );
100  mKeyboardTyPosInput->setScale( 1.0f );
101  mKeyboardTyPosInput->setSourceDevice( mKeyboardDevice );
102  mTyAxis->addInput( mKeyboardTyPosInput );
103 
104  // Keyboard Neg Tx
105  mKeyboardTxNegInput->setButtons( QVector<int>() << Qt::Key_Left );
106  mKeyboardTxNegInput->setScale( -1.0f );
107  mKeyboardTxNegInput->setSourceDevice( mKeyboardDevice );
108  mTxAxis->addInput( mKeyboardTxNegInput );
109 
110  // Keyboard Neg Ty
111  mKeyboardTyNegInput->setButtons( QVector<int>() << Qt::Key_Down );
112  mKeyboardTyNegInput->setScale( -1.0f );
113  mKeyboardTyNegInput->setSourceDevice( mKeyboardDevice );
114  mTyAxis->addInput( mKeyboardTyNegInput );
115 
116  mLogicalDevice->addAction( mLeftMouseButtonAction );
117  mLogicalDevice->addAction( mMiddleMouseButtonAction );
118  mLogicalDevice->addAction( mRightMouseButtonAction );
119  mLogicalDevice->addAction( mShiftAction );
120  mLogicalDevice->addAction( mCtrlAction );
121  mLogicalDevice->addAxis( mWheelAxis );
122  mLogicalDevice->addAxis( mTxAxis );
123  mLogicalDevice->addAxis( mTyAxis );
124 
125  // Disable the logical device when the entity is disabled
126  connect( this, &Qt3DCore::QEntity::enabledChanged,
127  mLogicalDevice, &Qt3DInput::QLogicalDevice::setEnabled );
128 
129  addComponent( mLogicalDevice );
130 }
131 
132 void QgsCameraController::addTerrainPicker( Qt3DRender::QObjectPicker *picker )
133 {
134  // object picker for terrain for correct map panning
135  connect( picker, &Qt3DRender::QObjectPicker::pressed, this, &QgsCameraController::onPickerMousePressed );
136 }
137 
138 void QgsCameraController::setCamera( Qt3DRender::QCamera *camera )
139 {
140  if ( mCamera == camera )
141  return;
142  mCamera = camera;
143 
144  mCameraData.setCamera( mCamera ); // initial setup
145 
146  // TODO: set camera's parent if not set already?
147  // TODO: registerDestructionHelper (?)
148  emit cameraChanged();
149 }
150 
152 {
153  if ( mViewport == viewport )
154  return;
155 
156  mViewport = viewport;
157  emit viewportChanged();
158 }
159 
160 void QgsCameraController::setCameraData( float x, float y, float dist, float pitch, float yaw )
161 {
162  mCameraData.x = x;
163  mCameraData.y = y;
164  mCameraData.dist = dist;
165  mCameraData.pitch = pitch;
166  mCameraData.yaw = yaw;
167 
168  if ( mCamera )
169  {
170  mCameraData.setCamera( mCamera );
171  }
172 }
173 
174 
175 static QVector3D unproject( const QVector3D &v, const QMatrix4x4 &modelView, const QMatrix4x4 &projection, const QRect &viewport )
176 {
177  // Reimplementation of QVector3D::unproject() - see qtbase/src/gui/math3d/qvector3d.cpp
178  // The only difference is that the original implementation uses tolerance 1e-5
179  // (see qFuzzyIsNull()) as a protection against division by zero. For us it is however
180  // common to get lower values (e.g. as low as 1e-8 when zoomed out to the whole Earth with web mercator).
181 
182  QMatrix4x4 inverse = QMatrix4x4( projection * modelView ).inverted();
183 
184  QVector4D tmp( v, 1.0f );
185  tmp.setX( ( tmp.x() - float( viewport.x() ) ) / float( viewport.width() ) );
186  tmp.setY( ( tmp.y() - float( viewport.y() ) ) / float( viewport.height() ) );
187  tmp = tmp * 2.0f - QVector4D( 1.0f, 1.0f, 1.0f, 1.0f );
188 
189  QVector4D obj = inverse * tmp;
190  if ( qgsDoubleNear( obj.w(), 0, 1e-10 ) )
191  obj.setW( 1.0f );
192  obj /= obj.w();
193  return obj.toVector3D();
194 }
195 
196 
197 float find_x_on_line( float x0, float y0, float x1, float y1, float y )
198 {
199  float d_x = x1 - x0;
200  float d_y = y1 - y0;
201  float k = ( y - y0 ) / d_y; // TODO: can we have d_y == 0 ?
202  return x0 + k * d_x;
203 }
204 
205 QPointF screen_point_to_point_on_plane( const QPointF &pt, const QRect &viewport, Qt3DRender::QCamera *camera, float y )
206 {
207  // get two points of the ray
208  QVector3D l0 = unproject( QVector3D( pt.x(), viewport.height() - pt.y(), 0 ), camera->viewMatrix(), camera->projectionMatrix(), viewport );
209  QVector3D l1 = unproject( QVector3D( pt.x(), viewport.height() - pt.y(), 1 ), camera->viewMatrix(), camera->projectionMatrix(), viewport );
210 
211  QVector3D p0( 0, y, 0 ); // a point on the plane
212  QVector3D n( 0, 1, 0 ); // normal of the plane
213  QVector3D l = l1 - l0; // vector in the direction of the line
214  float d = QVector3D::dotProduct( p0 - l0, n ) / QVector3D::dotProduct( l, n );
215  QVector3D p = d * l + l0;
216 
217  return QPointF( p.x(), p.z() );
218 }
219 
220 
222 {
223  if ( mCamera == nullptr )
224  return;
225 
226  CameraData oldCamData = mCameraData;
227 
228  int dx = mMousePos.x() - mLastMousePos.x();
229  int dy = mMousePos.y() - mLastMousePos.y();
230  mLastMousePos = mMousePos;
231 
232  double scaling = ( mCtrlAction->isActive() ? 0.1 : 1.0 );
233  mCameraData.dist -= scaling * mCameraData.dist * mWheelAxis->value() * 10 * dt;
234 
235  if ( mRightMouseButtonAction->isActive() )
236  {
237  mCameraData.dist -= mCameraData.dist * dy * 0.01;
238  }
239 
240  float tx = mTxAxis->value() * dt * mCameraData.dist * 1.5;
241  float ty = -mTyAxis->value() * dt * mCameraData.dist * 1.5;
242 
243  if ( !mShiftAction->isActive() && ( tx || ty ) )
244  {
245  // moving with keyboard - take into account yaw of camera
246  float t = sqrt( tx * tx + ty * ty );
247  float a = atan2( ty, tx ) - mCameraData.yaw * M_PI / 180;
248  float dx = cos( a ) * t;
249  float dy = sin( a ) * t;
250  mCameraData.x += dx;
251  mCameraData.y += dy;
252  }
253 
254  if ( ( mLeftMouseButtonAction->isActive() && mShiftAction->isActive() ) || mMiddleMouseButtonAction->isActive() )
255  {
256  // rotate/tilt using mouse
257  mCameraData.pitch += dy;
258  mCameraData.yaw -= dx / 2;
259  }
260  else if ( mShiftAction->isActive() && ( mTxAxis->value() || mTyAxis->value() ) )
261  {
262  // rotate/tilt using keyboard
263  mCameraData.pitch -= mTyAxis->value(); // down key = moving camera toward terrain
264  mCameraData.yaw -= mTxAxis->value(); // right key = moving camera clockwise
265  }
266  else if ( mLeftMouseButtonAction->isActive() && !mShiftAction->isActive() )
267  {
268  // translation works as if one grabbed a point on the plane and dragged it
269  // i.e. find out x,z of the previous mouse point, find out x,z of the current mouse point
270  // and use the difference
271 
272  float z = mLastPressedHeight;
273  QPointF p1 = screen_point_to_point_on_plane( QPointF( mMousePos - QPoint( dx, dy ) ), mViewport, mCamera, z );
274  QPointF p2 = screen_point_to_point_on_plane( QPointF( mMousePos ), mViewport, mCamera, z );
275 
276  mCameraData.x -= p2.x() - p1.x();
277  mCameraData.y -= p2.y() - p1.y();
278  }
279 
280  if ( std::isnan( mCameraData.x ) || std::isnan( mCameraData.y ) )
281  {
282  // something went horribly wrong but we need to at least try to fix it somehow
283  qDebug() << "camera position got NaN!";
284  mCameraData.x = mCameraData.y = 0;
285  }
286 
287  if ( mCameraData.pitch > 80 )
288  mCameraData.pitch = 80; // prevent going under the plane
289  if ( mCameraData.pitch < 0 )
290  mCameraData.pitch = 0; // prevent going over the head
291  if ( mCameraData.dist < 10 )
292  mCameraData.dist = 10;
293 
294  if ( mCameraData != oldCamData )
295  {
296  mCameraData.setCamera( mCamera );
297  emit cameraChanged();
298  }
299 }
300 
301 void QgsCameraController::resetView( float distance )
302 {
303  setViewFromTop( 0, 0, distance );
304 }
305 
306 void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
307 {
308  setCameraData( worldX, worldY, distance, 0, yaw );
309  // a basic setup to make frustum depth range long enough that it does not cull everything
310  mCamera->setNearPlane( distance / 2 );
311  mCamera->setFarPlane( distance * 2 );
312 
313  emit cameraChanged();
314 }
315 
317 {
318  return QgsVector3D( mCameraData.x, 0, mCameraData.y );
319 }
320 
322 {
323  setCameraData( point.x(), point.z(), mCameraData.dist, mCameraData.pitch, mCameraData.yaw );
324  emit cameraChanged();
325 }
326 
327 QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
328 {
329  QDomElement elemCamera = doc.createElement( "camera" );
330  elemCamera.setAttribute( QStringLiteral( "x" ), mCameraData.x );
331  elemCamera.setAttribute( QStringLiteral( "y" ), mCameraData.y );
332  elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraData.dist );
333  elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraData.pitch );
334  elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraData.yaw );
335  return elemCamera;
336 }
337 
338 void QgsCameraController::readXml( const QDomElement &elem )
339 {
340  float x = elem.attribute( QStringLiteral( "x" ) ).toFloat();
341  float y = elem.attribute( QStringLiteral( "y" ) ).toFloat();
342  float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
343  float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
344  float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
345  setCameraData( x, y, dist, pitch, yaw );
346 }
347 
348 void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
349 {
350  mMousePos = QPoint( mouse->x(), mouse->y() );
351 }
352 
353 void QgsCameraController::onPickerMousePressed( Qt3DRender::QPickEvent *pick )
354 {
355  mLastPressedHeight = pick->worldIntersection().y();
356 }
void addTerrainPicker(Qt3DRender::QObjectPicker *picker)
Connects to object picker attached to terrain entity.
3 Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double preci...
Definition: qgsvector3d.h:29
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...
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:251
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 z() const
Returns Z coordinate.
Definition: qgsvector3d.h:47
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 setLookingAtPoint(const QgsVector3D &point)
Sets the point toward which the camera is looking - this is used when world origin changes (e...
void readXml(const QDomElement &elem)
Reads camera configuration from 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...
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 setViewport(const QRect &viewport)
Sets viewport rectangle. Called internally from 3D canvas. Allows conversion of mouse coordinates...
QgsVector3D lookingAtPoint() const
Returns the point in the world coordinates towards which the camera is looking.
QPointF screen_point_to_point_on_plane(const QPointF &pt, const QRect &viewport, Qt3DRender::QCamera *camera, float y)
double x() const
Returns X coordinate.
Definition: qgsvector3d.h:43