3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
898 lines (681 loc) • 27.1 kB
JavaScript
/**
* Structure almost identical to Cesium, also the comments and the names are kept
* https://github.com/CesiumGS/cesium/blob/0a69f67b393ba194eefb7254600811c4b712ddc0/packages/engine/Source/Scene/Implicit3DTileContent.js
*/
import { LoaderBase, LoaderUtils } from '3d-tiles-renderer/core';
function isOctreeSubdivision( tile ) {
return tile.__implicitRoot.implicitTiling.subdivisionScheme === 'OCTREE';
}
function getBoundsDivider( tile ) {
return isOctreeSubdivision( tile ) ? 8 : 4;
}
function getSubtreeCoordinates( tile, parentTile ) {
if ( ! parentTile ) {
return [ 0, 0, 0 ];
}
const x = 2 * parentTile.__x + ( tile.__subtreeIdx % 2 );
const y = 2 * parentTile.__y + ( Math.floor( tile.__subtreeIdx / 2 ) % 2 );
const z = isOctreeSubdivision( tile ) ?
2 * parentTile.__z + ( Math.floor( tile.__subtreeIdx / 4 ) % 2 ) : 0;
return [ x, y, z ];
}
class SubtreeTile {
constructor( parentTile, childMortonIndex ) {
this.parent = parentTile;
this.children = [];
this.__level = parentTile.__level + 1;
this.__implicitRoot = parentTile.__implicitRoot;
// Index inside the tree
this.__subtreeIdx = childMortonIndex;
[ this.__x, this.__y, this.__z ] = getSubtreeCoordinates( this, parentTile );
}
static copy( tile ) {
const copyTile = {};
copyTile.children = [];
copyTile.__level = tile.__level;
copyTile.__implicitRoot = tile.__implicitRoot;
// Index inside the tree
copyTile.__subtreeIdx = tile.__subtreeIdx;
[ copyTile.__x, copyTile.__y, copyTile.__z ] = [ tile.__x, tile.__y, tile.__z ];
copyTile.boundingVolume = tile.boundingVolume;
copyTile.geometricError = tile.geometricError;
return copyTile;
}
}
export class SUBTREELoader extends LoaderBase {
constructor( tile ) {
super();
this.tile = tile;
this.rootTile = tile.__implicitRoot; // The implicit root tile
this.workingPath = null;
}
/**
* A helper object for storing the two parts of the subtree binary
*
* @typedef {object} Subtree
* @property {number} version
* @property {JSON} subtreeJson
* @property {ArrayBuffer} subtreeByte
* @private
*/
/**
*
* @param buffer
* @return {Subtree}
*/
parseBuffer( buffer ) {
const dataView = new DataView( buffer );
let offset = 0;
// 16-byte header
// 4 bytes
const magic = LoaderUtils.readMagicBytes( dataView );
console.assert( magic === 'subt', 'SUBTREELoader: The magic bytes equal "subt".' );
offset += 4;
// 4 bytes
const version = dataView.getUint32( offset, true );
console.assert( version === 1, 'SUBTREELoader: The version listed in the header is "1".' );
offset += 4;
// From Cesium
// Read the bottom 32 bits of the 64-bit byte length.
// This is ok for now because:
// 1) not all browsers have native 64-bit operations
// 2) the data is well under 4GB
// 8 bytes
const jsonLength = dataView.getUint32( offset, true );
offset += 8;
// 8 bytes
const byteLength = dataView.getUint32( offset, true );
offset += 8;
const subtreeJson = JSON.parse( LoaderUtils.arrayToString( new Uint8Array( buffer, offset, jsonLength ) ) );
offset += jsonLength;
const subtreeByte = buffer.slice( offset, offset + byteLength );
return {
version,
subtreeJson,
subtreeByte
};
}
async parse( buffer ) {
// todo here : handle json
const subtree = this.parseBuffer( buffer );
const subtreeJson = subtree.subtreeJson;
// TODO Handle metadata
/*
const subtreeMetadata = subtreeJson.subtreeMetadata;
subtree._metadata = subtreeMetadata;
*/
/*
Tile availability indicates which tiles exist within the subtree
Content availability indicates which tiles have associated content resources
Child subtree availability indicates what subtrees are reachable from this subtree
*/
// After identifying how availability is stored, put the results in this new array for consistent processing later
subtreeJson.contentAvailabilityHeaders = [].concat( subtreeJson.contentAvailability );
const bufferHeaders = this.preprocessBuffers( subtreeJson.buffers );
const bufferViewHeaders = this.preprocessBufferViews(
subtreeJson.bufferViews,
bufferHeaders
);
// Buffers and buffer views are inactive until explicitly marked active.
// This way we can avoid fetching buffers that will not be used.
this.markActiveBufferViews( subtreeJson, bufferViewHeaders );
// Await the active buffers. If a buffer is external (isExternal === true),
// fetch it from its URI.
const buffersU8 = await this.requestActiveBuffers(
bufferHeaders,
subtree.subtreeByte
);
const bufferViewsU8 = this.parseActiveBufferViews( bufferViewHeaders, buffersU8 );
this.parseAvailability( subtree, subtreeJson, bufferViewsU8 );
this.expandSubtree( this.tile, subtree );
}
/**
* Determine which buffer views need to be loaded into memory. This includes:
*
* <ul>
* <li>The tile availability bitstream (if a bitstream is defined)</li>
* <li>The content availability bitstream(s) (if a bitstream is defined)</li>
* <li>The child subtree availability bitstream (if a bitstream is defined)</li>
* </ul>
*
* <p>
* This function modifies the buffer view headers' isActive flags in place.
* </p>
*
* @param {JSON} subtreeJson The JSON chunk from the subtree
* @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
* @private
*/
markActiveBufferViews( subtreeJson, bufferViewHeaders ) {
let header;
const tileAvailabilityHeader = subtreeJson.tileAvailability;
// Check for bitstream first, which is part of the current schema.
// bufferView is the name of the bitstream from an older schema.
if ( ! isNaN( tileAvailabilityHeader.bitstream ) ) {
header = bufferViewHeaders[ tileAvailabilityHeader.bitstream ];
} else if ( ! isNaN( tileAvailabilityHeader.bufferView ) ) {
header = bufferViewHeaders[ tileAvailabilityHeader.bufferView ];
}
if ( header ) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
const contentAvailabilityHeaders = subtreeJson.contentAvailabilityHeaders;
for ( let i = 0; i < contentAvailabilityHeaders.length; i ++ ) {
header = undefined;
if ( ! isNaN( contentAvailabilityHeaders[ i ].bitstream ) ) {
header = bufferViewHeaders[ contentAvailabilityHeaders[ i ].bitstream ];
} else if ( ! isNaN( contentAvailabilityHeaders[ i ].bufferView ) ) {
header = bufferViewHeaders[ contentAvailabilityHeaders[ i ].bufferView ];
}
if ( header ) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
}
header = undefined;
const childSubtreeAvailabilityHeader = subtreeJson.childSubtreeAvailability;
if ( ! isNaN( childSubtreeAvailabilityHeader.bitstream ) ) {
header = bufferViewHeaders[ childSubtreeAvailabilityHeader.bitstream ];
} else if ( ! isNaN( childSubtreeAvailabilityHeader.bufferView ) ) {
header = bufferViewHeaders[ childSubtreeAvailabilityHeader.bufferView ];
}
if ( header ) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
}
/**
* Go through the list of buffers and gather all the active ones into
* a dictionary.
* <p>
* The results are put into a dictionary object. The keys are indices of
* buffers, and the values are Uint8Arrays of the contents. Only buffers
* marked with the isActive flag are fetched.
* </p>
* <p>
* The internal buffer (the subtree's binary chunk) is also stored in this
* dictionary if it is marked active.
* </p>
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
* @param {ArrayBuffer} internalBuffer The binary chunk of the subtree file
* @returns {object} buffersU8 A dictionary of buffer index to a Uint8Array of its contents.
* @private
*/
async requestActiveBuffers( bufferHeaders, internalBuffer ) {
const promises = [];
for ( let i = 0; i < bufferHeaders.length; i ++ ) {
const bufferHeader = bufferHeaders[ i ];
// If the buffer is not active, resolve with undefined.
if ( ! bufferHeader.isActive ) {
promises.push( Promise.resolve( ) );
} else if ( bufferHeader.isExternal ) {
// Get the absolute URI of the external buffer.
const url = this.parseImplicitURIBuffer(
this.tile,
this.rootTile.implicitTiling.subtrees.uri,
bufferHeader.uri
);
const fetchPromise = fetch( url, this.fetchOptions )
.then( response => {
if ( ! response.ok ) {
throw new Error( `SUBTREELoader: Failed to load external buffer from ${ bufferHeader.uri } with error code ${ response.status }.` );
}
return response.arrayBuffer();
} )
.then( arrayBuffer => new Uint8Array( arrayBuffer ) );
promises.push( fetchPromise );
} else {
promises.push( Promise.resolve( new Uint8Array( internalBuffer ) ) );
}
}
const bufferResults = await Promise.all( promises );
const buffersU8 = {};
for ( let i = 0; i < bufferResults.length; i ++ ) {
const result = bufferResults[ i ];
if ( result ) {
buffersU8[ i ] = result;
}
}
return buffersU8;
}
/**
* Go through the list of buffer views, and if they are marked as active,
* extract a subarray from one of the active buffers.
*
* @param {BufferViewHeader[]} bufferViewHeaders
* @param {object} buffersU8 A dictionary of buffer index to a Uint8Array of its contents.
* @returns {object} A dictionary of buffer view index to a Uint8Array of its contents.
* @private
*/
parseActiveBufferViews( bufferViewHeaders, buffersU8 ) {
const bufferViewsU8 = {};
for ( let i = 0; i < bufferViewHeaders.length; i ++ ) {
const bufferViewHeader = bufferViewHeaders[ i ];
if ( ! bufferViewHeader.isActive ) {
continue;
}
const start = bufferViewHeader.byteOffset;
const end = start + bufferViewHeader.byteLength;
const buffer = buffersU8[ bufferViewHeader.buffer ];
bufferViewsU8[ i ] = buffer.slice( start, end );
}
return bufferViewsU8;
}
/**
* A buffer header is the JSON header from the subtree JSON chunk plus
* a couple extra boolean flags for easy reference.
*
* Buffers are assumed inactive until explicitly marked active. This is used
* to avoid fetching unneeded buffers.
*
* @typedef {object} BufferHeader
* @property {boolean} isActive Whether this buffer is currently used.
* @property {string} [uri] The URI of the buffer (external buffers only)
* @property {number} byteLength The byte length of the buffer, including any padding contained within.
* @private
*/
/**
* Iterate over the list of buffers from the subtree JSON and add the isActive field for easier parsing later.
* This modifies the objects in place.
* @param {Object[]} [bufferHeaders=[]] The JSON from subtreeJson.buffers.
* @returns {BufferHeader[]} The same array of headers with additional fields.
* @private
*/
preprocessBuffers( bufferHeaders = [] ) {
for ( let i = 0; i < bufferHeaders.length; i ++ ) {
const bufferHeader = bufferHeaders[ i ];
bufferHeader.isActive = false;
bufferHeader.isExternal = !! bufferHeader.uri;
}
return bufferHeaders;
}
/**
* A buffer view header is the JSON header from the subtree JSON chunk plus
* the isActive flag and a reference to the header for the underlying buffer.
*
* @typedef {object} BufferViewHeader
* @property {BufferHeader} bufferHeader A reference to the header for the underlying buffer
* @property {boolean} isActive Whether this bufferView is currently used.
* @property {number} buffer The index of the underlying buffer.
* @property {number} byteOffset The start byte of the bufferView within the buffer.
* @property {number} byteLength The length of the bufferView. No padding is included in this length.
* @private
*/
/**
* Iterate the list of buffer views from the subtree JSON and add the
* isActive flag. Also save a reference to the bufferHeader.
*
* @param {Object[]} [bufferViewHeaders=[]] The JSON from subtree.bufferViews.
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers.
* @returns {BufferViewHeader[]} The same array of bufferView headers with additional fields.
* @private
*/
preprocessBufferViews( bufferViewHeaders = [], bufferHeaders ) {
for ( let i = 0; i < bufferViewHeaders.length; i ++ ) {
const bufferViewHeader = bufferViewHeaders[ i ];
bufferViewHeader.bufferHeader = bufferHeaders[ bufferViewHeader.buffer ];
bufferViewHeader.isActive = false;
// Keep the external flag for potential use in requestActiveBuffers
bufferViewHeader.isExternal = bufferViewHeader.bufferHeader.isExternal;
}
return bufferViewHeaders;
}
/**
* Parse the three availability bitstreams and store them in the subtree.
*
* @param {Subtree} subtree The subtree to modify.
* @param {Object} subtreeJson The subtree JSON.
* @param {Object} bufferViewsU8 A dictionary of buffer view index to a Uint8Array of its contents.
* @private
*/
parseAvailability( subtree, subtreeJson, bufferViewsU8 ) {
const branchingFactor = getBoundsDivider( this.rootTile );
const subtreeLevels = this.rootTile.implicitTiling.subtreeLevels;
const tileAvailabilityBits =
( Math.pow( branchingFactor, subtreeLevels ) - 1 ) / ( branchingFactor - 1 );
const childSubtreeBits = Math.pow( branchingFactor, subtreeLevels );
subtree._tileAvailability = this.parseAvailabilityBitstream(
subtreeJson.tileAvailability,
bufferViewsU8,
tileAvailabilityBits
);
subtree._contentAvailabilityBitstreams = [];
for ( let i = 0; i < subtreeJson.contentAvailabilityHeaders.length; i ++ ) {
const bitstream = this.parseAvailabilityBitstream(
subtreeJson.contentAvailabilityHeaders[ i ],
bufferViewsU8,
// content availability has the same length as tile availability.
tileAvailabilityBits
);
subtree._contentAvailabilityBitstreams.push( bitstream );
}
subtree._childSubtreeAvailability = this.parseAvailabilityBitstream(
subtreeJson.childSubtreeAvailability,
bufferViewsU8,
childSubtreeBits
);
}
/**
* Given the JSON describing an availability bitstream, turn it into an
* in-memory representation using an object. This handles bitstreams from a bufferView.
*
* @param {Object} availabilityJson A JSON object representing the availability.
* @param {Object} bufferViewsU8 A dictionary of buffer view index to its Uint8Array contents.
* @param {number} lengthBits The length of the availability bitstream in bits.
* @returns {object}
* @private
*/
parseAvailabilityBitstream(
availabilityJson,
bufferViewsU8,
lengthBits,
) {
if ( ! isNaN( availabilityJson.constant ) ) {
return {
constant: Boolean( availabilityJson.constant ),
lengthBits: lengthBits,
};
}
let bufferView;
// Check for bitstream first, which is part of the current schema.
// bufferView is the name of the bitstream from an older schema.
if ( ! isNaN( availabilityJson.bitstream ) ) {
bufferView = bufferViewsU8[ availabilityJson.bitstream ];
} else if ( ! isNaN( availabilityJson.bufferView ) ) {
bufferView = bufferViewsU8[ availabilityJson.bufferView ];
}
return {
bitstream: bufferView,
lengthBits: lengthBits
};
}
/**
* Expand a single subtree tile. This transcodes the subtree into
* a tree of {@link SubtreeTile}. The root of this tree is stored in
* the placeholder tile's children array. This method also creates
* tiles for the child subtrees to be lazily expanded as needed.
*
* @param {Object | SubtreeTile} subtreeRoot The first node of the subtree.
* @param {Subtree} subtree The parsed subtree.
* @private
*/
expandSubtree( subtreeRoot, subtree ) {
// TODO If multiple contents were supported then this tile could contain both renderable and un renderable content.
const contentTile = SubtreeTile.copy( subtreeRoot );
// If the subtree root tile has content, then create a placeholder child with cloned parameters
// Todo Multiple contents not handled, keep the first content found
for ( let i = 0; subtree && i < subtree._contentAvailabilityBitstreams.length; i ++ ) {
if ( subtree && this.getBit( subtree._contentAvailabilityBitstreams[ i ], 0 ) ) {
// Create a child holding the content uri, this child is similar to its parent and doesn't have any children.
contentTile.content = { uri: this.parseImplicitURI( subtreeRoot, this.rootTile.content.uri ) };
break;
}
}
subtreeRoot.children.push( contentTile );
// Creating each leaf inside the current subtree.
const bottomRow = this.transcodeSubtreeTiles(
contentTile,
subtree
);
// For each child subtree, create a tile containing the uri of the next subtree to fetch.
const childSubtrees = this.listChildSubtrees( subtree, bottomRow );
for ( let i = 0; i < childSubtrees.length; i ++ ) {
const subtreeLocator = childSubtrees[ i ];
const leafTile = subtreeLocator.tile;
const subtreeTile = this.deriveChildTile(
null,
leafTile,
null,
subtreeLocator.childMortonIndex
);
// Assign subtree uri as content.
subtreeTile.content = { uri: this.parseImplicitURI( subtreeTile, this.rootTile.implicitTiling.subtrees.uri ) };
leafTile.children.push( subtreeTile );
}
}
/**
* Transcode the implicitly defined tiles within this subtree and generate
* explicit {@link SubtreeTile} objects. This function only transcodes tiles,
* child subtrees are handled separately.
*
* @param {Object | SubtreeTile} subtreeRoot The root of the current subtree.
* @param {Subtree} subtree The subtree to get availability information.
* @returns {Array} The bottom row of transcoded tiles. This is helpful for processing child subtrees.
* @private
*/
transcodeSubtreeTiles( subtreeRoot, subtree ) {
// Sliding window over the levels of the tree.
// Each row is branchingFactor * length of previous row.
// Tiles within a row are ordered by Morton index.
let parentRow = [ subtreeRoot ];
let currentRow = [];
for ( let level = 1; level < this.rootTile.implicitTiling.subtreeLevels; level ++ ) {
const branchingFactor = getBoundsDivider( this.rootTile );
const levelOffset = ( Math.pow( branchingFactor, level ) - 1 ) / ( branchingFactor - 1 );
const numberOfChildren = branchingFactor * parentRow.length;
for ( let childMortonIndex = 0; childMortonIndex < numberOfChildren; childMortonIndex ++ ) {
const childBitIndex = levelOffset + childMortonIndex;
const parentMortonIndex = childMortonIndex >> Math.log2( branchingFactor );
const parentTile = parentRow[ parentMortonIndex ];
// Check if tile is available.
if ( ! this.getBit( subtree._tileAvailability, childBitIndex ) ) {
currentRow.push( undefined );
continue;
}
// Create a tile and add it as a child.
const childTile = this.deriveChildTile(
subtree,
parentTile,
childBitIndex,
childMortonIndex
);
parentTile.children.push( childTile );
currentRow.push( childTile );
}
parentRow = currentRow;
currentRow = [];
}
return parentRow;
}
/**
* Given a parent tile and information about which child to create, derive
* the properties of the child tile implicitly.
* <p>
* This creates a real tile for rendering.
* </p>
*
* @param {Subtree} subtree The subtree the child tile belongs to.
* @param {Object | SubtreeTile} parentTile The parent of the new child tile.
* @param {number} childBitIndex The index of the child tile within the tile's availability information.
* @param {number} childMortonIndex The morton index of the child tile relative to its parent.
* @returns {SubtreeTile} The new child tile.
* @private
*/
deriveChildTile(
subtree,
parentTile,
childBitIndex,
childMortonIndex
) {
const subtreeTile = new SubtreeTile( parentTile, childMortonIndex );
subtreeTile.boundingVolume = this.getTileBoundingVolume( subtreeTile );
subtreeTile.geometricError = this.getGeometricError( subtreeTile );
// Todo Multiple contents not handled, keep the first found content.
for ( let i = 0; subtree && i < subtree._contentAvailabilityBitstreams.length; i ++ ) {
if ( subtree && this.getBit( subtree._contentAvailabilityBitstreams[ i ], childBitIndex ) ) {
subtreeTile.content = { uri: this.parseImplicitURI( subtreeTile, this.rootTile.content.uri ) };
break;
}
}
return subtreeTile;
}
/**
* Get a bit from the bitstream as a Boolean. If the bitstream
* is a constant, the constant value is returned instead.
*
* @param {ParsedBitstream} object
* @param {number} index The integer index of the bit.
* @returns {boolean} The value of the bit.
* @private
*/
getBit( object, index ) {
if ( index < 0 || index >= object.lengthBits ) {
throw new Error( 'Bit index out of bounds.' );
}
if ( object.constant !== undefined ) {
return object.constant;
}
// byteIndex is floor(index / 8)
const byteIndex = index >> 3;
const bitIndex = index % 8;
return ( ( new Uint8Array( object.bitstream )[ byteIndex ] >> bitIndex ) & 1 ) === 1;
}
/**
* //TODO Adapt for Sphere
* To maintain numerical stability during this subdivision process,
* the actual bounding volumes should not be computed progressively by subdividing a non-root tile volume.
* Instead, the exact bounding volumes are computed directly for a given level.
* @param {Object | SubtreeTile} tile
* @return {Object} object containing the bounding volume.
*/
getTileBoundingVolume( tile ) {
const boundingVolume = {};
if ( this.rootTile.boundingVolume.region ) {
const region = [ ...this.rootTile.boundingVolume.region ];
const minX = region[ 0 ];
const maxX = region[ 2 ];
const minY = region[ 1 ];
const maxY = region[ 3 ];
const sizeX = ( maxX - minX ) / Math.pow( 2, tile.__level );
const sizeY = ( maxY - minY ) / Math.pow( 2, tile.__level );
region[ 0 ] = minX + sizeX * tile.__x; //west
region[ 2 ] = minX + sizeX * ( tile.__x + 1 ); //east
region[ 1 ] = minY + sizeY * tile.__y; //south
region[ 3 ] = minY + sizeY * ( tile.__y + 1 ); //north
for ( let k = 0; k < 4; k ++ ) {
const coord = region[ k ];
if ( coord < - Math.PI ) {
region[ k ] += 2 * Math.PI;
} else if ( coord > Math.PI ) {
region[ k ] -= 2 * Math.PI;
}
}
//Also divide the height in the case of octree.
if ( isOctreeSubdivision( tile ) ) {
const minZ = region[ 4 ];
const maxZ = region[ 5 ];
const sizeZ = ( maxZ - minZ ) / Math.pow( 2, tile.__level );
region[ 4 ] = minZ + sizeZ * tile.__z; //minimum height
region[ 5 ] = minZ + sizeZ * ( tile.__z + 1 ); //maximum height
}
boundingVolume.region = region;
}
if ( this.rootTile.boundingVolume.box ) {
// 0-2: center of the box
// 3-5: x axis direction and half length
// 6-8: y axis direction and half length
// 9-11: z axis direction and half length
const box = [ ...this.rootTile.boundingVolume.box ];
const cellSteps = 2 ** tile.__level - 1;
const scale = Math.pow( 2, - tile.__level );
const axisNumber = isOctreeSubdivision( tile ) ? 3 : 2;
for ( let i = 0; i < axisNumber; i ++ ) {
// scale the bounds axes
box[ 3 + i * 3 + 0 ] *= scale;
box[ 3 + i * 3 + 1 ] *= scale;
box[ 3 + i * 3 + 2 ] *= scale;
// axis vector
const x = box[ 3 + i * 3 + 0 ];
const y = box[ 3 + i * 3 + 1 ];
const z = box[ 3 + i * 3 + 2 ];
// adjust the center by the x, y and z axes
const axisOffset = i === 0 ? tile.__x : ( i === 1 ? tile.__y : tile.__z );
box[ 0 ] += 2 * x * ( - 0.5 * cellSteps + axisOffset );
box[ 1 ] += 2 * y * ( - 0.5 * cellSteps + axisOffset );
box[ 2 ] += 2 * z * ( - 0.5 * cellSteps + axisOffset );
}
boundingVolume.box = box;
}
return boundingVolume;
}
/**
* Each child’s geometricError is half of its parent’s geometricError.
* @param {Object | SubtreeTile} tile
* @return {number}
*/
getGeometricError( tile ) {
return this.rootTile.geometricError / Math.pow( 2, tile.__level );
}
/**
* Determine what child subtrees exist and return a list of information.
*
* @param {Object} subtree The subtree for looking up availability.
* @param {Array} bottomRow The bottom row of tiles in a transcoded subtree.
* @returns {[]} A list of identifiers for the child subtrees.
* @private
*/
listChildSubtrees( subtree, bottomRow ) {
const results = [];
const branchingFactor = getBoundsDivider( this.rootTile );
for ( let i = 0; i < bottomRow.length; i ++ ) {
const leafTile = bottomRow[ i ];
if ( leafTile === undefined ) {
continue;
}
for ( let j = 0; j < branchingFactor; j ++ ) {
const index = i * branchingFactor + j;
if ( this.getBit( subtree._childSubtreeAvailability, index ) ) {
results.push( {
tile: leafTile,
childMortonIndex: index
} );
}
}
}
return results;
}
/**
* Replaces placeholder tokens in a URI template with the corresponding tile properties.
*
* The URI template should contain the tokens:
* - `{level}` for the tile's subdivision level.
* - `{x}` for the tile's x-coordinate.
* - `{y}` for the tile's y-coordinate.
* - `{z}` for the tile's z-coordinate.
*
* @param {Object} tile - The tile object containing properties __level, __x, __y, and __z.
* @param {string} uri - The URI template string with placeholders.
* @returns {string} The URI with placeholders replaced by the tile's properties.
*/
parseImplicitURI( tile, uri ) {
uri = uri.replace( '{level}', tile.__level );
uri = uri.replace( '{x}', tile.__x );
uri = uri.replace( '{y}', tile.__y );
uri = uri.replace( '{z}', tile.__z );
return uri;
}
/**
* Generates the full external buffer URI for a tile by combining an implicit URI with a buffer URI.
*
* First, it parses the implicit URI using the tile properties and the provided template. Then, it creates a new URL
* relative to the tile's base path, removes the last path segment, and appends the buffer URI.
*
* @param {Object} tile - The tile object that contains properties:
* - __level: the subdivision level,
* - __x, __y, __z: the tile coordinates,
* @param {string} uri - The URI template string with placeholders for the tile (e.g., `{level}`, `{x}`, `{y}`, `{z}`).
* @param {string} bufUri - The buffer file name to append (e.g., "0_1.bin").
* @returns {string} The full external buffer URI.
*/
parseImplicitURIBuffer( tile, uri, bufUri ) {
// Generate the base tile URI by replacing placeholders
const subUri = this.parseImplicitURI( tile, uri );
// Create a URL object relative to the tile's base path
const url = new URL( subUri, this.workingPath + '/' );
// Remove the last path segment
url.pathname = url.pathname.substring( 0, url.pathname.lastIndexOf( '/' ) );
// Construct the final URL with the buffer URI appended
return new URL( url.pathname + '/' + bufUri, this.workingPath + '/' ).toString();
}
}