Skip to content
26 changes: 25 additions & 1 deletion src/core/3d/quick3dgeometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@
updateGeometry();
}

void Quick3DGeometry::setExtrusion( float extrusion )
{
extrusion = std::max( 0.0f, extrusion );
if ( qFuzzyCompare( mExtrusion, extrusion ) )
{
return;
}

mExtrusion = extrusion;
mDirty = true;
emit extrusionChanged();
updateGeometry();
}

void Quick3DGeometry::markDirtyAndUpdate()
{
mDirty = true;
Expand Down Expand Up @@ -376,8 +390,8 @@
return;
}

const int totalVertexCount = points.size() * singleSphereVertexCount;

Check warning on line 393 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 393 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]
const int totalIndexCount = points.size() * singleSphereIndexCount;

Check warning on line 394 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 394 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

QByteArray vertexData;
vertexData.resize( static_cast<qsizetype>( totalVertexCount ) * stride );
Expand Down Expand Up @@ -419,13 +433,18 @@

if ( isPolygon && mFillPolygons && path.size() >= 3 )
{
int ringSize = path.size();

Check warning on line 436 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'int' [-Wshorten-64-to-32]

Check warning on line 436 in src/core/3d/quick3dgeometry.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'int' [-Wshorten-64-to-32]
if ( ringSize > 3 && ( path.first() - path.last() ).length() < 0.001f )
{
--ringSize;
}
totalFillVertices += ringSize;
totalFillIndices += ( ringSize - 2 ) * 3;
if ( mExtrusion > 0.0f )
{
totalFillVertices += Quick3DGeometryUtils::polygonWallsVertexCount( ringSize );
totalFillIndices += Quick3DGeometryUtils::polygonWallsIndexCount( ringSize );
}
}
}

Expand Down Expand Up @@ -465,7 +484,12 @@
{
if ( path.size() >= 3 )
{
Quick3DGeometryUtils::generatePolygonFill( path, r, g, b, fillAlpha, vptr, iptr, vertexOffset, minBound, maxBound );
if ( mExtrusion > 0.0f )
{
Quick3DGeometryUtils::generatePolygonWalls( path, mExtrusion, r, g, b, a, vptr, iptr, vertexOffset, minBound, maxBound );
}
const float capAlpha = mExtrusion > 0.0f ? a : fillAlpha;
Quick3DGeometryUtils::generatePolygonFill( path, r, g, b, capAlpha, vptr, iptr, vertexOffset, minBound, maxBound, mExtrusion );
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/core/3d/quick3dgeometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class Quick3DGeometry : public QQuick3DGeometry
Q_PROPERTY( bool fillPolygons READ fillPolygons WRITE setFillPolygons NOTIFY fillPolygonsChanged )
//! How the geometry's Z values are combined with the terrain elevation
Q_PROPERTY( AltitudeClamping altitudeClamping READ altitudeClamping WRITE setAltitudeClamping NOTIFY altitudeClampingChanged )
//! Extrusion in scene units; when > 0, polygons are rendered as volumetric solids with walls and a roof cap
Q_PROPERTY( float extrusion READ extrusion WRITE setExtrusion NOTIFY extrusionChanged )

public:
explicit Quick3DGeometry( QQuick3DObject *parent = nullptr );
Expand Down Expand Up @@ -101,6 +103,9 @@ class Quick3DGeometry : public QQuick3DGeometry
AltitudeClamping altitudeClamping() const { return mAltitudeClamping; }
void setAltitudeClamping( AltitudeClamping clamping );

float extrusion() const { return mExtrusion; }
void setExtrusion( float extrusion );

signals:
void qgsGeometryChanged();
void crsChanged();
Expand All @@ -110,6 +115,7 @@ class Quick3DGeometry : public QQuick3DGeometry
void heightOffsetChanged();
void fillPolygonsChanged();
void altitudeClampingChanged();
void extrusionChanged();

private slots:
void markDirtyAndUpdate();
Expand Down Expand Up @@ -139,6 +145,7 @@ class Quick3DGeometry : public QQuick3DGeometry

float mLineWidth = 3.0f;
float mHeightOffset = 15.0f;
float mExtrusion = 0.0f;
bool mFillPolygons = true;
bool mDirty = true;
AltitudeClamping mAltitudeClamping = AltitudeClamping::Ignore;
Expand Down
61 changes: 58 additions & 3 deletions src/core/3d/quick3dgeometryutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
return false;
}

const int sz = indices.size();

Check warning on line 186 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 186 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]
for ( int i = 0; i < sz; ++i )
{
if ( i == prev || i == cur || i == next )
Expand All @@ -206,11 +206,64 @@
return true;
}

void Quick3DGeometryUtils::generatePolygonWalls( const QVector<QVector3D> &vertices,
float extrusion,
float r, float g, float b, float a,
float *&vptr, quint32 *&iptr,
quint32 &vertexOffset,
QVector3D &minBound, QVector3D &maxBound )
{
QVector<QVector3D> ring = vertices;
if ( ring.size() > 3 && ( ring.first() - ring.last() ).length() < 0.001f )
{
ring.removeLast();
}

const int n = ring.size();

Check warning on line 222 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 222 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]
if ( n < 2 )
{
return;
}

for ( int i = 0; i < n; ++i )
{
const QVector3D &edgeStart = ring[i];
const QVector3D &edgeEnd = ring[( i + 1 ) % n];
const QVector3D edgeStartTop = edgeStart + QVector3D( 0, extrusion, 0 );
const QVector3D edgeEndTop = edgeEnd + QVector3D( 0, extrusion, 0 );

const float dx = edgeEnd.x() - edgeStart.x();
const float dz = edgeEnd.z() - edgeStart.z();
const float len = std::sqrt( dx * dx + dz * dz );
const QVector3D normal = len > 1e-6f ? QVector3D( dz / len, 0.0f, -dx / len ) : QVector3D( 0.0f, 0.0f, 1.0f );

const quint32 base = vertexOffset;
// CCW winding seen from outside (outward normal): start, startTop, endTop, end
writeVertex( vptr, edgeStart, normal, r, g, b, a );
updateBounds( minBound, maxBound, edgeStart );
writeVertex( vptr, edgeStartTop, normal, r, g, b, a );
updateBounds( minBound, maxBound, edgeStartTop );
writeVertex( vptr, edgeEndTop, normal, r, g, b, a );
updateBounds( minBound, maxBound, edgeEndTop );
writeVertex( vptr, edgeEnd, normal, r, g, b, a );
updateBounds( minBound, maxBound, edgeEnd );
vertexOffset += 4;

*iptr++ = base + 0;
*iptr++ = base + 1;
*iptr++ = base + 2;
*iptr++ = base + 0;
*iptr++ = base + 2;
*iptr++ = base + 3;
}
}

void Quick3DGeometryUtils::generatePolygonFill( const QVector<QVector3D> &vertices,
float r, float g, float b, float a,
float *&vptr, quint32 *&iptr,
quint32 &vertexOffset,
QVector3D &minBound, QVector3D &maxBound )
QVector3D &minBound, QVector3D &maxBound,
float extrusion )
{
QVector<QVector3D> ring = vertices;
if ( ring.size() > 3 && ( ring.first() - ring.last() ).length() < 0.001f )
Expand All @@ -218,7 +271,7 @@
ring.removeLast();
}

const int n = ring.size();

Check warning on line 274 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 274 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]
if ( n < 3 )
{
return;
Expand All @@ -226,11 +279,13 @@

const quint32 baseVertex = vertexOffset;
const QVector3D upNormal( 0.0f, 1.0f, 0.0f );
const QVector3D lift( 0.0f, extrusion, 0.0f );

for ( int i = 0; i < n; ++i )
{
writeVertex( vptr, ring[i], upNormal, r, g, b, a );
updateBounds( minBound, maxBound, ring[i] );
const QVector3D pos = ring[i] + lift;
writeVertex( vptr, pos, upNormal, r, g, b, a );
updateBounds( minBound, maxBound, pos );
}
vertexOffset += n;

Expand Down Expand Up @@ -270,7 +325,7 @@
while ( indices.size() > 2 && safety-- > 0 )
{
bool earFound = false;
const int sz = indices.size();

Check warning on line 328 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, x64-osx, 10.15, x86_64, false)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]

Check warning on line 328 in src/core/3d/quick3dgeometryutils.cpp

View workflow job for this annotation

GitHub Actions / build (macos) (macos-15, arm64-osx, 11.0, arm64, true)

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'const int' [-Wshorten-64-to-32]
for ( int i = 0; i < sz; ++i )
{
const int prev = ( i + sz - 1 ) % sz;
Expand Down
24 changes: 23 additions & 1 deletion src/core/3d/quick3dgeometryutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,34 @@ class QFIELD_CORE_EXPORT Quick3DGeometryUtils
* fan from vertex 0 when ear clipping cannot make further progress (which
* keeps degenerate or self-touching rings from leaving uninitialised
* indices in the buffer).
*
* When \a extrusion is non-zero every fill vertex is shifted upward
* by that amount so the fill becomes a roof cap on top of extruded walls.
*/
static void generatePolygonFill( const QVector<QVector3D> &vertices,
float r, float g, float b, float a,
float *&vptr, quint32 *&iptr,
quint32 &vertexOffset,
QVector3D &minBound, QVector3D &maxBound );
QVector3D &minBound, QVector3D &maxBound,
float extrusion = 0.0f );

/**
* Generates vertical wall quads for each edge of a closed polygon ring,
* extruding from the base vertices upward by \a extrusion scene units.
* Each edge produces 4 vertices and 2 triangles (6 indices).
*/
static void generatePolygonWalls( const QVector<QVector3D> &vertices,
float extrusion,
float r, float g, float b, float a,
float *&vptr, quint32 *&iptr,
quint32 &vertexOffset,
QVector3D &minBound, QVector3D &maxBound );

//! Returns the number of vertices produced by generatePolygonWalls() for a ring with \a ringSize unique vertices.
static int polygonWallsVertexCount( int ringSize ) { return ringSize * 4; }

//! Returns the number of indices produced by generatePolygonWalls() for a ring with \a ringSize unique vertices.
static int polygonWallsIndexCount( int ringSize ) { return ringSize * 6; }

private:
//! Returns true if the triangle ( prev, cur, next ) of \a indices is an ear of \a ring on the XZ plane.
Expand Down
40 changes: 40 additions & 0 deletions src/core/3d/quick3dterrainprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,27 @@ void Quick3DTerrainProvider::calcNormalizedData()
delete terrainProvider;
}

// Reject gross low outliers (DEM spikes) as missing so they don't drag the height range down.
Comment thread
mohsenD98 marked this conversation as resolved.
const double outlierFence = Quick3DTerrainProvider::lowerOutlierFence( heights, 3.0 );
lowestHeight = std::numeric_limits<double>::max();
for ( int index = 0; index < heights.size(); ++index )
{
double &height = heights[index];
if ( std::isnan( height ) )
{
continue;
}
if ( height < outlierFence )
{
height = std::numeric_limits<double>::quiet_NaN();
missingValueIndexes << index;
}
else if ( lowestHeight > height )
{
lowestHeight = height;
}
}

if ( !missingValueIndexes.isEmpty() )
{
if ( lowestHeight == std::numeric_limits<double>::max() )
Expand All @@ -407,6 +428,25 @@ void Quick3DTerrainProvider::calcNormalizedData()
mFutureWatcher->setFuture( future );
}

double Quick3DTerrainProvider::lowerOutlierFence( QVector<double> samples, double factor )
{
samples.erase( std::remove_if( samples.begin(), samples.end(), []( double value ) { return std::isnan( value ); } ), samples.end() );
if ( samples.size() < 8 )
{
return std::numeric_limits<double>::lowest();
}

const auto quantileValue = [&samples]( double quantile ) {
const int index = qBound( 0, static_cast<int>( quantile * ( samples.size() - 1 ) ), static_cast<int>( samples.size() ) - 1 );
std::nth_element( samples.begin(), samples.begin() + index, samples.end() );
return samples[index];
};

const double firstQuartile = quantileValue( 0.25 );
const double thirdQuartile = quantileValue( 0.75 );
return firstQuartile - factor * ( thirdQuartile - firstQuartile );
}

double Quick3DTerrainProvider::sampleHeightFromTerrainProvider( double x, double y ) const
{
if ( !mTerrainProvider || !mProject )
Expand Down
3 changes: 3 additions & 0 deletions src/core/3d/quick3dterrainprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ class Quick3DTerrainProvider : public QObject
void onTerrainDataCalculated();
double sampleHeightFromTerrainProvider( double x, double y ) const;

//! Returns the Tukey lower fence ( Q1 - factor * IQR ) for the samples; heights below it are gross DEM spikes.
static double lowerOutlierFence( QVector<double> samples, double factor );

private:
QgsProject *mProject = nullptr;
QgsQuickMapSettings *mMapSettings = nullptr;
Expand Down
1 change: 1 addition & 0 deletions src/core/multifeaturelistmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class MultiFeatureListModel : public QSortFilterProxyModel
ConditionalFontItalicRole,
ConditionalFontUnderlineRole,
ConditionalFontStrikeOutRole,
ExtrusionRole,
};

explicit MultiFeatureListModel( QObject *parent = nullptr );
Expand Down
14 changes: 14 additions & 0 deletions src/core/multifeaturelistmodelbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ QHash<int, QByteArray> MultiFeatureListModelBase::roleNames() const
roleNames[MultiFeatureListModel::ConditionalFontStrikeOutRole] = "conditionalFontStrikeOut";
roleNames[MultiFeatureListModel::ConditionalFontItalicRole] = "conditionalFontItalic";
roleNames[MultiFeatureListModel::ConditionalFontBoldRole] = "conditionalFontBold";
roleNames[MultiFeatureListModel::ExtrusionRole] = "extrusion";

return roleNames;
}
Expand Down Expand Up @@ -466,6 +467,19 @@ QVariant MultiFeatureListModelBase::data( const QModelIndex &index, int role ) c

return false;
break;

case MultiFeatureListModel::ExtrusionRole:
{
if ( vlayer )
{
const QString heightField = LayerUtils::guessFriendlyHeightField( vlayer );
if ( !heightField.isEmpty() )
{
return feature->second.attribute( heightField ).toDouble();
}
}
return 0.0;
}
}

return QVariant();
Expand Down
41 changes: 41 additions & 0 deletions src/core/utils/layerutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include <qgsrasterlayer.h>
#include <qgsrasterlayerelevationproperties.h>
#include <qgssinglesymbolrenderer.h>
#include <qgsstringutils.h>
#include <qgssymbol.h>
#include <qgssymbollayer.h>
#include <qgstextbuffersettings.h>
Expand Down Expand Up @@ -551,6 +552,46 @@ bool LayerUtils::hasMValue( QgsVectorLayer *layer )
return QgsWkbTypes::hasM( layer->wkbType() );
}

QString LayerUtils::guessFriendlyHeightField( QgsVectorLayer *layer )
{
if ( !layer )
{
return QString();
Comment thread
mohsenD98 marked this conversation as resolved.
}

const QgsFields fields = layer->fields();
if ( fields.isEmpty() )
{
return QString();
}

static const QStringList sCandidates {
QStringLiteral( "extrusion" ),
QStringLiteral( "height" ),
Comment thread
mohsenD98 marked this conversation as resolved.
QStringLiteral( "hauteur" ), // French (height)
QStringLiteral( "hohe" ), // German (height)
};

for ( const QString &candidate : sCandidates )
{
for ( const QgsField &field : fields )
{
if ( !field.isNumeric() )
{
continue;
}

const QString fieldName = field.name();
if ( QgsStringUtils::unaccent( fieldName ).contains( candidate, Qt::CaseInsensitive ) )
{
return fieldName;
}
}
}

return QString();
}

QSet<QVariant> LayerUtils::uniqueValuesForVectorLayerFieldIndex( QgsVectorLayer *layer, int fieldIndex )
{
if ( !layer )
Expand Down
6 changes: 6 additions & 0 deletions src/core/utils/layerutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ class LayerUtils : public QObject
*/
Q_INVOKABLE static bool hasMValue( QgsVectorLayer *layer );

/**
* Guesses the name of the field in \a layer most likely to carry a per-feature
* height/extrusion value, or an empty string when no suitable field is found.
*/
Q_INVOKABLE static QString guessFriendlyHeightField( QgsVectorLayer *layer );

/**
* Returns a list of unique values for a given \a fieldIndex from the \a layer.
*/
Expand Down
4 changes: 3 additions & 1 deletion src/qml/3d/FeatureListSelectionHighlight3D.qml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Node {
lineWidth: featureListSelectionHighlight3D.lineWidth
heightOffset: featureListSelectionHighlight3D.heightOffset
altitudeClamping: featureListSelectionHighlight3D.altitudeClamping
extrusion: model.extrusion
color: model.featureSelected ? featureListSelectionHighlight3D.selectedColor : featureListSelectionHighlight3D.selectionModel.model.selectedCount === 0 && index === featureListSelectionHighlight3D.selectionModel.focusedItem ? featureListSelectionHighlight3D.focusedColor : featureListSelectionHighlight3D.color
}

Expand All @@ -46,7 +47,8 @@ Node {
metalness: 0.0
roughness: 1.0
vertexColorsEnabled: true
alphaMode: PrincipledMaterial.Blend
alphaMode: model.extrusion > 0 ? PrincipledMaterial.Opaque : PrincipledMaterial.Blend
depthDrawMode: Material.AlwaysDepthDraw
cullMode: PrincipledMaterial.NoCulling
}
]
Expand Down
Loading