QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
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 "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 const tinygltf::Scene &scene = model.scenes[model.defaultScene];
221
222 QgsVector3D tileTranslationEcef;
223 auto it = model.extensions.find( "CESIUM_RTC" );
224 if ( it != model.extensions.end() )
225 {
226 const tinygltf::Value v = it->second;
227 if ( v.IsObject() && v.Has( "center" ) )
228 {
229 const tinygltf::Value center = v.Get( "center" );
230 if ( center.IsArray() && center.Size() == 3 )
231 {
232 tileTranslationEcef = QgsVector3D( center.Get( 0 ).GetNumberAsDouble(), center.Get( 1 ).GetNumberAsDouble(), center.Get( 2 ).GetNumberAsDouble() );
233 }
234 }
235 }
236
237 if ( scene.nodes.size() == 0 )
238 return QgsVector3D();
239
240 int rootNodeIndex = scene.nodes[0];
241 tinygltf::Node &rootNode = model.nodes[rootNodeIndex];
242
243 if ( tileTranslationEcef.isNull() && rootNode.translation.size() )
244 {
245 QgsVector3D rootTranslation( rootNode.translation[0], rootNode.translation[1], rootNode.translation[2] );
246
247 // if root node of a GLTF contains translation by a large amount, let's handle it as the tile translation.
248 // this will ensure that we keep double precision rather than losing precision when dealing with floats
249 if ( rootTranslation.length() > 1e6 )
250 {
251 switch ( upAxis )
252 {
253 case Qgis::Axis::X:
254 QgsDebugError( QStringLiteral( "X up translation not yet supported" ) );
255 break;
256 case Qgis::Axis::Y:
257 {
258 // we flip Y/Z axes here because GLTF uses Y-up convention, while 3D Tiles use Z-up convention
259 tileTranslationEcef = QgsVector3D( rootTranslation.x(), -rootTranslation.z(), rootTranslation.y() );
260 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
261 break;
262 }
263 case Qgis::Axis::Z:
264 {
265 tileTranslationEcef = QgsVector3D( rootTranslation.x(), rootTranslation.y(), rootTranslation.z() );
266 rootNode.translation[0] = rootNode.translation[1] = rootNode.translation[2] = 0;
267 break;
268 }
269 }
270 }
271 }
272
273 return tileTranslationEcef;
274}
275
276
277bool QgsGltfUtils::loadImageDataWithQImage(
278 tinygltf::Image *image, const int image_idx, std::string *err,
279 std::string *warn, int req_width, int req_height,
280 const unsigned char *bytes, int size, void *user_data )
281{
282
283 if ( req_width != 0 || req_height != 0 )
284 {
285 if ( err )
286 {
287 ( *err ) += "Expecting zero req_width/req_height.\n";
288 }
289 return false;
290 }
291
292 ( void )warn;
293 ( void )user_data;
294
295 QImage img;
296 if ( !img.loadFromData( bytes, size ) )
297 {
298 if ( err )
299 {
300 ( *err ) +=
301 "Unknown image format. QImage cannot decode image data for image[" +
302 std::to_string( image_idx ) + "] name = \"" + image->name + "\".\n";
303 }
304 return false;
305 }
306
307 if ( img.format() != QImage::Format_RGB32 && img.format() != QImage::Format_ARGB32 )
308 {
309 // we may want to natively support other formats as well as long as such texture formats are allowed
310 img.convertTo( QImage::Format_RGB32 );
311 }
312
313 image->width = img.width();
314 image->height = img.height();
315 image->component = 4;
316 image->bits = 8;
317 image->pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE;
318
319 image->image.resize( static_cast<size_t>( image->width * image->height * image->component ) * size_t( image->bits / 8 ) );
320 std::copy( img.constBits(), img.constBits() + static_cast< std::size_t >( image->width ) * image->height * image->component * ( image->bits / 8 ), image->image.begin() );
321
322 return true;
323}
324
325bool QgsGltfUtils::loadGltfModel( const QByteArray &data, tinygltf::Model &model, QString *errors, QString *warnings )
326{
327 tinygltf::TinyGLTF loader;
328
329 loader.SetImageLoader( QgsGltfUtils::loadImageDataWithQImage, nullptr );
330
331 // in QGIS we always tend towards permissive handling of datasets, allowing
332 // users to load data wherever we can even if it's not strictly conformant
333 // with specifications...
334 // (and there's a lot of non-compliant GLTF out there!)
335 loader.SetParseStrictness( tinygltf::ParseStrictness::Permissive );
336
337 std::string baseDir; // TODO: may be useful to set it from baseUri
338 std::string err, warn;
339
340 bool res;
341 if ( data.startsWith( "glTF" ) ) // 4-byte magic value in binary GLTF
342 {
343 if ( data.at( 4 ) == 1 )
344 {
345 *errors = QObject::tr( "GLTF version 1 tiles cannot be loaded" );
346 return false;
347 }
348 res = loader.LoadBinaryFromMemory( &model, &err, &warn,
349 ( const unsigned char * )data.constData(), data.size(), baseDir );
350 }
351 else
352 {
353 res = loader.LoadASCIIFromString( &model, &err, &warn,
354 data.constData(), data.size(), baseDir );
355 }
356
357 if ( errors )
358 *errors = QString::fromStdString( err );
359 if ( warnings )
360 {
361 *warnings = QString::fromStdString( warn );
362
363 // strip unwanted warnings
364 const thread_local QRegularExpression rxFailedToLoadExternalUriForImage( QStringLiteral( "Failed to load external 'uri' for image\\[\\d+\\] name = \".*?\"\\n?" ) );
365 warnings->replace( rxFailedToLoadExternalUriForImage, QString() );
366 const thread_local QRegularExpression rxFileNotFound( QStringLiteral( "File not found : .*?\\n" ) );
367 warnings->replace( rxFileNotFound, QString() );
368 }
369
370 return res;
371}
372
Axis
Cartesian axes.
Definition qgis.h:1784
@ 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.
A simple 4x4 matrix implementation useful for transformation in 3D space.
QgsVector3D map(const QgsVector3D &vector) const
Matrix-vector multiplication (vector is converted to homogenous coordinates [X,Y,Z,...
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition qgsvector3d.h:32
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:51
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:53
bool isNull() const
Returns true if all three coordinates are zero.
Definition qgsvector3d.h:46
double x() const
Returns X coordinate.
Definition qgsvector3d.h:49
#define QgsDebugError(str)
Definition qgslogger.h:38