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