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