QGIS API Documentation 3.43.0-Master (13ba59af3e9)
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 the middle of the map
155 const double mapYCenter = 0.5 * ( mapExtent.yMinimum() + mapExtent.yMaximum() );
156 double mapWidthMeters = 0;
157 try
158 {
159 mapWidthMeters = context->distanceArea().measureLine(
160 QgsPointXY( mapExtent.xMinimum(), mapYCenter ),
161 QgsPointXY( mapExtent.xMaximum(), mapYCenter )
162 );
163 }
164 catch ( QgsCsException & )
165 {
166 // TODO report errors to user
167 QgsDebugError( QStringLiteral( "An error occurred while calculating length" ) );
168 }
169
170 const double mapMetersPerPixel = mapWidthMeters / context->outputSize().width();
171 const double maximumErrorInMeters = maximumErrorPixels * mapMetersPerPixel;
172
173 QgsTiledSceneRequest request;
174 request.setFeedback( feedback() );
175
176 // TODO what z range makes sense here??
177 const QVector< QgsVector3D > corners = QgsBox3D( mapExtent, -10000, 10000 ).corners();
178 QVector< double > x;
179 x.reserve( 8 );
180 QVector< double > y;
181 y.reserve( 8 );
182 QVector< double > z;
183 z.reserve( 8 );
184 for ( int i = 0; i < 8; ++i )
185 {
186 const QgsVector3D &corner = corners[i];
187 x.append( corner.x() );
188 y.append( corner.y() );
189 z.append( corner.z() );
190 }
191 mSceneToMapTransform.transformInPlace( x, y, z, Qgis::TransformDirection::Reverse );
192
193 const auto minMaxX = std::minmax_element( x.constBegin(), x.constEnd() );
194 const auto minMaxY = std::minmax_element( y.constBegin(), y.constEnd() );
195 const auto minMaxZ = std::minmax_element( z.constBegin(), z.constEnd() );
196 request.setFilterBox(
197 QgsOrientedBox3D::fromBox3D( QgsBox3D( *minMaxX.first, *minMaxY.first, *minMaxZ.first, *minMaxX.second, *minMaxY.second, *minMaxZ.second ) )
198 );
199
200 request.setRequiredGeometricError( maximumErrorInMeters );
201
202 return request;
203}
204
205bool QgsTiledSceneLayerRenderer::renderTiles( QgsTiledSceneRenderContext &context )
206{
207 const QgsRectangle mapExtent = context.renderContext().mapExtent();
208 auto tileIsVisibleInMap = [mapExtent, this]( const QgsTiledSceneTile & tile )->bool
209 {
210 // the trip from map CRS to scene CRS will have expanded out the bounding volumes for the tile request, so
211 // we want to cull any tiles which we've been given which don't actually intersect our visible map extent
212 // when we transform them back into the destination map CRS.
213 // This potentially saves us requesting data for tiles which aren't actually visible in the map.
214 const QgsGeometry tileGeometry( tile.boundingVolume().as2DGeometry( mSceneToMapTransform ) );
215 return tileGeometry.intersects( mapExtent );
216 };
217
218 QgsTiledSceneRequest request = createBaseRequest();
219 QVector< long long > tileIds = mIndex.getTiles( request );
220 while ( !tileIds.empty() )
221 {
222 if ( feedback() && feedback()->isCanceled() )
223 return false;
224
225 const long long tileId = tileIds.first();
226 tileIds.pop_front();
227
228 const QgsTiledSceneTile tile = mIndex.getTile( tileId );
229 if ( !tile.isValid() || !tileIsVisibleInMap( tile ) )
230 continue;
231
232 switch ( mIndex.childAvailability( tileId ) )
233 {
236 {
237 renderTile( tile, context );
238 break;
239 }
240
242 {
243 if ( mIndex.fetchHierarchy( tileId, feedback() ) )
244 {
245 request.setParentTileId( tileId );
246 const QVector< long long > newTileIdsToRender = mIndex.getTiles( request );
247 tileIds.append( newTileIdsToRender );
248
249 // do we still need to render the parent? Depends on the parent's refinement process...
250 const QgsTiledSceneTile tile = mIndex.getTile( tileId );
251 if ( tile.isValid() )
252 {
253 switch ( tile.refinementProcess() )
254 {
256 break;
258 renderTile( tile, context );
259 break;
260 }
261 }
262 }
263 break;
264 }
265 }
266 }
267 if ( feedback() && feedback()->isCanceled() )
268 return false;
269
270 const bool needsTextures = mRenderer->flags() & Qgis::TiledSceneRendererFlag::RequiresTextures;
271
272 std::sort( mPrimitiveData.begin(), mPrimitiveData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
273 {
274 // this isn't an exact science ;)
275 if ( qgsDoubleNear( a.z, b.z, 0.001 ) )
276 {
277 // for overlapping lines/triangles, ensure the line is drawn over the triangle
278 if ( a.type == PrimitiveType::Line )
279 return false;
280 else if ( b.type == PrimitiveType::Line )
281 return true;
282 }
283 return a.z < b.z;
284 } );
285 for ( const PrimitiveData &data : std::as_const( mPrimitiveData ) )
286 {
287 switch ( data.type )
288 {
289 case PrimitiveType::Line:
290 mRenderer->renderLine( context, data.coordinates );
291 break;
292
293 case PrimitiveType::Triangle:
294 if ( needsTextures )
295 {
296 context.setTextureImage( mTextures.value( data.textureId ) );
297 context.setTextureCoordinates( data.textureCoords[0], data.textureCoords[1],
298 data.textureCoords[2], data.textureCoords[3],
299 data.textureCoords[4], data.textureCoords[5] );
300 }
301 mRenderer->renderTriangle( context, data.coordinates );
302 break;
303 }
304 }
305
306 if ( mRenderTileBorders )
307 {
308 QPainter *painter = renderContext()->painter();
309 for ( const TileDetails &tile : std::as_const( mTileDetails ) )
310 {
311 QPen pen;
312 QBrush brush;
313 if ( tile.hasContent )
314 {
315 brush = QBrush( QColor( 0, 0, 255, 10 ) );
316 pen = QPen( QColor( 0, 0, 255, 150 ) );
317 }
318 else
319 {
320 brush = QBrush( QColor( 255, 0, 255, 10 ) );
321 pen = QPen( QColor( 255, 0, 255, 150 ) );
322 }
323 pen.setWidth( 2 );
324 painter->setPen( pen );
325 painter->setBrush( brush );
326 painter->drawPolygon( tile.boundary );
327#if 1
328 QgsTextFormat format;
329 format.setColor( QColor( 255, 0, 0 ) );
330 format.buffer().setEnabled( true );
331
332 QgsTextRenderer::drawText( QRectF( QPoint( 0, 0 ), renderContext()->outputSize() ).intersected( tile.boundary.boundingRect() ),
334 *renderContext(), format, true, Qgis::TextVerticalAlignment::VerticalCenter );
335#endif
336 }
337 }
338
339 return true;
340}
341
342void QgsTiledSceneLayerRenderer::renderTile( const QgsTiledSceneTile &tile, QgsTiledSceneRenderContext &context )
343{
344 const bool hasContent = renderTileContent( tile, context );
345
346 if ( mRenderTileBorders )
347 {
348 const QgsTiledSceneBoundingVolume &volume = tile.boundingVolume();
349 try
350 {
351 std::unique_ptr< QgsAbstractGeometry > volumeGeometry( volume.as2DGeometry( mSceneToMapTransform ) );
352 if ( QgsCurvePolygon *polygon = qgsgeometry_cast< QgsCurvePolygon * >( volumeGeometry.get() ) )
353 {
354 QPolygonF volumePolygon = polygon->exteriorRing()->asQPolygonF( );
355
356 // remove non-finite points, e.g. infinite or NaN points caused by reprojecting errors
357 volumePolygon.erase( std::remove_if( volumePolygon.begin(), volumePolygon.end(),
358 []( const QPointF point )
359 {
360 return !std::isfinite( point.x() ) || !std::isfinite( point.y() );
361 } ), volumePolygon.end() );
362
363 QPointF *ptr = volumePolygon.data();
364 for ( int i = 0; i < volumePolygon.size(); ++i, ++ptr )
365 {
366 renderContext()->mapToPixel().transformInPlace( ptr->rx(), ptr->ry() );
367 }
368
369 TileDetails details;
370 details.boundary = volumePolygon;
371 details.hasContent = hasContent;
372 details.id = QString::number( tile.id() );
373 mTileDetails.append( details );
374 }
375 }
376 catch ( QgsCsException & )
377 {
378 QgsDebugError( QStringLiteral( "Error transforming bounding volume" ) );
379 }
380 }
381}
382
383bool QgsTiledSceneLayerRenderer::renderTileContent( const QgsTiledSceneTile &tile, QgsTiledSceneRenderContext &context )
384{
385 const QString contentUri = tile.resources().value( QStringLiteral( "content" ) ).toString();
386 if ( contentUri.isEmpty() )
387 return false;
388
389 const QByteArray tileContent = mIndex.retrieveContent( contentUri, feedback() );
390 // When the operation is canceled, retrieveContent() will silently return an empty array
391 if ( feedback()->isCanceled() )
392 return false;
393
394 tinygltf::Model model;
395 QgsVector3D centerOffset;
396 mCurrentModelId++;
397 // TODO: Somehow de-hardcode this switch?
398 const auto &format = tile.metadata().value( QStringLiteral( "contentFormat" ) ).value<QString>();
399 if ( format == QLatin1String( "quantizedmesh" ) )
400 {
401 try
402 {
403 QgsQuantizedMeshTile qmTile( tileContent );
404 qmTile.removeDegenerateTriangles();
405 model = qmTile.toGltf();
406 }
408 {
409 QgsDebugError( QStringLiteral( "Failed to parse tile from '%1'" ).arg( contentUri ) );
410 return false;
411 }
412 }
413 else if ( format == QLatin1String( "cesiumtiles" ) )
414 {
416 if ( content.gltf.isEmpty() )
417 {
418 return false;
419 }
420 centerOffset = content.rtcCenter;
421
422 QString gltfErrors;
423 QString gltfWarnings;
424 const bool res = QgsGltfUtils::loadGltfModel( content.gltf, model,
425 &gltfErrors, &gltfWarnings );
426 if ( !gltfErrors.isEmpty() )
427 {
428 if ( !mErrors.contains( gltfErrors ) )
429 mErrors.append( gltfErrors );
430 QgsDebugError( QStringLiteral( "Error raised reading %1: %2" )
431 .arg( contentUri, gltfErrors ) );
432 }
433 if ( !gltfWarnings.isEmpty() )
434 {
435 QgsDebugError( QStringLiteral( "Warnings raised reading %1: %2" )
436 .arg( contentUri, gltfWarnings ) );
437 }
438 if ( !res ) return false;
439 }
440 else
441 return false;
442
443 const QgsVector3D tileTranslationEcef =
444 centerOffset +
445 QgsGltfUtils::extractTileTranslation(
446 model,
447 static_cast<Qgis::Axis>( tile.metadata()
448 .value( QStringLiteral( "gltfUpAxis" ),
449 static_cast<int>( Qgis::Axis::Y ) )
450 .toInt() ) );
451
452 bool sceneOk = false;
453 const std::size_t sceneIndex =
454 QgsGltfUtils::sourceSceneForModel( model, sceneOk );
455 if ( !sceneOk )
456 {
457 const QString error = QObject::tr( "No scenes found in model" );
458 mErrors.append( error );
460 QStringLiteral( "Error raised reading %1: %2" ).arg( contentUri, error ) );
461 }
462 else
463 {
464 const tinygltf::Scene &scene = model.scenes[sceneIndex];
465
466 std::function< void( int nodeIndex, const QMatrix4x4 &transform ) > traverseNode;
467 traverseNode = [&model, &context, &tileTranslationEcef, &tile, &contentUri, &traverseNode, this]( int nodeIndex, const QMatrix4x4 & parentTransform )
468 {
469 const tinygltf::Node &gltfNode = model.nodes[nodeIndex];
470 std::unique_ptr< QMatrix4x4 > gltfLocalTransform = QgsGltfUtils::parseNodeTransform( gltfNode );
471
472 if ( !parentTransform.isIdentity() )
473 {
474 if ( gltfLocalTransform )
475 *gltfLocalTransform = parentTransform * *gltfLocalTransform;
476 else
477 {
478 gltfLocalTransform.reset( new QMatrix4x4( parentTransform ) );
479 }
480 }
481
482 if ( gltfNode.mesh >= 0 )
483 {
484 const tinygltf::Mesh &mesh = model.meshes[gltfNode.mesh];
485
486 for ( const tinygltf::Primitive &primitive : mesh.primitives )
487 {
488 if ( context.renderContext().renderingStopped() )
489 break;
490
491 renderPrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform.get(), contentUri, context );
492 }
493 }
494
495 for ( int childNode : gltfNode.children )
496 {
497 traverseNode( childNode, gltfLocalTransform ? *gltfLocalTransform : QMatrix4x4() );
498 }
499 };
500
501 for ( int nodeIndex : scene.nodes )
502 {
503 traverseNode( nodeIndex, QMatrix4x4() );
504 }
505 }
506 return true;
507}
508
509void QgsTiledSceneLayerRenderer::renderPrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &contentUri, QgsTiledSceneRenderContext &context )
510{
511 switch ( primitive.mode )
512 {
513 case TINYGLTF_MODE_TRIANGLES:
514 if ( mRenderer->flags() & Qgis::TiledSceneRendererFlag::RendersTriangles )
515 renderTrianglePrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform, contentUri, context );
516 break;
517
518 case TINYGLTF_MODE_LINE:
519 if ( mRenderer->flags() & Qgis::TiledSceneRendererFlag::RendersLines )
520 renderLinePrimitive( model, primitive, tile, tileTranslationEcef, gltfLocalTransform, contentUri, context );
521 return;
522
523 case TINYGLTF_MODE_POINTS:
524 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_POINTS ) )
525 {
526 mErrors << QObject::tr( "Point objects in tiled scenes are not supported" );
527 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_POINTS );
528 }
529 return;
530
531 case TINYGLTF_MODE_LINE_LOOP:
532 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_LINE_LOOP ) )
533 {
534 mErrors << QObject::tr( "Line loops in tiled scenes are not supported" );
535 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_LINE_LOOP );
536 }
537 return;
538
539 case TINYGLTF_MODE_LINE_STRIP:
540 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_LINE_STRIP ) )
541 {
542 mErrors << QObject::tr( "Line strips in tiled scenes are not supported" );
543 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_LINE_STRIP );
544 }
545 return;
546
547 case TINYGLTF_MODE_TRIANGLE_STRIP:
548 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_TRIANGLE_STRIP ) )
549 {
550 mErrors << QObject::tr( "Triangular strips in tiled scenes are not supported" );
551 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_TRIANGLE_STRIP );
552 }
553 return;
554
555 case TINYGLTF_MODE_TRIANGLE_FAN:
556 if ( !mWarnedPrimitiveTypes.contains( TINYGLTF_MODE_TRIANGLE_FAN ) )
557 {
558 mErrors << QObject::tr( "Triangular fans in tiled scenes are not supported" );
559 mWarnedPrimitiveTypes.insert( TINYGLTF_MODE_TRIANGLE_FAN );
560 }
561 return;
562
563 default:
564 if ( !mWarnedPrimitiveTypes.contains( primitive.mode ) )
565 {
566 mErrors << QObject::tr( "Primitive type %1 in tiled scenes are not supported" ).arg( primitive.mode );
567 mWarnedPrimitiveTypes.insert( primitive.mode );
568 }
569 return;
570 }
571}
572
573void QgsTiledSceneLayerRenderer::renderTrianglePrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &contentUri, QgsTiledSceneRenderContext &context )
574{
575 auto posIt = primitive.attributes.find( "POSITION" );
576 if ( posIt == primitive.attributes.end() )
577 {
578 mErrors << QObject::tr( "Could not find POSITION attribute for primitive" );
579 return;
580 }
581 int positionAccessorIndex = posIt->second;
582
583 QVector< double > x;
584 QVector< double > y;
585 QVector< double > z;
586 QgsGltfUtils::accessorToMapCoordinates(
587 model, positionAccessorIndex, tile.transform() ? *tile.transform() : QgsMatrix4x4(),
588 &mSceneToMapTransform,
589 tileTranslationEcef,
590 gltfLocalTransform,
591 static_cast< Qgis::Axis >( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Y ) ).toInt() ),
592 x, y, z
593 );
594
596
597 const bool needsTextures = mRenderer->flags() & Qgis::TiledSceneRendererFlag::RequiresTextures;
598
599 QImage textureImage;
600 QVector< float > texturePointX;
601 QVector< float > texturePointY;
602 QPair< int, int > textureId{ -1, -1 };
603 if ( needsTextures && primitive.material != -1 )
604 {
605 const tinygltf::Material &material = model.materials[primitive.material];
606 const tinygltf::PbrMetallicRoughness &pbr = material.pbrMetallicRoughness;
607
608 if ( pbr.baseColorTexture.index >= 0
609 && static_cast< int >( model.textures.size() ) > pbr.baseColorTexture.index )
610 {
611 const tinygltf::Texture &tex = model.textures[pbr.baseColorTexture.index];
612
613 // Source can be undefined if texture is provided by an extension
614 if ( tex.source >= 0 )
615 {
616 switch ( QgsGltfUtils::imageResourceType( model, tex.source ) )
617 {
618 case QgsGltfUtils::ResourceType::Embedded:
619 textureImage = QgsGltfUtils::extractEmbeddedImage( model, tex.source );
620 break;
621
622 case QgsGltfUtils::ResourceType::Linked:
623 {
624 const QString linkedPath = QgsGltfUtils::linkedImagePath( model, tex.source );
625 const QString textureUri = QUrl( contentUri ).resolved( linkedPath ).toString();
626 const QByteArray rep = mIndex.retrieveContent( textureUri, feedback() );
627 if ( !rep.isEmpty() )
628 {
629 textureImage = QImage::fromData( rep );
630 }
631 break;
632 }
633 }
634 }
635
636 if ( !textureImage.isNull() )
637 {
638 auto texIt = primitive.attributes.find( "TEXCOORD_0" );
639 if ( texIt != primitive.attributes.end() )
640 {
641 QgsGltfUtils::extractTextureCoordinates(
642 model, texIt->second, texturePointX, texturePointY
643 );
644 }
645
646 textureId = qMakePair( mCurrentModelId, pbr.baseColorTexture.index );
647 }
648 }
649 else if ( qgsDoubleNear( pbr.baseColorFactor[3], 0 ) )
650 {
651 // transparent primitive, skip
652 return;
653 }
654 }
655
656 const QRect outputRect = QRect( QPoint( 0, 0 ), context.renderContext().outputSize() );
657 auto needTriangle = [&outputRect]( const QPolygonF & triangle ) -> bool
658 {
659 return triangle.boundingRect().intersects( outputRect );
660 };
661
662 const bool useTexture = !textureImage.isNull();
663 bool hasStoredTexture = false;
664
665 QVector< PrimitiveData > thisTileTriangleData;
666
667 if ( primitive.indices == -1 )
668 {
669 Q_ASSERT( x.size() % 3 == 0 );
670
671 thisTileTriangleData.reserve( x.size() );
672 for ( int i = 0; i < x.size(); i += 3 )
673 {
674 if ( context.renderContext().renderingStopped() )
675 break;
676
677 PrimitiveData data;
678 data.type = PrimitiveType::Triangle;
679 data.textureId = textureId;
680 if ( useTexture )
681 {
682 data.textureCoords[0] = texturePointX[i];
683 data.textureCoords[1] = texturePointY[i];
684 data.textureCoords[2] = texturePointX[i + 1];
685 data.textureCoords[3] = texturePointY[i + 1];
686 data.textureCoords[4] = texturePointX[i + 2];
687 data.textureCoords[5] = texturePointY[i + 2];
688 }
689 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] ) };
690 data.z = ( z[i] + z[i + 1] + z[i + 2] ) / 3;
691 if ( needTriangle( data.coordinates ) )
692 {
693 thisTileTriangleData.push_back( data );
694 if ( !hasStoredTexture && !textureImage.isNull() )
695 {
696 // have to make an explicit .copy() here, as we don't necessarily own the image data
697 mTextures.insert( textureId, textureImage.copy() );
698 hasStoredTexture = true;
699 }
700 }
701 }
702 }
703 else
704 {
705 const tinygltf::Accessor &primitiveAccessor = model.accessors[primitive.indices];
706 const tinygltf::BufferView &bvPrimitive = model.bufferViews[primitiveAccessor.bufferView];
707 const tinygltf::Buffer &bPrimitive = model.buffers[bvPrimitive.buffer];
708
709 Q_ASSERT( ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT
710 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT
711 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
712 && primitiveAccessor.type == TINYGLTF_TYPE_SCALAR );
713
714 const char *primitivePtr = reinterpret_cast< const char * >( bPrimitive.data.data() ) + bvPrimitive.byteOffset + primitiveAccessor.byteOffset;
715
716 thisTileTriangleData.reserve( primitiveAccessor.count / 3 );
717 for ( std::size_t i = 0; i < primitiveAccessor.count / 3; i++ )
718 {
719 if ( context.renderContext().renderingStopped() )
720 break;
721
722 unsigned int index1 = 0;
723 unsigned int index2 = 0;
724 unsigned int index3 = 0;
725
726 PrimitiveData data;
727 data.type = PrimitiveType::Triangle;
728 data.textureId = textureId;
729
730 if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT )
731 {
732 const unsigned short *usPtrPrimitive = reinterpret_cast< const unsigned short * >( primitivePtr );
733 if ( bvPrimitive.byteStride )
734 primitivePtr += bvPrimitive.byteStride;
735 else
736 primitivePtr += 3 * sizeof( unsigned short );
737
738 index1 = usPtrPrimitive[0];
739 index2 = usPtrPrimitive[1];
740 index3 = usPtrPrimitive[2];
741 }
742 else if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
743 {
744 const unsigned char *usPtrPrimitive = reinterpret_cast< const unsigned char * >( primitivePtr );
745 if ( bvPrimitive.byteStride )
746 primitivePtr += bvPrimitive.byteStride;
747 else
748 primitivePtr += 3 * sizeof( unsigned char );
749
750 index1 = usPtrPrimitive[0];
751 index2 = usPtrPrimitive[1];
752 index3 = usPtrPrimitive[2];
753 }
754 else
755 {
756 const unsigned int *uintPtrPrimitive = reinterpret_cast< const unsigned int * >( primitivePtr );
757 if ( bvPrimitive.byteStride )
758 primitivePtr += bvPrimitive.byteStride;
759 else
760 primitivePtr += 3 * sizeof( unsigned int );
761
762 index1 = uintPtrPrimitive[0];
763 index2 = uintPtrPrimitive[1];
764 index3 = uintPtrPrimitive[2];
765 }
766
767 if ( useTexture )
768 {
769 data.textureCoords[0] = texturePointX[index1];
770 data.textureCoords[1] = texturePointY[index1];
771 data.textureCoords[2] = texturePointX[index2];
772 data.textureCoords[3] = texturePointY[index2];
773 data.textureCoords[4] = texturePointX[index3];
774 data.textureCoords[5] = texturePointY[index3];
775 }
776
777 data.coordinates = { QVector<QPointF>{ QPointF( x[index1], y[index1] ), QPointF( x[index2], y[index2] ), QPointF( x[index3], y[index3] ), QPointF( x[index1], y[index1] ) } };
778 data.z = ( z[index1] + z[index2] + z[index3] ) / 3;
779 if ( needTriangle( data.coordinates ) )
780 {
781 thisTileTriangleData.push_back( data );
782 if ( !hasStoredTexture && !textureImage.isNull() )
783 {
784 // have to make an explicit .copy() here, as we don't necessarily own the image data
785 mTextures.insert( textureId, textureImage.copy() );
786 hasStoredTexture = true;
787 }
788 }
789 }
790 }
791
792 if ( context.renderContext().previewRenderPainter() )
793 {
794 // swap out the destination painter for the preview render painter, and render
795 // the triangles from this tile in a sorted order
796 QPainter *finalPainter = context.renderContext().painter();
798
799 std::sort( thisTileTriangleData.begin(), thisTileTriangleData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
800 {
801 return a.z < b.z;
802 } );
803
804 for ( const PrimitiveData &data : std::as_const( thisTileTriangleData ) )
805 {
806 if ( useTexture && data.textureId.first >= 0 )
807 {
808 context.setTextureImage( mTextures.value( data.textureId ) );
809 context.setTextureCoordinates( data.textureCoords[0], data.textureCoords[1],
810 data.textureCoords[2], data.textureCoords[3],
811 data.textureCoords[4], data.textureCoords[5] );
812 }
813 mRenderer->renderTriangle( context, data.coordinates );
814 }
815 context.renderContext().setPainter( finalPainter );
816 }
817
818 mPrimitiveData.append( thisTileTriangleData );
819
820 // as soon as first tile is rendered, we can start showing layer updates. But we still delay
821 // this by e.g. 3 seconds before we start forcing progressive updates, so that we don't show the unsorted
822 // z triangle render if the overall layer render only takes a second or so.
823 if ( mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
824 {
825 mReadyToCompose = true;
826 }
827}
828
829void QgsTiledSceneLayerRenderer::renderLinePrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive, const QgsTiledSceneTile &tile, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *gltfLocalTransform, const QString &, QgsTiledSceneRenderContext &context )
830{
831 auto posIt = primitive.attributes.find( "POSITION" );
832 if ( posIt == primitive.attributes.end() )
833 {
834 mErrors << QObject::tr( "Could not find POSITION attribute for primitive" );
835 return;
836 }
837 int positionAccessorIndex = posIt->second;
838
839 QVector< double > x;
840 QVector< double > y;
841 QVector< double > z;
842 QgsGltfUtils::accessorToMapCoordinates(
843 model, positionAccessorIndex, tile.transform() ? *tile.transform() : QgsMatrix4x4(),
844 &mSceneToMapTransform,
845 tileTranslationEcef,
846 gltfLocalTransform,
847 static_cast< Qgis::Axis >( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Y ) ).toInt() ),
848 x, y, z
849 );
850
852
853 const QRect outputRect = QRect( QPoint( 0, 0 ), context.renderContext().outputSize() );
854 auto needLine = [&outputRect]( const QPolygonF & line ) -> bool
855 {
856 return line.boundingRect().intersects( outputRect );
857 };
858
859 QVector< PrimitiveData > thisTileLineData;
860
861 if ( primitive.indices == -1 )
862 {
863 Q_ASSERT( x.size() % 2 == 0 );
864
865 thisTileLineData.reserve( x.size() );
866 for ( int i = 0; i < x.size(); i += 2 )
867 {
868 if ( context.renderContext().renderingStopped() )
869 break;
870
871 PrimitiveData data;
872 data.type = PrimitiveType::Line;
873 data.coordinates = QVector<QPointF> { QPointF( x[i], y[i] ), QPointF( x[i + 1], y[i + 1] ) };
874 // note -- we take the maximum z here, as we'd ideally like lines to be placed over similarish z valued triangles
875 data.z = std::max( z[i], z[i + 1] );
876 if ( needLine( data.coordinates ) )
877 {
878 thisTileLineData.push_back( data );
879 }
880 }
881 }
882 else
883 {
884 const tinygltf::Accessor &primitiveAccessor = model.accessors[primitive.indices];
885 const tinygltf::BufferView &bvPrimitive = model.bufferViews[primitiveAccessor.bufferView];
886 const tinygltf::Buffer &bPrimitive = model.buffers[bvPrimitive.buffer];
887
888 Q_ASSERT( ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT
889 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT
890 || primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
891 && primitiveAccessor.type == TINYGLTF_TYPE_SCALAR );
892
893 const char *primitivePtr = reinterpret_cast< const char * >( bPrimitive.data.data() ) + bvPrimitive.byteOffset + primitiveAccessor.byteOffset;
894
895 thisTileLineData.reserve( primitiveAccessor.count / 2 );
896 for ( std::size_t i = 0; i < primitiveAccessor.count / 2; i++ )
897 {
898 if ( context.renderContext().renderingStopped() )
899 break;
900
901 unsigned int index1 = 0;
902 unsigned int index2 = 0;
903
904 PrimitiveData data;
905 data.type = PrimitiveType::Line;
906
907 if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT )
908 {
909 const unsigned short *usPtrPrimitive = reinterpret_cast< const unsigned short * >( primitivePtr );
910 if ( bvPrimitive.byteStride )
911 primitivePtr += bvPrimitive.byteStride;
912 else
913 primitivePtr += 2 * sizeof( unsigned short );
914
915 index1 = usPtrPrimitive[0];
916 index2 = usPtrPrimitive[1];
917 }
918 else if ( primitiveAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE )
919 {
920 const unsigned char *usPtrPrimitive = reinterpret_cast< const unsigned char * >( primitivePtr );
921 if ( bvPrimitive.byteStride )
922 primitivePtr += bvPrimitive.byteStride;
923 else
924 primitivePtr += 2 * sizeof( unsigned char );
925
926 index1 = usPtrPrimitive[0];
927 index2 = usPtrPrimitive[1];
928 }
929 else
930 {
931 const unsigned int *uintPtrPrimitive = reinterpret_cast< const unsigned int * >( primitivePtr );
932 if ( bvPrimitive.byteStride )
933 primitivePtr += bvPrimitive.byteStride;
934 else
935 primitivePtr += 2 * sizeof( unsigned int );
936
937 index1 = uintPtrPrimitive[0];
938 index2 = uintPtrPrimitive[1];
939 }
940
941 data.coordinates = { QVector<QPointF>{ QPointF( x[index1], y[index1] ), QPointF( x[index2], y[index2] ) } };
942 // note -- we take the maximum z here, as we'd ideally like lines to be placed over similarish z valued triangles
943 data.z = std::max( z[index1], z[index2] );
944 if ( needLine( data.coordinates ) )
945 {
946 thisTileLineData.push_back( data );
947 }
948 }
949 }
950
951 if ( context.renderContext().previewRenderPainter() )
952 {
953 // swap out the destination painter for the preview render painter, and render
954 // the triangles from this tile in a sorted order
955 QPainter *finalPainter = context.renderContext().painter();
957
958 std::sort( thisTileLineData.begin(), thisTileLineData.end(), []( const PrimitiveData & a, const PrimitiveData & b )
959 {
960 return a.z < b.z;
961 } );
962
963 for ( const PrimitiveData &data : std::as_const( thisTileLineData ) )
964 {
965 mRenderer->renderLine( context, data.coordinates );
966 }
967 context.renderContext().setPainter( finalPainter );
968 }
969
970 mPrimitiveData.append( thisTileLineData );
971
972 // as soon as first tile is rendered, we can start showing layer updates. But we still delay
973 // this by e.g. 3 seconds before we start forcing progressive updates, so that we don't show the unsorted
974 // z primitive render if the overall layer render only takes a second or so.
975 if ( mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
976 {
977 mReadyToCompose = true;
978 }
979}
The Qgis class provides global constants for use throughout the application.
Definition qgis.h:54
QFlags< MapLayerRendererFlag > MapLayerRendererFlags
Flags which control how map layer renderers behave.
Definition qgis.h:2730
@ 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:2399
@ 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.
Class for doing transforms between two map 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.
A class to represent 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.
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:52
double x() const
Returns X coordinate.
Definition qgsvector3d.h:48
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:6206
#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.