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