3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
447 lines (288 loc) • 10.5 kB
JavaScript
import { WebGLArrayRenderTarget, MeshBasicMaterial, DataTexture, REVISION } from 'three';
import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
import { ExpandingBatchedMesh } from './ExpandingBatchedMesh.js';
import { convertMapToArrayTexture, isColorWhite } from './utilities.js';
const _textureRenderQuad = new FullScreenQuad( new MeshBasicMaterial() );
const _whiteTex = new DataTexture( new Uint8Array( [ 255, 255, 255, 255 ] ), 1, 1 );
_whiteTex.needsUpdate = true;
export class BatchedTilesPlugin {
constructor( options = {} ) {
if ( parseInt( REVISION ) < 170 ) {
throw new Error( 'BatchedTilesPlugin: Three.js revision 170 or higher required.' );
}
options = {
instanceCount: 500,
vertexCount: 750,
indexCount: 2000,
expandPercent: 0.25,
maxInstanceCount: Infinity,
discardOriginalContent: true,
textureSize: null,
material: null,
renderer: null,
...options
};
this.name = 'BATCHED_TILES_PLUGIN';
this.priority = - 1;
// limit the amount of instances to the size of a 3d texture to avoid over flowing the
const gl = options.renderer.getContext();
// save options
this.instanceCount = options.instanceCount;
this.vertexCount = options.vertexCount;
this.indexCount = options.indexCount;
this.material = options.material ? options.material.clone() : null;
this.expandPercent = options.expandPercent;
this.maxInstanceCount = Math.min( options.maxInstanceCount, gl.getParameter( gl.MAX_3D_TEXTURE_SIZE ) );
this.renderer = options.renderer;
this.discardOriginalContent = options.discardOriginalContent;
this.textureSize = options.textureSize;
// local variables
this.batchedMesh = null;
this.arrayTarget = null;
this.tiles = null;
this._onLoadModel = null;
this._onDisposeModel = null;
this._onVisibilityChange = null;
this._tileToInstanceId = new Map();
}
init( tiles ) {
this._onDisposeModel = ( { scene, tile } ) => {
this.removeSceneFromBatchedMesh( scene, tile );
};
// register events
tiles.addEventListener( 'dispose-model', this._onDisposeModel );
this.tiles = tiles;
}
initTextureArray( target ) {
if ( this.arrayTarget !== null || target.material.map === null ) {
return;
}
const { instanceCount, renderer, textureSize, batchedMesh } = this;
// init the array texture render target
const map = target.material.map;
const textureOptions = {
colorSpace: map.colorSpace,
wrapS: map.wrapS,
wrapT: map.wrapT,
wrapR: map.wrapS,
// TODO: Generating mipmaps for the volume every time a new texture is added is extremely slow
// generateMipmaps: map.generateMipmaps,
// minFilter: map.minFilter,
magFilter: map.magFilter,
};
const arrayTarget = new WebGLArrayRenderTarget( textureSize || map.image.width, textureSize || map.image.height, instanceCount );
Object.assign( arrayTarget.texture, textureOptions );
renderer.initRenderTarget( arrayTarget );
// assign the material
batchedMesh.material.map = arrayTarget.texture;
this.arrayTarget = arrayTarget;
// once the texture array is initialized we fill in textures for all previously-initialized instances
// since they may have been skipped due to not having textures
this._tileToInstanceId.forEach( value => {
value.forEach( id => {
this.assignTextureToLayer( _whiteTex, id );
} );
} );
}
// init the batched mesh if it's not ready
initBatchedMesh( target ) {
if ( this.batchedMesh !== null ) {
return;
}
// init the batched mesh
const { instanceCount, vertexCount, indexCount, tiles } = this;
const material = this.material ? this.material : new target.material.constructor();
const batchedMesh = new ExpandingBatchedMesh( instanceCount, instanceCount * vertexCount, instanceCount * indexCount, material );
batchedMesh.name = 'BatchTilesPlugin';
batchedMesh.frustumCulled = false;
tiles.group.add( batchedMesh );
batchedMesh.updateMatrixWorld();
convertMapToArrayTexture( batchedMesh.material );
this.batchedMesh = batchedMesh;
}
setTileVisible( tile, visible ) {
const scene = tile.cached.scene;
if ( visible ) {
// Add tileset to the batched mesh if it hasn't been added already
this.addSceneToBatchedMesh( scene, tile );
}
if ( this._tileToInstanceId.has( tile ) ) {
const instanceIds = this._tileToInstanceId.get( tile );
instanceIds.forEach( instanceId => {
this.batchedMesh.setVisibleAt( instanceId, visible );
} );
// TODO: this should be handled by the base tiles renderer
const tiles = this.tiles;
if ( visible ) {
tiles.visibleTiles.add( tile );
} else {
tiles.visibleTiles.delete( tile );
}
// dispatch the event that is blocked otherwise
tiles.dispatchEvent( {
type: 'tile-visibility-change',
scene,
tile,
visible,
} );
return true;
}
return false;
}
unloadTileFromGPU( scene, tile ) {
if ( ! this.discardOriginalContent && this._tileToInstanceId.has( tile ) ) {
this.removeSceneFromBatchedMesh( scene, tile );
return true;
}
return false;
}
// render the given into the given layer
assignTextureToLayer( texture, layer ) {
// if the array target has not been created yet then skip the assignment and expansion
if ( ! this.arrayTarget ) {
return;
}
this.expandArrayTargetIfNeeded();
const { renderer } = this;
const currentRenderTarget = renderer.getRenderTarget();
// render the layer
renderer.setRenderTarget( this.arrayTarget, layer );
_textureRenderQuad.material.map = texture;
_textureRenderQuad.render( renderer );
// TODO: perform a copy if the texture is already the appropriate size
// reset state
renderer.setRenderTarget( currentRenderTarget );
_textureRenderQuad.material.map = null;
texture.dispose();
}
// check if the array texture target needs to be expanded
expandArrayTargetIfNeeded() {
const { batchedMesh, arrayTarget, renderer } = this;
const targetDepth = Math.min( batchedMesh.maxInstanceCount, this.maxInstanceCount );
if ( targetDepth > arrayTarget.depth ) {
// create a new array texture target
const textureOptions = {
colorSpace: arrayTarget.texture.colorSpace,
wrapS: arrayTarget.texture.wrapS,
wrapT: arrayTarget.texture.wrapT,
generateMipmaps: arrayTarget.texture.generateMipmaps,
minFilter: arrayTarget.texture.minFilter,
magFilter: arrayTarget.texture.magFilter,
};
const newArrayTarget = new WebGLArrayRenderTarget( arrayTarget.width, arrayTarget.height, targetDepth );
Object.assign( newArrayTarget.texture, textureOptions );
// copy the contents
renderer.initRenderTarget( newArrayTarget );
renderer.copyTextureToTexture( arrayTarget.texture, newArrayTarget.texture );
// replace the old array target
arrayTarget.dispose();
batchedMesh.material.map = newArrayTarget.texture;
this.arrayTarget = newArrayTarget;
}
}
removeSceneFromBatchedMesh( scene, tile ) {
if ( this._tileToInstanceId.has( tile ) ) {
const instanceIds = this._tileToInstanceId.get( tile );
this._tileToInstanceId.delete( tile );
instanceIds.forEach( instanceId => {
this.batchedMesh.deleteInstance( instanceId );
} );
}
}
addSceneToBatchedMesh( scene, tile ) {
if ( this._tileToInstanceId.has( tile ) ) {
return;
}
// find the meshes in the scene
const meshes = [];
scene.traverse( c => {
if ( c.isMesh ) {
meshes.push( c );
}
} );
// don't add the geometry if it doesn't have the right attributes
let hasCorrectAttributes = true;
meshes.forEach( mesh => {
if ( this.batchedMesh && hasCorrectAttributes ) {
const attrs = mesh.geometry.attributes;
const batchedAttrs = this.batchedMesh.geometry.attributes;
for ( const key in batchedAttrs ) {
if ( ! ( key in attrs ) ) {
hasCorrectAttributes = false;
return;
}
}
}
} );
const canAddMeshes = ! this.batchedMesh || this.batchedMesh.instanceCount + meshes.length <= this.maxInstanceCount;
if ( hasCorrectAttributes && canAddMeshes ) {
scene.updateMatrixWorld();
const instanceIds = [];
this._tileToInstanceId.set( tile, instanceIds );
meshes.forEach( mesh => {
this.initBatchedMesh( mesh );
this.initTextureArray( mesh );
const { geometry, material } = mesh;
const { batchedMesh, expandPercent } = this;
// assign expandPercent in case it has changed
batchedMesh.expandPercent = expandPercent;
const geometryId = batchedMesh.addGeometry( geometry, this.vertexCount, this.indexCount );
const instanceId = batchedMesh.addInstance( geometryId );
instanceIds.push( instanceId );
batchedMesh.setMatrixAt( instanceId, mesh.matrixWorld );
batchedMesh.setVisibleAt( instanceId, false );
if ( ! isColorWhite( material.color ) ) {
material.color.setHSL( Math.random(), 0.5, 0.5 );
batchedMesh.setColorAt( instanceId, material.color );
}
// render the material
const texture = material.map;
if ( texture ) {
this.assignTextureToLayer( texture, instanceId );
} else {
this.assignTextureToLayer( _whiteTex, instanceId );
}
} );
// discard all data if the option is set
// TODO: this would be best done in a more general way
if ( this.discardOriginalContent ) {
tile.cached.textures.forEach( tex => {
if ( tex.image instanceof ImageBitmap ) {
tex.image.close();
}
} );
tile.cached.scene = null;
tile.cached.materials = [];
tile.cached.geometries = [];
tile.cached.textures = [];
}
}
}
// Override raycasting per tile to defer to the batched mesh
raycastTile( tile, scene, raycaster, intersects ) {
if ( ! this._tileToInstanceId.has( tile ) ) {
return false;
}
const instanceIds = this._tileToInstanceId.get( tile );
instanceIds.forEach( instanceId => {
this.batchedMesh.raycastInstance( instanceId, raycaster, intersects );
} );
return true;
}
dispose() {
const { arrayTarget, tiles, batchedMesh } = this;
if ( arrayTarget ) {
arrayTarget.dispose();
}
if ( batchedMesh ) {
batchedMesh.material.dispose();
batchedMesh.geometry.dispose();
batchedMesh.dispose();
batchedMesh.removeFromParent();
}
tiles.removeEventListener( 'dispose-model', this._onDisposeModel );
}
getTileBatchIds( tile ) {
return this._tileToInstanceId.get( tile );
}
}