diff --git a/src/3d/qgs3dutils.cpp b/src/3d/qgs3dutils.cpp index cea60bafd8d3..af0412675860 100644 --- a/src/3d/qgs3dutils.cpp +++ b/src/3d/qgs3dutils.cpp @@ -82,6 +82,30 @@ AltitudeBinding Qgs3DUtils::altBindingFromString( const QString &str ) return AltBindCentroid; } +QString Qgs3DUtils::cullingModeToString( Qt3DRender::QCullFace::CullingMode mode ) +{ + switch ( mode ) + { + case Qt3DRender::QCullFace::NoCulling: return QStringLiteral( "no-culling" ); + case Qt3DRender::QCullFace::Front: return QStringLiteral( "front" ); + case Qt3DRender::QCullFace::Back: return QStringLiteral( "back" ); + case Qt3DRender::QCullFace::FrontAndBack: return QStringLiteral( "front-and-back" ); + } + return QString(); +} + +Qt3DRender::QCullFace::CullingMode Qgs3DUtils::cullingModeFromString( const QString &str ) +{ + if ( str == QStringLiteral( "front" ) ) + return Qt3DRender::QCullFace::Front; + else if ( str == QStringLiteral( "back" ) ) + return Qt3DRender::QCullFace::Back; + else if ( str == QStringLiteral( "front-and-back" ) ) + return Qt3DRender::QCullFace::FrontAndBack; + else + return Qt3DRender::QCullFace::NoCulling; +} + void Qgs3DUtils::clampAltitudes( QgsLineString *lineString, AltitudeClamping altClamp, AltitudeBinding altBind, const QgsPoint ¢roid, float height, const Qgs3DMapSettings &map ) { diff --git a/src/3d/qgs3dutils.h b/src/3d/qgs3dutils.h index c5ccd87f6269..d06c37855eb3 100644 --- a/src/3d/qgs3dutils.h +++ b/src/3d/qgs3dutils.h @@ -22,6 +22,8 @@ class QgsPolygon; #include "qgs3dmapsettings.h" #include "qgsaabb.h" +#include + //! how to handle altitude of vector features enum AltitudeClamping { @@ -64,6 +66,11 @@ class _3D_EXPORT Qgs3DUtils //! Converts a string to a value from AltitudeBinding enum static AltitudeBinding altBindingFromString( const QString &str ); + //! Converts a value from CullingMode enum to a string + static QString cullingModeToString( Qt3DRender::QCullFace::CullingMode mode ); + //! Converts a string to a value from CullingMode enum + static Qt3DRender::QCullFace::CullingMode cullingModeFromString( const QString &str ); + //! Clamps altitude of vertices of a linestring according to the settings static void clampAltitudes( QgsLineString *lineString, AltitudeClamping altClamp, AltitudeBinding altBind, const QgsPoint ¢roid, float height, const Qgs3DMapSettings &map ); //! Clamps altitude of vertices of a polygon according to the settings diff --git a/src/3d/qgstessellatedpolygongeometry.cpp b/src/3d/qgstessellatedpolygongeometry.cpp index 7f2689400a8c..502729262284 100644 --- a/src/3d/qgstessellatedpolygongeometry.cpp +++ b/src/3d/qgstessellatedpolygongeometry.cpp @@ -58,14 +58,10 @@ QgsTessellatedPolygonGeometry::QgsTessellatedPolygonGeometry( QNode *parent ) QgsTessellatedPolygonGeometry::~QgsTessellatedPolygonGeometry() { - qDeleteAll( mPolygons ); } void QgsTessellatedPolygonGeometry::setPolygons( const QList &polygons, const QgsPointXY &origin, float extrusionHeight, const QList &extrusionHeightPerPolygon ) { - qDeleteAll( mPolygons ); - mPolygons = polygons; - QgsTessellator tessellator( origin.x(), origin.y(), mWithNormals ); for ( int i = 0; i < polygons.count(); ++i ) { @@ -74,6 +70,8 @@ void QgsTessellatedPolygonGeometry::setPolygons( const QList &poly tessellator.addPolygon( *polygon, extr ); } + qDeleteAll( polygons ); + QByteArray data( ( const char * )tessellator.data().constData(), tessellator.data().count() * sizeof( float ) ); int nVerts = data.count() / tessellator.stride(); diff --git a/src/3d/qgstessellatedpolygongeometry.h b/src/3d/qgstessellatedpolygongeometry.h index 7d64554c8e41..8c49c38668f5 100644 --- a/src/3d/qgstessellatedpolygongeometry.h +++ b/src/3d/qgstessellatedpolygongeometry.h @@ -45,7 +45,6 @@ class QgsTessellatedPolygonGeometry : public Qt3DRender::QGeometry void setPolygons( const QList &polygons, const QgsPointXY &origin, float extrusionHeight, const QList &extrusionHeightPerPolygon = QList() ); private: - QList mPolygons; Qt3DRender::QAttribute *mPositionAttribute = nullptr; Qt3DRender::QAttribute *mNormalAttribute = nullptr; diff --git a/src/3d/qgstessellator.cpp b/src/3d/qgstessellator.cpp index 9c788445dad2..0ba19fca2bae 100644 --- a/src/3d/qgstessellator.cpp +++ b/src/3d/qgstessellator.cpp @@ -38,6 +38,7 @@ static void make_quad( float x0, float y0, float z0, float x1, float y1, float z // perpendicular vector in plane to [x,y] is [-y,x] QVector3D vn( -dy, 0, dx ); + vn = -vn; vn.normalize(); // triangle 1 @@ -108,8 +109,8 @@ static void _makeWalls( const QgsCurve &ring, bool ccw, float extrusionHeight, Q ring.pointAt( is_counter_clockwise == ccw ? i : ring.numPoints() - i - 1, pt, vt ); float x0 = ptPrev.x() - originX, y0 = ptPrev.y() - originY; float x1 = pt.x() - originX, y1 = pt.y() - originY; - float z0 = ptPrev.z(); - float z1 = pt.z(); + float z0 = std::isnan( ptPrev.z() ) ? 0 : ptPrev.z(); + float z1 = std::isnan( pt.z() ) ? 0 : pt.z(); // make a quad make_quad( x0, y0, z0, x1, y1, z1, extrusionHeight, data, addNormals ); @@ -170,6 +171,7 @@ static QVector3D _calculateNormal( const QgsCurve *curve, double originX, double } QVector3D normal( nx, ny, nz ); + //normal = -normal; // TODO: some datasets seem to work better with, others without inversion normal.normalize(); return normal; } @@ -197,24 +199,21 @@ static void _normalVectorToXYVectors( const QVector3D &pNormal, QVector3D &pXVec } -static void _ringToPoly2tri( const QgsCurve *ring, const QgsPoint &ptFirst, const QMatrix4x4 &toNewBase, std::vector &polyline, QHash &zHash ) +static void _ringToPoly2tri( const QgsCurve *ring, std::vector &polyline, QHash &zHash ) { QgsVertexId::VertexType vt; QgsPoint pt; const int pCount = ring->numPoints(); - double x0 = ptFirst.x(), y0 = ptFirst.y(), z0 = ( std::isnan( ptFirst.z() ) ? 0 : ptFirst.z() ); polyline.reserve( pCount ); for ( int i = 0; i < pCount - 1; ++i ) { ring->pointAt( i, pt, vt ); - QVector4D tempPt( pt.x() - x0, pt.y() - y0, std::isnan( pt.z() ) ? 0 : pt.z() - z0, 0 ); - QVector4D newBasePt = toNewBase * tempPt; - const float x = newBasePt.x(); - const float y = newBasePt.y(); - const float z = newBasePt.z(); + const float x = pt.x(); + const float y = pt.y(); + const float z = pt.z(); const bool found = std::find_if( polyline.begin(), polyline.end(), [x, y]( p2t::Point *&p ) { return *p == p2t::Point( x, y ); } ) != polyline.end(); @@ -229,6 +228,35 @@ static void _ringToPoly2tri( const QgsCurve *ring, const QgsPoint &ptFirst, cons } } +static QgsCurve *_transform_ring_to_new_base( const QgsCurve &curve, const QgsPoint &pt0, const QMatrix4x4 *toNewBase ) +{ + int count = curve.numPoints(); + QVector pts; + pts.reserve( count ); + QgsVertexId::VertexType vt; + for ( int i = 0; i < count; ++i ) + { + QgsPoint pt; + curve.pointAt( i, pt, vt ); + QgsPoint pt2( QgsWkbTypes::PointZ, pt.x() - pt0.x(), pt.y() - pt0.y(), std::isnan( pt.z() ) ? 0 : pt.z() - pt0.z() ); + QVector4D v( pt2.x(), pt2.y(), pt2.z(), 0 ); + if ( toNewBase ) + v = toNewBase->map( v ); + pts << QgsPoint( QgsWkbTypes::PointZ, v.x(), v.y(), v.z() ); + } + return new QgsLineString( pts ); +} + + +static QgsPolygon *_transform_polygon_to_new_base( const QgsPolygon &polygon, const QgsPoint &pt0, const QMatrix4x4 *toNewBase ) +{ + QgsPolygon *p = new QgsPolygon; + p->setExteriorRing( _transform_ring_to_new_base( *polygon.exteriorRing(), pt0, toNewBase ) ); + for ( int i = 0; i < polygon.numInteriorRings(); ++i ) + p->addInteriorRing( _transform_ring_to_new_base( *polygon.interiorRing( i ), pt0, toNewBase ) ); + return p; +} + static bool _check_intersecting_rings( const QgsPolygon &polygon ) { // At this point we assume that input polygons are valid according to the OGC definition. @@ -290,47 +318,14 @@ double _minimum_distance_between_coordinates( const QgsPolygon &polygon ) void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeight ) { - if ( _minimum_distance_between_coordinates( polygon ) < 0.001 ) - { - // when the distances between coordinates of input points are very small, - // the triangulation likes to crash on numerical errors - when the distances are ~ 1e-5 - // Assuming that the coordinates should be in a projected CRS, we should be able - // to simplify geometries that may cause problems and avoid possible crashes - QgsGeometry polygonSimplified = QgsGeometry( polygon.clone() ).simplify( 0.001 ); - const QgsPolygon *polygonSimplifiedData = qgsgeometry_cast( polygonSimplified.constGet() ); - if ( _minimum_distance_between_coordinates( *polygonSimplifiedData ) < 0.001 ) - { - // Failed to fix that. It could be a really tiny geometry... or maybe they gave us - // geometry in unprojected lat/lon coordinates - QgsMessageLog::logMessage( "geometry's coordinates are too close to each other and simplification failed - skipping", "3D" ); - } - else - { - addPolygon( *polygonSimplifiedData, extrusionHeight ); - } - return; - } - - if ( !_check_intersecting_rings( polygon ) ) - { - // skip the polygon - it would cause a crash inside poly2tri library - QgsMessageLog::logMessage( "polygon rings intersect each other - skipping", "3D" ); - return; - } - const QgsCurve *exterior = polygon.exteriorRing(); - QList< std::vector > polylinesToDelete; - QHash z; - - std::vector polyline; - const QVector3D pNormal = _calculateNormal( exterior, mOriginX, mOriginY ); const int pCount = exterior->numPoints(); - // Polygon is a triangle - if ( pCount == 4 ) + if ( pCount == 4 && polygon.numInteriorRings() == 0 ) { + // polygon is a triangle - write vertices to the output data array without triangulation QgsPoint pt; QgsVertexId::VertexType vt; for ( int i = 0; i < 3; i++ ) @@ -346,88 +341,113 @@ void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeigh if ( !qgsDoubleNear( pNormal.length(), 1, 0.001 ) ) return; // this should not happen - pNormal should be normalized to unit length - QVector3D pXVector, pYVector; - _normalVectorToXYVectors( pNormal, pXVector, pYVector ); - - // so now we have three orthogonal unit vectors defining new base - // let's build transform matrix. We actually need just a 3x3 matrix, - // but Qt does not have good support for it, so using 4x4 matrix instead. - QMatrix4x4 toNewBase( - pXVector.x(), pXVector.y(), pXVector.z(), 0, - pYVector.x(), pYVector.y(), pYVector.z(), 0, - pNormal.x(), pNormal.y(), pNormal.z(), 0, - 0, 0, 0, 0 ); - - // our 3x3 matrix is orthogonal, so for inverse we only need to transpose it - QMatrix4x4 toOldBase = toNewBase.transposed(); + std::unique_ptr toNewBase, toOldBase; + if ( pNormal != QVector3D( 0, 0, 1 ) ) + { + // this is not a horizontal plane - need to reproject the polygon to a new base so that + // we can do the triangulation in a plane + + QVector3D pXVector, pYVector; + _normalVectorToXYVectors( pNormal, pXVector, pYVector ); + + // so now we have three orthogonal unit vectors defining new base + // let's build transform matrix. We actually need just a 3x3 matrix, + // but Qt does not have good support for it, so using 4x4 matrix instead. + toNewBase.reset( new QMatrix4x4( + pXVector.x(), pXVector.y(), pXVector.z(), 0, + pYVector.x(), pYVector.y(), pYVector.z(), 0, + pNormal.x(), pNormal.y(), pNormal.z(), 0, + 0, 0, 0, 0 ) ); + + // our 3x3 matrix is orthogonal, so for inverse we only need to transpose it + toOldBase.reset( new QMatrix4x4( toNewBase->transposed() ) ); + } - const QgsPoint ptFirst( exterior->startPoint() ); - _ringToPoly2tri( exterior, ptFirst, toNewBase, polyline, z ); - polylinesToDelete << polyline; + const QgsPoint ptStart( exterior->startPoint() ); + const QgsPoint pt0( QgsWkbTypes::PointZ, ptStart.x(), ptStart.y(), std::isnan( ptStart.z() ) ? 0 : ptStart.z() ); - // TODO: robustness (no nearly duplicate points, invalid geometries ...) + // subtract ptFirst from geometry for better numerical stability in triangulation + // and apply new 3D vector base if the polygon is not horizontal + std::unique_ptr polygonNew( _transform_polygon_to_new_base( polygon, pt0, toNewBase.get() ) ); - double x0 = ptFirst.x(), y0 = ptFirst.y(), z0 = ( std::isnan( ptFirst.z() ) ? 0 : ptFirst.z() ); - if ( polyline.size() == 3 && polygon.numInteriorRings() == 0 ) + if ( _minimum_distance_between_coordinates( *polygonNew ) < 0.001 ) { - for ( std::vector::iterator it = polyline.begin(); it != polyline.end(); it++ ) + // when the distances between coordinates of input points are very small, + // the triangulation likes to crash on numerical errors - when the distances are ~ 1e-5 + // Assuming that the coordinates should be in a projected CRS, we should be able + // to simplify geometries that may cause problems and avoid possible crashes + QgsGeometry polygonSimplified = QgsGeometry( polygonNew->clone() ).simplify( 0.001 ); + const QgsPolygon *polygonSimplifiedData = qgsgeometry_cast( polygonSimplified.constGet() ); + if ( _minimum_distance_between_coordinates( *polygonSimplifiedData ) < 0.001 ) + { + // Failed to fix that. It could be a really tiny geometry... or maybe they gave us + // geometry in unprojected lat/lon coordinates + QgsMessageLog::logMessage( "geometry's coordinates are too close to each other and simplification failed - skipping", "3D" ); + return; + } + else { - p2t::Point *p = *it; - QVector4D ptInNewBase( p->x, p->y, z[p], 0 ); - QVector4D nPoint = toOldBase * ptInNewBase; - const double fx = nPoint.x() - mOriginX + x0; - const double fy = nPoint.y() - mOriginY + y0; - const double fz = nPoint.z() + extrusionHeight + z0; - mData << fx << fz << -fy; - if ( mAddNormals ) - mData << pNormal.x() << pNormal.z() << - pNormal.y(); + polygonNew.reset( polygonSimplifiedData->clone() ); } } - else if ( polyline.size() >= 3 ) + + if ( !_check_intersecting_rings( *polygonNew.get() ) ) { - p2t::CDT *cdt = new p2t::CDT( polyline ); + // skip the polygon - it would cause a crash inside poly2tri library + QgsMessageLog::logMessage( "polygon rings intersect each other - skipping", "3D" ); + return; + } - // polygon holes - for ( int i = 0; i < polygon.numInteriorRings(); ++i ) - { - std::vector holePolyline; - const QgsCurve *hole = polygon.interiorRing( i ); + QList< std::vector > polylinesToDelete; + QHash z; - _ringToPoly2tri( hole, ptFirst, toNewBase, holePolyline, z ); + // polygon exterior + std::vector polyline; + _ringToPoly2tri( polygonNew->exteriorRing(), polyline, z ); + polylinesToDelete << polyline; - cdt->AddHole( holePolyline ); - polylinesToDelete << holePolyline; - } + std::unique_ptr cdt( new p2t::CDT( polyline ) ); - try - { - cdt->Triangulate(); + // polygon holes + for ( int i = 0; i < polygonNew->numInteriorRings(); ++i ) + { + std::vector holePolyline; + const QgsCurve *hole = polygonNew->interiorRing( i ); + + _ringToPoly2tri( hole, holePolyline, z ); + + cdt->AddHole( holePolyline ); + polylinesToDelete << holePolyline; + } + + // run triangulation and write vertices to the output data array + try + { + cdt->Triangulate(); - std::vector triangles = cdt->GetTriangles(); + std::vector triangles = cdt->GetTriangles(); - for ( size_t i = 0; i < triangles.size(); ++i ) + for ( size_t i = 0; i < triangles.size(); ++i ) + { + p2t::Triangle *t = triangles[i]; + for ( int j = 0; j < 3; ++j ) { - p2t::Triangle *t = triangles[i]; - for ( int j = 0; j < 3; ++j ) - { - p2t::Point *p = t->GetPoint( j ); - QVector4D ptInNewBase( p->x, p->y, z[p], 0 ); - QVector4D nPoint = toOldBase * ptInNewBase; - const double fx = nPoint.x() - mOriginX + x0; - const double fy = nPoint.y() - mOriginY + y0; - const double fz = nPoint.z() + extrusionHeight + z0; - mData << fx << fz << -fy; - if ( mAddNormals ) - mData << pNormal.x() << pNormal.z() << - pNormal.y(); - } + p2t::Point *p = t->GetPoint( j ); + QVector4D pt( p->x, p->y, z[p], 0 ); + if ( toOldBase ) + pt = *toOldBase * pt; + const double fx = pt.x() - mOriginX + pt0.x(); + const double fy = pt.y() - mOriginY + pt0.y(); + const double fz = pt.z() + extrusionHeight + pt0.z(); + mData << fx << fz << -fy; + if ( mAddNormals ) + mData << pNormal.x() << pNormal.z() << - pNormal.y(); } } - catch ( ... ) - { - QgsMessageLog::logMessage( "Triangulation failed. Skipping polygon...", "3D" ); - } - - delete cdt; + } + catch ( ... ) + { + QgsMessageLog::logMessage( "Triangulation failed. Skipping polygon...", "3D" ); } for ( int i = 0; i < polylinesToDelete.count(); ++i ) diff --git a/src/3d/symbols/qgspolygon3dsymbol.cpp b/src/3d/symbols/qgspolygon3dsymbol.cpp index 6057074144db..3ddca251d10a 100644 --- a/src/3d/symbols/qgspolygon3dsymbol.cpp +++ b/src/3d/symbols/qgspolygon3dsymbol.cpp @@ -31,6 +31,7 @@ void QgsPolygon3DSymbol::writeXml( QDomElement &elem, const QgsReadWriteContext elemDataProperties.setAttribute( QStringLiteral( "alt-binding" ), Qgs3DUtils::altBindingToString( mAltBinding ) ); elemDataProperties.setAttribute( QStringLiteral( "height" ), mHeight ); elemDataProperties.setAttribute( QStringLiteral( "extrusion-height" ), mExtrusionHeight ); + elemDataProperties.setAttribute( QStringLiteral( "culling-mode" ), Qgs3DUtils::cullingModeToString( mCullingMode ) ); elem.appendChild( elemDataProperties ); QDomElement elemMaterial = doc.createElement( QStringLiteral( "material" ) ); @@ -51,6 +52,7 @@ void QgsPolygon3DSymbol::readXml( const QDomElement &elem, const QgsReadWriteCon mAltBinding = Qgs3DUtils::altBindingFromString( elemDataProperties.attribute( QStringLiteral( "alt-binding" ) ) ); mHeight = elemDataProperties.attribute( QStringLiteral( "height" ) ).toFloat(); mExtrusionHeight = elemDataProperties.attribute( QStringLiteral( "extrusion-height" ) ).toFloat(); + mCullingMode = Qgs3DUtils::cullingModeFromString( elemDataProperties.attribute( QStringLiteral( "culling-mode" ) ) ); QDomElement elemMaterial = elem.firstChildElement( QStringLiteral( "material" ) ); mMaterial.readXml( elemMaterial ); diff --git a/src/3d/symbols/qgspolygon3dsymbol.h b/src/3d/symbols/qgspolygon3dsymbol.h index 4f1512d93bbf..0b32d3514760 100644 --- a/src/3d/symbols/qgspolygon3dsymbol.h +++ b/src/3d/symbols/qgspolygon3dsymbol.h @@ -22,6 +22,7 @@ #include "qgsphongmaterialsettings.h" #include "qgs3dutils.h" +#include /** * \ingroup 3d @@ -65,6 +66,11 @@ class _3D_EXPORT QgsPolygon3DSymbol : public QgsAbstract3DSymbol //! Sets material used for shading of the symbol void setMaterial( const QgsPhongMaterialSettings &material ) { mMaterial = material; } + //! Returns front/back culling mode + Qt3DRender::QCullFace::CullingMode cullingMode() const { return mCullingMode; } + //! Sets front/back culling mode + void setCullingMode( Qt3DRender::QCullFace::CullingMode mode ) { mCullingMode = mode; } + private: //! how to handle altitude of vector features AltitudeClamping mAltClamping = AltClampRelative; @@ -74,6 +80,7 @@ class _3D_EXPORT QgsPolygon3DSymbol : public QgsAbstract3DSymbol float mHeight = 0.0f; //!< Base height of polygons float mExtrusionHeight = 0.0f; //!< How much to extrude (0 means no walls) QgsPhongMaterialSettings mMaterial; //!< Defines appearance of objects + Qt3DRender::QCullFace::CullingMode mCullingMode = Qt3DRender::QCullFace::NoCulling; //!< Front/back culling mode }; diff --git a/src/3d/symbols/qgspolygon3dsymbol_p.cpp b/src/3d/symbols/qgspolygon3dsymbol_p.cpp index 0ac9395fbdff..2204a12f74ec 100644 --- a/src/3d/symbols/qgspolygon3dsymbol_p.cpp +++ b/src/3d/symbols/qgspolygon3dsymbol_p.cpp @@ -21,6 +21,9 @@ #include "qgs3dutils.h" #include +#include +#include +#include #include "qgsvectorlayer.h" #include "qgsmultipolygon.h" @@ -103,9 +106,24 @@ void QgsPolygon3DSymbolEntity::addEntityForNotSelectedPolygons( const Qgs3DMapSe entity->setParent( this ); } + Qt3DExtras::QPhongMaterial *QgsPolygon3DSymbolEntity::material( const QgsPolygon3DSymbol &symbol ) const { Qt3DExtras::QPhongMaterial *material = new Qt3DExtras::QPhongMaterial; + + // front/back side culling + auto techniques = material->effect()->techniques(); + for ( auto tit = techniques.constBegin(); tit != techniques.constEnd(); ++tit ) + { + auto renderPasses = ( *tit )->renderPasses(); + for ( auto rpit = renderPasses.begin(); rpit != renderPasses.end(); ++rpit ) + { + Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace; + cullFace->setMode( symbol.cullingMode() ); + ( *rpit )->addRenderState( cullFace ); + } + } + material->setAmbient( symbol.material().ambient() ); material->setDiffuse( symbol.material().diffuse() ); material->setSpecular( symbol.material().specular() ); diff --git a/src/app/3d/qgspolygon3dsymbolwidget.cpp b/src/app/3d/qgspolygon3dsymbolwidget.cpp index e615a8b4d859..29ed24033014 100644 --- a/src/app/3d/qgspolygon3dsymbolwidget.cpp +++ b/src/app/3d/qgspolygon3dsymbolwidget.cpp @@ -31,17 +31,45 @@ QgsPolygon3DSymbolWidget::QgsPolygon3DSymbolWidget( QWidget *parent ) connect( spinExtrusion, static_cast( &QDoubleSpinBox::valueChanged ), this, &QgsPolygon3DSymbolWidget::changed ); connect( cboAltClamping, static_cast( &QComboBox::currentIndexChanged ), this, &QgsPolygon3DSymbolWidget::changed ); connect( cboAltBinding, static_cast( &QComboBox::currentIndexChanged ), this, &QgsPolygon3DSymbolWidget::changed ); + connect( cboCullingMode, static_cast( &QComboBox::currentIndexChanged ), this, &QgsPolygon3DSymbolWidget::changed ); connect( widgetMaterial, &QgsPhongMaterialWidget::changed, this, &QgsPolygon3DSymbolWidget::changed ); connect( btnHeightDD, &QgsPropertyOverrideButton::changed, this, &QgsPolygon3DSymbolWidget::changed ); connect( btnExtrusionDD, &QgsPropertyOverrideButton::changed, this, &QgsPolygon3DSymbolWidget::changed ); } + +static int _cullingModeToIndex( Qt3DRender::QCullFace::CullingMode mode ) +{ + switch ( mode ) + { + case Qt3DRender::QCullFace::NoCulling: return 0; + case Qt3DRender::QCullFace::Front: return 1; + case Qt3DRender::QCullFace::Back: return 2; + case Qt3DRender::QCullFace::FrontAndBack: return 3; + } + return 0; +} + +static Qt3DRender::QCullFace::CullingMode _cullingModeFromIndex( int index ) +{ + switch ( index ) + { + case 0: return Qt3DRender::QCullFace::NoCulling; + case 1: return Qt3DRender::QCullFace::Front; + case 2: return Qt3DRender::QCullFace::Back; + case 3: return Qt3DRender::QCullFace::FrontAndBack; + } + return Qt3DRender::QCullFace::NoCulling; +} + + void QgsPolygon3DSymbolWidget::setSymbol( const QgsPolygon3DSymbol &symbol, QgsVectorLayer *layer ) { spinHeight->setValue( symbol.height() ); spinExtrusion->setValue( symbol.extrusionHeight() ); cboAltClamping->setCurrentIndex( ( int ) symbol.altitudeClamping() ); cboAltBinding->setCurrentIndex( ( int ) symbol.altitudeBinding() ); + cboCullingMode->setCurrentIndex( _cullingModeToIndex( symbol.cullingMode() ) ); widgetMaterial->setMaterial( symbol.material() ); btnHeightDD->init( QgsAbstract3DSymbol::PropertyHeight, symbol.dataDefinedProperties(), QgsAbstract3DSymbol::propertyDefinitions(), layer, true ); @@ -55,6 +83,7 @@ QgsPolygon3DSymbol QgsPolygon3DSymbolWidget::symbol() const sym.setExtrusionHeight( spinExtrusion->value() ); sym.setAltitudeClamping( ( AltitudeClamping ) cboAltClamping->currentIndex() ); sym.setAltitudeBinding( ( AltitudeBinding ) cboAltBinding->currentIndex() ); + sym.setCullingMode( _cullingModeFromIndex( cboCullingMode->currentIndex() ) ); sym.setMaterial( widgetMaterial->material() ); QgsPropertyCollection ddp; diff --git a/src/ui/3d/polygon3dsymbolwidget.ui b/src/ui/3d/polygon3dsymbolwidget.ui index 1954f98c249b..5ddee6e73352 100644 --- a/src/ui/3d/polygon3dsymbolwidget.ui +++ b/src/ui/3d/polygon3dsymbolwidget.ui @@ -6,7 +6,7 @@ 0 0 - 538 + 561 452 @@ -14,10 +14,10 @@ Form - - + + - Height + Altitude Clamping @@ -31,13 +31,6 @@ - - - - ... - - - @@ -45,24 +38,48 @@ - - - - 99999.000000000000000 + + + + ... - - + + ... - - + + - Altitude Clamping + Height + + + + + + + + Vertex + + + + + Centroid + + + + + + + + + + + Qt::Horizontal @@ -92,30 +109,39 @@ - - + + + + 99999.000000000000000 + + + + + + + Culling Mode + + + + + - Vertex + No culling - Centroid + Front + + + + + Back - - - - Qt::Horizontal - - - - - - diff --git a/tests/src/3d/testqgstessellator.cpp b/tests/src/3d/testqgstessellator.cpp index 9a77edf5ba40..87271904d19a 100644 --- a/tests/src/3d/testqgstessellator.cpp +++ b/tests/src/3d/testqgstessellator.cpp @@ -22,6 +22,11 @@ #include "qgstessellator.h" #include "qgsmultipolygon.h" +static bool qgsVectorNear( const QVector3D &v1, const QVector3D &v2, double eps ) +{ + return qgsDoubleNear( v1.x(), v2.x(), eps ) && qgsDoubleNear( v1.y(), v2.y(), eps ) && qgsDoubleNear( v1.z(), v2.z(), eps ); +} + /** * Simple structure to record an expected triangle from tessellator. * Triangle vertices are expected to be in counter-clockwise order. @@ -55,8 +60,13 @@ struct TriangleCoords bool operator==( const TriangleCoords &other ) const { // TODO: allow that the two triangles have coordinates shifted (but still in the same order) - return pts[0] == other.pts[0] && pts[1] == other.pts[1] && pts[2] == other.pts[2] && - normals[0] == other.normals[0] && normals[1] == other.normals[1] && normals[2] == other.normals[2]; + const double eps = 1e-6; + return qgsVectorNear( pts[0], other.pts[0], eps ) && + qgsVectorNear( pts[1], other.pts[1], eps ) && + qgsVectorNear( pts[2], other.pts[2], eps ) && + qgsVectorNear( normals[0], other.normals[0], eps ) && + qgsVectorNear( normals[1], other.normals[1], eps ) && + qgsVectorNear( normals[2], other.normals[2], eps ); } bool operator!=( const TriangleCoords &other ) const @@ -175,6 +185,42 @@ void TestQgsTessellator::testBasic() void TestQgsTessellator::testWalls() { + QgsPolygon rect; + rect.fromWkt( "POLYGON((0 0, 3 0, 3 2, 0 2, 0 0))" ); + + QVector3D zPos( 0, 0, 1 ); + QVector3D xPos( 1, 0, 0 ); + QVector3D yPos( 0, 1, 0 ); + QVector3D xNeg( -1, 0, 0 ); + QVector3D yNeg( 0, -1, 0 ); + + QList tcRect; + tcRect << TriangleCoords( QVector3D( 0, 2, 1 ), QVector3D( 3, 0, 1 ), QVector3D( 3, 2, 1 ), zPos, zPos, zPos ); + tcRect << TriangleCoords( QVector3D( 0, 2, 1 ), QVector3D( 0, 0, 1 ), QVector3D( 3, 0, 1 ), zPos, zPos, zPos ); + tcRect << TriangleCoords( QVector3D( 0, 0, 1 ), QVector3D( 0, 2, 1 ), QVector3D( 0, 0, 0 ), xNeg, xNeg, xNeg ); + tcRect << TriangleCoords( QVector3D( 0, 0, 0 ), QVector3D( 0, 2, 1 ), QVector3D( 0, 2, 0 ), xNeg, xNeg, xNeg ); + tcRect << TriangleCoords( QVector3D( 0, 2, 1 ), QVector3D( 3, 2, 1 ), QVector3D( 0, 2, 0 ), yPos, yPos, yPos ); + tcRect << TriangleCoords( QVector3D( 0, 2, 0 ), QVector3D( 3, 2, 1 ), QVector3D( 3, 2, 0 ), yPos, yPos, yPos ); + tcRect << TriangleCoords( QVector3D( 3, 2, 1 ), QVector3D( 3, 0, 1 ), QVector3D( 3, 2, 0 ), xPos, xPos, xPos ); + tcRect << TriangleCoords( QVector3D( 3, 2, 0 ), QVector3D( 3, 0, 1 ), QVector3D( 3, 0, 0 ), xPos, xPos, xPos ); + tcRect << TriangleCoords( QVector3D( 3, 0, 1 ), QVector3D( 0, 0, 1 ), QVector3D( 3, 0, 0 ), yNeg, yNeg, yNeg ); + tcRect << TriangleCoords( QVector3D( 3, 0, 0 ), QVector3D( 0, 0, 1 ), QVector3D( 0, 0, 0 ), yNeg, yNeg, yNeg ); + + QgsTessellator tRect( 0, 0, true ); + tRect.addPolygon( rect, 1 ); + QVERIFY( checkTriangleOutput( tRect.data(), true, tcRect ) ); + + // try to extrude a polygon with reverse (clock-wise) order of vertices and check it is still fine + + QgsPolygon rectRev; + rectRev.fromWkt( "POLYGON((0 0, 0 2, 3 2, 3 0, 0 0))" ); + + QgsTessellator tRectRev( 0, 0, true ); + tRectRev.addPolygon( rectRev, 1 ); + QVERIFY( checkTriangleOutput( tRectRev.data(), true, tcRect ) ); + + // this is a more complicated polygon with Z coordinates where the "roof" is not in one plane + QgsPolygon polygonZ; polygonZ.fromWkt( "POLYGONZ((1 1 1, 2 1 2, 3 2 3, 1 2 4, 1 1 1))" ); @@ -216,6 +262,19 @@ void TestQgsTessellator::asMultiPolygon() void TestQgsTessellator::testBadCoordinates() { + // check with a vertical "wall" polygon - if the Z coordinates are ignored, + // the polygon may be incorrectly considered as having close/repeated coordinates + QList tcZ; + tcZ << TriangleCoords( QVector3D( 1, 2, 2 ), QVector3D( 2, 1, 1 ), QVector3D( 2, 1, 2 ) ); + tcZ << TriangleCoords( QVector3D( 1, 2, 2 ), QVector3D( 1, 2, 1 ), QVector3D( 2, 1, 1 ) ); + + QgsPolygon polygonZ; + polygonZ.fromWkt( "POLYGONZ((1 2 1, 2 1 1, 2 1 2, 1 2 2, 1 2 1))" ); + + QgsTessellator tZ( 0, 0, false ); + tZ.addPolygon( polygonZ, 0 ); + QVERIFY( checkTriangleOutput( tZ.data(), false, tcZ ) ); + // triangulation would crash for me with this polygon if there is no simplification // to remove the coordinates that are very close to each other QgsPolygon polygon;