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 
QgsCameraController(Qt3DCore::QNode * parent)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 
setTerrainEntity(QgsTerrainEntity * te)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 
setCamera(Qt3DRender::QCamera * camera)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 
setViewport(QRect viewport)84 void QgsCameraController::setViewport( QRect viewport )
85 {
86   if ( mViewport == viewport )
87     return;
88 
89   mViewport = viewport;
90   emit viewportChanged();
91 }
92 
93 
unproject(QVector3D v,const QMatrix4x4 & modelView,const QMatrix4x4 & projection,QRect viewport)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 
find_x_on_line(float x0,float y0,float x1,float y1,float y)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 
screen_point_to_point_on_plane(QPointF pt,QRect viewport,Qt3DRender::QCamera * camera,float y)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 
rotateCamera(float diffPitch,float diffYaw)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 
frameTriggered(float dt)171 void QgsCameraController::frameTriggered( float dt )
172 {
173   Q_UNUSED( dt )
174 }
175 
resetView(float distance)176 void QgsCameraController::resetView( float distance )
177 {
178   setViewFromTop( 0, 0, distance );
179 }
180 
setViewFromTop(float worldX,float worldY,float distance,float yaw)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 
lookingAtPoint() const195 QgsVector3D QgsCameraController::lookingAtPoint() const
196 {
197   return mCameraPose.centerPoint();
198 }
199 
setLookingAtPoint(const QgsVector3D & point,float distance,float pitch,float yaw)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 
setCameraPose(const QgsCameraPose & camPose)210 void QgsCameraController::setCameraPose( const QgsCameraPose &camPose )
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 
writeXml(QDomDocument & doc) const223 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 
readXml(const QDomElement & elem)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 
updateCameraFromPose(bool centerPointChanged)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 
onPositionChanged(Qt3DInput::QMouseEvent * mouse)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 += 0.2f * dy;
300     yaw -= 0.2f * dx;
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;
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 
zoom(float factor)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 
onWheel(Qt3DInput::QWheelEvent * wheel)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 
onMousePressed(Qt3DInput::QMouseEvent * mouse)354 void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
355 {
356   Q_UNUSED( mouse )
357   mKeyboardHandler->setFocus( true );
358 }
359 
onMouseReleased(Qt3DInput::QMouseEvent * mouse)360 void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
361 {
362   Q_UNUSED( mouse )
363 }
364 
onKeyPressed(Qt3DInput::QKeyEvent * event)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)
404       tiltUpAroundViewCenter( ty );
405       rotateAroundViewCenter( tx );
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 
onKeyReleased(Qt3DInput::QKeyEvent * event)426 void QgsCameraController::onKeyReleased( Qt3DInput::QKeyEvent *event )
427 {
428   Q_UNUSED( event )
429 }
430 
onPickerMousePressed(Qt3DRender::QPickEvent * pick)431 void QgsCameraController::onPickerMousePressed( Qt3DRender::QPickEvent *pick )
432 {
433   mLastPressedHeight = pick->worldIntersection().y();
434 }
435 
tiltUpAroundViewCenter(float deltaPitch)436 void QgsCameraController::tiltUpAroundViewCenter( float deltaPitch )
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 
rotateAroundViewCenter(float deltaYaw)445 void QgsCameraController::rotateAroundViewCenter( float deltaYaw )
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 
setCameraHeadingAngle(float angle)456 void QgsCameraController::setCameraHeadingAngle( float angle )
457 {
458   mCameraPose.setHeadingAngle( angle );
459   updateCameraFromPose();
460 }
461 
moveView(float tx,float ty)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 }
480