QGIS API Documentation 3.43.0-Master (b60ef06885e)
qgsglobechunkedentity.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsglobechunkedentity.cpp
3 --------------------------------------
4 Date : March 2025
5 Copyright : (C) 2025 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 <QByteArray>
19#include <QImage>
20
21#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
22#include <Qt3DRender/QAttribute>
23#include <Qt3DRender/QBuffer>
24#include <Qt3DRender/QGeometry>
25
26typedef Qt3DRender::QAttribute Qt3DQAttribute;
27typedef Qt3DRender::QBuffer Qt3DQBuffer;
28typedef Qt3DRender::QGeometry Qt3DQGeometry;
29#else
30#include <Qt3DCore/QAttribute>
31#include <Qt3DCore/QBuffer>
32#include <Qt3DCore/QGeometry>
33
34typedef Qt3DCore::QAttribute Qt3DQAttribute;
35typedef Qt3DCore::QBuffer Qt3DQBuffer;
36typedef Qt3DCore::QGeometry Qt3DQGeometry;
37#endif
38
39#include <Qt3DCore/QEntity>
40#include <Qt3DRender/QGeometryRenderer>
41#include <Qt3DRender/QTexture>
42#include <Qt3DRender/QTextureImage>
43#include <Qt3DExtras/QTextureMaterial>
44
45#include "qgs3dmapsettings.h"
46#include "qgs3dutils.h"
47#include "qgschunkloader.h"
50#include "qgsdistancearea.h"
51#include "qgsgeotransform.h"
55
56
58
59static Qt3DCore::QEntity *makeGlobeMesh( double lonMin, double lonMax, double latMin, double latMax, int lonSliceCount, int latSliceCount, const QgsCoordinateTransform &globeCrsToLatLon, QImage textureQImage, QString textureDebugText )
60{
61 double lonRange = lonMax - lonMin;
62 double latRange = latMax - latMin;
63 double lonStep = lonRange / ( double ) ( lonSliceCount - 1 );
64 double latStep = latRange / ( double ) ( latSliceCount - 1 );
65
66 std::vector<double> x, y, z;
67 int pointCount = latSliceCount * lonSliceCount;
68 x.reserve( pointCount );
69 y.reserve( pointCount );
70 z.reserve( pointCount );
71
72 for ( int latSliceIndex = 0; latSliceIndex < latSliceCount; ++latSliceIndex )
73 {
74 double lat = latSliceIndex * latStep + latMin;
75 for ( int lonSliceIndex = 0; lonSliceIndex < lonSliceCount; ++lonSliceIndex )
76 {
77 double lon = lonSliceIndex * lonStep + lonMin;
78 x.push_back( lon );
79 y.push_back( lat );
80 z.push_back( 0 );
81 }
82 }
83
84 globeCrsToLatLon.transformCoords( pointCount, x.data(), y.data(), z.data(), Qgis::TransformDirection::Reverse );
85
86 // estimate origin of coordinates for this tile, to make the relative coordinates
87 // small to avoid numerical precision issues when rendering
88 // (avoids mesh jumping around when zoomed in very close to it)
89 QgsVector3D meshOriginLatLon( ( lonMin + lonMax ) / 2, ( latMin + latMax ) / 2, 0 );
90 QgsVector3D meshOrigin = globeCrsToLatLon.transform( meshOriginLatLon, Qgis::TransformDirection::Reverse );
91
92 int stride = ( 3 + 2 + 3 ) * sizeof( float );
93
94 QByteArray bufferBytes;
95 bufferBytes.resize( stride * pointCount );
96 float *fptr = ( float * ) bufferBytes.data();
97 for ( int i = 0; i < ( int ) pointCount; ++i )
98 {
99 *fptr++ = static_cast<float>( x[i] - meshOrigin.x() );
100 *fptr++ = static_cast<float>( y[i] - meshOrigin.y() );
101 *fptr++ = static_cast<float>( z[i] - meshOrigin.z() );
102
103 int vi = i / lonSliceCount;
104 int ui = i % lonSliceCount;
105 float v = static_cast<float>( vi ) / static_cast<float>( latSliceCount - 1 );
106 float u = static_cast<float>( ui ) / static_cast<float>( lonSliceCount - 1 );
107 *fptr++ = u;
108 *fptr++ = 1 - v;
109
110 QVector3D n = QVector3D( static_cast<float>( x[i] ), static_cast<float>( y[i] ), static_cast<float>( z[i] ) ).normalized();
111 *fptr++ = n.x();
112 *fptr++ = n.y();
113 *fptr++ = n.z();
114 }
115
116 int faces = ( lonSliceCount - 1 ) * ( latSliceCount - 1 ) * 2;
117 int indices = faces * 3;
118
119 QByteArray indexBytes;
120 indexBytes.resize( indices * static_cast<int>( sizeof( ushort ) ) );
121
122 quint16 *indexPtr = reinterpret_cast<quint16 *>( indexBytes.data() );
123 for ( int latSliceIndex = 0; latSliceIndex < latSliceCount - 1; ++latSliceIndex )
124 {
125 int latSliceStartIndex = latSliceIndex * lonSliceCount;
126 int nextLatSliceStartIndex = lonSliceCount + latSliceStartIndex;
127 for ( int lonSliceIndex = 0; lonSliceIndex < lonSliceCount - 1; ++lonSliceIndex )
128 {
129 indexPtr[0] = latSliceStartIndex + lonSliceIndex;
130 indexPtr[1] = lonSliceIndex + latSliceStartIndex + 1;
131 indexPtr[2] = nextLatSliceStartIndex + lonSliceIndex;
132
133 indexPtr[3] = nextLatSliceStartIndex + lonSliceIndex;
134 indexPtr[4] = lonSliceIndex + latSliceStartIndex + 1;
135 indexPtr[5] = lonSliceIndex + nextLatSliceStartIndex + 1;
136
137 indexPtr += 6;
138 }
139 }
140
141 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity;
142
143 Qt3DQBuffer *vertexBuffer = new Qt3DQBuffer( entity );
144 vertexBuffer->setData( bufferBytes );
145
146 Qt3DQBuffer *indexBuffer = new Qt3DQBuffer( entity );
147 indexBuffer->setData( indexBytes );
148
149 Qt3DQAttribute *positionAttribute = new Qt3DQAttribute( entity );
150 positionAttribute->setName( Qt3DQAttribute::defaultPositionAttributeName() );
151 positionAttribute->setVertexBaseType( Qt3DQAttribute::Float );
152 positionAttribute->setVertexSize( 3 );
153 positionAttribute->setAttributeType( Qt3DQAttribute::VertexAttribute );
154 positionAttribute->setBuffer( vertexBuffer );
155 positionAttribute->setByteStride( stride );
156 positionAttribute->setCount( pointCount );
157
158 Qt3DQAttribute *texCoordAttribute = new Qt3DQAttribute( entity );
159 texCoordAttribute->setName( Qt3DQAttribute::defaultTextureCoordinateAttributeName() );
160 texCoordAttribute->setVertexBaseType( Qt3DQAttribute::Float );
161 texCoordAttribute->setVertexSize( 2 );
162 texCoordAttribute->setAttributeType( Qt3DQAttribute::VertexAttribute );
163 texCoordAttribute->setBuffer( vertexBuffer );
164 texCoordAttribute->setByteStride( stride );
165 texCoordAttribute->setByteOffset( 3 * sizeof( float ) );
166 texCoordAttribute->setCount( pointCount );
167
168 Qt3DQAttribute *normalAttribute = new Qt3DQAttribute( entity );
169 normalAttribute->setName( Qt3DQAttribute::defaultNormalAttributeName() );
170 normalAttribute->setVertexBaseType( Qt3DQAttribute::Float );
171 normalAttribute->setVertexSize( 3 );
172 normalAttribute->setAttributeType( Qt3DQAttribute::VertexAttribute );
173 normalAttribute->setBuffer( vertexBuffer );
174 normalAttribute->setByteStride( stride );
175 normalAttribute->setByteOffset( 5 * sizeof( float ) );
176 normalAttribute->setCount( pointCount );
177
178 Qt3DQAttribute *indexAttribute = new Qt3DQAttribute( entity );
179 indexAttribute->setAttributeType( Qt3DQAttribute::IndexAttribute );
180 indexAttribute->setVertexBaseType( Qt3DQAttribute::UnsignedShort );
181 indexAttribute->setBuffer( indexBuffer );
182 indexAttribute->setCount( faces * 3 );
183
184 Qt3DQGeometry *geometry = new Qt3DQGeometry( entity );
185 geometry->addAttribute( positionAttribute );
186 geometry->addAttribute( texCoordAttribute );
187 geometry->addAttribute( normalAttribute );
188 geometry->addAttribute( indexAttribute );
189
190 Qt3DRender::QGeometryRenderer *geomRenderer = new Qt3DRender::QGeometryRenderer( entity );
191 geomRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::Triangles );
192 geomRenderer->setVertexCount( faces * 3 );
193 geomRenderer->setGeometry( geometry );
194
195 QgsTerrainTextureImage *textureImage = new QgsTerrainTextureImage( textureQImage, QgsRectangle( lonMin, latMin, lonMax, latMax ), textureDebugText, entity );
196
197 Qt3DRender::QTexture2D *texture = new Qt3DRender::QTexture2D( entity );
198 texture->addTextureImage( textureImage );
199 texture->setMinificationFilter( Qt3DRender::QTexture2D::Linear );
200 texture->setMagnificationFilter( Qt3DRender::QTexture2D::Linear );
201
202 Qt3DExtras::QTextureMaterial *material = new Qt3DExtras::QTextureMaterial( entity );
203 material->setTexture( texture );
204
205 QgsGeoTransform *geoTransform = new QgsGeoTransform( entity );
206 geoTransform->setGeoTranslation( meshOrigin );
207
208 entity->addComponent( material );
209 entity->addComponent( geomRenderer );
210 entity->addComponent( geoTransform );
211 return entity;
212}
213
214
215static void globeNodeIdToLatLon( QgsChunkNodeId n, double &latMin, double &latMax, double &lonMin, double &lonMax )
216{
217 if ( n == QgsChunkNodeId( 0, 0, 0, 0 ) )
218 {
219 latMin = -90;
220 lonMin = -180;
221 latMax = 90;
222 lonMax = 180;
223 return;
224 }
225
226 double tileSize = 180.0 / std::pow( 2.0, n.d - 1 );
227 lonMin = n.x * tileSize - 180.0;
228 latMin = n.y * tileSize - 90.0;
229 lonMax = lonMin + tileSize;
230 latMax = latMin + tileSize;
231}
232
233
234static QgsBox3D globeNodeIdToBox3D( QgsChunkNodeId n, const QgsCoordinateTransform &globeCrsToLatLon )
235{
236 double latMin, latMax, lonMin, lonMax;
237 globeNodeIdToLatLon( n, latMin, latMax, lonMin, lonMax );
238
239 Q_ASSERT( latMax - latMin <= 90 && lonMax - lonMin <= 90 ); // for larger extents we would need more points than just corners
240
241 QVector<double> x, y, z;
242 int pointCount = 4;
243 x.reserve( pointCount );
244 y.reserve( pointCount );
245 z.reserve( pointCount );
246
247 x.push_back( lonMin );
248 y.push_back( latMin );
249 z.push_back( 0 );
250 x.push_back( lonMin );
251 y.push_back( latMax );
252 z.push_back( 0 );
253 x.push_back( lonMax );
254 y.push_back( latMin );
255 z.push_back( 0 );
256 x.push_back( lonMax );
257 y.push_back( latMax );
258 z.push_back( 0 );
259
260 globeCrsToLatLon.transformCoords( pointCount, x.data(), y.data(), z.data(), Qgis::TransformDirection::Reverse );
261
262 QgsBox3D box( QgsVector3D( x[0], y[0], z[0] ), QgsVector3D( x[1], y[1], z[1] ) );
263 box.combineWith( x[2], y[2], z[2] );
264 box.combineWith( x[3], y[3], z[3] );
265 return box;
266}
267
268
269// ---------------
270
271
272class QgsGlobeChunkLoader : public QgsChunkLoader
273{
274 public:
275 QgsGlobeChunkLoader( QgsChunkNode *node, QgsTerrainTextureGenerator *textureGenerator, const QgsCoordinateTransform &globeCrsToLatLon )
276 : QgsChunkLoader( node )
277 , mTextureGenerator( textureGenerator )
278 , mGlobeCrsToLatLon( globeCrsToLatLon )
279 {
280 connect( mTextureGenerator, &QgsTerrainTextureGenerator::tileReady, this, [=]( int job, const QImage &img ) {
281 if ( job == mJobId )
282 {
283 mTexture = img;
284 emit finished();
285 }
286 } );
287
288 double latMin, latMax, lonMin, lonMax;
289 globeNodeIdToLatLon( node->tileId(), latMin, latMax, lonMin, lonMax );
290 QgsRectangle extent( lonMin, latMin, lonMax, latMax );
291 mJobId = mTextureGenerator->render( extent, node->tileId(), node->tileId().text() );
292 }
293
294 Qt3DCore::QEntity *createEntity( Qt3DCore::QEntity *parent ) override
295 {
296 if ( mNode->tileId() == QgsChunkNodeId( 0, 0, 0, 0 ) )
297 {
298 return new Qt3DCore::QEntity( parent );
299 }
300
301 double latMin, latMax, lonMin, lonMax;
302 globeNodeIdToLatLon( mNode->tileId(), latMin, latMax, lonMin, lonMax );
303
304 // This is quite ad-hoc estimation how many slices we need. It could
305 // be improved by basing the calculation on sagitta
306 int d = mNode->tileId().d;
307 int slices;
308 if ( d <= 4 )
309 slices = 19;
310 else if ( d <= 8 )
311 slices = 9;
312 else if ( d <= 12 )
313 slices = 5;
314 else
315 slices = 2;
316
317 Qt3DCore::QEntity *e = makeGlobeMesh( lonMin, lonMax, latMin, latMax, slices, slices, mGlobeCrsToLatLon, mTexture, mNode->tileId().text() );
318 e->setParent( parent );
319 return e;
320 }
321
322 private:
323 QgsTerrainTextureGenerator *mTextureGenerator;
324 QgsCoordinateTransform mGlobeCrsToLatLon;
325 int mJobId;
326 QImage mTexture;
327};
328
329
330// ---------------
331
332
333class QgsGlobeChunkLoaderFactory : public QgsChunkLoaderFactory
334{
335 public:
336 QgsGlobeChunkLoaderFactory( Qgs3DMapSettings *mapSettings )
337 : mMapSettings( mapSettings )
338 {
339 mTextureGenerator = new QgsTerrainTextureGenerator( *mapSettings );
340
341 // it does not matter what kind of ellipsoid is used, this is for rough estimates
342 mDistanceArea.setEllipsoid( mapSettings->crs().ellipsoidAcronym() );
343
344 mGlobeCrsToLatLon = QgsCoordinateTransform( mapSettings->crs(), mapSettings->crs().toGeographicCrs(), mapSettings->transformContext() );
345
346 mRadiusX = mGlobeCrsToLatLon.transform( QgsVector3D( 0, 0, 0 ), Qgis::TransformDirection::Reverse ).x();
347 mRadiusY = mGlobeCrsToLatLon.transform( QgsVector3D( 90, 0, 0 ), Qgis::TransformDirection::Reverse ).y();
348 mRadiusZ = mGlobeCrsToLatLon.transform( QgsVector3D( 0, 90, 0 ), Qgis::TransformDirection::Reverse ).z();
349 }
350
351 ~QgsGlobeChunkLoaderFactory()
352 {
353 delete mTextureGenerator;
354 }
355
356 QgsChunkLoader *createChunkLoader( QgsChunkNode *node ) const override
357 {
358 return new QgsGlobeChunkLoader( node, mTextureGenerator, mGlobeCrsToLatLon );
359 }
360
361 QgsChunkNode *createRootNode() const override
362 {
363 QgsBox3D rootNodeBox3D( -mRadiusX, -mRadiusY, -mRadiusZ, +mRadiusX, +mRadiusY, +mRadiusZ );
364 // use very high error to force immediate switch to level 1 (two hemispheres)
365 QgsChunkNode *node = new QgsChunkNode( QgsChunkNodeId( 0, 0, 0, 0 ), rootNodeBox3D, 999'999 );
366 return node;
367 }
368
369 QVector<QgsChunkNode *> createChildren( QgsChunkNode *node ) const override
370 {
371 QVector<QgsChunkNode *> children;
372 if ( node->tileId().d == 0 )
373 {
374 double d1 = mDistanceArea.measureLine( QgsPointXY( 0, 0 ), QgsPointXY( 90, 0 ) );
375 double d2 = mDistanceArea.measureLine( QgsPointXY( 0, 0 ), QgsPointXY( 0, 90 ) );
376 float error = static_cast<float>( std::max( d1, d2 ) ) / static_cast<float>( mMapSettings->terrainSettings()->mapTileResolution() );
377
378 QgsBox3D boxWest( -mRadiusX, -mRadiusY, -mRadiusZ, +mRadiusX, 0, +mRadiusZ );
379 QgsBox3D boxEast( -mRadiusX, 0, -mRadiusY, +mRadiusX, +mRadiusY, +mRadiusZ );
380
381 // two children: western and eastern hemisphere
382 QgsChunkNode *west = new QgsChunkNode( QgsChunkNodeId( 1, 0, 0, 0 ), boxWest, error, node );
383 QgsChunkNode *east = new QgsChunkNode( QgsChunkNodeId( 1, 1, 0, 0 ), boxEast, error, node );
384 children << west << east;
385 }
386 else if ( node->error() > mMapSettings->terrainSettings()->maximumGroundError() )
387 {
388 QgsChunkNodeId nid = node->tileId();
389
390 double latMin, latMax, lonMin, lonMax;
391 globeNodeIdToLatLon( nid, latMin, latMax, lonMin, lonMax );
392 QgsChunkNodeId cid1( nid.d + 1, nid.x * 2, nid.y * 2 );
393 QgsChunkNodeId cid2( nid.d + 1, nid.x * 2 + 1, nid.y * 2 );
394 QgsChunkNodeId cid3( nid.d + 1, nid.x * 2, nid.y * 2 + 1 );
395 QgsChunkNodeId cid4( nid.d + 1, nid.x * 2 + 1, nid.y * 2 + 1 );
396
397 double d1 = mDistanceArea.measureLine( QgsPointXY( lonMin, latMin ), QgsPointXY( lonMin + ( lonMax - lonMin ) / 2, latMin ) );
398 double d2 = mDistanceArea.measureLine( QgsPointXY( lonMin, latMin ), QgsPointXY( lonMin, latMin + ( latMax - latMin ) / 2 ) );
399 float error = static_cast<float>( std::max( d1, d2 ) ) / static_cast<float>( mMapSettings->terrainSettings()->mapTileResolution() );
400
401 children << new QgsChunkNode( cid1, globeNodeIdToBox3D( cid1, mGlobeCrsToLatLon ), error, node )
402 << new QgsChunkNode( cid2, globeNodeIdToBox3D( cid2, mGlobeCrsToLatLon ), error, node )
403 << new QgsChunkNode( cid3, globeNodeIdToBox3D( cid3, mGlobeCrsToLatLon ), error, node )
404 << new QgsChunkNode( cid4, globeNodeIdToBox3D( cid4, mGlobeCrsToLatLon ), error, node );
405 }
406
407 return children;
408 }
409
410 private:
411 Qgs3DMapSettings *mMapSettings = nullptr;
412 QgsTerrainTextureGenerator *mTextureGenerator = nullptr; // owned by the factory
413 QgsDistanceArea mDistanceArea;
414 QgsCoordinateTransform mGlobeCrsToLatLon;
415 double mRadiusX, mRadiusY, mRadiusZ;
416};
417
418
419// ---------------
420
421
422QgsGlobeEntity::QgsGlobeEntity( Qgs3DMapSettings *mapSettings )
423 : QgsChunkedEntity( mapSettings, mapSettings->terrainSettings()->maximumScreenError(), new QgsGlobeChunkLoaderFactory( mapSettings ), true )
424{
425}
426
427QgsGlobeEntity::~QgsGlobeEntity()
428{
429 // cancel / wait for jobs
430 cancelActiveJobs();
431}
432
433QVector<QgsRayCastingUtils::RayHit> QgsGlobeEntity::rayIntersection( const QgsRayCastingUtils::Ray3D &ray, const QgsRayCastingUtils::RayCastContext &context ) const
434{
435 Q_UNUSED( context );
436
437 float minDist = -1;
438 QVector3D intersectionPoint;
439 const QList<QgsChunkNode *> active = activeNodes();
440 for ( QgsChunkNode *node : active )
441 {
442 QgsAABB nodeBbox = Qgs3DUtils::mapToWorldExtent( node->box3D(), mMapSettings->origin() );
443
444 if ( node->entity() && ( minDist < 0 || nodeBbox.distanceFromPoint( ray.origin() ) < minDist ) && QgsRayCastingUtils::rayBoxIntersection( ray, nodeBbox ) )
445 {
446 QgsGeoTransform *nodeGeoTransform = node->entity()->findChild<QgsGeoTransform *>();
447 Q_ASSERT( nodeGeoTransform );
448 const QList<Qt3DRender::QGeometryRenderer *> rendLst = node->entity()->findChildren<Qt3DRender::QGeometryRenderer *>();
449 for ( Qt3DRender::QGeometryRenderer *rend : rendLst )
450 {
451 QVector3D nodeIntPoint;
452 int triangleIndex = -1;
453 bool success = QgsRayCastingUtils::rayMeshIntersection( rend, ray, nodeGeoTransform->matrix(), nodeIntPoint, triangleIndex );
454 if ( success )
455 {
456 float dist = ( ray.origin() - nodeIntPoint ).length();
457 if ( minDist < 0 || dist < minDist )
458 {
459 minDist = dist;
460 intersectionPoint = nodeIntPoint;
461 }
462 }
463 }
464 }
465 }
466
467 QVector<QgsRayCastingUtils::RayHit> result;
468 if ( minDist >= 0 )
469 {
470 result.append( QgsRayCastingUtils::RayHit( minDist, intersectionPoint ) );
471 }
472 return result;
473}
474
475
@ Reverse
Reverse/inverse transform (from destination to source)
Definition of the world.
QgsCoordinateReferenceSystem crs() const
Returns 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
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:43
QString ellipsoidAcronym() const
Returns the ellipsoid acronym for the ellipsoid used by the CRS.
QgsCoordinateReferenceSystem toGeographicCrs() const
Returns the geographic CRS associated with this CRS object.
Handles coordinate transforms between two coordinate systems.
void transformCoords(int numPoint, double *x, double *y, double *z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform an array of coordinates to the destination CRS.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
A general purpose distance and area calculator, capable of performing ellipsoid based calculations.
Represents a 2D point.
Definition qgspointxy.h:60
double x
Definition qgspointxy.h:63
A rectangle specified with double values.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:30
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:49
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:51
double x() const
Returns X coordinate.
Definition qgsvector3d.h:47
Qt3DCore::QAttribute Qt3DQAttribute
Qt3DCore::QBuffer Qt3DQBuffer
Qt3DCore::QGeometry Qt3DQGeometry
Qt3DCore::QAttribute Qt3DQAttribute
Qt3DCore::QBuffer Qt3DQBuffer
Qt3DCore::QGeometry Qt3DQGeometry
Helper struct to store ray casting parameters.
Helper struct to store ray casting results.