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