QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
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 <QString>
34#include <Qt3DRender/QGeometryRenderer>
35#include <QtConcurrentRun>
36
37#include "moc_qgstiledscenechunkloader_p.cpp"
38
39using namespace Qt::StringLiterals;
40
42
43size_t qHash( const QgsChunkNodeId &n )
44{
45 return n.uniqueId;
46}
47
48static bool hasLargeBounds( const QgsTiledSceneTile &t, const QgsCoordinateTransform &boundsTransform )
49{
50 if ( t.geometricError() > 1e6 )
51 return true;
52
53 if ( t.boundingVolume().box().isNull() )
54 return true;
55
56 Q_ASSERT( boundsTransform.destinationCrs().mapUnits() == Qgis::DistanceUnit::Meters );
57 QgsBox3D bounds = t.boundingVolume().bounds( boundsTransform );
58 return bounds.width() > 1e5 || bounds.height() > 1e5 || bounds.depth() > 1e5;
59}
60
62
63QgsTiledSceneChunkLoader::QgsTiledSceneChunkLoader( QgsChunkNode *node, const QgsTiledSceneIndex &index, const QgsTiledSceneChunkLoaderFactory &factory, double zValueScale, double zValueOffset )
64 : QgsChunkLoader( node )
65 , mFactory( factory )
66 , mZValueScale( zValueScale )
67 , mZValueOffset( zValueOffset )
68 , mIndex( index )
69{}
70
71void QgsTiledSceneChunkLoader::start()
72{
73 QgsChunkNode *node = chunk();
74
75 mFutureWatcher = new QFutureWatcher<void>( this );
76 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
77
78 const QgsCoordinateTransform &boundsTransform = mFactory.mBoundsTransform;
79
80 const QgsChunkNodeId tileId = node->tileId();
81 const QgsVector3D chunkOrigin = node->box3D().center();
82 const bool isGlobe = mFactory.mRenderContext.crs().type() == Qgis::CrsType::Geocentric;
83 const QFuture<void> future = QtConcurrent::run( [this, tileId, boundsTransform, chunkOrigin, isGlobe] {
84 const QgsTiledSceneTile tile = mIndex.getTile( tileId.uniqueId );
85
86 // we do not load tiles that are too big when not in globe scene mode...
87 // the problem is that their 3D bounding boxes with ECEF coordinates are huge
88 // and we are unable to turn them into planar bounding boxes
89 if ( !isGlobe && hasLargeBounds( tile, boundsTransform ) )
90 return;
91
92 QString uri = tile.resources().value( u"content"_s ).toString();
93 if ( uri.isEmpty() )
94 {
95 // nothing to show for this tile
96 // TODO: can we skip loading it at all?
97 return;
98 }
99
100 uri = tile.baseUrl().resolved( uri ).toString();
101 QByteArray content = mFactory.mIndex.retrieveContent( uri );
102 if ( content.isEmpty() )
103 {
104 // the request probably failed
105 // TODO: how can we report it?
106 return;
107 }
108
109 QgsGltf3DUtils::EntityTransform entityTransform;
110 entityTransform.tileTransform = ( tile.transform() ? *tile.transform() : QgsMatrix4x4() );
111 entityTransform.chunkOriginTargetCrs = chunkOrigin;
112 entityTransform.ecefToTargetCrs = &mFactory.mBoundsTransform;
113 entityTransform.zValueScale = mZValueScale;
114 entityTransform.zValueOffset = mZValueOffset;
115 entityTransform.gltfUpAxis = static_cast<Qgis::Axis>( tile.metadata().value( u"gltfUpAxis"_s, static_cast<int>( Qgis::Axis::Y ) ).toInt() );
116
117 const QString &format = tile.metadata().value( u"contentFormat"_s ).value<QString>();
118 QStringList errors;
119 if ( format == "quantizedmesh"_L1 )
120 {
121 try
122 {
123 QgsQuantizedMeshTile qmTile( content );
124 qmTile.removeDegenerateTriangles();
125 tinygltf::Model model = qmTile.toGltf( true, 100 );
126 mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, uri, &errors );
127 }
129 {
130 errors.append( u"Failed to parse tile from '%1'"_s.arg( uri ) );
131 }
132 }
133 else if ( format == "cesiumtiles"_L1 )
134 {
136 if ( tileContent.gltf.isEmpty() )
137 return;
138 entityTransform.tileTransform.translate( tileContent.rtcCenter );
139 mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
140 }
141 else if ( format == "draco"_L1 )
142 {
143 QgsGltfUtils::I3SNodeContext i3sContext;
144 i3sContext.initFromTile( tile, mFactory.mLayerCrs, mFactory.mBoundsTransform.sourceCrs(), mFactory.mRenderContext.transformContext() );
145
146 QString dracoLoadError;
147 tinygltf::Model model;
148 if ( !QgsGltfUtils::loadDracoModel( content, i3sContext, model, &dracoLoadError ) )
149 {
150 errors.append( dracoLoadError );
151 return;
152 }
153
154 mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, QString(), &errors );
155 }
156 else
157 return; // unsupported tile content type
158
159 // TODO: report errors somewhere?
160 if ( !errors.isEmpty() )
161 {
162 QgsDebugError( "gltf load errors: " + errors.join( '\n' ) );
163 }
164
165 if ( mEntity )
166 {
167 QgsGeoTransform *transform = new QgsGeoTransform;
168 transform->setGeoTranslation( chunkOrigin );
169 mEntity->addComponent( transform );
170
171 mEntity->moveToThread( QgsApplication::instance()->thread() );
172 }
173 } );
174
175 // emit finished() as soon as the handler is populated with features
176 mFutureWatcher->setFuture( future );
177}
178
179QgsTiledSceneChunkLoader::~QgsTiledSceneChunkLoader()
180{
181 if ( !mFutureWatcher->isFinished() )
182 {
183 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
184 mFutureWatcher->waitForFinished();
185 }
186}
187
188Qt3DCore::QEntity *QgsTiledSceneChunkLoader::createEntity( Qt3DCore::QEntity *parent )
189{
190 if ( mEntity )
191 mEntity->setParent( parent );
192 return mEntity;
193}
194
196
197QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory(
198 const Qgs3DRenderContext &context, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, QgsCoordinateReferenceSystem layerCrs, double zValueScale, double zValueOffset
199)
200 : mRenderContext( context )
201 , mIndex( index )
202 , mZValueScale( zValueScale )
203 , mZValueOffset( zValueOffset )
204 , mLayerCrs( layerCrs )
205{
206 mBoundsTransform = QgsCoordinateTransform( tileCrs, context.crs(), context.transformContext() );
207}
208
209QgsChunkLoader *QgsTiledSceneChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
210{
211 return new QgsTiledSceneChunkLoader( node, mIndex, *this, mZValueScale, mZValueOffset );
212}
213
214QgsChunkNode *QgsTiledSceneChunkLoaderFactory::nodeForTile( const QgsTiledSceneTile &t, const QgsChunkNodeId &nodeId, QgsChunkNode *parent ) const
215{
216 QgsChunkNode *node = nullptr;
217 if ( mRenderContext.crs().type() != Qgis::CrsType::Geocentric && hasLargeBounds( t, mBoundsTransform ) )
218 {
219 // use the full extent of the scene
220 QgsVector3D v0( mRenderContext.extent().xMinimum(), mRenderContext.extent().yMinimum(), -100 );
221 QgsVector3D v1( mRenderContext.extent().xMaximum(), mRenderContext.extent().yMaximum(), +100 );
222 QgsBox3D box3D( v0, v1 );
223 float err = std::min( 1e6, t.geometricError() );
224 node = new QgsChunkNode( nodeId, box3D, err, parent );
225 }
226 else
227 {
228 QgsBox3D box = t.boundingVolume().bounds( mBoundsTransform );
229 box.setZMinimum( box.zMinimum() * mZValueScale + mZValueOffset );
230 box.setZMaximum( box.zMaximum() * mZValueScale + mZValueOffset );
231 node = new QgsChunkNode( nodeId, box, t.geometricError(), parent );
232 }
233
234 node->setRefinementProcess( t.refinementProcess() );
235 return node;
236}
237
238
239QgsChunkNode *QgsTiledSceneChunkLoaderFactory::createRootNode() const
240{
241 const QgsTiledSceneTile t = mIndex.rootTile();
242 return nodeForTile( t, QgsChunkNodeId( t.id() ), nullptr );
243}
244
245
246QVector<QgsChunkNode *> QgsTiledSceneChunkLoaderFactory::createChildren( QgsChunkNode *node ) const
247{
248 QVector<QgsChunkNode *> children;
249 const long long indexTileId = node->tileId().uniqueId;
250
251 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
252 Q_ASSERT( mIndex.childAvailability( indexTileId ) != Qgis::TileChildrenAvailability::NeedFetching );
253
254 const QVector<long long> childIds = mIndex.childTileIds( indexTileId );
255 for ( long long childId : childIds )
256 {
257 const QgsChunkNodeId chId( childId );
258 QgsTiledSceneTile t = mIndex.getTile( childId );
259
260 // first check if this node should be even considered
261 // XXX: This check doesn't work for Quantized Mesh layers and possibly some
262 // Cesium 3D tiles as well. For now this hack is in place to make sure both
263 // work in practice.
264 if ( t.metadata()["contentFormat"] == "cesiumtiles"_L1 && mRenderContext.crs().type() != Qgis::CrsType::Geocentric && hasLargeBounds( t, mBoundsTransform ) )
265 {
266 // if the tile is huge, let's try to see if our scene is actually inside
267 // (if not, let' skip this child altogether!)
268 // TODO: make OBB of our scene in ECEF rather than just using center of the scene?
269 const QgsOrientedBox3D obb = t.boundingVolume().box();
270 const QgsPointXY c = mRenderContext.extent().center();
271 const QgsVector3D cEcef = mBoundsTransform.transform( QgsVector3D( c.x(), c.y(), 0 ), Qgis::TransformDirection::Reverse );
272 const QgsVector3D ecef2 = cEcef - obb.center();
273 const double *half = obb.halfAxes();
274 // this is an approximate check anyway, no need for double precision matrix/vector
275 // clang-format off
276 QMatrix4x4 rot(
277 half[0], half[3], half[6], 0,
278 half[1], half[4], half[7], 0,
279 half[2], half[5], half[8], 0,
280 0, 0, 0, 1
281 );
282 // clang-format on
283 QVector3D aaa = rot.inverted().map( ecef2.toVector3D() );
284 if ( aaa.x() > 1 || aaa.y() > 1 || aaa.z() > 1 || aaa.x() < -1 || aaa.y() < -1 || aaa.z() < -1 )
285 {
286 continue;
287 }
288 }
289
290 // fetching of hierarchy is handled by canCreateChildren() + prepareChildren()
291 Q_ASSERT( mIndex.childAvailability( childId ) != Qgis::TileChildrenAvailability::NeedFetching );
292
293 QgsChunkNode *nChild = nodeForTile( t, chId, node );
294 children.append( nChild );
295 }
296 return children;
297}
298
299bool QgsTiledSceneChunkLoaderFactory::canCreateChildren( QgsChunkNode *node )
300{
301 long long nodeId = node->tileId().uniqueId;
302 if ( mFutureHierarchyFetches.contains( nodeId ) || mPendingHierarchyFetches.contains( nodeId ) )
303 return false;
304
305 if ( mIndex.childAvailability( nodeId ) == Qgis::TileChildrenAvailability::NeedFetching )
306 {
307 mFutureHierarchyFetches.insert( nodeId );
308 return false;
309 }
310
311 // we need to make sure that if a child tile's content references another tileset JSON,
312 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
313 // end up trying to load tileset JSON file instead of the actual content
314
315 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
316 for ( long long childId : childIds )
317 {
318 if ( mFutureHierarchyFetches.contains( childId ) || mPendingHierarchyFetches.contains( childId ) )
319 return false;
320
321 if ( mIndex.childAvailability( childId ) == Qgis::TileChildrenAvailability::NeedFetching )
322 {
323 mFutureHierarchyFetches.insert( childId );
324 return false;
325 }
326 }
327 return true;
328}
329
330void QgsTiledSceneChunkLoaderFactory::fetchHierarchyForNode( long long nodeId, QgsChunkNode *origNode )
331{
332 Q_ASSERT( !mPendingHierarchyFetches.contains( nodeId ) );
333 mFutureHierarchyFetches.remove( nodeId );
334 mPendingHierarchyFetches.insert( nodeId );
335
336 QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>( this );
337 connect( futureWatcher, &QFutureWatcher<void>::finished, this, [this, origNode, nodeId, futureWatcher] {
338 mPendingHierarchyFetches.remove( nodeId );
339 emit childrenPrepared( origNode );
340 futureWatcher->deleteLater();
341 } );
342 futureWatcher->setFuture( QtConcurrent::run( [this, nodeId] { mIndex.fetchHierarchy( nodeId ); } ) );
343}
344
345void QgsTiledSceneChunkLoaderFactory::prepareChildren( QgsChunkNode *node )
346{
347 long long nodeId = node->tileId().uniqueId;
348 if ( mFutureHierarchyFetches.contains( nodeId ) )
349 {
350 fetchHierarchyForNode( nodeId, node );
351 return;
352 }
353
354 // we need to make sure that if a child tile's content references another tileset JSON,
355 // we fetch its hierarchy before a chunk node is created for such child tile - otherwise we
356 // end up trying to load tileset JSON file instead of the actual content
357
358 const QVector<long long> childIds = mIndex.childTileIds( nodeId );
359 for ( long long childId : childIds )
360 {
361 if ( mFutureHierarchyFetches.contains( childId ) )
362 {
363 fetchHierarchyForNode( childId, node );
364 }
365 }
366}
367
368
370
371QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity(
372 Qgs3DMapSettings *map,
373 const QgsTiledSceneIndex &index,
376 double maximumScreenError,
377 bool showBoundingBoxes,
378 double zValueScale,
379 double zValueOffset
380)
381 : QgsChunkedEntity( map, maximumScreenError, new QgsTiledSceneChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), index, tileCrs, layerCrs, zValueScale, zValueOffset ), true )
382 , mIndex( index )
383{
384 setShowBoundingBoxes( showBoundingBoxes );
385}
386
387QgsTiledSceneLayerChunkedEntity::~QgsTiledSceneLayerChunkedEntity()
388{
389 // cancel / wait for jobs
390 cancelActiveJobs();
391}
392
393int QgsTiledSceneLayerChunkedEntity::pendingJobsCount() const
394{
395 return QgsChunkedEntity::pendingJobsCount() + static_cast<QgsTiledSceneChunkLoaderFactory *>( mChunkLoaderFactory )->mPendingHierarchyFetches.count();
396}
397
398QList<QgsRayCastHit> QgsTiledSceneLayerChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
399{
400 Q_UNUSED( context );
401 QgsDebugMsgLevel( u"Ray cast on tiled scene layer"_s, 2 );
402#ifdef QGISDEBUG
403 int nodeUsed = 0;
404 int nodesAll = 0;
405 int hits = 0;
406#endif
407
408 QList<QgsRayCastHit> result;
409 float minDist = -1;
410 QVector3D intersectionPoint;
411 QgsChunkNode *minNode = nullptr;
412 int minTriangleIndex = -1;
413
414 const QList<QgsChunkNode *> active = activeNodes();
415 for ( QgsChunkNode *node : active )
416 {
417#ifdef QGISDEBUG
418 nodesAll++;
419#endif
420
421 QgsAABB nodeBbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
422
423 if ( node->entity() && ( minDist < 0 || nodeBbox.distanceFromPoint( ray.origin() ) < minDist ) && QgsRayCastingUtils::rayBoxIntersection( ray, nodeBbox ) )
424 {
425#ifdef QGISDEBUG
426 nodeUsed++;
427#endif
428 const QList<Qt3DRender::QGeometryRenderer *> rendLst = node->entity()->findChildren<Qt3DRender::QGeometryRenderer *>();
429 for ( Qt3DRender::QGeometryRenderer *rend : rendLst )
430 {
431 QVector3D nodeIntPoint;
432 int triangleIndex = -1;
433 QgsGeoTransform *nodeGeoTransform = node->entity()->findChild<QgsGeoTransform *>();
434 Q_ASSERT( nodeGeoTransform );
435 bool success = QgsRayCastingUtils::rayMeshIntersection( rend, ray, context.maximumDistance(), nodeGeoTransform->matrix(), nodeIntPoint, triangleIndex );
436 if ( success )
437 {
438#ifdef QGISDEBUG
439 hits++;
440#endif
441 float dist = ( ray.origin() - nodeIntPoint ).length();
442 if ( minDist < 0 || dist < minDist )
443 {
444 minDist = dist;
445 minNode = node;
446 minTriangleIndex = triangleIndex;
447 intersectionPoint = nodeIntPoint;
448 }
449 }
450 }
451 }
452 }
453
454 if ( minDist >= 0 )
455 {
456 QVariantMap vm;
457 QgsTiledSceneTile tile = mIndex.getTile( minNode->tileId().uniqueId );
458 // at this point this is mostly for debugging - we may want to change/rename what's returned here
459 vm[u"node_id"_s] = tile.id();
460 vm[u"node_error"_s] = tile.geometricError();
461 vm[u"node_content"_s] = tile.resources().value( u"content"_s );
462 vm[u"triangle_index"_s] = minTriangleIndex;
463
464 QgsRayCastHit hit;
465 hit.setDistance( minDist );
466 hit.setMapCoordinates( mMapSettings->worldToMapCoordinates( intersectionPoint ) );
467 hit.setProperties( vm );
468 result.append( hit );
469 }
470
471 QgsDebugMsgLevel( u"Active Nodes: %1, checked nodes: %2, hits found: %3"_s.arg( nodesAll ).arg( nodeUsed ).arg( hits ), 2 );
472 return result;
473}
474
@ Meters
Meters.
Definition qgis.h:5171
@ Geocentric
Geocentric CRS.
Definition qgis.h:2412
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Definition qgis.h:6027
Axis
Cartesian axes.
Definition qgis.h:2540
@ Y
Y-axis.
Definition qgis.h:2542
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2766
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:33
float distanceFromPoint(float x, float y, float z) const
Returns shortest distance from the box to a point.
Definition qgsaabb.cpp:50
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:94
double depth() const
Returns the depth of the box.
Definition qgsbox3d.h:301
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:99
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:268
double width() const
Returns the width of the box.
Definition qgsbox3d.h:287
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:261
double height() const
Returns the height of the box.
Definition qgsbox3d.h:294
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:62
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:43
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:33
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:611
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59
Encapsulates the contents of a 3D tile.
QgsVector3D rtcCenter
Center position of relative-to-center coordinates (when used).
QByteArray gltf
GLTF binary content.