QGIS API Documentation  3.2.0-Bonn (bc43194)
qgs3dmapscene.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgs3dmapscene.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 "qgs3dmapscene.h"
17 
18 #include <Qt3DRender/QCamera>
19 #include <Qt3DRender/QMesh>
20 #include <Qt3DRender/QPickingSettings>
21 #include <Qt3DRender/QRenderSettings>
22 #include <Qt3DRender/QSceneLoader>
23 #include <Qt3DExtras/QForwardRenderer>
24 #include <Qt3DExtras/QPhongMaterial>
25 #include <Qt3DExtras/QSkyboxEntity>
26 #include <Qt3DLogic/QFrameAction>
27 
28 #include <QTimer>
29 
30 #include "qgsaabb.h"
31 #include "qgs3dmapsettings.h"
32 #include "qgs3dutils.h"
33 #include "qgsabstract3drenderer.h"
34 #include "qgscameracontroller.h"
35 #include "qgschunkedentity_p.h"
36 #include "qgschunknode_p.h"
37 #include "qgsterrainentity_p.h"
38 #include "qgsterraingenerator.h"
39 #include "qgsvectorlayer.h"
41 
42 
43 Qgs3DMapScene::Qgs3DMapScene( const Qgs3DMapSettings &map, Qt3DExtras::QForwardRenderer *defaultFrameGraph, Qt3DRender::QRenderSettings *renderSettings, Qt3DRender::QCamera *camera, const QRect &viewportRect, Qt3DCore::QNode *parent )
44  : Qt3DCore::QEntity( parent )
45  , mMap( map )
46  , mForwardRenderer( defaultFrameGraph )
47 {
48 
49  connect( &map, &Qgs3DMapSettings::backgroundColorChanged, this, &Qgs3DMapScene::onBackgroundColorChanged );
50  onBackgroundColorChanged();
51 
52  // TODO: strange - setting OnDemand render policy still keeps QGIS busy (Qt 5.9.0)
53  // actually it is more busy than with the default "Always" policy although there are no changes in the scene.
54  //renderSettings->setRenderPolicy( Qt3DRender::QRenderSettings::OnDemand );
55 
56 #if QT_VERSION >= 0x050900
57  // we want precise picking of terrain (also bounding volume picking does not seem to work - not sure why)
58  renderSettings->pickingSettings()->setPickMethod( Qt3DRender::QPickingSettings::TrianglePicking );
59 #endif
60 
61  // Camera
62  float aspectRatio = ( float )viewportRect.width() / viewportRect.height();
63  camera->lens()->setPerspectiveProjection( 45.0f, aspectRatio, 10.f, 10000.0f );
64 
65  mFrameAction = new Qt3DLogic::QFrameAction();
66  connect( mFrameAction, &Qt3DLogic::QFrameAction::triggered,
67  this, &Qgs3DMapScene::onFrameTriggered );
68  addComponent( mFrameAction ); // takes ownership
69 
70  // Camera controlling
71  mCameraController = new QgsCameraController( this ); // attaches to the scene
72  mCameraController->setViewport( viewportRect );
73  mCameraController->setCamera( camera );
74  mCameraController->resetView( 1000 );
75 
76  // create terrain entity
77 
78  createTerrain();
79  connect( &map, &Qgs3DMapSettings::terrainGeneratorChanged, this, &Qgs3DMapScene::createTerrain );
80  connect( &map, &Qgs3DMapSettings::terrainVerticalScaleChanged, this, &Qgs3DMapScene::createTerrain );
81  connect( &map, &Qgs3DMapSettings::mapTileResolutionChanged, this, &Qgs3DMapScene::createTerrain );
82  connect( &map, &Qgs3DMapSettings::maxTerrainScreenErrorChanged, this, &Qgs3DMapScene::createTerrain );
83  connect( &map, &Qgs3DMapSettings::maxTerrainGroundErrorChanged, this, &Qgs3DMapScene::createTerrain );
84 
85  // create entities of renderers
86 
87  Q_FOREACH ( const QgsAbstract3DRenderer *renderer, map.renderers() )
88  {
89  Qt3DCore::QEntity *newEntity = renderer->createEntity( map );
90  newEntity->setParent( this );
91  }
92 
93  // listen to changes of layers in order to add/remove 3D renderer entities
94  connect( &map, &Qgs3DMapSettings::layersChanged, this, &Qgs3DMapScene::onLayersChanged );
95 
96  Qt3DCore::QEntity *lightEntity = new Qt3DCore::QEntity;
97  Qt3DCore::QTransform *lightTransform = new Qt3DCore::QTransform;
98  lightTransform->setTranslation( QVector3D( 0, 1000, 0 ) );
99  // defaults: white, intensity 0.5
100  // attenuation: constant 1.0, linear 0.0, quadratic 0.0
101  Qt3DRender::QPointLight *light = new Qt3DRender::QPointLight;
102  light->setConstantAttenuation( 0 );
103  //light->setColor(Qt::white);
104  //light->setIntensity(0.5);
105  lightEntity->addComponent( light );
106  lightEntity->addComponent( lightTransform );
107  lightEntity->setParent( this );
108 
109 
110 #if 0
111  ChunkedEntity *testChunkEntity = new ChunkedEntity( AABB( -500, 0, -500, 500, 100, 500 ), 2.f, 3.f, 7, new TestChunkLoaderFactory );
112  testChunkEntity->setEnabled( false );
113  testChunkEntity->setParent( this );
114  chunkEntities << testChunkEntity;
115 #endif
116 
117  connect( mCameraController, &QgsCameraController::cameraChanged, this, &Qgs3DMapScene::onCameraChanged );
118  connect( mCameraController, &QgsCameraController::viewportChanged, this, &Qgs3DMapScene::onCameraChanged );
119 
120 #if 0
121  // experiments with loading of existing 3D models.
122 
123  // scene loader only gets loaded only when added to a scene...
124  // it loads everything: geometries, materials, transforms, lights, cameras (if any)
125  Qt3DCore::QEntity *loaderEntity = new Qt3DCore::QEntity;
126  Qt3DRender::QSceneLoader *loader = new Qt3DRender::QSceneLoader;
127  loader->setSource( QUrl( "file:///home/martin/Downloads/LowPolyModels/tree.dae" ) );
128  loaderEntity->addComponent( loader );
129  loaderEntity->setParent( this );
130 
131  // mesh loads just geometry as one geometry...
132  // so if there are different materials (e.g. colors) used in the model, this information is lost
133  Qt3DCore::QEntity *meshEntity = new Qt3DCore::QEntity;
134  Qt3DRender::QMesh *mesh = new Qt3DRender::QMesh;
135  mesh->setSource( QUrl( "file:///home/martin/Downloads/LowPolyModels/tree.obj" ) );
136  meshEntity->addComponent( mesh );
137  Qt3DExtras::QPhongMaterial *material = new Qt3DExtras::QPhongMaterial;
138  material->setAmbient( Qt::red );
139  meshEntity->addComponent( material );
140  Qt3DCore::QTransform *meshTransform = new Qt3DCore::QTransform;
141  meshTransform->setScale( 1 );
142  meshEntity->addComponent( meshTransform );
143  meshEntity->setParent( this );
144 #endif
145 
146  if ( map.hasSkyboxEnabled() )
147  {
148  Qt3DExtras::QSkyboxEntity *skybox = new Qt3DExtras::QSkyboxEntity;
149  skybox->setBaseName( map.skyboxFileBase() );
150  skybox->setExtension( map.skyboxFileExtension() );
151  skybox->setParent( this );
152 
153  // docs say frustum culling must be disabled for skybox.
154  // it _somehow_ works even when frustum culling is enabled with some camera positions,
155  // but then when zoomed in more it would disappear - so let's keep frustum culling disabled
156  defaultFrameGraph->setFrustumCullingEnabled( false );
157  }
158 
159  // force initial update of chunked entities
160  onCameraChanged();
161 }
162 
164 {
165  QgsRectangle extent = mMap.terrainGenerator()->extent();
166  float side = std::max( extent.width(), extent.height() );
167  mCameraController->resetView( side ); // assuming FOV being 45 degrees
168 }
169 
171 {
172  return mTerrain ? mTerrain->pendingJobsCount() : 0;
173 }
174 
175 QgsChunkedEntity::SceneState _sceneState( QgsCameraController *cameraController )
176 {
177  Qt3DRender::QCamera *camera = cameraController->camera();
178  QgsChunkedEntity::SceneState state;
179  state.cameraFov = camera->fieldOfView();
180  state.cameraPos = camera->position();
181  QRect rect = cameraController->viewport();
182  state.screenSizePx = std::max( rect.width(), rect.height() ); // TODO: is this correct?
183  state.viewProjectionMatrix = camera->projectionMatrix() * camera->viewMatrix();
184  return state;
185 }
186 
187 void Qgs3DMapScene::onCameraChanged()
188 {
189  Q_FOREACH ( QgsChunkedEntity *entity, mChunkEntities )
190  {
191  if ( entity->isEnabled() )
192  entity->update( _sceneState( mCameraController ) );
193  }
194 
195  // Update near and far plane from the terrain.
196  // this needs to be done with great care as we have kind of circular dependency here:
197  // active nodes are culled based on the current frustum (which involves near + far plane)
198  // and then based on active nodes we set near and far plane.
199  //
200  // All of this is just heuristics assuming that all other stuff is being rendered somewhere
201  // around the area where the terrain is.
202  //
203  // Near/far plane is setup in order to make best use of the depth buffer to avoid:
204  // 1. precision errors - if the range is too great
205  // 2. unwanted clipping of scene - if the range is too small
206 
207  if ( mTerrain )
208  {
209  Qt3DRender::QCamera *camera = cameraController()->camera();
210  QMatrix4x4 viewMatrix = camera->viewMatrix();
211  float fnear = 1e9;
212  float ffar = 0;
213 
214  QList<QgsChunkNode *> activeNodes = mTerrain->activeNodes();
215 
216  // it could be that there are no active nodes - they could be all culled or because root node
217  // is not yet loaded - we still need at least something to understand bounds of our scene
218  // so lets use the root node
219  if ( activeNodes.isEmpty() )
220  activeNodes << mTerrain->rootNode();
221 
222  Q_FOREACH ( QgsChunkNode *node, activeNodes )
223  {
224  // project each corner of bbox to camera coordinates
225  // and determine closest and farthest point.
226  QgsAABB bbox = node->bbox();
227  for ( int i = 0; i < 8; ++i )
228  {
229  QVector4D p( ( ( i >> 0 ) & 1 ) ? bbox.xMin : bbox.xMax,
230  ( ( i >> 1 ) & 1 ) ? bbox.yMin : bbox.yMax,
231  ( ( i >> 2 ) & 1 ) ? bbox.zMin : bbox.zMax, 1 );
232  QVector4D pc = viewMatrix * p;
233 
234  float dst = -pc.z(); // in camera coordinates, x grows right, y grows down, z grows to the back
235  if ( dst < fnear )
236  fnear = dst;
237  if ( dst > ffar )
238  ffar = dst;
239  }
240  }
241  if ( fnear < 1 )
242  fnear = 1; // does not really make sense to use negative far plane (behind camera)
243 
244  if ( fnear == 1e9 && ffar == 0 )
245  {
246  // the update didn't work out... this should not happen
247  // well at least temprarily use some conservative starting values
248  qDebug() << "oops... this should not happen! couldn't determine near/far plane. defaulting to 1...1e9";
249  fnear = 1;
250  ffar = 1e9;
251  }
252 
253  // set near/far plane - with some tolerance in front/behind expected near/far planes
254  camera->setFarPlane( ffar * 2 );
255  camera->setNearPlane( fnear / 2 );
256  }
257  else
258  qDebug() << "no terrain - not setting near/far plane";
259 
260  //qDebug() << "camera near/far" << mCameraController->camera()->nearPlane() << mCameraController->camera()->farPlane();
261 }
262 
263 void Qgs3DMapScene::onFrameTriggered( float dt )
264 {
265  mCameraController->frameTriggered( dt );
266 
267  Q_FOREACH ( QgsChunkedEntity *entity, mChunkEntities )
268  {
269  if ( entity->isEnabled() && entity->needsUpdate() )
270  {
271  qDebug() << "need for update";
272  entity->update( _sceneState( mCameraController ) );
273  }
274  }
275 }
276 
277 void Qgs3DMapScene::createTerrain()
278 {
279  if ( mTerrain )
280  {
281  mChunkEntities.removeOne( mTerrain );
282 
283  mTerrain->deleteLater();
284  mTerrain = nullptr;
285  }
286 
287  if ( !mTerrainUpdateScheduled )
288  {
289  // defer re-creation of terrain: there may be multiple invocations of this slot, so create the new entity just once
290  QTimer::singleShot( 0, this, &Qgs3DMapScene::createTerrainDeferred );
291  mTerrainUpdateScheduled = true;
292  }
293 }
294 
295 void Qgs3DMapScene::createTerrainDeferred()
296 {
297  double tile0width = mMap.terrainGenerator()->extent().width();
298  int maxZoomLevel = Qgs3DUtils::maxZoomLevel( tile0width, mMap.mapTileResolution(), mMap.maxTerrainGroundError() );
299 
300  mTerrain = new QgsTerrainEntity( maxZoomLevel, mMap );
301  //mTerrain->setEnabled(false);
302  mTerrain->setParent( this );
303 
304  if ( mMap.showTerrainBoundingBoxes() )
305  mTerrain->setShowBoundingBoxes( true );
306 
307  mCameraController->addTerrainPicker( mTerrain->terrainPicker() );
308 
309  mChunkEntities << mTerrain;
310 
311  onCameraChanged(); // force update of the new terrain
312 
313  // make sure that renderers for layers are re-created as well
314  Q_FOREACH ( QgsMapLayer *layer, mMap.layers() )
315  {
316  // remove old entity - if any
317  removeLayerEntity( layer );
318 
319  // add new entity - if any 3D renderer
320  addLayerEntity( layer );
321  }
322 
323  mTerrainUpdateScheduled = false;
324 
325  connect( mTerrain, &QgsTerrainEntity::pendingJobsCountChanged, this, &Qgs3DMapScene::terrainPendingJobsCountChanged );
326 
327  emit terrainEntityChanged();
328 }
329 
330 void Qgs3DMapScene::onBackgroundColorChanged()
331 {
332  mForwardRenderer->setClearColor( mMap.backgroundColor() );
333 }
334 
335 void Qgs3DMapScene::onLayerRenderer3DChanged()
336 {
337  QgsMapLayer *layer = qobject_cast<QgsMapLayer *>( sender() );
338  Q_ASSERT( layer );
339 
340  // remove old entity - if any
341  removeLayerEntity( layer );
342 
343  // add new entity - if any 3D renderer
344  addLayerEntity( layer );
345 }
346 
347 void Qgs3DMapScene::onLayersChanged()
348 {
349  QSet<QgsMapLayer *> layersBefore = QSet<QgsMapLayer *>::fromList( mLayerEntities.keys() );
350  QList<QgsMapLayer *> layersAdded;
351  Q_FOREACH ( QgsMapLayer *layer, mMap.layers() )
352  {
353  if ( !layersBefore.contains( layer ) )
354  {
355  layersAdded << layer;
356  }
357  else
358  {
359  layersBefore.remove( layer );
360  }
361  }
362 
363  // what is left in layersBefore are layers that have been removed
364  Q_FOREACH ( QgsMapLayer *layer, layersBefore )
365  {
366  removeLayerEntity( layer );
367  }
368 
369  Q_FOREACH ( QgsMapLayer *layer, layersAdded )
370  {
371  addLayerEntity( layer );
372  }
373 }
374 
375 void Qgs3DMapScene::addLayerEntity( QgsMapLayer *layer )
376 {
377  QgsAbstract3DRenderer *renderer = layer->renderer3D();
378  if ( renderer )
379  {
380  // Fix vector layer's renderer to make sure the renderer is pointing to its layer.
381  // It has happened before that renderer pointed to a different layer (probably after copying a style).
382  // This is a bit of a hack and it should be handled in QgsMapLayer::setRenderer3D() but in qgis_core
383  // the vector layer 3D renderer class is not available. Maybe we need an intermediate map layer 3D renderer
384  // class in qgis_core that can be used to handle this case nicely.
385  if ( layer->type() == QgsMapLayer::VectorLayer && renderer->type() == "vector" )
386  {
387  static_cast<QgsVectorLayer3DRenderer *>( renderer )->setLayer( static_cast<QgsVectorLayer *>( layer ) );
388  }
389 
390  Qt3DCore::QEntity *newEntity = renderer->createEntity( mMap );
391  if ( newEntity )
392  {
393  newEntity->setParent( this );
394  mLayerEntities.insert( layer, newEntity );
395  }
396  }
397 
398  connect( layer, &QgsMapLayer::renderer3DChanged, this, &Qgs3DMapScene::onLayerRenderer3DChanged );
399 
400  if ( layer->type() == QgsMapLayer::VectorLayer )
401  {
402  QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
403  connect( vlayer, &QgsVectorLayer::selectionChanged, this, &Qgs3DMapScene::onLayerRenderer3DChanged );
404  }
405 }
406 
407 void Qgs3DMapScene::removeLayerEntity( QgsMapLayer *layer )
408 {
409  Qt3DCore::QEntity *entity = mLayerEntities.take( layer );
410  if ( entity )
411  entity->deleteLater();
412 
413  disconnect( layer, &QgsMapLayer::renderer3DChanged, this, &Qgs3DMapScene::onLayerRenderer3DChanged );
414 
415  if ( layer->type() == QgsMapLayer::VectorLayer )
416  {
417  QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
418  disconnect( vlayer, &QgsVectorLayer::selectionChanged, this, &Qgs3DMapScene::onLayerRenderer3DChanged );
419  }
420 }
void addTerrainPicker(Qt3DRender::QObjectPicker *picker)
Connects to object picker attached to terrain entity.
Qgs3DMapScene(const Qgs3DMapSettings &map, Qt3DExtras::QForwardRenderer *defaultFrameGraph, Qt3DRender::QRenderSettings *renderSettings, Qt3DRender::QCamera *camera, const QRect &viewportRect, Qt3DCore::QNode *parent=nullptr)
Constructs a 3D scene based on map settings and Qt 3D renderer configuration.
QList< QgsMapLayer * > layers() const
Returns the list of map layers to be rendered as a texture of the terrain.
void cameraChanged()
Emitted when camera has been updated.
3 Axis-aligned bounding box - in world coords.
Definition: qgsaabb.h:30
A rectangle specified with double values.
Definition: qgsrectangle.h:40
Base class for all map layer types.
Definition: qgsmaplayer.h:61
void maxTerrainGroundErrorChanged()
Emitted when the maximum terrain ground error has changed.
bool showTerrainBoundingBoxes() const
Returns whether to display bounding boxes of terrain tiles (for debugging)
void frameTriggered(float dt)
Called internally from 3D scene when a new frame is generated. Updates camera according to keyboard/m...
Base class for all renderers that may to participate in 3D view.
void layersChanged()
Emitted when the list of map layers for terrain texture has changed.
QString skyboxFileBase() const
Returns base part of filenames of skybox (see setSkybox())
virtual Qt3DCore::QEntity * createEntity(const Qgs3DMapSettings &map) const =0
Returns a 3D entity that will be used to show renderer&#39;s data in 3D scene.
void terrainPendingJobsCountChanged()
Emitted when the number of terrain&#39;s pending jobs changes.
float zMax
Definition: qgsaabb.h:80
void resetView(float distance)
Move camera back to the initial position (looking down towards origin of world&#39;s coordinates) ...
void terrainVerticalScaleChanged()
Emitted when the vertical scale of the terrain has changed.
3 Definition of the world
QgsMapLayer::LayerType type() const
Returns the type of the layer.
float maxTerrainGroundError() const
Returns maximum ground error of terrain tiles in world units.
float zMin
Definition: qgsaabb.h:77
void viewportChanged()
Emitted when viewport rectangle has been updated.
float yMax
Definition: qgsaabb.h:79
virtual QString type() const =0
Returns unique identifier of the renderer class (used to identify subclass)
bool hasSkyboxEnabled() const
Returns whether skybox is enabled.
double width() const
Returns the width of the rectangle.
Definition: qgsrectangle.h:201
int mapTileResolution() const
Returns resolution (in pixels) of the texture of a terrain tile.
void terrainGeneratorChanged()
Emitted when the terrain generator has changed.
QColor backgroundColor() const
Returns background color of the 3D map view.
void selectionChanged(const QgsFeatureIds &selected, const QgsFeatureIds &deselected, bool clearAndSelect)
This signal is emitted when selection was changed.
3 Object that controls camera movement based on user input
static int maxZoomLevel(double tile0width, double tileResolution, double maxError)
Calculates the highest needed zoom level for tiles in quad-tree given width of the base tile (zoom le...
Definition: qgs3dutils.cpp:30
void mapTileResolutionChanged()
Emitted when the map tile resoulution has changed.
Qt3DRender::QCamera camera
QgsAbstract3DRenderer * renderer3D() const
Returns 3D renderer associated with the layer.
float xMin
Definition: qgsaabb.h:75
3D renderer that renders all features of a vector layer with the same 3D symbol.
float xMax
Definition: qgsaabb.h:78
void setCamera(Qt3DRender::QCamera *camera)
Assigns camera that should be controlled by this class. Called internally from 3D scene...
void maxTerrainScreenErrorChanged()
Emitted when the maximum terrain screen error has changed.
void setViewport(const QRect &viewport)
Sets viewport rectangle. Called internally from 3D canvas. Allows conversion of mouse coordinates...
QList< QgsAbstract3DRenderer * > renderers() const
Returns list of extra 3D renderers.
float yMin
Definition: qgsaabb.h:76
void viewZoomFull()
Resets camera view to show the whole scene (top view)
int terrainPendingJobsCount() const
Returns number of pending jobs of the terrain entity.
virtual QgsRectangle extent() const =0
extent of the terrain in terrain&#39;s CRS
QString skyboxFileExtension() const
Returns extension part of filenames of skybox (see setSkybox())
void terrainEntityChanged()
Emitted when the current terrain entity is replaced by a new one.
void backgroundColorChanged()
Emitted when the background color has changed.
QgsChunkedEntity::SceneState _sceneState(QgsCameraController *cameraController)
Represents a vector layer which manages a vector based data sets.
QgsCameraController * cameraController()
Returns camera controller.
Definition: qgs3dmapscene.h:58
double height() const
Returns the height of the rectangle.
Definition: qgsrectangle.h:208
void renderer3DChanged()
Signal emitted when 3D renderer associated with the layer has changed.
QgsTerrainGenerator * terrainGenerator() const
Returns terrain generator. It takes care of producing terrain tiles from the input data...