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