QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 "qgsgltfutils.h"
17
19#include "qgsexception.h"
20#include "qgsmatrix4x4.h"
21#include "qgsconfig.h"
22#include "qgslogger.h"
23
24#include <QImage>
25#include <QMatrix4x4>
26#include <QRegularExpression>
27
28#define TINYGLTF_IMPLEMENTATION // should be defined just in one CPP file
29
30// decompression of meshes with Draco is optional, but recommended
31// because some 3D Tiles datasets use it (KHR_draco_mesh_compression is an optional extension of GLTF)
32#ifdef HAVE_DRACO
33#define TINYGLTF_ENABLE_DRACO
34#endif
35
36#define TINYGLTF_NO_STB_IMAGE // we use QImage-based reading of images
37#define TINYGLTF_NO_STB_IMAGE_WRITE // we don't need writing of images
38//#define TINYGLTF_NO_FS
39
40//#include <fstream>
41#include "tiny_gltf.h"
42
44
45
46bool 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 )
47{
48 const tinygltf::Accessor &accessor = model.accessors[accessorIndex];
49 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
50 const tinygltf::Buffer &b = model.buffers[bv.buffer];
51
52 if ( accessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || accessor.type != TINYGLTF_TYPE_VEC3 )
53 {
54 // we may support more input types in the future if needed
55 return false;
56 }
57
58 const unsigned char *ptr = b.data.data() + bv.byteOffset + accessor.byteOffset;
59
60 vx.resize( accessor.count );
61 vy.resize( accessor.count );
62 vz.resize( accessor.count );
63 double *vxOut = vx.data();
64 double *vyOut = vy.data();
65 double *vzOut = vz.data();
66 for ( int i = 0; i < static_cast<int>( accessor.count ); ++i )
67 {
68 const float *fptr = reinterpret_cast<const float *>( ptr );
69 QVector3D vOrig( fptr[0], fptr[1], fptr[2] );
70
71 if ( nodeTransform )
72 vOrig = nodeTransform->map( vOrig );
73
75 switch ( gltfUpAxis )
76 {
77 case Qgis::Axis::X:
78 {
79 QgsDebugError( QStringLiteral( "X up translation not yet supported" ) );
80 v = tileTransform.map( tileTranslationEcef );
81 break;
82 }
83
84 case Qgis::Axis::Y:
85 {
86 // go from y-up to z-up according to 3D Tiles spec
87 QVector3D vFlip( vOrig.x(), -vOrig.z(), vOrig.y() );
88 v = tileTransform.map( QgsVector3D( vFlip ) + tileTranslationEcef );
89 break;
90 }
91
92 case Qgis::Axis::Z:
93 {
94 v = tileTransform.map( QgsVector3D( vOrig ) + tileTranslationEcef );
95 break;
96 }
97 }
98
99 *vxOut++ = v.x();
100 *vyOut++ = v.y();
101 *vzOut++ = v.z();
102
103 if ( bv.byteStride )
104 ptr += bv.byteStride;
105 else
106 ptr += 3 * sizeof( float );
107 }
108
109 if ( ecefToTargetCrs )
110 {
111 try
112 {
113 ecefToTargetCrs->transformCoords( accessor.count, vx.data(), vy.data(), vz.data() );
114 }
115 catch ( QgsCsException & )
116 {
117 return false;
118 }
119 }
120
121 return true;
122}
123
124bool QgsGltfUtils::extractTextureCoordinates( const tinygltf::Model &model, int accessorIndex, QVector<float> &x, QVector<float> &y )
125{
126 const tinygltf::Accessor &accessor = model.accessors[accessorIndex];
127 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
128 const tinygltf::Buffer &b = model.buffers[bv.buffer];
129
130 if ( accessor.componentType != TINYGLTF_PARAMETER_TYPE_FLOAT || accessor.type != TINYGLTF_TYPE_VEC2 )
131 {
132 return false;
133 }
134
135 const unsigned char *ptr = b.data.data() + bv.byteOffset + accessor.byteOffset;
136 x.resize( accessor.count );
137 y.resize( accessor.count );
138
139 float *xOut = x.data();
140 float *yOut = y.data();
141
142 for ( std::size_t i = 0; i < accessor.count; i++ )
143 {
144 const float *fptr = reinterpret_cast< const float * >( ptr );
145
146 *xOut++ = fptr[0];
147 *yOut++ = fptr[1];
148
149 if ( bv.byteStride )
150 ptr += bv.byteStride;
151 else
152 ptr += 2 * sizeof( float );
153 }
154 return true;
155}
156
157QgsGltfUtils::ResourceType QgsGltfUtils::imageResourceType( const tinygltf::Model &model, int index )
158{
159 const tinygltf::Image &img = model.images[index];
160
161 if ( !img.image.empty() )
162 {
163 return ResourceType::Embedded;
164 }
165 else
166 {
167 return ResourceType::Linked;
168 }
169}
170
171QImage QgsGltfUtils::extractEmbeddedImage( const tinygltf::Model &model, int index )
172{
173 const tinygltf::Image &img = model.images[index];
174 if ( !img.image.empty() )
175 return QImage( img.image.data(), img.width, img.height, QImage::Format_ARGB32 );
176 else
177 return QImage();
178}
179
180QString QgsGltfUtils::linkedImagePath( const tinygltf::Model &model, int index )
181{
182 const tinygltf::Image &img = model.images[index];
183 return QString::fromStdString( img.uri );
184}
185
186std::unique_ptr<QMatrix4x4> QgsGltfUtils::parseNodeTransform( const tinygltf::Node &node )
187{
188 // read node's transform: either specified with 4x4 "matrix" element
189 // -OR- given by "translation", "rotation" and "scale" elements (to be combined as T * R * S)
190 std::unique_ptr<QMatrix4x4> matrix;
191 if ( !node.matrix.empty() )
192 {
193 matrix.reset( new QMatrix4x4 );
194 float *mdata = matrix->data();
195 for ( int i = 0; i < 16; ++i )
196 mdata[i] = static_cast< float >( node.matrix[i] );
197 }
198 else if ( node.translation.size() || node.rotation.size() || node.scale.size() )
199 {
200 matrix.reset( new QMatrix4x4 );
201 if ( node.scale.size() )
202 {
203 matrix->scale( static_cast< float >( node.scale[0] ), static_cast< float >( node.scale[1] ), static_cast< float >( node.scale[2] ) );
204 }
205 if ( node.rotation.size() )
206 {
207 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] ) ) );
208 }
209 if ( node.translation.size() )
210 {
211 matrix->translate( static_cast< float >( node.translation[0] ), static_cast< float >( node.translation[1] ), static_cast< float >( node.translation[2] ) );
212 }
213 }
214 return matrix;
215}
216
217
218QgsVector3D QgsGltfUtils::extractTileTranslation( tinygltf::Model &model, Qgis::Axis upAxis )
219{
220 bool sceneOk = false;
221 const std::size_t sceneIndex = QgsGltfUtils::sourceSceneForModel( model, sceneOk );
222 if ( !sceneOk )
223 {
224 return QgsVector3D();
225 }
226
227 const tinygltf::Scene &scene = model.scenes[sceneIndex];
228
229 QgsVector3D tileTranslationEcef;
230 auto it = model.extensions.find( "CESIUM_RTC" );
231 if ( it != model.extensions.end() )
232 {
233 const tinygltf::Value v = it->second;
234 if ( v.IsObject() && v.Has( "center" ) )
235 {
236 const tinygltf::Value center = v.Get( "center" );
237 if ( center.IsArray() && center.Size() == 3 )
238 {
239 tileTranslationEcef = QgsVector3D( center.Get( 0 ).GetNumberAsDouble(), center.Get( 1 ).GetNumberAsDouble(), center.Get( 2 ).GetNumberAsDouble() );
240 }
241 }
242 }
243
244 if ( scene.nodes.size() == 0 )
245 return QgsVector3D();
246
247 int rootNodeIndex = scene.nodes[0];
248 tinygltf::Node &rootNode = model.nodes[rootNodeIndex];
249
250 if ( tileTranslationEcef.isNull() && rootNode.translation.size() )
251 {
252 QgsVector3D rootTranslation( rootNode.translation[0], rootNode.translation[1], rootNode.translation[2] );
253
254 // if root node of a GLTF contains translation by a large amount, let's handle it as the tile translation.
255 // this will ensure that we keep double precision rather than losing precision when dealing with floats
256 if ( rootTranslation.length() > 1e6 )
257 {
258 switch ( upAxis )
259 {
260 case Qgis::Axis::X:
261 QgsDebugError( QStringLiteral( "X up translation not yet supported" ) );
262 break;
263 case Qgis::Axis::Y:
264 {
265 // we flip Y/Z axes here because GLTF uses Y-up convention, while 3D Tiles use Z-up convention
266 tileTranslationEcef = QgsVector3D( rootTranslation.x(), -rootTranslation.z(), rootTranslation.y() );
267 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
268 break;
269 }
270 case Qgis::Axis::Z:
271 {
272 tileTranslationEcef = QgsVector3D( rootTranslation.x(), rootTranslation.y(), rootTranslation.z() );
273 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
274 break;
275 }
276 }
277 }
278 }
279
280 return tileTranslationEcef;
281}
282
283
284bool QgsGltfUtils::loadImageDataWithQImage(
285 tinygltf::Image *image, const int image_idx, std::string *err,
286 std::string *warn, int req_width, int req_height,
287 const unsigned char *bytes, int size, void *user_data )
288{
289
290 if ( req_width != 0 || req_height != 0 )
291 {
292 if ( err )
293 {
294 ( *err ) += "Expecting zero req_width/req_height.\n";
295 }
296 return false;
297 }
298
299 ( void )warn;
300 ( void )user_data;
301
302 QImage img;
303 if ( !img.loadFromData( bytes, size ) )
304 {
305 if ( err )
306 {
307 ( *err ) +=
308 "Unknown image format. QImage cannot decode image data for image[" +
309 std::to_string( image_idx ) + "] name = \"" + image->name + "\".\n";
310 }
311 return false;
312 }
313
314 if ( img.format() != QImage::Format_RGB32 && img.format() != QImage::Format_ARGB32 )
315 {
316 // we may want to natively support other formats as well as long as such texture formats are allowed
317 img.convertTo( QImage::Format_RGB32 );
318 }
319
320 image->width = img.width();
321 image->height = img.height();
322 image->component = 4;
323 image->bits = 8;
324 image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE;
325
326 image->image.resize( static_cast<size_t>( image->width * image->height * image->component ) * size_t( image->bits / 8 ) );
327 std::copy( img.constBits(), img.constBits() + static_cast< std::size_t >( image->width ) * image->height * image->component * ( image->bits / 8 ), image->image.begin() );
328
329 return true;
330}
331
332bool QgsGltfUtils::loadGltfModel( const QByteArray &data, tinygltf::Model &model, QString *errors, QString *warnings )
333{
334 tinygltf::TinyGLTF loader;
335
336 loader.SetImageLoader( QgsGltfUtils::loadImageDataWithQImage, nullptr );
337
338 // in QGIS we always tend towards permissive handling of datasets, allowing
339 // users to load data wherever we can even if it's not strictly conformant
340 // with specifications...
341 // (and there's a lot of non-compliant GLTF out there!)
342 loader.SetParseStrictness( tinygltf::ParseStrictness::Permissive );
343
344 std::string baseDir; // TODO: may be useful to set it from baseUri
345 std::string err, warn;
346
347 bool res;
348 if ( data.startsWith( "glTF" ) ) // 4-byte magic value in binary GLTF
349 {
350 if ( data.at( 4 ) == 1 )
351 {
352 *errors = QObject::tr( "GLTF version 1 tiles cannot be loaded" );
353 return false;
354 }
355 res = loader.LoadBinaryFromMemory( &model, &err, &warn,
356 ( const unsigned char * )data.constData(), data.size(), baseDir );
357 }
358 else
359 {
360 res = loader.LoadASCIIFromString( &model, &err, &warn,
361 data.constData(), data.size(), baseDir );
362 }
363
364 if ( errors )
365 *errors = QString::fromStdString( err );
366 if ( warnings )
367 {
368 *warnings = QString::fromStdString( warn );
369
370 // strip unwanted warnings
371 const thread_local QRegularExpression rxFailedToLoadExternalUriForImage( QStringLiteral( "Failed to load external 'uri' for image\\[\\d+\\] name = \".*?\"\\n?" ) );
372 warnings->replace( rxFailedToLoadExternalUriForImage, QString() );
373 const thread_local QRegularExpression rxFileNotFound( QStringLiteral( "File not found : .*?\\n" ) );
374 warnings->replace( rxFileNotFound, QString() );
375 }
376
377 return res;
378}
379
380std::size_t QgsGltfUtils::sourceSceneForModel( const tinygltf::Model &model, bool &ok )
381{
382 ok = false;
383 if ( model.scenes.empty() )
384 {
385 return 0;
386 }
387
388 ok = true;
389 int index = model.defaultScene;
390 if ( index >= 0 && static_cast< std::size_t>( index ) < model.scenes.size() )
391 {
392 return index;
393 }
394
395 // just return first scene
396 return 0;
397}
398
Axis
Cartesian axes.
Definition: qgis.h:1989
@ X
X-axis.
@ Z
Z-axis.
@ Y
Y-axis.
Class for doing transforms between two map 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.
Definition: qgsexception.h:67
A simple 4x4 matrix implementation useful for transformation in 3D space.
Definition: qgsmatrix4x4.h:40
QgsVector3D map(const QgsVector3D &vector) const
Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,...
Definition: qgsmatrix4x4.h:80
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition: qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition: qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition: qgsvector3d.h:52
bool isNull() const
Returns true if all three coordinates are zero.
Definition: qgsvector3d.h:45
double x() const
Returns X coordinate.
Definition: qgsvector3d.h:48
#define QgsDebugError(str)
Definition: qgslogger.h:38