QGIS API Documentation 3.99.0-Master (09f76ad7019)
Loading...
Searching...
No Matches
qgsgltfutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsgltfutils.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#include "qgsconfig.h"
17#include "qgsgltfutils.h"
18
19#include <memory>
20
21#include "qgsexception.h"
22#include "qgslogger.h"
23#include "qgsmatrix4x4.h"
24#include "qgstiledscenetile.h"
25#include "qgsziputils.h"
26
27#include <QImage>
28#include <QMatrix4x4>
29#include <QQuaternion>
30#include <QRegularExpression>
31#include <QString>
32
33using namespace Qt::StringLiterals;
34
35#define TINYGLTF_IMPLEMENTATION // should be defined just in one CPP file
36
37// decompression of meshes with Draco is optional, but recommended
38// because some 3D Tiles datasets use it (KHR_draco_mesh_compression is an optional extension of GLTF)
39#ifdef HAVE_DRACO
40#define TINYGLTF_ENABLE_DRACO
41#endif
42
43#define TINYGLTF_NO_STB_IMAGE // we use QImage-based reading of images
44#define TINYGLTF_NO_STB_IMAGE_WRITE // we don't need writing of images
45//#define TINYGLTF_NO_FS
46
47//#include <fstream>
48#include "tiny_gltf.h"
49
51
52
53bool QgsGltfUtils::accessorToMapCoordinates( const tinygltf::Model &model, int accessorIndex, const QgsMatrix4x4 &tileTransform, const QgsCoordinateTransform *ecefToTargetCrs, const QgsVector3D &tileTranslationEcef, const QMatrix4x4 *nodeTransform, Qgis::Axis gltfUpAxis, QVector<double> &vx, QVector<double> &vy, QVector<double> &vz )
54{
55 const tinygltf::Accessor &accessor = model.accessors[accessorIndex];
56 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
57 const tinygltf::Buffer &b = model.buffers[bv.buffer];
58
59 if ( accessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || accessor.type != TINYGLTF_TYPE_VEC3 )
60 {
61 // we may support more input types in the future if needed
62 return false;
63 }
64
65 const unsigned char *ptr = b.data.data() + bv.byteOffset + accessor.byteOffset;
66
67 vx.resize( accessor.count );
68 vy.resize( accessor.count );
69 vz.resize( accessor.count );
70 double *vxOut = vx.data();
71 double *vyOut = vy.data();
72 double *vzOut = vz.data();
73 for ( int i = 0; i < static_cast<int>( accessor.count ); ++i )
74 {
75 const float *fptr = reinterpret_cast<const float *>( ptr );
76 QVector3D vOrig( fptr[0], fptr[1], fptr[2] );
77
78 if ( nodeTransform )
79 vOrig = nodeTransform->map( vOrig );
80
82 switch ( gltfUpAxis )
83 {
84 case Qgis::Axis::X:
85 {
86 QgsDebugError( u"X up translation not yet supported"_s );
87 v = tileTransform.map( tileTranslationEcef );
88 break;
89 }
90
91 case Qgis::Axis::Y:
92 {
93 // go from y-up to z-up according to 3D Tiles spec
94 QVector3D vFlip( vOrig.x(), -vOrig.z(), vOrig.y() );
95 v = tileTransform.map( QgsVector3D( vFlip ) + tileTranslationEcef );
96 break;
97 }
98
99 case Qgis::Axis::Z:
100 {
101 v = tileTransform.map( QgsVector3D( vOrig ) + tileTranslationEcef );
102 break;
103 }
104 }
105
106 *vxOut++ = v.x();
107 *vyOut++ = v.y();
108 *vzOut++ = v.z();
109
110 if ( bv.byteStride )
111 ptr += bv.byteStride;
112 else
113 ptr += 3 * sizeof( float );
114 }
115
116 if ( ecefToTargetCrs )
117 {
118 try
119 {
120 ecefToTargetCrs->transformCoords( accessor.count, vx.data(), vy.data(), vz.data() );
121 }
122 catch ( QgsCsException & )
123 {
124 return false;
125 }
126 }
127
128 return true;
129}
130
131bool QgsGltfUtils::extractTextureCoordinates( const tinygltf::Model &model, int accessorIndex, QVector<float> &x, QVector<float> &y )
132{
133 const tinygltf::Accessor &accessor = model.accessors[accessorIndex];
134 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
135 const tinygltf::Buffer &b = model.buffers[bv.buffer];
136
137 if ( accessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || accessor.type != TINYGLTF_TYPE_VEC2 )
138 {
139 return false;
140 }
141
142 const unsigned char *ptr = b.data.data() + bv.byteOffset + accessor.byteOffset;
143 x.resize( accessor.count );
144 y.resize( accessor.count );
145
146 float *xOut = x.data();
147 float *yOut = y.data();
148
149 for ( std::size_t i = 0; i < accessor.count; i++ )
150 {
151 const float *fptr = reinterpret_cast< const float * >( ptr );
152
153 *xOut++ = fptr[0];
154 *yOut++ = fptr[1];
155
156 if ( bv.byteStride )
157 ptr += bv.byteStride;
158 else
159 ptr += 2 * sizeof( float );
160 }
161 return true;
162}
163
164QgsGltfUtils::ResourceType QgsGltfUtils::imageResourceType( const tinygltf::Model &model, int index )
165{
166 const tinygltf::Image &img = model.images[index];
167
168 if ( !img.image.empty() )
169 {
170 return ResourceType::Embedded;
171 }
172 else
173 {
174 return ResourceType::Linked;
175 }
176}
177
178QImage QgsGltfUtils::extractEmbeddedImage( const tinygltf::Model &model, int index )
179{
180 const tinygltf::Image &img = model.images[index];
181 if ( !img.image.empty() )
182 return QImage( img.image.data(), img.width, img.height, QImage::Format_ARGB32 );
183 else
184 return QImage();
185}
186
187QString QgsGltfUtils::linkedImagePath( const tinygltf::Model &model, int index )
188{
189 const tinygltf::Image &img = model.images[index];
190 return QString::fromStdString( img.uri );
191}
192
193std::unique_ptr<QMatrix4x4> QgsGltfUtils::parseNodeTransform( const tinygltf::Node &node )
194{
195 // read node's transform: either specified with 4x4 "matrix" element
196 // -OR- given by "translation", "rotation" and "scale" elements (to be combined as T * R * S)
197 std::unique_ptr<QMatrix4x4> matrix;
198 if ( !node.matrix.empty() )
199 {
200 matrix = std::make_unique<QMatrix4x4>( );
201 float *mdata = matrix->data();
202 for ( int i = 0; i < 16; ++i )
203 mdata[i] = static_cast< float >( node.matrix[i] );
204 }
205 else if ( node.translation.size() || node.rotation.size() || node.scale.size() )
206 {
207 matrix = std::make_unique<QMatrix4x4>( );
208 if ( node.scale.size() )
209 {
210 matrix->scale( static_cast< float >( node.scale[0] ), static_cast< float >( node.scale[1] ), static_cast< float >( node.scale[2] ) );
211 }
212 if ( node.rotation.size() )
213 {
214 matrix->rotate( QQuaternion( static_cast< float >( node.rotation[3] ), static_cast< float >( node.rotation[0] ), static_cast< float >( node.rotation[1] ), static_cast< float >( node.rotation[2] ) ) );
215 }
216 if ( node.translation.size() )
217 {
218 matrix->translate( static_cast< float >( node.translation[0] ), static_cast< float >( node.translation[1] ), static_cast< float >( node.translation[2] ) );
219 }
220 }
221 return matrix;
222}
223
224
225QgsVector3D QgsGltfUtils::extractTileTranslation( tinygltf::Model &model, Qgis::Axis upAxis )
226{
227 bool sceneOk = false;
228 const std::size_t sceneIndex = QgsGltfUtils::sourceSceneForModel( model, sceneOk );
229 if ( !sceneOk )
230 {
231 return QgsVector3D();
232 }
233
234 const tinygltf::Scene &scene = model.scenes[sceneIndex];
235
236 QgsVector3D tileTranslationEcef;
237 auto it = model.extensions.find( "CESIUM_RTC" );
238 if ( it != model.extensions.end() )
239 {
240 const tinygltf::Value v = it->second;
241 if ( v.IsObject() && v.Has( "center" ) )
242 {
243 const tinygltf::Value center = v.Get( "center" );
244 if ( center.IsArray() && center.Size() == 3 )
245 {
246 tileTranslationEcef = QgsVector3D( center.Get( 0 ).GetNumberAsDouble(), center.Get( 1 ).GetNumberAsDouble(), center.Get( 2 ).GetNumberAsDouble() );
247 }
248 }
249 }
250
251 if ( scene.nodes.size() == 0 )
252 return QgsVector3D();
253
254 int rootNodeIndex = scene.nodes[0];
255 tinygltf::Node &rootNode = model.nodes[rootNodeIndex];
256
257 if ( tileTranslationEcef.isNull() && rootNode.translation.size() )
258 {
259 QgsVector3D rootTranslation( rootNode.translation[0], rootNode.translation[1], rootNode.translation[2] );
260
261 // if root node of a GLTF contains translation by a large amount, let's handle it as the tile translation.
262 // this will ensure that we keep double precision rather than losing precision when dealing with floats
263 if ( rootTranslation.length() > 1e6 )
264 {
265 switch ( upAxis )
266 {
267 case Qgis::Axis::X:
268 QgsDebugError( u"X up translation not yet supported"_s );
269 break;
270 case Qgis::Axis::Y:
271 {
272 // we flip Y/Z axes here because GLTF uses Y-up convention, while 3D Tiles use Z-up convention
273 tileTranslationEcef = QgsVector3D( rootTranslation.x(), -rootTranslation.z(), rootTranslation.y() );
274 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
275 break;
276 }
277 case Qgis::Axis::Z:
278 {
279 tileTranslationEcef = QgsVector3D( rootTranslation.x(), rootTranslation.y(), rootTranslation.z() );
280 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
281 break;
282 }
283 }
284 }
285 }
286
287 return tileTranslationEcef;
288}
289
290
291bool QgsGltfUtils::loadImageDataWithQImage(
292 tinygltf::Image *image, const int image_idx, std::string *err,
293 std::string *warn, int req_width, int req_height,
294 const unsigned char *bytes, int size, void *user_data )
295{
296
297 if ( req_width != 0 || req_height != 0 )
298 {
299 if ( err )
300 {
301 ( *err ) += "Expecting zero req_width/req_height.\n";
302 }
303 return false;
304 }
305
306 ( void )warn;
307 ( void )user_data;
308
309 QImage img;
310 if ( !img.loadFromData( bytes, size ) )
311 {
312 if ( err )
313 {
314 ( *err ) +=
315 "Unknown image format. QImage cannot decode image data for image[" +
316 std::to_string( image_idx ) + "] name = \"" + image->name + "\".\n";
317 }
318 return false;
319 }
320
321 if ( img.format() != QImage::Format_RGB32 && img.format() != QImage::Format_ARGB32 )
322 {
323 // we may want to natively support other formats as well as long as such texture formats are allowed
324 img.convertTo( QImage::Format_RGB32 );
325 }
326
327 image->width = img.width();
328 image->height = img.height();
329 image->component = 4;
330 image->bits = 8;
331 image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE;
332
333 image->image.resize( static_cast<size_t>( image->width * image->height * image->component ) * size_t( image->bits / 8 ) );
334 std::copy( img.constBits(), img.constBits() + static_cast< std::size_t >( image->width ) * image->height * image->component * ( image->bits / 8 ), image->image.begin() );
335
336 return true;
337}
338
339bool QgsGltfUtils::loadGltfModel( const QByteArray &data, tinygltf::Model &model, QString *errors, QString *warnings )
340{
341 tinygltf::TinyGLTF loader;
342
343 loader.SetImageLoader( QgsGltfUtils::loadImageDataWithQImage, nullptr );
344
345 // in QGIS we always tend towards permissive handling of datasets, allowing
346 // users to load data wherever we can even if it's not strictly conformant
347 // with specifications...
348 // (and there's a lot of non-compliant GLTF out there!)
349 loader.SetParseStrictness( tinygltf::ParseStrictness::Permissive );
350
351 std::string baseDir; // TODO: may be useful to set it from baseUri
352 std::string err, warn;
353
354 bool res;
355 if ( data.startsWith( "glTF" ) ) // 4-byte magic value in binary GLTF
356 {
357 if ( data.at( 4 ) == 1 )
358 {
359 *errors = QObject::tr( "GLTF version 1 tiles cannot be loaded" );
360 return false;
361 }
362 res = loader.LoadBinaryFromMemory( &model, &err, &warn,
363 ( const unsigned char * )data.constData(), data.size(), baseDir );
364 }
365 else
366 {
367 res = loader.LoadASCIIFromString( &model, &err, &warn,
368 data.constData(), data.size(), baseDir );
369 }
370
371 if ( errors )
372 *errors = QString::fromStdString( err );
373 if ( warnings )
374 {
375 *warnings = QString::fromStdString( warn );
376
377 // strip unwanted warnings
378 const thread_local QRegularExpression rxFailedToLoadExternalUriForImage( u"Failed to load external 'uri' for image\\[\\d+\\] name = \".*?\"\\n?"_s );
379 warnings->replace( rxFailedToLoadExternalUriForImage, QString() );
380 const thread_local QRegularExpression rxFileNotFound( u"File not found : .*?\\n"_s );
381 warnings->replace( rxFileNotFound, QString() );
382 }
383
384 return res;
385}
386
387std::size_t QgsGltfUtils::sourceSceneForModel( const tinygltf::Model &model, bool &ok )
388{
389 ok = false;
390 if ( model.scenes.empty() )
391 {
392 return 0;
393 }
394
395 ok = true;
396 int index = model.defaultScene;
397 if ( index >= 0 && static_cast< std::size_t>( index ) < model.scenes.size() )
398 {
399 return index;
400 }
401
402 // just return first scene
403 return 0;
404}
405
406
407#ifdef HAVE_DRACO
408
409void dumpDracoModelInfo( draco::Mesh *dracoMesh )
410{
411 std::cout << "Decoded Draco Mesh:" << dracoMesh->num_points() << " points / " << dracoMesh->num_faces() << " faces" << std::endl;
412 draco::GeometryMetadata *geometryMetadata = dracoMesh->metadata();
413
414 std::cout << "Global Geometry Metadata:" << std::endl;
415 for ( const auto &entry : geometryMetadata->entries() )
416 {
417 std::cout << " Key: " << entry.first << ", Value: " << entry.second.data().size() << std::endl;
418 }
419
420 std::cout << "\nAttribute Metadata:" << std::endl;
421 for ( int32_t i = 0; i < dracoMesh->num_attributes(); ++i )
422 {
423 const draco::PointAttribute *attribute = dracoMesh->attribute( i );
424 if ( !attribute )
425 continue;
426
427 std::cout << " Attribute ID: " << attribute->unique_id() << " / " << draco::PointAttribute::TypeToString( attribute->attribute_type() ) << std::endl;
428 if ( const draco::AttributeMetadata *attributeMetadata = geometryMetadata->attribute_metadata( attribute->unique_id() ) )
429 {
430 for ( const auto &entry : attributeMetadata->entries() )
431 {
432 std::cout << " Key: " << entry.first << ", Length: " << entry.second.data().size() << std::endl;
433 }
434 }
435 }
436}
437
438
439bool QgsGltfUtils::loadDracoModel( const QByteArray &data, const I3SNodeContext &context, tinygltf::Model &model, QString *errors )
440{
441 //
442 // SLPK and Extracted SLPK have the files gzipped
443 //
444
445 QByteArray dataExtracted;
446 if ( data.startsWith( QByteArray( "\x1f\x8b", 2 ) ) )
447 {
448 if ( !QgsZipUtils::decodeGzip( data, dataExtracted ) )
449 {
450 if ( errors )
451 *errors = "Failed to decode gzipped model";
452 return false;
453 }
454 }
455 else
456 {
457 dataExtracted = data;
458 }
459
460 //
461 // load the model in decoder and do basic sanity checks
462 //
463
464 draco::Decoder decoder;
465 draco::DecoderBuffer decoderBuffer;
466 decoderBuffer.Init( dataExtracted.constData(), dataExtracted.size() );
467
468 draco::StatusOr<draco::EncodedGeometryType> geometryTypeStatus = decoder.GetEncodedGeometryType( &decoderBuffer );
469 if ( !geometryTypeStatus.ok() )
470 {
471 if ( errors )
472 *errors = "Failed to get geometry type: " + QString( geometryTypeStatus.status().error_msg() );
473 return false;
474 }
475 if ( geometryTypeStatus.value() != draco::EncodedGeometryType::TRIANGULAR_MESH )
476 {
477 if ( errors )
478 *errors = "Not a triangular mesh";
479 return false;
480 }
481
482 draco::StatusOr<std::unique_ptr<draco::Mesh>> meshStatus = decoder.DecodeMeshFromBuffer( &decoderBuffer );
483 if ( !meshStatus.ok() )
484 {
485 if ( errors )
486 *errors = "Failed to decode mesh: " + QString( meshStatus.status().error_msg() );
487 return false;
488 }
489
490 std::unique_ptr<draco::Mesh> dracoMesh = std::move( meshStatus ).value();
491
492 draco::GeometryMetadata *geometryMetadata = dracoMesh->metadata();
493 if ( !geometryMetadata )
494 {
495 if ( errors )
496 *errors = "Geometry metadata missing";
497 return false;
498 }
499
500 int posAccessorIndex = -1;
501 int normalAccessorIndex = -1;
502 int uvAccessorIndex = -1;
503 int indicesAccessorIndex = -1;
504
505 //
506 // parse XYZ position coordinates
507 //
508
509 const draco::PointAttribute *posAttribute = dracoMesh->GetNamedAttribute( draco::GeometryAttribute::POSITION );
510 if ( posAttribute )
511 {
512 double scaleX = 1, scaleY = 1;
513 const draco::AttributeMetadata *posMetadata = geometryMetadata->attribute_metadata( posAttribute->unique_id() );
514 if ( posMetadata )
515 {
516 posMetadata->GetEntryDouble( "i3s-scale_x", &scaleX );
517 posMetadata->GetEntryDouble( "i3s-scale_y", &scaleY );
518 }
519
520 QgsVector3D nodeCenterLonLat = context.datasetToSceneTransform.transform( context.nodeCenterEcef, Qgis::TransformDirection::Reverse );
521
522 std::vector<unsigned char> posData( dracoMesh->num_points() * 3 * sizeof( float ) );
523 float *posPtr = reinterpret_cast<float *>( posData.data() );
524
525 float values[4];
526 for ( draco::PointIndex i( 0 ); i < dracoMesh->num_points(); ++i )
527 {
528 posAttribute->ConvertValue<float>( posAttribute->mapped_index( i ), posAttribute->num_components(), values );
529
530 // when using EPSG:4326, the X,Y coordinates are in degrees(!) relative to the node's center (in lat/lon degrees),
531 // but they are scaled (because they are several orders of magnitude smaller than Z coordinates).
532 // That scaling is applied so that Draco's compression works well.
533 // when using local CRS, scaling is not applied (not needed)
534 if ( context.isGlobalMode )
535 {
536 double lonDeg = double( values[0] ) * scaleX + nodeCenterLonLat.x();
537 double latDeg = double( values[1] ) * scaleY + nodeCenterLonLat.y();
538 double alt = double( values[2] ) + nodeCenterLonLat.z();
539 QgsVector3D ecef = context.datasetToSceneTransform.transform( QgsVector3D( lonDeg, latDeg, alt ) );
540 QgsVector3D localPos = ecef - context.nodeCenterEcef;
541
542 values[0] = static_cast<float>( localPos.x() );
543 values[1] = static_cast<float>( localPos.y() );
544 values[2] = static_cast<float>( localPos.z() );
545 }
546
547 posPtr[i.value() * 3 + 0] = values[0];
548 posPtr[i.value() * 3 + 1] = values[1];
549 posPtr[i.value() * 3 + 2] = values[2];
550 }
551
552 tinygltf::Buffer posBuffer;
553 posBuffer.data = posData;
554 model.buffers.emplace_back( std::move( posBuffer ) );
555
556 tinygltf::BufferView posBufferView;
557 posBufferView.buffer = static_cast<int>( model.buffers.size() ) - 1;
558 posBufferView.byteOffset = 0;
559 posBufferView.byteLength = posData.size();
560 posBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER;
561 model.bufferViews.emplace_back( std::move( posBufferView ) );
562
563 tinygltf::Accessor posAccessor;
564 posAccessor.bufferView = static_cast<int>( model.bufferViews.size() ) - 1;
565 posAccessor.byteOffset = 0;
566 posAccessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT;
567 posAccessor.count = dracoMesh->num_points();
568 posAccessor.type = TINYGLTF_TYPE_VEC3;
569 model.accessors.emplace_back( std::move( posAccessor ) );
570
571 posAccessorIndex = static_cast<int>( model.accessors.size() ) - 1;
572 }
573
574 //
575 // parse normal vectors
576 //
577
578 const draco::PointAttribute *normalAttribute = dracoMesh->GetNamedAttribute( draco::GeometryAttribute::NORMAL );
579 if ( normalAttribute )
580 {
581 std::vector<unsigned char> normalData( dracoMesh->num_points() * 3 * sizeof( float ) );
582 float *normalPtr = reinterpret_cast<float *>( normalData.data() );
583
584 float values[3];
585 for ( draco::PointIndex i( 0 ); i < dracoMesh->num_points(); ++i )
586 {
587 normalAttribute->ConvertValue<float>( normalAttribute->mapped_index( i ), normalAttribute->num_components(), values );
588
589 normalPtr[i.value() * 3 + 0] = values[0];
590 normalPtr[i.value() * 3 + 1] = values[1];
591 normalPtr[i.value() * 3 + 2] = values[2];
592 }
593
594 tinygltf::Buffer normalBuffer;
595 normalBuffer.data = normalData;
596 model.buffers.emplace_back( std::move( normalBuffer ) );
597
598 tinygltf::BufferView normalBufferView;
599 normalBufferView.buffer = static_cast<int>( model.buffers.size() ) - 1;
600 normalBufferView.byteOffset = 0;
601 normalBufferView.byteLength = normalData.size();
602 normalBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER;
603 model.bufferViews.emplace_back( std::move( normalBufferView ) );
604
605 tinygltf::Accessor normalAccessor;
606 normalAccessor.bufferView = static_cast<int>( model.bufferViews.size() ) - 1;
607 normalAccessor.byteOffset = 0;
608 normalAccessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT;
609 normalAccessor.count = dracoMesh->num_points();
610 normalAccessor.type = TINYGLTF_TYPE_VEC3;
611 model.accessors.emplace_back( std::move( normalAccessor ) );
612
613 normalAccessorIndex = static_cast<int>( model.accessors.size() ) - 1;
614 }
615
616 //
617 // parse UV texture coordinates
618 //
619
620 const draco::PointAttribute *uvAttribute = dracoMesh->GetNamedAttribute( draco::GeometryAttribute::TEX_COORD );
621 if ( uvAttribute )
622 {
623 std::vector<unsigned char> uvData( dracoMesh->num_points() * 2 * sizeof( float ) );
624 float *uvPtr = reinterpret_cast<float *>( uvData.data() );
625
626 // try to find UV region attribute - if it exists, we will need to adjust
627 // texture UV values based on its regions
628 const draco::PointAttribute *uvRegionAttribute = nullptr;
629 for ( int32_t i = 0; i < dracoMesh->num_attributes(); ++i )
630 {
631 const draco::PointAttribute *attribute = dracoMesh->attribute( i );
632 if ( !attribute )
633 continue;
634
635 draco::AttributeMetadata *attributeMetadata = geometryMetadata->attribute_metadata( attribute->unique_id() );
636 if ( !attributeMetadata )
637 continue;
638
639 std::string i3sAttributeType;
640 if ( attributeMetadata->GetEntryString( "i3s-attribute-type", &i3sAttributeType ) && i3sAttributeType == "uv-region" )
641 {
642 uvRegionAttribute = attribute;
643 }
644 }
645
646 float values[2];
647 for ( draco::PointIndex i( 0 ); i < dracoMesh->num_points(); ++i )
648 {
649 uvAttribute->ConvertValue<float>( uvAttribute->mapped_index( i ), uvAttribute->num_components(), values );
650
651 if ( uvRegionAttribute )
652 {
653 // UV regions are 4 x uint16 per each vertex [uMin, vMin, uMax, vMax], and they define
654 // a sub-region within a texture to which UV coordinates of each vertex belong.
655 // I have no idea why there's such extra complication for clients... the final
656 // UV coordinates could have been easily calculated by the dataset producer.
657 uint16_t uvRegion[4];
658 uvRegionAttribute->ConvertValue<uint16_t>( uvRegionAttribute->mapped_index( i ), uvRegionAttribute->num_components(), uvRegion );
659 float uMin = static_cast<float>( uvRegion[0] ) / 65535.f;
660 float vMin = static_cast<float>( uvRegion[1] ) / 65535.f;
661 float uMax = static_cast<float>( uvRegion[2] ) / 65535.f;
662 float vMax = static_cast<float>( uvRegion[3] ) / 65535.f;
663 values[0] = uMin + values[0] * ( uMax - uMin );
664 values[1] = vMin + values[1] * ( vMax - vMin );
665 }
666
667 uvPtr[i.value() * 2 + 0] = values[0];
668 uvPtr[i.value() * 2 + 1] = values[1];
669 }
670
671 tinygltf::Buffer uvBuffer;
672 uvBuffer.data = uvData;
673 model.buffers.emplace_back( std::move( uvBuffer ) );
674
675 tinygltf::BufferView uvBufferView;
676 uvBufferView.buffer = static_cast<int>( model.buffers.size() ) - 1;
677 uvBufferView.byteOffset = 0;
678 uvBufferView.byteLength = uvData.size();
679 uvBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER;
680 model.bufferViews.emplace_back( std::move( uvBufferView ) ) ;
681
682 tinygltf::Accessor uvAccessor;
683 uvAccessor.bufferView = static_cast<int>( model.bufferViews.size() ) - 1;
684 uvAccessor.byteOffset = 0;
685 uvAccessor.componentType = TINYGLTF_COMPONENT_TYPE_FLOAT;
686 uvAccessor.count = dracoMesh->num_points();
687 uvAccessor.type = TINYGLTF_TYPE_VEC2;
688 model.accessors.emplace_back( std::move( uvAccessor ) );
689
690 uvAccessorIndex = static_cast<int>( model.accessors.size() ) - 1;
691 }
692
693 //
694 // parse indices of triangles
695 //
696
697 // TODO: to save some memory, we could use only 1 or 2 bytes per vertex if the mesh is small enough
698 std::vector<unsigned char> indexData;
699 indexData.resize( dracoMesh->num_faces() * 3 * sizeof( quint32 ) );
700 Q_ASSERT( sizeof( dracoMesh->face( draco::FaceIndex( 0 ) )[0] ) == sizeof( quint32 ) );
701 memcpy( indexData.data(), &dracoMesh->face( draco::FaceIndex( 0 ) )[0], indexData.size() );
702
703 tinygltf::Buffer gltfIndexBuffer;
704 gltfIndexBuffer.data = indexData;
705 model.buffers.emplace_back( std::move( gltfIndexBuffer ) );
706
707 tinygltf::BufferView indexBufferView;
708 indexBufferView.buffer = static_cast<int>( model.buffers.size() ) - 1;
709 indexBufferView.byteLength = dracoMesh->num_faces() * 3 * sizeof( quint32 );
710 indexBufferView.byteOffset = 0;
711 indexBufferView.byteStride = 0;
712 indexBufferView.target = TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER;
713 model.bufferViews.emplace_back( std::move( indexBufferView ) );
714
715 tinygltf::Accessor indicesAccessor;
716 indicesAccessor.bufferView = static_cast<int>( model.bufferViews.size() ) - 1;
717 indicesAccessor.byteOffset = 0;
718 indicesAccessor.count = dracoMesh->num_faces() * 3;
719 indicesAccessor.type = TINYGLTF_TYPE_SCALAR;
720 indicesAccessor.componentType = TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT;
721 model.accessors.emplace_back( std::move( indicesAccessor ) );
722
723 indicesAccessorIndex = static_cast<int>( model.accessors.size() ) - 1;
724
725 //
726 // construct the GLTF model
727 //
728
729 tinygltf::Material material;
730 int materialIndex = loadMaterialFromMetadata( context.materialInfo, model );
731
732 tinygltf::Primitive primitive;
733 primitive.mode = TINYGLTF_MODE_TRIANGLES;
734 primitive.material = materialIndex;
735 primitive.indices = indicesAccessorIndex;
736 if ( posAccessorIndex != -1 )
737 primitive.attributes["POSITION"] = posAccessorIndex;
738 if ( normalAccessorIndex != -1 )
739 primitive.attributes["NORMAL"] = normalAccessorIndex;
740 if ( uvAccessorIndex != -1 )
741 primitive.attributes["TEXCOORD_0"] = uvAccessorIndex;
742
743 tinygltf::Mesh tiny_mesh;
744 tiny_mesh.primitives.emplace_back( std::move( primitive ) );
745 model.meshes.emplace_back( std::move( tiny_mesh ) );
746
747 tinygltf::Node node;
748 node.mesh = 0;
749 model.nodes.emplace_back( std::move( node ) );
750
751 tinygltf::Scene scene;
752 scene.nodes.push_back( 0 );
753 model.scenes.emplace_back( std::move( scene ) );
754
755 model.defaultScene = 0;
756 model.asset.version = "2.0";
757
758 return true;
759}
760#else
761
762bool QgsGltfUtils::loadDracoModel( const QByteArray &data, const I3SNodeContext &context, tinygltf::Model &model, QString *errors )
763{
764 Q_UNUSED( data );
765 Q_UNUSED( context );
766 Q_UNUSED( model );
767 if ( errors )
768 *errors = "Cannot load geometry - QGIS was built without Draco library.";
769 return false;
770}
771
772#endif
773
774int QgsGltfUtils::loadMaterialFromMetadata( const QVariantMap &materialInfo, tinygltf::Model &model )
775{
776 tinygltf::Material material;
777 material.name = "DefaultMaterial";
778
779 QVariantList colorList = materialInfo["pbrBaseColorFactor"].toList();
780 material.pbrMetallicRoughness.baseColorFactor = { colorList[0].toDouble(), colorList[1].toDouble(), colorList[2].toDouble(), colorList[3].toDouble() };
781
782 if ( materialInfo.contains( "pbrBaseColorTexture" ) )
783 {
784 QString baseColorTextureUri = materialInfo["pbrBaseColorTexture"].toString();
785
786 tinygltf::Image img;
787 img.uri = baseColorTextureUri.toStdString(); // file:/// or http:// ... will be fetched by QGIS
788 model.images.emplace_back( std::move( img ) );
789
790 tinygltf::Texture tex;
791 tex.source = static_cast<int>( model.images.size() ) - 1;
792 model.textures.emplace_back( std::move( tex ) );
793
794 material.pbrMetallicRoughness.baseColorTexture.index = static_cast<int>( model.textures.size() ) - 1;
795 }
796
797 if ( materialInfo.contains( "doubleSided" ) )
798 {
799 material.doubleSided = materialInfo["doubleSided"].toInt();
800 }
801
802 // add the new material to the model
803 model.materials.emplace_back( std::move( material ) );
804
805 return static_cast<int>( model.materials.size() ) - 1;
806}
807
808bool QgsGltfUtils::writeGltfModel( const tinygltf::Model &model, const QString &outputFilename )
809{
810 tinygltf::TinyGLTF gltf;
811 bool res = gltf.WriteGltfSceneToFile( &model,
812 outputFilename.toStdString(),
813 false, // embedImages
814 true, // embedBuffers
815 false, // prettyPrint
816 true ); // writeBinary
817 return res;
818}
819
820void QgsGltfUtils::I3SNodeContext::initFromTile( const QgsTiledSceneTile &tile, const QgsCoordinateReferenceSystem &layerCrs, const QgsCoordinateReferenceSystem &sceneCrs, const QgsCoordinateTransformContext &transformContext )
821{
822 const QVariantMap tileMetadata = tile.metadata();
823
824 materialInfo = tileMetadata[u"material"_s].toMap();
825 isGlobalMode = sceneCrs.type() == Qgis::CrsType::Geocentric;
826 if ( isGlobalMode )
827 {
828 nodeCenterEcef = tile.boundingVolume().box().center();
829 datasetToSceneTransform = QgsCoordinateTransform( layerCrs, sceneCrs, transformContext );
830 }
831}
832
@ Geocentric
Geocentric CRS.
Definition qgis.h:2388
Axis
Cartesian axes.
Definition qgis.h:2509
@ X
X-axis.
Definition qgis.h:2510
@ Z
Z-axis.
Definition qgis.h:2512
@ Y
Y-axis.
Definition qgis.h:2511
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2731
Represents a coordinate reference system (CRS).
Qgis::CrsType type() const
Returns the type of the CRS.
Contains information about the context in which a coordinate transform is executed.
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.
Custom exception class for Coordinate Reference System related exceptions.
A simple 4x4 matrix implementation useful for transformation in 3D space.
QgsVector3D map(const QgsVector3D &vector) const
Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,...
QgsVector3D center() const
Returns the vector to the center of the box.
QgsOrientedBox3D box() const
Returns the volume's oriented box.
Represents an individual tile from a tiled scene data source.
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
QVariantMap metadata() const
Returns additional metadata attached to the tile.
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:52
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:54
bool isNull() const
Returns true if all three coordinates are zero.
Definition qgsvector3d.h:47
double x() const
Returns X coordinate.
Definition qgsvector3d.h:50
static bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success.
#define QgsDebugError(str)
Definition qgslogger.h:59