UNPKG

3d-tiles-renderer

Version:

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

428 lines (277 loc) 10.7 kB
import { MathUtils } from 'three'; import { ProjectionScheme } from './ProjectionScheme.js'; 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(); } 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 = new ProjectionScheme( 'none' ); this._levels = []; } // build the zoom levels setLevel( level, options = {} ) { const levels = this._levels; while ( levels.length < level ) { levels.push( null ); } const { tileSplitX = 2, tileSplitY = 2, } = options; const { tilePixelWidth = 256, tilePixelHeight = 256, tileCountX = tileSplitX ** level, tileCountY = tileSplitY ** 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 number of tiles that the tiles at this layer split in to tileSplitX, tileSplitY, // 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 { tileCountY, tileBounds, pixelHeight, pixelWidth, tilePixelHeight, tilePixelWidth } = this.getLevel( level ); const xStride = tilePixelWidth / pixelWidth; const yStride = tilePixelHeight / pixelHeight; 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 ( normalized ) { bounds[ 0 ] = projection.convertLongitudeToNormalized( bounds[ 0 ] ); bounds[ 1 ] = projection.convertLatitudeToNormalized( bounds[ 1 ] ); bounds[ 2 ] = projection.convertLongitudeToNormalized( bounds[ 2 ] ); bounds[ 3 ] = projection.convertLatitudeToNormalized( 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, clamp = 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 ( clamp ) { bounds = this.clampToBounds( bounds, true ); } if ( ! normalized ) { bounds[ 0 ] = projection.convertNormalizedToLongitude( bounds[ 0 ] ); bounds[ 1 ] = projection.convertNormalizedToLatitude( bounds[ 1 ] ); bounds[ 2 ] = projection.convertNormalizedToLongitude( bounds[ 2 ] ); bounds[ 3 ] = projection.convertNormalizedToLatitude( bounds[ 3 ] ); } return bounds; } toNormalizedPoint( x, y ) { return this.projection.toNormalizedPoint( x, y ); } toNormalizedRange( range ) { return this.projection.toNormalizedRange( range ); } toCartographicPoint( x, y ) { return this.projection.toCartographicPoint( x, y ); } toCartographicRange( range ) { return this.projection.toCartographicRange( range ); } 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; } clampToBounds( range, normalized = false ) { return this.projection.clampToBounds( range, normalized ); } }