QGIS API Documentation 4.1.0-Master (659fe69c07c)
Loading...
Searching...
No Matches
qgsgltf3dutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsgltf3dutils.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
16
17#include "qgsgltf3dutils.h"
18
19#include <memory>
20
21#include "qgs3dutils.h"
24#include "qgsgltfutils.h"
25#include "qgslogger.h"
28#include "qgstexturematerial.h"
29#include "qgsziputils.h"
30
31#include <QFile>
32#include <QFileInfo>
33#include <QHash>
34#include <QMatrix4x4>
35#include <QSet>
36#include <QString>
37#include <Qt3DCore/QAttribute>
38#include <Qt3DCore/QBuffer>
39#include <Qt3DCore/QEntity>
40#include <Qt3DCore/QGeometry>
41#include <Qt3DRender/QGeometryRenderer>
42#include <Qt3DRender/QTexture>
43
44using namespace Qt::StringLiterals;
45
47
48static Qt3DCore::QAttribute::VertexBaseType parseVertexBaseType( int componentType )
49{
50 switch ( componentType )
51 {
52 case TINYGLTF_COMPONENT_TYPE_BYTE:
53 return Qt3DCore::QAttribute::Byte;
54 case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
55 return Qt3DCore::QAttribute::UnsignedByte;
56 case TINYGLTF_COMPONENT_TYPE_SHORT:
57 return Qt3DCore::QAttribute::Short;
58 case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
59 return Qt3DCore::QAttribute::UnsignedShort;
60 case TINYGLTF_COMPONENT_TYPE_INT:
61 return Qt3DCore::QAttribute::Int;
62 case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
63 return Qt3DCore::QAttribute::UnsignedInt;
64 case TINYGLTF_COMPONENT_TYPE_FLOAT:
65 return Qt3DCore::QAttribute::Float;
66 case TINYGLTF_COMPONENT_TYPE_DOUBLE:
67 return Qt3DCore::QAttribute::Double;
68 }
69 Q_ASSERT( false );
70 return Qt3DCore::QAttribute::UnsignedInt;
71}
72
73
74static Qt3DRender::QAbstractTexture::Filter parseTextureFilter( int filter )
75{
76 switch ( filter )
77 {
78 case TINYGLTF_TEXTURE_FILTER_NEAREST:
79 return Qt3DRender::QTexture2D::Nearest;
80 case TINYGLTF_TEXTURE_FILTER_LINEAR:
81 return Qt3DRender::QTexture2D::Linear;
82 case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST:
83 return Qt3DRender::QTexture2D::NearestMipMapNearest;
84 case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST:
85 return Qt3DRender::QTexture2D::LinearMipMapNearest;
86 case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR:
87 return Qt3DRender::QTexture2D::NearestMipMapLinear;
88 case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR:
89 return Qt3DRender::QTexture2D::LinearMipMapLinear;
90 }
91
92 // play it safe and handle malformed models
93 return Qt3DRender::QTexture2D::Nearest;
94}
95
96static Qt3DRender::QTextureWrapMode::WrapMode parseTextureWrapMode( int wrapMode )
97{
98 switch ( wrapMode )
99 {
100 case TINYGLTF_TEXTURE_WRAP_REPEAT:
101 return Qt3DRender::QTextureWrapMode::Repeat;
102 case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
103 return Qt3DRender::QTextureWrapMode::ClampToEdge;
104 case TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT:
105 return Qt3DRender::QTextureWrapMode::MirroredRepeat;
106 }
107 // some malformed GLTF models have incorrect texture wrap modes (eg
108 // https://qld.digitaltwin.terria.io/api/v0/data/b73ccb60-66ef-4470-8c3c-44af36c4d69b/CBD/tileset.json )
109 return Qt3DRender::QTextureWrapMode::Repeat;
110}
111
112
113static Qt3DCore::QAttribute *parseAttribute( tinygltf::Model &model, int accessorIndex )
114{
115 tinygltf::Accessor &accessor = model.accessors[accessorIndex];
116 tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
117 tinygltf::Buffer &b = model.buffers[bv.buffer];
118
119 // TODO: only ever create one QBuffer for a buffer even if it is used multiple times
120 QByteArray byteArray( reinterpret_cast<const char *>( b.data.data() ),
121 static_cast<int>( b.data.size() ) ); // makes a deep copy
122 Qt3DCore::QBuffer *buffer = new Qt3DCore::QBuffer();
123 buffer->setData( byteArray );
124
125 Qt3DCore::QAttribute *attribute = new Qt3DCore::QAttribute();
126
127 // "target" is optional, can be zero
128 if ( bv.target == TINYGLTF_TARGET_ARRAY_BUFFER )
129 attribute->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
130 else if ( bv.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER )
131 attribute->setAttributeType( Qt3DCore::QAttribute::IndexAttribute );
132
133 attribute->setBuffer( buffer );
134 attribute->setByteOffset( bv.byteOffset + accessor.byteOffset );
135 attribute->setByteStride( bv.byteStride ); // could be zero, it seems that's fine (assuming packed)
136 attribute->setCount( accessor.count );
137 attribute->setVertexBaseType( parseVertexBaseType( accessor.componentType ) );
138 attribute->setVertexSize( tinygltf::GetNumComponentsInType( accessor.type ) );
139
140 return attribute;
141}
142
143
144static Qt3DCore::QAttribute *reprojectPositions( tinygltf::Model &model, int accessorIndex, const QgsGltf3DUtils::EntityTransform &transform, const QgsVector3D &tileTranslationEcef, QMatrix4x4 *matrix )
145{
146 tinygltf::Accessor &accessor = model.accessors[accessorIndex];
147
148 QVector<double> vx, vy, vz;
149 bool res = QgsGltfUtils::accessorToMapCoordinates( model, accessorIndex, transform.tileTransform, transform.ecefToTargetCrs, tileTranslationEcef, matrix, transform.gltfUpAxis, vx, vy, vz );
150 if ( !res )
151 return nullptr;
152
153 QByteArray byteArray;
154 byteArray.resize( accessor.count * 4 * 3 );
155 float *out = reinterpret_cast<float *>( byteArray.data() );
156
157 QgsVector3D sceneOrigin = transform.chunkOriginTargetCrs;
158 for ( int i = 0; i < static_cast<int>( accessor.count ); ++i )
159 {
160 double x = vx[i] - sceneOrigin.x();
161 double y = vy[i] - sceneOrigin.y();
162 double z = ( vz[i] * transform.zValueScale ) + transform.zValueOffset - sceneOrigin.z();
163
164 out[i * 3 + 0] = static_cast<float>( x );
165 out[i * 3 + 1] = static_cast<float>( y );
166 out[i * 3 + 2] = static_cast<float>( z );
167 }
168
169 Qt3DCore::QBuffer *buffer = new Qt3DCore::QBuffer();
170 buffer->setData( byteArray );
171
172 Qt3DCore::QAttribute *attribute = new Qt3DCore::QAttribute();
173 attribute->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
174 attribute->setBuffer( buffer );
175 attribute->setByteOffset( 0 );
176 attribute->setByteStride( 12 );
177 attribute->setCount( accessor.count );
178 attribute->setVertexBaseType( Qt3DCore::QAttribute::Float );
179 attribute->setVertexSize( 3 );
180
181 return attribute;
182}
183
184class TinyGltfTextureImageDataGenerator : public Qt3DRender::QTextureImageDataGenerator
185{
186 public:
187 TinyGltfTextureImageDataGenerator( Qt3DRender::QTextureImageDataPtr imagePtr )
188 : mImagePtr( imagePtr )
189 {}
190
191 Qt3DRender::QTextureImageDataPtr operator()() override { return mImagePtr; }
192
193 qintptr id() const override { return reinterpret_cast<qintptr>( &Qt3DCore::FunctorType<TinyGltfTextureImageDataGenerator>::id ); }
194
195 bool operator==( const QTextureImageDataGenerator &other ) const override
196 {
197 const TinyGltfTextureImageDataGenerator *otherFunctor = dynamic_cast<const TinyGltfTextureImageDataGenerator *>( &other );
198 return otherFunctor && mImagePtr.get() == otherFunctor->mImagePtr.get();
199 }
200
201 Qt3DRender::QTextureImageDataPtr mImagePtr;
202};
203
204class TinyGltfTextureImage : public Qt3DRender::QAbstractTextureImage
205{
206 Q_OBJECT
207 public:
208 TinyGltfTextureImage( tinygltf::Image &image )
209 {
210 Q_ASSERT( image.bits == 8 );
211 Q_ASSERT( image.component == 4 );
212 Q_ASSERT( image.pixel_type == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE );
213
214 imgDataPtr.reset( new Qt3DRender::QTextureImageData );
215 imgDataPtr->setWidth( image.width );
216 imgDataPtr->setHeight( image.height );
217 imgDataPtr->setDepth( 1 ); // not sure what this is
218 imgDataPtr->setFaces( 1 );
219 imgDataPtr->setLayers( 1 );
220 imgDataPtr->setMipLevels( 1 );
221 QByteArray imageBytes( reinterpret_cast<const char *>( image.image.data() ), image.image.size() );
222 imgDataPtr->setData( imageBytes, 4 );
223 imgDataPtr->setFormat( QOpenGLTexture::RGBA8_UNorm );
224 imgDataPtr->setPixelFormat( QOpenGLTexture::BGRA ); // when using tinygltf with STB_image, pixel format is QOpenGLTexture::RGBA
225 imgDataPtr->setPixelType( QOpenGLTexture::UInt8 );
226 imgDataPtr->setTarget( QOpenGLTexture::Target2D );
227 }
228
229 Qt3DRender::QTextureImageDataGeneratorPtr dataGenerator() const override { return Qt3DRender::QTextureImageDataGeneratorPtr( new TinyGltfTextureImageDataGenerator( imgDataPtr ) ); }
230
231 Qt3DRender::QTextureImageDataPtr imgDataPtr;
232};
233
234
235// TODO: move elsewhere
236static QByteArray fetchUri( const QUrl &url, QStringList *errors )
237{
238 if ( url.scheme().startsWith( "http" ) )
239 {
240 QNetworkRequest request = QNetworkRequest( url );
241 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
242 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
243 QgsBlockingNetworkRequest networkRequest;
244 // TODO: setup auth, setup headers
245 if ( networkRequest.get( request ) != QgsBlockingNetworkRequest::NoError )
246 {
247 if ( errors )
248 *errors << u"Failed to download image: %1"_s.arg( url.toString() );
249 }
250 else
251 {
252 const QgsNetworkReplyContent content = networkRequest.reply();
253 return content.content();
254 }
255 }
256 else if ( url.isLocalFile() )
257 {
258 QString localFilePath = url.toLocalFile();
259 if ( localFilePath.contains( ".slpk/" ) ) // we need to extract the image from SLPK archive
260 {
261 const QStringList parts = localFilePath.split( u".slpk/"_s );
262 if ( parts.size() == 2 )
263 {
264 QString slpkPath = parts[0] + ".slpk";
265 QString imagePath = parts[1];
266
267 QByteArray imageData;
268 if ( QgsZipUtils::extractFileFromZip( slpkPath, imagePath, imageData ) )
269 {
270 return imageData;
271 }
272 else
273 {
274 if ( errors )
275 *errors << u"Unable to extract image '%1' from SLPK archive: %2"_s.arg( imagePath ).arg( slpkPath );
276 }
277 }
278 else
279 {
280 if ( errors )
281 *errors << u"Missing image path in SLPK archive: %1"_s.arg( localFilePath );
282 }
283 }
284 else if ( QFile::exists( localFilePath ) )
285 {
286 QFile f( localFilePath );
287 if ( f.open( QIODevice::ReadOnly ) )
288 {
289 return f.readAll();
290 }
291 }
292 else
293 {
294 if ( errors )
295 *errors << u"Unable to open image: %1"_s.arg( url.toString() );
296 }
297 }
298 return QByteArray();
299}
300
301// Returns NULLPTR if primitive should not be rendered
302static std::unique_ptr<QgsMaterial> parseMaterial( tinygltf::Model &model, int materialIndex, QString baseUri, QStringList *errors, const QgsMaterialContext &context )
303{
304 if ( materialIndex < 0 )
305 {
306 // material unspecified - using default
307 auto defaultMaterial = std::make_unique<QgsMetalRoughMaterial>();
308 defaultMaterial->setEnvironmentalLightingEnabled( true );
309 defaultMaterial->setMetalness( 1 );
310 defaultMaterial->setRoughness( 1 );
311 defaultMaterial->setBaseColor( QColor::fromRgbF( 1, 1, 1 ) );
312 return defaultMaterial;
313 }
314
315 tinygltf::Material &material = model.materials[materialIndex];
316 tinygltf::PbrMetallicRoughness &pbr = material.pbrMetallicRoughness;
317
318 if ( pbr.baseColorTexture.index >= 0 )
319 {
320 tinygltf::Texture &tex = model.textures[pbr.baseColorTexture.index];
321
322 // Source can be undefined if texture is provided by an extension
323 if ( tex.source < 0 )
324 {
325 auto pbrMaterial = std::make_unique<QgsMetalRoughMaterial>();
326 pbrMaterial->setEnvironmentalLightingEnabled( true );
327 pbrMaterial->setMetalness( pbr.metallicFactor ); // [0..1] or texture
328 pbrMaterial->setRoughness( pbr.roughnessFactor );
329 pbrMaterial->setBaseColor( QColor::fromRgbF( pbr.baseColorFactor[0], pbr.baseColorFactor[1], pbr.baseColorFactor[2], pbr.baseColorFactor[3] ) );
330 return pbrMaterial;
331 }
332
333 tinygltf::Image &img = model.images[tex.source];
334
335 if ( !img.uri.empty() )
336 {
337 QString imgUri = QString::fromStdString( img.uri );
338 QUrl url = QUrl( baseUri ).resolved( imgUri );
339 QByteArray ba = fetchUri( url, errors );
340 if ( !ba.isEmpty() )
341 {
342 if ( !QgsGltfUtils::loadImageDataWithQImage( &img, -1, nullptr, nullptr, 0, 0, ( const unsigned char * ) ba.constData(), ba.size(), nullptr ) )
343 {
344 if ( errors )
345 *errors << u"Failed to load image: %1"_s.arg( imgUri );
346 }
347 }
348 }
349
350 if ( img.image.empty() )
351 {
352 auto pbrMaterial = std::make_unique<QgsMetalRoughMaterial>();
353 pbrMaterial->setEnvironmentalLightingEnabled( true );
354 pbrMaterial->setMetalness( pbr.metallicFactor ); // [0..1] or texture
355 pbrMaterial->setRoughness( pbr.roughnessFactor );
356 pbrMaterial->setBaseColor( QColor::fromRgbF( pbr.baseColorFactor[0], pbr.baseColorFactor[1], pbr.baseColorFactor[2], pbr.baseColorFactor[3] ) );
357 return pbrMaterial;
358 }
359
360 TinyGltfTextureImage *textureImage = new TinyGltfTextureImage( img );
361
362 Qt3DRender::QTexture2D *texture = new Qt3DRender::QTexture2D;
363 texture->addTextureImage( textureImage ); // textures take the ownership of textureImage if has no parant
364
365 Qgs3DUtils::setTextureFiltering( texture, context );
366
367 texture->setFormat( Qt3DRender::QAbstractTexture::SRGB8_Alpha8 );
368
369 if ( tex.sampler >= 0 )
370 {
371 tinygltf::Sampler &sampler = model.samplers[tex.sampler];
372 if ( sampler.minFilter >= 0 )
373 texture->setMinificationFilter( parseTextureFilter( sampler.minFilter ) );
374 if ( sampler.magFilter >= 0 )
375 texture->setMagnificationFilter( parseTextureFilter( sampler.magFilter ) );
376 Qt3DRender::QTextureWrapMode wrapMode;
377 wrapMode.setX( parseTextureWrapMode( sampler.wrapS ) );
378 wrapMode.setY( parseTextureWrapMode( sampler.wrapT ) );
379 texture->setWrapMode( wrapMode );
380 }
381
382 // We should be using PBR material unless unlit material is requested using KHR_materials_unlit
383 // GLTF extension, but in various datasets that extension is not used (even though it should have been).
384 // In the future we may want to have a switch whether to use unlit material or PBR material...
385 auto mat = std::make_unique<QgsTextureMaterial>();
386 mat->setTexture( texture );
387 return mat;
388 }
389
390 if ( qgsDoubleNear( pbr.baseColorFactor[3], 0 ) )
391 return nullptr; // completely transparent primitive, just skip it
392
393 auto pbrMaterial = std::make_unique<QgsMetalRoughMaterial>();
394 pbrMaterial->setEnvironmentalLightingEnabled( true );
395 pbrMaterial->setMetalness( pbr.metallicFactor ); // [0..1] or texture
396 pbrMaterial->setRoughness( pbr.roughnessFactor );
397 pbrMaterial->setBaseColor( QColor::fromRgbF( pbr.baseColorFactor[0], pbr.baseColorFactor[1], pbr.baseColorFactor[2], pbr.baseColorFactor[3] ) );
398 return pbrMaterial;
399}
400
401
402static QVector<Qt3DCore::QEntity *> parseNode(
403 tinygltf::Model &model,
404 int nodeIndex,
405 const QgsGltf3DUtils::EntityTransform &transform,
406 const QgsVector3D &tileTranslationEcef,
407 QString baseUri,
408 QMatrix4x4 parentTransform,
409 const Qgs3DRenderContext &context,
410 QStringList *errors
411)
412{
414 tinygltf::Node &node = model.nodes[nodeIndex];
415
416 QVector<Qt3DCore::QEntity *> entities;
417
418 // transform
419 std::unique_ptr<QMatrix4x4> matrix = QgsGltfUtils::parseNodeTransform( node );
420 if ( !parentTransform.isIdentity() )
421 {
422 if ( matrix )
423 *matrix = parentTransform * *matrix;
424 else
425 {
426 matrix = std::make_unique<QMatrix4x4>( parentTransform );
427 }
428 }
429
430 // mesh — skip nodes with EXT_mesh_gpu_instancing (handled separately by createInstancedEntities)
431 if ( node.mesh >= 0 && node.extensions.find( "EXT_mesh_gpu_instancing" ) == node.extensions.end() )
432 {
433 tinygltf::Mesh &mesh = model.meshes[node.mesh];
434
435 for ( const tinygltf::Primitive &primitive : mesh.primitives )
436 {
437 if ( primitive.mode != TINYGLTF_MODE_TRIANGLES )
438 {
439 if ( errors )
440 *errors << u"Unsupported mesh primitive: %1"_s.arg( primitive.mode );
441 continue;
442 }
443
444 auto posIt = primitive.attributes.find( "POSITION" );
445 Q_ASSERT( posIt != primitive.attributes.end() );
446 int positionAccessorIndex = posIt->second;
447
448 tinygltf::Accessor &posAccessor = model.accessors[positionAccessorIndex];
449 if ( posAccessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || posAccessor.type != TINYGLTF_TYPE_VEC3 )
450 {
451 if ( errors )
452 *errors << u"Unsupported position accessor type: %1 / %2"_s.arg( posAccessor.componentType ).arg( posAccessor.type );
453 continue;
454 }
455
456 std::unique_ptr<QgsMaterial> material = parseMaterial( model, primitive.material, baseUri, errors, materialContext );
457 if ( !material )
458 {
459 // primitive should be skipped, eg fully transparent material
460 continue;
461 }
462
463 Qt3DCore::QGeometry *geom = new Qt3DCore::QGeometry;
464
465 Qt3DCore::QAttribute *positionAttribute = reprojectPositions( model, positionAccessorIndex, transform, tileTranslationEcef, matrix.get() );
466 positionAttribute->setName( Qt3DCore::QAttribute::defaultPositionAttributeName() );
467 geom->addAttribute( positionAttribute );
468
469 auto normalIt = primitive.attributes.find( "NORMAL" );
470 if ( normalIt != primitive.attributes.end() )
471 {
472 int normalAccessorIndex = normalIt->second;
473 Qt3DCore::QAttribute *normalAttribute = parseAttribute( model, normalAccessorIndex );
474 normalAttribute->setName( Qt3DCore::QAttribute::defaultNormalAttributeName() );
475 geom->addAttribute( normalAttribute );
476
477 // TODO: we may need to transform normal vectors when we are altering positions
478 // (but quite often normals are actually note needed - e.g. when using textured data)
479 }
480
481 auto texIt = primitive.attributes.find( "TEXCOORD_0" );
482 if ( texIt != primitive.attributes.end() )
483 {
484 int texAccessorIndex = texIt->second;
485 Qt3DCore::QAttribute *texAttribute = parseAttribute( model, texAccessorIndex );
486 texAttribute->setName( Qt3DCore::QAttribute::defaultTextureCoordinateAttributeName() );
487 geom->addAttribute( texAttribute );
488 }
489
490 Qt3DCore::QAttribute *indexAttribute = nullptr;
491 if ( primitive.indices != -1 )
492 {
493 indexAttribute = parseAttribute( model, primitive.indices );
494 geom->addAttribute( indexAttribute );
495 }
496
497 Qt3DRender::QGeometryRenderer *geomRenderer = new Qt3DRender::QGeometryRenderer;
498 geomRenderer->setGeometry( geom );
499 geomRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::Triangles ); // looks like same values as "mode"
500 geomRenderer->setVertexCount( indexAttribute ? indexAttribute->count() : model.accessors[positionAccessorIndex].count );
501
502 // if we are using PBR material, and normal vectors are not present in the data,
503 // they should be auto-generated by us (according to GLTF spec)
504 if ( normalIt == primitive.attributes.end() )
505 {
506 if ( QgsMetalRoughMaterial *pbrMat = qobject_cast<QgsMetalRoughMaterial *>( material.get() ) )
507 {
508 pbrMat->setFlatShadingEnabled( true );
509 }
510 }
511
512 Qt3DCore::QEntity *primitiveEntity = new Qt3DCore::QEntity;
513 primitiveEntity->addComponent( geomRenderer );
514 primitiveEntity->addComponent( material.release() );
515 entities << primitiveEntity;
516 }
517 }
518
519 // recursively add children
520 for ( int childNodeIndex : node.children )
521 {
522 entities << parseNode( model, childNodeIndex, transform, tileTranslationEcef, baseUri, matrix ? *matrix : QMatrix4x4(), context, errors );
523 }
524
525 return entities;
526}
527
528
532static QgsMatrix4x4 floatToDoubleMatrix( const QMatrix4x4 &fm )
533{
534 const float *f = fm.constData(); // column-major
535 return QgsMatrix4x4( f[0], f[4], f[8], f[12], f[1], f[5], f[9], f[13], f[2], f[6], f[10], f[14], f[3], f[7], f[11], f[15] );
536}
537
538
542static QMatrix3x3 matrixFromColumns( const QVector3D &col0, const QVector3D &col1, const QVector3D &col2 )
543{
544 // QMatrix3x3 constructor takes row-major data
545 const float d[9] = {
546 col0.x(),
547 col1.x(),
548 col2.x(),
549 col0.y(),
550 col1.y(),
551 col2.y(),
552 col0.z(),
553 col1.z(),
554 col2.z(),
555 };
556 return QMatrix3x3( d );
557}
558
559
565static QMatrix3x3 extractRotation3x3( const QgsMatrix4x4 &matrix, QVector3D &scale )
566{
567 const double *md = matrix.constData(); // column-major
568 // casting to float should be fine (does not extract translation which needs double precision)
569 const QVector3D col0( static_cast<float>( md[0] ), static_cast<float>( md[1] ), static_cast<float>( md[2] ) );
570 const QVector3D col1( static_cast<float>( md[4] ), static_cast<float>( md[5] ), static_cast<float>( md[6] ) );
571 const QVector3D col2( static_cast<float>( md[8] ), static_cast<float>( md[9] ), static_cast<float>( md[10] ) );
572
573 const float sx = col0.length();
574 const float sy = col1.length();
575 const float sz = col2.length();
576 scale = QVector3D( sx, sy, sz );
577
578 if ( sx == 0 || sy == 0 || sz == 0 )
579 return QMatrix3x3();
580
581 return matrixFromColumns( col0 / sx, col1 / sy, col2 / sz );
582}
583
584
585QMatrix3x3 QgsGltf3DUtils::ecefToTargetCrsRotationCorrection( const QgsVector3D &ecefPos, const QgsVector3D &mapPos, const QgsCoordinateTransform &ecefToTargetCrs )
586{
587 // Local ECEF basis vectors at this point:
588 // up = normalize(x, y, z) — geocentric up (≤0.3° error vs ellipsoid normal)
589 // east = normalize(-y, x, 0) — tangent to the parallel
590 // north = cross(up, east)
591 const double x = ecefPos.x(), y = ecefPos.y(), z = ecefPos.z();
592 const double len = std::sqrt( x * x + y * y + z * z );
593 const QgsVector3D upEcef( x / len, y / len, z / len );
594
595 const double eastLenXY = std::sqrt( y * y + x * x );
596 const QgsVector3D eastEcef( -y / eastLenXY, x / eastLenXY, 0 );
597
598 const QgsVector3D northEcef = QgsVector3D::crossProduct( upEcef, eastEcef );
599
600 // Compute corresponding vectors in target CRS by reprojecting perturbed ECEF points.
601 // Use a small delta (~1 meter) along each ECEF basis vector.
602 constexpr double delta = 1.0; // meters
603 double eX = x + delta * eastEcef.x(), eY = y + delta * eastEcef.y(), eZ = z + delta * eastEcef.z();
604 double nX = x + delta * northEcef.x(), nY = y + delta * northEcef.y(), nZ = z + delta * northEcef.z();
605 double uX = x + delta * upEcef.x(), uY = y + delta * upEcef.y(), uZ = z + delta * upEcef.z();
606
607 try
608 {
609 ecefToTargetCrs.transformInPlace( eX, eY, eZ );
610 ecefToTargetCrs.transformInPlace( nX, nY, nZ );
611 ecefToTargetCrs.transformInPlace( uX, uY, uZ );
612 }
613 catch ( QgsCsException & )
614 {
615 return QMatrix3x3(); // identity on failure
616 }
617
618 // Target CRS basis vectors (differences from the reprojected base point, then normalized)
619 QgsVector3D eastCrs( eX - mapPos.x(), eY - mapPos.y(), eZ - mapPos.z() );
620 QgsVector3D northCrs( nX - mapPos.x(), nY - mapPos.y(), nZ - mapPos.z() );
621 QgsVector3D upCrs( uX - mapPos.x(), uY - mapPos.y(), uZ - mapPos.z() );
622 eastCrs.normalize();
623 northCrs.normalize();
624 upCrs.normalize();
625
626 // Correction matrix C = T × Eᵀ
627 // where E = [east_ecef | north_ecef | up_ecef] (columns, orthonormal)
628 // T = [east_crs | north_crs | up_crs] (columns)
629 // Since E is orthonormal, E⁻¹ = Eᵀ, so C = T × Eᵀ.
630 const QMatrix3x3 ecefBasis = matrixFromColumns( eastEcef.toVector3D(), northEcef.toVector3D(), upEcef.toVector3D() ); // E
631 const QMatrix3x3 crsBasis = matrixFromColumns( eastCrs.toVector3D(), northCrs.toVector3D(), upCrs.toVector3D() ); // T
632 return crsBasis * ecefBasis.transposed();
633}
634
635
636QVector<QgsGltf3DUtils::InstanceChunkTransform> QgsGltf3DUtils::tileSpaceToChunkLocal( const QgsGltfUtils::InstancedPrimitive &primitive, const QgsGltf3DUtils::EntityTransform &transform )
637{
638 QVector<InstanceChunkTransform> result;
639 result.resize( primitive.instanceTransforms.size() );
640
641 if ( primitive.instanceTransforms.isEmpty() )
642 return result;
643
644 const QgsVector3D sceneOrigin = transform.chunkOriginTargetCrs;
645
646 // ECEF-to-target-CRS rotation correction matrix, computed once from first instance.
647 // All instances in a tile are close enough that one correction suffices.
648 QMatrix3x3 correctionMatrix;
649 bool hasCorrectionMatrix = false;
650
651 for ( int i = 0; i < primitive.instanceTransforms.size(); ++i )
652 {
653 // Compose with tile transform in double precision:
654 // ecefMatrix = tileTransform × instanceTransforms[i]
655 const QgsMatrix4x4 instanceDouble = floatToDoubleMatrix( primitive.instanceTransforms[i] );
656 const QgsMatrix4x4 ecefMatrix = transform.tileTransform * instanceDouble;
657
658 // Extract ECEF position from column 3 (double precision)
659 const double *md = ecefMatrix.constData(); // column-major
660 const QgsVector3D ecefPos( md[12], md[13], md[14] );
661
662 // Reproject ECEF → target CRS
663 double mapX = ecefPos.x();
664 double mapY = ecefPos.y();
665 double mapZ = ecefPos.z();
666 if ( transform.ecefToTargetCrs )
667 {
668 try
669 {
670 transform.ecefToTargetCrs->transformInPlace( mapX, mapY, mapZ );
671 }
672 catch ( QgsCsException & )
673 {
674 continue;
675 }
676 }
677
678 // Compute the correction matrix on the first successfully reprojected instance
679 if ( !hasCorrectionMatrix && transform.ecefToTargetCrs )
680 {
681 hasCorrectionMatrix = true;
682 correctionMatrix = ecefToTargetCrsRotationCorrection( ecefPos, QgsVector3D( mapX, mapY, mapZ ), *transform.ecefToTargetCrs );
683 }
684
685 // Apply z value modifications
686 mapZ = mapZ * transform.zValueScale + transform.zValueOffset;
687
688 // Chunk-local translation
689 result[i].translation = QVector3D( static_cast<float>( mapX - sceneOrigin.x() ), static_cast<float>( mapY - sceneOrigin.y() ), static_cast<float>( mapZ - sceneOrigin.z() ) );
690
691 // Extract rotation and scale from the 3×3 part of ecefMatrix (double precision)
692 QVector3D scale;
693 QMatrix3x3 rotEcef = extractRotation3x3( ecefMatrix, scale );
694
695 result[i].scale = scale;
696
697 if ( scale.x() == 0 || scale.y() == 0 || scale.z() == 0 )
698 {
699 result[i].rotation = QQuaternion();
700 continue;
701 }
702
703 // Apply ECEF-to-CRS rotation correction if available
704 const QMatrix3x3 rotCrs = hasCorrectionMatrix ? correctionMatrix * rotEcef : rotEcef;
705 result[i].rotation = QQuaternion::fromRotationMatrix( rotCrs );
706 }
707
708 return result;
709}
710
711
712void QgsGltf3DUtils::createInstanceBuffer( Qt3DCore::QGeometry *geometry, const QVector<InstanceChunkTransform> &instances )
713{
714 const int stride = 10 * sizeof( float ); // vec3 + vec4 + vec3 = 10 floats = 40 bytes
715 QByteArray bufferData;
716 bufferData.resize( instances.size() * stride );
717 float *dst = reinterpret_cast<float *>( bufferData.data() );
718
719 for ( const auto &inst : instances )
720 {
721 // translation (vec3)
722 *dst++ = inst.translation.x();
723 *dst++ = inst.translation.y();
724 *dst++ = inst.translation.z();
725 // rotation (vec4: x, y, z, w)
726 *dst++ = inst.rotation.x();
727 *dst++ = inst.rotation.y();
728 *dst++ = inst.rotation.z();
729 *dst++ = inst.rotation.scalar();
730 // scale (vec3)
731 *dst++ = inst.scale.x();
732 *dst++ = inst.scale.y();
733 *dst++ = inst.scale.z();
734 }
735
736 Qt3DCore::QBuffer *buffer = new Qt3DCore::QBuffer;
737 buffer->setData( bufferData );
738
739 // Translation attribute — matches "in vec3 instanceTranslation" in shader
740 Qt3DCore::QAttribute *transAttr = new Qt3DCore::QAttribute;
741 transAttr->setName( u"instanceTranslation"_s );
742 transAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
743 transAttr->setVertexSize( 3 );
744 transAttr->setByteStride( stride );
745 transAttr->setByteOffset( 0 );
746 transAttr->setDivisor( 1 );
747 transAttr->setCount( instances.size() );
748 transAttr->setBuffer( buffer );
749 geometry->addAttribute( transAttr );
750
751 // Rotation attribute — matches "in vec4 instanceRotation" in shader
752 Qt3DCore::QAttribute *rotAttr = new Qt3DCore::QAttribute;
753 rotAttr->setName( u"instanceRotation"_s );
754 rotAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
755 rotAttr->setVertexSize( 4 );
756 rotAttr->setByteStride( stride );
757 rotAttr->setByteOffset( 3 * sizeof( float ) );
758 rotAttr->setDivisor( 1 );
759 rotAttr->setCount( instances.size() );
760 rotAttr->setBuffer( buffer );
761 geometry->addAttribute( rotAttr );
762
763 // Scale attribute — matches "in vec3 instanceScale" in shader
764 Qt3DCore::QAttribute *scaleAttr = new Qt3DCore::QAttribute;
765 scaleAttr->setName( u"instanceScale"_s );
766 scaleAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
767 scaleAttr->setVertexSize( 3 );
768 scaleAttr->setByteStride( stride );
769 scaleAttr->setByteOffset( 7 * sizeof( float ) );
770 scaleAttr->setDivisor( 1 );
771 scaleAttr->setCount( instances.size() );
772 scaleAttr->setBuffer( buffer );
773 geometry->addAttribute( scaleAttr );
774}
775
776
777static Qt3DCore::QAttribute *rawPositions( tinygltf::Model &model, int accessorIndex )
778{
779 tinygltf::Accessor &accessor = model.accessors[accessorIndex];
780 tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
781 tinygltf::Buffer &b = model.buffers[bv.buffer];
782
783 if ( accessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || accessor.type != TINYGLTF_TYPE_VEC3 )
784 return nullptr;
785
786 const unsigned char *ptr = b.data.data() + bv.byteOffset + accessor.byteOffset;
787 const int byteStride = bv.byteStride ? bv.byteStride : 3 * sizeof( float );
788
789 QByteArray byteArray;
790 byteArray.resize( accessor.count * 3 * sizeof( float ) );
791 float *out = reinterpret_cast<float *>( byteArray.data() );
792
793 for ( std::size_t i = 0; i < accessor.count; ++i )
794 {
795 const float *fptr = reinterpret_cast<const float *>( ptr + i * byteStride );
796 out[i * 3 + 0] = fptr[0];
797 out[i * 3 + 1] = fptr[1];
798 out[i * 3 + 2] = fptr[2];
799 }
800
801 Qt3DCore::QBuffer *buffer = new Qt3DCore::QBuffer;
802 buffer->setData( byteArray );
803
804 Qt3DCore::QAttribute *attribute = new Qt3DCore::QAttribute;
805 attribute->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
806 attribute->setBuffer( buffer );
807 attribute->setByteOffset( 0 );
808 attribute->setByteStride( 12 );
809 attribute->setCount( accessor.count );
810 attribute->setVertexBaseType( Qt3DCore::QAttribute::Float );
811 attribute->setVertexSize( 3 );
812
813 return attribute;
814}
815
816
817QVector<Qt3DCore::QEntity *> QgsGltf3DUtils::createInstancedEntities(
818 tinygltf::Model &model,
819 const QVector<QgsGltfUtils::InstancedPrimitive> &primitives,
820 const QgsGltf3DUtils::EntityTransform &transform,
821 const QString &baseUri,
822 const QgsMaterialContext &context,
823 QStringList *errors
824)
825{
826 QVector<Qt3DCore::QEntity *> entities;
827
828 for ( const QgsGltfUtils::InstancedPrimitive &entry : primitives )
829 {
830 if ( entry.meshIndex < 0 || entry.meshIndex >= static_cast<int>( model.meshes.size() ) )
831 continue;
832
833 const tinygltf::Mesh &mesh = model.meshes[entry.meshIndex];
834 if ( entry.primitiveIndex < 0 || entry.primitiveIndex >= static_cast<int>( mesh.primitives.size() ) )
835 continue;
836
837 const tinygltf::Primitive &primitive = mesh.primitives[entry.primitiveIndex];
838 if ( primitive.mode != TINYGLTF_MODE_TRIANGLES )
839 {
840 if ( errors )
841 *errors << u"Unsupported mesh primitive mode for instancing: %1"_s.arg( primitive.mode );
842 continue;
843 }
844
845 auto posIt = primitive.attributes.find( "POSITION" );
846 if ( posIt == primitive.attributes.end() )
847 continue;
848
849 int positionAccessorIndex = posIt->second;
850
851 // Parse material
852 std::unique_ptr<QgsMaterial> material = parseMaterial( model, entry.materialIndex, baseUri, errors, context );
853 if ( !material )
854 continue;
855
856 // Enable instancing on the material
857 if ( QgsMetalRoughMaterial *pbrMat = qobject_cast<QgsMetalRoughMaterial *>( material.get() ) )
859 else if ( QgsTextureMaterial *texMat = qobject_cast<QgsTextureMaterial *>( material.get() ) )
861
862 // Build geometry with raw positions (no transform, no axis flip)
863 auto geom = std::make_unique<Qt3DCore::QGeometry>();
864
865 Qt3DCore::QAttribute *positionAttribute = rawPositions( model, positionAccessorIndex );
866 if ( !positionAttribute )
867 {
868 continue;
869 }
870 positionAttribute->setName( Qt3DCore::QAttribute::defaultPositionAttributeName() );
871 geom->addAttribute( positionAttribute );
872
873 auto normalIt = primitive.attributes.find( "NORMAL" );
874 if ( normalIt != primitive.attributes.end() )
875 {
876 Qt3DCore::QAttribute *normalAttribute = parseAttribute( model, normalIt->second );
877 normalAttribute->setName( Qt3DCore::QAttribute::defaultNormalAttributeName() );
878 geom->addAttribute( normalAttribute );
879 }
880 else
881 {
882 // Enable flat shading if no normals
883 if ( QgsMetalRoughMaterial *pbrMat = qobject_cast<QgsMetalRoughMaterial *>( material.get() ) )
884 pbrMat->setFlatShadingEnabled( true );
885 }
886
887 auto texIt = primitive.attributes.find( "TEXCOORD_0" );
888 if ( texIt != primitive.attributes.end() )
889 {
890 Qt3DCore::QAttribute *texAttribute = parseAttribute( model, texIt->second );
891 texAttribute->setName( Qt3DCore::QAttribute::defaultTextureCoordinateAttributeName() );
892 geom->addAttribute( texAttribute );
893 }
894
895 Qt3DCore::QAttribute *indexAttribute = nullptr;
896 if ( primitive.indices != -1 )
897 {
898 indexAttribute = parseAttribute( model, primitive.indices );
899 geom->addAttribute( indexAttribute );
900 }
901
902 // Convert tile-space matrices to chunk-local T/R/S
903 const QVector<InstanceChunkTransform> chunkTransforms = tileSpaceToChunkLocal( entry, transform );
904 if ( chunkTransforms.isEmpty() )
905 {
906 continue;
907 }
908
909 // Add per-instance attributes
910 createInstanceBuffer( geom.get(), chunkTransforms );
911
912 // Create geometry renderer with instancing
913 Qt3DRender::QGeometryRenderer *geomRenderer = new Qt3DRender::QGeometryRenderer;
914 geomRenderer->setGeometry( geom.release() ); // geom gets parented to geomRenderer
915 geomRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::Triangles );
916 geomRenderer->setVertexCount( indexAttribute ? indexAttribute->count() : model.accessors[positionAccessorIndex].count );
917 geomRenderer->setInstanceCount( chunkTransforms.size() );
918
919 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity;
920 entity->addComponent( geomRenderer );
921 entity->addComponent( material.release() );
922 entities << entity;
923 }
924
925 return entities;
926}
927
928Qt3DCore::QEntity *QgsGltf3DUtils::parsedGltfToEntity( tinygltf::Model &model, const QgsGltf3DUtils::EntityTransform &transform, QString baseUri, const Qgs3DRenderContext &context, QStringList *errors )
929{
930 bool sceneOk = false;
931 const std::size_t sceneIndex = QgsGltfUtils::sourceSceneForModel( model, sceneOk );
932 if ( !sceneOk )
933 {
934 if ( errors )
935 *errors << "No scenes present in the gltf data!";
936 return nullptr;
937 }
938
939 tinygltf::Scene &scene = model.scenes[sceneIndex];
940
941 if ( scene.nodes.size() == 0 )
942 {
943 if ( errors )
944 *errors << "No nodes present in the gltf data!";
945 return nullptr;
946 }
947
948 const QgsVector3D tileTranslationEcef = QgsGltfUtils::extractTileTranslation( model );
949
950 Qt3DCore::QEntity *rootEntity = new Qt3DCore::QEntity;
951 for ( const int nodeIndex : scene.nodes )
952 {
953 const QVector<Qt3DCore::QEntity *> entities = parseNode( model, nodeIndex, transform, tileTranslationEcef, baseUri, QMatrix4x4(), context, errors );
954 for ( Qt3DCore::QEntity *e : entities )
955 e->setParent( rootEntity );
956 }
957 return rootEntity;
958}
959
960
961Qt3DCore::QEntity *QgsGltf3DUtils::gltfToEntity( const QByteArray &data, const QgsGltf3DUtils::EntityTransform &transform, const QString &baseUri, const Qgs3DRenderContext &context, QStringList *errors )
962{
963 tinygltf::Model model;
964 QString gltfErrors, gltfWarnings;
965
966 bool res = QgsGltfUtils::loadGltfModel( data, model, &gltfErrors, &gltfWarnings );
967 if ( !gltfErrors.isEmpty() )
968 {
969 QgsDebugError( u"Error raised reading %1: %2"_s.arg( baseUri, gltfErrors ) );
970 }
971 if ( !gltfWarnings.isEmpty() )
972 {
973 QgsDebugError( u"Warnings raised reading %1: %2"_s.arg( baseUri, gltfWarnings ) );
974 }
975 if ( !res )
976 {
977 if ( errors )
978 {
979 errors->append( u"GLTF load error: "_s + gltfErrors );
980 }
981 return nullptr;
982 }
983
984 return parsedGltfToEntity( model, transform, baseUri, context, errors );
985}
986
987static Qt3DCore::QAttribute *parseAttributeCached( tinygltf::Model &model, int accessorIndex, QHash<int, Qt3DCore::QBuffer *> &bufferViewCache, Qt3DCore::QNode *bufferParent )
988{
989 tinygltf::Accessor &accessor = model.accessors[accessorIndex];
990 tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
991 tinygltf::Buffer &b = model.buffers[bv.buffer];
992
993 Qt3DCore::QBuffer *&buffer = bufferViewCache[accessor.bufferView];
994 if ( !buffer )
995 {
996 const std::size_t len = bv.byteLength > 0 ? bv.byteLength : b.data.size() - bv.byteOffset;
997 const QByteArray data( reinterpret_cast<const char *>( b.data.data() + bv.byteOffset ), static_cast<int>( len ) );
998 buffer = new Qt3DCore::QBuffer( bufferParent );
999 buffer->setData( data );
1000 }
1001
1002 Qt3DCore::QAttribute *attribute = new Qt3DCore::QAttribute();
1003 if ( bv.target == TINYGLTF_TARGET_ARRAY_BUFFER )
1004 attribute->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
1005 else if ( bv.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER )
1006 attribute->setAttributeType( Qt3DCore::QAttribute::IndexAttribute );
1007
1008 attribute->setBuffer( buffer );
1009 attribute->setByteOffset( static_cast<uint>( accessor.byteOffset ) );
1010 attribute->setByteStride( bv.byteStride );
1011 attribute->setCount( static_cast<uint>( accessor.count ) );
1012 attribute->setVertexBaseType( parseVertexBaseType( accessor.componentType ) );
1013 attribute->setVertexSize( tinygltf::GetNumComponentsInType( accessor.type ) );
1014 return attribute;
1015}
1016
1017
1018static void collectGltfPrimitives(
1019 tinygltf::Model &model,
1020 int nodeIndex,
1021 const QMatrix4x4 &parentTransform,
1022 const QString &baseUri,
1023 const QgsMaterialContext &context,
1024 std::vector<QgsMeshNodeData> &result,
1025 QStringList *errors,
1026 QHash<int, Qt3DCore::QBuffer *> &bufferViewCache,
1027 Qt3DCore::QNode *bufferParent
1028)
1029{
1030 if ( nodeIndex < 0 || static_cast<std::size_t>( nodeIndex ) >= model.nodes.size() )
1031 return;
1032
1033 tinygltf::Node &node = model.nodes[nodeIndex];
1034
1035 std::unique_ptr<QMatrix4x4> matrix = QgsGltfUtils::parseNodeTransform( node );
1036 if ( !parentTransform.isIdentity() )
1037 {
1038 if ( matrix )
1039 *matrix = parentTransform * *matrix;
1040 else
1041 matrix = std::make_unique<QMatrix4x4>( parentTransform );
1042 }
1043 const QMatrix4x4 &currentTransform = matrix ? *matrix : parentTransform;
1044
1045 if ( node.mesh >= 0 )
1046 {
1047 const tinygltf::Mesh &mesh = model.meshes[node.mesh];
1048 for ( const tinygltf::Primitive &primitive : mesh.primitives )
1049 {
1050 if ( primitive.mode != TINYGLTF_MODE_TRIANGLES )
1051 {
1052 if ( errors )
1053 *errors << u"Unsupported primitive mode: %1"_s.arg( primitive.mode );
1054 continue;
1055 }
1056
1057 auto posIt = primitive.attributes.find( "POSITION" );
1058 if ( posIt == primitive.attributes.end() )
1059 continue;
1060
1061 std::unique_ptr<QgsMaterial> material = parseMaterial( model, primitive.material, baseUri, errors, context );
1062 if ( !material )
1063 continue;
1064
1065 auto geom = std::make_unique<Qt3DCore::QGeometry>();
1066
1067 Qt3DCore::QAttribute *posAttr = parseAttributeCached( model, posIt->second, bufferViewCache, bufferParent );
1068 if ( !posAttr )
1069 continue;
1070 posAttr->setName( Qt3DCore::QAttribute::defaultPositionAttributeName() );
1071 geom->addAttribute( posAttr );
1072
1073 auto normalIt = primitive.attributes.find( "NORMAL" );
1074 if ( normalIt != primitive.attributes.end() )
1075 {
1076 Qt3DCore::QAttribute *normalAttr = parseAttributeCached( model, normalIt->second, bufferViewCache, bufferParent );
1077 normalAttr->setName( Qt3DCore::QAttribute::defaultNormalAttributeName() );
1078 geom->addAttribute( normalAttr );
1079 }
1080 else
1081 {
1082 if ( QgsMetalRoughMaterial *pbrMat = qobject_cast<QgsMetalRoughMaterial *>( material.get() ) )
1083 pbrMat->setFlatShadingEnabled( true );
1084 }
1085
1086 auto texIt = primitive.attributes.find( "TEXCOORD_0" );
1087 if ( texIt != primitive.attributes.end() )
1088 {
1089 Qt3DCore::QAttribute *texAttr = parseAttributeCached( model, texIt->second, bufferViewCache, bufferParent );
1090 texAttr->setName( Qt3DCore::QAttribute::defaultTextureCoordinateAttributeName() );
1091 geom->addAttribute( texAttr );
1092 }
1093
1094 Qt3DCore::QAttribute *indexAttribute = nullptr;
1095 if ( primitive.indices != -1 )
1096 {
1097 indexAttribute = parseAttributeCached( model, primitive.indices, bufferViewCache, bufferParent );
1098 geom->addAttribute( indexAttribute );
1099 }
1100
1101 result.push_back( { std::move( geom ), std::move( material ), currentTransform } );
1102 }
1103 }
1104
1105 for ( int childIndex : node.children )
1106 collectGltfPrimitives( model, childIndex, currentTransform, baseUri, context, result, errors, bufferViewCache, bufferParent );
1107}
1108
1109std::vector<QgsMeshNodeData> QgsGltf3DUtils::buildGltfGeometries( const QString &filePath, const QgsMaterialContext &context, QStringList *errors, Qt3DCore::QNode *bufferParent )
1110{
1111 QFile f( filePath );
1112 if ( !f.open( QIODevice::ReadOnly ) )
1113 {
1114 if ( errors )
1115 *errors << u"Cannot open GLTF file: %1"_s.arg( filePath );
1116 return {};
1117 }
1118 const QByteArray data = f.readAll();
1119
1120 tinygltf::Model model;
1121 QString gltfErrors, gltfWarnings;
1122 const QString baseDir = QFileInfo( filePath ).absolutePath();
1123 if ( !QgsGltfUtils::loadGltfModel( data, model, &gltfErrors, &gltfWarnings, baseDir ) )
1124 {
1125 QgsDebugError( u"Failed to load GLTF model '%1': %2"_s.arg( filePath, gltfErrors ) );
1126 if ( errors )
1127 *errors << u"GLTF load error: "_s + gltfErrors;
1128 return {};
1129 }
1130 if ( !gltfWarnings.isEmpty() )
1131 QgsDebugMsgLevel( u"GLTF warnings for '%1': %2"_s.arg( filePath, gltfWarnings ), 2 );
1132
1133 bool sceneOk = false;
1134 const std::size_t sceneIndex = QgsGltfUtils::sourceSceneForModel( model, sceneOk );
1135 if ( !sceneOk )
1136 {
1137 QgsDebugError( u"No valid scene in GLTF model '%1'"_s.arg( filePath ) );
1138 return {};
1139 }
1140
1141 const QString baseUri = QUrl::fromLocalFile( filePath ).toString();
1142 std::vector<QgsMeshNodeData> result;
1143
1144 QHash<int, Qt3DCore::QBuffer *> bufferViewCache;
1145 for ( const int nodeIndex : model.scenes[sceneIndex].nodes )
1146 collectGltfPrimitives( model, nodeIndex, QMatrix4x4(), baseUri, context, result, errors, bufferViewCache, bufferParent );
1147
1148 return result;
1149}
1150
1151
1152// For TinyGltfTextureImage
1153#include "qgsgltf3dutils.moc"
1154
@ DataDefinedRotation
Per-instance data-defined rotation.
Definition qgis.h:4395
@ DataDefinedScale
Per-instance data-defined scale.
Definition qgis.h:4394
Rendering context for preparation of 3D entities.
static void setTextureFiltering(Qt3DRender::QAbstractTexture *texture, const QgsMaterialContext &context)
Sets the default filtering options for a texture.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Handles coordinate transforms between two 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...
Custom exception class for Coordinate Reference System related exceptions.
Context settings for a material.
static QgsMaterialContext fromRenderContext(const Qgs3DRenderContext &context)
Constructs a material context from the settings in a 3D render context.
A simple 4x4 matrix implementation useful for transformation in 3D space.
const double * constData() const
Returns pointer to the matrix data (stored in column-major order).
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:60
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:62
QVector3D toVector3D() const
Converts the current object to QVector3D.
double x() const
Returns X coordinate.
Definition qgsvector3d.h:58
static QgsVector3D crossProduct(const QgsVector3D &v1, const QgsVector3D &v2)
Returns the cross product of two vectors.
static bool extractFileFromZip(const QString &zipFilename, const QString &filenameInZip, QByteArray &bytesOut)
Extracts a file from a zip archive, returns true on success.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7340
bool operator==(const QgsFeatureIterator &fi1, const QgsFeatureIterator &fi2)
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80
#define QgsDebugError(str)
Definition qgslogger.h:71