Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Improve handling of vertical ranges
So far we used a fixed vertical range for 3D bounding boxes with range [0, 500]
This of course would not work well if Z values are outside of this range
(e.g. tiles may not show at all or get pruned prematurely)

The approach now is to start with an estimate of the vertical range based
on few features from the source vector layer and then once our tile is completely
loaded, update the 3D bounding box to better represent the actual 3D extent.
  • Loading branch information
wonder-sk committed Jan 12, 2020
1 parent db50965 commit 9009971
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 12 deletions.
14 changes: 14 additions & 0 deletions python/core/auto_generated/qgstessellator.sip.in
Expand Up @@ -66,6 +66,20 @@ Returns size of one vertex entry in bytes
%End


float zMinimum() const;
%Docstring
Returns minimal Z value of the data (in world coordinates)

.. versionadded:: 3.12
%End

float zMaximum() const;
%Docstring
Returns maximal Z value of the data (in world coordinates)

.. versionadded:: 3.12
%End

};

/************************************************************************
Expand Down
40 changes: 38 additions & 2 deletions src/3d/qgs3dutils.cpp
Expand Up @@ -461,10 +461,9 @@ static QgsRectangle _tryReprojectExtent2D( const QgsRectangle &extent, const Qgs
return extentMapCrs;
}

QgsAABB Qgs3DUtils::layerToWorldExtent( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &layerCrs, const QgsVector3D &mapOrigin, const QgsCoordinateReferenceSystem &mapCrs, const QgsCoordinateTransformContext &context )
QgsAABB Qgs3DUtils::layerToWorldExtent( const QgsRectangle &extent, double zMin, double zMax, const QgsCoordinateReferenceSystem &layerCrs, const QgsVector3D &mapOrigin, const QgsCoordinateReferenceSystem &mapCrs, const QgsCoordinateTransformContext &context )
{
QgsRectangle extentMapCrs( _tryReprojectExtent2D( extent, layerCrs, mapCrs, context ) );
double zMin = 0, zMax = 500; // TODO: figure out Z range
return mapToWorldExtent( extentMapCrs, zMin, zMax, mapOrigin );
}

Expand Down Expand Up @@ -516,6 +515,43 @@ QgsVector3D Qgs3DUtils::transformWorldCoordinates( const QgsVector3D &worldPoint
return mapToWorldCoordinates( mapPoint2, origin2 );
}

void Qgs3DUtils::estimateVectorLayerZRange( QgsVectorLayer *layer, double &zMin, double &zMax )
{
if ( QgsWkbTypes::hasZ( layer->wkbType() ) )
{
zMin = std::numeric_limits<double>::max();
zMin = std::numeric_limits<double>::min();

int featureCount = 0;
QgsFeature f;
QgsFeatureIterator it = layer->getFeatures( QgsFeatureRequest().setNoAttributes() );

This comment has been minimized.

Copy link
@nyalldawson

nyalldawson Jan 12, 2020

Collaborator

You could call setLimit on the iterator - that avoids providers like postgres fetching too many features in the initial block

while ( it.nextFeature( f ) )
{
if ( featureCount >= 100 )
break;
QgsGeometry g = f.geometry();
for ( auto vit = g.vertices_begin(); vit != g.vertices_end(); ++vit )
{
double z = ( *vit ).z();
if ( z < zMin ) zMin = z;
if ( z > zMax ) zMax = z;
}
featureCount++;
}

if ( featureCount == 0 )
{
zMin = 0;
zMax = 0;
}
}
else
{
zMin = 0;
zMax = 0;
}
}

std::unique_ptr<QgsAbstract3DSymbol> Qgs3DUtils::symbolForGeometryType( QgsWkbTypes::GeometryType geomType )
{
switch ( geomType )
Expand Down
10 changes: 9 additions & 1 deletion src/3d/qgs3dutils.h
Expand Up @@ -132,7 +132,7 @@ class _3D_EXPORT Qgs3DUtils
static QgsVector3D worldToMapCoordinates( const QgsVector3D &worldCoords, const QgsVector3D &origin );

//! Converts extent (in map layer's CRS) to axis aligned bounding box in 3D world coordinates
static QgsAABB layerToWorldExtent( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &layerCrs, const QgsVector3D &mapOrigin, const QgsCoordinateReferenceSystem &mapCrs, const QgsCoordinateTransformContext &context );
static QgsAABB layerToWorldExtent( const QgsRectangle &extent, double zMin, double zMax, const QgsCoordinateReferenceSystem &layerCrs, const QgsVector3D &mapOrigin, const QgsCoordinateReferenceSystem &mapCrs, const QgsCoordinateTransformContext &context );
//! Converts axis aligned bounding box in 3D world coordinates to extent in map layer CRS
static QgsRectangle worldToLayerExtent( const QgsAABB &bbox, const QgsCoordinateReferenceSystem &layerCrs, const QgsVector3D &mapOrigin, const QgsCoordinateReferenceSystem &mapCrs, const QgsCoordinateTransformContext &context );

Expand All @@ -145,6 +145,14 @@ class _3D_EXPORT Qgs3DUtils
static QgsVector3D transformWorldCoordinates( const QgsVector3D &worldPoint1, const QgsVector3D &origin1, const QgsCoordinateReferenceSystem &crs1, const QgsVector3D &origin2, const QgsCoordinateReferenceSystem &crs2,
const QgsCoordinateTransformContext &context );

/**
* Try to estimate range of Z values used in the given vector layer and store that in zMin and zMax.
* The implementation scans a small amount of features and looks at the Z values of geometries
* (we don't need exact range, just a rough estimate is fine to know where to expect the data to be).
* For layers with geometries without Z values, the returned range will be [0, 0].
*/
static void estimateVectorLayerZRange( QgsVectorLayer *layer, double &zMin, double &zMax );

//! Returns a new 3D symbol based on given geometry type (or NULLPTR if geometry type is not supported)
static std::unique_ptr<QgsAbstract3DSymbol> symbolForGeometryType( QgsWkbTypes::GeometryType geomType );

Expand Down
11 changes: 11 additions & 0 deletions src/3d/qgsfeature3dhandler_p.cpp
Expand Up @@ -60,4 +60,15 @@ namespace Qgs3DSymbolImpl

}

void QgsFeature3DHandler::updateZRangeFromPositions( const QVector<QVector3D> &positions )
{
for ( const QVector3D &pos : positions )
{
if ( pos.y() < mZMin )
mZMin = pos.y();
if ( pos.y() > mZMax )
mZMax = pos.y();
}
}

/// @endcond
20 changes: 20 additions & 0 deletions src/3d/qgsfeature3dhandler_p.h
Expand Up @@ -104,6 +104,26 @@ class QgsFeature3DHandler
* to a 3D entity object(s) attached to the given parent.
*/
virtual void finalize( Qt3DCore::QEntity *parent, const Qgs3DRenderContext &context ) = 0;

/**
* Returns minimal Z value of the data (in world coordinates).
* \note this method should not be called before call to finalize() - it may not be initialized
*/
float zMinimum() const { return mZMin; }

/**
* Returns maximal Z value of the data (in world coordinates).
* \note this method should not be called before call to finalize() - it may not be initialized
*/
float zMaximum() const { return mZMax; }

protected:
//! updates zMinimum, zMaximum from the vector of positions in 3D world coordinates
void updateZRangeFromPositions( const QVector<QVector3D> &positions );

protected:
float mZMin = std::numeric_limits<double>::max();
float mZMax = std::numeric_limits<double>::min();
};


Expand Down
5 changes: 4 additions & 1 deletion src/3d/qgsrulebased3drenderer.cpp
Expand Up @@ -406,7 +406,10 @@ Qt3DCore::QEntity *QgsRuleBased3DRenderer::createEntity( const Qgs3DMapSettings
if ( !vl )
return nullptr;

return new QgsRuleBasedChunkedEntity( vl, tilingSettings(), mRootRule, map );
double zMin, zMax;
Qgs3DUtils::estimateVectorLayerZRange( vl, zMin, zMax );

return new QgsRuleBasedChunkedEntity( vl, zMin, zMax, tilingSettings(), mRootRule, map );
}

void QgsRuleBased3DRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
Expand Down
22 changes: 20 additions & 2 deletions src/3d/qgsrulebasedchunkloader_p.cpp
Expand Up @@ -118,9 +118,27 @@ Qt3DCore::QEntity *QgsRuleBasedChunkLoader::createEntity( Qt3DCore::QEntity *par
return new Qt3DCore::QEntity( parent ); // dummy entity
}

float zMin = std::numeric_limits<float>::max();
float zMax = std::numeric_limits<float>::min();

Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
for ( QgsFeature3DHandler *handler : mHandlers.values() )
{
handler->finalize( entity, mContext );
if ( handler->zMinimum() < zMin )
zMin = handler->zMinimum();
if ( handler->zMaximum() > zMax )
zMax = handler->zMaximum();
}

// fix the vertical range of the node from the estimated vertical range to the true range
if ( zMin != std::numeric_limits<float>::max() && zMax != std::numeric_limits<float>::min() )
{
QgsAABB box = mNode->bbox();
box.yMin = zMin;
box.yMax = zMax;
mNode->setExactBbox( box );
}

return entity;
}
Expand Down Expand Up @@ -149,8 +167,8 @@ QgsChunkLoader *QgsRuleBasedChunkLoaderFactory::createChunkLoader( QgsChunkNode

///////////////

QgsRuleBasedChunkedEntity::QgsRuleBasedChunkedEntity( QgsVectorLayer *vl, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsRuleBased3DRenderer::Rule *rootRule, const Qgs3DMapSettings &map )
: QgsChunkedEntity( Qgs3DUtils::layerToWorldExtent( vl->extent(), vl->crs(), map.origin(), map.crs(), map.transformContext() ),
QgsRuleBasedChunkedEntity::QgsRuleBasedChunkedEntity( QgsVectorLayer *vl, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsRuleBased3DRenderer::Rule *rootRule, const Qgs3DMapSettings &map )
: QgsChunkedEntity( Qgs3DUtils::layerToWorldExtent( vl->extent(), zMin, zMax, vl->crs(), map.origin(), map.crs(), map.transformContext() ),
-1, // rootError TODO: negative error should mean that the node does not contain anything
-1, // tau = max. allowed screen error. TODO: negative tau should mean that we need to go until leaves are reached
tilingSettings.zoomLevelsCount() - 1,
Expand Down
2 changes: 1 addition & 1 deletion src/3d/qgsrulebasedchunkloader_p.h
Expand Up @@ -109,7 +109,7 @@ class QgsRuleBasedChunkedEntity : public QgsChunkedEntity
Q_OBJECT
public:
//! Constructs the entity. The argument maxLevel determines how deep the tree of tiles will be
explicit QgsRuleBasedChunkedEntity( QgsVectorLayer *vl, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsRuleBased3DRenderer::Rule *rootRule, const Qgs3DMapSettings &map );
explicit QgsRuleBasedChunkedEntity( QgsVectorLayer *vl, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsRuleBased3DRenderer::Rule *rootRule, const Qgs3DMapSettings &map );

~QgsRuleBasedChunkedEntity();
};
Expand Down
6 changes: 5 additions & 1 deletion src/3d/qgsvectorlayer3drenderer.cpp
Expand Up @@ -15,6 +15,7 @@

#include "qgsvectorlayer3drenderer.h"

#include "qgs3dutils.h"
#include "qgschunkedentity_p.h"
#include "qgsline3dsymbol.h"
#include "qgspoint3dsymbol.h"
Expand Down Expand Up @@ -70,7 +71,10 @@ Qt3DCore::QEntity *QgsVectorLayer3DRenderer::createEntity( const Qgs3DMapSetting
if ( !mSymbol || !vl )
return nullptr;

return new QgsVectorLayerChunkedEntity( vl, tilingSettings(), mSymbol.get(), map );
double zMin, zMax;
Qgs3DUtils::estimateVectorLayerZRange( vl, zMin, zMax );

return new QgsVectorLayerChunkedEntity( vl, zMin, zMax, tilingSettings(), mSymbol.get(), map );
}

void QgsVectorLayer3DRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
Expand Down
4 changes: 2 additions & 2 deletions src/3d/qgsvectorlayerchunkloader_p.cpp
Expand Up @@ -153,8 +153,8 @@ QgsChunkLoader *QgsVectorLayerChunkLoaderFactory::createChunkLoader( QgsChunkNod
///////////////


QgsVectorLayerChunkedEntity::QgsVectorLayerChunkedEntity( QgsVectorLayer *vl, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsAbstract3DSymbol *symbol, const Qgs3DMapSettings &map )
: QgsChunkedEntity( Qgs3DUtils::layerToWorldExtent( vl->extent(), vl->crs(), map.origin(), map.crs(), map.transformContext() ),
QgsVectorLayerChunkedEntity::QgsVectorLayerChunkedEntity( QgsVectorLayer *vl, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsAbstract3DSymbol *symbol, const Qgs3DMapSettings &map )
: QgsChunkedEntity( Qgs3DUtils::layerToWorldExtent( vl->extent(), zMin, zMax, vl->crs(), map.origin(), map.crs(), map.transformContext() ),
-1, // rootError TODO: negative error should mean that the node does not contain anything
-1, // tau = max. allowed screen error. TODO: negative tau should mean that we need to go until leaves are reached
tilingSettings.zoomLevelsCount() - 1,
Expand Down
2 changes: 1 addition & 1 deletion src/3d/qgsvectorlayerchunkloader_p.h
Expand Up @@ -107,7 +107,7 @@ class QgsVectorLayerChunkedEntity : public QgsChunkedEntity
Q_OBJECT
public:
//! Constructs the entity. The argument maxLevel determines how deep the tree of tiles will be
explicit QgsVectorLayerChunkedEntity( QgsVectorLayer *vl, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsAbstract3DSymbol *symbol, const Qgs3DMapSettings &map );
explicit QgsVectorLayerChunkedEntity( QgsVectorLayer *vl, double zMin, double zMax, const QgsVectorLayer3DTilingSettings &tilingSettings, QgsAbstract3DSymbol *symbol, const Qgs3DMapSettings &map );

~QgsVectorLayerChunkedEntity();
};
Expand Down
9 changes: 9 additions & 0 deletions src/3d/symbols/qgsline3dsymbol_p.cpp
Expand Up @@ -159,6 +159,9 @@ void QgsBufferedLine3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, const
// create entity for selected and not selected
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

mZMin = std::min( outNormal.tessellator->zMinimum(), outSelected.tessellator->zMinimum() );
mZMax = std::max( outNormal.tessellator->zMaximum(), outSelected.tessellator->zMaximum() );
}


Expand Down Expand Up @@ -267,6 +270,9 @@ void QgsSimpleLine3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, const Qg
// create entity for selected and not selected
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

updateZRangeFromPositions( outNormal.vertices );
updateZRangeFromPositions( outSelected.vertices );
}


Expand Down Expand Up @@ -379,6 +385,9 @@ void QgsThickLine3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, const Qgs
// create entity for selected and not selected
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

updateZRangeFromPositions( outNormal.vertices );
updateZRangeFromPositions( outSelected.vertices );
}


Expand Down
24 changes: 24 additions & 0 deletions src/3d/symbols/qgspoint3dsymbol_p.cpp
Expand Up @@ -116,6 +116,14 @@ void QgsInstancedPoint3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, cons
{
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

updateZRangeFromPositions( outNormal.positions );
updateZRangeFromPositions( outSelected.positions );

// the elevation offset is applied in the vertex shader so let's account for it as well
float symbolHeight = mSymbol.transform().data()[13];
mZMin += symbolHeight;
mZMax += symbolHeight;
}

void QgsInstancedPoint3DSymbolHandler::makeEntity( Qt3DCore::QEntity *parent, const Qgs3DRenderContext &context, PointData &out, bool selected )
Expand Down Expand Up @@ -388,6 +396,14 @@ void QgsModelPoint3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, const Qg
{
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

updateZRangeFromPositions( outNormal.positions );
updateZRangeFromPositions( outSelected.positions );

// the elevation offset is applied separately in QTransform added to sub-entities
float symbolHeight = mSymbol.transform().data()[13];
mZMin += symbolHeight;
mZMax += symbolHeight;
}

void QgsModelPoint3DSymbolHandler::makeEntity( Qt3DCore::QEntity *parent, const Qgs3DRenderContext &context, PointData &out, bool selected )
Expand Down Expand Up @@ -520,6 +536,14 @@ void QgsPoint3DBillboardSymbolHandler::finalize( Qt3DCore::QEntity *parent, cons
{
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

updateZRangeFromPositions( outNormal.positions );
updateZRangeFromPositions( outSelected.positions );

// the elevation offset is applied externally through a QTransform of QEntity so let's account for it
float billboardHeight = mSymbol.transform().data()[13];
mZMin += billboardHeight;
mZMax += billboardHeight;
}

void QgsPoint3DBillboardSymbolHandler::makeEntity( Qt3DCore::QEntity *parent, const Qgs3DRenderContext &context, PointData &out, bool selected )
Expand Down
3 changes: 3 additions & 0 deletions src/3d/symbols/qgspolygon3dsymbol_p.cpp
Expand Up @@ -188,6 +188,9 @@ void QgsPolygon3DSymbolHandler::finalize( Qt3DCore::QEntity *parent, const Qgs3D
makeEntity( parent, context, outNormal, false );
makeEntity( parent, context, outSelected, true );

mZMin = std::min( outNormal.tessellator->zMinimum(), outSelected.tessellator->zMinimum() );
mZMax = std::max( outNormal.tessellator->zMaximum(), outSelected.tessellator->zMaximum() );

// add entity for edges
if ( mSymbol.edgesEnabled() && !outEdges.indexes.isEmpty() )
{
Expand Down
23 changes: 22 additions & 1 deletion src/core/qgstessellator.cpp
Expand Up @@ -431,6 +431,9 @@ void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeigh
if ( pCount == 0 )
return;

float zMin = std::numeric_limits<float>::max();
float zMax = std::numeric_limits<float>::min();

if ( pCount == 4 && polygon.numInteriorRings() == 0 )
{
// polygon is a triangle - write vertices to the output data array without triangulation
Expand All @@ -439,7 +442,13 @@ void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeigh
const double *zData = !mNoZ ? exterior->zData() : nullptr;
for ( int i = 0; i < 3; i++ )
{
mData << *xData++ - mOriginX << ( mNoZ ? 0 : *zData++ ) << - *yData++ + mOriginY;
float z = ( mNoZ ? 0 : *zData++ );
if ( z < zMin )
zMin = z;
if ( z > zMax )
zMax = z;

mData << *xData++ - mOriginX << z << - *yData++ + mOriginY;
if ( mAddNormals )
mData << pNormal.x() << pNormal.z() << - pNormal.y();
}
Expand Down Expand Up @@ -568,6 +577,11 @@ void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeigh
const double fx = ( pt.x() / scaleX ) - mOriginX + pt0.x();
const double fy = ( pt.y() / scaleY ) - mOriginY + pt0.y();
const double fz = mNoZ ? 0 : ( pt.z() + extrusionHeight + pt0.z() );
if ( fz < zMin )
zMin = fz;
if ( fz > zMax )
zMax = fz;

mData << fx << fz << -fy;
if ( mAddNormals )
mData << pNormal.x() << pNormal.z() << - pNormal.y();
Expand Down Expand Up @@ -608,7 +622,14 @@ void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeigh

for ( int i = 0; i < polygon.numInteriorRings(); ++i )
_makeWalls( *qgsgeometry_cast< const QgsLineString * >( polygon.interiorRing( i ) ), true, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY );

zMax += extrusionHeight;
}

if ( zMin < mZMin )
mZMin = zMin;
if ( zMax > mZMax )
mZMax = zMax;
}

QgsPoint getPointFromData( QVector< float >::const_iterator &it )
Expand Down

0 comments on commit 9009971

Please sign in to comment.