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