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