Skip to content
Permalink
Browse files

Add auto-tests for 3D scene rendering using the off-screen 3D engine

  • Loading branch information
wonder-sk committed Jul 25, 2018
1 parent df394c7 commit 30d68125375527f5ef0e16ded86a64d8c1b63b27
Showing with 240 additions and 4 deletions.
  1. +26 −4 src/3d/qgs3dmapscene.cpp
  2. +2 −0 src/3d/qgs3dmapscene.h
  3. +3 −0 tests/src/3d/CMakeLists.txt
  4. +209 −0 tests/src/3d/testqgs3drendering.cpp
@@ -190,6 +190,20 @@ QgsChunkedEntity::SceneState _sceneState( QgsCameraController *cameraController
}

void Qgs3DMapScene::onCameraChanged()
{
updateScene();
bool changedCameraPlanes = updateCameraNearFarPlanes();

if ( changedCameraPlanes )
{
// repeat update of entities - because we have updated camera's near/far planes,
// the active nodes may have changed as well
updateScene();
updateCameraNearFarPlanes();
}
}

void Qgs3DMapScene::updateScene()
{
for ( QgsChunkedEntity *entity : qgis::as_const( mChunkEntities ) )
{
@@ -198,7 +212,10 @@ void Qgs3DMapScene::onCameraChanged()
}

updateSceneState();
}

bool Qgs3DMapScene::updateCameraNearFarPlanes()
{
// Update near and far plane from the terrain.
// this needs to be done with great care as we have kind of circular dependency here:
// active nodes are culled based on the current frustum (which involves near + far plane)
@@ -258,14 +275,19 @@ void Qgs3DMapScene::onCameraChanged()
}

// set near/far plane - with some tolerance in front/behind expected near/far planes
camera->setFarPlane( ffar * 2 );
camera->setNearPlane( fnear / 2 );

float newFar = ffar * 2;
float newNear = fnear / 2;
if ( !qgsFloatNear( newFar, camera->farPlane() ) || !qgsFloatNear( newNear, camera->nearPlane() ) )
{
camera->setFarPlane( newFar );
camera->setNearPlane( newNear );
return true;
}
}
else
qDebug() << "no terrain - not setting near/far plane";

//qDebug() << "camera near/far" << mCameraController->camera()->nearPlane() << mCameraController->camera()->farPlane();
return false;
}

void Qgs3DMapScene::onFrameTriggered( float dt )
@@ -99,6 +99,8 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
void addCameraViewCenterEntity( Qt3DRender::QCamera *camera );
void setSceneState( SceneState state );
void updateSceneState();
void updateScene();
bool updateCameraNearFarPlanes();

private:
const Qgs3DMapSettings &mMap;
@@ -9,8 +9,10 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/tests/core #for render checker class
${CMAKE_SOURCE_DIR}/src/3d
${CMAKE_SOURCE_DIR}/src/3d/chunks
${CMAKE_SOURCE_DIR}/src/3d/symbols
${CMAKE_SOURCE_DIR}/src/3d/terrain
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/3d
${CMAKE_SOURCE_DIR}/src/core/expression
${CMAKE_SOURCE_DIR}/src/core/auth
${CMAKE_SOURCE_DIR}/src/core/geometry
@@ -68,4 +70,5 @@ MACRO (ADD_QGIS_TEST testname testsrc)
ENDMACRO (ADD_QGIS_TEST)

ADD_QGIS_TEST(3dutilstest testqgs3dutils.cpp)
ADD_QGIS_TEST(3drenderingtest testqgs3drendering.cpp)
ADD_QGIS_TEST(tessellatortest testqgstessellator.cpp)
@@ -0,0 +1,209 @@
/***************************************************************************
testqgs3drendering.cpp
--------------------------------------
Date : July 2018
Copyright : (C) 2018 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgstest.h"
#include "qgsrenderchecker.h"

#include "qgsproject.h"
#include "qgsrasterlayer.h"
#include "qgsvectorlayer.h"

#include "qgs3dmapscene.h"
#include "qgs3dmapsettings.h"
#include "qgs3dutils.h"
#include "qgscameracontroller.h"
#include "qgschunknode_p.h"
#include "qgsdemterraingenerator.h"
#include "qgsflatterraingenerator.h"
#include "qgsoffscreen3dengine.h"
#include "qgspolygon3dsymbol.h"
#include "qgsterrainentity_p.h"
#include "qgsvectorlayer3drenderer.h"


class TestQgs3DRendering : public QObject
{
Q_OBJECT

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 testFlatTerrain();
void testDemTerrain();
void testExtrudedPolygons();

private:
bool renderCheck( const QString &testName, QImage &image, int mismatchCount = 0 );

QString mReport;

std::unique_ptr<QgsProject> mProject;
QgsRasterLayer *mLayerDtm;
QgsRasterLayer *mLayerRgb;
QgsVectorLayer *mLayerBuildings;
};

//runs before all tests
void TestQgs3DRendering::initTestCase()
{
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();

mReport = QStringLiteral( "<h1>3D Rendering Tests</h1>\n" );

mProject.reset( new QgsProject );

QString dataDir( TEST_DATA_DIR );
mLayerDtm = new QgsRasterLayer( dataDir + "/3d/dtm.tif", "dtm", "gdal" );
QVERIFY( mLayerDtm->isValid() );
mProject->addMapLayer( mLayerDtm );

mLayerRgb = new QgsRasterLayer( dataDir + "/3d/rgb.tif", "rgb", "gdal" );
QVERIFY( mLayerRgb->isValid() );
mProject->addMapLayer( mLayerRgb );

mLayerBuildings = new QgsVectorLayer( dataDir + "/3d/buildings.shp", "buildings", "ogr" );
QVERIFY( mLayerBuildings->isValid() );
mProject->addMapLayer( mLayerBuildings );

QgsPhongMaterialSettings material;
material.setAmbient( Qt::lightGray );
QgsPolygon3DSymbol *symbol3d = new QgsPolygon3DSymbol;
symbol3d->setMaterial( material );
symbol3d->setExtrusionHeight( 10.f );
QgsVectorLayer3DRenderer *renderer3d = new QgsVectorLayer3DRenderer( symbol3d );
mLayerBuildings->setRenderer3D( renderer3d );

mProject->setCrs( mLayerDtm->crs() );
}

//runs after all tests
void TestQgs3DRendering::cleanupTestCase()
{
mProject.reset();

QString myReportFile = QDir::tempPath() + "/qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
}

QgsApplication::exitQgis();
}

void TestQgs3DRendering::testFlatTerrain()
{
QgsRectangle fullExtent = mLayerDtm->extent();

Qgs3DMapSettings *map = new Qgs3DMapSettings;
map->setCrs( mProject->crs() );
map->setOrigin( QgsVector3D( fullExtent.center().x(), fullExtent.center().y(), 0 ) );
map->setLayers( QList<QgsMapLayer *>() << mLayerRgb );

QgsFlatTerrainGenerator *flatTerrain = new QgsFlatTerrainGenerator;
flatTerrain->setCrs( map->crs() );
flatTerrain->setExtent( fullExtent );
map->setTerrainGenerator( flatTerrain );

QgsOffscreen3DEngine engine;
Qgs3DMapScene *scene = new Qgs3DMapScene( *map, &engine );
engine.setRootEntity( scene );

// look from the top
scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 0, 0 );
QImage img = Qgs3DUtils::captureSceneImage( engine, scene );
QVERIFY( renderCheck( "flat_terrain_1", img, 40 ) );

// tilted view (pitch = 60 degrees)
scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 60, 0 );
QImage img2 = Qgs3DUtils::captureSceneImage( engine, scene );
QVERIFY( renderCheck( "flat_terrain_2", img2, 40 ) );

// also add horizontal rotation (yaw = 45 degrees)
scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2500, 60, 45 );
QImage img3 = Qgs3DUtils::captureSceneImage( engine, scene );
QVERIFY( renderCheck( "flat_terrain_3", img3, 40 ) );
}

void TestQgs3DRendering::testDemTerrain()
{
QgsRectangle fullExtent = mLayerDtm->extent();

Qgs3DMapSettings *map = new Qgs3DMapSettings;
map->setCrs( mProject->crs() );
map->setOrigin( QgsVector3D( fullExtent.center().x(), fullExtent.center().y(), 0 ) );
map->setLayers( QList<QgsMapLayer *>() << mLayerRgb );

QgsDemTerrainGenerator *demTerrain = new QgsDemTerrainGenerator;
demTerrain->setLayer( mLayerDtm );
map->setTerrainGenerator( demTerrain );
map->setTerrainVerticalScale( 3 );

QgsOffscreen3DEngine engine;
Qgs3DMapScene *scene = new Qgs3DMapScene( *map, &engine );
engine.setRootEntity( scene );

scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 0 ), 2000, 60, 0 );
QImage img3 = Qgs3DUtils::captureSceneImage( engine, scene );

QVERIFY( renderCheck( "dem_terrain_1", img3, 40 ) );
}

void TestQgs3DRendering::testExtrudedPolygons()
{
QgsRectangle fullExtent = mLayerDtm->extent();

Qgs3DMapSettings *map = new Qgs3DMapSettings;
map->setCrs( mProject->crs() );
map->setOrigin( QgsVector3D( fullExtent.center().x(), fullExtent.center().y(), 0 ) );
map->setLayers( QList<QgsMapLayer *>() << mLayerRgb << mLayerBuildings );

QgsFlatTerrainGenerator *flatTerrain = new QgsFlatTerrainGenerator;
flatTerrain->setCrs( map->crs() );
flatTerrain->setExtent( fullExtent );
map->setTerrainGenerator( flatTerrain );

QgsOffscreen3DEngine engine;
Qgs3DMapScene *scene = new Qgs3DMapScene( *map, &engine );
engine.setRootEntity( scene );

scene->cameraController()->setLookingAtPoint( QgsVector3D( 0, 0, 250 ), 500, 45, 0 );
QImage img = Qgs3DUtils::captureSceneImage( engine, scene );

QVERIFY( renderCheck( "polygon3d_extrusion", img, 40 ) );
}


bool TestQgs3DRendering::renderCheck( const QString &testName, QImage &image, int mismatchCount )
{
mReport += "<h2>" + testName + "</h2>\n";
QString myTmpDir = QDir::tempPath() + '/';
QString myFileName = myTmpDir + testName + ".png";
image.save( myFileName, "PNG" );
QgsRenderChecker myChecker;
myChecker.setControlPathPrefix( QStringLiteral( "3d" ) );
myChecker.setControlName( "expected_" + testName );
myChecker.setRenderedImage( myFileName );
bool myResultFlag = myChecker.compareImages( testName, mismatchCount );
mReport += myChecker.report();
return myResultFlag;
}

QGSTEST_MAIN( TestQgs3DRendering )
#include "testqgs3drendering.moc"

0 comments on commit 30d6812

Please sign in to comment.
You can’t perform that action at this time.