Navigation Menu

Skip to content

Commit

Permalink
Add skirts to hide cracks between adjacent terrain tiles
Browse files Browse the repository at this point in the history
This adds user configurable skirt height to DEM terrain generator.
Skirts are vertical walls at the edges of each terrain tile that make
cracks (discontinuities) much less apparent.

If there are visible cracks in terrain in the 3D view, try increasing skirt height.
The ideal skirt height depends on the scale of the map and range of altitudes.
  • Loading branch information
wonder-sk committed Oct 26, 2017
1 parent ba75123 commit 8c32520
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 38 deletions.
4 changes: 4 additions & 0 deletions src/3d/terrain/qgsdemterraingenerator.cpp
Expand Up @@ -23,6 +23,7 @@

QgsDemTerrainGenerator::QgsDemTerrainGenerator()
: mResolution( 16 )
, mSkirtHeight( 10.f )
{
}

Expand All @@ -47,6 +48,7 @@ QgsTerrainGenerator *QgsDemTerrainGenerator::clone() const
QgsDemTerrainGenerator *cloned = new QgsDemTerrainGenerator;
cloned->mLayer = mLayer;
cloned->mResolution = mResolution;
cloned->mSkirtHeight = mSkirtHeight;
cloned->updateGenerator();
return cloned;
}
Expand All @@ -71,12 +73,14 @@ void QgsDemTerrainGenerator::writeXml( QDomElement &elem ) const
{
elem.setAttribute( "layer", mLayer.layerId );
elem.setAttribute( "resolution", mResolution );
elem.setAttribute( "skirt-height", mSkirtHeight );
}

void QgsDemTerrainGenerator::readXml( const QDomElement &elem )
{
mLayer = QgsMapLayerRef( elem.attribute( "layer" ) );
mResolution = elem.attribute( "resolution" ).toInt();
mSkirtHeight = elem.attribute( "skirt-height" ).toFloat();
}

void QgsDemTerrainGenerator::resolveReferences( const QgsProject &project )
Expand Down
7 changes: 7 additions & 0 deletions src/3d/terrain/qgsdemterraingenerator.h
Expand Up @@ -49,6 +49,11 @@ class _3D_EXPORT QgsDemTerrainGenerator : public QgsTerrainGenerator
//! Returns resolution of the generator (how many elevation samples on one side of a terrain tile)
int resolution() const { return mResolution; }

//! Sets skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between adjacent tiles.
void setSkirtHeight( float skirtHeight ) { mSkirtHeight = skirtHeight; }
//! Returns skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between adjacent tiles.
float skirtHeight() const { return mSkirtHeight; }

//! Returns height map generator object - takes care of extraction of elevations from the layer)
QgsDemHeightMapGenerator *heightMapGenerator() { return mHeightMapGenerator; }

Expand All @@ -71,6 +76,8 @@ class _3D_EXPORT QgsDemTerrainGenerator : public QgsTerrainGenerator
QgsMapLayerRef mLayer;
//! how many vertices to place on one side of the tile
int mResolution;
//! height of the "skirts" at the edges of tiles to hide cracks between adjacent cracks
float mSkirtHeight;
};


Expand Down
61 changes: 40 additions & 21 deletions src/3d/terrain/qgsdemterraintilegeometry_p.cpp
Expand Up @@ -24,14 +24,15 @@
using namespace Qt3DRender;


static QByteArray createPlaneVertexData( int res, const QByteArray &heights )
static QByteArray createPlaneVertexData( int res, float skirtHeight, const QByteArray &heights )
{
Q_ASSERT( res >= 2 );
Q_ASSERT( heights.count() == res * res * ( int )sizeof( float ) );

const float *zBits = ( const float * ) heights.constData();
const float *zData = ( const float * ) heights.constData();
const float *zBits = zData;

const int nVerts = res * res;
const int nVerts = ( res + 2 ) * ( res + 2 );

// Populate a buffer with the interleaved per-vertex data with
// vec3 pos, vec2 texCoord, vec3 normal, vec4 tangent
Expand All @@ -51,26 +52,35 @@ static QByteArray createPlaneVertexData( int res, const QByteArray &heights )
const float dv = 1.0 / ( resolution.height() - 1 );

// Iterate over z
for ( int j = 0; j < resolution.height(); ++j )
for ( int j = -1; j <= resolution.height(); ++j )
{
const float z = z0 + static_cast<float>( j ) * dz;
const float v = static_cast<float>( j ) * dv;
int jBound = qBound( 0, j, resolution.height() - 1 );
const float z = z0 + static_cast<float>( jBound ) * dz;
const float v = static_cast<float>( jBound ) * dv;

// Iterate over x
for ( int i = 0; i < resolution.width(); ++i )
for ( int i = -1; i <= resolution.width(); ++i )
{
const float x = x0 + static_cast<float>( i ) * dx;
const float u = static_cast<float>( i ) * du;
int iBound = qBound( 0, i, resolution.width() - 1 );
const float x = x0 + static_cast<float>( iBound ) * dx;
const float u = static_cast<float>( iBound ) * du;

float height;
if ( i == iBound && j == jBound )
height = *zBits++;
else
height = zData[ jBound * resolution.width() + iBound ] - skirtHeight;

// position
*fptr++ = x;
*fptr++ = *zBits++;
*fptr++ = height;
*fptr++ = z;

// texture coordinates
*fptr++ = u;
*fptr++ = v;

// TODO: compute correct normals based on neighboring pixels
// normal
*fptr++ = 0.0f;
*fptr++ = 1.0f;
Expand All @@ -85,22 +95,25 @@ static QByteArray createPlaneVertexData( int res, const QByteArray &heights )
static QByteArray createPlaneIndexData( int res )
{
QSize resolution( res, res );
int numVerticesX = resolution.width() + 2;
int numVerticesZ = resolution.height() + 2;

// Create the index data. 2 triangles per rectangular face
const int faces = 2 * ( resolution.width() - 1 ) * ( resolution.height() - 1 );
const int faces = 2 * ( numVerticesX - 1 ) * ( numVerticesZ - 1 );
const quint32 indices = 3 * faces;
Q_ASSERT( indices < std::numeric_limits<quint32>::max() );
QByteArray indexBytes;
indexBytes.resize( indices * sizeof( quint32 ) );
quint32 *indexPtr = reinterpret_cast<quint32 *>( indexBytes.data() );

// Iterate over z
for ( int j = 0; j < resolution.height() - 1; ++j )
for ( int j = 0; j < numVerticesZ - 1; ++j )
{
const int rowStartIndex = j * resolution.width();
const int nextRowStartIndex = ( j + 1 ) * resolution.width();
const int rowStartIndex = j * numVerticesX;
const int nextRowStartIndex = ( j + 1 ) * numVerticesX;

// Iterate over x
for ( int i = 0; i < resolution.width() - 1; ++i )
for ( int i = 0; i < numVerticesX - 1; ++i )
{
// Split quad into two triangles
*indexPtr++ = rowStartIndex + i;
Expand All @@ -122,23 +135,25 @@ static QByteArray createPlaneIndexData( int res )
class PlaneVertexBufferFunctor : public QBufferDataGenerator
{
public:
explicit PlaneVertexBufferFunctor( int resolution, const QByteArray &heightMap )
explicit PlaneVertexBufferFunctor( int resolution, float skirtHeight, const QByteArray &heightMap )
: mResolution( resolution )
, mSkirtHeight( skirtHeight )
, mHeightMap( heightMap )
{}

~PlaneVertexBufferFunctor() {}

QByteArray operator()() final
{
return createPlaneVertexData( mResolution, mHeightMap );
return createPlaneVertexData( mResolution, mSkirtHeight, mHeightMap );
}

bool operator ==( const QBufferDataGenerator &other ) const final
{
const PlaneVertexBufferFunctor *otherFunctor = functor_cast<PlaneVertexBufferFunctor>( &other );
if ( otherFunctor != nullptr )
return ( otherFunctor->mResolution == mResolution &&
otherFunctor->mSkirtHeight == mSkirtHeight &&
otherFunctor->mHeightMap == mHeightMap );
return false;
}
Expand All @@ -147,6 +162,7 @@ class PlaneVertexBufferFunctor : public QBufferDataGenerator

private:
int mResolution;
float mSkirtHeight;
QByteArray mHeightMap;
};

Expand Down Expand Up @@ -184,9 +200,10 @@ class PlaneIndexBufferFunctor : public QBufferDataGenerator
// ------------


DemTerrainTileGeometry::DemTerrainTileGeometry( int resolution, const QByteArray &heightMap, DemTerrainTileGeometry::QNode *parent )
DemTerrainTileGeometry::DemTerrainTileGeometry( int resolution, float skirtHeight, const QByteArray &heightMap, DemTerrainTileGeometry::QNode *parent )
: QGeometry( parent )
, mResolution( resolution )
, mSkirtHeight( skirtHeight )
, mHeightMap( heightMap )
{
init();
Expand All @@ -201,9 +218,11 @@ void DemTerrainTileGeometry::init()
mVertexBuffer = new Qt3DRender::QBuffer( Qt3DRender::QBuffer::VertexBuffer, this );
mIndexBuffer = new Qt3DRender::QBuffer( Qt3DRender::QBuffer::IndexBuffer, this );

const int nVerts = mResolution * mResolution;
int nVertsX = mResolution + 2;
int nVertsZ = mResolution + 2;
const int nVerts = nVertsX * nVertsZ;
const int stride = ( 3 + 2 + 3 ) * sizeof( float );
const int faces = 2 * ( mResolution - 1 ) * ( mResolution - 1 );
const int faces = 2 * ( nVertsX - 1 ) * ( nVertsZ - 1 );

mPositionAttribute->setName( QAttribute::defaultPositionAttributeName() );
mPositionAttribute->setVertexBaseType( QAttribute::Float );
Expand Down Expand Up @@ -238,7 +257,7 @@ void DemTerrainTileGeometry::init()
// Each primitive has 3 vertives
mIndexAttribute->setCount( faces * 3 );

mVertexBuffer->setDataGenerator( QSharedPointer<PlaneVertexBufferFunctor>::create( mResolution, mHeightMap ) );
mVertexBuffer->setDataGenerator( QSharedPointer<PlaneVertexBufferFunctor>::create( mResolution, mSkirtHeight, mHeightMap ) );
mIndexBuffer->setDataGenerator( QSharedPointer<PlaneIndexBufferFunctor>::create( mResolution ) );

addAttribute( mPositionAttribute );
Expand Down
3 changes: 2 additions & 1 deletion src/3d/terrain/qgsdemterraintilegeometry_p.h
Expand Up @@ -55,13 +55,14 @@ class DemTerrainTileGeometry : public Qt3DRender::QGeometry
* Constructs a terrain tile geometry. Resolution is the number of vertices on one side of the tile,
* heightMap is array of float values with one height value for each vertex
*/
explicit DemTerrainTileGeometry( int resolution, const QByteArray &heightMap, QNode *parent = nullptr );
explicit DemTerrainTileGeometry( int resolution, float skirtHeight, const QByteArray &heightMap, QNode *parent = nullptr );
~DemTerrainTileGeometry() = default;

private:
void init();

int mResolution;
float mSkirtHeight;
QByteArray mHeightMap;
Qt3DRender::QAttribute *mPositionAttribute = nullptr;
Qt3DRender::QAttribute *mNormalAttribute = nullptr;
Expand Down
3 changes: 2 additions & 1 deletion src/3d/terrain/qgsdemterraintileloader_p.cpp
Expand Up @@ -54,6 +54,7 @@ QgsDemTerrainTileLoader::QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, Qgs
connect( generator->heightMapGenerator(), &QgsDemHeightMapGenerator::heightMapReady, this, &QgsDemTerrainTileLoader::onHeightMapReady );
mHeightMapJobId = generator->heightMapGenerator()->render( node->tileX(), node->tileY(), node->tileZ() );
mResolution = generator->heightMapGenerator()->resolution();
mSkirtHeight = generator->skirtHeight();
}

QgsDemTerrainTileLoader::~QgsDemTerrainTileLoader()
Expand All @@ -67,7 +68,7 @@ Qt3DCore::QEntity *QgsDemTerrainTileLoader::createEntity( Qt3DCore::QEntity *par
// create geometry renderer

Qt3DRender::QGeometryRenderer *mesh = new Qt3DRender::QGeometryRenderer;
mesh->setGeometry( new DemTerrainTileGeometry( mResolution, mHeightMap, mesh ) );
mesh->setGeometry( new DemTerrainTileGeometry( mResolution, mSkirtHeight, mHeightMap, mesh ) );
entity->addComponent( mesh ); // takes ownership if the component has no parent

// create material
Expand Down
1 change: 1 addition & 0 deletions src/3d/terrain/qgsdemterraintileloader_p.h
Expand Up @@ -61,6 +61,7 @@ class QgsDemTerrainTileLoader : public QgsTerrainTileLoader
int mHeightMapJobId;
QByteArray mHeightMap;
int mResolution;
float mSkirtHeight;
};


Expand Down
28 changes: 22 additions & 6 deletions src/app/3d/qgs3dmapconfigwidget.cpp
Expand Up @@ -42,13 +42,15 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas
{
QgsDemTerrainGenerator *demTerrainGen = static_cast<QgsDemTerrainGenerator *>( terrainGen );
spinTerrainResolution->setValue( demTerrainGen->resolution() );
spinTerrainSkirtHeight->setValue( demTerrainGen->skirtHeight() );
cboTerrainLayer->setLayer( demTerrainGen->layer() );
}
else
{
cboTerrainLayer->setLayer( nullptr );
spinTerrainResolution->setEnabled( false );
spinTerrainResolution->setValue( 16 );
spinTerrainSkirtHeight->setValue( 10 );
}

spinTerrainScale->setValue( mMap->terrainVerticalScale() );
Expand All @@ -74,13 +76,27 @@ void Qgs3DMapConfigWidget::apply()
{
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );

// TODO: what if just changing generator's properties
if ( demLayer && mMap->terrainGenerator()->type() != QgsTerrainGenerator::Dem )
if ( demLayer )
{
QgsDemTerrainGenerator *demTerrainGen = new QgsDemTerrainGenerator;
demTerrainGen->setLayer( demLayer );
demTerrainGen->setResolution( spinTerrainResolution->value() );
mMap->setTerrainGenerator( demTerrainGen );
bool tGenNeedsUpdate = true;
if ( mMap->terrainGenerator()->type() == QgsTerrainGenerator::Dem )
{
// if we already have a DEM terrain generator, check whether there was actually any change
QgsDemTerrainGenerator *oldDemTerrainGen = static_cast<QgsDemTerrainGenerator *>( mMap->terrainGenerator() );
if ( oldDemTerrainGen->layer() == demLayer &&
oldDemTerrainGen->resolution() == spinTerrainResolution->value() &&
oldDemTerrainGen->skirtHeight() == spinTerrainSkirtHeight->value() )
tGenNeedsUpdate = false;
}

if ( tGenNeedsUpdate )
{
QgsDemTerrainGenerator *demTerrainGen = new QgsDemTerrainGenerator;
demTerrainGen->setLayer( demLayer );
demTerrainGen->setResolution( spinTerrainResolution->value() );
demTerrainGen->setSkirtHeight( spinTerrainSkirtHeight->value() );
mMap->setTerrainGenerator( demTerrainGen );
}
}
else if ( !demLayer && mMap->terrainGenerator()->type() != QgsTerrainGenerator::Flat )
{
Expand Down

0 comments on commit 8c32520

Please sign in to comment.