QGIS API Documentation 4.3.0-Master (0cfde48c85b)
Loading...
Searching...
No Matches
qgscesiumutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscesiumutils.cpp
3 --------------------
4 begin : July 2023
5 copyright : (C) 2023 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ******************************************************************
8 ***************************************************************************/
9
10/***************************************************************************
11 * *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; either version 2 of the License, or *
15 * (at your option) any later version. *
16 * *
17 ***************************************************************************/
18
19#include "qgscesiumutils.h"
20
21#include <cstring>
22#include <nlohmann/json.hpp>
23
27#include "qgsellipsoidutils.h"
28#include "qgsgltfutils.h"
29#include "qgsjsonutils.h"
30#include "qgslogger.h"
31#include "qgsmatrix4x4.h"
32#include "qgsorientedbox3d.h"
33#include "qgssphere.h"
35#include "tiny_gltf.h"
36
37#include <QFile>
38#include <QGenericMatrix>
39#include <QIODevice>
40#include <QMatrix3x3>
41#include <QNetworkRequest>
42#include <QString>
43#include <QUrlQuery>
44#include <QtCore/QBuffer>
45
46using namespace Qt::StringLiterals;
47
49{
50 try
51 {
52 // The latitude and longitude values are given in radians!
53 // TODO -- is this ALWAYS the case? What if there's a region root bounding volume, but a transform object present? What if there's crs metadata specifying a different crs?
54
55 const double west = region[0].get<double>() * 180 / M_PI;
56 const double south = region[1].get<double>() * 180 / M_PI;
57 const double east = region[2].get<double>() * 180 / M_PI;
58 const double north = region[3].get<double>() * 180 / M_PI;
59 double minHeight = region[4].get<double>();
60 double maxHeight = region[5].get<double>();
61
62 return QgsBox3D( west, south, minHeight, east, north, maxHeight );
63 }
64 catch ( nlohmann::json::exception & )
65 {
66 return QgsBox3D();
67 }
68}
69
70QgsBox3D QgsCesiumUtils::parseRegion( const QVariantList &region )
71{
72 if ( region.size() != 6 )
73 return QgsBox3D();
74
76}
77
79{
80 if ( box.size() != 12 )
81 return QgsOrientedBox3D();
82
83 try
84 {
86 for ( int i = 0; i < 3; ++i )
87 {
88 res.mCenter[i] = box[i].get<double>();
89 }
90 for ( int i = 0; i < 9; ++i )
91 {
92 res.mHalfAxes[i] = box[i + 3].get<double>();
93 }
94 return res;
95 }
96 catch ( nlohmann::json::exception & )
97 {
98 return QgsOrientedBox3D();
99 }
100}
101
103{
104 if ( box.size() != 12 )
105 return QgsOrientedBox3D();
106
108}
109
111{
112 if ( sphere.size() != 4 )
113 return QgsSphere();
114
115 try
116 {
117 const double centerX = sphere[0].get<double>();
118 const double centerY = sphere[1].get<double>();
119 const double centerZ = sphere[2].get<double>();
120 const double radius = sphere[3].get<double>();
121 return QgsSphere( centerX, centerY, centerZ, radius );
122 }
123 catch ( nlohmann::json::exception & )
124 {
125 return QgsSphere();
126 }
127}
128
129QgsSphere QgsCesiumUtils::parseSphere( const QVariantList &sphere )
130{
131 if ( sphere.size() != 4 )
132 return QgsSphere();
133
134 return parseSphere( QgsJsonUtils::jsonFromVariant( sphere ) );
135}
136
138{
139 if ( !transform.isIdentity() )
140 {
141 // center is transformed, radius is scaled by maximum scalar from transform
142 // see https://github.com/CesiumGS/cesium-native/blob/fd20f5e272850dde6b58c74059e6de767fe25df6/Cesium3DTilesSelection/src/BoundingVolume.cpp#L33
143 const QgsVector3D center = transform.map( sphere.centerVector() );
144 const double uniformScale = std::max(
145 std::max(
146 std::sqrt( transform.constData()[0] * transform.constData()[0] + transform.constData()[1] * transform.constData()[1] + transform.constData()[2] * transform.constData()[2] ),
147 std::sqrt( transform.constData()[4] * transform.constData()[4] + transform.constData()[5] * transform.constData()[5] + transform.constData()[6] * transform.constData()[6] )
148 ),
149 std::sqrt( transform.constData()[8] * transform.constData()[8] + transform.constData()[9] * transform.constData()[9] + transform.constData()[10] * transform.constData()[10] )
150 );
151
152 return QgsSphere( center.x(), center.y(), center.z(), sphere.radius() * uniformScale );
153 }
154 return sphere;
155}
156
158{
159 struct b3dmHeader
160 {
161 unsigned char magic[4];
162 quint32 version;
163 quint32 byteLength;
164 quint32 featureTableJsonByteLength;
165 quint32 featureTableBinaryByteLength;
166 quint32 batchTableJsonByteLength;
167 quint32 batchTableBinaryByteLength;
168 };
169
171 if ( tileContent.size() < static_cast<int>( sizeof( b3dmHeader ) ) )
172 return res;
173
174 b3dmHeader hdr;
175 memcpy( &hdr, tileContent.constData(), sizeof( b3dmHeader ) );
176
177 const QString featureTableJson( tileContent.mid( sizeof( b3dmHeader ), hdr.featureTableJsonByteLength ) );
178 if ( !featureTableJson.isEmpty() )
179 {
180 try
181 {
182 const json featureTable = json::parse( featureTableJson.toStdString() );
183 if ( featureTable.contains( "RTC_CENTER" ) )
184 {
185 const auto &rtcCenterJson = featureTable["RTC_CENTER"];
186 if ( rtcCenterJson.is_array() && rtcCenterJson.size() == 3 )
187 {
188 res.rtcCenter.setX( rtcCenterJson[0].get<double>() );
189 res.rtcCenter.setY( rtcCenterJson[1].get<double>() );
190 res.rtcCenter.setZ( rtcCenterJson[2].get<double>() );
191 }
192 else
193 {
194 QgsDebugError( u"Invalid RTC_CENTER value"_s );
195 }
196 }
197 }
198 catch ( json::parse_error &ex )
199 {
200 QgsDebugError( u"Error parsing feature table JSON: %1"_s.arg( ex.what() ) );
201 }
202 }
203
204 res.gltf = tileContent.mid( sizeof( b3dmHeader ) + hdr.featureTableJsonByteLength + hdr.featureTableBinaryByteLength + hdr.batchTableJsonByteLength + hdr.batchTableBinaryByteLength );
205 return res;
206}
207
221static void computeEastNorthUpQuaternions( const QVector<QVector3D> &positions, const QgsVector3D &rtcCenter, const QgsMatrix4x4 &tileTransform, QVector<QQuaternion> &rotations )
222{
223 const int count = positions.size();
224 rotations.resize( count );
225
226 if ( count == 0 )
227 return;
228
229 const QgsCoordinateReferenceSystem ecefCrs( u"EPSG:4978"_s );
230 const QgsCoordinateReferenceSystem geodeticCrs( u"EPSG:4979"_s );
231 QgsCoordinateTransform ecefToGeodetic( ecefCrs, geodeticCrs, QgsCoordinateTransformContext() );
232 QgsCoordinateTransform geodeticToEcef( geodeticCrs, ecefCrs, QgsCoordinateTransformContext() );
233
234 constexpr double delta = 0.001; // degrees
235
236 // Batch transform: we need base points + east perturbation + north perturbation = 3*count points
237 const int totalPts = 3 * count;
238 QVector<double> gx( totalPts ), gy( totalPts ), gz( totalPts );
239
240 // First, convert tile-local positions to ECEF using the tile transform,
241 // then convert ECEF positions to geodetic
242 QVector<double> px( count ), py( count ), pz( count );
243 for ( int i = 0; i < count; ++i )
244 {
245 const QgsVector3D posLocal( static_cast<double>( positions[i].x() ) + rtcCenter.x(), static_cast<double>( positions[i].y() ) + rtcCenter.y(), static_cast<double>( positions[i].z() ) + rtcCenter.z() );
246 const QgsVector3D ecef = tileTransform.map( posLocal );
247 px[i] = ecef.x();
248 py[i] = ecef.y();
249 pz[i] = ecef.z();
250 }
251
252 try
253 {
254 ecefToGeodetic.transformCoords( count, px.data(), py.data(), pz.data() );
255 }
256 catch ( QgsCsException & )
257 {
258 // fallback to identity rotations
259 for ( int i = 0; i < count; ++i )
260 rotations[i] = QQuaternion();
261 return;
262 }
263
264 // px/py/pz are now geodetic (lon, lat, h)
265 // Build batch: base, east-perturbed, north-perturbed
266 for ( int i = 0; i < count; ++i )
267 {
268 // base point
269 gx[i] = px[i];
270 gy[i] = py[i];
271 gz[i] = pz[i];
272
273 // east-perturbed
274 gx[count + i] = px[i] + delta;
275 gy[count + i] = py[i];
276 gz[count + i] = pz[i];
277
278 // north-perturbed
279 gx[2 * count + i] = px[i];
280 gy[2 * count + i] = py[i] + delta;
281 gz[2 * count + i] = pz[i];
282 }
283
284 try
285 {
286 geodeticToEcef.transformCoords( totalPts, gx.data(), gy.data(), gz.data() );
287 }
288 catch ( QgsCsException & )
289 {
290 for ( int i = 0; i < count; ++i )
291 rotations[i] = QQuaternion();
292 return;
293 }
294
295 // gx/gy/gz are now ECEF for all 3*count points
296
297 // Extract the tile transform's 3×3 rotation part (column-major in QgsMatrix4x4).
298 // We need its transpose (= inverse for orthogonal matrices) to transform
299 // ECEF direction vectors back to tile-local space, because the instance
300 // rotation quaternion is applied in tile-local space, not ECEF.
301 const double *td = tileTransform.constData();
302 // Columns of the 3×3 rotation part (column-major: col0=[0,1,2], col1=[4,5,6], col2=[8,9,10])
303 const QVector3D tileCol0( static_cast<float>( td[0] ), static_cast<float>( td[1] ), static_cast<float>( td[2] ) );
304 const QVector3D tileCol1( static_cast<float>( td[4] ), static_cast<float>( td[5] ), static_cast<float>( td[6] ) );
305 const QVector3D tileCol2( static_cast<float>( td[8] ), static_cast<float>( td[9] ), static_cast<float>( td[10] ) );
306
307 // Normalize columns to handle any scale in the tile transform
308 const float len0 = tileCol0.length();
309 const float len1 = tileCol1.length();
310 const float len2 = tileCol2.length();
311 const bool hasTileRotation = len0 > 0 && len1 > 0 && len2 > 0 && !tileTransform.isIdentity();
312
313 for ( int i = 0; i < count; ++i )
314 {
315 const QVector3D base( static_cast<float>( gx[i] ), static_cast<float>( gy[i] ), static_cast<float>( gz[i] ) );
316 const QVector3D eastPt( static_cast<float>( gx[count + i] ), static_cast<float>( gy[count + i] ), static_cast<float>( gz[count + i] ) );
317 const QVector3D northPt( static_cast<float>( gx[2 * count + i] ), static_cast<float>( gy[2 * count + i] ), static_cast<float>( gz[2 * count + i] ) );
318
319 QVector3D east = ( eastPt - base ).normalized();
320 QVector3D north = ( northPt - base ).normalized();
321 QVector3D up = QVector3D::crossProduct( east, north ).normalized();
322
323 if ( hasTileRotation )
324 {
325 // Transform ECEF directions to tile-local space using R_tile^T
326 // (transpose of rotation = inverse for orthogonal matrices).
327 // R^T × v = (dot(col0,v)/|col0|², dot(col1,v)/|col1|², dot(col2,v)/|col2|²)
328 // With normalized columns: R^T × v = (dot(col0/|col0|, v), ...)
329 const QVector3D nc0 = tileCol0 / len0;
330 const QVector3D nc1 = tileCol1 / len1;
331 const QVector3D nc2 = tileCol2 / len2;
332
333 const QVector3D eastLocal( QVector3D::dotProduct( nc0, east ), QVector3D::dotProduct( nc1, east ), QVector3D::dotProduct( nc2, east ) );
334 const QVector3D upLocal( QVector3D::dotProduct( nc0, up ), QVector3D::dotProduct( nc1, up ), QVector3D::dotProduct( nc2, up ) );
335 east = eastLocal;
336 up = upLocal;
337 }
338
339 rotations[i] = QgsEllipsoidUtils::quaternionFromNormalUpRight( up, east );
340 }
341}
342
343
344// Helper: build the axis flip matrix from gltfUpAxis
345static QMatrix4x4 axisFlipMatrix( Qgis::Axis gltfUpAxis )
346{
347 QMatrix4x4 F;
348 switch ( gltfUpAxis )
349 {
350 case Qgis::Axis::Y:
351 // Y-up → Z-up: (x,y,z) → (x,-z,y)
352 F = QMatrix4x4( 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1 );
353 break;
354 case Qgis::Axis::X:
355 case Qgis::Axis::Z:
356 // identity (already Z-up or X-up not supported yet)
357 break;
358 }
359 return F;
360}
361
362
363// Helper: parse EXT_mesh_gpu_instancing from a glTF node
364static bool parseExtMeshGpuInstancing(
365 const tinygltf::Model &model, const tinygltf::Node &node, QVector<QVector3D> &translations, QVector<QQuaternion> &rotations, QVector<QVector3D> &scales, int &instanceCount
366)
367{
368 auto extIt = node.extensions.find( "EXT_mesh_gpu_instancing" );
369 if ( extIt == node.extensions.end() )
370 return false;
371
372 const tinygltf::Value &extValue = extIt->second;
373 if ( !extValue.IsObject() || !extValue.Has( "attributes" ) )
374 return false;
375
376 const tinygltf::Value &attributes = extValue.Get( "attributes" );
377 if ( !attributes.IsObject() )
378 return false;
379
380 instanceCount = 0;
381
382 // Helper lambda: read a VEC3 FLOAT accessor into a QVector<QVector3D>
383 auto readVec3Accessor = [&model, &instanceCount]( const tinygltf::Value &attributes, const std::string &name, QVector<QVector3D> &out, const QVector3D &defaultValue ) -> bool {
384 if ( attributes.Has( name ) )
385 {
386 int accessorIdx = attributes.Get( name ).GetNumberAsInt();
387 if ( accessorIdx < 0 || accessorIdx >= static_cast<int>( model.accessors.size() ) )
388 return false;
389
390 const tinygltf::Accessor &accessor = model.accessors[accessorIdx];
391 if ( accessor.type != TINYGLTF_TYPE_VEC3 || accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT )
392 return false;
393
394 const int count = static_cast<int>( accessor.count );
395 if ( instanceCount == 0 )
396 instanceCount = count;
397 else if ( instanceCount != count )
398 return false;
399
400 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
401 const tinygltf::Buffer &buf = model.buffers[bv.buffer];
402 const unsigned char *ptr = buf.data.data() + bv.byteOffset + accessor.byteOffset;
403 const int stride = bv.byteStride ? static_cast<int>( bv.byteStride ) : 3 * static_cast<int>( sizeof( float ) );
404
405 out.resize( count );
406 const unsigned char *row = ptr;
407 for ( int i = 0; i < count; ++i, row += stride )
408 {
409 const float *fptr = reinterpret_cast<const float *>( row );
410 out[i] = QVector3D( fptr[0], fptr[1], fptr[2] );
411 }
412 }
413 else
414 {
415 // use default
416 if ( instanceCount > 0 )
417 out.fill( defaultValue, instanceCount );
418 }
419 return true;
420 };
421
422 if ( !readVec3Accessor( attributes, "TRANSLATION", translations, QVector3D( 0, 0, 0 ) ) )
423 return false;
424
425 if ( !readVec3Accessor( attributes, "SCALE", scales, QVector3D( 1, 1, 1 ) ) )
426 return false;
427
428 // Read ROTATION: VEC4 FLOAT accessor → per-instance quaternions
429 if ( attributes.Has( "ROTATION" ) )
430 {
431 int accessorIdx = attributes.Get( "ROTATION" ).GetNumberAsInt();
432 if ( accessorIdx < 0 || accessorIdx >= static_cast<int>( model.accessors.size() ) )
433 return false;
434
435 const tinygltf::Accessor &accessor = model.accessors[accessorIdx];
436 if ( accessor.type != TINYGLTF_TYPE_VEC4 )
437 return false;
438
439 const int count = static_cast<int>( accessor.count );
440 if ( instanceCount == 0 )
441 instanceCount = count;
442 else if ( instanceCount != count )
443 return false;
444
445 const tinygltf::BufferView &bv = model.bufferViews[accessor.bufferView];
446 const tinygltf::Buffer &buf = model.buffers[bv.buffer];
447 const unsigned char *ptr = buf.data.data() + bv.byteOffset + accessor.byteOffset;
448
449 rotations.resize( count );
450
451 if ( accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT )
452 {
453 const int stride = bv.byteStride ? static_cast<int>( bv.byteStride ) : 4 * static_cast<int>( sizeof( float ) );
454 const unsigned char *row = ptr;
455 for ( int i = 0; i < count; ++i, row += stride )
456 {
457 const float *fptr = reinterpret_cast<const float *>( row );
458 // glTF quaternion: (x, y, z, w)
459 rotations[i] = QQuaternion( fptr[3], fptr[0], fptr[1], fptr[2] );
460 }
461 }
462 else if ( accessor.componentType == TINYGLTF_COMPONENT_TYPE_SHORT )
463 {
464 const int stride = bv.byteStride ? static_cast<int>( bv.byteStride ) : 4 * static_cast<int>( sizeof( short ) );
465 const unsigned char *row = ptr;
466 for ( int i = 0; i < count; ++i, row += stride )
467 {
468 const short *sptr = reinterpret_cast<const short *>( row );
469 // Normalized short: divide by 32767.0
470 rotations[i] = QQuaternion( static_cast<float>( sptr[3] ) / 32767.0f, static_cast<float>( sptr[0] ) / 32767.0f, static_cast<float>( sptr[1] ) / 32767.0f, static_cast<float>( sptr[2] ) / 32767.0f );
471 }
472 }
473 else if ( accessor.componentType == TINYGLTF_COMPONENT_TYPE_BYTE )
474 {
475 const int stride = bv.byteStride ? static_cast<int>( bv.byteStride ) : 4 * static_cast<int>( sizeof( char ) );
476 const unsigned char *row = ptr;
477 for ( int i = 0; i < count; ++i, row += stride )
478 {
479 const signed char *bptr = reinterpret_cast<const signed char *>( row );
480 // Normalized byte: divide by 127.0
481 rotations[i] = QQuaternion( static_cast<float>( bptr[3] ) / 127.0f, static_cast<float>( bptr[0] ) / 127.0f, static_cast<float>( bptr[1] ) / 127.0f, static_cast<float>( bptr[2] ) / 127.0f );
482 }
483 }
484 else
485 {
486 return false;
487 }
488 }
489 else
490 {
491 if ( instanceCount > 0 )
492 rotations.fill( QQuaternion(), instanceCount );
493 }
494
495 // Fill defaults if only some attributes were specified
496 if ( instanceCount > 0 )
497 {
498 if ( translations.isEmpty() )
499 translations.fill( QVector3D( 0, 0, 0 ), instanceCount );
500 if ( rotations.isEmpty() )
501 rotations.fill( QQuaternion(), instanceCount );
502 if ( scales.isEmpty() )
503 scales.fill( QVector3D( 1, 1, 1 ), instanceCount );
504 }
505
506 return instanceCount > 0;
507}
508
509
510// Helper: build a 4x4 matrix from translation, rotation, scale
511static QMatrix4x4 trsMatrix( const QVector3D &t, const QQuaternion &r, const QVector3D &s )
512{
513 QMatrix4x4 mat;
514 mat.translate( t );
515 mat.rotate( r );
516 mat.scale( s );
517 return mat;
518}
519
520
521QVector<QgsGltfUtils::InstancedPrimitive> QgsCesiumUtils::resolveInstancing(
522 const tinygltf::Model &model, const std::optional<TileI3dmData> &tileInstancing, Qgis::Axis gltfUpAxis, const QgsMatrix4x4 &tileTransform, const QgsVector3D &rtcCenter
523)
524{
525 QVector<QgsGltfUtils::InstancedPrimitive> result;
526
527 bool sceneOk = false;
528 const std::size_t sceneIndex = QgsGltfUtils::sourceSceneForModel( model, sceneOk );
529 if ( !sceneOk )
530 return result;
531
532 const tinygltf::Scene &scene = model.scenes[sceneIndex];
533 const QMatrix4x4 F = axisFlipMatrix( gltfUpAxis );
534
535 // Recursive node walker
536 std::function<void( int nodeIndex, const QMatrix4x4 &parentTransform )> walkNode;
537 walkNode = [&]( int nodeIndex, const QMatrix4x4 &parentTransform ) {
538 if ( nodeIndex < 0 || nodeIndex >= static_cast<int>( model.nodes.size() ) )
539 return;
540
541 const tinygltf::Node &node = model.nodes[nodeIndex];
542
543 // Compute accumulated local transform M for this node
544 std::unique_ptr<QMatrix4x4> localTransform = QgsGltfUtils::parseNodeTransform( node );
545 QMatrix4x4 M = parentTransform;
546 if ( localTransform )
547 M = parentTransform * *localTransform;
548
549 if ( node.mesh >= 0 )
550 {
551 const tinygltf::Mesh &mesh = model.meshes[node.mesh];
552
553 if ( tileInstancing.has_value() )
554 {
555 // i3dm path: every mesh node gets the same instances
556 // fullMatrix = T(pos_i) × R_i × S_i × F × M
557 const QMatrix4x4 FM = F * M;
558 QgsCesiumUtils::TileI3dmData inst = tileInstancing.value();
559
560 // Deferred EAST_NORTH_UP: compute ENU rotations now that the tile transform is available
561 if ( inst.eastNorthUp )
562 {
563 computeEastNorthUpQuaternions( inst.translations, rtcCenter, tileTransform, inst.rotations );
564 }
565
566 for ( int pIdx = 0; pIdx < static_cast<int>( mesh.primitives.size() ); ++pIdx )
567 {
568 QgsGltfUtils::InstancedPrimitive entry;
569 entry.meshIndex = node.mesh;
570 entry.primitiveIndex = pIdx;
571 entry.materialIndex = mesh.primitives[pIdx].material;
572 entry.instanceTransforms.resize( inst.instanceCount );
573
574 for ( int i = 0; i < inst.instanceCount; ++i )
575 {
576 entry.instanceTransforms[i] = trsMatrix( inst.translations[i], inst.rotations[i], inst.scales[i] ) * FM;
577 }
578
579 result.append( std::move( entry ) );
580 }
581 }
582 else
583 {
584 // Check for EXT_mesh_gpu_instancing on this node
585 QVector<QVector3D> translations;
586 QVector<QQuaternion> rotations;
587 QVector<QVector3D> scales;
588 int instanceCount = 0;
589
590 if ( parseExtMeshGpuInstancing( model, node, translations, rotations, scales, instanceCount ) )
591 {
592 // EXT path: fullMatrix = F × M × T_i × R_i × S_i
593 const QMatrix4x4 FM = F * M;
594
595 for ( int pIdx = 0; pIdx < static_cast<int>( mesh.primitives.size() ); ++pIdx )
596 {
597 QgsGltfUtils::InstancedPrimitive entry;
598 entry.meshIndex = node.mesh;
599 entry.primitiveIndex = pIdx;
600 entry.materialIndex = mesh.primitives[pIdx].material;
601 entry.instanceTransforms.resize( instanceCount );
602
603 for ( int i = 0; i < instanceCount; ++i )
604 {
605 entry.instanceTransforms[i] = FM * trsMatrix( translations[i], rotations[i], scales[i] );
606 }
607
608 result.append( std::move( entry ) );
609 }
610 }
611 // else: non-instanced node, skip — handled by existing code path
612 }
613 }
614
615 // Recurse to children
616 for ( int childIndex : node.children )
617 {
618 walkNode( childIndex, M );
619 }
620 };
621
622 for ( int nodeIndex : scene.nodes )
623 {
624 walkNode( nodeIndex, QMatrix4x4() );
625 }
626
627 return result;
628}
629
630
631static QgsCesiumUtils::TileContents extractGltfFromI3dm( const QByteArray &tileContent, const QString &baseUri )
632{
633 struct i3dmHeader
634 {
635 unsigned char magic[4];
636 quint32 version;
637 quint32 byteLength;
638 quint32 featureTableJsonByteLength;
639 quint32 featureTableBinaryByteLength;
640 quint32 batchTableJsonByteLength;
641 quint32 batchTableBinaryByteLength;
642 quint32 gltfFormat;
643 };
644
646
647 if ( tileContent.size() < static_cast<int>( sizeof( i3dmHeader ) ) )
648 return res;
649
650 i3dmHeader hdr;
651 memcpy( &hdr, tileContent.constData(), sizeof( i3dmHeader ) );
652
653 const int featureTableJsonOffset = sizeof( i3dmHeader );
654 const int featureTableBinaryOffset = featureTableJsonOffset + static_cast<int>( hdr.featureTableJsonByteLength );
655
656 // Parse feature table JSON
657 const QString featureTableJson( tileContent.mid( featureTableJsonOffset, hdr.featureTableJsonByteLength ) );
658 if ( featureTableJson.isEmpty() )
659 {
660 QgsDebugError( u"i3dm: empty feature table JSON"_s );
661 return res;
662 }
663
664 int instanceCount = 0;
665 bool eastNorthUp = false;
666 int positionByteOffset = -1;
667 int normalUpByteOffset = -1;
668 int normalRightByteOffset = -1;
669 int scaleByteOffset = -1;
670 int scaleNonUniformByteOffset = -1;
671
672 try
673 {
674 const json featureTable = json::parse( featureTableJson.toStdString() );
675
676 if ( !featureTable.contains( "INSTANCES_LENGTH" ) )
677 {
678 QgsDebugError( u"i3dm: INSTANCES_LENGTH not found in feature table"_s );
679 return res;
680 }
681 instanceCount = featureTable["INSTANCES_LENGTH"].get<int>();
682
683 if ( featureTable.contains( "RTC_CENTER" ) )
684 {
685 const auto &rtcCenterJson = featureTable["RTC_CENTER"];
686 if ( rtcCenterJson.is_array() && rtcCenterJson.size() == 3 )
687 {
688 res.rtcCenter.setX( rtcCenterJson[0].get<double>() );
689 res.rtcCenter.setY( rtcCenterJson[1].get<double>() );
690 res.rtcCenter.setZ( rtcCenterJson[2].get<double>() );
691 }
692 }
693
694 if ( featureTable.contains( "EAST_NORTH_UP" ) )
695 {
696 eastNorthUp = featureTable["EAST_NORTH_UP"].get<bool>();
697 }
698
699 // Get byte offsets for binary properties
700 if ( featureTable.contains( "POSITION" ) )
701 {
702 const auto &posJson = featureTable["POSITION"];
703 if ( posJson.is_object() && posJson.contains( "byteOffset" ) )
704 positionByteOffset = posJson["byteOffset"].get<int>();
705 }
706 if ( featureTable.contains( "NORMAL_UP" ) )
707 {
708 const auto &nuJson = featureTable["NORMAL_UP"];
709 if ( nuJson.is_object() && nuJson.contains( "byteOffset" ) )
710 normalUpByteOffset = nuJson["byteOffset"].get<int>();
711 }
712 if ( featureTable.contains( "NORMAL_RIGHT" ) )
713 {
714 const auto &nrJson = featureTable["NORMAL_RIGHT"];
715 if ( nrJson.is_object() && nrJson.contains( "byteOffset" ) )
716 normalRightByteOffset = nrJson["byteOffset"].get<int>();
717 }
718 if ( featureTable.contains( "SCALE" ) )
719 {
720 const auto &sJson = featureTable["SCALE"];
721 if ( sJson.is_object() && sJson.contains( "byteOffset" ) )
722 scaleByteOffset = sJson["byteOffset"].get<int>();
723 }
724 if ( featureTable.contains( "SCALE_NON_UNIFORM" ) )
725 {
726 const auto &snuJson = featureTable["SCALE_NON_UNIFORM"];
727 if ( snuJson.is_object() && snuJson.contains( "byteOffset" ) )
728 scaleNonUniformByteOffset = snuJson["byteOffset"].get<int>();
729 }
730 }
731 catch ( json::parse_error &ex )
732 {
733 QgsDebugError( u"i3dm: error parsing feature table JSON: %1"_s.arg( ex.what() ) );
734 return res;
735 }
736
737 if ( instanceCount <= 0 || positionByteOffset < 0 )
738 {
739 QgsDebugError( u"i3dm: invalid instance count or missing POSITION"_s );
740 return res;
741 }
742
743 const char *featureBinaryPtr = tileContent.constData() + featureTableBinaryOffset;
744 const int featureBinarySize = static_cast<int>( hdr.featureTableBinaryByteLength );
745
747 instancing.instanceCount = instanceCount;
748
749 // Read POSITION: instanceCount × float32[3]
750 {
751 const int requiredSize = positionByteOffset + instanceCount * 3 * static_cast<int>( sizeof( float ) );
752 if ( requiredSize > featureBinarySize )
753 {
754 QgsDebugError( u"i3dm: POSITION data exceeds feature table binary size"_s );
755 return res;
756 }
757 instancing.translations.resize( instanceCount );
758 const float *posPtr = reinterpret_cast<const float *>( featureBinaryPtr + positionByteOffset );
759 for ( int i = 0; i < instanceCount; ++i, posPtr += 3 )
760 {
761 instancing.translations[i] = QVector3D( posPtr[0], posPtr[1], posPtr[2] );
762 }
763 }
764
765 // Read rotations from NORMAL_UP + NORMAL_RIGHT, or EAST_NORTH_UP, or identity
766 if ( normalUpByteOffset >= 0 && normalRightByteOffset >= 0 )
767 {
768 const int requiredUp = normalUpByteOffset + instanceCount * 3 * static_cast<int>( sizeof( float ) );
769 const int requiredRight = normalRightByteOffset + instanceCount * 3 * static_cast<int>( sizeof( float ) );
770 if ( requiredUp > featureBinarySize || requiredRight > featureBinarySize )
771 {
772 QgsDebugError( u"i3dm: NORMAL_UP/NORMAL_RIGHT data exceeds feature table binary size"_s );
773 return res;
774 }
775 instancing.rotations.resize( instanceCount );
776 const float *upPtr = reinterpret_cast<const float *>( featureBinaryPtr + normalUpByteOffset );
777 const float *rightPtr = reinterpret_cast<const float *>( featureBinaryPtr + normalRightByteOffset );
778 for ( int i = 0; i < instanceCount; ++i, upPtr += 3, rightPtr += 3 )
779 {
780 const QVector3D normalUp( upPtr[0], upPtr[1], upPtr[2] );
781 const QVector3D normalRight( rightPtr[0], rightPtr[1], rightPtr[2] );
782 instancing.rotations[i] = QgsEllipsoidUtils::quaternionFromNormalUpRight( normalUp, normalRight );
783 }
784 }
785 else if ( eastNorthUp )
786 {
787 // Defer ENU computation: the tile transform (from tileset.json) is needed
788 // to convert tile-local positions to ECEF, but is not available at parse time.
789 // Store the flag so resolveInstancing() can compute ENU rotations later.
790 instancing.eastNorthUp = true;
791 instancing.rotations.fill( QQuaternion(), instanceCount );
792 }
793 else
794 {
795 instancing.rotations.fill( QQuaternion(), instanceCount );
796 }
797
798 // Read SCALE or SCALE_NON_UNIFORM
799 if ( scaleNonUniformByteOffset >= 0 )
800 {
801 const int required = scaleNonUniformByteOffset + instanceCount * 3 * static_cast<int>( sizeof( float ) );
802 if ( required > featureBinarySize )
803 {
804 QgsDebugError( u"i3dm: SCALE_NON_UNIFORM data exceeds feature table binary size"_s );
805 return res;
806 }
807 instancing.scales.resize( instanceCount );
808 const float *scalePtr = reinterpret_cast<const float *>( featureBinaryPtr + scaleNonUniformByteOffset );
809 for ( int i = 0; i < instanceCount; ++i, scalePtr += 3 )
810 {
811 instancing.scales[i] = QVector3D( scalePtr[0], scalePtr[1], scalePtr[2] );
812 }
813 }
814 else if ( scaleByteOffset >= 0 )
815 {
816 const int required = scaleByteOffset + instanceCount * static_cast<int>( sizeof( float ) );
817 if ( required > featureBinarySize )
818 {
819 QgsDebugError( u"i3dm: SCALE data exceeds feature table binary size"_s );
820 return res;
821 }
822 instancing.scales.resize( instanceCount );
823 const float *scalePtr = reinterpret_cast<const float *>( featureBinaryPtr + scaleByteOffset );
824 for ( int i = 0; i < instanceCount; ++i )
825 {
826 const float s = scalePtr[i];
827 instancing.scales[i] = QVector3D( s, s, s );
828 }
829 }
830 else
831 {
832 instancing.scales.fill( QVector3D( 1, 1, 1 ), instanceCount );
833 }
834
835 res.instancing = std::move( instancing );
836
837 // Extract embedded glTF body
838 const int gltfOffset = featureTableBinaryOffset
839 + static_cast<int>( hdr.featureTableBinaryByteLength )
840 + static_cast<int>( hdr.batchTableJsonByteLength )
841 + static_cast<int>( hdr.batchTableBinaryByteLength );
842 QByteArray gltfContent = tileContent.mid( gltfOffset );
843
844 if ( hdr.gltfFormat == 1 ) // embedded
845 {
846 res.gltf = gltfContent;
847 }
848 else if ( hdr.gltfFormat == 0 ) // gltf is actually only a URI, not real content
849 {
850 QString gltfUri = QString::fromUtf8( gltfContent );
851 // URI may be relative to the .i3dm location
852 QUrl url = QUrl( baseUri ).resolved( gltfUri );
853
854 if ( url.scheme().startsWith( "http" ) )
855 {
856 QNetworkRequest request = QNetworkRequest( url );
857 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
858 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
859 QgsBlockingNetworkRequest networkRequest;
860 // TODO: setup auth, setup headers
861 if ( networkRequest.get( request ) != QgsBlockingNetworkRequest::NoError )
862 {
863 QgsDebugError( u"i3dm: Failed to download GLTF: %1"_s.arg( url.toString() ) );
864 }
865 else
866 {
867 const QgsNetworkReplyContent content = networkRequest.reply();
868 res.gltf = content.content();
869 }
870 }
871 else if ( url.isLocalFile() )
872 {
873 QString localFilePath = url.toLocalFile();
874 if ( QFile::exists( localFilePath ) )
875 {
876 QFile f( localFilePath );
877 if ( f.open( QIODevice::ReadOnly ) )
878 {
879 res.gltf = f.readAll();
880 }
881 }
882 else
883 {
884 QgsDebugError( u"i3dm: Failed to open local GLTF: %1"_s.arg( url.toString() ) );
885 }
886 }
887 }
888 else
889 {
890 QgsDebugError( u"i3dm: Unknown gltf format: %1"_s.arg( hdr.gltfFormat ) );
891 }
892
893 return res;
894}
895
896static QVector<QgsCesiumUtils::TileContents> extractGltfFromCmpt( const QByteArray &tileContent, int depth = 0 )
897{
898 struct cmptHeader
899 {
900 unsigned char magic[4];
901 quint32 version;
902 quint32 byteLength;
903 quint32 tilesLength;
904 };
905
906 QVector<QgsCesiumUtils::TileContents> result;
907
908 if ( depth > 10 )
909 {
910 // avoid infinite recursion with badly formed tiles
911 QgsDebugError( u"cmpt recursion depth exceeded"_s );
912 return result;
913 }
914
915 if ( tileContent.size() < static_cast<int>( sizeof( cmptHeader ) ) )
916 return result;
917
918 cmptHeader hdr;
919 memcpy( &hdr, tileContent.constData(), sizeof( cmptHeader ) );
920
921 if ( hdr.version != 1 )
922 {
923 QgsDebugError( u"Unsupported cmpt version %1"_s.arg( hdr.version ) );
924 return result;
925 }
926
927 if ( static_cast<quint32>( tileContent.size() ) < hdr.byteLength )
928 return result;
929
930 int offset = static_cast<int>( sizeof( cmptHeader ) );
931 for ( quint32 i = 0; i < hdr.tilesLength; ++i )
932 {
933 // all inner tiles have the following header: magic (4 bytes), version (uint32), byteLength (uint32)
934 const quint32 innerByteLength = *reinterpret_cast<const quint32 *>( tileContent.constData() + offset + 8 );
935
936 if ( innerByteLength < 12 || offset + static_cast<int>( innerByteLength ) > static_cast<int>( hdr.byteLength ) )
937 {
938 QgsDebugError( u"cmpt with bad inner tile (at index %1)"_s.arg( i ) );
939 break;
940 }
941
942 const QByteArray innerTile = tileContent.mid( offset, innerByteLength );
943
944 if ( innerTile.startsWith( QByteArray( "cmpt" ) ) )
945 {
946 result.append( extractGltfFromCmpt( innerTile, depth + 1 ) );
947 }
948 else
949 {
950 result.append( QgsCesiumUtils::extractTileContent( innerTile ) );
951 }
952
953 offset += static_cast<int>( innerByteLength );
954 }
955
956 return result;
957}
958
959
961{
962 TileContents res;
963 if ( tileContent.startsWith( QByteArray( "b3dm" ) ) )
964 {
965 const B3DMContents b3dmContents = QgsCesiumUtils::extractGltfFromB3dm( tileContent );
966 res.gltf = b3dmContents.gltf;
967 res.rtcCenter = b3dmContents.rtcCenter;
968 return res;
969 }
970 else if ( tileContent.startsWith( QByteArray( "glTF" ) ) )
971 {
972 res.gltf = tileContent;
973 return res;
974 }
975 else
976 {
977 // unsupported tile content type
978 return res;
979 }
980}
981
982QVector<QgsCesiumUtils::TileContents> QgsCesiumUtils::extractTileContent( const QByteArray &tileContent, const QString &baseUri )
983{
984 QVector<TileContents> result;
985 if ( tileContent.startsWith( QByteArray( "b3dm" ) ) )
986 {
987 const B3DMContents b3dmContents = QgsCesiumUtils::extractGltfFromB3dm( tileContent );
988 TileContents contents;
989 contents.gltf = b3dmContents.gltf;
990 contents.rtcCenter = b3dmContents.rtcCenter;
991 result.append( contents );
992 }
993 else if ( tileContent.startsWith( QByteArray( "i3dm" ) ) )
994 {
995 TileContents contents = extractGltfFromI3dm( tileContent, baseUri );
996 result.append( contents );
997 }
998 else if ( tileContent.startsWith( QByteArray( "glTF" ) ) )
999 {
1000 TileContents contents;
1001 contents.gltf = tileContent;
1002 result.append( contents );
1003 }
1004 else if ( tileContent.startsWith( QByteArray( "cmpt" ) ) )
1005 {
1006 result = extractGltfFromCmpt( tileContent );
1007 }
1008 else
1009 {
1010 QgsDebugError( u"extractGltfFromTileContent: unknown tile format, size=%1, magic=%2"_s.arg( tileContent.size() ).arg( QString::fromLatin1( tileContent.left( 4 ) ) ) );
1011 }
1012 return result;
1013}
1014
1016{
1017 if ( region.width() > 20 || region.height() > 20 )
1018 {
1019 // treat very large regions as global -- these will not transform correctly to EPSG:4978
1021 }
1022
1023 // Transform the 8 corners of the region from EPSG:4979 to EPSG:4978
1024 QVector< QgsVector3D > corners = region.corners();
1025 QVector< double > x;
1026 x.reserve( 8 );
1027 QVector< double > y;
1028 y.reserve( 8 );
1029 QVector< double > z;
1030 z.reserve( 8 );
1031 for ( int i = 0; i < 8; ++i )
1032 {
1033 const QgsVector3D &corner = corners[i];
1034 x.append( corner.x() );
1035 y.append( corner.y() );
1036 z.append( corner.z() );
1037 }
1038 QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( u"EPSG:4979"_s ), QgsCoordinateReferenceSystem( u"EPSG:4978"_s ), transformContext );
1040 try
1041 {
1042 ct.transformInPlace( x, y, z );
1043 }
1044 catch ( QgsCsException & )
1045 {
1046 QgsDebugError( u"Cannot transform region bounding volume"_s );
1047 }
1048
1049 const auto minMaxX = std::minmax_element( x.constBegin(), x.constEnd() );
1050 const auto minMaxY = std::minmax_element( y.constBegin(), y.constEnd() );
1051 const auto minMaxZ = std::minmax_element( z.constBegin(), z.constEnd() );
1052 // note that matrix transforms are NOT applied to region bounding volumes!
1053 return QgsTiledSceneBoundingVolume( QgsOrientedBox3D::fromBox3D( QgsBox3D( *minMaxX.first, *minMaxY.first, *minMaxZ.first, *minMaxX.second, *minMaxY.second, *minMaxZ.second ) ) );
1054}
1055
1056QString QgsCesiumUtils::appendQueryFromBaseUrl( const QString &contentUri, const QUrl &baseUrl )
1057{
1058 // This is to support a case seen with Google's tiles. Root URL is something like this:
1059 // https://tile.googleapis.com/.../root.json?key=123
1060 // The returned JSON contains relative links with "session" (e.g. "/.../abc.json?session=456")
1061 // When fetching such abc.json, we have to include also "key" from the original URL!
1062 // Then the content of abc.json contains relative links (e.g. "/.../xyz.glb") and we
1063 // need to add both "key" and "session" (otherwise requests fail).
1064
1065 QUrlQuery contentQuery( QUrl( contentUri ).query() );
1066 const QList<QPair<QString, QString>> baseUrlQueryItems = QUrlQuery( baseUrl.query() ).queryItems();
1067 for ( const QPair<QString, QString> &kv : baseUrlQueryItems )
1068 {
1069 contentQuery.addQueryItem( kv.first, kv.second );
1070 }
1071 QUrl newContentUrl( contentUri );
1072 newContentUrl.setQuery( contentQuery );
1073 return newContentUrl.toString();
1074}
Axis
Cartesian axes.
Definition qgis.h:2658
@ X
X-axis.
Definition qgis.h:2659
@ Z
Z-axis.
Definition qgis.h:2661
@ Y
Y-axis.
Definition qgis.h:2660
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
QVector< QgsVector3D > corners() const
Returns an array of all box corners as 3D vectors.
Definition qgsbox3d.cpp:357
double width() const
Returns the width of the box.
Definition qgsbox3d.h:287
double height() const
Returns the height of the box.
Definition qgsbox3d.h:294
static QgsSphere parseSphere(const json &sphere)
Parses a sphere object from a Cesium JSON document.
static B3DMContents extractGltfFromB3dm(const QByteArray &tileContent)
Extracts GLTF binary data and other contents from the legacy b3dm (Batched 3D Model) tile format.
static QString appendQueryFromBaseUrl(const QString &contentUri, const QUrl &baseUrl)
Copies any query items from the base URL to the content URI - to replicate undocumented Cesium JS beh...
static QgsOrientedBox3D parseBox(const json &box)
Parses a box object from a Cesium JSON document to an oriented bounding box.
static QVector< QgsGltfUtils::InstancedPrimitive > resolveInstancing(const tinygltf::Model &model, const std::optional< TileI3dmData > &tileInstancing, Qgis::Axis gltfUpAxis, const QgsMatrix4x4 &tileTransform, const QgsVector3D &rtcCenter)
Resolves instancing from either i3dm data or EXT_mesh_gpu_instancing.
static QgsTiledSceneBoundingVolume boundingVolumeFromRegion(const QgsBox3D &region, const QgsCoordinateTransformContext &transformContext)
Calculates oriented bounding box in EPSG:4978 from "region" defined with min/max lat/lon coordinates ...
static QgsBox3D parseRegion(const json &region)
Parses a region object from a Cesium JSON object to a 3D box.
static QgsSphere transformSphere(const QgsSphere &sphere, const QgsMatrix4x4 &transform)
Applies a transform to a sphere.
static QVector< QgsCesiumUtils::TileContents > extractTileContent(const QByteArray &tileContent, const QString &baseUri=QString())
Parses tile content and returns a list of TileContents.
static Q_DECL_DEPRECATED TileContents extractGltfFromTileContent(const QByteArray &tileContent)
Parses tile content.
Represents a coordinate reference system (CRS).
Contains information about the context in which a coordinate transform is executed.
Handles coordinate transforms between two coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
void transformInPlace(double &x, double &y, double &z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms an array of x, y and z double coordinates in place, from the source CRS to the destination...
Custom exception class for Coordinate Reference System related exceptions.
static QQuaternion quaternionFromNormalUpRight(const QVector3D &normalUp, const QVector3D &normalRight)
Builds a rotation quaternion from an "up" direction and a "right" direction, with the remaining basis...
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
A simple 4x4 matrix implementation useful for transformation in 3D space.
bool isIdentity() const
Returns whether this matrix is an identity matrix.
QgsVector3D map(const QgsVector3D &vector) const
Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,...
const double * constData() const
Returns pointer to the matrix data (stored in column-major order).
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
Represents a oriented (rotated) box in 3 dimensions.
static QgsOrientedBox3D fromBox3D(const QgsBox3D &box)
Constructs an oriented box from an axis-aligned bounding box.
A spherical geometry object.
Definition qgssphere.h:46
QgsVector3D centerVector() const
Returns the vector to the center of the sphere.
Definition qgssphere.cpp:47
double radius() const
Returns the radius of the sphere.
Definition qgssphere.h:144
Represents a bounding volume for a tiled scene.
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
void setZ(double z)
Sets Z coordinate.
Definition qgsvector3d.h:80
double x() const
Returns X coordinate.
Definition qgsvector3d.h:58
void setX(double x)
Sets X coordinate.
Definition qgsvector3d.h:68
void setY(double y)
Sets Y coordinate.
Definition qgsvector3d.h:74
bool ANALYSIS_EXPORT normalRight(Vector3D *v1, Vector3D *result, double length)
Assigns the vector 'result', which is normal to the vector 'v1', on the right side of v1 and has leng...
#define QgsDebugError(str)
Definition qgslogger.h:71
Encapsulates the contents of a B3DM file.
QByteArray gltf
GLTF binary content.
QgsVector3D rtcCenter
Optional RTC center.
Encapsulates the contents of a 3D tile.
QgsVector3D rtcCenter
Center position of relative-to-center coordinates (when used).
QByteArray gltf
GLTF binary content.
std::optional< TileI3dmData > instancing
Optional instancing data, populated for i3dm tiles.
Raw per-instance data parsed from an i3dm feature table of a single tile.
QVector< QVector3D > translations
ECEF-relative positions (Z-up), relative to RTC_CENTER.
QVector< QVector3D > scales
Per-axis scale - (1,1,1) if unspecified.
int instanceCount
Number of instances.
bool eastNorthUp
Whether EAST_NORTH_UP rotations should be computed (deferred until tile transform is available).
QVector< QQuaternion > rotations
Quaternion (x,y,z,w) - identity if unspecified.