Compare commits

...

10 Commits

Author SHA1 Message Date
qgis-bot
1cf7fdf7cb
Merge 56598c47f4d4caf79a2aeb191a53dd52c01456bc into 4cab2daf50a0faa6c4570f212b3fefcff594ad4f 2025-10-01 07:32:55 +01:00
Nyall Dawson
4cab2daf50 Update src/gui/processing/qgsprocessingoutputdestinationwidget.cpp 2025-10-01 16:03:18 +10:00
Alexander Bruy
33cba935d2 change default folder for Processing outputs to $HOME/processing to make
it more visible and accessible
2025-10-01 16:03:18 +10:00
Alexander Bruy
62830e0412 provide default value when reading output folder setting to generate
destination path in Processing (fix #61965)
2025-10-01 16:03:18 +10:00
Jean Felder
56598c47f4 testqgs3dexporter: Remove useless comments 2025-09-30 05:44:15 +00:00
Jean Felder
9ed2c4e71d testqgs3dexporter: Activate test3DSceneExporterBig on CI 2025-09-30 05:44:15 +00:00
Jean Felder
df70848176 testqgs3dexporter: Add an export test on a scene with a flat terrain 2025-09-30 05:44:15 +00:00
Jean Felder
af2127db90 testqgs3dexporter: Remove unused terrain_scene_export test
This test was never activated because the obj file was too big.
2025-09-30 05:44:15 +00:00
Jean Felder
14b74f0eb7 tests(3d): Factor out the exporter tests
Move it to a dedicated file.
2025-09-30 05:44:14 +00:00
Jean Felder
857626fb2e qgs3dsceneexporter: Fix terrain export
In the different terrain cases, the generated tiles have an origin at
0 unlike other objects which have their origin correctly set. Fix the
issue by setting the correct origin. This is the same logic that is
applied in `Qgs3DMapScene::createTerrainDeferred` when a new terrain
entity is created.

Fixes: b0e038559dc8627ba5d8bfe0bd419634f9cfc3af
2025-09-30 05:44:14 +00:00
8 changed files with 46856 additions and 203 deletions

View File

@ -41,7 +41,7 @@ def userFolder():
def defaultOutputFolder():
folder = os.path.join(userFolder(), "outputs")
folder = os.path.join(QDir.homePath(), "processing")
if not QDir(folder).exists():
QDir().mkpath(folder)

View File

@ -83,6 +83,7 @@ typedef Qt3DCore::QGeometry Qt3DQGeometry;
#include "qgs3dutils.h"
#include "qgsimagetexture.h"
#include "qgstessellatedpolygongeometry.h"
#include "qgsgeotransform.h"
#include <numeric>
@ -285,15 +286,15 @@ void Qgs3DSceneExporter::parseTerrain( QgsTerrainEntity *terrain, const QString
switch ( generator->type() )
{
case QgsTerrainGenerator::Dem:
terrainTile = getDemTerrainEntity( terrain, node );
terrainTile = getDemTerrainEntity( terrain, node, settings->origin() );
parseDemTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
case QgsTerrainGenerator::Flat:
terrainTile = getFlatTerrainEntity( terrain, node );
terrainTile = getFlatTerrainEntity( terrain, node, settings->origin() );
parseFlatTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
case QgsTerrainGenerator::Mesh:
terrainTile = getMeshTerrainEntity( terrain, node );
terrainTile = getMeshTerrainEntity( terrain, node, settings->origin() );
parseMeshTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
// TODO: implement other terrain types
@ -304,7 +305,7 @@ void Qgs3DSceneExporter::parseTerrain( QgsTerrainEntity *terrain, const QString
textureGenerator->setTextureSize( oldResolution );
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
QgsFlatTerrainGenerator *generator = dynamic_cast<QgsFlatTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
FlatTerrainChunkLoader *flatTerrainLoader = qobject_cast<FlatTerrainChunkLoader *>( generator->createChunkLoader( node ) );
@ -313,10 +314,17 @@ QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity
// the entity we created will be deallocated once the scene exporter is deallocated
Qt3DCore::QEntity *entity = flatTerrainLoader->createEntity( this );
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( entity );
const QList<QgsGeoTransform *> transforms = entity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
return tileEntity;
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
// Just create a new tile (we don't need to export exact level of details as in the scene)
// create the entity synchronously and then it will be deleted once our scene exporter instance is deallocated
@ -327,16 +335,30 @@ QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity
if ( mExportTextures )
terrain->textureGenerator()->waitForFinished();
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( loader->createEntity( this ) );
const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
delete generator;
return tileEntity;
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
QgsMeshTerrainGenerator *generator = dynamic_cast<QgsMeshTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
QgsMeshTerrainTileLoader *loader = qobject_cast<QgsMeshTerrainTileLoader *>( generator->createChunkLoader( node ) );
// TODO: export textures
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( loader->createEntity( this ) );
const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
return tileEntity;
}

View File

@ -37,12 +37,13 @@ class QgsDemTerrainGenerator;
class QgsChunkNode;
class Qgs3DExportObject;
class QgsTerrainTextureGenerator;
class QgsVector3D;
class QgsVectorLayer;
class QgsPolygon3DSymbol;
class QgsLine3DSymbol;
class QgsPoint3DSymbol;
class QgsMeshEntity;
class TestQgs3DRendering;
class TestQgs3DExporter;
#define SIP_NO_FILE
@ -126,11 +127,11 @@ class _3D_EXPORT Qgs3DSceneExporter : public Qt3DCore::QEntity
Qgs3DExportObject *processPoints( Qt3DCore::QEntity *entity, const QString &objectNamePrefix );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Constructs a Qgs3DExportObject from the DEM tile entity
void parseDemTile( QgsTerrainTileEntity *tileEntity, const QString &layerName );
@ -157,7 +158,7 @@ class _3D_EXPORT Qgs3DSceneExporter : public Qt3DCore::QEntity
friend QgsPolygon3DSymbol;
friend QgsLine3DSymbol;
friend QgsPoint3DSymbol;
friend TestQgs3DRendering;
friend TestQgs3DExporter;
};
#endif // QGS3DSCENEEXPORTER_H

View File

@ -25,6 +25,7 @@
#include "qgsprocessingcontext.h"
#include "qgsprocessingalgorithm.h"
#include "qgsfieldmappingwidget.h"
#include "qgsapplication.h"
#include <QMenu>
#include <QFileDialog>
#include <QInputDialog>
@ -175,7 +176,7 @@ QVariant QgsProcessingLayerOutputDestinationWidget::value() const
if ( folder == '.' )
{
// output name does not include a folder - use default
QString defaultFolder = settings.value( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ) ).toString();
QString defaultFolder = settings.value( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), QStringLiteral( "%1/processing" ).arg( QDir::homePath() ) ).toString();
key = QDir( defaultFolder ).filePath( key );
}
}

View File

@ -21,6 +21,7 @@ add_subdirectory(sandbox)
set(TESTS
testqgs3dcameracontroller.cpp
testqgs3dexporter.cpp
testqgs3dmapscene.cpp
testqgs3dmaterial.cpp
testqgs3drendering.cpp

View File

@ -0,0 +1,302 @@
/***************************************************************************
testqgs3dexporter.cpp
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Jean Felder
Email : jean dot felder at oslandia 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 "qgstest.h"
#include "qgsrasterlayer.h"
#include "qgsvectorlayer.h"
#include "qgs3d.h"
#include "qgs3dmapscene.h"
#include "qgs3dmapsettings.h"
#include "qgs3drendercontext.h"
#include "qgs3dsceneexporter.h"
#include "qgs3dutils.h"
#include "qgsdemterrainsettings.h"
#include "qgsflatterraingenerator.h"
#include "qgsflatterrainsettings.h"
#include "qgsoffscreen3dengine.h"
#include "qgspointlightsettings.h"
#include "qgspolygon3dsymbol.h"
#include "qgsvectorlayer3drenderer.h"
class TestQgs3DExporter : public QgsTest
{
Q_OBJECT
public:
TestQgs3DExporter()
: QgsTest( QStringLiteral( "3D Exporter Tests" ), QStringLiteral( "3d" ) )
{}
private slots:
void initTestCase(); // will be called before the first testfunction is executed.
void cleanupTestCase(); // will be called after the last testfunction was executed.
void test3DSceneExporter();
void test3DSceneExporterBig();
void test3DSceneExporterFlatTerrain();
private:
void do3DSceneExport( const QString &testName, int zoomLevelsCount, int expectedObjectCount, int expectedFeatureCount, int maxFaceCount, Qgs3DMapScene *scene, QgsVectorLayer *layerPoly, QgsOffscreen3DEngine *engine, QgsTerrainEntity *terrainEntity = nullptr );
QgsVectorLayer *mLayerBuildings = nullptr;
};
// runs before all tests
void TestQgs3DExporter::initTestCase()
{
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();
Qgs3D::initialize();
mLayerBuildings = new QgsVectorLayer( testDataPath( "/3d/buildings.shp" ), "buildings", "ogr" );
QVERIFY( mLayerBuildings->isValid() );
// best to keep buildings without 2D renderer so it is not painted on the terrain
// so we do not get some possible artifacts
mLayerBuildings->setRenderer( nullptr );
QgsPhongMaterialSettings materialSettings;
materialSettings.setAmbient( Qt::lightGray );
QgsPolygon3DSymbol *symbol3d = new QgsPolygon3DSymbol;
symbol3d->setMaterialSettings( materialSettings.clone() );
symbol3d->setExtrusionHeight( 10.f );
symbol3d->setAltitudeClamping( Qgis::AltitudeClamping::Relative );
QgsVectorLayer3DRenderer *renderer3d = new QgsVectorLayer3DRenderer( symbol3d );
mLayerBuildings->setRenderer3D( renderer3d );
}
//runs after all tests
void TestQgs3DExporter::cleanupTestCase()
{
QgsApplication::exitQgis();
}
void TestQgs3DExporter::do3DSceneExport( const QString &testName, int zoomLevelsCount, int expectedObjectCount, int expectedFeatureCount, int maxFaceCount, Qgs3DMapScene *scene, QgsVectorLayer *layerPoly, QgsOffscreen3DEngine *engine, QgsTerrainEntity *terrainEntity )
{
// 3d renderer must be replaced to have the tiling updated
QgsVectorLayer3DRenderer *renderer3d = dynamic_cast<QgsVectorLayer3DRenderer *>( layerPoly->renderer3D() );
QgsVectorLayer3DRenderer *newRenderer3d = new QgsVectorLayer3DRenderer( renderer3d->symbol()->clone() );
QgsVectorLayer3DTilingSettings tilingSettings;
tilingSettings.setZoomLevelsCount( zoomLevelsCount );
tilingSettings.setShowBoundingBoxes( true );
newRenderer3d->setTilingSettings( tilingSettings );
layerPoly->setRenderer3D( newRenderer3d );
Qgs3DUtils::captureSceneImage( *engine, scene );
Qgs3DSceneExporter exporter;
exporter.setTerrainResolution( 128 );
exporter.setSmoothEdges( false );
exporter.setExportNormals( true );
exporter.setExportTextures( false );
exporter.setTerrainTextureResolution( 512 );
exporter.setScale( 1.0 );
QVERIFY( exporter.parseVectorLayerEntity( scene->layerEntity( layerPoly ), layerPoly ) );
if ( terrainEntity )
exporter.parseTerrain( terrainEntity, "DEM_Tile" );
QString objFileName = QString( "%1-%2" ).arg( testName ).arg( zoomLevelsCount );
const bool saved = exporter.save( objFileName, QDir::tempPath(), 3 );
QVERIFY( saved );
size_t sum = 0;
for ( auto o : qAsConst( exporter.mObjects ) )
{
if ( !terrainEntity ) // not compatible with terrain entity
QVERIFY( o->indexes().size() * 3 <= o->vertexPosition().size() );
sum += o->indexes().size();
}
QCOMPARE( sum, maxFaceCount );
QCOMPARE( exporter.mExportedFeatureIds.size(), expectedFeatureCount );
QCOMPARE( exporter.mObjects.size(), expectedObjectCount );
QFile file( QString( "%1/%2.obj" ).arg( QDir::tempPath(), objFileName ) );
file.open( QIODevice::ReadOnly | QIODevice::Text );
QTextStream fileStream( &file );
// check the generated obj file
QGSCOMPARELONGSTR( testName.toStdString().c_str(), QString( "%1.obj" ).arg( objFileName ), fileStream.readAll().toUtf8() );
}
void TestQgs3DExporter::test3DSceneExporter()
{
QgsVectorLayer *layerPoly = new QgsVectorLayer( testDataPath( "/3d/polygons.gpkg.gz" ), "polygons", "ogr" );
QVERIFY( layerPoly->isValid() );
const QgsRectangle fullExtent = layerPoly->extent();
QgsPolygon3DSymbol *symbol3d = new QgsPolygon3DSymbol();
symbol3d->setExtrusionHeight( 10.f );
QgsPhongMaterialSettings materialSettings;
materialSettings.setAmbient( Qt::lightGray );
symbol3d->setMaterialSettings( materialSettings.clone() );
QgsVectorLayer3DRenderer *renderer3d = new QgsVectorLayer3DRenderer( symbol3d );
layerPoly->setRenderer3D( renderer3d );
QgsProject project;
project.setCrs( QgsCoordinateReferenceSystem::fromEpsgId( 3857 ) );
project.addMapLayer( layerPoly );
Qgs3DMapSettings mapSettings;
mapSettings.setCrs( project.crs() );
mapSettings.setExtent( fullExtent );
mapSettings.setLayers( { layerPoly } );
mapSettings.setTransformContext( project.transformContext() );
mapSettings.setPathResolver( project.pathResolver() );
mapSettings.setMapThemeCollection( project.mapThemeCollection() );
mapSettings.setOutputDpi( 92 );
QPoint winSize = QPoint( 640, 480 ); // default window size
QgsOffscreen3DEngine engine;
engine.setSize( QSize( winSize.x(), winSize.y() ) );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 7000, 20.0, -10.0 );
engine.setRootEntity( scene );
const int nbFaces = 165;
const int nbFeat = 3;
// =========== check with 1 big tile ==> 1 exported object
do3DSceneExport( "scene_export", 1, 1, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 4 tiles ==> 1 exported objects
do3DSceneExport( "scene_export", 2, 1, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 9 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 3, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 16 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 4, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 25 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 5, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
delete scene;
mapSettings.setLayers( {} );
}
void TestQgs3DExporter::test3DSceneExporterBig()
{
QgsRasterLayer *layerDtm = new QgsRasterLayer( testDataPath( "/3d/dtm.tif" ), "dtm", "gdal" );
QVERIFY( layerDtm->isValid() );
const QgsRectangle fullExtent = layerDtm->extent();
QgsProject project;
project.setCrs( layerDtm->crs() );
project.addMapLayer( layerDtm );
Qgs3DMapSettings mapSettings;
mapSettings.setCrs( project.crs() );
mapSettings.setExtent( fullExtent );
mapSettings.setLayers( { layerDtm, mLayerBuildings } );
mapSettings.setTransformContext( project.transformContext() );
mapSettings.setPathResolver( project.pathResolver() );
mapSettings.setMapThemeCollection( project.mapThemeCollection() );
QgsDemTerrainSettings *demTerrainSettings = new QgsDemTerrainSettings;
demTerrainSettings->setLayer( layerDtm );
demTerrainSettings->setVerticalScale( 3 );
mapSettings.setTerrainSettings( demTerrainSettings );
QgsPointLightSettings defaultPointLight;
defaultPointLight.setPosition( QgsVector3D( 0, 400, 0 ) );
defaultPointLight.setConstantAttenuation( 0 );
mapSettings.setLightSources( { defaultPointLight.clone() } );
mapSettings.setOutputDpi( 92 );
QPoint winSize = QPoint( 640, 480 ); // default window size
QgsOffscreen3DEngine engine;
engine.setSize( QSize( winSize.x(), winSize.y() ) );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
engine.setRootEntity( scene );
scene->cameraController()->setLookingAtPoint( QVector3D( 0, 0, 0 ), 1500, 40.0, -10.0 );
const int nbFaces = 19869;
const int nbFeat = 401;
// =========== check with 1 big tile ==> 1 exported object
do3DSceneExport( "big_scene_export", 1, 1, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 4 tiles ==> 4 exported objects
do3DSceneExport( "big_scene_export", 2, 4, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 9 tiles ==> 14 exported objects
do3DSceneExport( "big_scene_export", 3, 14, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 16 tiles ==> 32 exported objects
do3DSceneExport( "big_scene_export", 4, 32, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 25 tiles ==> 70 exported objects
do3DSceneExport( "big_scene_export", 5, 70, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
delete scene;
mapSettings.setLayers( {} );
}
void TestQgs3DExporter::test3DSceneExporterFlatTerrain()
{
QgsRasterLayer *layerRgb = new QgsRasterLayer( testDataPath( "/3d/rgb.tif" ), "rgb", "gdal" );
QVERIFY( layerRgb->isValid() );
const QgsRectangle fullExtent = layerRgb->extent();
QgsProject project;
project.setCrs( layerRgb->crs() );
project.addMapLayer( layerRgb );
Qgs3DMapSettings mapSettings;
mapSettings.setCrs( project.crs() );
mapSettings.setExtent( fullExtent );
mapSettings.setLayers( { layerRgb, mLayerBuildings } );
mapSettings.setTransformContext( project.transformContext() );
mapSettings.setPathResolver( project.pathResolver() );
mapSettings.setMapThemeCollection( project.mapThemeCollection() );
QgsFlatTerrainSettings *flatTerrainSettings = new QgsFlatTerrainSettings;
mapSettings.setTerrainSettings( flatTerrainSettings );
std::unique_ptr<QgsTerrainGenerator> generator = flatTerrainSettings->createTerrainGenerator( Qgs3DRenderContext::fromMapSettings( &mapSettings ) );
QVERIFY( dynamic_cast<QgsFlatTerrainGenerator *>( generator.get() )->isValid() );
QCOMPARE( dynamic_cast<QgsFlatTerrainGenerator *>( generator.get() )->crs(), mapSettings.crs() );
QgsPointLightSettings defaultPointLight;
defaultPointLight.setPosition( QgsVector3D( 0, 400, 0 ) );
defaultPointLight.setConstantAttenuation( 0 );
mapSettings.setLightSources( { defaultPointLight.clone() } );
mapSettings.setOutputDpi( 92 );
QPoint winSize = QPoint( 640, 480 ); // default window size
QgsOffscreen3DEngine engine;
engine.setSize( QSize( winSize.x(), winSize.y() ) );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
engine.setRootEntity( scene );
scene->cameraController()->setLookingAtPoint( QVector3D( 0, 0, 0 ), 1500, 40.0, -10.0 );
do3DSceneExport( "flat_terrain_scene_export", 5, 70, 401, 19875, scene, mLayerBuildings, &engine, scene->terrainEntity() );
delete scene;
mapSettings.setLayers( {} );
}
QGSTEST_MAIN( TestQgs3DExporter )
#include "testqgs3dexporter.moc"

View File

@ -50,7 +50,6 @@
#include "qgsfillsymbol.h"
#include "qgsmarkersymbol.h"
#include "qgsgoochmaterialsettings.h"
#include "qgs3dsceneexporter.h"
#include "qgsdirectionallightsettings.h"
#include "qgsmetalroughmaterialsettings.h"
#include "qgspointlightsettings.h"
@ -109,14 +108,10 @@ class TestQgs3DRendering : public QgsTest
void testDepthBuffer();
void testAmbientOcclusion();
void testDebugMap();
void test3DSceneExporter();
void test3DSceneExporterBig();
private:
QImage convertDepthImageToGrayscaleImage( const QImage &depthImage );
void do3DSceneExport( const QString &testName, int zoomLevelsCount, int expectedObjectCount, int expectedFeatureCount, int maxFaceCount, Qgs3DMapScene *scene, QgsVectorLayer *layerPoly, QgsOffscreen3DEngine *engine, QgsTerrainEntity *terrainEntity = nullptr );
std::unique_ptr<QgsProject> mProject;
QgsRasterLayer *mLayerDtm = nullptr;
QgsRasterLayer *mLayerRgb = nullptr;
@ -2392,190 +2387,5 @@ void TestQgs3DRendering::testDebugMap()
mapSettings.setLayers( {} );
}
void TestQgs3DRendering::do3DSceneExport( const QString &testName, int zoomLevelsCount, int expectedObjectCount, int expectedFeatureCount, int maxFaceCount, Qgs3DMapScene *scene, QgsVectorLayer *layerPoly, QgsOffscreen3DEngine *engine, QgsTerrainEntity *terrainEntity )
{
// 3d renderer must be replaced to have the tiling updated
QgsVectorLayer3DRenderer *renderer3d = dynamic_cast<QgsVectorLayer3DRenderer *>( layerPoly->renderer3D() );
QgsVectorLayer3DRenderer *newRenderer3d = new QgsVectorLayer3DRenderer( renderer3d->symbol()->clone() );
QgsVectorLayer3DTilingSettings tilingSettings;
tilingSettings.setZoomLevelsCount( zoomLevelsCount );
tilingSettings.setShowBoundingBoxes( true );
newRenderer3d->setTilingSettings( tilingSettings );
layerPoly->setRenderer3D( newRenderer3d );
Qgs3DUtils::captureSceneImage( *engine, scene );
Qgs3DSceneExporter exporter;
exporter.setTerrainResolution( 128 );
exporter.setSmoothEdges( false );
exporter.setExportNormals( true );
exporter.setExportTextures( false );
exporter.setTerrainTextureResolution( 512 );
exporter.setScale( 1.0 );
QVERIFY( exporter.parseVectorLayerEntity( scene->layerEntity( layerPoly ), layerPoly ) );
if ( terrainEntity )
exporter.parseTerrain( terrainEntity, "DEM_Tile" );
QString objFileName = QString( "%1-%2" ).arg( testName ).arg( zoomLevelsCount );
const bool saved = exporter.save( objFileName, QDir::tempPath(), 3 );
QVERIFY( saved );
int sum = 0;
for ( auto o : qAsConst( exporter.mObjects ) )
{
if ( !terrainEntity ) // not comptabible with terrain entity
QVERIFY( o->indexes().size() * 3 <= o->vertexPosition().size() );
sum += o->indexes().size();
}
QCOMPARE( sum, maxFaceCount );
QCOMPARE( exporter.mExportedFeatureIds.size(), expectedFeatureCount );
QCOMPARE( exporter.mObjects.size(), expectedObjectCount );
QFile file( QString( "%1/%2.obj" ).arg( QDir::tempPath(), objFileName ) );
file.open( QIODevice::ReadOnly | QIODevice::Text );
QTextStream fileStream( &file );
if ( !terrainEntity ) // dump with terrain are too big to stay in GIT
QGSCOMPARELONGSTR( testName.toStdString().c_str(), QString( "%1.obj" ).arg( objFileName ), fileStream.readAll().toUtf8() );
}
void TestQgs3DRendering::test3DSceneExporter()
{
// =============================================
// =========== creating Qgs3DMapSettings
QgsVectorLayer *layerPoly = new QgsVectorLayer( testDataPath( "/3d/polygons.gpkg.gz" ), "polygons", "ogr" );
QVERIFY( layerPoly->isValid() );
const QgsRectangle fullExtent = layerPoly->extent();
// =========== create polygon 3D renderer
QgsPolygon3DSymbol *symbol3d = new QgsPolygon3DSymbol();
symbol3d->setExtrusionHeight( 10.f );
QgsPhongMaterialSettings materialSettings;
materialSettings.setAmbient( Qt::lightGray );
symbol3d->setMaterialSettings( materialSettings.clone() );
QgsVectorLayer3DRenderer *renderer3d = new QgsVectorLayer3DRenderer( symbol3d );
layerPoly->setRenderer3D( renderer3d );
QgsProject project;
project.setCrs( QgsCoordinateReferenceSystem::fromEpsgId( 3857 ) );
project.addMapLayer( layerPoly );
// =========== create scene 3D settings
Qgs3DMapSettings mapSettings;
mapSettings.setCrs( project.crs() );
mapSettings.setExtent( fullExtent );
mapSettings.setLayers( { layerPoly } );
mapSettings.setTransformContext( project.transformContext() );
mapSettings.setPathResolver( project.pathResolver() );
mapSettings.setMapThemeCollection( project.mapThemeCollection() );
mapSettings.setOutputDpi( 92 );
// =========== creating Qgs3DMapScene
QPoint winSize = QPoint( 640, 480 ); // default window size
QgsOffscreen3DEngine engine;
engine.setSize( QSize( winSize.x(), winSize.y() ) );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 7000, 20.0, -10.0 );
engine.setRootEntity( scene );
const int nbFaces = 165;
const int nbFeat = 3;
// =========== check with 1 big tile ==> 1 exported object
do3DSceneExport( "scene_export", 1, 1, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 4 tiles ==> 1 exported objects
do3DSceneExport( "scene_export", 2, 1, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 9 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 3, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 16 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 4, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
// =========== check with 25 tiles ==> 3 exported objects
do3DSceneExport( "scene_export", 5, 3, nbFeat, nbFaces, scene, layerPoly, &engine );
delete scene;
mapSettings.setLayers( {} );
}
void TestQgs3DRendering::test3DSceneExporterBig()
{
// In Qt 6, this test does not work on CI
// because somewhere after 10K lines, one of the values is -0.304 instead of -0.305
#if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 )
if ( QgsTest::isCIRun() )
{
QSKIP( "fails on CI" );
}
#endif
// =============================================
// =========== creating Qgs3DMapSettings
QgsRasterLayer *layerDtm = new QgsRasterLayer( testDataPath( "/3d/dtm.tif" ), "dtm", "gdal" );
QVERIFY( layerDtm->isValid() );
const QgsRectangle fullExtent = layerDtm->extent();
QgsProject project;
project.setCrs( layerDtm->crs() );
project.addMapLayer( layerDtm );
Qgs3DMapSettings mapSettings;
mapSettings.setCrs( project.crs() );
mapSettings.setExtent( fullExtent );
mapSettings.setLayers( { layerDtm, mLayerBuildings } );
mapSettings.setTransformContext( project.transformContext() );
mapSettings.setPathResolver( project.pathResolver() );
mapSettings.setMapThemeCollection( project.mapThemeCollection() );
QgsDemTerrainSettings *demTerrainSettings = new QgsDemTerrainSettings;
demTerrainSettings->setLayer( layerDtm );
demTerrainSettings->setVerticalScale( 3 );
mapSettings.setTerrainSettings( demTerrainSettings );
QgsPointLightSettings defaultPointLight;
defaultPointLight.setPosition( QgsVector3D( 0, 400, 0 ) );
defaultPointLight.setConstantAttenuation( 0 );
mapSettings.setLightSources( { defaultPointLight.clone() } );
mapSettings.setOutputDpi( 92 );
// =========== creating Qgs3DMapScene
QPoint winSize = QPoint( 640, 480 ); // default window size
QgsOffscreen3DEngine engine;
engine.setSize( QSize( winSize.x(), winSize.y() ) );
Qgs3DMapScene *scene = new Qgs3DMapScene( mapSettings, &engine );
engine.setRootEntity( scene );
// =========== set camera position
scene->cameraController()->setLookingAtPoint( QVector3D( 0, 0, 0 ), 1500, 40.0, -10.0 );
const int nbFaces = 19869;
const int nbFeat = 401;
// =========== check with 1 big tile ==> 1 exported object
do3DSceneExport( "big_scene_export", 1, 1, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 4 tiles ==> 4 exported objects
do3DSceneExport( "big_scene_export", 2, 4, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 9 tiles ==> 14 exported objects
do3DSceneExport( "big_scene_export", 3, 14, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 16 tiles ==> 32 exported objects
do3DSceneExport( "big_scene_export", 4, 32, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 25 tiles ==> 70 exported objects
do3DSceneExport( "big_scene_export", 5, 70, nbFeat, nbFaces, scene, mLayerBuildings, &engine );
// =========== check with 25 tiles + terrain ==> 70+1 exported objects
do3DSceneExport( "terrain_scene_export", 5, 71, nbFeat, 119715, scene, mLayerBuildings, &engine, scene->terrainEntity() );
delete scene;
mapSettings.setLayers( {} );
}
QGSTEST_MAIN( TestQgs3DRendering )
#include "testqgs3drendering.moc"