UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

466 lines (302 loc) 11.5 kB
import { MathUtils } from 'three'; function doBoundsIntersect( a, b ) { const [ aMinX, aMinY, aMaxX, aMaxY ] = a; const [ bMinX, bMinY, bMaxX, bMaxY ] = b; return ! ( aMinX >= bMaxX || aMaxX <= bMinX || aMinY >= bMaxY || aMaxY <= bMinY ); } // Class for storing and querying a tiling scheme including a bounds, origin, and negative tile indices. // Assumes that tiles are split into four child tiles at each level. // Projection Bounds: The full extent of content representable by the projection. // Content Bounds: The range within the content bounds contains relevant, loadable, and renderable data. // Tile Bounds: The per-layer extent covered by the tiles to be loaded. This range may be larger than // both the projection and content bounds. export class TilingScheme { get levelCount() { return this._levels.length; } get maxLevel() { return this.levelCount - 1; } get minLevel() { const levels = this._levels; for ( let i = 0; i < levels.length; i ++ ) { if ( levels[ i ] !== null ) { return i; } } return - 1; } // prioritize user-set bounds over projection bounds if present get contentBounds() { return this._contentBounds ?? this.projection?.getBounds() ?? [ 0, 0, 1, 1 ]; } get aspectRatio() { const { pixelWidth, pixelHeight } = this.getLevel( this.maxLevel ); return pixelWidth / pixelHeight; } constructor() { this.flipY = false; this.pixelOverlap = 0; // The origin and bounds this._contentBounds = null; this.projection = null; this._levels = []; } // build the zoom levels setLevel( level, options = {} ) { const levels = this._levels; while ( levels.length < level ) { levels.push( null ); } const { tilePixelWidth = 256, tilePixelHeight = 256, tileCountX = 2 ** level, tileCountY = 2 ** level, tileBounds = null, } = options; const { pixelWidth = tilePixelWidth * tileCountX, pixelHeight = tilePixelHeight * tileCountY, } = options; levels[ level ] = { // The pixel resolution of each tile. tilePixelWidth, tilePixelHeight, // The total pixel resolution of the final image at this level. These numbers // may not be a round multiple of the tile width. pixelWidth, pixelHeight, // Or the total number of tiles that can be loaded at this level. tileCountX, tileCountY, // The bounds covered by the extent of the tiles at this loaded. The actual content covered by the overall tileset // may be a subset of this range (eg there may be unused space). tileBounds, }; } generateLevels( levels, rootTileX, rootTileY, options = {} ) { const { minLevel = 0, tilePixelWidth = 256, tilePixelHeight = 256, } = options; const maxLevel = levels - 1; const { pixelWidth = tilePixelWidth * rootTileX * ( 2 ** maxLevel ), pixelHeight = tilePixelHeight * rootTileY * ( 2 ** maxLevel ), } = options; for ( let level = minLevel; level < levels; level ++ ) { const invLevel = levels - level - 1; const levelPixelWidth = Math.ceil( pixelWidth * ( 2 ** - invLevel ) ); const levelPixelHeight = Math.ceil( pixelHeight * ( 2 ** - invLevel ) ); const tileCountX = Math.ceil( levelPixelWidth / tilePixelWidth ); const tileCountY = Math.ceil( levelPixelHeight / tilePixelHeight ); this.setLevel( level, { tilePixelWidth, tilePixelHeight, pixelWidth: levelPixelWidth, pixelHeight: levelPixelHeight, tileCountX, tileCountY, } ); } } getLevel( level ) { return this._levels[ level ]; } // bounds representing the contentful region of the image setContentBounds( minX, minY, maxX, maxY ) { this._contentBounds = [ minX, minY, maxX, maxY ]; } setProjection( projection ) { this.projection = projection; } // query functions getTileAtPoint( bx, by, level, normalized = false ) { const { flipY } = this; const { tileCountX, tileCountY, tileBounds } = this.getLevel( level ); const xStride = 1 / tileCountX; const yStride = 1 / tileCountY; if ( ! normalized ) { [ bx, by ] = this.toNormalizedPoint( bx, by ); } if ( tileBounds ) { const normalizedBounds = this.toNormalizedRange( tileBounds ); bx = MathUtils.mapLinear( bx, normalizedBounds[ 0 ], normalizedBounds[ 2 ], 0, 1 ); by = MathUtils.mapLinear( by, normalizedBounds[ 1 ], normalizedBounds[ 3 ], 0, 1 ); } const tx = Math.floor( bx / xStride ); let ty = Math.floor( by / yStride ); if ( flipY ) { ty = tileCountY - 1 - ty; } return [ tx, ty ]; } getTilesInRange( minX, minY, maxX, maxY, level, normalized = false ) { // check if the range is outside the content bounds const range = [ minX, minY, maxX, maxY ]; const contentBounds = this.getContentBounds( normalized ); let tileBounds = this.getLevel( level ).tileBounds; if ( ! doBoundsIntersect( range, contentBounds ) ) { return [ 0, 0, - 1, - 1 ]; } // check if the range is outside the tile bounds if ( tileBounds ) { if ( normalized ) { tileBounds = this.toNormalizedRange( tileBounds ); } if ( ! doBoundsIntersect( range, contentBounds ) ) { return [ 0, 0, - 1, - 1 ]; } } const [ clampedMinX, clampedMinY, clampedMaxX, clampedMaxY ] = this.clampToContentBounds( range, normalized ); const minTile = this.getTileAtPoint( clampedMinX, clampedMinY, level, normalized ); const maxTile = this.getTileAtPoint( clampedMaxX, clampedMaxY, level, normalized ); if ( this.flipY ) { [ minTile[ 1 ], maxTile[ 1 ] ] = [ maxTile[ 1 ], minTile[ 1 ] ]; } const { tileCountX, tileCountY } = this.getLevel( level ); const [ minTileX, minTileY ] = minTile; const [ maxTileX, maxTileY ] = maxTile; if ( maxTileX < 0 || maxTileY < 0 || minTileX >= tileCountX || minTileY >= tileCountY ) { return [ 0, 0, - 1, - 1 ]; } return [ MathUtils.clamp( minTileX, 0, tileCountX - 1 ), MathUtils.clamp( minTileY, 0, tileCountY - 1 ), MathUtils.clamp( maxTileX, 0, tileCountX - 1 ), MathUtils.clamp( maxTileY, 0, tileCountY - 1 ), ]; } getTileExists( x, y, level ) { const [ rminx, rminy, rmaxx, rmaxy ] = this.contentBounds; const [ tminx, tminy, tmaxx, tmaxy ] = this.getTileBounds( x, y, level ); const isDegenerate = tminx >= tmaxx || tminy >= tmaxy; // TODO: is supporting "just touch" correct? return ! isDegenerate && tminx <= rmaxx && tminy <= rmaxy && tmaxx >= rminx && tmaxy >= rminy; } getContentBounds( normalized = false ) { const { projection } = this; const bounds = [ ...this.contentBounds ]; if ( projection && normalized ) { bounds[ 0 ] = projection.convertLongitudeToProjection( bounds[ 0 ] ); bounds[ 1 ] = projection.convertLatitudeToProjection( bounds[ 1 ] ); bounds[ 2 ] = projection.convertLongitudeToProjection( bounds[ 2 ] ); bounds[ 3 ] = projection.convertLatitudeToProjection( bounds[ 3 ] ); } return bounds; } // returns the UV range associated with the content in the given tile getTileContentUVBounds( x, y, level ) { const [ minU, minV, maxU, maxV ] = this.getTileBounds( x, y, level, true, true ); const [ fullMinU, fullMinV, fullMaxU, fullMaxV ] = this.getTileBounds( x, y, level, true, false ); return [ MathUtils.mapLinear( minU, fullMinU, fullMaxU, 0, 1 ), MathUtils.mapLinear( minV, fullMinV, fullMaxV, 0, 1 ), MathUtils.mapLinear( maxU, fullMinU, fullMaxU, 0, 1 ), MathUtils.mapLinear( maxV, fullMinV, fullMaxV, 0, 1 ), ]; } getTileBounds( x, y, level, normalized = false, clampToProjection = true ) { const { flipY, pixelOverlap, projection } = this; const { tilePixelWidth, tilePixelHeight, pixelWidth, pixelHeight, tileBounds } = this.getLevel( level ); let tileLeft = tilePixelWidth * x - pixelOverlap; let tileTop = tilePixelHeight * y - pixelOverlap; let tileRight = tileLeft + tilePixelWidth + pixelOverlap * 2; let tileBottom = tileTop + tilePixelHeight + pixelOverlap * 2; // clamp tileLeft = Math.max( tileLeft, 0 ); tileTop = Math.max( tileTop, 0 ); tileRight = Math.min( tileRight, pixelWidth ); tileBottom = Math.min( tileBottom, pixelHeight ); // normalized tileLeft = tileLeft / pixelWidth; tileRight = tileRight / pixelWidth; tileTop = tileTop / pixelHeight; tileBottom = tileBottom / pixelHeight; // invert y if ( flipY ) { const extents = ( tileBottom - tileTop ) / 2; const centerY = ( tileTop + tileBottom ) / 2; const invCenterY = 1.0 - centerY; tileTop = invCenterY - extents; tileBottom = invCenterY + extents; } let bounds = [ tileLeft, tileTop, tileRight, tileBottom ]; if ( tileBounds ) { const normBounds = this.toNormalizedRange( tileBounds ); bounds[ 0 ] = MathUtils.mapLinear( bounds[ 0 ], 0, 1, normBounds[ 0 ], normBounds[ 2 ] ); bounds[ 2 ] = MathUtils.mapLinear( bounds[ 2 ], 0, 1, normBounds[ 0 ], normBounds[ 2 ] ); bounds[ 1 ] = MathUtils.mapLinear( bounds[ 1 ], 0, 1, normBounds[ 1 ], normBounds[ 3 ] ); bounds[ 3 ] = MathUtils.mapLinear( bounds[ 3 ], 0, 1, normBounds[ 1 ], normBounds[ 3 ] ); } if ( clampToProjection ) { bounds = this.clampToProjectionBounds( bounds, true ); } if ( projection && ! normalized ) { bounds[ 0 ] = projection.convertProjectionToLongitude( bounds[ 0 ] ); bounds[ 1 ] = projection.convertProjectionToLatitude( bounds[ 1 ] ); bounds[ 2 ] = projection.convertProjectionToLongitude( bounds[ 2 ] ); bounds[ 3 ] = projection.convertProjectionToLatitude( bounds[ 3 ] ); } return bounds; } toNormalizedPoint( x, y ) { const { projection } = this; const result = [ x, y ]; if ( this.projection ) { result[ 0 ] = projection.convertLongitudeToProjection( result[ 0 ] ); result[ 1 ] = projection.convertLatitudeToProjection( result[ 1 ] ); } return result; } toNormalizedRange( range ) { return [ ...this.toNormalizedPoint( range[ 0 ], range[ 1 ] ), ...this.toNormalizedPoint( range[ 2 ], range[ 3 ] ), ]; } toCartographicPoint( x, y ) { const { projection } = this; const result = [ x, y ]; if ( this.projection ) { result[ 0 ] = projection.convertProjectionToLongitude( result[ 0 ] ); result[ 1 ] = projection.convertProjectionToLatitude( result[ 1 ] ); } else { throw new Error( 'TilingScheme: Projection not available.' ); } return result; } toCartographicRange( range ) { return [ ...this.toCartographicPoint( range[ 0 ], range[ 1 ] ), ...this.toCartographicPoint( range[ 2 ], range[ 3 ] ), ]; } clampToContentBounds( range, normalized = false ) { const result = [ ...range ]; const [ minX, minY, maxX, maxY ] = this.getContentBounds( normalized ); result[ 0 ] = MathUtils.clamp( result[ 0 ], minX, maxX ); result[ 1 ] = MathUtils.clamp( result[ 1 ], minY, maxY ); result[ 2 ] = MathUtils.clamp( result[ 2 ], minX, maxX ); result[ 3 ] = MathUtils.clamp( result[ 3 ], minY, maxY ); return result; } clampToProjectionBounds( range, normalized = false ) { const result = [ ...range ]; const { projection } = this; let clampBounds; if ( normalized || ! projection ) { clampBounds = [ 0, 0, 1, 1 ]; } else { clampBounds = projection.getBounds(); } const [ minX, minY, maxX, maxY ] = clampBounds; result[ 0 ] = MathUtils.clamp( result[ 0 ], minX, maxX ); result[ 1 ] = MathUtils.clamp( result[ 1 ], minY, maxY ); result[ 2 ] = MathUtils.clamp( result[ 2 ], minX, maxX ); result[ 3 ] = MathUtils.clamp( result[ 3 ], minY, maxY ); return result; } }