QGIS API Documentation 4.1.0-Master (64dc32379c2)
Loading...
Searching...
No Matches
qgscesiumimplicittiling.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscesiumimplicittiling.cpp
3 ---------------------------
4 begin : March 2026
5 copyright : (C) 2026 by Martin Dobias
6 email : wonder dot sk at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include "qgscesiumutils.h"
21#include "qgslogger.h"
22#include "qgstiledscenenode.h"
23#include "qgstiledscenetile.h"
24
25#include <QString>
26
27using namespace Qt::StringLiterals;
28
29int QgsCesiumImplicitTiling::subtreeTileCount( int levels )
30{
31 // Total tiles in a quadtree is 1 + 4 + 16 + 64 + ... which is
32 // a geometric series, with a bit of math we get closed form: (4^levels - 1) / 3
33 return ( static_cast<int>( std::pow( 4, levels ) ) - 1 ) / 3;
34}
35
36int QgsCesiumImplicitTiling::mortonIndex( int x, int y )
37{
38 // this interleaves 16 bits of X and Y to a single 32-bit value
39 // (bits from X end up at bit positions 0,2,4,... and from Y end
40 // up at bit positions 1,3,5,...)
41 int morton = 0;
42 for ( int i = 0; i < 16; ++i )
43 {
44 morton |= ( ( x & ( 1 << i ) ) << i ) | ( ( y & ( 1 << i ) ) << ( i + 1 ) );
45 }
46 return morton;
47}
48
49int QgsCesiumImplicitTiling::subtreeBitIndex( int localLevel, int localX, int localY )
50{
51 int offsetForLevel = subtreeTileCount( localLevel );
52 return offsetForLevel + mortonIndex( localX, localY );
53}
54
55QString QgsCesiumImplicitTiling::expandTemplateUri( const QString &templateUri, const QUrl &baseUrl, const TileCoordinate &coord )
56{
57 QString expanded = templateUri;
58 expanded.replace( "{level}"_L1, QString::number( coord.level ) );
59 expanded.replace( "{x}"_L1, QString::number( coord.x ) );
60 expanded.replace( "{y}"_L1, QString::number( coord.y ) );
61
62 const QString resolved = baseUrl.resolved( QUrl( expanded ) ).toString();
63 if ( baseUrl.hasQuery() && QUrl( expanded ).isRelative() )
64 return QgsCesiumUtils::appendQueryFromBaseUrl( resolved, baseUrl );
65 return resolved;
66}
67
68
69QgsBox3D QgsCesiumImplicitTiling::computeImplicitRegionBoundingVolume( const QgsBox3D &rootRegion, const TileCoordinate &coord )
70{
71 const double divisor = std::pow( 2.0, coord.level );
72 const double tileWidth = rootRegion.width() / divisor;
73 const double tileHeight = rootRegion.height() / divisor;
74
75 const double xMin = rootRegion.xMinimum() + coord.x * tileWidth;
76 const double yMin = rootRegion.yMinimum() + coord.y * tileHeight;
77
78 return QgsBox3D( xMin, yMin, rootRegion.zMinimum(), xMin + tileWidth, yMin + tileHeight, rootRegion.zMaximum() );
79}
80
81
82QgsTiledSceneBoundingVolume QgsCesiumImplicitTiling::computeImplicitBoundingVolume( const QgsTiledSceneBoundingVolume &rootVolume, const TileCoordinate &coord )
83{
84 const QgsOrientedBox3D &rootBox = rootVolume.box();
85 if ( rootBox.isNull() )
86 return rootVolume;
87
88 const double *halfAxes = rootBox.halfAxes();
89 const double divisor = std::pow( 2.0, coord.level );
90
91 // Half-axes vectors (rows of the 3x3 matrix)
92 // halfAxes[0..2] = first half-axis (u)
93 // halfAxes[3..5] = second half-axis (v)
94 // halfAxes[6..8] = third half-axis (w) — unchanged for quadtree
95
96 // Child half-axes: u and v are divided by 2^level, w stays the same
97 const double childHalfAxes[9]
98 = { halfAxes[0] / divisor, halfAxes[1] / divisor, halfAxes[2] / divisor, halfAxes[3] / divisor, halfAxes[4] / divisor, halfAxes[5] / divisor, halfAxes[6], halfAxes[7], halfAxes[8] };
99
100 // child tile's center relative to the tiling grid (with range [0,0] to [1,1])
101 const double dx = ( coord.x + 0.5 ) / divisor;
102 const double dy = ( coord.y + 0.5 ) / divisor;
103
104 // turn the range from [0,1]-[1,1] to [-1,-1]-[1,1] so that we can use it
105 // with half-axes in the next step
106 const double fx = dx * 2 - 1;
107 const double fy = dy * 2 - 1;
108
109 const double childCenter[3] = { rootBox.centerX() + fx * halfAxes[0] + fy * halfAxes[3], rootBox.centerY() + fx * halfAxes[1] + fy * halfAxes[4], rootBox.centerZ() + fx * halfAxes[2] + fy * halfAxes[5] };
110
111 return QgsTiledSceneBoundingVolume( QgsOrientedBox3D(
112 QList<double>( { childCenter[0], childCenter[1], childCenter[2] } ),
113 QList<double>( { childHalfAxes[0], childHalfAxes[1], childHalfAxes[2], childHalfAxes[3], childHalfAxes[4], childHalfAxes[5], childHalfAxes[6], childHalfAxes[7], childHalfAxes[8] } )
114 ) );
115}
116
117
119{
120 Subtree result;
121
122 if ( data.isEmpty() )
123 return result;
124
125 // Subtree binary format:
126 // Header (24 bytes): magic "subt" (4), version (4), jsonByteLength (8), binaryByteLength (8)
127 if ( data.size() < 24 )
128 {
129 QgsDebugError( u"Subtree data too short"_s );
130 return result;
131 }
132
133 const char *ptr = data.constData();
134 if ( memcmp( ptr, "subt", 4 ) != 0 )
135 {
136 // Maybe it's a JSON subtree (the spec allows JSON format too)
137 try
138 {
139 const auto subtreeJson = json::parse( data.toStdString() );
140 // TODO: handle JSON-only subtree format if needed
141 QgsDebugError( u"JSON subtree format not yet supported"_s );
142 return result;
143 }
144 catch ( json::parse_error & )
145 {
146 QgsDebugError( u"Invalid subtree data (not binary or JSON)"_s );
147 return result;
148 }
149 }
150
151 // Parse header
152 quint64 jsonByteLength = 0;
153 quint64 binaryByteLength = 0;
154 memcpy( &jsonByteLength, ptr + 8, 8 );
155 memcpy( &binaryByteLength, ptr + 16, 8 );
156
157 const int headerSize = 24;
158 if ( static_cast<quint64>( data.size() ) < headerSize + jsonByteLength )
159 {
160 QgsDebugError( u"Subtree data truncated"_s );
161 return result;
162 }
163
164 // Parse JSON chunk
165 const QByteArray jsonChunk = data.mid( headerSize, static_cast<int>( jsonByteLength ) );
166 json subtreeJson;
167 try
168 {
169 subtreeJson = json::parse( jsonChunk.toStdString() );
170 }
171 catch ( json::parse_error & )
172 {
173 QgsDebugError( u"Cannot parse subtree JSON chunk"_s );
174 return result;
175 }
176
177 // Binary chunk starts after JSON chunk (8-byte aligned)
178 const quint64 binaryStart = headerSize + ( ( jsonByteLength + 7 ) & ~static_cast<quint64>( 7 ) );
179 const QByteArray binaryChunk = ( binaryByteLength > 0 && static_cast<quint64>( data.size() ) >= binaryStart + binaryByteLength )
180 ? data.mid( static_cast<int>( binaryStart ), static_cast<int>( binaryByteLength ) )
181 : QByteArray();
182
183 // Parse bufferViews
184 std::vector<QPair<int, int>> bufferViews; // (byteOffset, byteLength)
185 if ( subtreeJson.contains( "bufferViews" ) )
186 {
187 for ( const auto &bv : subtreeJson["bufferViews"] )
188 {
189 const int byteOffset = bv.value( "byteOffset", 0 );
190 const int byteLength = bv["byteLength"].get<int>();
191 bufferViews.push_back( { byteOffset, byteLength } );
192 }
193 }
194
195 // Helper to parse an availability object into a QBitArray
196 auto parseAvailability = [&]( const json &availJson, int bitCount ) -> QBitArray {
197 QBitArray bits( bitCount, false );
198 if ( availJson.contains( "constant" ) )
199 {
200 const int constant = availJson["constant"].get<int>();
201 if ( constant == 1 )
202 bits.fill( true );
203 // constant == 0 means all false (already initialized)
204 }
205 else if ( availJson.contains( "bitstream" ) )
206 {
207 const int bitstreamIdx = availJson["bitstream"].get<int>();
208 if ( bitstreamIdx >= 0 && bitstreamIdx < static_cast<int>( bufferViews.size() ) )
209 {
210 const auto &[byteOffset, byteLength] = bufferViews[bitstreamIdx];
211 if ( byteOffset + byteLength <= binaryChunk.size() )
212 {
213 const unsigned char *bitstreamData = reinterpret_cast<const unsigned char *>( binaryChunk.constData() + byteOffset );
214 for ( int i = 0; i < bitCount && ( i / 8 ) < byteLength; ++i )
215 {
216 if ( bitstreamData[i / 8] & ( 1 << ( i % 8 ) ) )
217 bits.setBit( i );
218 }
219 }
220 }
221 }
222 return bits;
223 };
224
225 // Total tile count in subtree: (4^S - 1) / 3
226 const int totalTiles = subtreeTileCount( tilingData.subtreeLevels );
227
228 // Child subtree count: 4^S (nodes at the bottom level)
229 const int childSubtreeCount = static_cast<int>( std::pow( 4, tilingData.subtreeLevels ) );
230
231 if ( subtreeJson.contains( "tileAvailability" ) )
232 result.tileAvailability = parseAvailability( subtreeJson["tileAvailability"], totalTiles );
233 else
234 result.tileAvailability = QBitArray( totalTiles, true );
235
236 if ( subtreeJson.contains( "contentAvailability" ) )
237 {
238 // contentAvailability can be an array (one per content) or a single object
239 const auto &contentAvail = subtreeJson["contentAvailability"];
240 if ( contentAvail.is_array() && !contentAvail.empty() )
241 result.contentAvailability = parseAvailability( contentAvail[0], totalTiles ); // TODO: if there are multiple contents - multiple arrays
242 else if ( contentAvail.is_object() )
243 result.contentAvailability = parseAvailability( contentAvail, totalTiles );
244 else
245 result.contentAvailability = QBitArray( totalTiles, false );
246 }
247 else
248 {
249 result.contentAvailability = QBitArray( totalTiles, false );
250 }
251
252 if ( subtreeJson.contains( "childSubtreeAvailability" ) )
253 result.childSubtreeAvailability = parseAvailability( subtreeJson["childSubtreeAvailability"], childSubtreeCount );
254 else
255 result.childSubtreeAvailability = QBitArray( childSubtreeCount, false );
256
257 return result;
258}
259
260
262{
263 const int subtreeRootLevel = ( coord.level / subtreeLevels ) * subtreeLevels;
264 const int localLevel = coord.level - subtreeRootLevel;
265 const int subtreeRootX = coord.x >> localLevel;
266 const int subtreeRootY = coord.y >> localLevel;
267 return QgsCesiumImplicitTiling::TileCoordinate { subtreeRootLevel, subtreeRootX, subtreeRootY };
268}
269
270
271QMap<QgsCesiumImplicitTiling::TileCoordinate, QgsTiledSceneNode *> QgsCesiumImplicitTiling::createImplicitTilingChildren(
272 QgsTiledSceneNode *node, const TileCoordinate &coord, Root &tilingData, const TileCoordinate &subtreeCoord, QgsCoordinateTransformContext &transformContext, long long &nextTileId
273)
274{
275 QMap<TileCoordinate, QgsTiledSceneNode *> children;
276 const Subtree &subtree = tilingData.subtreeCache[subtreeCoord];
277
278 // Generate up to 4 children
279 const int childLevel = coord.level + 1;
280 for ( int dy = 0; dy < 2; ++dy )
281 {
282 for ( int dx = 0; dx < 2; ++dx )
283 {
284 const int childX = 2 * coord.x + dx;
285 const int childY = 2 * coord.y + dy;
286 const int childLocalLevel = childLevel - subtreeCoord.level;
287 const TileCoordinate childCoord { childLevel, childX, childY };
288
289 bool childAvail = false;
290 bool childHasContent = false;
291
292 if ( childLocalLevel < tilingData.subtreeLevels )
293 {
294 const int bitIdx = subtreeBitIndex( childLocalLevel, childX - ( subtreeCoord.x << childLocalLevel ), childY - ( subtreeCoord.y << childLocalLevel ) );
295 if ( bitIdx < subtree.tileAvailability.size() )
296 childAvail = subtree.tileAvailability.testBit( bitIdx );
297 if ( bitIdx < subtree.contentAvailability.size() )
298 childHasContent = subtree.contentAvailability.testBit( bitIdx );
299 }
300 else
301 {
302 const int childSubtreeIdx = mortonIndex( childX - ( subtreeCoord.x << childLocalLevel ), childY - ( subtreeCoord.y << childLocalLevel ) );
303 if ( childSubtreeIdx < subtree.childSubtreeAvailability.size() )
304 {
305 childAvail = subtree.childSubtreeAvailability.testBit( childSubtreeIdx );
306 // Don't assume content exists — the child subtree root's content
307 // availability is in its own subtree, not in the parent's.
308 // Content will be resolved when the child's subtree is fetched.
309 childHasContent = false;
310 }
311 }
312
313 if ( !childAvail )
314 continue;
315
316 auto childTile = std::make_unique<QgsTiledSceneTile>( nextTileId++ );
317 childTile->setGeometricError( tilingData.rootGeometricError / std::pow( 2.0, childLevel ) );
318 childTile->setRefinementProcess( tilingData.refinementProcess );
319
320 // Compute the child bounding volume. For region-based tilesets, subdivide in lat/lon
321 // space and convert the sub-region to an oriented box only at the last step.
322 if ( tilingData.rootRegion.has_value() )
323 {
324 const QgsBox3D childRegion = computeImplicitRegionBoundingVolume( *tilingData.rootRegion, childCoord );
325 childTile->setBoundingVolume( QgsCesiumUtils::boundingVolumeFromRegion( childRegion, transformContext ) );
326 }
327 else
328 {
329 childTile->setBoundingVolume( computeImplicitBoundingVolume( tilingData.rootBoundingVolume, childCoord ) );
330 }
331 childTile->setBaseUrl( tilingData.baseUrl );
332 childTile->setMetadata( {
333 { u"gltfUpAxis"_s, static_cast<int>( tilingData.gltfUpAxis ) },
334 { u"contentFormat"_s, u"cesiumtiles"_s },
335 } );
336
337 if ( tilingData.rootTransform.has_value() )
338 childTile->setTransform( *tilingData.rootTransform );
339
340 if ( childHasContent && !tilingData.contentUriTemplate.isEmpty() )
341 {
342 const QString contentUri = expandTemplateUri( tilingData.contentUriTemplate, tilingData.baseUrl, childCoord );
343 childTile->setResources( { { u"content"_s, contentUri } } );
344 }
345
346 auto childNode = std::make_unique<QgsTiledSceneNode>( childTile.release() );
347 children.insert( childCoord, childNode.get() );
348 node->addChild( childNode.release() );
349 }
350 }
351 return children;
352}
353
354
356{
357 if ( coord.level + 1 >= tilingData.availableLevels )
358 return Qgis::TileChildrenAvailability::NoChildren; // outside of our zoom levels
359
360 // Check whether the subtree covering this tile has been fetched
361 const TileCoordinate subtreeCoord = tileCoordinateToParentSubtree( coord, tilingData.subtreeLevels );
362 const auto subtreeCacheIt = tilingData.subtreeCache.constFind( subtreeCoord );
363 if ( subtreeCacheIt == tilingData.subtreeCache.constEnd() )
364 {
365 // Subtree not yet fetched — must fetch to determine children
367 }
368
369 const int localLevel = coord.level - subtreeCoord.level;
370 if ( localLevel + 1 < tilingData.subtreeLevels )
371 {
372 // Children would be within this subtree; since none were created during subtree
373 // population, they don't exist
375 }
376
377 // Tile is at the deepest level of its subtree — check childSubtreeAvailability
378 const Subtree &subtree = subtreeCacheIt.value();
379 const int childLocalLevel = localLevel + 1;
380 for ( int dy = 0; dy < 2; ++dy )
381 {
382 for ( int dx = 0; dx < 2; ++dx )
383 {
384 const int childX = 2 * coord.x + dx;
385 const int childY = 2 * coord.y + dy;
386 const int childSubtreeIdx = mortonIndex( childX - ( subtreeCoord.x << childLocalLevel ), childY - ( subtreeCoord.y << childLocalLevel ) );
387 if ( childSubtreeIdx < subtree.childSubtreeAvailability.size() && subtree.childSubtreeAvailability.testBit( childSubtreeIdx ) )
389 }
390 }
392}
393
394bool QgsCesiumImplicitTiling::parseImplicitTiling( const json &json, QgsTiledSceneNode *newNode, const QUrl &baseUrl, Qgis::Axis gltfUpAxis, Root &tilingData )
395{
396 const auto &implicit = json["implicitTiling"];
397 const std::string scheme = implicit.value( "subdivisionScheme", "" );
398 if ( scheme == "QUADTREE" )
399 {
400 if ( !implicit.contains( "availableLevels" ) || !implicit.contains( "subtreeLevels" ) )
401 {
402 QgsDebugError( u"Implicit tiling is missing required availableLevels or subtreeLevels"_s );
403 return false;
404 }
405 tilingData.availableLevels = implicit["availableLevels"].get<int>();
406 tilingData.subtreeLevels = implicit["subtreeLevels"].get<int>();
407
408 if ( implicit.contains( "subtrees" ) && implicit["subtrees"].contains( "uri" ) )
409 tilingData.subtreeUriTemplate = QString::fromStdString( implicit["subtrees"]["uri"].get<std::string>() );
410
411 if ( tilingData.subtreeUriTemplate.isEmpty() )
412 {
413 QgsDebugError( u"Implicit tiling is missing required subtrees.uri"_s );
414 return false;
415 }
416
417 if ( json.contains( "content" ) && json["content"].contains( "uri" ) )
418 tilingData.contentUriTemplate = QString::fromStdString( json["content"]["uri"].get<std::string>() );
419
420 // TODO: "contents" with multiple contents not supported yet
421
422 tilingData.baseUrl = baseUrl;
423 tilingData.rootBoundingVolume = newNode->tile()->boundingVolume();
424 // If the root tile uses a "region" bounding volume, store the original lat/lon region
425 // so that implicit tile subdivision can be done in geographic space rather than on
426 // the derived oriented bounding box.
427 if ( json.contains( "boundingVolume" ) && json["boundingVolume"].contains( "region" ) )
428 {
429 const QgsBox3D region = QgsCesiumUtils::parseRegion( json["boundingVolume"]["region"] );
430 if ( !region.isNull() )
431 tilingData.rootRegion = region;
432 }
433 tilingData.rootGeometricError = newNode->tile()->geometricError();
434 tilingData.refinementProcess = newNode->tile()->refinementProcess();
435 if ( newNode->tile()->transform() )
436 tilingData.rootTransform = *newNode->tile()->transform();
437 tilingData.gltfUpAxis = gltfUpAxis;
438
439 // Clear the template content URI — it will be resolved after subtree fetch
440 newNode->tile()->setResources( {} );
441 return true;
442 }
443 else
444 {
445 QgsDebugError( u"Unsupported implicit tiling subdivision scheme: %1"_s.arg( QString::fromStdString( scheme ) ) );
446 return false;
447 }
448}
TileChildrenAvailability
Possible availability states for a tile's children.
Definition qgis.h:6194
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Definition qgis.h:6197
@ NoChildren
Tile is known to have no children.
Definition qgis.h:6195
Axis
Cartesian axes.
Definition qgis.h:2607
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
double xMinimum() const
Returns the minimum x value.
Definition qgsbox3d.h:205
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:268
double width() const
Returns the width of the box.
Definition qgsbox3d.h:287
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:261
double yMinimum() const
Returns the minimum y value.
Definition qgsbox3d.h:233
double height() const
Returns the height of the box.
Definition qgsbox3d.h:294
bool isNull() const
Test if the box is null (holding no spatial information).
Definition qgsbox3d.cpp:311
static QMap< TileCoordinate, QgsTiledSceneNode * > createImplicitTilingChildren(QgsTiledSceneNode *node, const TileCoordinate &coord, Root &tilingData, const TileCoordinate &subtreeCoord, QgsCoordinateTransformContext &transformContext, long long &nextTileId)
Creates immediate children of a node according to implicit tiling.
static TileCoordinate tileCoordinateToParentSubtree(TileCoordinate coord, int subtreeLevels)
Returns parent subtree's tile coordinates of the given tile.
static QString expandTemplateUri(const QString &templateUri, const QUrl &baseUrl, const TileCoordinate &coord)
Expands template URI (using {level}, {x}, {y} markers) to the final URI.
static Qgis::TileChildrenAvailability childAvailability(const Root &tilingData, const TileCoordinate &coord)
Returns tile availability of a child based on cached subtree data.
static bool parseImplicitTiling(const json &tileJson, QgsTiledSceneNode *newNode, const QUrl &baseUrl, Qgis::Axis gltfUpAxis, Root &tilingData)
Parses JSON definition of implicit tiling into tilingData argument and returns true on success.
static Subtree parseSubtree(const Root &tilingData, const QByteArray &data)
Parses subtree definition and returns it.
static int subtreeBitIndex(int localLevel, int localX, int localY)
Returns the bit index within a subtree's availability bitstream for a given local tile position.
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 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.
Contains information about the context in which a coordinate transform is executed.
const double * halfAxes() const
Returns the half axes matrix;.
double centerZ() const
Returns the center z-coordinate.
bool isNull() const
Returns true if the box is a null box.
double centerX() const
Returns the center x-coordinate.
double centerY() const
Returns the center y-coordinate.
Represents a bounding volume for a tiled scene.
QgsOrientedBox3D box() const
Returns the volume's oriented box.
Allows representing QgsTiledSceneTiles in a hierarchical tree.
void addChild(QgsTiledSceneNode *child)
Adds a child to this node.
QgsTiledSceneTile * tile()
Returns the tile associated with the node.
Qgis::TileRefinementProcess refinementProcess() const
Returns the tile's refinement process.
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
const QgsMatrix4x4 * transform() const
Returns the tile's transform.
void setResources(const QVariantMap &resources)
Sets the resources attached to the tile.
double geometricError() const
Returns the tile's geometric error, which is the error, in meters, of the tile's simplified represent...
#define QgsDebugError(str)
Definition qgslogger.h:59
Definition of root implicit tiling node (typically root node of the whole tileset,...
QMap< TileCoordinate, Subtree > subtreeCache
Qgis::TileRefinementProcess refinementProcess
std::optional< QgsBox3D > rootRegion
if the root node uses "region" bounding volume (in lat/lon), we use it to create child regions and th...
int subtreeLevels
how many levels are stored in a single subtree
int availableLevels
total number of available levels within the implicit tiling
std::optional< QgsMatrix4x4 > rootTransform
QgsTiledSceneBoundingVolume rootBoundingVolume
if the root node uses OBB as the bounding volume, we use it directly to create child volumes
Data about subtree of a node - there should be subtree at least for root implicit tiling node,...
QBitArray contentAvailability
Bit array whether a tile has some content.
QBitArray tileAvailability
Bit array whether a tile is available.
QBitArray childSubtreeAvailability
Bit array to know where to look for more subtree definitions deeper in the hierarchy.
Contains ZXY coordinates of a node within implicit tiling.