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