QGIS API Documentation 3.99.0-Master (f78f5286a64)
qgstiledscenelayerrenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstiledscenelayerrenderer.cpp
3 --------------------
4 begin : June 2023
5 copyright : (C) 2023 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18
20#include "qgscurve.h"
21#include "qgslogger.h"
24#include "qgstiledscenelayer.h"
25#include "qgsfeedback.h"
26#include "qgsmapclippingutils.h"
27#include "qgsrendercontext.h"
29#include "qgstiledscenetile.h"
31#include "qgsgltfutils.h"
32#include "qgscesiumutils.h"
33#include "qgscurvepolygon.h"
34#include "qgstextrenderer.h"
35#include "qgsruntimeprofiler.h"
36#include "qgsapplication.h"
37
38#include <QMatrix4x4>
39#include <qglobal.h>
40
41#define TINYGLTF_NO_STB_IMAGE // we use QImage-based reading of images
42#define TINYGLTF_NO_STB_IMAGE_WRITE // we don't need writing of images
43#include "tiny_gltf.h"
44
46 : QgsMapLayerRenderer( layer->id(), &context )
47 , mLayerName( layer->name() )
48 , mFeedback( new QgsFeedback )
49 , mEnableProfile( context.flags() & Qgis::RenderContextFlag::RecordProfile )
50{
51 // We must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering
52 // or use some locking to prevent read/write from multiple threads
53 if ( !layer->dataProvider() || !layer->renderer() )
54 return;
55
56 QElapsedTimer timer;
57 timer.start();
58
59 mRenderer.reset( layer->renderer()->clone() );
60
61 mSceneCrs = layer->dataProvider()->sceneCrs();
62
64 mLayerBoundingVolume = layer->dataProvider()->boundingVolume();
65
66 mIndex = layer->dataProvider()->index();
67 mRenderTileBorders = mRenderer->isTileBorderRenderingEnabled();
68
69 mReadyToCompose = false;
70
71 mPreparationTime = timer.elapsed();
72}
73
75
77{
78 if ( !mIndex.isValid() )
79 return false;
80
81 std::unique_ptr< QgsScopedRuntimeProfile > profile;
82 if ( mEnableProfile )
83 {
84 profile = std::make_unique< QgsScopedRuntimeProfile >( mLayerName, QStringLiteral( "rendering" ), layerId() );
85 if ( mPreparationTime > 0 )
86 QgsApplication::profiler()->record( QObject::tr( "Create renderer" ), mPreparationTime / 1000.0, QStringLiteral( "rendering" ) );
87 }
88
89 std::unique_ptr< QgsScopedRuntimeProfile > preparingProfile;
90 if ( mEnableProfile )
91 {
92 preparingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Preparing render" ), QStringLiteral( "rendering" ) );
93 }
94
96 QgsTiledSceneRenderContext context( *rc, mFeedback.get() );
97
98 // Set up the render configuration options
99 QPainter *painter = rc->painter();
100
101 QgsScopedQPainterState painterState( painter );
102 rc->setPainterFlagsUsingContext( painter );
103
104 if ( !mClippingRegions.empty() )
105 {
106 bool needsPainterClipPath = false;
107 const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *rc, Qgis::LayerType::VectorTile, needsPainterClipPath );
108 if ( needsPainterClipPath )
109 rc->painter()->setClipPath( path, Qt::IntersectClip );
110 }
111
112 mElapsedTimer.start();
113
114 mSceneToMapTransform = QgsCoordinateTransform( mSceneCrs, rc->coordinateTransform().destinationCrs(), rc->transformContext() );
115
116 mRenderer->startRender( context );
117
118 preparingProfile.reset();
119 std::unique_ptr< QgsScopedRuntimeProfile > renderingProfile;
120 if ( mEnableProfile )
121 {
122 renderingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Rendering" ), QStringLiteral( "rendering" ) );
123 }
124
125 const bool result = renderTiles( context );
126 mRenderer->stopRender( context );
127 mReadyToCompose = true;
128
129 return result;
130}
131
133{
134 // we want to show temporary incremental renders we retrieve each tile in the scene, as this can be slow and
135 // we need to show the user that some activity is happening here.
136 // But we can't render the final layer result incrementally, as we need to collect ALL the content from the
137 // scene before we can sort it by z order and avoid random z-order stacking artifacts!
138 // So we request here a preview render image for the temporary incremental updates:
140}
141
143{
144 return mRenderer ? ( mRenderer->flags() & Qgis::TiledSceneRendererFlag::ForceRasterRender ) : false;
145}
146
147QgsTiledSceneRequest QgsTiledSceneLayerRenderer::createBaseRequest()
148{
149 const QgsRenderContext *context = renderContext();
150 const QgsRectangle mapExtent = context->mapExtent();
151
152 // calculate maximum screen error in METERS
153 const double maximumErrorPixels = context->convertToPainterUnits( mRenderer->maximumScreenError(), mRenderer->maximumScreenErrorUnit() );
154 // calculate width in meters across one pixel in the middle of the map
155 const double mapYCenter = 0.5 * ( mapExtent.yMinimum() + mapExtent.yMaximum() );
156 const double mapXCenter = 0.5 * ( mapExtent.xMinimum() + mapExtent.xMaximum() );
157 const double onePixelDistanceX = ( mapExtent.xMaximum() - mapExtent.xMinimum() ) / context->outputSize().width();
158 double mapMetersPerPixel = 0;
159 try
160 {
161 mapMetersPerPixel = context->distanceArea().measureLine(
162 QgsPointXY( mapXCenter, mapYCenter ),
163 QgsPointXY( mapXCenter + onePixelDistanceX, mapYCenter )
164 );
165 }
166 catch ( QgsCsException & )
167 {
168 // TODO report errors to user
169 QgsDebugError( QStringLiteral( "An error occurred while calculating length" ) );
170 }
171
172 const double maximumErrorInMeters = maximumErrorPixels * mapMetersPerPixel;
173
174 QgsTiledSceneRequest request;
175 request.setFeedback( feedback() );
176
177 // TODO what z range makes sense here??
178 const QVector< QgsVector3D > corners = QgsBox3D( mapExtent, -10000, 10000 ).corners();
179 QVector< double > x;
180 x.reserve( 8 );
181 QVector< double > y;
182 y.reserve( 8 );
183 QVector< double > z;
184 z.reserve( 8 );
185 for ( int i = 0; i < 8; ++i )
186 {
187 const QgsVector3D &corner = corners[i];
188 x.append( corner.x() );
189 y.append( corner.y() );
190 z.append( corner.z() );
191 }
192 mSceneToMapTransform.transformInPlace( x, y, z, Qgis::TransformDirection::Reverse );
193
194 const auto minMaxX = std::minmax_element( x.constBegin(), x.constEnd() );
195 const auto minMaxY = std::minmax_element( y.constBegin(), y.constEnd() );
196 const auto minMaxZ = std::minmax_element( z.constBegin(), z.constEnd() );
197 request.setFilterBox(
198 QgsOrientedBox3D::fromBox3D( QgsBox3D( *minMaxX.first, *minMaxY.first, *minMaxZ.first, *minMaxX.second, *minMaxY.second, *minMaxZ.second ) )
199 );
200
201 request.setRequiredGeometricError( maximumErrorInMeters );
202
203 return request;
204}
205
206bool QgsTiledSceneLayerRenderer::renderTiles( QgsTiledSceneRenderContext &context )
207{
208 const QgsRectangle mapExtent = context.renderContext().mapExtent();
209 auto tileIsVisibleInMap = [mapExtent, this]( const QgsTiledSceneTile & tile )->bool
210 {
211 // the trip from map CRS to scene CRS will have expanded out the bounding volumes for the tile request, so
212 // we want to cull any tiles which we've been given which don't actually intersect our visible map extent
213 // when we transform them back into the destination map CRS.
214 // This potentially saves us requesting data for tiles which aren't actually visible in the map.
215 const QgsGeometry tileGeometry( tile.boundingVolume().as2DGeometry( mSceneToMapTransform ) );
216 return tileGeometry.intersects( mapExtent );
217 };
218
219 QgsTiledSceneRequest request = createBaseRequest();
220 QVector< long long > tileIds = mIndex.getTiles( request );
221 while ( !tileIds.empty() )
222 {
223 if ( feedback() && feedback()->isCanceled() )
224 return false;
225
226 const long long tileId = tileIds.first();
227 tileIds.pop_front();
228
229 const QgsTiledSceneTile tile = mIndex.getTile( tileId );
230 if ( !tile.isValid() || !tileIsVisibleInMap( tile ) )
231 continue;
232
233 switch ( mIndex.childAvailability( tileId ) )
234 {
237 {
238 renderTile( tile, context );
239 break;
240 }
241
243 {
244 if ( mIndex.fetchHierarchy( tileId, feedback() ) )
245 {
246 request.setParentTileId( tileId );
247 const QVector< long long > newTileIdsToRender = mIndex.getTiles( request );
248 tileIds.append( newTileIdsToRender );
249
250 // do we still need to render the parent? Depends on the parent's refinement process...
251 const QgsTiledSceneTile tile = mIndex.getTile( tileId );
252 if ( tile.isValid() )
253 {
254 switch ( tile.refinementProcess() )
255 {
257 break;
259 renderTile( tile, context );
260 break;
261 }
262 }
263 }
264 break;
265 }
266 }
267 }
268 if ( feedback() && feedback()->isCanceled() )
269 return false;
270
271 const bool needsTextures = mRenderer->flags() & Qgis::TiledSceneRendererFlag::RequiresTextures;
272
273 std::sort( mPrimitiveData.begin(), mPrimitiveData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
274 {
275 // this isn't an exact science ;)
276 if ( qgsDoubleNear( a.z, b.z, 0.001 ) )
277 {
278 // for overlapping lines/triangles, ensure the line is drawn over the triangle
279 if ( a.type == PrimitiveType::Line )
280 return false;
281 else if ( b.type == PrimitiveType::Line )
282 return true;
283 }
284 return a.z < b.z;
285 } );
286 for ( const PrimitiveData &data : std::as_const( mPrimitiveData ) )
287 {
288 switch ( data.type )
289 {
290 case PrimitiveType::Line:
291 mRenderer->renderLine( context, data.coordinates );
292 break;
293
294 case PrimitiveType::Triangle:
295 if ( needsTextures )
296 {
297 context.setTextureImage( mTextures.value( data.textureId ) );
298 context.setTextureCoordinates( data.textureCoords[0], data.textureCoords[1],
299 data.textureCoords[2], data.textureCoords[3],
300 data.textureCoords[4], data.textureCoords[5] );
301 }
302 mRenderer->renderTriangle( context, data.coordinates );
303 break;
304 }
305 }
306
307 if ( mRenderTileBorders )
308 {
309 QPainter *painter = renderContext()->painter();
310 for ( const TileDetails &tile : std::as_const( mTileDetails ) )
311 {
312 QPen pen;
313 QBrush brush;
314 if ( tile.hasContent )
315 {
316 brush = QBrush( QColor( 0, 0, 255, 10 ) );
317 pen = QPen( QColor( 0, 0, 255, 150 ) );
318 }
319 else
320 {
321 brush = QBrush( QColor( 255, 0, 255, 10 ) );
322 pen = QPen( QColor( 255, 0, 255, 150 ) );
323 }
324 pen.setWidth( 2 );
325 painter->setPen( pen );
326 painter->setBrush( brush );
327 painter->drawPolygon( tile.boundary );
328#if 1
329 QgsTextFormat format;
330 format.setColor( QColor( 255, 0, 0 ) );
331 format.buffer().setEnabled( true );
332
333 QgsTextRenderer::drawText( QRectF( QPoint( 0, 0 ), renderContext()->outputSize() ).intersected( tile.boundary.boundingRect() ),
335 *renderContext(), format, true, Qgis::TextVerticalAlignment::VerticalCenter );
336#endif
337 }
338 }
339
340 return true;
341}
342
343void QgsTiledSceneLayerRenderer::renderTile( const QgsTiledSceneTile &tile, QgsTiledSceneRenderContext &context )
344{
345 const bool hasContent = renderTileContent( tile, context );
346
347 if ( mRenderTileBorders )
348 {
349 const QgsTiledSceneBoundingVolume &volume = tile.boundingVolume();
350 try
351 {
352 std::unique_ptr< QgsAbstractGeometry > volumeGeometry( volume.as2DGeometry( mSceneToMapTransform ) );
353 if ( QgsCurvePolygon *polygon = qgsgeometry_cast< QgsCurvePolygon * >( volumeGeometry.get() ) )
354 {
355 QPolygonF volumePolygon = polygon->exteriorRing()->asQPolygonF( );
356
357 // remove non-finite points, e.g. infinite or NaN points caused by reprojecting errors
358 volumePolygon.erase( std::remove_if( volumePolygon.begin(), volumePolygon.end(),
359 []( const QPointF point )
360 {
361 return !std::isfinite( point.x() ) || !std::isfinite( point.y() );
362 } ), volumePolygon.end() );
363
364 QPointF *ptr = volumePolygon.data();
365 for ( int i = 0; i < volumePolygon.size(); ++i, ++ptr )
366 {
367 renderContext()->mapToPixel().transformInPlace( ptr->rx(), ptr->ry() );
368 }
369
370 TileDetails details;
371 details.boundary = volumePolygon;
372 details.hasContent = hasContent;
373 details.id = QString::number( tile.id() );
374 mTileDetails.append( details );
375 }
376 }
377 catch ( QgsCsException & )
378 {
379 QgsDebugError( QStringLiteral( "Error transforming bounding volume" ) );
380 }
381 }
382}
383
384bool QgsTiledSceneLayerRenderer::renderTileContent( const QgsTiledSceneTile &tile, QgsTiledSceneRenderContext &context )
385{
386 const QString contentUri = tile.resources().value( QStringLiteral( "content" ) ).toString();
387 if ( contentUri.isEmpty() )
388 return false;
389
390 const QByteArray tileContent = mIndex.retrieveContent( contentUri, feedback() );
391 // When the operation is canceled, retrieveContent() will silently return an empty array
392 if ( feedback()->isCanceled() )
393 return false;
394
395 tinygltf::Model model;
396 QgsVector3D centerOffset;
397 mCurrentModelId++;
398 // TODO: Somehow de-hardcode this switch?
399 const auto &format = tile.metadata().value( QStringLiteral( "contentFormat" ) ).value<QString>();
400 if ( format == QLatin1String( "quantizedmesh" ) )
401 {
402 try
403 {
404 QgsQuantizedMeshTile qmTile( tileContent );
405 qmTile.removeDegenerateTriangles();
406 model = qmTile.toGltf();
407 }
409 {
410 QgsDebugError( QStringLiteral( "Failed to parse tile from '%1'" ).arg( contentUri ) );
411 return false;
412 }
413 }
414 else if ( format == QLatin1String( "cesiumtiles" ) )
415 {
417 if ( content.gltf.isEmpty() )
418 {
419 return false;
420 }
421 centerOffset = content.rtcCenter;
422
423 QString gltfErrors;
424 QString gltfWarnings;
425 const bool res = QgsGltfUtils::loadGltfModel( content.gltf, model,
426 &gltfErrors, &gltfWarnings );
427 if ( !gltfErrors.isEmpty() )
428 {
429 if ( !mErrors.contains( gltfErrors ) )
430 mErrors.append( gltfErrors );
431 QgsDebugError( QStringLiteral( "Error raised reading %1: %2" )
432 .arg( contentUri, gltfErrors ) );
433 }
434 if ( !gltfWarnings.isEmpty() )
435 {
436 QgsDebugError( QStringLiteral( "Warnings raised reading %1: %2" )
437 .arg( contentUri, gltfWarnings ) );
438 }
439 if ( !res ) return false;
440 }
441 else
442 return false;
443
444 const QgsVector3D tileTranslationEcef =
445 centerOffset +
446 QgsGltfUtils::extractTileTranslation(
447 model,
448 static_cast<Qgis::Axis>( tile.metadata()
449 .value( QStringLiteral( "gltfUpAxis" ),
450 static_cast<int>( Qgis::Axis::Y ) )
451 .toInt() ) );
452
453 bool sceneOk = false;
454 const std::size_t sceneIndex =
455 QgsGltfUtils::sourceSceneForModel( model, sceneOk );
456 if ( !sceneOk )
457 {
458 const QString error = QObject::tr( "No scenes found in model" );
459 mErrors.append( error );
461 QStringLiteral( "Error raised reading %1: %2" ).arg( contentUri, error ) );
462 }
463 else
464 {
465 const tinygltf::Scene &scene = model.scenes[sceneIndex];
466
467 std::function< void( int nodeIndex, const QMatrix4x4 &transform ) > traverseNode;
468 traverseNode = [&model, &context, &tileTranslationEcef, &tile, &contentUri, &traverseNode, this]( int nodeIndex, const QMatrix4x4 & parentTransform )
469 {
470 const tinygltf::Node &gltfNode = model.nodes[nodeIndex];
471 std::unique_ptr< QMatrix4x4 > gltfLocalTransform = QgsGltfUtils::parseNodeTransform( gltfNode );
472
473 if ( !parentTransform.isIdentity() )
474 {
475 if ( gltfLocalTransform )
476 *gltfLocalTransform = parentTransform * *gltfLocalTransform;
477 else
478 {
479 gltfLocalTransform.reset( new QMatrix4x4( parentTransform ) );
480 }
481 }
482
483 if ( gltfNode.mesh >= 0 )
484 {
485 const tinygltf::Mesh &mesh = model.meshes[gltfNode.mesh];
486
487 for ( const tinygltf::Primitive &primitive : mesh.primitives )
488 {
489 if ( context.renderContext().renderingStopped() )
490 break;
491
492 renderPrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform.get(), contentUri, context );
493 }
494 }
495
496 for ( int childNode : gltfNode.children )
497 {
498 traverseNode( childNode, gltfLocalTransform ? *gltfLocalTransform : QMatrix4x4() );
499 }
500 };
501
502 for ( int nodeIndex : scene.nodes )
503 {
504 traverseNode( nodeIndex, QMatrix4x4() );
505 }
506 }
507 return true;
508}
509
510void QgsTiledSceneLayerRenderer::renderPrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &contentUri, QgsTiledSceneRenderContext &context )
511{
512 switch ( primitive.mode )
513 {
514 case TINYGLTF_MODE_TRIANGLES:
515 if ( mRenderer->flags() & Qgis::TiledSceneRendererFlag::RendersTriangles )
516 renderTrianglePrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform, contentUri, context );
517 break;
518
519 case TINYGLTF_MODE_LINE:
520 if ( mRenderer->flags() & Qgis::TiledSceneRendererFlag::RendersLines )
521 renderLinePrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform, contentUri, context );
522 return;
523
524 case TINYGLTF_MODE_POINTS:
525 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_POINTS ) )
526 {
527 mErrors << QObject::tr( "Point objects in tiled scenes are not supported" );
528 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_POINTS );
529 }
530 return;
531
532 case TINYGLTF_MODE_LINE_LOOP:
533 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_LINE_LOOP ) )
534 {
535 mErrors << QObject::tr( "Line loops in tiled scenes are not supported" );
536 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_LINE_LOOP );
537 }
538 return;
539
540 case TINYGLTF_MODE_LINE_STRIP:
541 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_LINE_STRIP ) )
542 {
543 mErrors << QObject::tr( "Line strips in tiled scenes are not supported" );
544 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_LINE_STRIP );
545 }
546 return;
547
548 case TINYGLTF_MODE_TRIANGLE_STRIP:
549 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_TRIANGLE_STRIP ) )
550 {
551 mErrors << QObject::tr( "Triangular strips in tiled scenes are not supported" );
552 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_TRIANGLE_STRIP );
553 }
554 return;
555
556 case TINYGLTF_MODE_TRIANGLE_FAN:
557 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_TRIANGLE_FAN ) )
558 {
559 mErrors << QObject::tr( "Triangular fans in tiled scenes are not supported" );
560 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_TRIANGLE_FAN );
561 }
562 return;
563
564 default:
565 if ( !mWarnedPrimitiveTypes.contains( primitive.mode ) )
566 {
567 mErrors << QObject::tr( "Primitive type %1 in tiled scenes are not supported" ).arg( primitive.mode );
568 mWarnedPrimitiveTypes.insert( primitive.mode );
569 }
570 return;
571 }
572}
573
574void QgsTiledSceneLayerRenderer::renderTrianglePrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &contentUri, QgsTiledSceneRenderContext &context )
575{
576 auto posIt = primitive.attributes.find( "POSITION" );
577 if ( posIt == primitive.attributes.end() )
578 {
579 mErrors << QObject::tr( "Could not find POSITION attribute for primitive" );
580 return;
581 }
582 int positionAccessorIndex = posIt->second;
583
584 QVector< double > x;
585 QVector< double > y;
586 QVector< double > z;
587 QgsGltfUtils::accessorToMapCoordinates(
588 model, positionAccessorIndex, tile.transform() ? *tile.transform() : QgsMatrix4x4(),
589 &mSceneToMapTransform,
590 tileTranslationEcef,
591 gltfLocalTransform,
592 static_cast< Qgis::Axis >( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Y ) ).toInt() ),
593 x, y, z
594 );
595
597
598 const bool needsTextures = mRenderer->flags() & Qgis::TiledSceneRendererFlag::RequiresTextures;
599
600 QImage textureImage;
601 QVector< float > texturePointX;
602 QVector< float > texturePointY;
603 QPair< int, int > textureId{ -1, -1 };
604 if ( needsTextures && primitive.material != -1 )
605 {
606 const tinygltf::Material &material = model.materials[primitive.material];
607 const tinygltf::PbrMetallicRoughness &pbr = material.pbrMetallicRoughness;
608
609 if ( pbr.baseColorTexture.index >= 0
610 && static_cast< int >( model.textures.size() ) > pbr.baseColorTexture.index )
611 {
612 const tinygltf::Texture &tex = model.textures[pbr.baseColorTexture.index];
613
614 // Source can be undefined if texture is provided by an extension
615 if ( tex.source >= 0 )
616 {
617 switch ( QgsGltfUtils::imageResourceType( model, tex.source ) )
618 {
619 case QgsGltfUtils::ResourceType::Embedded:
620 textureImage = QgsGltfUtils::extractEmbeddedImage( model, tex.source );
621 break;
622
623 case QgsGltfUtils::ResourceType::Linked:
624 {
625 const QString linkedPath = QgsGltfUtils::linkedImagePath( model, tex.source );
626 const QString textureUri = QUrl( contentUri ).resolved( linkedPath ).toString();
627 const QByteArray rep = mIndex.retrieveContent( textureUri, feedback() );
628 if ( !rep.isEmpty() )
629 {
630 textureImage = QImage::fromData( rep );
631 }
632 break;
633 }
634 }
635 }
636
637 if ( !textureImage.isNull() )
638 {
639 auto texIt = primitive.attributes.find( "TEXCOORD_0" );
640 if ( texIt != primitive.attributes.end() )
641 {
642 QgsGltfUtils::extractTextureCoordinates(
643 model, texIt->second, texturePointX, texturePointY
644 );
645 }
646
647 textureId = qMakePair( mCurrentModelId, pbr.baseColorTexture.index );
648 }
649 }
650 else if ( qgsDoubleNear( pbr.baseColorFactor[3], 0 ) )
651 {
652 // transparent primitive, skip
653 return;
654 }
655 }
656
657 const QRect outputRect = QRect( QPoint( 0, 0 ), context.renderContext().outputSize() );
658 auto needTriangle = [&outputRect]( const QPolygonF & triangle ) -> bool
659 {
660 return triangle.boundingRect().intersects( outputRect );
661 };
662
663 const bool useTexture = !textureImage.isNull();
664 bool hasStoredTexture = false;
665
666 QVector< PrimitiveData > thisTileTriangleData;
667
668 if ( primitive.indices == -1 )
669 {
670 Q_ASSERT( x.size() % 3 == 0 );
671
672 thisTileTriangleData.reserve( x.size() );
673 for ( int i = 0; i < x.size(); i += 3 )
674 {
675 if ( context.renderContext().renderingStopped() )
676 break;
677
678 PrimitiveData data;
679 data.type = PrimitiveType::Triangle;
680 data.textureId = textureId;
681 if ( useTexture )
682 {
683 data.textureCoords[0] = texturePointX[i];
684 data.textureCoords[1] = texturePointY[i];
685 data.textureCoords[2] = texturePointX[i + 1];
686 data.textureCoords[3] = texturePointY[i + 1];
687 data.textureCoords[4] = texturePointX[i + 2];
688 data.textureCoords[5] = texturePointY[i + 2];
689 }
690 data.coordinates = QVector<QPointF> { QPointF( x[i], y[i] ), QPointF( x[i + 1], y[i + 1] ), QPointF( x[i + 2], y[i + 2] ), QPointF( x[i], y[i] ) };
691 data.z = ( z[i] + z[i + 1] + z[i + 2] ) / 3;
692 if ( needTriangle( data.coordinates ) )
693 {
694 thisTileTriangleData.push_back( data );
695 if ( !hasStoredTexture && !textureImage.isNull() )
696 {
697 // have to make an explicit .copy() here, as we don't necessarily own the image data
698 mTextures.insert( textureId, textureImage.copy() );
699 hasStoredTexture = true;
700 }
701 }
702 }
703 }
704 else
705 {
706 const tinygltf::Accessor &primitiveAccessor = model.accessors[primitive.indices];
707 const tinygltf::BufferView &bvPrimitive = model.bufferViews[primitiveAccessor.bufferView];
708 const tinygltf::Buffer &bPrimitive = model.buffers[bvPrimitive.buffer];
709
710 Q_ASSERT( ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT
711 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT
712 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
713 && primitiveAccessor.type == TINYGLTF_TYPE_SCALAR );
714
715 const char *primitivePtr = reinterpret_cast< const char * >( bPrimitive.data.data() ) + bvPrimitive.byteOffset + primitiveAccessor.byteOffset;
716
717 thisTileTriangleData.reserve( primitiveAccessor.count / 3 );
718 for ( std::size_t i = 0; i < primitiveAccessor.count / 3; i++ )
719 {
720 if ( context.renderContext().renderingStopped() )
721 break;
722
723 unsigned int index1 = 0;
724 unsigned int index2 = 0;
725 unsigned int index3 = 0;
726
727 PrimitiveData data;
728 data.type = PrimitiveType::Triangle;
729 data.textureId = textureId;
730
731 if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT )
732 {
733 const unsigned short *usPtrPrimitive = reinterpret_cast< const unsigned short * >( primitivePtr );
734 if ( bvPrimitive.byteStride )
735 primitivePtr += bvPrimitive.byteStride;
736 else
737 primitivePtr += 3 * sizeof( unsigned short );
738
739 index1 = usPtrPrimitive[0];
740 index2 = usPtrPrimitive[1];
741 index3 = usPtrPrimitive[2];
742 }
743 else if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
744 {
745 const unsigned char *usPtrPrimitive = reinterpret_cast< const unsigned char * >( primitivePtr );
746 if ( bvPrimitive.byteStride )
747 primitivePtr += bvPrimitive.byteStride;
748 else
749 primitivePtr += 3 * sizeof( unsigned char );
750
751 index1 = usPtrPrimitive[0];
752 index2 = usPtrPrimitive[1];
753 index3 = usPtrPrimitive[2];
754 }
755 else
756 {
757 const unsigned int *uintPtrPrimitive = reinterpret_cast< const unsigned int * >( primitivePtr );
758 if ( bvPrimitive.byteStride )
759 primitivePtr += bvPrimitive.byteStride;
760 else
761 primitivePtr += 3 * sizeof( unsigned int );
762
763 index1 = uintPtrPrimitive[0];
764 index2 = uintPtrPrimitive[1];
765 index3 = uintPtrPrimitive[2];
766 }
767
768 if ( useTexture )
769 {
770 data.textureCoords[0] = texturePointX[index1];
771 data.textureCoords[1] = texturePointY[index1];
772 data.textureCoords[2] = texturePointX[index2];
773 data.textureCoords[3] = texturePointY[index2];
774 data.textureCoords[4] = texturePointX[index3];
775 data.textureCoords[5] = texturePointY[index3];
776 }
777
778 data.coordinates = { QVector<QPointF>{ QPointF( x[index1], y[index1] ), QPointF( x[index2], y[index2] ), QPointF( x[index3], y[index3] ), QPointF( x[index1], y[index1] ) } };
779 data.z = ( z[index1] + z[index2] + z[index3] ) / 3;
780 if ( needTriangle( data.coordinates ) )
781 {
782 thisTileTriangleData.push_back( data );
783 if ( !hasStoredTexture && !textureImage.isNull() )
784 {
785 // have to make an explicit .copy() here, as we don't necessarily own the image data
786 mTextures.insert( textureId, textureImage.copy() );
787 hasStoredTexture = true;
788 }
789 }
790 }
791 }
792
793 if ( context.renderContext().previewRenderPainter() )
794 {
795 // swap out the destination painter for the preview render painter, and render
796 // the triangles from this tile in a sorted order
797 QPainter *finalPainter = context.renderContext().painter();
799
800 std::sort( thisTileTriangleData.begin(), thisTileTriangleData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
801 {
802 return a.z < b.z;
803 } );
804
805 for ( const PrimitiveData &data : std::as_const( thisTileTriangleData ) )
806 {
807 if ( useTexture && data.textureId.first >= 0 )
808 {
809 context.setTextureImage( mTextures.value( data.textureId ) );
810 context.setTextureCoordinates( data.textureCoords[0], data.textureCoords[1],
811 data.textureCoords[2], data.textureCoords[3],
812 data.textureCoords[4], data.textureCoords[5] );
813 }
814 mRenderer->renderTriangle( context, data.coordinates );
815 }
816 context.renderContext().setPainter( finalPainter );
817 }
818
819 mPrimitiveData.append( thisTileTriangleData );
820
821 // as soon as first tile is rendered, we can start showing layer updates. But we still delay
822 // this by e.g. 3 seconds before we start forcing progressive updates, so that we don't show the unsorted
823 // z triangle render if the overall layer render only takes a second or so.
824 if ( mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
825 {
826 mReadyToCompose = true;
827 }
828}
829
830void QgsTiledSceneLayerRenderer::renderLinePrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &, QgsTiledSceneRenderContext &context )
831{
832 auto posIt = primitive.attributes.find( "POSITION" );
833 if ( posIt == primitive.attributes.end() )
834 {
835 mErrors << QObject::tr( "Could not find POSITION attribute for primitive" );
836 return;
837 }
838 int positionAccessorIndex = posIt->second;
839
840 QVector< double > x;
841 QVector< double > y;
842 QVector< double > z;
843 QgsGltfUtils::accessorToMapCoordinates(
844 model, positionAccessorIndex, tile.transform() ? *tile.transform() : QgsMatrix4x4(),
845 &mSceneToMapTransform,
846 tileTranslationEcef,
847 gltfLocalTransform,
848 static_cast< Qgis::Axis >( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Y ) ).toInt() ),
849 x, y, z
850 );
851
853
854 const QRect outputRect = QRect( QPoint( 0, 0 ), context.renderContext().outputSize() );
855 auto needLine = [&outputRect]( const QPolygonF & line ) -> bool
856 {
857 return line.boundingRect().intersects( outputRect );
858 };
859
860 QVector< PrimitiveData > thisTileLineData;
861
862 if ( primitive.indices == -1 )
863 {
864 Q_ASSERT( x.size() % 2 == 0 );
865
866 thisTileLineData.reserve( x.size() );
867 for ( int i = 0; i < x.size(); i += 2 )
868 {
869 if ( context.renderContext().renderingStopped() )
870 break;
871
872 PrimitiveData data;
873 data.type = PrimitiveType::Line;
874 data.coordinates = QVector<QPointF> { QPointF( x[i], y[i] ), QPointF( x[i + 1], y[i + 1] ) };
875 // note -- we take the maximum z here, as we'd ideally like lines to be placed over similarish z valued triangles
876 data.z = std::max( z[i], z[i + 1] );
877 if ( needLine( data.coordinates ) )
878 {
879 thisTileLineData.push_back( data );
880 }
881 }
882 }
883 else
884 {
885 const tinygltf::Accessor &primitiveAccessor = model.accessors[primitive.indices];
886 const tinygltf::BufferView &bvPrimitive = model.bufferViews[primitiveAccessor.bufferView];
887 const tinygltf::Buffer &bPrimitive = model.buffers[bvPrimitive.buffer];
888
889 Q_ASSERT( ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT
890 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT
891 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
892 && primitiveAccessor.type == TINYGLTF_TYPE_SCALAR );
893
894 const char *primitivePtr = reinterpret_cast< const char * >( bPrimitive.data.data() ) + bvPrimitive.byteOffset + primitiveAccessor.byteOffset;
895
896 thisTileLineData.reserve( primitiveAccessor.count / 2 );
897 for ( std::size_t i = 0; i < primitiveAccessor.count / 2; i++ )
898 {
899 if ( context.renderContext().renderingStopped() )
900 break;
901
902 unsigned int index1 = 0;
903 unsigned int index2 = 0;
904
905 PrimitiveData data;
906 data.type = PrimitiveType::Line;
907
908 if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT )
909 {
910 const unsigned short *usPtrPrimitive = reinterpret_cast< const unsigned short * >( primitivePtr );
911 if ( bvPrimitive.byteStride )
912 primitivePtr += bvPrimitive.byteStride;
913 else
914 primitivePtr += 2 * sizeof( unsigned short );
915
916 index1 = usPtrPrimitive[0];
917 index2 = usPtrPrimitive[1];
918 }
919 else if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
920 {
921 const unsigned char *usPtrPrimitive = reinterpret_cast< const unsigned char * >( primitivePtr );
922 if ( bvPrimitive.byteStride )
923 primitivePtr += bvPrimitive.byteStride;
924 else
925 primitivePtr += 2 * sizeof( unsigned char );
926
927 index1 = usPtrPrimitive[0];
928 index2 = usPtrPrimitive[1];
929 }
930 else
931 {
932 const unsigned int *uintPtrPrimitive = reinterpret_cast< const unsigned int * >( primitivePtr );
933 if ( bvPrimitive.byteStride )
934 primitivePtr += bvPrimitive.byteStride;
935 else
936 primitivePtr += 2 * sizeof( unsigned int );
937
938 index1 = uintPtrPrimitive[0];
939 index2 = uintPtrPrimitive[1];
940 }
941
942 data.coordinates = { QVector<QPointF>{ QPointF( x[index1], y[index1] ), QPointF( x[index2], y[index2] ) } };
943 // note -- we take the maximum z here, as we'd ideally like lines to be placed over similarish z valued triangles
944 data.z = std::max( z[index1], z[index2] );
945 if ( needLine( data.coordinates ) )
946 {
947 thisTileLineData.push_back( data );
948 }
949 }
950 }
951
952 if ( context.renderContext().previewRenderPainter() )
953 {
954 // swap out the destination painter for the preview render painter, and render
955 // the triangles from this tile in a sorted order
956 QPainter *finalPainter = context.renderContext().painter();
958
959 std::sort( thisTileLineData.begin(), thisTileLineData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
960 {
961 return a.z < b.z;
962 } );
963
964 for ( const PrimitiveData &data : std::as_const( thisTileLineData ) )
965 {
966 mRenderer->renderLine( context, data.coordinates );
967 }
968 context.renderContext().setPainter( finalPainter );
969 }
970
971 mPrimitiveData.append( thisTileLineData );
972
973 // as soon as first tile is rendered, we can start showing layer updates. But we still delay
974 // this by e.g. 3 seconds before we start forcing progressive updates, so that we don't show the unsorted
975 // z primitive render if the overall layer render only takes a second or so.
976 if ( mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
977 {
978 mReadyToCompose = true;
979 }
980}
Provides global constants and enumerations for use throughout the application.
Definition qgis.h:54
QFlags< MapLayerRendererFlag > MapLayerRendererFlags
Flags which control how map layer renderers behave.
Definition qgis.h:2746
@ RendersLines
Renderer can render line primitives.
@ RequiresTextures
Renderer requires textures.
@ ForceRasterRender
Layer should always be rendered as a raster image.
@ RendersTriangles
Renderer can render triangle primitives.
@ Available
Tile children are already available.
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
@ NoChildren
Tile is known to have no children.
@ VectorTile
Vector tile layer. Added in QGIS 3.14.
@ RenderPartialOutputOverPreviousCachedImage
When rendering temporary in-progress preview renders, these preview renders can be drawn over any pre...
@ RenderPartialOutputs
The renderer benefits from rendering temporary in-progress preview renders. These are temporary resul...
@ VerticalCenter
Center align.
Axis
Cartesian axes.
Definition qgis.h:2401
@ Y
Y-axis.
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
@ Replacement
When tile is refined then its children should be used in place of itself.
@ Reverse
Reverse/inverse transform (from destination to source)
static QgsRuntimeProfiler * profiler()
Returns the application runtime profiler.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:43
QVector< QgsVector3D > corners() const
Returns an array of all box corners as 3D vectors.
Definition qgsbox3d.cpp:359
static TileContents extractGltfFromTileContent(const QByteArray &tileContent)
Parses tile content.
Handles coordinate transforms between two coordinate systems.
void transformInPlace(double &x, double &y, double &z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms an array of x, y and z double coordinates in place, from the source CRS to the destination...
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
Custom exception class for Coordinate Reference System related exceptions.
Curve polygon geometry type.
double measureLine(const QVector< QgsPointXY > &points) const
Measures the length of a line with multiple segments.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
A geometry is the spatial representation of a feature.
static QPainterPath calculatePainterClipRegion(const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, Qgis::LayerType layerType, bool &shouldClip)
Returns a QPainterPath representing the intersection of clipping regions from context which should be...
static QList< QgsMapClippingRegion > collectClippingRegionsForLayer(const QgsRenderContext &context, const QgsMapLayer *layer)
Collects the list of map clipping regions from a context which apply to a map layer.
Base class for utility classes that encapsulate information necessary for rendering of map layers.
bool mReadyToCompose
The flag must be set to false in renderer's constructor if wants to use the smarter map redraws funct...
static constexpr int MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE
Maximum time (in ms) to allow display of a previously cached preview image while rendering layers,...
QString layerId() const
Gets access to the ID of the layer rendered by this class.
QgsRenderContext * renderContext()
Returns the render context associated with the renderer.
void transformInPlace(double &x, double &y) const
Transforms map coordinates to device coordinates.
A simple 4x4 matrix implementation useful for transformation in 3D space.
static QgsOrientedBox3D fromBox3D(const QgsBox3D &box)
Constructs an oriented box from an axis-aligned bounding box.
Represents a 2D point.
Definition qgspointxy.h:60
Exception thrown on failure to parse Quantized Mesh tile (malformed data).
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
Contains information about the context of a rendering operation.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
const QgsDistanceArea & distanceArea() const
A general purpose distance and area calculator, capable of performing ellipsoid based calculations.
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
QgsCoordinateTransformContext transformContext() const
Returns the context's coordinate transform context, which stores various information regarding which ...
QSize outputSize() const
Returns the size of the resulting rendered image, in pixels.
QgsRectangle mapExtent() const
Returns the original extent of the map being rendered.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
QPainter * previewRenderPainter()
Returns the const destination QPainter for temporary in-progress preview renders.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
void record(const QString &name, double time, const QString &group="startup", const QString &id=QString())
Manually adds a profile event with the given name and total time (in seconds).
Scoped object for saving and restoring a QPainter object's state.
void setEnabled(bool enabled)
Sets whether the text buffer will be drawn.
Container for all settings relating to text rendering.
void setColor(const QColor &color)
Sets the color that text will be rendered in.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
Represents a bounding volume for a tiled scene.
QgsAbstractGeometry * as2DGeometry(const QgsCoordinateTransform &transform=QgsCoordinateTransform(), Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Returns a new geometry representing the 2-dimensional X/Y center slice of the volume.
virtual const QgsCoordinateReferenceSystem sceneCrs() const =0
Returns the original coordinate reference system for the tiled scene data.
virtual QgsTiledSceneIndex index() const =0
Returns the provider's tile index.
virtual const QgsTiledSceneBoundingVolume & boundingVolume() const =0
Returns the bounding volume for the data provider.
Qgis::TileChildrenAvailability childAvailability(long long id) const
Returns the availability for a tile's children.
QByteArray retrieveContent(const QString &uri, QgsFeedback *feedback=nullptr)
Retrieves index content for the specified uri.
bool fetchHierarchy(long long id, QgsFeedback *feedback=nullptr)
Populates the tile with the given id by fetching any sub datasets attached to the tile.
QgsTiledSceneTile getTile(long long id)
Returns the tile with matching id, or an invalid tile if the matching tile is not available.
QVector< long long > getTiles(const QgsTiledSceneRequest &request)
Returns the list of tile IDs which match the given request.
bool isValid() const
Returns true if the index is valid.
bool forceRasterRender() const override
Returns true if the renderer must be rendered to a raster paint device (e.g.
QgsTiledSceneLayerRenderer(QgsTiledSceneLayer *layer, QgsRenderContext &context)
Ctor.
QgsFeedback * feedback() const override
Access to feedback object of the layer renderer (may be nullptr)
bool render() override
Do the rendering (based on data stored in the class).
Qgis::MapLayerRendererFlags flags() const override
Returns flags which control how the map layer rendering behaves.
Represents a map layer supporting display of tiled scene objects.
QgsTiledSceneDataProvider * dataProvider() override
Returns the layer's data provider, it may be nullptr.
QgsTiledSceneRenderer * renderer()
Returns the 2D renderer for the tiled scene.
Encapsulates the render context for a 2D tiled scene rendering operation.
void setTextureImage(const QImage &image)
Sets the current texture image.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
void setTextureCoordinates(float textureX1, float textureY1, float textureX2, float textureY2, float textureX3, float textureY3)
Sets the current texture coordinates.
virtual QgsTiledSceneRenderer * clone() const =0
Create a deep copy of this renderer.
Tiled scene data request.
void setParentTileId(long long id)
Sets the parent tile id, if filtering is to be limited to children of a specific tile.
void setFilterBox(const QgsOrientedBox3D &box)
Sets the box from which data will be taken.
void setFeedback(QgsFeedback *feedback)
Attach a feedback object that can be queried regularly by the request to check if it should be cancel...
void setRequiredGeometricError(double error)
Sets the required geometric error threshold for the returned tiles, in meters.
Represents an individual tile from a tiled scene data source.
bool isValid() const
Returns true if the tile is a valid tile (i.e.
Qgis::TileRefinementProcess refinementProcess() const
Returns the tile's refinement process.
QVariantMap resources() const
Returns the resources attached to the tile.
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
QVariantMap metadata() const
Returns additional metadata attached to the tile.
long long id() const
Returns the tile's unique ID.
const QgsMatrix4x4 * transform() const
Returns the tile's transform.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:30
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:49
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:51
double x() const
Returns X coordinate.
Definition qgsvector3d.h:47
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:6392
#define QgsDebugError(str)
Definition qgslogger.h:40
Encapsulates the contents of a 3D tile.
QgsVector3D rtcCenter
Center position of relative-to-center coordinates (when used)
QByteArray gltf
GLTF binary content.