QGIS/src/3d/qgscameracontroller.cpp
2025-06-30 10:53:53 +12:00

1463 lines
48 KiB
C++

/***************************************************************************
qgscameracontroller.cpp
--------------------------------------
Date : July 2017
Copyright : (C) 2017 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgscameracontroller.h"
#include "moc_qgscameracontroller.cpp"
#include "qgseventtracing.h"
#include "qgsraycastingutils_p.h"
#include "qgsvector3d.h"
#include "qgswindow3dengine.h"
#include "qgs3dmapscene.h"
#include "qgsterrainentity.h"
#include "qgis.h"
#include "qgs3dutils.h"
#include <QDomDocument>
#include <Qt3DRender/QCamera>
#include <Qt3DInput>
#include <QStringLiteral>
#include <QQuaternion>
#include <cmath>
#include "qgslogger.h"
QgsCameraController::QgsCameraController( Qgs3DMapScene *scene )
: Qt3DCore::QEntity( scene )
, mScene( scene )
, mCamera( scene->engine()->camera() )
, mCameraBefore( new Qt3DRender::QCamera )
, mMouseHandler( new Qt3DInput::QMouseHandler )
, mKeyboardHandler( new Qt3DInput::QKeyboardHandler )
, mOrigin( scene->mapSettings()->origin() )
{
mMouseHandler->setSourceDevice( new Qt3DInput::QMouseDevice() );
connect( mMouseHandler, &Qt3DInput::QMouseHandler::positionChanged, this, &QgsCameraController::onPositionChanged );
connect( mMouseHandler, &Qt3DInput::QMouseHandler::wheel, this, &QgsCameraController::onWheel );
connect( mMouseHandler, &Qt3DInput::QMouseHandler::pressed, this, &QgsCameraController::onMousePressed );
connect( mMouseHandler, &Qt3DInput::QMouseHandler::released, this, &QgsCameraController::onMouseReleased );
addComponent( mMouseHandler );
// Disable the handlers when the entity is disabled
connect( this, &Qt3DCore::QEntity::enabledChanged, mMouseHandler, &Qt3DInput::QMouseHandler::setEnabled );
connect( this, &Qt3DCore::QEntity::enabledChanged, mKeyboardHandler, &Qt3DInput::QKeyboardHandler::setEnabled );
mFpsNavTimer = new QTimer( this );
mFpsNavTimer->setInterval( 10 );
connect( mFpsNavTimer, &QTimer::timeout, this, &QgsCameraController::applyFlyModeKeyMovements );
mFpsNavTimer->start();
if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
{
setCameraNavigationMode( mScene->mapSettings()->cameraNavigationMode() );
mGlobeCrsToLatLon = QgsCoordinateTransform( mScene->mapSettings()->crs(), mScene->mapSettings()->crs().toGeographicCrs(), mScene->mapSettings()->transformContext() );
}
}
QgsCameraController::~QgsCameraController() = default;
QWindow *QgsCameraController::window() const
{
QgsWindow3DEngine *windowEngine = qobject_cast<QgsWindow3DEngine *>( mScene->engine() );
return windowEngine ? windowEngine->window() : nullptr;
}
void QgsCameraController::setCameraNavigationMode( Qgis::NavigationMode navigationMode )
{
if ( navigationMode == mCameraNavigationMode )
return;
mCameraNavigationMode = navigationMode;
mIgnoreNextMouseMove = true;
emit navigationModeChanged( mCameraNavigationMode );
}
void QgsCameraController::setCameraMovementSpeed( double movementSpeed )
{
if ( movementSpeed == mCameraMovementSpeed )
return;
// If the speed becomes 0, navigation does not work anymore
// If the speed becomes too important, only one walk can move the view far from the scene.
mCameraMovementSpeed = std::clamp( movementSpeed, 0.05, 150.0 );
emit cameraMovementSpeedChanged( mCameraMovementSpeed );
}
void QgsCameraController::setVerticalAxisInversion( Qgis::VerticalAxisInversion inversion )
{
mVerticalAxisInversion = inversion;
}
void QgsCameraController::rotateCamera( float diffPitch, float diffHeading )
{
float newPitch = mCameraPose.pitchAngle() + diffPitch;
float newHeading = mCameraPose.headingAngle() + diffHeading;
newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
switch ( mScene->mapSettings()->sceneMode() )
{
case Qgis::SceneMode::Globe:
{
// When on a globe, we need to calculate "where is up" (the normal of a tangent plane).
// Also it uses different axes than the standard plane-based view.
// See QgsCameraPose::updateCameraGlobe(), we basically want to make sure
// that after an update, the camera stays in the same spot.
QgsVector3D viewCenterLatLon;
try
{
viewCenterLatLon = mGlobeCrsToLatLon.transform( mCameraPose.centerPoint() + mOrigin );
}
catch ( const QgsCsException & )
{
QgsDebugError( QStringLiteral( "rotateCamera: ECEF -> lat,lon transform failed!" ) );
return;
}
QQuaternion qLatLon = QQuaternion::fromAxisAndAngle( QVector3D( 0, 0, 1 ), static_cast<float>( viewCenterLatLon.x() ) )
* QQuaternion::fromAxisAndAngle( QVector3D( 0, -1, 0 ), static_cast<float>( viewCenterLatLon.y() ) );
QQuaternion qPitchHeading = QQuaternion::fromAxisAndAngle( QVector3D( 1, 0, 0 ), newHeading )
* QQuaternion::fromAxisAndAngle( QVector3D( 0, 1, 0 ), newPitch );
QVector3D newCameraToCenter = ( qLatLon * qPitchHeading * QVector3D( -1, 0, 0 ) ) * mCameraPose.distanceFromCenterPoint();
mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
mCameraPose.setPitchAngle( newPitch );
mCameraPose.setHeadingAngle( newHeading );
updateCameraFromPose();
return;
}
case Qgis::SceneMode::Local:
{
QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
QVector3D newCameraToCenter = q * QVector3D( 0, 0, -mCameraPose.distanceFromCenterPoint() );
mCameraPose.setCenterPoint( mCamera->position() + newCameraToCenter );
mCameraPose.setPitchAngle( newPitch );
mCameraPose.setHeadingAngle( newHeading );
updateCameraFromPose();
return;
}
}
}
void QgsCameraController::rotateCameraAroundPivot( float newPitch, float newHeading, const QVector3D &pivotPoint )
{
const float oldPitch = mCameraPose.pitchAngle();
const float oldHeading = mCameraPose.headingAngle();
newPitch = std::clamp( newPitch, 0.f, 180.f ); // prevent going over the head
// First undo the previously applied rotation, then apply the new rotation
// (We can't just apply our euler angles difference because the camera may be already rotated)
const QQuaternion qNew = Qgs3DUtils::rotationFromPitchHeadingAngles( newPitch, newHeading );
const QQuaternion qOld = Qgs3DUtils::rotationFromPitchHeadingAngles( oldPitch, oldHeading );
const QQuaternion q = qNew * qOld.conjugated();
const QVector3D newViewCenter = q * ( mCamera->viewCenter() - pivotPoint ) + pivotPoint;
mCameraPose.setCenterPoint( newViewCenter );
mCameraPose.setPitchAngle( newPitch );
mCameraPose.setHeadingAngle( newHeading );
updateCameraFromPose();
}
void QgsCameraController::zoomCameraAroundPivot( const QVector3D &oldCameraPosition, double zoomFactor, const QVector3D &pivotPoint )
{
// step 1: move camera along the line connecting reference camera position and our pivot point
QVector3D newCamPosition = pivotPoint + ( oldCameraPosition - pivotPoint ) * zoomFactor;
double newDistance = mCameraPose.distanceFromCenterPoint() * zoomFactor;
// step 2: using the new camera position and distance from center, calculate new view center
QQuaternion q = Qgs3DUtils::rotationFromPitchHeadingAngles( mCameraPose.pitchAngle(), mCameraPose.headingAngle() );
QVector3D cameraToCenter = q * QVector3D( 0, 0, -newDistance );
QVector3D newViewCenter = newCamPosition + cameraToCenter;
mCameraPose.setDistanceFromCenterPoint( newDistance );
mCameraPose.setCenterPoint( newViewCenter );
updateCameraFromPose();
}
void QgsCameraController::frameTriggered( float dt )
{
Q_UNUSED( dt )
if ( mCameraChanged )
{
emit cameraChanged();
mCameraChanged = false;
}
}
void QgsCameraController::resetView( float distance )
{
QgsPointXY extentCenter = mScene->mapSettings()->extent().center();
QgsVector3D origin = mScene->mapSettings()->origin();
setViewFromTop( extentCenter.x() - origin.x(), extentCenter.y() - origin.y(), distance );
}
void QgsCameraController::setViewFromTop( float worldX, float worldY, float distance, float yaw )
{
if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
{
QgsDebugError( QStringLiteral( "setViewFromTop() should not be used with globe!" ) );
return;
}
QgsCameraPose camPose;
QgsTerrainEntity *terrain = mScene->terrainEntity();
const float terrainElevationOffset = terrain ? terrain->terrainElevationOffset() : 0.0f;
camPose.setCenterPoint( QgsVector3D( worldX, worldY, terrainElevationOffset - mScene->mapSettings()->origin().z() ) );
camPose.setDistanceFromCenterPoint( distance );
camPose.setHeadingAngle( yaw );
// a basic setup to make frustum depth range long enough that it does not cull everything
mCamera->setNearPlane( distance / 2 );
mCamera->setFarPlane( distance * 2 );
// we force the updateCameraNearFarPlanes() in Qgs3DMapScene to properly set the planes
setCameraPose( camPose, true );
}
QgsVector3D QgsCameraController::lookingAtPoint() const
{
return mCameraPose.centerPoint();
}
void QgsCameraController::setLookingAtPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
{
QgsCameraPose camPose;
camPose.setCenterPoint( point );
camPose.setDistanceFromCenterPoint( distance );
camPose.setPitchAngle( pitch );
camPose.setHeadingAngle( yaw );
setCameraPose( camPose );
}
QgsVector3D QgsCameraController::lookingAtMapPoint() const
{
return lookingAtPoint() + mOrigin;
}
void QgsCameraController::setLookingAtMapPoint( const QgsVector3D &point, float distance, float pitch, float yaw )
{
setLookingAtPoint( point - mOrigin, distance, pitch, yaw );
}
void QgsCameraController::setCameraPose( const QgsCameraPose &camPose, bool force )
{
if ( camPose == mCameraPose && !force )
return;
mCameraPose = camPose;
updateCameraFromPose();
}
QDomElement QgsCameraController::writeXml( QDomDocument &doc ) const
{
QDomElement elemCamera = doc.createElement( QStringLiteral( "camera" ) );
// Save center point in map coordinates, since our world origin won't be
// the same on loading
QgsVector3D centerPoint = mCameraPose.centerPoint() + mOrigin;
elemCamera.setAttribute( QStringLiteral( "xMap" ), centerPoint.x() );
elemCamera.setAttribute( QStringLiteral( "yMap" ), centerPoint.y() );
elemCamera.setAttribute( QStringLiteral( "zMap" ), centerPoint.z() );
elemCamera.setAttribute( QStringLiteral( "dist" ), mCameraPose.distanceFromCenterPoint() );
elemCamera.setAttribute( QStringLiteral( "pitch" ), mCameraPose.pitchAngle() );
elemCamera.setAttribute( QStringLiteral( "yaw" ), mCameraPose.headingAngle() );
return elemCamera;
}
void QgsCameraController::readXml( const QDomElement &elem, QgsVector3D savedOrigin )
{
const float dist = elem.attribute( QStringLiteral( "dist" ) ).toFloat();
const float pitch = elem.attribute( QStringLiteral( "pitch" ) ).toFloat();
const float yaw = elem.attribute( QStringLiteral( "yaw" ) ).toFloat();
QgsVector3D centerPoint;
if ( elem.hasAttribute( "xMap" ) )
{
// Prefer newer point saved in map coordinates ...
const double x = elem.attribute( QStringLiteral( "xMap" ) ).toDouble();
const double y = elem.attribute( QStringLiteral( "yMap" ) ).toDouble();
const double z = elem.attribute( QStringLiteral( "zMap" ) ).toDouble();
centerPoint = QgsVector3D( x, y, z ) - mOrigin;
}
else
{
// ... but allow use of older origin-relative coordinates.
const double x = elem.attribute( QStringLiteral( "x" ) ).toDouble();
const double y = elem.attribute( QStringLiteral( "y" ) ).toDouble();
const double elev = elem.attribute( QStringLiteral( "elev" ) ).toDouble();
centerPoint = QgsVector3D( x, elev, y ) - savedOrigin + mOrigin;
}
setLookingAtPoint( centerPoint, dist, pitch, yaw );
}
double QgsCameraController::sampleDepthBuffer( int px, int py )
{
if ( !mDepthBufferIsReady )
{
QgsDebugError( QStringLiteral( "Asked to sample depth buffer, but depth buffer not ready!" ) );
}
double depth = 1;
if ( QWindow *win = window() )
{
// on high DPI screens, the mouse position is in device-independent pixels,
// but the depth buffer is in physical pixels...
px = static_cast<int>( px * win->devicePixelRatio() );
py = static_cast<int>( py * win->devicePixelRatio() );
}
// Sample the neighbouring pixels for the closest point to the camera
for ( int x = px - 3; x <= px + 3; ++x )
{
for ( int y = py - 3; y <= py + 3; ++y )
{
if ( mDepthBufferImage.valid( x, y ) )
{
depth = std::min( depth, Qgs3DUtils::decodeDepth( mDepthBufferImage.pixel( x, y ) ) );
}
}
}
return depth;
}
double QgsCameraController::depthBufferNonVoidAverage()
{
// Cache the computed depth, since averaging over all pixels can be expensive
if ( mDepthBufferNonVoidAverage != -1 )
return mDepthBufferNonVoidAverage;
// Returns the average of depth values that are not 1 (void area)
double depth = 0;
int samplesCount = 0;
// Make sure we can do the cast
Q_ASSERT( mDepthBufferImage.format() == QImage::Format_RGB32 );
for ( int y = 0; y < mDepthBufferImage.height(); ++y )
{
const QRgb *line = reinterpret_cast<const QRgb *>( mDepthBufferImage.constScanLine( y ) );
for ( int x = 0; x < mDepthBufferImage.width(); ++x )
{
double d = Qgs3DUtils::decodeDepth( line[x] );
if ( d < 1 )
{
depth += d;
samplesCount += 1;
}
}
}
// if the whole buffer is white, a depth cannot be computed
if ( samplesCount == 0 )
depth = 1.0;
else
depth /= samplesCount;
mDepthBufferNonVoidAverage = depth;
return depth;
}
QgsVector3D QgsCameraController::moveGeocentricPoint( const QgsVector3D &point, double latDiff, double lonDiff )
{
try
{
QgsVector3D pointLatLon = mGlobeCrsToLatLon.transform( point );
pointLatLon.setX( pointLatLon.x() + lonDiff );
pointLatLon.setY( std::clamp( pointLatLon.y() + latDiff, -90., 90. ) );
return mGlobeCrsToLatLon.transform( pointLatLon, Qgis::TransformDirection::Reverse );
}
catch ( const QgsCsException & )
{
QgsDebugError( QStringLiteral( "moveGeocentricPoint: transform failed!" ) );
return point;
}
}
void QgsCameraController::globeMoveCenterPoint( double latDiff, double lonDiff )
{
const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
const QgsVector3D newViewCenter = moveGeocentricPoint( viewCenter, latDiff, lonDiff );
mCameraPose.setCenterPoint( newViewCenter - mOrigin );
updateCameraFromPose();
}
void QgsCameraController::globeZoom( float factor )
{
mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * factor );
updateCameraFromPose();
}
void QgsCameraController::globeUpdatePitchAngle( float angleDiff )
{
mCameraPose.setPitchAngle( std::clamp( mCameraPose.pitchAngle() + angleDiff, 0.f, 90.f ) );
updateCameraFromPose();
}
void QgsCameraController::globeUpdateHeadingAngle( float angleDiff )
{
mCameraPose.setHeadingAngle( mCameraPose.headingAngle() + angleDiff );
updateCameraFromPose();
}
void QgsCameraController::resetGlobe( float distance, double lat, double lon )
{
QgsVector3D mapPoint;
try
{
mapPoint = mGlobeCrsToLatLon.transform( QgsVector3D( lon, lat, 0 ), Qgis::TransformDirection::Reverse );
}
catch ( const QgsCsException & )
{
QgsDebugError( QStringLiteral( "resetGlobe: transform failed!" ) );
return;
}
QgsCameraPose cp;
cp.setCenterPoint( mapPoint - mOrigin );
cp.setDistanceFromCenterPoint( distance );
setCameraPose( cp );
}
void QgsCameraController::updateCameraFromPose()
{
if ( mCamera )
{
if ( mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe )
{
const QgsVector3D viewCenter = mCameraPose.centerPoint() + mOrigin;
QgsVector3D viewCenterLatLon;
try
{
viewCenterLatLon = mGlobeCrsToLatLon.transform( viewCenter );
}
catch ( const QgsCsException & )
{
QgsDebugError( QStringLiteral( "updateCameraFromPose: transform failed!" ) );
return;
}
mCameraPose.updateCameraGlobe( mCamera, viewCenterLatLon.y(), viewCenterLatLon.x() );
}
else
{
mCameraPose.updateCamera( mCamera );
}
mCameraChanged = true;
}
}
void QgsCameraController::moveCameraPositionBy( const QVector3D &posDiff )
{
mCameraPose.setCenterPoint( mCameraPose.centerPoint() + posDiff );
updateCameraFromPose();
}
void QgsCameraController::onPositionChanged( Qt3DInput::QMouseEvent *mouse )
{
if ( !mInputHandlersEnabled )
return;
QgsEventTracing::ScopedEvent traceEvent( QStringLiteral( "3D" ), QStringLiteral( "QgsCameraController::onPositionChanged" ) );
switch ( mCameraNavigationMode )
{
case Qgis::NavigationMode::TerrainBased:
onPositionChangedTerrainNavigation( mouse );
break;
case Qgis::NavigationMode::Walk:
onPositionChangedFlyNavigation( mouse );
break;
case Qgis::NavigationMode::GlobeTerrainBased:
onPositionChangedGlobeTerrainNavigation( mouse );
break;
}
}
bool QgsCameraController::screenPointToWorldPos( QPoint position, double &depth, QVector3D &worldPosition )
{
depth = sampleDepthBuffer( position.x(), position.y() );
// if there's nothing around the given position, try to get just any depth
// from the scene as a coarse approximation...
if ( depth == 1 )
{
depth = depthBufferNonVoidAverage();
}
worldPosition = Qgs3DUtils::screenPointToWorldPos( position, depth, mScene->engine()->size(), mDepthBufferCamera.get() );
if ( !std::isfinite( worldPosition.x() ) || !std::isfinite( worldPosition.y() ) || !std::isfinite( worldPosition.z() ) )
{
QgsDebugMsgLevel( QStringLiteral( "screenPointToWorldPos: position is NaN or Inf. This should not happen." ), 2 );
return false;
}
return true;
}
void QgsCameraController::onPositionChangedTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
{
if ( mIgnoreNextMouseMove )
{
mIgnoreNextMouseMove = false;
mMousePos = QPoint( mouse->x(), mouse->y() );
return;
}
const int dx = mouse->x() - mMousePos.x();
const int dy = mouse->y() - mMousePos.y();
const bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
const bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
{
// rotate/tilt using mouse (camera moves as it rotates around the clicked point)
setMouseParameters( MouseOperation::RotationCenter, mMousePos );
float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
if ( !mDepthBufferIsReady )
return;
if ( !mRotationCenterCalculated )
{
double depth;
QVector3D worldPosition;
if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
{
mRotationCenter = worldPosition;
mRotationDistanceFromCenter = ( mRotationCenter - mCameraBefore->position() ).length();
emit cameraRotationCenterChanged( mRotationCenter );
mRotationCenterCalculated = true;
}
}
rotateCameraAroundPivot( mRotationPitch + pitchDiff, mRotationYaw + yawDiff, mRotationCenter );
}
else if ( hasLeftButton && hasCtrl && !hasShift )
{
setMouseParameters( MouseOperation::RotationCamera );
// rotate/tilt using mouse (camera stays at one position as it rotates)
const float diffPitch = 0.2f * dy;
const float diffYaw = -0.2f * dx;
rotateCamera( diffPitch, diffYaw );
}
else if ( hasLeftButton && !hasShift && !hasCtrl )
{
// translation works as if one grabbed a point on the 3D viewer and dragged it
setMouseParameters( MouseOperation::Translation, mMousePos );
if ( !mDepthBufferIsReady )
return;
if ( !mDragPointCalculated )
{
double depth;
QVector3D worldPosition;
if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
{
mDragDepth = depth;
mDragPoint = worldPosition;
mDragPointCalculated = true;
}
}
QVector3D cameraBeforeDragPos = mCameraBefore->position();
QVector3D moveToPosition = Qgs3DUtils::screenPointToWorldPos( mMousePos, mDragDepth, mScene->engine()->size(), mCameraBefore.get() );
QVector3D cameraBeforeToMoveToPos = ( moveToPosition - mCameraBefore->position() ).normalized();
QVector3D cameraBeforeToDragPointPos = ( mDragPoint - mCameraBefore->position() ).normalized();
// Make sure the rays are not horizontal (add small z shift if it is)
if ( cameraBeforeToMoveToPos.z() == 0 )
{
cameraBeforeToMoveToPos.setZ( 0.01 );
cameraBeforeToMoveToPos = cameraBeforeToMoveToPos.normalized();
}
if ( cameraBeforeToDragPointPos.z() == 0 )
{
cameraBeforeToDragPointPos.setZ( 0.01 );
cameraBeforeToDragPointPos = cameraBeforeToDragPointPos.normalized();
}
double d1 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToMoveToPos.z();
double d2 = ( mDragPoint.z() - cameraBeforeDragPos.z() ) / cameraBeforeToDragPointPos.z();
QVector3D from = cameraBeforeDragPos + d1 * cameraBeforeToMoveToPos;
QVector3D to = cameraBeforeDragPos + d2 * cameraBeforeToDragPointPos;
QVector3D shiftVector = to - from;
mCameraPose.setCenterPoint( mCameraBefore->viewCenter() + shiftVector );
updateCameraFromPose();
}
else if ( hasLeftButton && hasShift && hasCtrl )
{
// change the camera elevation, similar to pageUp/pageDown
QgsVector3D center = mCameraPose.centerPoint();
double tElev = mMousePos.y() - mouse->y();
center.set( center.x(), center.y(), center.z() + tElev * 0.5 );
mCameraPose.setCenterPoint( center );
updateCameraFromPose();
}
else if ( hasRightButton && !hasShift && !hasCtrl )
{
setMouseParameters( MouseOperation::Zoom, mMousePos );
if ( !mDepthBufferIsReady )
return;
if ( !mDragPointCalculated )
{
double depth;
QVector3D worldPosition;
if ( screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
{
mDragPoint = worldPosition;
mDragPointCalculated = true;
}
}
float oldDist = ( mCameraBefore->position() - mDragPoint ).length();
float newDist = oldDist;
int yOffset = 0;
int screenHeight = mScene->engine()->size().height();
QWindow *win = window();
if ( win )
{
yOffset = win->mapToGlobal( QPoint( 0, 0 ) ).y();
screenHeight = win->screen()->size().height();
}
// Applies smoothing
if ( mMousePos.y() > mClickPoint.y() ) // zoom in
{
double f = ( double ) ( mMousePos.y() - mClickPoint.y() ) / ( double ) ( screenHeight - mClickPoint.y() - yOffset );
f = std::max( 0.0, std::min( 1.0, f ) );
f = 1 - ( std::expm1( -2 * f ) ) / ( std::expm1( -2 ) );
newDist = newDist * f;
}
else // zoom out
{
double f = 1 - ( double ) ( mMousePos.y() + yOffset ) / ( double ) ( mClickPoint.y() + yOffset );
f = std::max( 0.0, std::min( 1.0, f ) );
f = ( std::expm1( 2 * f ) ) / ( std::expm1( 2 ) );
newDist = newDist + 2 * newDist * f;
}
double zoomFactor = newDist / oldDist;
zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mDragPoint );
}
mMousePos = QPoint( mouse->x(), mouse->y() );
}
void QgsCameraController::onPositionChangedGlobeTerrainNavigation( Qt3DInput::QMouseEvent *mouse )
{
const bool hasShift = ( mouse->modifiers() & Qt::ShiftModifier );
const bool hasCtrl = ( mouse->modifiers() & Qt::ControlModifier );
const bool hasLeftButton = ( mouse->buttons() & Qt::LeftButton );
const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
if ( ( hasLeftButton && hasShift && !hasCtrl ) || ( hasMiddleButton && !hasShift && !hasCtrl ) )
{
setMouseParameters( MouseOperation::RotationCenter, mMousePos );
const float scale = static_cast<float>( std::max( mScene->engine()->size().width(), mScene->engine()->size().height() ) );
const float pitchDiff = 180.0f * static_cast<float>( mouse->y() - mClickPoint.y() ) / scale;
const float yawDiff = -180.0f * static_cast<float>( mouse->x() - mClickPoint.x() ) / scale;
mCameraPose.setPitchAngle( mRotationPitch + pitchDiff );
mCameraPose.setHeadingAngle( mRotationYaw + yawDiff );
updateCameraFromPose();
return;
}
if ( !( mouse->buttons() & Qt::LeftButton ) )
return;
// translation works as if one grabbed a point on the 3D viewer and dragged it
setMouseParameters( MouseOperation::Translation, mMousePos );
if ( !mDepthBufferIsReady )
return;
if ( !mDragPointCalculated )
{
double depth;
QVector3D worldPosition;
if ( !screenPointToWorldPos( mClickPoint, depth, worldPosition ) )
return;
mDragDepth = depth;
mDragPoint = worldPosition;
mDragPointCalculated = true;
}
// Approximate the globe as a sphere with a center in (0,0,0) map coords and
// of radius the same as at startPosMap.
const QgsVector3D startPosMap = QgsVector3D( mDragPoint ) + mOrigin;
const double sphereRadiusMap = startPosMap.length();
// Find the intersection of this sphere and the ray from the current clicked point.
const QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( QPoint( mouse->x(), mouse->y() ), mScene->engine()->size(), mCameraBefore.get() );
const QgsVector3D rayOriginMap = QgsVector3D( ray.origin() ) + mOrigin;
// From equations of ray and sphere
const double quadA = QVector3D::dotProduct( ray.direction(), ray.direction() );
const double quadB = 2 * QgsVector3D::dotProduct( ray.direction(), rayOriginMap );
const double quadC = QgsVector3D::dotProduct( rayOriginMap, rayOriginMap ) - sphereRadiusMap * sphereRadiusMap;
const double disc = quadB * quadB - 4 * quadA * quadC;
if ( disc < 0 )
// Ray misses sphere
return;
// Distance to intersection along ray (take smaller root, closer to camera)
const double rayDistMap = ( -quadB - sqrt( disc ) ) / ( 2 * quadA );
if ( rayDistMap < 0 )
{
QgsDebugError( QStringLiteral( "Sphere intersection result negative, cancelling move" ) );
return;
}
const QgsVector3D newPosMap = rayOriginMap + QgsVector3D( ray.direction() ) * rayDistMap;
// now that we have old and new mouse position in ECEF coordinates,
// let's figure out the difference in lat/lon angles and update the center point
QgsVector3D oldLatLon, newLatLon;
try
{
oldLatLon = mGlobeCrsToLatLon.transform( startPosMap );
newLatLon = mGlobeCrsToLatLon.transform( newPosMap );
}
catch ( const QgsCsException & )
{
QgsDebugError( QStringLiteral( "onPositionChangedGlobeTerrainNavigation: transform failed!" ) );
return;
}
const double latDiff = oldLatLon.y() - newLatLon.y();
const double lonDiff = oldLatLon.x() - newLatLon.x();
const QgsVector3D newVC = moveGeocentricPoint( mMousePressViewCenter, latDiff, lonDiff );
const QgsVector3D newVCWorld = newVC - mOrigin;
mCameraPose.setCenterPoint( newVCWorld );
updateCameraFromPose();
}
void QgsCameraController::zoom( float factor )
{
// zoom in/out
float dist = mCameraPose.distanceFromCenterPoint();
dist -= dist * factor * 0.01f;
mCameraPose.setDistanceFromCenterPoint( dist );
updateCameraFromPose();
}
void QgsCameraController::handleTerrainNavigationWheelZoom()
{
if ( !mDepthBufferIsReady )
return;
if ( !mZoomPointCalculated )
{
double depth;
QVector3D worldPosition;
if ( screenPointToWorldPos( mMousePos, depth, worldPosition ) )
{
mZoomPoint = worldPosition;
mZoomPointCalculated = true;
}
}
double oldDist = ( mZoomPoint - mCameraBefore->position() ).length();
// Each step of the scroll wheel decreases distance by 20%
double newDist = std::pow( 0.8, mCumulatedWheelY ) * oldDist;
// Make sure we don't clip the thing we're zooming to.
newDist = std::max( newDist, 2.0 );
double zoomFactor = newDist / oldDist;
// Don't change the distance too suddenly to hopefully prevent numerical instability
zoomFactor = std::clamp( zoomFactor, 0.01, 100.0 );
zoomCameraAroundPivot( mCameraBefore->position(), zoomFactor, mZoomPoint );
mCumulatedWheelY = 0;
setMouseParameters( MouseOperation::None );
}
void QgsCameraController::onWheel( Qt3DInput::QWheelEvent *wheel )
{
if ( !mInputHandlersEnabled )
return;
switch ( mCameraNavigationMode )
{
case Qgis::NavigationMode::Walk:
{
const float scaling = ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1f : 1.0f ) / 1000.f;
setCameraMovementSpeed( mCameraMovementSpeed + mCameraMovementSpeed * scaling * wheel->angleDelta().y() );
break;
}
case Qgis::NavigationMode::TerrainBased:
{
// Scale our variable to roughly "number of normal steps", with Ctrl
// increasing granularity 10x
const double scaling = ( 1.0 / 120.0 ) * ( ( wheel->modifiers() & Qt::ControlModifier ) != 0 ? 0.1 : 1.0 );
// Apparently angleDelta needs to be accumulated
// see: https://doc.qt.io/qt-5/qwheelevent.html#angleDelta
mCumulatedWheelY += scaling * wheel->angleDelta().y();
if ( mCurrentOperation != MouseOperation::ZoomWheel )
{
setMouseParameters( MouseOperation::ZoomWheel );
// The actual zooming will happen after we get a new depth buffer
}
else
{
handleTerrainNavigationWheelZoom();
}
break;
}
case Qgis::NavigationMode::GlobeTerrainBased:
{
float wheelAmount = static_cast<float>( wheel->angleDelta().y() );
float factor = abs( wheelAmount ) / 1000.f;
float mulFactor = wheelAmount > 0 ? ( 1 - factor ) : ( 1 + factor );
mCameraPose.setDistanceFromCenterPoint( mCameraPose.distanceFromCenterPoint() * mulFactor );
updateCameraFromPose();
break;
}
}
}
void QgsCameraController::onMousePressed( Qt3DInput::QMouseEvent *mouse )
{
if ( !mInputHandlersEnabled )
return;
mKeyboardHandler->setFocus( true );
if ( mouse->button() == Qt3DInput::QMouseEvent::MiddleButton || ( ( mouse->modifiers() & Qt::ShiftModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) || ( ( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) )
{
mMousePos = QPoint( mouse->x(), mouse->y() );
if ( mCaptureFpsMouseMovements )
mIgnoreNextMouseMove = true;
const MouseOperation operation {
( mouse->modifiers() & Qt::ControlModifier ) != 0 && mouse->button() == Qt3DInput::QMouseEvent::LeftButton ? MouseOperation::RotationCamera : MouseOperation::RotationCenter
};
setMouseParameters( operation, mMousePos );
}
else if ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton || mouse->button() == Qt3DInput::QMouseEvent::RightButton )
{
mMousePos = QPoint( mouse->x(), mouse->y() );
if ( mCaptureFpsMouseMovements )
mIgnoreNextMouseMove = true;
const MouseOperation operation = ( mouse->button() == Qt3DInput::QMouseEvent::LeftButton ) ? MouseOperation::Translation : MouseOperation::Zoom;
setMouseParameters( operation, mMousePos );
}
}
void QgsCameraController::onMouseReleased( Qt3DInput::QMouseEvent *mouse )
{
Q_UNUSED( mouse )
if ( !mInputHandlersEnabled )
return;
setMouseParameters( MouseOperation::None );
}
bool QgsCameraController::onKeyPressedTerrainNavigation( QKeyEvent *event )
{
const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
const bool hasCtrl = ( event->modifiers() & Qt::ControlModifier );
int tx = 0, ty = 0, tElev = 0;
switch ( event->key() )
{
case Qt::Key_Left:
tx -= 1;
break;
case Qt::Key_Right:
tx += 1;
break;
case Qt::Key_Up:
ty += 1;
break;
case Qt::Key_Down:
ty -= 1;
break;
case Qt::Key_PageDown:
tElev -= 1;
break;
case Qt::Key_PageUp:
tElev += 1;
break;
default:
break;
}
if ( tx || ty )
{
if ( !hasShift && !hasCtrl )
{
moveView( tx, ty );
}
else if ( hasShift && !hasCtrl )
{
// rotate/tilt using keyboard (camera moves as it rotates around its view center)
tiltUpAroundViewCenter( ty );
rotateAroundViewCenter( tx );
}
else if ( hasCtrl && !hasShift )
{
// rotate/tilt using keyboard (camera stays at one position as it rotates)
const float diffPitch = ty; // down key = rotating camera down
const float diffYaw = -tx; // right key = rotating camera to the right
rotateCamera( diffPitch, diffYaw );
}
return true;
}
if ( tElev )
{
QgsVector3D center = mCameraPose.centerPoint();
center.set( center.x(), center.y(), center.z() + tElev * 10 );
mCameraPose.setCenterPoint( center );
return true;
updateCameraFromPose();
}
return false;
}
bool QgsCameraController::onKeyPressedGlobeTerrainNavigation( QKeyEvent *event )
{
// both move factor and zoom factor are just empirically picked numbers
// that seem to work well (providing steps that are not too big / not too small)
constexpr float MOVE_FACTOR = 0.000001f; // multiplied by distance to get angle
constexpr float ZOOM_FACTOR = 0.9f;
const bool hasShift = ( event->modifiers() & Qt::ShiftModifier );
switch ( event->key() )
{
case Qt::Key_Left:
if ( hasShift )
globeUpdateHeadingAngle( -5 );
else
globeMoveCenterPoint( 0, -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
return true;
case Qt::Key_Right:
if ( hasShift )
globeUpdateHeadingAngle( 5 );
else
globeMoveCenterPoint( 0, MOVE_FACTOR * mCameraPose.distanceFromCenterPoint() );
return true;
case Qt::Key_Up:
if ( hasShift )
globeUpdatePitchAngle( -5 );
else
globeMoveCenterPoint( MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
return true;
case Qt::Key_Down:
if ( hasShift )
globeUpdatePitchAngle( 5 );
else
globeMoveCenterPoint( -MOVE_FACTOR * mCameraPose.distanceFromCenterPoint(), 0 );
return true;
case Qt::Key_PageDown:
globeZoom( ZOOM_FACTOR );
return true;
case Qt::Key_PageUp:
globeZoom( 1 / ZOOM_FACTOR );
return true;
default:
break;
}
return false;
}
static const QSet<int> walkNavigationSavedKeys = {
Qt::Key_Left,
Qt::Key_A,
Qt::Key_Right,
Qt::Key_D,
Qt::Key_Up,
Qt::Key_W,
Qt::Key_Down,
Qt::Key_S,
Qt::Key_PageUp,
Qt::Key_E,
Qt::Key_PageDown,
Qt::Key_Q,
};
bool QgsCameraController::onKeyPressedFlyNavigation( QKeyEvent *event )
{
switch ( event->key() )
{
case Qt::Key_QuoteLeft:
{
// toggle mouse lock mode
mCaptureFpsMouseMovements = !mCaptureFpsMouseMovements;
mIgnoreNextMouseMove = true;
if ( mCaptureFpsMouseMovements )
{
qApp->setOverrideCursor( QCursor( Qt::BlankCursor ) );
}
else
{
qApp->restoreOverrideCursor();
}
event->accept();
return true;
}
case Qt::Key_Escape:
{
// always exit mouse lock mode
if ( mCaptureFpsMouseMovements )
{
mCaptureFpsMouseMovements = false;
mIgnoreNextMouseMove = true;
qApp->restoreOverrideCursor();
event->accept();
return true;
}
break;
}
default:
break;
}
if ( walkNavigationSavedKeys.contains( event->key() ) )
{
if ( !event->isAutoRepeat() )
{
mDepressedKeys.insert( event->key() );
}
event->accept();
return true;
}
return false;
}
void QgsCameraController::walkView( double tx, double ty, double tz )
{
const QVector3D cameraUp = mCamera->upVector().normalized();
const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
QVector3D cameraPosDiff( 0.0f, 0.0f, 0.0f );
if ( tx != 0.0 )
{
cameraPosDiff += static_cast<float>( tx ) * cameraFront;
}
if ( ty != 0.0 )
{
cameraPosDiff += static_cast<float>( ty ) * cameraLeft;
}
if ( tz != 0.0 )
{
cameraPosDiff += static_cast<float>( tz ) * QVector3D( 0.0f, 0.0f, 1.0f );
}
moveCameraPositionBy( cameraPosDiff );
}
void QgsCameraController::applyFlyModeKeyMovements()
{
if ( mCameraNavigationMode != Qgis::NavigationMode::Walk )
return;
// shift = "run", ctrl = "slow walk"
const bool shiftPressed = mDepressedKeys.contains( Qt::Key_Shift );
const bool ctrlPressed = mDepressedKeys.contains( Qt::Key_Control );
const double movementSpeed = mCameraMovementSpeed * ( shiftPressed ? 2 : 1 ) * ( ctrlPressed ? 0.1 : 1 );
bool changed = false;
double x = 0.0;
double y = 0.0;
double z = 0.0;
if ( mDepressedKeys.contains( Qt::Key_Left ) || mDepressedKeys.contains( Qt::Key_A ) )
{
changed = true;
y += movementSpeed;
}
if ( mDepressedKeys.contains( Qt::Key_Right ) || mDepressedKeys.contains( Qt::Key_D ) )
{
changed = true;
y -= movementSpeed;
}
if ( mDepressedKeys.contains( Qt::Key_Up ) || mDepressedKeys.contains( Qt::Key_W ) )
{
changed = true;
x += movementSpeed;
}
if ( mDepressedKeys.contains( Qt::Key_Down ) || mDepressedKeys.contains( Qt::Key_S ) )
{
changed = true;
x -= movementSpeed;
}
// note -- vertical axis movements are slower by default then horizontal ones, as GIS projects
// tend to have much more limited elevation range vs ground range
static constexpr double ELEVATION_MOVEMENT_SCALE = 0.5;
if ( mDepressedKeys.contains( Qt::Key_PageUp ) || mDepressedKeys.contains( Qt::Key_E ) )
{
changed = true;
z += ELEVATION_MOVEMENT_SCALE * movementSpeed;
}
if ( mDepressedKeys.contains( Qt::Key_PageDown ) || mDepressedKeys.contains( Qt::Key_Q ) )
{
changed = true;
z -= ELEVATION_MOVEMENT_SCALE * movementSpeed;
}
if ( changed )
walkView( x, y, z );
}
void QgsCameraController::onPositionChangedFlyNavigation( Qt3DInput::QMouseEvent *mouse )
{
const bool hasMiddleButton = ( mouse->buttons() & Qt::MiddleButton );
const bool hasRightButton = ( mouse->buttons() & Qt::RightButton );
const double dx = mCaptureFpsMouseMovements ? QCursor::pos().x() - mMousePos.x() : mouse->x() - mMousePos.x();
const double dy = mCaptureFpsMouseMovements ? QCursor::pos().y() - mMousePos.y() : mouse->y() - mMousePos.y();
mMousePos = mCaptureFpsMouseMovements ? QCursor::pos() : QPoint( mouse->x(), mouse->y() );
if ( mIgnoreNextMouseMove )
{
mIgnoreNextMouseMove = false;
return;
}
if ( hasMiddleButton )
{
// middle button drag = pan camera in place (strafe)
const QVector3D cameraUp = mCamera->upVector().normalized();
const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
const QVector3D cameraLeft = QVector3D::crossProduct( cameraUp, cameraFront );
const QVector3D cameraPosDiff = -dx * cameraLeft - dy * cameraUp;
moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 10.0 );
}
else if ( hasRightButton )
{
// right button drag = camera dolly
const QVector3D cameraFront = ( QVector3D( mCameraPose.centerPoint().x(), mCameraPose.centerPoint().y(), mCameraPose.centerPoint().z() ) - mCamera->position() ).normalized();
const QVector3D cameraPosDiff = dy * cameraFront;
moveCameraPositionBy( mCameraMovementSpeed * cameraPosDiff / 5.0 );
}
else
{
if ( mCaptureFpsMouseMovements )
{
float diffPitch = -0.2f * dy;
switch ( mVerticalAxisInversion )
{
case Qgis::VerticalAxisInversion::Always:
diffPitch *= -1;
break;
case Qgis::VerticalAxisInversion::WhenDragging:
case Qgis::VerticalAxisInversion::Never:
break;
}
const float diffYaw = -0.2f * dx;
rotateCamera( diffPitch, diffYaw );
}
else if ( mouse->buttons() & Qt::LeftButton )
{
float diffPitch = -0.2f * dy;
switch ( mVerticalAxisInversion )
{
case Qgis::VerticalAxisInversion::Always:
case Qgis::VerticalAxisInversion::WhenDragging:
diffPitch *= -1;
break;
case Qgis::VerticalAxisInversion::Never:
break;
}
const float diffYaw = -0.2f * dx;
rotateCamera( diffPitch, diffYaw );
}
}
if ( mCaptureFpsMouseMovements )
{
mIgnoreNextMouseMove = true;
// reset cursor back to center of map widget
emit setCursorPosition( QPoint( mScene->engine()->size().width() / 2, mScene->engine()->size().height() / 2 ) );
}
}
void QgsCameraController::tiltUpAroundViewCenter( float deltaPitch )
{
// Tilt up the view by deltaPitch around the view center (camera moves)
float pitch = mCameraPose.pitchAngle();
pitch -= deltaPitch; // down key = moving camera toward terrain
mCameraPose.setPitchAngle( pitch );
updateCameraFromPose();
}
void QgsCameraController::rotateAroundViewCenter( float deltaYaw )
{
// Rotate clockwise the view by deltaYaw around the view center (camera moves)
float yaw = mCameraPose.headingAngle();
yaw -= deltaYaw; // right key = moving camera clockwise
mCameraPose.setHeadingAngle( yaw );
updateCameraFromPose();
}
void QgsCameraController::setCameraHeadingAngle( float angle )
{
mCameraPose.setHeadingAngle( angle );
updateCameraFromPose();
}
void QgsCameraController::moveView( float tx, float ty )
{
const float yaw = mCameraPose.headingAngle();
const float dist = mCameraPose.distanceFromCenterPoint();
const float x = tx * dist * 0.02f;
const float y = -ty * dist * 0.02f;
// moving with keyboard - take into account yaw of camera
const float t = sqrt( x * x + y * y );
const float a = atan2( y, x ) - yaw * M_PI / 180;
const float dx = cos( a ) * t;
const float dy = sin( a ) * t;
QgsVector3D center = mCameraPose.centerPoint();
center.set( center.x() + dx, center.y() - dy, center.z() );
mCameraPose.setCenterPoint( center );
updateCameraFromPose();
}
bool QgsCameraController::keyboardEventFilter( QKeyEvent *event )
{
if ( !mInputHandlersEnabled )
return false;
if ( event->type() == QKeyEvent::Type::KeyRelease )
{
if ( !event->isAutoRepeat() && mDepressedKeys.contains( event->key() ) )
{
mDepressedKeys.remove( event->key() );
return true;
}
}
else if ( event->type() == QEvent::ShortcutOverride )
{
if ( event->modifiers() & Qt::ControlModifier )
{
switch ( event->key() )
{
case Qt::Key_QuoteLeft:
{
// switch navigation mode
switch ( mCameraNavigationMode )
{
case Qgis::NavigationMode::Walk:
setCameraNavigationMode(
mScene->mapSettings()->sceneMode() == Qgis::SceneMode::Globe
? Qgis::NavigationMode::GlobeTerrainBased
: Qgis::NavigationMode::TerrainBased
);
break;
case Qgis::NavigationMode::TerrainBased:
case Qgis::NavigationMode::GlobeTerrainBased:
setCameraNavigationMode( Qgis::NavigationMode::Walk );
break;
}
event->accept();
return true;
}
// Make sure to sync the key combinations with strings in Qgs3DAxis::createMenu()!
case Qt::Key_8:
rotateCameraToNorth();
return true;
case Qt::Key_6:
rotateCameraToEast();
return true;
case Qt::Key_2:
rotateCameraToSouth();
return true;
case Qt::Key_4:
rotateCameraToWest();
return true;
case Qt::Key_9:
rotateCameraToTop();
return true;
case Qt::Key_3:
rotateCameraToBottom();
return true;
case Qt::Key_5:
rotateCameraToHome();
return true;
default:
break;
}
}
switch ( mCameraNavigationMode )
{
case Qgis::NavigationMode::Walk:
return onKeyPressedFlyNavigation( event );
case Qgis::NavigationMode::TerrainBased:
return onKeyPressedTerrainNavigation( event );
case Qgis::NavigationMode::GlobeTerrainBased:
return onKeyPressedGlobeTerrainNavigation( event );
}
}
return false;
}
void QgsCameraController::depthBufferCaptured( const QImage &depthImage )
{
mDepthBufferImage = depthImage;
mDepthBufferIsReady = true;
mDepthBufferNonVoidAverage = -1;
// To read distances from the captured depth buffer, we need to know the
// camera parameters it was rendered with. This seems like the closest
// place to save them, though I have no idea if they can't be changed
// between the rendering and now anyway...
mDepthBufferCamera = Qgs3DUtils::copyCamera( mCamera );
if ( mCurrentOperation == MouseOperation::ZoomWheel )
{
handleTerrainNavigationWheelZoom();
}
}
bool QgsCameraController::isATranslationRotationSequence( MouseOperation newOperation ) const
{
return std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), newOperation ) != std::end( mTranslateOrRotate ) && std::find( mTranslateOrRotate.begin(), mTranslateOrRotate.end(), mCurrentOperation ) != std::end( mTranslateOrRotate );
}
void QgsCameraController::setMouseParameters( const MouseOperation &newOperation, const QPoint &clickPoint )
{
if ( newOperation == mCurrentOperation )
{
return;
}
if ( newOperation == MouseOperation::None )
{
mClickPoint = QPoint();
}
// click point and rotation angles are updated if:
// - it has never been computed
// - the current and new operations are both rotation and translation
// Indeed, if the sequence such as rotation -> zoom -> rotation updating mClickPoint on
// the click point does not need to be updated because the relative mouse position is kept
// during a zoom operation
else if ( mClickPoint.isNull() || isATranslationRotationSequence( newOperation ) )
{
mClickPoint = clickPoint;
mRotationPitch = mCameraPose.pitchAngle();
mRotationYaw = mCameraPose.headingAngle();
}
mCurrentOperation = newOperation;
mDepthBufferIsReady = false;
mRotationCenterCalculated = false;
mDragPointCalculated = false;
mZoomPointCalculated = false;
if ( mCurrentOperation != MouseOperation::None && mCurrentOperation != MouseOperation::RotationCamera )
{
mMousePressViewCenter = mCameraPose.centerPoint() + mOrigin;
mCameraBefore = Qgs3DUtils::copyCamera( mCamera );
emit requestDepthBufferCapture();
}
}
void QgsCameraController::setOrigin( const QgsVector3D &origin )
{
QgsVector3D diff = origin - mOrigin;
mCameraPose.setCenterPoint( mCameraPose.centerPoint() - diff );
// update other members that depend on world coordinates
mCameraBefore->setPosition( ( QgsVector3D( mCameraBefore->position() ) - diff ).toVector3D() );
mCameraBefore->setViewCenter( ( QgsVector3D( mCameraBefore->viewCenter() ) - diff ).toVector3D() );
mDragPoint = ( QgsVector3D( mDragPoint ) - diff ).toVector3D();
mRotationCenter = ( QgsVector3D( mRotationCenter ) - diff ).toVector3D();
mOrigin = origin;
updateCameraFromPose();
}
void QgsCameraController::rotateToRespectingTerrain( float pitch, float yaw )
{
QgsVector3D pos = lookingAtPoint();
double elevation = 0.0;
if ( mScene->mapSettings()->terrainRenderingEnabled() )
{
QgsDebugMsgLevel( "Checking elevation from terrain...", 2 );
QVector3D camPos = mCamera->position();
QgsRayCastingUtils::Ray3D ray( camPos, pos.toVector3D() - camPos, mCamera->farPlane() );
const QVector<QgsRayCastingUtils::RayHit> hits = mScene->terrainEntity()->rayIntersection( ray, QgsRayCastingUtils::RayCastContext() );
if ( !hits.isEmpty() )
{
elevation = hits.at( 0 ).pos.z();
QgsDebugMsgLevel( QString( "Computed elevation from terrain: %1" ).arg( elevation ), 2 );
}
else
{
QgsDebugMsgLevel( "Unable to obtain elevation from terrain", 2 );
}
}
pos.set( pos.x(), pos.y(), elevation + mScene->terrainEntity()->terrainElevationOffset() );
setLookingAtPoint( pos, ( mCamera->position() - pos.toVector3D() ).length(), pitch, yaw );
}