QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
qgstiledscenechunkloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstiledscenechunkloader_p.cpp
3 --------------------------------------
4 Date : July 2023
5 Copyright : (C) 2023 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgs3dmapsettings.h"
19#include "qgs3dutils.h"
20#include "qgsapplication.h"
21#include "qgscesiumutils.h"
23#include "qgsgeotransform.h"
24#include "qgsgltf3dutils.h"
25#include "qgsgltfutils.h"
27#include "qgsray3d.h"
28#include "qgsraycastcontext.h"
29#include "qgsraycastingutils.h"
31#include "qgstiledscenetile.h"
32
33#include <QtConcurrentRun>
34
35#include "moc_qgstiledscenechunkloader_p.cpp"
36
38
39size_t qHash( const QgsChunkNodeId &n )
40{
41 return n.uniqueId;
42}
43
44static bool hasLargeBounds( const QgsTiledSceneTile &t, const QgsCoordinateTransform &boundsTransform )
45{
46 if ( t.geometricError() > 1e6 )
47 return true;
48
49 if ( t.boundingVolume().box().isNull() )
50 return true;
51
52 Q_ASSERT( boundsTransform.destinationCrs().mapUnits() == Qgis::DistanceUnit::Meters );
53 QgsBox3D bounds = t.boundingVolume().bounds( boundsTransform );
54 return bounds.width() > 1e5 || bounds.height() > 1e5 || bounds.depth() > 1e5;
55}
56
58
59QgsTiledSceneChunkLoader::QgsTiledSceneChunkLoader( QgsChunkNode *node, const QgsTiledSceneIndex &index, const QgsTiledSceneChunkLoaderFactory &factory, double zValueScale, double zValueOffset )
60 : QgsChunkLoader( node )
61 , mFactory( factory )
62 , mZValueScale( zValueScale )
63 , mZValueOffset( zValueOffset )
64 , mIndex( index )
65{
66}
67
68void QgsTiledSceneChunkLoader::start()
69{
70 QgsChunkNode *node = chunk();
71
72 mFutureWatcher = new QFutureWatcher<void>( this );
73 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
74
75 const QgsCoordinateTransform &boundsTransform = mFactory.mBoundsTransform;
76
77 const QgsChunkNodeId tileId = node->tileId();
78 const QgsVector3D chunkOrigin = node->box3D().center();
79 const bool isGlobe = mFactory.mRenderContext.crs().type() == Qgis::CrsType::Geocentric;
80 const QFuture<void> future = QtConcurrent::run( [this, tileId, boundsTransform, chunkOrigin, isGlobe] {
81 const QgsTiledSceneTile tile = mIndex.getTile( tileId.uniqueId );
82
83 // we do not load tiles that are too big when not in globe scene mode...
84 // the problem is that their 3D bounding boxes with ECEF coordinates are huge
85 // and we are unable to turn them into planar bounding boxes
86 if ( !isGlobe && hasLargeBounds( tile, boundsTransform ) )
87 return;
88
89 QString uri = tile.resources().value( QStringLiteral( "content" ) ).toString();
90 if ( uri.isEmpty() )
91 {
92 // nothing to show for this tile
93 // TODO: can we skip loading it at all?
94 return;
95 }
96
97 uri = tile.baseUrl().resolved( uri ).toString();
98 QByteArray content = mFactory.mIndex.retrieveContent( uri );
99 if ( content.isEmpty() )
100 {
101 // the request probably failed
102 // TODO: how can we report it?
103 return;
104 }
105
106 QgsGltf3DUtils::EntityTransform entityTransform;
107 entityTransform.tileTransform = ( tile.transform() ? *tile.transform() : QgsMatrix4x4() );
108 entityTransform.chunkOriginTargetCrs = chunkOrigin;
109 entityTransform.ecefToTargetCrs = &mFactory.mBoundsTransform;
110 entityTransform.zValueScale = mZValueScale;
111 entityTransform.zValueOffset = mZValueOffset;
112 entityTransform.gltfUpAxis = static_cast<Qgis::Axis>( tile.metadata().value( QStringLiteral( "gltfUpAxis" ), static_cast<int>( Qgis::Axis::Y ) ).toInt() );
113
114 const QString &format = tile.metadata().value( QStringLiteral( "contentFormat" ) ).value<QString>();
115 QStringList errors;
116 if ( format == QLatin1String( "quantizedmesh" ) )
117 {
118 try
119 {
120 QgsQuantizedMeshTile qmTile( content );
121 qmTile.removeDegenerateTriangles();
122 tinygltf::Model model = qmTile.toGltf( true, 100 );
123 mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, uri, &errors );
124 }
126 {
127 errors.append( QStringLiteral( "Failed to parse tile from '%1'" ).arg( uri ) );
128 }
129 }
130 else if ( format == QLatin1String( "cesiumtiles" ) )
131 {
133 if ( tileContent.gltf.isEmpty() )
134 return;
135 entityTransform.tileTransform.translate( tileContent.rtcCenter );
136 mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
137 }
138 else if ( format == QLatin1String( "draco" ) )
139 {
140 QgsGltfUtils::I3SNodeContext i3sContext;
141 i3sContext.initFromTile( tile, mFactory.mLayerCrs, mFactory.mBoundsTransform.sourceCrs(), mFactory.mRenderContext.transformContext() );
142
143 QString dracoLoadError;
144 tinygltf::Model model;
145 if ( !QgsGltfUtils::loadDracoModel( content, i3sContext, model, &dracoLoadError ) )
146 {
147 errors.append( dracoLoadError );
148 return;
149 }
150
151 mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, QString(), &errors );
152 }
153 else
154 return; // unsupported tile content type
155
156 // TODO: report errors somewhere?
157 if ( !errors.isEmpty() )
158 {
159 QgsDebugError( "gltf load errors: " + errors.join( '\n' ) );
160 }
161
162 if ( mEntity )
163 {
164 QgsGeoTransform *transform = new QgsGeoTransform;
165 transform->setGeoTranslation( chunkOrigin );
166 mEntity->addComponent( transform );
167
168 mEntity->moveToThread( QgsApplication::instance()->thread() );
169 }
170 } );
171
172 // emit finished() as soon as the handler is populated with features
173 mFutureWatcher->setFuture( future );
174}
175
176QgsTiledSceneChunkLoader::~QgsTiledSceneChunkLoader()
177{
178 if ( !mFutureWatcher->isFinished() )
179 {
180 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
181 mFutureWatcher->waitForFinished();
182 }
183}
184
185Qt3DCore::QEntity *QgsTiledSceneChunkLoader::createEntity( Qt3DCore::QEntity *parent )
186{
187 if ( mEntity )
188 mEntity->setParent( parent );
189 return mEntity;
190}
191
193
194QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory(
195 const Qgs3DRenderContext &context,
196 const QgsTiledSceneIndex &index,
199 double zValueScale,
200 double zValueOffset
201)
202 : mRenderContext( context )
203 , mIndex( index )
204 , mZValueScale( zValueScale )
205 , mZValueOffset( zValueOffset )
206 , mLayerCrs( layerCrs )
207{
208 mBoundsTransform = QgsCoordinateTransform( tileCrs, context.crs(), context.transformContext() );
209}
210
211QgsChunkLoader *QgsTiledSceneChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
212{
213 return new QgsTiledSceneChunkLoader( node, mIndex, *this, mZValueScale, mZValueOffset );
214}
215
216QgsChunkNode *QgsTiledSceneChunkLoaderFactory::nodeForTile( const QgsTiledSceneTile &t, const QgsChunkNodeId &nodeId, QgsChunkNode *parent ) const
217{
218 QgsChunkNode *node = nullptr;
219 if ( mRenderContext.crs().type() != Qgis::CrsType::Geocentric && hasLargeBounds( t, mBoundsTransform ) )
220 {
221 // use the full extent of the scene
222 QgsVector3D v0( mRenderContext.extent().xMinimum(), mRenderContext.extent().yMinimum(), -100 );
223 QgsVector3D v1( mRenderContext.extent().xMaximum(), mRenderContext.extent().yMaximum(), +100 );
224 QgsBox3D box3D( v0, v1 );
225 float err = std::min( 1e6, t.geometricError() );
226 node = new QgsChunkNode( nodeId, box3D, err, parent );
227 }
228 else
229 {
230 QgsBox3D box = t.boundingVolume().bounds( mBoundsTransform );
231 box.setZMinimum( box.zMinimum() * mZValueScale + mZValueOffset );
232 box.setZMaximum( box.zMaximum() * mZValueScale + mZValueOffset );
233 node = new QgsChunkNode( nodeId, box, t.geometricError(), parent );
234 }
235
236 node->setRefinementProcess( t.refinementProcess() );
237 return node;
238}
239
240
241QgsChunkNode *QgsTiledSceneChunkLoaderFactory::createRootNode() const
242{
243 const QgsTiledSceneTile t = mIndex.rootTile();
244 return nodeForTile( t, QgsChunkNodeId( t.id() ), nullptr );
245}
246
247
248QVector<QgsChunkNode *> QgsTiledSceneChunkLoaderFactory::createChildren( QgsChunkNode *node ) const
249{
250 QVector<QgsChunkNode *> children;
251 const long long indexTileId = node->tileId().uniqueId;
252
253 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
254 Q_ASSERT( mIndex.childAvailability( indexTileId ) != Qgis::TileChildrenAvailability::NeedFetching );
255
256 const QVector<long long> childIds = mIndex.childTileIds( indexTileId );
257 for ( long long childId : childIds )
258 {
259 const QgsChunkNodeId chId( childId );
260 QgsTiledSceneTile t = mIndex.getTile( childId );
261
262 // first check if this node should be even considered
263 // XXX: This check doesn't work for Quantized Mesh layers and possibly some
264 // Cesium 3D tiles as well. For now this hack is in place to make sure both
265 // work in practice.
266 if ( t.metadata()["contentFormat"] == QStringLiteral( "cesiumtiles" )
267 && mRenderContext.crs().type() != Qgis::CrsType::Geocentric
268 && hasLargeBounds( t, mBoundsTransform ) )
269 {
270 // if the tile is huge, let's try to see if our scene is actually inside
271 // (if not, let' skip this child altogether!)
272 // TODO: make OBB of our scene in ECEF rather than just using center of the scene?
273 const QgsOrientedBox3D obb = t.boundingVolume().box();
274 const QgsPointXY c = mRenderContext.extent().center();
275 const QgsVector3D cEcef = mBoundsTransform.transform( QgsVector3D( c.x(), c.y(), 0 ), Qgis::TransformDirection::Reverse );
276 const QgsVector3D ecef2 = cEcef - obb.center();
277 const double *half = obb.halfAxes();
278 // this is an approximate check anyway, no need for double precision matrix/vector
279 QMatrix4x4 rot(
280 half[0], half[3], half[6], 0,
281 half[1], half[4], half[7], 0,
282 half[2], half[5], half[8], 0,
283 0, 0, 0, 1
284 );
285 QVector3D aaa = rot.inverted().map( ecef2.toVector3D() );
286 if ( aaa.x() > 1 || aaa.y() > 1 || aaa.z() > 1 || aaa.x() < -1 || aaa.y() < -1 || aaa.z() < -1 )
287 {
288 continue;
289 }
290 }
291
292 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
293 Q_ASSERT( mIndex.childAvailability( childId ) != Qgis::TileChildrenAvailability::NeedFetching );
294
295 QgsChunkNode *nChild = nodeForTile( t, chId, node );
296 children.append( nChild );
297 }
298 return children;
299}
300
301bool QgsTiledSceneChunkLoaderFactory::canCreateChildren( QgsChunkNode *node )
302{
303 long long nodeId = node->tileId().uniqueId;
304 if ( mFutureHierarchyFetches.contains( nodeId ) || mPendingHierarchyFetches.contains( nodeId ) )
305 return false;
306
307 if ( mIndex.childAvailability( nodeId ) == Qgis::TileChildrenAvailability::NeedFetching )
308 {
309 mFutureHierarchyFetches.insert( nodeId );
310 return false;
311 }
312
313 // we need to make sure that if a child tile's content references another tileset JSON,
314 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
315 // end up trying to load tileset JSON file instead of the actual content
316
317 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
318 for ( long long childId : childIds )
319 {
320 if ( mFutureHierarchyFetches.contains( childId ) || mPendingHierarchyFetches.contains( childId ) )
321 return false;
322
323 if ( mIndex.childAvailability( childId ) == Qgis::TileChildrenAvailability::NeedFetching )
324 {
325 mFutureHierarchyFetches.insert( childId );
326 return false;
327 }
328 }
329 return true;
330}
331
332void QgsTiledSceneChunkLoaderFactory::fetchHierarchyForNode( long long nodeId, QgsChunkNode *origNode )
333{
334 Q_ASSERT( !mPendingHierarchyFetches.contains( nodeId ) );
335 mFutureHierarchyFetches.remove( nodeId );
336 mPendingHierarchyFetches.insert( nodeId );
337
338 QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>( this );
339 connect( futureWatcher, &QFutureWatcher<void>::finished, this, [this, origNode, nodeId, futureWatcher] {
340 mPendingHierarchyFetches.remove( nodeId );
341 emit childrenPrepared( origNode );
342 futureWatcher->deleteLater();
343 } );
344 futureWatcher->setFuture( QtConcurrent::run( [this, nodeId] {
345 mIndex.fetchHierarchy( nodeId );
346 } ) );
347}
348
349void QgsTiledSceneChunkLoaderFactory::prepareChildren( QgsChunkNode *node )
350{
351 long long nodeId = node->tileId().uniqueId;
352 if ( mFutureHierarchyFetches.contains( nodeId ) )
353 {
354 fetchHierarchyForNode( nodeId, node );
355 return;
356 }
357
358 // we need to make sure that if a child tile's content references another tileset JSON,
359 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
360 // end up trying to load tileset JSON file instead of the actual content
361
362 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
363 for ( long long childId : childIds )
364 {
365 if ( mFutureHierarchyFetches.contains( childId ) )
366 {
367 fetchHierarchyForNode( childId, node );
368 }
369 }
370}
371
372
374
375QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity(
376 Qgs3DMapSettings *map,
377 const QgsTiledSceneIndex &index,
380 double maximumScreenError,
381 bool showBoundingBoxes,
382 double zValueScale,
383 double zValueOffset
384)
385 : QgsChunkedEntity( map, maximumScreenError, new QgsTiledSceneChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), index, tileCrs, layerCrs, zValueScale, zValueOffset ), true )
386 , mIndex( index )
387{
388 setShowBoundingBoxes( showBoundingBoxes );
389}
390
391QgsTiledSceneLayerChunkedEntity::~QgsTiledSceneLayerChunkedEntity()
392{
393 // cancel / wait for jobs
394 cancelActiveJobs();
395}
396
397int QgsTiledSceneLayerChunkedEntity::pendingJobsCount() const
398{
399 return QgsChunkedEntity::pendingJobsCount() + static_cast<QgsTiledSceneChunkLoaderFactory *>( mChunkLoaderFactory )->mPendingHierarchyFetches.count();
400}
401
402QList<QgsRayCastHit> QgsTiledSceneLayerChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
403{
404 Q_UNUSED( context );
405 QgsDebugMsgLevel( QStringLiteral( "Ray cast on tiled scene layer" ), 2 );
406#ifdef QGISDEBUG
407 int nodeUsed = 0;
408 int nodesAll = 0;
409 int hits = 0;
410#endif
411
412 QList<QgsRayCastHit> result;
413 float minDist = -1;
414 QVector3D intersectionPoint;
415 QgsChunkNode *minNode = nullptr;
416 int minTriangleIndex = -1;
417
418 const QList<QgsChunkNode *> active = activeNodes();
419 for ( QgsChunkNode *node : active )
420 {
421#ifdef QGISDEBUG
422 nodesAll++;
423#endif
424
425 QgsAABB nodeBbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
426
427 if ( node->entity() && ( minDist < 0 || nodeBbox.distanceFromPoint( ray.origin() ) < minDist ) && QgsRayCastingUtils::rayBoxIntersection( ray, nodeBbox ) )
428 {
429#ifdef QGISDEBUG
430 nodeUsed++;
431#endif
432 const QList<Qt3DRender::QGeometryRenderer *> rendLst = node->entity()->findChildren<Qt3DRender::QGeometryRenderer *>();
433 for ( Qt3DRender::QGeometryRenderer *rend : rendLst )
434 {
435 QVector3D nodeIntPoint;
436 int triangleIndex = -1;
437 QgsGeoTransform *nodeGeoTransform = node->entity()->findChild<QgsGeoTransform *>();
438 Q_ASSERT( nodeGeoTransform );
439 bool success = QgsRayCastingUtils::rayMeshIntersection( rend, ray, context.maximumDistance(), nodeGeoTransform->matrix(), nodeIntPoint, triangleIndex );
440 if ( success )
441 {
442#ifdef QGISDEBUG
443 hits++;
444#endif
445 float dist = ( ray.origin() - nodeIntPoint ).length();
446 if ( minDist < 0 || dist < minDist )
447 {
448 minDist = dist;
449 minNode = node;
450 minTriangleIndex = triangleIndex;
451 intersectionPoint = nodeIntPoint;
452 }
453 }
454 }
455 }
456 }
457
458 if ( minDist >= 0 )
459 {
460 QVariantMap vm;
461 QgsTiledSceneTile tile = mIndex.getTile( minNode->tileId().uniqueId );
462 // at this point this is mostly for debugging - we may want to change/rename what's returned here
463 vm[QStringLiteral( "node_id" )] = tile.id();
464 vm[QStringLiteral( "node_error" )] = tile.geometricError();
465 vm[QStringLiteral( "node_content" )] = tile.resources().value( QStringLiteral( "content" ) );
466 vm[QStringLiteral( "triangle_index" )] = minTriangleIndex;
467
468 QgsRayCastHit hit;
469 hit.setDistance( minDist );
470 hit.setMapCoordinates( mMapSettings->worldToMapCoordinates( intersectionPoint ) );
471 hit.setProperties( vm );
472 result.append( hit );
473 }
474
475 QgsDebugMsgLevel( QStringLiteral( "Active Nodes: %1, checked nodes: %2, hits found: %3" ).arg( nodesAll ).arg( nodeUsed ).arg( hits ), 2 );
476 return result;
477}
478
@ Meters
Meters.
Definition qgis.h:5014
@ Geocentric
Geocentric CRS.
Definition qgis.h:2330
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Definition qgis.h:5671
Axis
Cartesian axes.
Definition qgis.h:2451
@ Y
Y-axis.
Definition qgis.h:2453
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2673
Definition of the world.
Rendering context for preparation of 3D entities.
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system used in the 3D scene.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which stores various information regarding which datum tran...
static QgsAABB mapToWorldExtent(const QgsRectangle &extent, double zMin, double zMax, const QgsVector3D &mapOrigin)
Converts map extent to axis aligned bounding box in 3D world coordinates.
Axis-aligned bounding box - in world coords.
Definition qgsaabb.h:35
float distanceFromPoint(float x, float y, float z) const
Returns shortest distance from the box to a point.
Definition qgsaabb.cpp:46
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:42
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:90
double depth() const
Returns the depth of the box.
Definition qgsbox3d.h:291
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:95
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:258
double width() const
Returns the width of the box.
Definition qgsbox3d.h:277
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:251
double height() const
Returns the height of the box.
Definition qgsbox3d.h:284
static TileContents extractGltfFromTileContent(const QByteArray &tileContent)
Parses tile content.
Represents a coordinate reference system (CRS).
Handles coordinate transforms between two coordinate systems.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
A simple 4x4 matrix implementation useful for transformation in 3D space.
Represents a oriented (rotated) box in 3 dimensions.
const double * halfAxes() const
Returns the half axes matrix;.
bool isNull() const
Returns true if the box is a null box.
QgsVector3D center() const
Returns the vector to the center of the box.
Represents a 2D point.
Definition qgspointxy.h:60
Exception thrown on failure to parse Quantized Mesh tile (malformed data).
A representation of a ray in 3D.
Definition qgsray3d.h:31
QVector3D origin() const
Returns the origin of the ray.
Definition qgsray3d.h:44
Responsible for defining parameters of the ray casting operations in 3D map canvases.
float maximumDistance() const
The maximum distance from ray origin to look for hits when casting a ray.
Contains details about the ray intersecting entities when ray casting in a 3D map canvas.
void setProperties(const QVariantMap &attributes)
Sets the point cloud point attributes, empty map if hit was not on a point cloud point.
void setMapCoordinates(const QgsVector3D &point)
Sets the hit point position in 3d map coordinates.
void setDistance(double distance)
Sets the hit's distance from the ray's origin.
QgsOrientedBox3D box() const
Returns the volume's oriented box.
QgsBox3D bounds(const QgsCoordinateTransform &transform=QgsCoordinateTransform(), Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Returns the axis aligned bounding box of the volume.
An index for tiled scene data providers.
Represents an individual tile from a tiled scene data source.
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.
double geometricError() const
Returns the tile's geometric error, which is the error, in meters, of the tile's simplified represent...
QUrl baseUrl() const
Returns the tile's base URL.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:30
QVector3D toVector3D() const
Converts the current object to QVector3D.
bool rayBoxIntersection(const QgsRay3D &ray, const QgsAABB &nodeBbox)
Tests whether an axis aligned box is intersected by a ray.
bool rayMeshIntersection(Qt3DRender::QGeometryRenderer *geometryRenderer, const QgsRay3D &r, float maxDist, const QMatrix4x4 &worldTransform, QVector3D &intPt, int &triangleIndex)
Tests whether a triangular mesh is intersected by a ray.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
uint qHash(const QVariant &variant)
Hash for QVariant.
Definition qgis.cpp:603
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:61
#define QgsDebugError(str)
Definition qgslogger.h:57
Encapsulates the contents of a 3D tile.
QgsVector3D rtcCenter
Center position of relative-to-center coordinates (when used).
QByteArray gltf
GLTF binary content.