QGIS API Documentation 4.3.0-Master (16eddb2670e)
Loading...
Searching...
No Matches
qgs3dsceneexporter.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgs3dsceneexporter.cpp
3 --------------------------------------
4 Date : June 2020
5 Copyright : (C) 2020 by Belgacem Nedjima
6 Email : gb underscore nedjima at esi dot dz
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 "qgs3dsceneexporter.h"
17
18#include "qgs3dexportobject.h"
19#include "qgs3dmapsettings.h"
20#include "qgs3dutils.h"
23#include "qgschunknode.h"
28#include "qgsfutureutils.h"
29#include "qgsgeotransform.h"
30#include "qgsimagetexture.h"
31#include "qgsmeshterraingenerator.h"
32#include "qgsterrainentity.h"
33#include "qgsterraingenerator.h"
38#include "qgsvectorlayer.h"
40
41#include <QByteArray>
42#include <QFile>
43#include <QString>
44#include <QTextStream>
45#include <QVector>
46#include <Qt3DCore/QAttribute>
47#include <Qt3DCore/QBuffer>
48#include <Qt3DCore/QComponent>
49#include <Qt3DCore/QEntity>
50#include <Qt3DCore/QGeometry>
51#include <Qt3DCore/QNode>
52#include <Qt3DCore/QTransform>
53#include <Qt3DExtras/QConeGeometry>
54#include <Qt3DExtras/QCuboidGeometry>
55#include <Qt3DExtras/QCylinderGeometry>
56#include <Qt3DExtras/QDiffuseSpecularMaterial>
57#include <Qt3DExtras/QExtrudedTextMesh>
58#include <Qt3DExtras/QPhongMaterial>
59#include <Qt3DExtras/QPlaneGeometry>
60#include <Qt3DExtras/QSphereGeometry>
61#include <Qt3DExtras/QTextureMaterial>
62#include <Qt3DExtras/QTorusGeometry>
63#include <Qt3DRender/QAbstractTexture>
64#include <Qt3DRender/QAbstractTextureImage>
65#include <Qt3DRender/QGeometryRenderer>
66#include <Qt3DRender/QMesh>
67#include <Qt3DRender/QParameter>
68#include <Qt3DRender/QSceneLoader>
69#include <Qt3DRender/QTexture>
70#include <Qt3DRender/QTextureImage>
71
72#include "moc_qgs3dsceneexporter.cpp"
73
74using namespace Qt::StringLiterals;
75
76template<typename T> QVector<T> getAttributeData( Qt3DCore::QAttribute *attribute, const QByteArray &data )
77{
78 const uint bytesOffset = attribute->byteOffset();
79 const uint bytesStride = attribute->byteStride();
80 const uint vertexSize = attribute->vertexSize();
81 const uint dataSize = static_cast<uint>( data.size() );
82 QVector<T> result;
83
84 if ( bytesStride == 0 )
85 {
86 QgsDebugError( "bytesStride==0, the attribute probably was not set properly" );
87 return result;
88 }
89
90 const char *pData = data.constData();
91 for ( unsigned int i = bytesOffset; i < dataSize; i += bytesStride )
92 {
93 for ( unsigned int j = 0; j < vertexSize * sizeof( T ); j += sizeof( T ) )
94 {
95 T v;
96 memcpy( &v, pData + i + j, sizeof( T ) );
97 result.push_back( v );
98 }
99 }
100 return result;
101}
102
103template<typename T> QVector<uint> _getIndexDataImplementation( const QByteArray &data )
104{
105 QVector<uint> result;
106 const char *pData = data.constData();
107 for ( int i = 0; i < data.size(); i += sizeof( T ) )
108 {
109 T v;
110 memcpy( &v, pData + i, sizeof( T ) );
111 result.push_back( ( uint ) v );
112 }
113 return result;
114}
115
116QVector<uint> getIndexData( Qt3DCore::QAttribute *indexAttribute, const QByteArray &data )
117{
118 switch ( indexAttribute->vertexBaseType() )
119 {
120 case Qt3DCore::QAttribute::VertexBaseType::Int:
122 case Qt3DCore::QAttribute::VertexBaseType::UnsignedInt:
124 case Qt3DCore::QAttribute::VertexBaseType::Short:
126 case Qt3DCore::QAttribute::VertexBaseType::UnsignedShort:
128 case Qt3DCore::QAttribute::VertexBaseType::Byte:
130 case Qt3DCore::QAttribute::VertexBaseType::UnsignedByte:
132 default:
133 QgsDebugError( "Probably trying to get index data using an attribute that has vertex data" );
134 break;
135 }
136 return QVector<uint>();
137}
138
139QByteArray getData( Qt3DCore::QBuffer *buffer )
140{
141 QByteArray bytes = buffer->data();
142 if ( bytes.isNull() )
143 {
144 QgsDebugError( "QBuffer is null" );
145 }
146 return bytes;
147}
148
149Qt3DCore::QAttribute *findAttribute( Qt3DCore::QGeometry *geometry, const QString &name, Qt3DCore::QAttribute::AttributeType type )
150{
151 QVector<Qt3DCore::QAttribute *> attributes = geometry->attributes();
152 for ( Qt3DCore::QAttribute *attribute : attributes )
153 {
154 if ( attribute->attributeType() != type )
155 continue;
156 if ( name.isEmpty() || attribute->name() == name )
157 return attribute;
158 }
159 return nullptr;
160}
161
162template<typename Component> Component *findTypedComponent( Qt3DCore::QEntity *entity )
163{
164 if ( !entity )
165 return nullptr;
166 QVector<Qt3DCore::QComponent *> components = entity->components();
167 for ( Qt3DCore::QComponent *component : components )
168 {
169 Component *typedComponent = qobject_cast<Component *>( component );
170 if ( typedComponent )
171 return typedComponent;
172 }
173 return nullptr;
174}
175
176bool Qgs3DSceneExporter::parseVectorLayerEntity( Qt3DCore::QEntity *entity, QgsVectorLayer *layer )
177{
178 QgsAbstract3DRenderer *abstractRenderer = layer->renderer3D();
179 const QString rendererType = abstractRenderer->type();
180
181 if ( rendererType == "rulebased" )
182 {
183 int prevSize = mObjects.size();
184 // Potential bug: meshes loaded using Qt3DRender::QSceneLoader will probably have wrong scale and translation
185 const QList<Qt3DRender::QGeometryRenderer *> renderers = entity->findChildren<Qt3DRender::QGeometryRenderer *>();
186 for ( Qt3DRender::QGeometryRenderer *renderer : renderers )
187 {
188 Qt3DCore::QEntity *parentEntity = qobject_cast<Qt3DCore::QEntity *>( renderer->parent() );
189 if ( !parentEntity )
190 continue;
191 Qgs3DExportObject *object = processGeometryRenderer( renderer, layer->name() + u"_"_s );
192 if ( !object )
193 continue;
194 if ( mExportTextures )
195 processEntityMaterial( parentEntity, object );
196 mObjects.push_back( object );
197 }
198
199 mObjects << processLines( entity, layer->name() + u"_"_s );
200
201 return mObjects.size() > prevSize;
202 }
203
204 else if ( rendererType == "vector" )
205 {
206 QgsVectorLayer3DRenderer *vectorLayerRenderer = dynamic_cast<QgsVectorLayer3DRenderer *>( abstractRenderer );
207 if ( vectorLayerRenderer )
208 {
209 const QgsAbstract3DSymbol *symbol = vectorLayerRenderer->symbol();
210 return symbol->exportGeometries( this, entity, layer->name() + u"_"_s );
211 }
212 else
213 return false;
214 }
215
216 else
217 {
218 // TODO: handle pointcloud/mesh/etc. layers
219 QgsDebugMsgLevel( u"Type '%1' of layer '%2' is not exportable."_s.arg( layer->name(), rendererType ), 2 );
220 return false;
221 }
222
223 return false;
224}
225
226void Qgs3DSceneExporter::processEntityMaterial( Qt3DCore::QEntity *entity, Qgs3DExportObject *object ) const
227{
228 Qt3DExtras::QPhongMaterial *phongMaterial = findTypedComponent<Qt3DExtras::QPhongMaterial>( entity );
229 if ( phongMaterial )
230 {
232 object->setupMaterial( &material );
233 }
234
235 Qt3DExtras::QDiffuseSpecularMaterial *diffuseMapMaterial = findTypedComponent<Qt3DExtras::QDiffuseSpecularMaterial>( entity );
236 if ( diffuseMapMaterial )
237 {
238 const Qt3DRender::QTexture2D *diffuseTexture = diffuseMapMaterial->diffuse().value<Qt3DRender::QTexture2D *>();
239 if ( diffuseTexture )
240 {
241 const QVector<Qt3DRender::QAbstractTextureImage *> textureImages = diffuseTexture->textureImages();
242 for ( const Qt3DRender::QAbstractTextureImage *tex : textureImages )
243 {
244 const QgsImageTexture *imageTexture = dynamic_cast<const QgsImageTexture *>( tex );
245 if ( imageTexture )
246 {
247 const QImage image = imageTexture->getImage();
248 object->setTextureImage( image );
249 break;
250 }
251 }
252 }
253 }
254}
255
256void Qgs3DSceneExporter::parseTerrain( QgsTerrainEntity *terrain, const QString &layerName )
257{
258 Qgs3DMapSettings *settings = terrain->mapSettings();
259 if ( !settings->terrainRenderingEnabled() || !mTerrainExportEnabled )
260 return;
261
262 QgsChunkNode *node = terrain->rootNode();
263
264 QgsTerrainGenerator *generator = settings->terrainGenerator();
265 if ( !generator )
266 return;
267 QgsTerrainTileEntity *terrainTile = nullptr;
268 QgsTerrainTextureGenerator *textureGenerator = terrain->textureGenerator();
269 const QSize oldResolution = textureGenerator->textureSize();
270 textureGenerator->setTextureSize( QSize( mTerrainTextureResolution, mTerrainTextureResolution ) );
271 switch ( generator->type() )
272 {
274 terrainTile = getDemTerrainEntity( terrain, node, settings->origin() );
275 parseDemTile( terrainTile, layerName + u"_"_s );
276 break;
278 terrainTile = getFlatTerrainEntity( terrain, node, settings->origin() );
279 parseFlatTile( terrainTile, layerName + u"_"_s );
280 break;
282 terrainTile = getMeshTerrainEntity( terrain, node, settings->origin() );
283 parseMeshTile( terrainTile, layerName + u"_"_s );
284 break;
285 // TODO: implement other terrain types
288 break;
289 }
290 textureGenerator->setTextureSize( oldResolution );
291}
292
293QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
294{
295 QgsFlatTerrainGenerator *generator = qgis::down_cast<QgsFlatTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
296 QFuture<QgsChunkLoaderResult> resultFuture = generator->loadChunk( node );
297 QgsFutureUtils::waitForFinished( resultFuture );
298 // the entity we created will be deallocated once the scene exporter is deallocated
299 Qt3DCore::QEntity *entity = resultFuture.takeResult().createEntity( this );
300 QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( entity );
301
302 const QList<QgsGeoTransform *> transforms = entity->findChildren<QgsGeoTransform *>();
303 for ( QgsGeoTransform *transform : transforms )
304 {
305 transform->setOrigin( mapOrigin );
306 }
307
308 return tileEntity;
309}
310
311QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
312{
313 // Just create a new tile (we don't need to export exact level of details as in the scene)
314 // create the entity synchronously and then it will be deleted once our scene exporter instance is deallocated
315 QgsDemTerrainGenerator *generator = qgis::down_cast<QgsDemTerrainGenerator *>( terrain->mapSettings()->terrainGenerator()->clone() );
316 generator->setResolution( mTerrainResolution );
317 QFuture<QgsChunkLoaderResult> resultFuture = generator->loadChunk( node );
318 QgsFutureUtils::waitForFinished( resultFuture );
319 QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( resultFuture.takeResult().createEntity( this ) );
320
321 const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
322 for ( QgsGeoTransform *transform : transforms )
323 {
324 transform->setOrigin( mapOrigin );
325 }
326
327 delete generator;
328 return tileEntity;
329}
330
331QgsTerrainTileEntity *Qgs3DSceneExporter::getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
332{
333 QgsMeshTerrainGenerator *generator = qgis::down_cast<QgsMeshTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
334 QFuture<QgsChunkLoaderResult> resultFuture = generator->loadChunk( node );
335 QgsFutureUtils::waitForFinished( resultFuture );
336 QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( resultFuture.takeResult().createEntity( this ) );
337
338 const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
339 for ( QgsGeoTransform *transform : transforms )
340 {
341 transform->setOrigin( mapOrigin );
342 }
343
344 return tileEntity;
345}
346
347void Qgs3DSceneExporter::parseFlatTile( QgsTerrainTileEntity *tileEntity, const QString &layerName )
348{
349 Qt3DRender::QGeometryRenderer *mesh = findTypedComponent<Qt3DRender::QGeometryRenderer>( tileEntity );
350 Qt3DCore::QTransform *transform = findTypedComponent<Qt3DCore::QTransform>( tileEntity );
351
352 Qt3DCore::QGeometry *geometry = mesh->geometry();
353 Qt3DExtras::QPlaneGeometry *tileGeometry = qobject_cast<Qt3DExtras::QPlaneGeometry *>( geometry );
354 if ( !tileGeometry )
355 {
356 QgsDebugError( "Qt3DExtras::QPlaneGeometry* is expected but something else was given" );
357 return;
358 }
359
360 // Generate vertice data
361 Qt3DCore::QAttribute *positionAttribute = tileGeometry->positionAttribute();
362 if ( !positionAttribute )
363 {
364 QgsDebugError( QString( "Cannot export '%1' - geometry has no position attribute!" ).arg( layerName ) );
365 return;
366 }
367 const QByteArray verticesBytes = getData( positionAttribute->buffer() );
368 if ( verticesBytes.isNull() )
369 {
370 QgsDebugError( QString( "Geometry for '%1' has position attribute with empty data!" ).arg( layerName ) );
371 return;
372 }
373 const QVector<float> positionBuffer = getAttributeData<float>( positionAttribute, verticesBytes );
374
375 // Generate index data
376 Qt3DCore::QAttribute *indexAttribute = tileGeometry->indexAttribute();
377 if ( !indexAttribute )
378 {
379 QgsDebugError( QString( "Cannot export '%1' - geometry has no index attribute!" ).arg( layerName ) );
380 return;
381 }
382 const QByteArray indexBytes = getData( indexAttribute->buffer() );
383 if ( indexBytes.isNull() )
384 {
385 QgsDebugError( QString( "Geometry for '%1' has index attribute with empty data!" ).arg( layerName ) );
386 return;
387 }
388 const QVector<uint> indexesBuffer = getIndexData( indexAttribute, indexBytes );
389
390 QString objectNamePrefix = layerName;
391 if ( objectNamePrefix != QString() )
392 objectNamePrefix += QString();
393
394 Qgs3DExportObject *object = new Qgs3DExportObject( getObjectName( objectNamePrefix + u"Flat_tile"_s ) );
395 mObjects.push_back( object );
396
397 object->setSmoothEdges( mSmoothEdges );
398 object->setupTriangle( positionBuffer, indexesBuffer, transform->matrix() );
399
400 if ( mExportNormals )
401 {
402 // Everts
403 QVector<float> normalsBuffer;
404 for ( int i = 0; i < positionBuffer.size(); i += 3 )
405 normalsBuffer << 0.0f << 1.0f << 0.0f;
406 object->setupNormalCoordinates( normalsBuffer, transform->matrix() );
407 }
408
409 Qt3DCore::QAttribute *texCoordsAttribute = tileGeometry->texCoordAttribute();
410 if ( mExportTextures && texCoordsAttribute )
411 {
412 // Reuse vertex buffer data for texture coordinates
413 const QVector<float> texCoords = getAttributeData<float>( texCoordsAttribute, verticesBytes );
414 object->setupTextureCoordinates( texCoords );
415
416 QgsTerrainTextureImage *textureImage = tileEntity->textureImage();
417 const QImage img = textureImage->getImage();
418 object->setTextureImage( img );
419 }
420}
421
422void Qgs3DSceneExporter::parseDemTile( QgsTerrainTileEntity *tileEntity, const QString &layerName )
423{
424 Qt3DRender::QGeometryRenderer *mesh = findTypedComponent<Qt3DRender::QGeometryRenderer>( tileEntity );
425 Qt3DCore::QTransform *transform = findTypedComponent<Qt3DCore::QTransform>( tileEntity );
426
427 Qt3DCore::QGeometry *geometry = mesh->geometry();
428 DemTerrainTileGeometry *tileGeometry = qobject_cast<DemTerrainTileGeometry *>( geometry );
429 if ( !tileGeometry )
430 {
431 QgsDebugError( "DemTerrainTileGeometry* is expected but something else was given" );
432 return;
433 }
434
435 Qt3DCore::QAttribute *positionAttribute = tileGeometry->positionAttribute();
436 const QByteArray positionBytes = positionAttribute->buffer()->data();
437 const QVector<float> positionBuffer = getAttributeData<float>( positionAttribute, positionBytes );
438
439 Qt3DCore::QAttribute *indexAttribute = tileGeometry->indexAttribute();
440 const QByteArray indexBytes = indexAttribute->buffer()->data();
441 const QVector<unsigned int> indexBuffer = getIndexData( indexAttribute, indexBytes );
442
443 Qgs3DExportObject *object = new Qgs3DExportObject( getObjectName( layerName + u"DEM_tile"_s ) );
444 mObjects.push_back( object );
445
446 object->setSmoothEdges( mSmoothEdges );
447 object->setupTriangle( positionBuffer, indexBuffer, transform->matrix() );
448
449 Qt3DCore::QAttribute *normalsAttributes = tileGeometry->normalAttribute();
450 if ( mExportNormals && normalsAttributes )
451 {
452 const QByteArray normalsBytes = normalsAttributes->buffer()->data();
453 const QVector<float> normalsBuffer = getAttributeData<float>( normalsAttributes, normalsBytes );
454 object->setupNormalCoordinates( normalsBuffer, transform->matrix() );
455 }
456
457 Qt3DCore::QAttribute *texCoordsAttribute = tileGeometry->texCoordsAttribute();
458 if ( mExportTextures && texCoordsAttribute )
459 {
460 const QByteArray texCoordsBytes = texCoordsAttribute->buffer()->data();
461 const QVector<float> texCoordsBuffer = getAttributeData<float>( texCoordsAttribute, texCoordsBytes );
462 object->setupTextureCoordinates( texCoordsBuffer );
463
464 QgsTerrainTextureImage *textureImage = tileEntity->textureImage();
465 const QImage img = textureImage->getImage();
466 object->setTextureImage( img );
467 }
468}
469
470void Qgs3DSceneExporter::parseMeshTile( QgsTerrainTileEntity *tileEntity, const QString &layerName )
471{
472 QString objectNamePrefix = layerName;
473 if ( objectNamePrefix != QString() )
474 objectNamePrefix += '_'_L1;
475
476 const QList<Qt3DRender::QGeometryRenderer *> renderers = tileEntity->findChildren<Qt3DRender::QGeometryRenderer *>();
477 for ( Qt3DRender::QGeometryRenderer *renderer : renderers )
478 {
479 Qgs3DExportObject *obj = processGeometryRenderer( renderer, objectNamePrefix );
480 if ( !obj )
481 continue;
482 mObjects << obj;
483 }
484}
485
486QVector<Qgs3DExportObject *> Qgs3DSceneExporter::processInstancedPointGeometry( Qt3DCore::QEntity *entity, const QString &objectNamePrefix )
487{
488 // built-in Qt3D geometries (e.g. cylinder, plane, ...) assume Y axis going "up",
489 // They are rotated to have their Z axis goes "up", like the rest of the scene
490 // Retrieve the rotation matrix
491 const QMatrix4x4 instanceMaterialTransform( 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 );
492
493 QVector3D symbolScale;
494 QVector4D symbolRotation;
495 const QList<Qt3DRender::QMaterial *> materials = entity->findChildren<Qt3DRender::QMaterial *>();
496 for ( Qt3DRender::QMaterial *material : materials )
497 {
498 for ( const Qt3DRender::QParameter *parameter : material->parameters() )
499 {
500 if ( parameter->name() == "symbolScale"_L1 )
501 {
502 symbolScale = parameter->value().value<QVector3D>();
503 }
504 else if ( parameter->name() == "symbolRotation"_L1 )
505 {
506 symbolRotation = parameter->value().value<QVector4D>();
507 }
508 }
509 }
510
511 QQuaternion rotationQuat( symbolRotation.w(), symbolRotation.x(), symbolRotation.y(), symbolRotation.z() );
512
513 QVector<Qgs3DExportObject *> objects;
514 const QList<Qt3DCore::QGeometry *> geometriesList = entity->findChildren<Qt3DCore::QGeometry *>();
515 for ( Qt3DCore::QGeometry *geometry : geometriesList )
516 {
517 Qt3DCore::QAttribute *positionAttribute = findAttribute( geometry, Qt3DCore::QAttribute::defaultPositionAttributeName(), Qt3DCore::QAttribute::VertexAttribute );
518 Qt3DCore::QAttribute *indexAttribute = findAttribute( geometry, QString(), Qt3DCore::QAttribute::IndexAttribute );
519 if ( !positionAttribute || !indexAttribute )
520 {
521 QgsDebugError( QString( "Cannot export '%1' - geometry has no position or index attribute!" ).arg( objectNamePrefix ) );
522 continue;
523 }
524
525 Qt3DCore::QAttribute *instanceScaleAttribute = findAttribute( geometry, u"instanceScale"_s, Qt3DCore::QAttribute::VertexAttribute );
526 Qt3DCore::QAttribute *instanceRotationAttribute = findAttribute( geometry, u"instanceRotation"_s, Qt3DCore::QAttribute::VertexAttribute );
527
528 const QByteArray vertexBytes = positionAttribute->buffer()->data();
529 const QByteArray indexBytes = indexAttribute->buffer()->data();
530 if ( vertexBytes.isNull() || indexBytes.isNull() )
531 {
532 QgsDebugError( QString( "Geometry for '%1' has position or index attribute with empty data!" ).arg( objectNamePrefix ) );
533 continue;
534 }
535
536 const QVector<float> positionData = getAttributeData<float>( positionAttribute, vertexBytes );
537 const QVector<uint> indexData = getIndexData( indexAttribute, indexBytes );
538
539 const QVector<QVector3D> instanceScaleData = instanceScaleAttribute ? getAttributeData<QVector3D>( instanceScaleAttribute, instanceScaleAttribute->buffer()->data() ) : QVector<QVector3D>();
540 const QVector<QVector4D> instanceRotationData = instanceRotationAttribute ? getAttributeData<QVector4D>( instanceRotationAttribute, instanceRotationAttribute->buffer()->data() )
541 : QVector<QVector4D>();
542
543 Qt3DCore::QAttribute *instanceDataAttribute = findAttribute( geometry, u"instanceTranslation"_s, Qt3DCore::QAttribute::VertexAttribute );
544 if ( !instanceDataAttribute )
545 {
546 QgsDebugError( QString( "Cannot export '%1' - geometry has no instanceTranslation attribute!" ).arg( objectNamePrefix ) );
547 continue;
548 }
549 const QByteArray instancePositionBytes = getData( instanceDataAttribute->buffer() );
550 if ( instancePositionBytes.isNull() )
551 {
552 QgsDebugError( QString( "Geometry for '%1' has instanceTranslation attribute with empty data!" ).arg( objectNamePrefix ) );
553 continue;
554 }
555 QVector<float> instancePosition = getAttributeData<float>( instanceDataAttribute, instancePositionBytes );
556
557 int instanceIndex = 0;
558 for ( int i = 0; i < instancePosition.size(); i += 3, instanceIndex++ )
559 {
560 Qgs3DExportObject *object = new Qgs3DExportObject( getObjectName( objectNamePrefix + u"instance_point"_s ) );
561 objects.push_back( object );
562 QMatrix4x4 instanceTransform;
563 instanceTransform.translate( instancePosition[i], instancePosition[i + 1], instancePosition[i + 2] );
564
565 QQuaternion instanceRotation = rotationQuat;
566 if ( instanceRotationAttribute )
567 {
568 const QVector4D instanceRotationVec = instanceRotationData[instanceIndex];
569 instanceRotation = QQuaternion( instanceRotationVec.w(), instanceRotationVec.x(), instanceRotationVec.y(), instanceRotationVec.z() );
570 }
571 instanceTransform.rotate( instanceRotation );
572 instanceTransform.scale( instanceScaleAttribute ? instanceScaleData[instanceIndex] : symbolScale );
573 instanceTransform *= instanceMaterialTransform;
574 object->setupTriangle( positionData, indexData, instanceTransform );
575
576 object->setSmoothEdges( mSmoothEdges );
577
578 Qt3DCore::QAttribute *normalsAttribute = findAttribute( geometry, Qt3DCore::QAttribute::defaultNormalAttributeName(), Qt3DCore::QAttribute::VertexAttribute );
579 if ( mExportNormals && normalsAttribute )
580 {
581 // Reuse vertex bytes
582 const QVector<float> normalsData = getAttributeData<float>( normalsAttribute, vertexBytes );
583 object->setupNormalCoordinates( normalsData, instanceTransform );
584 }
585 }
586 }
587
588 return objects;
589}
590
591QVector<Qgs3DExportObject *> Qgs3DSceneExporter::processSceneLoaderGeometries( Qt3DRender::QSceneLoader *sceneLoader, const QString &objectNamePrefix )
592{
593 QVector<Qgs3DExportObject *> objects;
594 Qt3DCore::QEntity *sceneLoaderParent = qobject_cast<Qt3DCore::QEntity *>( sceneLoader->parent() );
595 Qt3DCore::QTransform *entityTransform = findTypedComponent<Qt3DCore::QTransform>( sceneLoaderParent );
596 QMatrix4x4 sceneTransform;
597 if ( entityTransform )
598 {
599 sceneTransform = entityTransform->matrix();
600 }
601 QStringList entityNames = sceneLoader->entityNames();
602 for ( const QString &entityName : entityNames )
603 {
604 Qt3DRender::QGeometryRenderer *mesh = qobject_cast<Qt3DRender::QGeometryRenderer *>( sceneLoader->component( entityName, Qt3DRender::QSceneLoader::GeometryRendererComponent ) );
605 Qgs3DExportObject *object = processGeometryRenderer( mesh, objectNamePrefix, sceneTransform );
606 if ( !object )
607 continue;
608 objects.push_back( object );
609 }
610 return objects;
611}
612
613Qgs3DExportObject *Qgs3DSceneExporter::processGeometryRenderer( Qt3DRender::QGeometryRenderer *geomRenderer, const QString &objectNamePrefix, const QMatrix4x4 &sceneTransform )
614{
615 // We only export triangles for now
616 if ( geomRenderer->primitiveType() != Qt3DRender::QGeometryRenderer::Triangles )
617 return nullptr;
618
619 Qt3DCore::QGeometry *geometry = geomRenderer->geometry();
620 if ( !geometry )
621 return nullptr;
622
623 if ( findAttribute( geometry, u"pointA"_s, Qt3DCore::QAttribute::VertexAttribute ) )
624 return nullptr;
625
626 // === Compute triangleIndexStartingIndiceToKeep according to duplicated features
627 //
628 // In the case of polygons, we have multiple feature geometries within the same geometry object (QgsTessellatedPolygonGeometry).
629 // The QgsTessellatedPolygonGeometry class holds the list of all feature ids included.
630 // To avoid exporting the same geometry (it can be included in multiple QgsTessellatedPolygonGeometry) more than once,
631 // we keep a list of already exported fid and compare with the fid of the current QgsTessellatedPolygonGeometry.
632 // As we cannot retrieve the specific geometry part for a featureId from the QgsTessellatedPolygonGeometry, we only reject
633 // the geometry if all the featureid are already present.
634 QVector<std::pair<uint, uint>> triangleIndexStartingIndiceToKeep;
635 QgsTessellatedPolygonGeometry *tessGeom = dynamic_cast<QgsTessellatedPolygonGeometry *>( geometry );
636 if ( tessGeom )
637 {
638 QVector<QgsFeatureId> featureIds = tessGeom->featureIds();
639
640 QSet<QgsFeatureId> tempFeatToAdd;
641 QVector<uint> triangleIndex = tessGeom->triangleIndexStartingIndices();
642
643 for ( int idx = 0; idx < featureIds.size(); idx++ )
644 {
645 const QgsFeatureId feat = featureIds[idx];
646 if ( !mExportedFeatureIds.contains( feat ) )
647 {
648 // add the feature (as it was unknown) to temp set and not to the mExportedFeatureIds (as featureIds can have the same id multiple times)
649 tempFeatToAdd += feat;
650
651 // keep the feature triangle indexes
652 const uint startIdx = triangleIndex[idx];
653 const uint endIdx = idx < triangleIndex.size() - 1 ? triangleIndex[idx + 1] : std::numeric_limits<uint>::max();
654
655 if ( startIdx < endIdx ) // keep only valid intervals
656 triangleIndexStartingIndiceToKeep.append( std::pair<uint, uint>( startIdx, endIdx ) );
657 }
658 }
659 mExportedFeatureIds += tempFeatToAdd;
660
661 if ( triangleIndexStartingIndiceToKeep.isEmpty() ) // all featureid are already exported
662 {
663 return nullptr;
664 }
665 }
666
667 // === Compute inherited scale and translation from child entity to parent
668 QMatrix4x4 transformMatrix = sceneTransform;
669 QObject *parent = geomRenderer->parent();
670 while ( parent )
671 {
672 Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( parent );
673 Qt3DCore::QTransform *transform = findTypedComponent<Qt3DCore::QTransform>( entity );
674 if ( transform )
675 {
676 transformMatrix = transform->matrix() * transformMatrix;
677 }
678 parent = parent->parent();
679 }
680
681 Qt3DCore::QAttribute *positionAttribute = nullptr;
682 Qt3DCore::QAttribute *indexAttribute = nullptr;
683 QByteArray indexBytes, vertexBytes;
684 QVector<uint> indexDataTmp;
685 QVector<uint> indexData;
686 QVector<float> positionData;
687
688 // === Extract position data
689 positionAttribute = findAttribute( geometry, Qt3DCore::QAttribute::defaultPositionAttributeName(), Qt3DCore::QAttribute::VertexAttribute );
690 if ( !positionAttribute )
691 {
692 QgsDebugError( QString( "Cannot export '%1' - geometry has no position attribute!" ).arg( objectNamePrefix ) );
693 return nullptr;
694 }
695
696 vertexBytes = getData( positionAttribute->buffer() );
697 if ( vertexBytes.isNull() )
698 {
699 QgsDebugError( QString( "Will not export '%1' as geometry has empty position data!" ).arg( objectNamePrefix ) );
700 return nullptr;
701 }
702
703 positionData = getAttributeData<float>( positionAttribute, vertexBytes );
704
705 // === Search for face index data
706 QVector<Qt3DCore::QAttribute *> attributes = geometry->attributes();
707 for ( Qt3DCore::QAttribute *attribute : attributes )
708 {
709 if ( attribute->attributeType() == Qt3DCore::QAttribute::IndexAttribute )
710 {
711 indexAttribute = attribute;
712 indexBytes = getData( indexAttribute->buffer() );
713 if ( indexBytes.isNull() )
714 {
715 QgsDebugError( QString( "Geometry for '%1' has index attribute with empty data!" ).arg( objectNamePrefix ) );
716 }
717 else
718 {
719 indexDataTmp = getIndexData( indexAttribute, indexBytes );
720 break;
721 }
722 }
723 }
724
725 // for tessellated polygons that don't have index attributes, build them from positionData
726 if ( !indexAttribute )
727 {
728 for ( uint i = 0; i < static_cast<uint>( positionData.size() / 3 ); ++i )
729 {
730 indexDataTmp.push_back( i );
731 }
732 }
733
734 // === Filter face index data if needed
735 if ( triangleIndexStartingIndiceToKeep.isEmpty() )
736 {
737 // when geometry is NOT a QgsTessellatedPolygonGeometry, no filter, take them all
738 indexData.append( indexDataTmp );
739 }
740 else
741 {
742 // when geometry is a QgsTessellatedPolygonGeometry, filter according to triangleIndexStartingIndiceToKeep
743 int intervalIdx = 0;
744 const int triangleIndexStartingIndiceToKeepSize = triangleIndexStartingIndiceToKeep.size();
745 const uint indexDataTmpSize = static_cast<uint>( indexDataTmp.size() );
746 for ( uint i = 0; i + 2 < indexDataTmpSize; i += 3 )
747 {
748 const uint triangleIdx = i / 3;
749
750 // search for valid triangle index interval
751 while ( intervalIdx < triangleIndexStartingIndiceToKeepSize && triangleIdx >= triangleIndexStartingIndiceToKeep[intervalIdx].second )
752 {
753 intervalIdx++;
754 }
755
756 // keep only triangles within the triangle index interval
757 if ( intervalIdx < triangleIndexStartingIndiceToKeepSize && triangleIdx >= triangleIndexStartingIndiceToKeep[intervalIdx].first && triangleIdx < triangleIndexStartingIndiceToKeep[intervalIdx].second )
758 {
759 indexData.push_back( indexDataTmp[static_cast<int>( i )] );
760 indexData.push_back( indexDataTmp[static_cast<int>( i + 1 )] );
761 indexData.push_back( indexDataTmp[static_cast<int>( i + 2 )] );
762 }
763 }
764 }
765
766 // === Create Qgs3DExportObject
767 Qgs3DExportObject *object = new Qgs3DExportObject( getObjectName( objectNamePrefix + u"mesh_geometry"_s ) );
768 object->setupTriangle( positionData, indexData, transformMatrix );
769
770 Qt3DCore::QAttribute *normalsAttribute = findAttribute( geometry, Qt3DCore::QAttribute::defaultNormalAttributeName(), Qt3DCore::QAttribute::VertexAttribute );
771 if ( mExportNormals && normalsAttribute )
772 {
773 // Reuse vertex bytes
774 const QVector<float> normalsData = getAttributeData<float>( normalsAttribute, vertexBytes );
775 object->setupNormalCoordinates( normalsData, transformMatrix );
776 }
777
778 Qt3DCore::QAttribute *texCoordsAttribute = findAttribute( geometry, Qt3DCore::QAttribute::defaultTextureCoordinateAttributeName(), Qt3DCore::QAttribute::VertexAttribute );
779 if ( mExportTextures && texCoordsAttribute )
780 {
781 // Reuse vertex bytes
782 const QVector<float> texCoordsData = getAttributeData<float>( texCoordsAttribute, vertexBytes );
783 object->setupTextureCoordinates( texCoordsData );
784 }
785
786 return object;
787}
788
789QVector<Qgs3DExportObject *> Qgs3DSceneExporter::processLines( Qt3DCore::QEntity *entity, const QString &objectNamePrefix )
790{
791 QVector<Qgs3DExportObject *> objs;
792 const QList<Qt3DRender::QGeometryRenderer *> renderers = entity->findChildren<Qt3DRender::QGeometryRenderer *>();
793 for ( Qt3DRender::QGeometryRenderer *renderer : renderers )
794 {
795 Qt3DCore::QGeometry *geom = renderer->geometry();
796 Qt3DCore::QAttribute *pointAAttribute = findAttribute( geom, u"pointA"_s, Qt3DCore::QAttribute::VertexAttribute );
797 Qt3DCore::QAttribute *pointBAttribute = findAttribute( geom, u"pointB"_s, Qt3DCore::QAttribute::VertexAttribute );
798 Qt3DCore::QAttribute *pointCAttribute = findAttribute( geom, u"pointC"_s, Qt3DCore::QAttribute::VertexAttribute );
799 if ( !pointAAttribute || !pointBAttribute || pointCAttribute )
800 continue;
801
802 const QByteArray pointABytes = getData( pointAAttribute->buffer() );
803 const QByteArray pointBBytes = getData( pointBAttribute->buffer() );
804 if ( pointABytes.isNull() || pointBBytes.isNull() )
805 {
806 QgsDebugError( QString( "Geometry for '%1' has pointA or pointB attribute with empty data!" ).arg( objectNamePrefix ) );
807 continue;
808 }
809 const QVector<float> pointAData = getAttributeData<float>( pointAAttribute, pointABytes );
810 const QVector<float> pointBData = getAttributeData<float>( pointBAttribute, pointBBytes );
811
812 QVector<float> positionData;
813 positionData.reserve( pointAData.size() + pointBData.size() );
814 for ( int i = 0; i + 2 < pointAData.size(); i += 3 )
815 {
816 positionData << pointAData[i] << pointAData[i + 1] << pointAData[i + 2];
817 positionData << pointBData[i] << pointBData[i + 1] << pointBData[i + 2];
818 }
819
820 Qgs3DExportObject *exportObject = new Qgs3DExportObject( getObjectName( objectNamePrefix + u"line"_s ) );
821 exportObject->setupLine( positionData );
822
823 objs.push_back( exportObject );
824 }
825 return objs;
826}
827
828Qgs3DExportObject *Qgs3DSceneExporter::processPoints( Qt3DCore::QEntity *entity, const QString &objectNamePrefix )
829{
830 QVector<float> points;
831 const QList<Qt3DRender::QGeometryRenderer *> renderers = entity->findChildren<Qt3DRender::QGeometryRenderer *>();
832 for ( Qt3DRender::QGeometryRenderer *renderer : renderers )
833 {
834 Qt3DCore::QGeometry *geometry = qobject_cast<QgsBillboardGeometry *>( renderer->geometry() );
835 if ( !geometry )
836 continue;
837 Qt3DCore::QAttribute *positionAttribute = findAttribute( geometry, "instancePosition", Qt3DCore::QAttribute::VertexAttribute );
838 if ( !positionAttribute )
839 {
840 QgsDebugError( QString( "Cannot export '%1' - geometry has no position attribute!" ).arg( objectNamePrefix ) );
841 continue;
842 }
843 const QByteArray positionBytes = getData( positionAttribute->buffer() );
844 if ( positionBytes.isNull() )
845 {
846 QgsDebugError( QString( "Geometry for '%1' has position attribute with empty data!" ).arg( objectNamePrefix ) );
847 continue;
848 }
849 const QVector<float> positions = getAttributeData<float>( positionAttribute, positionBytes );
850 points << positions;
851 }
852 Qgs3DExportObject *obj = new Qgs3DExportObject( getObjectName( objectNamePrefix + u"points"_s ) );
853 obj->setupPoint( points );
854 return obj;
855}
856
857bool Qgs3DSceneExporter::save( QString sceneName, QString sceneFolderPath, const Qgis::Export3DSceneFormat &exportFormat, int precision ) const
858{
859 if ( mObjects.isEmpty() )
860 {
861 return false;
862 }
863
864 switch ( exportFormat )
865 {
867 return saveObj( sceneName, sceneFolderPath, precision );
869 return saveStl( sceneName, sceneFolderPath, precision );
870 }
871
873}
874
875bool Qgs3DSceneExporter::saveObj( QString sceneName, QString sceneFolderPath, int precision ) const
876{
877 const QString objFilePath = QDir( sceneFolderPath ).filePath( sceneName + u".obj"_s );
878 const QString mtlFilePath = QDir( sceneFolderPath ).filePath( sceneName + u".mtl"_s );
879
880 QFile file( objFilePath );
881 if ( !file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
882 {
883 QgsDebugError( u"Scene can not be exported to '%1'. File access error."_s.arg( objFilePath ) );
884 return false;
885 }
886 QFile mtlFile( mtlFilePath );
887 if ( !mtlFile.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
888 {
889 QgsDebugError( u"Scene can not be exported to '%1'. File access error."_s.arg( mtlFilePath ) );
890 return false;
891 }
892
893 QVector3D center;
894 float scale;
895 getSceneCenterAndScale( center, scale );
896
897 QTextStream out( &file );
898 // set material library name
899 const QString mtlLibName = sceneName + ".mtl";
900 out << "mtllib " << mtlLibName << "\n";
901
902 QTextStream mtlOut( &mtlFile );
903 for ( Qgs3DExportObject *obj : std::as_const( mObjects ) )
904 {
905 if ( !obj )
906 continue;
907
908 const QString materialName = obj->saveMaterial( mtlOut, sceneFolderPath );
909 obj->saveTo( out, scale, center, Qgis::Export3DSceneFormat::Obj, precision, materialName );
910 }
911
912 QgsDebugMsgLevel( u"Scene exported to '%1'"_s.arg( objFilePath ), 2 );
913 return true;
914}
915
916bool Qgs3DSceneExporter::saveStl( QString sceneName, QString sceneFolderPath, int precision ) const
917{
918 const QString stlFilePath = QDir( sceneFolderPath ).filePath( sceneName + u".stl"_s );
919
920 QFile file( stlFilePath );
921 if ( !file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
922 {
923 QgsDebugError( u"Scene can not be exported to '%1'. File access error."_s.arg( stlFilePath ) );
924 return false;
925 }
926
927 QVector3D center;
928 float scale;
929 getSceneCenterAndScale( center, scale );
930
931 QTextStream out( &file );
932
933 for ( Qgs3DExportObject *object : std::as_const( mObjects ) )
934 {
935 if ( !object )
936 continue;
937
938 object->saveTo( out, scale, center, Qgis::Export3DSceneFormat::StlAscii, precision );
939 }
940
941 QgsDebugMsgLevel( u"Scene exported to '%1'"_s.arg( stlFilePath ), 2 );
942 return true;
943}
944
945void Qgs3DSceneExporter::getSceneCenterAndScale( QVector3D &center, float &scale ) const
946{
947 const float minFloat = std::numeric_limits<float>::lowest();
948 const float maxfloat = std::numeric_limits<float>::max();
949
950 float minX = maxfloat;
951 float minY = maxfloat;
952 float minZ = maxfloat;
953 float maxX = minFloat;
954 float maxY = minFloat;
955 float maxZ = minFloat;
956 for ( Qgs3DExportObject *obj : std::as_const( mObjects ) )
957 {
958 obj->objectBounds( minX, minY, minZ, maxX, maxY, maxZ );
959 }
960
961 const float diffX = maxX - minX;
962 const float diffY = maxY - minY;
963 const float diffZ = maxZ - minZ;
964
965 center.setX( ( minX + maxX ) / 2.0f );
966 center.setY( ( minY + maxY ) / 2.0f );
967 center.setZ( ( minZ + maxZ ) / 2.0f );
968
969 scale = std::max( diffX, std::max( diffY, diffZ ) ) / mScale;
970}
971
972QString Qgs3DSceneExporter::getObjectName( const QString &name )
973{
974 QString ret = name;
975 if ( mUsedObjectNamesCounter.contains( name ) )
976 {
977 ret = u"%1%2"_s.arg( name ).arg( mUsedObjectNamesCounter[name] );
978 mUsedObjectNamesCounter[name]++;
979 }
980 else
981 mUsedObjectNamesCounter[name] = 2;
982 return ret;
983}
Export3DSceneFormat
The file format used when exporting a 3D scene.
Definition qgis.h:4638
@ StlAscii
STL ascii format.
Definition qgis.h:4640
@ Obj
Wavefront OBJ format.
Definition qgis.h:4639
Manages the data of each object of the scene (positions, normals, texture coordinates ....
void setupPoint(const QVector< float > &positionsBuffer)
sets point positions coordinates
QString saveMaterial(QTextStream &mtlOut, const QString &folder) const
saves the texture of the object and material information
void objectBounds(float &minX, float &minY, float &minZ, float &maxX, float &maxY, float &maxZ) const
Updates the box bounds explained with the current object bounds This expands the bounding box if the ...
void setupLine(const QVector< float > &positionsBuffer)
sets line indexes and positions coordinates
void saveTo(QTextStream &out, float scale, const QVector3D &center, const Qgis::Export3DSceneFormat &exportFormat=Qgis::Export3DSceneFormat::Obj, int precision=6, QString materialName=QString()) const
Exports the 3D object to an OBJ or STL compatible output stream.
Definition of the world.
QgsTerrainGenerator * terrainGenerator() const
Returns the terrain generator.
bool terrainRenderingEnabled() const
Returns whether the 2D terrain surface will be rendered.
QgsVector3D origin() const
Returns coordinates in map CRS at which 3D scene has origin (0,0,0).
void parseTerrain(QgsTerrainEntity *terrain, const QString &layer)
Creates terrain export objects from the terrain entity.
float scale() const
Returns the scale of the exported 3D model.
bool parseVectorLayerEntity(Qt3DCore::QEntity *entity, QgsVectorLayer *layer)
Creates necessary export objects from entity if it represents valid vector layer entity Returns false...
bool save(QString sceneName, QString sceneFolderPath, const Qgis::Export3DSceneFormat &exportFormat=Qgis::Export3DSceneFormat::Obj, int precision=6) const
Saves the scene to a file Returns false if the operation failed.
static QgsPhongMaterialSettings phongMaterialFromQt3DComponent(Qt3DExtras::QPhongMaterial *material)
Returns phong material settings object based on the Qt3D material.
Base class for all renderers that participate in 3D views.
virtual QString type() const =0
Returns unique identifier of the renderer class (used to identify subclass).
Abstract base class for 3D symbols that are used by VectorLayer3DRenderer objects.
virtual bool exportGeometries(Qgs3DSceneExporter *exporter, Qt3DCore::QEntity *entity, const QString &objectNamePrefix) const
Exports the geometries contained within the hierarchy of entity.
QFuture< QgsChunkLoaderResult > loadChunk(QgsChunkNode *node) override
void setResolution(int resolution)
Sets resolution of the generator (how many elevation samples on one side of a terrain tile).
Terrain generator that creates a simple square flat area.
QFuture< QgsChunkLoaderResult > loadChunk(QgsChunkNode *node) override
static void waitForFinished(QFuture< void > future)
Waits for the future to become finished without blocking the main thread's event loop.
QImage getImage() const
Returns the image.
QString name
Definition qgsmaplayer.h:87
QgsAbstract3DRenderer * renderer3D() const
Returns 3D renderer associated with the layer.
Basic shading material used for rendering based on the Phong shading model with three color component...
Base class for generators of terrain.
@ QuantizedMesh
Terrain is built from quantized mesh tiles.
@ Dem
Terrain is built from raster layer with digital elevation model.
@ Online
Terrain is built from downloaded tiles with digital elevation model.
@ Mesh
Terrain is built from mesh layer with z value on vertices.
@ Flat
The whole terrain is flat area.
virtual Type type() const =0
What texture generator implementation is this.
QVector< QgsFeatureId > featureIds() const
Returns included feature ids.
QVector< uint > triangleIndexStartingIndices() const
Returns triangle index for features. For a feature featureIds()[i], matching triangles start at trian...
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
3D renderer that renders all features of a vector layer with the same 3D symbol.
const QgsAbstract3DSymbol * symbol() const
Returns 3D symbol associated with the renderer.
Represents a vector layer which manages a vector based dataset.
#define BUILTIN_UNREACHABLE
Definition qgis.h:8415
QVector< uint > getIndexData(Qt3DCore::QAttribute *indexAttribute, const QByteArray &data)
Qt3DCore::QAttribute * findAttribute(Qt3DCore::QGeometry *geometry, const QString &name, Qt3DCore::QAttribute::AttributeType type)
QVector< uint > _getIndexDataImplementation(const QByteArray &data)
Component * findTypedComponent(Qt3DCore::QEntity *entity)
QVector< T > getAttributeData(Qt3DCore::QAttribute *attribute, const QByteArray &data)
QByteArray getData(Qt3DCore::QBuffer *buffer)
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80
#define QgsDebugError(str)
Definition qgslogger.h:71