UNPKG

3d-tiles-renderer

Version:

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

337 lines (226 loc) 7.35 kB
import { Mesh, MeshBasicMaterial, PlaneGeometry, MathUtils, Vector2 } from 'three'; const _uv = /* @__PURE__ */ new Vector2(); export const TILE_X = Symbol( 'TILE_X' ); export const TILE_Y = Symbol( 'TILE_Y' ); export const TILE_LEVEL = Symbol( 'TILE_LEVEL' ); // Base class for supporting tiled images with a consistent size / resolution per tile export class ImageFormatPlugin { get tiling() { return this.imageSource.tiling; } constructor( options = {} ) { const { pixelSize = null, center = false, useRecommendedSettings = true, imageSource = null, } = options; this.priority = - 10; this.tiles = null; // tiling scheme this.imageSource = imageSource; // options this.pixelSize = pixelSize; this.center = center; this.useRecommendedSettings = useRecommendedSettings; if ( pixelSize !== null ) { console.warn( 'ImageFormatPlugin: "pixelSize" has been deprecated in favor of scaling the tiles root.' ); } } // Plugin functions init( tiles ) { if ( this.useRecommendedSettings ) { tiles.errorTarget = 1; // TODO: apply skip traversal settings here once supported, as well, for faster loading } this.tiles = tiles; this.imageSource.fetchOptions = tiles.fetchOptions; this.imageSource.fetchData = ( url, options ) => { tiles.invokeAllPlugins( plugin => url = plugin.preprocessURL ? plugin.preprocessURL( url, null ) : url ); return tiles.invokeOnePlugin( plugin => plugin !== this && plugin.fetchData && plugin.fetchData( url, options ) ); }; } async loadRootTileset() { const { tiles, imageSource } = this; imageSource.url = imageSource.url || tiles.rootURL; tiles.invokeAllPlugins( plugin => imageSource.url = plugin.preprocessURL ? plugin.preprocessURL( imageSource.url, null ) : imageSource.url ); await imageSource.init(); tiles.rootURL = imageSource.url; return this.getTileset( imageSource.url ); } async parseToMesh( buffer, tile, extension, uri, abortSignal ) { if ( abortSignal.aborted ) { return null; } // Construct texture const { imageSource } = this; const tx = tile[ TILE_X ]; const ty = tile[ TILE_Y ]; const level = tile[ TILE_LEVEL ]; const texture = await imageSource.processBufferToTexture( buffer ); // clean up the texture if it's not going to be used. if ( abortSignal.aborted ) { texture.dispose(); texture.image.close(); return null; } imageSource.setData( tx, ty, level, texture ); // Construct mesh let sx = 1, sy = 1; let x = 0, y = 0, z = 0; const boundingBox = tile.boundingVolume.box; if ( boundingBox ) { [ x, y, z ] = boundingBox; sx = boundingBox[ 3 ]; sy = boundingBox[ 7 ]; } // adjust the geometry transform itself rather than the mesh because it reduces the artifact errors // when using batched mesh rendering. const geometry = new PlaneGeometry( 2 * sx, 2 * sy ); const mesh = new Mesh( geometry, new MeshBasicMaterial( { map: texture, transparent: true } ) ); mesh.position.set( x, y, z ); const tiling = imageSource.tiling; const uvRange = tiling.getTileContentUVBounds( tx, ty, level ); const { uv } = geometry.attributes; for ( let i = 0; i < uv.count; i ++ ) { _uv.fromBufferAttribute( uv, i ); _uv.x = MathUtils.mapLinear( _uv.x, 0, 1, uvRange[ 0 ], uvRange[ 2 ] ); _uv.y = MathUtils.mapLinear( _uv.y, 0, 1, uvRange[ 1 ], uvRange[ 3 ] ); uv.setXY( i, _uv.x, _uv.y ); } return mesh; } preprocessNode( tile ) { // generate children const { tiling } = this; const maxLevel = tiling.maxLevel; const level = tile[ TILE_LEVEL ]; if ( level < maxLevel && tile.parent !== null ) { this.expandChildren( tile ); } } disposeTile( tile ) { const tx = tile[ TILE_X ]; const ty = tile[ TILE_Y ]; const level = tile[ TILE_LEVEL ]; const { imageSource } = this; if ( imageSource.has( tx, ty, level ) ) { // only dispose of the image data if it hasn't been aborted imageSource.release( tx, ty, level ); } } // Local functions getTileset( baseUrl ) { const { tiling, tiles } = this; const minLevel = tiling.minLevel; const { tileCountX, tileCountY } = tiling.getLevel( minLevel ); // generate all children for the root const children = []; for ( let x = 0; x < tileCountX; x ++ ) { for ( let y = 0; y < tileCountY; y ++ ) { const child = this.createChild( x, y, minLevel ); if ( child !== null ) { children.push( child ); } } } // generate tileset const tileset = { asset: { version: '1.1' }, geometricError: 1e5, root: { refine: 'REPLACE', geometricError: 1e5, boundingVolume: this.createBoundingVolume( 0, 0, - 1 ), children, [ TILE_LEVEL ]: - 1, [ TILE_X ]: 0, [ TILE_Y ]: 0, } }; tiles.preprocessTileset( tileset, baseUrl ); return tileset; } getUrl( x, y, level ) { return this.imageSource.getUrl( x, y, level ); } createBoundingVolume( x, y, level ) { const { center, pixelSize, tiling } = this; const { pixelWidth, pixelHeight } = tiling.getLevel( tiling.maxLevel ); // calculate the world space bounds position from the range const [ minX, minY, maxX, maxY ] = level === - 1 ? tiling.getContentBounds( true ) : tiling.getTileBounds( x, y, level, true ); let extentsX = ( maxX - minX ) / 2; let extentsY = ( maxY - minY ) / 2; let centerX = minX + extentsX; let centerY = minY + extentsY; if ( center ) { centerX -= 0.5; centerY -= 0.5; } // scale the fields if ( pixelSize ) { centerX *= pixelWidth * pixelSize; extentsX *= pixelWidth * pixelSize; centerY *= pixelHeight * pixelSize; extentsY *= pixelHeight * pixelSize; } else { centerX *= tiling.aspectRatio; extentsX *= tiling.aspectRatio; } // return bounding box return { box: [ // center centerX, centerY, 0, // x, y, z half vectors extentsX, 0.0, 0.0, 0.0, extentsY, 0.0, 0.0, 0.0, 0.0, ], }; } createChild( x, y, level ) { const { pixelSize, tiling } = this; if ( ! tiling.getTileExists( x, y, level ) ) { return null; } // Calculate geometric error: size of one pixel in world space. // The tile contents span [0, 1] along Y and [0, aspectRatio] along X. const { pixelWidth, pixelHeight } = tiling.getLevel( level ); let geometricError = Math.max( tiling.aspectRatio / pixelWidth, 1 / pixelHeight ); // apply deprecated pixelSize scaling if specified if ( pixelSize ) { const maxLevelInfo = tiling.getLevel( tiling.maxLevel ); geometricError *= pixelSize * Math.max( maxLevelInfo.pixelWidth, maxLevelInfo.pixelHeight ); } // Generate the node return { refine: 'REPLACE', geometricError: geometricError, boundingVolume: this.createBoundingVolume( x, y, level ), content: { uri: this.getUrl( x, y, level ), }, children: [], // save the tile params so we can expand later [ TILE_X ]: x, [ TILE_Y ]: y, [ TILE_LEVEL ]: level, }; } expandChildren( tile ) { const level = tile[ TILE_LEVEL ]; const x = tile[ TILE_X ]; const y = tile[ TILE_Y ]; for ( let cx = 0; cx < 2; cx ++ ) { for ( let cy = 0; cy < 2; cy ++ ) { const child = this.createChild( 2 * x + cx, 2 * y + cy, level + 1 ); if ( child ) { tile.children.push( child ); } } } } }