Skip to content
Permalink
Browse files
Camera navigation improvements (#45979)
Fixes two issues in camera navigation:
- Unintuitive camera rotation
- Wrong center point when zooming in/out and rotating

See qgis/QGIS-Enhancement-Proposals#215

This PR employs the following changes:
- The zoom in functionality will zoom in towards the real 3D position of an object in the scene instead the camera view center point used previously.
- The rotation will use the real clicked 3D position of a pixel as well instead of the camera view center point.
- The press and drag behaviour is improved to shift the map in real 3D coordinates instead of some arbitrary measurement (you can see the clicked pixel following the cursor instead of drifting away).
  • Loading branch information
NEDJIMAbelgacem committed Jan 5, 2022
1 parent df64116 commit b88b0807775e0d9ccd1f5803d15c0a313fe9f485
@@ -348,6 +348,20 @@ Sets whether to show camera's view center as a sphere (for debugging)
Returns whether to show camera's view center as a sphere (for debugging)

.. versionadded:: 3.4
%End

void setShowCameraRotationCenter( bool enabled );
%Docstring
Sets whether to show camera's rotation center as a sphere (for debugging)

.. versionadded:: 3.24
%End

bool showCameraRotationCenter() const;
%Docstring
Returns whether to show camera's rotation center as a sphere (for debugging)

.. versionadded:: 3.24
%End

void setShowLightSourceOrigins( bool enabled );
@@ -671,6 +685,13 @@ Emitted when the flag whether terrain's tile info is shown has changed
Emitted when the flag whether camera's view center is shown has changed

.. versionadded:: 3.4
%End

void showCameraRotationCenterChanged();
%Docstring
Emitted when the flag whether camera's rotation center is shown has changed

.. versionadded:: 3.24
%End

void showLightSourceOriginsChanged();
@@ -103,6 +103,17 @@ Returns the perpendicular point of vector ``vp`` from [``v1`` - ``v2``]
%Docstring
Returns a string representation of the 3D vector.
Members will be truncated to the specified ``precision``.
%End

QVector3D toVector3D() const;
%Docstring
Converts the current object to QVector3D

.. warning::

the conversion may decrease the accuracy (double to float values conversion)

.. versionadded:: 3.24
%End

SIP_PYOBJECT __repr__();
@@ -118,6 +118,7 @@ Qgs3DMapScene::Qgs3DMapScene( const Qgs3DMapSettings &map, QgsAbstract3DEngine *
mCameraController->resetView( 1000 );

addCameraViewCenterEntity( mEngine->camera() );
addCameraRotationCenterEntity( mCameraController );
updateLights();

// create terrain entity
@@ -410,6 +411,7 @@ static void _updateNearFarPlane( const QList<QgsChunkNode *> &activeNodes, const

QVector4D pc = viewMatrix * p;


float dst = -pc.z(); // in camera coordinates, x grows right, y grows down, z grows to the back
fnear = std::min( fnear, dst );
ffar = std::max( ffar, dst );
@@ -1144,3 +1146,31 @@ QgsRectangle Qgs3DMapScene::sceneExtent()

return extent;
}

void Qgs3DMapScene::addCameraRotationCenterEntity( QgsCameraController *controller )
{
mEntityRotationCenter = new Qt3DCore::QEntity;

Qt3DCore::QTransform *trCameraViewCenter = new Qt3DCore::QTransform;
mEntityRotationCenter->addComponent( trCameraViewCenter );
Qt3DExtras::QPhongMaterial *materialCameraViewCenter = new Qt3DExtras::QPhongMaterial;
materialCameraViewCenter->setAmbient( Qt::blue );
mEntityRotationCenter->addComponent( materialCameraViewCenter );
Qt3DExtras::QSphereMesh *rendererCameraViewCenter = new Qt3DExtras::QSphereMesh;
rendererCameraViewCenter->setRadius( 10 );
mEntityRotationCenter->addComponent( rendererCameraViewCenter );
mEntityRotationCenter->setEnabled( true );
mEntityRotationCenter->setParent( this );

connect( controller, &QgsCameraController::cameraRotationCenterChanged, this, [trCameraViewCenter]( QVector3D center )
{
trCameraViewCenter->setTranslation( center );
} );

mEntityRotationCenter->setEnabled( mMap.showCameraRotationCenter() );

connect( &mMap, &Qgs3DMapSettings::showCameraRotationCenterChanged, this, [this]
{
mEntityRotationCenter->setEnabled( mMap.showCameraRotationCenter() );
} );
}
@@ -175,6 +175,7 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
void addLayerEntity( QgsMapLayer *layer );
void removeLayerEntity( QgsMapLayer *layer );
void addCameraViewCenterEntity( Qt3DRender::QCamera *camera );
void addCameraRotationCenterEntity( QgsCameraController *controller );
void setSceneState( SceneState state );
void updateSceneState();
void updateScene();
@@ -204,6 +205,8 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
QList<Qt3DCore::QEntity *> mLightOriginEntities;
QList<QgsMapLayer *> mModelVectorLayers;
QgsSkyboxEntity *mSkybox = nullptr;
//! Entity that shows rotation center = useful for debugging camera issues
Qt3DCore::QEntity *mEntityRotationCenter = nullptr;
};

#endif // QGS3DMAPSCENE_H
@@ -49,6 +49,7 @@ Qgs3DMapSettings::Qgs3DMapSettings( const Qgs3DMapSettings &other )
, mShowTerrainBoundingBoxes( other.mShowTerrainBoundingBoxes )
, mShowTerrainTileInfo( other.mShowTerrainTileInfo )
, mShowCameraViewCenter( other.mShowCameraViewCenter )
, mShowCameraRotationCenter( other.mShowCameraRotationCenter )
, mShowLightSources( other.mShowLightSources )
, mShowLabels( other.mShowLabels )
, mPointLights( other.mPointLights )
@@ -263,6 +264,7 @@ void Qgs3DMapSettings::readXml( const QDomElement &elem, const QgsReadWriteConte
mShowTerrainBoundingBoxes = elemDebug.attribute( QStringLiteral( "bounding-boxes" ), QStringLiteral( "0" ) ).toInt();
mShowTerrainTileInfo = elemDebug.attribute( QStringLiteral( "terrain-tile-info" ), QStringLiteral( "0" ) ).toInt();
mShowCameraViewCenter = elemDebug.attribute( QStringLiteral( "camera-view-center" ), QStringLiteral( "0" ) ).toInt();
mShowCameraRotationCenter = elemDebug.attribute( QStringLiteral( "camera-rotation-center" ), QStringLiteral( "0" ) ).toInt();
mShowLightSources = elemDebug.attribute( QStringLiteral( "show-light-sources" ), QStringLiteral( "0" ) ).toInt();
mIsFpsCounterEnabled = elemDebug.attribute( QStringLiteral( "show-fps-counter" ), QStringLiteral( "0" ) ).toInt();

@@ -375,6 +377,7 @@ QDomElement Qgs3DMapSettings::writeXml( QDomDocument &doc, const QgsReadWriteCon
elemDebug.setAttribute( QStringLiteral( "bounding-boxes" ), mShowTerrainBoundingBoxes ? 1 : 0 );
elemDebug.setAttribute( QStringLiteral( "terrain-tile-info" ), mShowTerrainTileInfo ? 1 : 0 );
elemDebug.setAttribute( QStringLiteral( "camera-view-center" ), mShowCameraViewCenter ? 1 : 0 );
elemDebug.setAttribute( QStringLiteral( "camera-rotation-center" ), mShowCameraRotationCenter ? 1 : 0 );
elemDebug.setAttribute( QStringLiteral( "show-light-sources" ), mShowLightSources ? 1 : 0 );
elemDebug.setAttribute( QStringLiteral( "show-fps-counter" ), mIsFpsCounterEnabled ? 1 : 0 );
elem.appendChild( elemDebug );
@@ -642,6 +645,16 @@ void Qgs3DMapSettings::setShowCameraViewCenter( bool enabled )
emit showCameraViewCenterChanged();
}

void Qgs3DMapSettings::setShowCameraRotationCenter( bool enabled )
{
if ( mShowCameraRotationCenter == enabled )
return;

mShowCameraRotationCenter = enabled;
emit showCameraRotationCenterChanged();
}


void Qgs3DMapSettings::setShowLightSourceOrigins( bool enabled )
{
if ( mShowLightSources == enabled )
@@ -331,6 +331,18 @@ class _3D_EXPORT Qgs3DMapSettings : public QObject, public QgsTemporalRangeObjec
*/
bool showCameraViewCenter() const { return mShowCameraViewCenter; }

/**
* Sets whether to show camera's rotation center as a sphere (for debugging)
* \since QGIS 3.24
*/
void setShowCameraRotationCenter( bool enabled );

/**
* Returns whether to show camera's rotation center as a sphere (for debugging)
* \since QGIS 3.24
*/
bool showCameraRotationCenter() const { return mShowCameraRotationCenter; }

/**
* Sets whether to show light source origins as a sphere (for debugging)
* \since QGIS 3.16
@@ -617,6 +629,12 @@ class _3D_EXPORT Qgs3DMapSettings : public QObject, public QgsTemporalRangeObjec
*/
void showCameraViewCenterChanged();

/**
* Emitted when the flag whether camera's rotation center is shown has changed
* \since QGIS 3.24
*/
void showCameraRotationCenterChanged();

/**
* Emitted when the flag whether light source origins are shown has changed.
* \since QGIS 3.15
@@ -733,6 +751,7 @@ class _3D_EXPORT Qgs3DMapSettings : public QObject, public QgsTemporalRangeObjec
bool mShowTerrainBoundingBoxes = false; //!< Whether to show bounding boxes of entities - useful for debugging
bool mShowTerrainTileInfo = false; //!< Whether to draw extra information about terrain tiles to the textures - useful for debugging
bool mShowCameraViewCenter = false; //!< Whether to show camera view center as a sphere - useful for debugging
bool mShowCameraRotationCenter = false; //!< Whether to show camera rotation center as a sphere - useful for debugging
bool mShowLightSources = false; //!< Whether to show the origin of light sources
bool mShowLabels = false; //!< Whether to display labels on terrain tiles
QList<QgsPointLightSettings> mPointLights; //!< List of point lights defined for the scene
@@ -37,6 +37,7 @@
#include "qgspoint3dsymbol.h"
#include "qgspolygon3dsymbol.h"

#include <QtMath>
#include <Qt3DExtras/QPhongMaterial>
#include <Qt3DRender/QRenderSettings>

@@ -85,6 +86,51 @@ QImage Qgs3DUtils::captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene
return resImage;
}

QImage Qgs3DUtils::captureSceneDepthBuffer( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene )
{
QImage resImage;
QEventLoop evLoop;

// We need to change render policy to RenderPolicy::Always, since otherwise render capture node won't work
engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::Always );

auto requestImageFcn = [&engine, scene]
{
if ( scene->sceneState() == Qgs3DMapScene::Ready )
{
engine.requestCaptureImage();
}
};

auto saveImageFcn = [&evLoop, &resImage]( const QImage & img )
{
resImage = img;
evLoop.quit();
};

QMetaObject::Connection conn1 = QObject::connect( &engine, &QgsAbstract3DEngine::imageCaptured, saveImageFcn );
QMetaObject::Connection conn2;

if ( scene->sceneState() == Qgs3DMapScene::Ready )
{
requestImageFcn();
}
else
{
// first wait until scene is loaded
conn2 = QObject::connect( scene, &Qgs3DMapScene::sceneStateChanged, requestImageFcn );
}

evLoop.exec();

QObject::disconnect( conn1 );
if ( conn2 )
QObject::disconnect( conn2 );

engine.renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::OnDemand );
return resImage;
}

bool Qgs3DUtils::exportAnimation( const Qgs3DAnimationSettings &animationSettings,
const Qgs3DMapSettings &mapSettings,
int framesPerSecond,
@@ -596,3 +642,39 @@ QgsRay3D Qgs3DUtils::rayFromScreenPoint( const QPoint &point, const QSize &windo

return QgsRay3D( QVector3D( rayOriginWorld ), rayDirWorld );
}

QVector3D Qgs3DUtils::screenPointToWorldPos( const QPoint &screenPoint, double depth, const QSize &screenSize, Qt3DRender::QCamera *camera )
{
double near = camera->nearPlane();
double far = camera->farPlane();
double distance = ( 2.0 * near * far ) / ( far + near - ( depth * 2 - 1 ) * ( far - near ) );

QgsRay3D ray = Qgs3DUtils::rayFromScreenPoint( screenPoint, screenSize, camera );
double dot = QVector3D::dotProduct( ray.direction(), camera->viewVector().normalized() );
distance /= dot;

return ray.origin() + distance * ray.direction();
}

void Qgs3DUtils::pitchAndYawFromViewVector( QVector3D vect, double &pitch, double &yaw )
{
vect.normalize();

pitch = qRadiansToDegrees( qAcos( vect.y() ) );
yaw = qRadiansToDegrees( qAtan2( -vect.z(), vect.x() ) ) + 90;
}

QVector2D Qgs3DUtils::screenToTextureCoordinates( QVector2D screenXY, QSize winSize )
{
return QVector2D( screenXY.x() / winSize.width(), 1 - screenXY.y() / winSize.width() );
}

QVector2D Qgs3DUtils::textureToScreenCoordinates( QVector2D textureXY, QSize winSize )
{
return QVector2D( textureXY.x() * winSize.width(), ( 1 - textureXY.y() ) * winSize.height() );
}

double Qgs3DUtils::decodeDepth( const QColor &pixel )
{
return pixel.redF() / 255.0 / 255.0 + pixel.greenF() / 255.0 + pixel.blueF();
}
@@ -60,6 +60,16 @@ class _3D_EXPORT Qgs3DUtils
*/
static QImage captureSceneImage( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene );

/**
* Captures the depth buffer of the current 3D scene of a 3D engine. The function waits
* until the scene is not fully loaded/updated before capturing the image.
*
* \note In order to get more precision, the depth values are encoded into RGB colors,
* use Qgs3DUtils::decodeDepth() to get the correct depth value.
* \since QGIS 3.24
*/
static QImage captureSceneDepthBuffer( QgsAbstract3DEngine &engine, Qgs3DMapScene *scene );

/**
* Captures 3D animation frames to the selected folder
*
@@ -180,6 +190,42 @@ class _3D_EXPORT Qgs3DUtils

//! Convert from clicked point on the screen to a ray in world coordinates
static QgsRay3D rayFromScreenPoint( const QPoint &point, const QSize &windowSize, Qt3DRender::QCamera *camera );

/**
* Converts the clicked mouse position to the corresponding 3D world coordinates
* \since QGIS 3.24
*/
static QVector3D screenPointToWorldPos( const QPoint &screenPoint, double depth, const QSize &screenSize, Qt3DRender::QCamera *camera );

/**
* Function used to extract the pitch and yaw (also known as heading) angles in degrees from the view vector of the camera [cameraViewCenter - cameraPosition]
* \since QGIS 3.24
*/
static void pitchAndYawFromViewVector( QVector3D vect, double &pitch, double &yaw );

/**
* Converts from screen coordinates to texture coordinates
* \note Expected return values are in [0, 1] range
* \see textureToScreenCoordinates()
* \since QGIS 3.24
*/
static QVector2D screenToTextureCoordinates( QVector2D screenXY, QSize winSize );

/**
* Converts from texture coordinates coordinates to screen coordinates
* \note Expected return values are in [0, winSize.width], [0, winSize.height] range
* \see screenToTextureCoordinates()
* \since QGIS 3.24
*/
static QVector2D textureToScreenCoordinates( QVector2D textureXY, QSize winSize );

/**
* Decodes the depth value from the pixel's color value
* The depth value is encoded from OpenGL side (the depth render pass) into the 3 RGB channels to preserve precision.
*
* \since QGIS 3.24
*/
static double decodeDepth( const QColor &pixel );
};

#endif // QGS3DUTILS_H
@@ -40,6 +40,20 @@ void QgsAbstract3DEngine::requestCaptureImage()
} );
}

void QgsAbstract3DEngine::requestDepthBufferCapture()
{
Qt3DRender::QRenderCaptureReply *captureReply;
captureReply = mFrameGraph->depthRenderCapture()->requestCapture();
// We need to change render policy to RenderPolicy::Always, since otherwise render capture node won't work
this->renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::Always );
connect( captureReply, &Qt3DRender::QRenderCaptureReply::completed, this, [ = ]
{
emit depthBufferCaptured( captureReply->image() );
this->renderSettings()->setRenderPolicy( Qt3DRender::QRenderSettings::RenderPolicy::OnDemand );
captureReply->deleteLater();
} );
}

void QgsAbstract3DEngine::setRenderCaptureEnabled( bool enabled )
{
mFrameGraph->setRenderCaptureEnabled( enabled );

0 comments on commit b88b080

Please sign in to comment.