Move DEM terrain tile loader to a separate file

This commit is contained in:
Martin Dobias 2017-09-26 09:23:59 +02:00
parent 53c9852786
commit 6c8a3b918b
5 changed files with 354 additions and 345 deletions

View File

@ -29,6 +29,7 @@ SET(QGIS_3D_SRCS
terrain/qgsdemterraingenerator.cpp
terrain/qgsdemterraintilegeometry_p.cpp
terrain/qgsdemterraintileloader_p.cpp
terrain/qgsflatterraingenerator.cpp
terrain/qgsterrainentity_p.cpp
terrain/qgsterraingenerator.cpp
@ -54,7 +55,7 @@ SET(QGIS_3D_MOC_HDRS
chunks/chunkedentity.h
chunks/chunkloader.h
terrain/qgsdemterraingenerator.h
terrain/qgsdemterraintileloader_p.h
terrain/qgsdemterraintilegeometry_p.h
terrain/qgsterrainentity_p.h
terrain/qgsterraingenerator.h
@ -98,6 +99,7 @@ SET(QGIS_3D_HDRS
terrain/qgsdemterraingenerator.h
terrain/qgsdemterraintilegeometry_p.h
terrain/qgsdemterraintileloader_p.h
terrain/qgsflatterraingenerator.h
terrain/qgsterrainentity_p.h
terrain/qgsterraingenerator.h

View File

@ -1,47 +1,9 @@
#include "qgsdemterraingenerator.h"
#include "qgs3dmapsettings.h"
#include "qgsdemterraintilegeometry_p.h"
#include "qgsterrainentity_p.h"
#include "qgsterraintexturegenerator_p.h"
#include "qgsterraintileentity_p.h"
#include <Qt3DRender/QGeometryRenderer>
#include "qgsdemterraintileloader_p.h"
#include "qgsrasterlayer.h"
#include "chunknode.h"
#if 0
static QByteArray _temporaryHeightMap( int res )
{
QByteArray heightMap;
int count = res * res;
heightMap.resize( count * sizeof( float ) );
float *bits = ( float * ) heightMap.data();
for ( int i = 0; i < count; ++i )
bits[i] = 0;
return heightMap;
}
#endif
static void _heightMapMinMax( const QByteArray &heightMap, float &zMin, float &zMax )
{
const float *zBits = ( const float * ) heightMap.constData();
int zCount = heightMap.count() / sizeof( float );
zMin = zBits[0];
zMax = zBits[0];
for ( int i = 0; i < zCount; ++i )
{
float z = zBits[i];
zMin = qMin( zMin, z );
zMax = qMax( zMax, z );
}
}
// ---------------
QgsDemTerrainGenerator::QgsDemTerrainGenerator()
@ -105,7 +67,7 @@ void QgsDemTerrainGenerator::resolveReferences( const QgsProject &project )
ChunkLoader *QgsDemTerrainGenerator::createChunkLoader( ChunkNode *node ) const
{
return new DemTerrainChunkLoader( mTerrain, node );
return new QgsDemTerrainTileLoader( mTerrain, node );
}
void QgsDemTerrainGenerator::updateGenerator()
@ -122,212 +84,3 @@ void QgsDemTerrainGenerator::updateGenerator()
mHeightMapGenerator.reset();
}
}
// ------------
DemTerrainChunkLoader::DemTerrainChunkLoader( QgsTerrainEntity *terrain, ChunkNode *node )
: QgsTerrainTileLoader( terrain, node )
, resolution( 0 )
{
const Qgs3DMapSettings &map = terrain->map3D();
QgsDemTerrainGenerator *generator = static_cast<QgsDemTerrainGenerator *>( map.terrainGenerator() );
// get heightmap asynchronously
connect( generator->heightMapGenerator(), &QgsDemHeightMapGenerator::heightMapReady, this, &DemTerrainChunkLoader::onHeightMapReady );
heightMapJobId = generator->heightMapGenerator()->render( node->x, node->y, node->z );
resolution = generator->heightMapGenerator()->resolution();
}
DemTerrainChunkLoader::~DemTerrainChunkLoader()
{
qDebug() << "DEM chunk loader done.";
}
Qt3DCore::QEntity *DemTerrainChunkLoader::createEntity( Qt3DCore::QEntity *parent )
{
QgsTerrainTileEntity *entity = new QgsTerrainTileEntity;
// create geometry renderer
Qt3DRender::QGeometryRenderer *mesh = new Qt3DRender::QGeometryRenderer;
mesh->setGeometry( new DemTerrainTileGeometry( resolution, heightMap, mesh ) );
entity->addComponent( mesh ); // takes ownership if the component has no parent
// create material
createTextureComponent( entity );
// create transform
Qt3DCore::QTransform *transform;
transform = new Qt3DCore::QTransform();
entity->addComponent( transform );
float zMin, zMax;
_heightMapMinMax( heightMap, zMin, zMax );
const Qgs3DMapSettings &map = terrain()->map3D();
QgsRectangle extent = map.terrainGenerator()->terrainTilingScheme.tileToExtent( node->x, node->y, node->z ); //node->extent;
double x0 = extent.xMinimum() - map.originX;
double y0 = extent.yMinimum() - map.originY;
double side = extent.width();
double half = side / 2;
transform->setScale3D( QVector3D( side, map.terrainVerticalScale(), side ) );
transform->setTranslation( QVector3D( x0 + half, 0, - ( y0 + half ) ) );
node->setExactBbox( AABB( x0, zMin * map.terrainVerticalScale(), -y0, x0 + side, zMax * map.terrainVerticalScale(), -( y0 + side ) ) );
entity->setEnabled( false );
entity->setParent( parent );
return entity;
}
void DemTerrainChunkLoader::onHeightMapReady( int jobId, const QByteArray &heightMap )
{
if ( heightMapJobId == jobId )
{
this->heightMap = heightMap;
heightMapJobId = -1;
// continue loading - texture
loadTexture();
}
}
// ---------------------
#include <qgsrasterlayer.h>
#include <QtConcurrent/QtConcurrentRun>
#include <QFutureWatcher>
QgsDemHeightMapGenerator::QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution )
: dtm( dtm )
, clonedProvider( ( QgsRasterDataProvider * )dtm->dataProvider()->clone() )
, tilingScheme( tilingScheme )
, res( resolution )
, lastJobId( 0 )
{
}
QgsDemHeightMapGenerator::~QgsDemHeightMapGenerator()
{
delete clonedProvider;
}
#include <QElapsedTimer>
static QByteArray _readDtmData( QgsRasterDataProvider *provider, const QgsRectangle &extent, int res )
{
QElapsedTimer t;
t.start();
// TODO: use feedback object? (but GDAL currently does not support cancelation anyway)
QgsRasterBlock *block = provider->block( 1, extent, res, res );
QByteArray data;
if ( block )
{
block->convert( Qgis::Float32 ); // currently we expect just floats
data = block->data();
data.detach(); // this should make a deep copy
delete block;
}
qDebug() << "[TT] read block time " << t.elapsed() << "ms";
return data;
}
int QgsDemHeightMapGenerator::render( int x, int y, int z )
{
Q_ASSERT( jobs.isEmpty() ); // should be always just one active job...
// extend the rect by half-pixel on each side? to get the values in "corners"
QgsRectangle extent = tilingScheme.tileToExtent( x, y, z );
float mapUnitsPerPixel = extent.width() / res;
extent.grow( mapUnitsPerPixel / 2 );
// but make sure not to go beyond the full extent (returns invalid values)
QgsRectangle fullExtent = tilingScheme.tileToExtent( 0, 0, 0 );
extent = extent.intersect( &fullExtent );
JobData jd;
jd.jobId = ++lastJobId;
jd.extent = extent;
jd.timer.start();
// make a clone of the data provider so it is safe to use in worker thread
jd.future = QtConcurrent::run( _readDtmData, clonedProvider, extent, res );
QFutureWatcher<QByteArray> *fw = new QFutureWatcher<QByteArray>;
fw->setFuture( jd.future );
connect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
jobs.insert( fw, jd );
qDebug() << "[TT] added job: " << jd.jobId << " " << x << "|" << y << "|" << z << " .... in queue: " << jobs.count();
return jd.jobId;
}
QByteArray QgsDemHeightMapGenerator::renderSynchronously( int x, int y, int z )
{
// extend the rect by half-pixel on each side? to get the values in "corners"
QgsRectangle extent = tilingScheme.tileToExtent( x, y, z );
float mapUnitsPerPixel = extent.width() / res;
extent.grow( mapUnitsPerPixel / 2 );
// but make sure not to go beyond the full extent (returns invalid values)
QgsRectangle fullExtent = tilingScheme.tileToExtent( 0, 0, 0 );
extent = extent.intersect( &fullExtent );
QgsRasterBlock *block = dtm->dataProvider()->block( 1, extent, res, res );
QByteArray data;
if ( block )
{
block->convert( Qgis::Float32 ); // currently we expect just floats
data = block->data();
data.detach(); // this should make a deep copy
delete block;
}
return data;
}
float QgsDemHeightMapGenerator::heightAt( double x, double y )
{
// TODO: this is quite a primitive implementation: better to use heightmaps currently in use
int res = 1024;
QgsRectangle rect = dtm->extent();
if ( dtmCoarseData.isEmpty() )
{
QgsRasterBlock *block = dtm->dataProvider()->block( 1, rect, res, res );
block->convert( Qgis::Float32 );
dtmCoarseData = block->data();
dtmCoarseData.detach(); // make a deep copy
delete block;
}
int cellX = ( int )( ( x - rect.xMinimum() ) / rect.width() * res + .5f );
int cellY = ( int )( ( rect.yMaximum() - y ) / rect.height() * res + .5f );
cellX = qBound( 0, cellX, res - 1 );
cellY = qBound( 0, cellY, res - 1 );
const float *data = ( const float * ) dtmCoarseData.constData();
return data[cellX + cellY * res];
}
void QgsDemHeightMapGenerator::onFutureFinished()
{
QFutureWatcher<QByteArray> *fw = static_cast<QFutureWatcher<QByteArray>*>( sender() );
Q_ASSERT( fw );
Q_ASSERT( jobs.contains( fw ) );
JobData jobData = jobs.value( fw );
jobs.remove( fw );
fw->deleteLater();
qDebug() << "[TT] finished job " << jobData.jobId << "total time " << jobData.timer.elapsed() << "ms ... in queue: " << jobs.count();
QByteArray data = jobData.future.result();
emit heightMapReady( jobData.jobId, data );
}

View File

@ -4,7 +4,6 @@
#include "qgis_3d.h"
#include "qgsterraingenerator.h"
#include "qgsterraintileloader_p.h"
#include <memory>
@ -58,98 +57,4 @@ class _3D_EXPORT QgsDemTerrainGenerator : public QgsTerrainGenerator
};
/** \ingroup 3d
* Chunk loader for DEM terrain tiles.
* \since QGIS 3.0
*/
class DemTerrainChunkLoader : public QgsTerrainTileLoader
{
Q_OBJECT
public:
//! Constructs loader for the given chunk node
DemTerrainChunkLoader( QgsTerrainEntity *terrain, ChunkNode *node );
~DemTerrainChunkLoader();
virtual Qt3DCore::QEntity *createEntity( Qt3DCore::QEntity *parent );
private slots:
void onHeightMapReady( int jobId, const QByteArray &heightMap );
private:
int heightMapJobId;
QByteArray heightMap;
int resolution;
};
#include <QtConcurrent/QtConcurrentRun>
#include <QFutureWatcher>
#include <QElapsedTimer>
#include "qgsrectangle.h"
class QgsRasterDataProvider;
/** \ingroup 3d
* Utility class to asynchronously create heightmaps from DEM raster for given tiles of terrain.
* \since QGIS 3.0
*/
class QgsDemHeightMapGenerator : public QObject
{
Q_OBJECT
public:
//! Constructs height map generator based on a raster layer with elevation model,
//! terrain's tiling scheme and height map resolution (number of height values on each side of tile)
QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution );
~QgsDemHeightMapGenerator();
//! asynchronous terrain read for a tile (array of floats)
int render( int x, int y, int z );
//! synchronous terrain read for a tile
QByteArray renderSynchronously( int x, int y, int z );
//! Returns resolution(number of height values on each side of tile)
int resolution() const { return res; }
//! returns height at given position (in terrain's CRS)
float heightAt( double x, double y );
signals:
//! emitted when a previously requested heightmap is ready
void heightMapReady( int jobId, const QByteArray &heightMap );
private slots:
void onFutureFinished();
private:
//! raster used to build terrain
QgsRasterLayer *dtm;
//! cloned provider to be used in worker thread
QgsRasterDataProvider *clonedProvider;
QgsTilingScheme tilingScheme;
int res;
int lastJobId;
struct JobData
{
int jobId;
QgsRectangle extent;
QFuture<QByteArray> future;
QFutureWatcher<QByteArray> *fw;
QElapsedTimer timer;
};
QHash<QFutureWatcher<QByteArray>*, JobData> jobs;
//! used for height queries
QByteArray dtmCoarseData;
};
#endif // QGSDEMTERRAINGENERATOR_H

View File

@ -0,0 +1,235 @@
#include "qgsdemterraintileloader_p.h"
#include "chunknode.h"
#include "qgs3dmapsettings.h"
#include "qgsdemterraingenerator.h"
#include "qgsdemterraintilegeometry_p.h"
#include "qgsterrainentity_p.h"
#include "qgsterraintexturegenerator_p.h"
#include "qgsterraintileentity_p.h"
#include <Qt3DRender/QGeometryRenderer>
///@cond PRIVATE
static void _heightMapMinMax( const QByteArray &heightMap, float &zMin, float &zMax )
{
const float *zBits = ( const float * ) heightMap.constData();
int zCount = heightMap.count() / sizeof( float );
zMin = zBits[0];
zMax = zBits[0];
for ( int i = 0; i < zCount; ++i )
{
float z = zBits[i];
zMin = qMin( zMin, z );
zMax = qMax( zMax, z );
}
}
QgsDemTerrainTileLoader::QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, ChunkNode *node )
: QgsTerrainTileLoader( terrain, node )
, resolution( 0 )
{
const Qgs3DMapSettings &map = terrain->map3D();
QgsDemTerrainGenerator *generator = static_cast<QgsDemTerrainGenerator *>( map.terrainGenerator() );
// get heightmap asynchronously
connect( generator->heightMapGenerator(), &QgsDemHeightMapGenerator::heightMapReady, this, &QgsDemTerrainTileLoader::onHeightMapReady );
heightMapJobId = generator->heightMapGenerator()->render( node->x, node->y, node->z );
resolution = generator->heightMapGenerator()->resolution();
}
QgsDemTerrainTileLoader::~QgsDemTerrainTileLoader()
{
qDebug() << "DEM chunk loader done.";
}
Qt3DCore::QEntity *QgsDemTerrainTileLoader::createEntity( Qt3DCore::QEntity *parent )
{
QgsTerrainTileEntity *entity = new QgsTerrainTileEntity;
// create geometry renderer
Qt3DRender::QGeometryRenderer *mesh = new Qt3DRender::QGeometryRenderer;
mesh->setGeometry( new DemTerrainTileGeometry( resolution, heightMap, mesh ) );
entity->addComponent( mesh ); // takes ownership if the component has no parent
// create material
createTextureComponent( entity );
// create transform
Qt3DCore::QTransform *transform;
transform = new Qt3DCore::QTransform();
entity->addComponent( transform );
float zMin, zMax;
_heightMapMinMax( heightMap, zMin, zMax );
const Qgs3DMapSettings &map = terrain()->map3D();
QgsRectangle extent = map.terrainGenerator()->terrainTilingScheme.tileToExtent( node->x, node->y, node->z ); //node->extent;
double x0 = extent.xMinimum() - map.originX;
double y0 = extent.yMinimum() - map.originY;
double side = extent.width();
double half = side / 2;
transform->setScale3D( QVector3D( side, map.terrainVerticalScale(), side ) );
transform->setTranslation( QVector3D( x0 + half, 0, - ( y0 + half ) ) );
node->setExactBbox( AABB( x0, zMin * map.terrainVerticalScale(), -y0, x0 + side, zMax * map.terrainVerticalScale(), -( y0 + side ) ) );
entity->setEnabled( false );
entity->setParent( parent );
return entity;
}
void QgsDemTerrainTileLoader::onHeightMapReady( int jobId, const QByteArray &heightMap )
{
if ( heightMapJobId == jobId )
{
this->heightMap = heightMap;
heightMapJobId = -1;
// continue loading - texture
loadTexture();
}
}
// ---------------------
#include <qgsrasterlayer.h>
#include <QtConcurrent/QtConcurrentRun>
#include <QFutureWatcher>
QgsDemHeightMapGenerator::QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution )
: dtm( dtm )
, clonedProvider( ( QgsRasterDataProvider * )dtm->dataProvider()->clone() )
, tilingScheme( tilingScheme )
, res( resolution )
, lastJobId( 0 )
{
}
QgsDemHeightMapGenerator::~QgsDemHeightMapGenerator()
{
delete clonedProvider;
}
#include <QElapsedTimer>
static QByteArray _readDtmData( QgsRasterDataProvider *provider, const QgsRectangle &extent, int res )
{
QElapsedTimer t;
t.start();
// TODO: use feedback object? (but GDAL currently does not support cancelation anyway)
QgsRasterBlock *block = provider->block( 1, extent, res, res );
QByteArray data;
if ( block )
{
block->convert( Qgis::Float32 ); // currently we expect just floats
data = block->data();
data.detach(); // this should make a deep copy
delete block;
}
qDebug() << "[TT] read block time " << t.elapsed() << "ms";
return data;
}
int QgsDemHeightMapGenerator::render( int x, int y, int z )
{
Q_ASSERT( jobs.isEmpty() ); // should be always just one active job...
// extend the rect by half-pixel on each side? to get the values in "corners"
QgsRectangle extent = tilingScheme.tileToExtent( x, y, z );
float mapUnitsPerPixel = extent.width() / res;
extent.grow( mapUnitsPerPixel / 2 );
// but make sure not to go beyond the full extent (returns invalid values)
QgsRectangle fullExtent = tilingScheme.tileToExtent( 0, 0, 0 );
extent = extent.intersect( &fullExtent );
JobData jd;
jd.jobId = ++lastJobId;
jd.extent = extent;
jd.timer.start();
// make a clone of the data provider so it is safe to use in worker thread
jd.future = QtConcurrent::run( _readDtmData, clonedProvider, extent, res );
QFutureWatcher<QByteArray> *fw = new QFutureWatcher<QByteArray>;
fw->setFuture( jd.future );
connect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
jobs.insert( fw, jd );
qDebug() << "[TT] added job: " << jd.jobId << " " << x << "|" << y << "|" << z << " .... in queue: " << jobs.count();
return jd.jobId;
}
QByteArray QgsDemHeightMapGenerator::renderSynchronously( int x, int y, int z )
{
// extend the rect by half-pixel on each side? to get the values in "corners"
QgsRectangle extent = tilingScheme.tileToExtent( x, y, z );
float mapUnitsPerPixel = extent.width() / res;
extent.grow( mapUnitsPerPixel / 2 );
// but make sure not to go beyond the full extent (returns invalid values)
QgsRectangle fullExtent = tilingScheme.tileToExtent( 0, 0, 0 );
extent = extent.intersect( &fullExtent );
QgsRasterBlock *block = dtm->dataProvider()->block( 1, extent, res, res );
QByteArray data;
if ( block )
{
block->convert( Qgis::Float32 ); // currently we expect just floats
data = block->data();
data.detach(); // this should make a deep copy
delete block;
}
return data;
}
float QgsDemHeightMapGenerator::heightAt( double x, double y )
{
// TODO: this is quite a primitive implementation: better to use heightmaps currently in use
int res = 1024;
QgsRectangle rect = dtm->extent();
if ( dtmCoarseData.isEmpty() )
{
QgsRasterBlock *block = dtm->dataProvider()->block( 1, rect, res, res );
block->convert( Qgis::Float32 );
dtmCoarseData = block->data();
dtmCoarseData.detach(); // make a deep copy
delete block;
}
int cellX = ( int )( ( x - rect.xMinimum() ) / rect.width() * res + .5f );
int cellY = ( int )( ( rect.yMaximum() - y ) / rect.height() * res + .5f );
cellX = qBound( 0, cellX, res - 1 );
cellY = qBound( 0, cellY, res - 1 );
const float *data = ( const float * ) dtmCoarseData.constData();
return data[cellX + cellY * res];
}
void QgsDemHeightMapGenerator::onFutureFinished()
{
QFutureWatcher<QByteArray> *fw = static_cast<QFutureWatcher<QByteArray>*>( sender() );
Q_ASSERT( fw );
Q_ASSERT( jobs.contains( fw ) );
JobData jobData = jobs.value( fw );
jobs.remove( fw );
fw->deleteLater();
qDebug() << "[TT] finished job " << jobData.jobId << "total time " << jobData.timer.elapsed() << "ms ... in queue: " << jobs.count();
QByteArray data = jobData.future.result();
emit heightMapReady( jobData.jobId, data );
}
/// @endcond

View File

@ -0,0 +1,114 @@
#ifndef QGSDEMTERRAINTILELOADER_P_H
#define QGSDEMTERRAINTILELOADER_P_H
///@cond PRIVATE
//
// W A R N I N G
// -------------
//
// This file is not part of the QGIS API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
#include <QtConcurrent/QtConcurrentRun>
#include <QFutureWatcher>
#include <QElapsedTimer>
#include "qgsrectangle.h"
#include "qgsterraintileloader_p.h"
#include "qgstilingscheme.h"
class QgsRasterDataProvider;
class QgsRasterLayer;
/** \ingroup 3d
* Chunk loader for DEM terrain tiles.
* \since QGIS 3.0
*/
class QgsDemTerrainTileLoader : public QgsTerrainTileLoader
{
Q_OBJECT
public:
//! Constructs loader for the given chunk node
QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, ChunkNode *node );
~QgsDemTerrainTileLoader();
virtual Qt3DCore::QEntity *createEntity( Qt3DCore::QEntity *parent );
private slots:
void onHeightMapReady( int jobId, const QByteArray &heightMap );
private:
int heightMapJobId;
QByteArray heightMap;
int resolution;
};
/** \ingroup 3d
* Utility class to asynchronously create heightmaps from DEM raster for given tiles of terrain.
* \since QGIS 3.0
*/
class QgsDemHeightMapGenerator : public QObject
{
Q_OBJECT
public:
//! Constructs height map generator based on a raster layer with elevation model,
//! terrain's tiling scheme and height map resolution (number of height values on each side of tile)
QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution );
~QgsDemHeightMapGenerator();
//! asynchronous terrain read for a tile (array of floats)
int render( int x, int y, int z );
//! synchronous terrain read for a tile
QByteArray renderSynchronously( int x, int y, int z );
//! Returns resolution(number of height values on each side of tile)
int resolution() const { return res; }
//! returns height at given position (in terrain's CRS)
float heightAt( double x, double y );
signals:
//! emitted when a previously requested heightmap is ready
void heightMapReady( int jobId, const QByteArray &heightMap );
private slots:
void onFutureFinished();
private:
//! raster used to build terrain
QgsRasterLayer *dtm;
//! cloned provider to be used in worker thread
QgsRasterDataProvider *clonedProvider;
QgsTilingScheme tilingScheme;
int res;
int lastJobId;
struct JobData
{
int jobId;
QgsRectangle extent;
QFuture<QByteArray> future;
QFutureWatcher<QByteArray> *fw;
QElapsedTimer timer;
};
QHash<QFutureWatcher<QByteArray>*, JobData> jobs;
//! used for height queries
QByteArray dtmCoarseData;
};
/// @endcond
#endif // QGSDEMTERRAINTILELOADER_P_H