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