import { BufferAttribute, BufferGeometry, Color, FileLoader, Loader, LinearSRGBColorSpace, SRGBColorSpace } from 'three'; const _taskCache = new WeakMap(); class DRACOLoader extends Loader { constructor( manager ) { super( manager ); this.decoderPath = ''; this.decoderConfig = {}; this.decoderBinary = null; this.decoderPending = null; this.workerLimit = 4; this.workerPool = []; this.workerNextTaskID = 1; this.workerSourceURL = ''; this.defaultAttributeIDs = { position: 'POSITION', normal: 'NORMAL', color: 'COLOR', uv: 'TEX_COORD' }; this.defaultAttributeTypes = { position: 'Float32Array', normal: 'Float32Array', color: 'Float32Array', uv: 'Float32Array' }; } setDecoderPath( path ) { this.decoderPath = path; return this; } setDecoderConfig( config ) { this.decoderConfig = config; return this; } setWorkerLimit( workerLimit ) { this.workerLimit = workerLimit; return this; } load( url, onLoad, onProgress, onError ) { const loader = new FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( this.requestHeader ); loader.setWithCredentials( this.withCredentials ); loader.load( url, ( buffer ) => { this.parse( buffer, onLoad, onError ); }, onProgress, onError ); } parse( buffer, onLoad, onError = ()=>{} ) { this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError ); } decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) { const taskConfig = { attributeIDs: attributeIDs || this.defaultAttributeIDs, attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !! attributeIDs, vertexColorSpace: vertexColorSpace, }; return this.decodeGeometry( buffer, taskConfig ).then( callback ).catch( onError ); } decodeGeometry( buffer, taskConfig ) { const taskKey = JSON.stringify( taskConfig ); // Check for an existing task using this buffer. A transferred buffer cannot be transferred // again from this thread. if ( _taskCache.has( buffer ) ) { const cachedTask = _taskCache.get( buffer ); if ( cachedTask.key === taskKey ) { return cachedTask.promise; } else if ( buffer.byteLength === 0 ) { // Technically, it would be possible to wait for the previous task to complete, // transfer the buffer back, and decode again with the second configuration. That // is complex, and I don't know of any reason to decode a Draco buffer twice in // different ways, so this is left unimplemented. throw new Error( 'THREE.DRACOLoader: Unable to re-decode a buffer with different ' + 'settings. Buffer has already been transferred.' ); } } // let worker; const taskID = this.workerNextTaskID ++; const taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance // when the task completes. const geometryPending = this._getWorker( taskID, taskCost ) .then( ( _worker ) => { worker = _worker; return new Promise( ( resolve, reject ) => { worker._callbacks[ taskID ] = { resolve, reject }; worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] ); // this.debug(); } ); } ) .then( ( message ) => this._createGeometry( message.geometry ) ); // Remove task from the task list. // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) geometryPending .catch( () => true ) .then( () => { if ( worker && taskID ) { this._releaseTask( worker, taskID ); // this.debug(); } } ); // Cache the task result. _taskCache.set( buffer, { key: taskKey, promise: geometryPending } ); return geometryPending; } _createGeometry( geometryData ) { const geometry = new BufferGeometry(); if ( geometryData.index ) { geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) ); } for ( let i = 0; i < geometryData.attributes.length; i ++ ) { const result = geometryData.attributes[ i ]; const name = result.name; const array = result.array; const itemSize = result.itemSize; const attribute = new BufferAttribute( array, itemSize ); if ( name === 'color' ) { this._assignVertexColorSpace( attribute, result.vertexColorSpace ); attribute.normalized = ( array instanceof Float32Array ) === false; } geometry.setAttribute( name, attribute ); } return geometry; } _assignVertexColorSpace( attribute, inputColorSpace ) { // While .drc files do not specify colorspace, the only 'official' tooling // is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc // file is passed into .load() or .parse(). GLTFLoader uses internal APIs // to decode geometry, and vertex colors are already Linear-sRGB in there. if ( inputColorSpace !== SRGBColorSpace ) return; const _color = new Color(); for ( let i = 0, il = attribute.count; i < il; i ++ ) { _color.fromBufferAttribute( attribute, i ).convertSRGBToLinear(); attribute.setXYZ( i, _color.r, _color.g, _color.b ); } } _loadLibrary( url, responseType ) { const loader = new FileLoader( this.manager ); loader.setPath( this.decoderPath ); loader.setResponseType( responseType ); loader.setWithCredentials( this.withCredentials ); return new Promise( ( resolve, reject ) => { loader.load( url, resolve, undefined, reject ); } ); } preload() { this._initDecoder(); return this; } _initDecoder() { if ( this.decoderPending ) return this.decoderPending; const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; const librariesPending = []; if ( useJS ) { librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) ); } else { librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) ); librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) ); } this.decoderPending = Promise.all( librariesPending ) .then( ( libraries ) => { const jsContent = libraries[ 0 ]; if ( ! useJS ) { this.decoderConfig.wasmBinary = libraries[ 1 ]; } const fn = DRACOWorker.toString(); const body = [ '/* draco decoder */', jsContent, '', '/* worker */', fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) ].join( '\n' ); this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) ); } ); return this.decoderPending; } _getWorker( taskID, taskCost ) { return this._initDecoder().then( () => { if ( this.workerPool.length < this.workerLimit ) { const worker = new Worker( this.workerSourceURL ); worker._callbacks = {}; worker._taskCosts = {}; worker._taskLoad = 0; worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } ); worker.onmessage = function ( e ) { const message = e.data; switch ( message.type ) { case 'decode': worker._callbacks[ message.id ].resolve( message ); break; case 'error': worker._callbacks[ message.id ].reject( message ); break; default: console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' ); } }; this.workerPool.push( worker ); } else { this.workerPool.sort( function ( a, b ) { return a._taskLoad > b._taskLoad ? - 1 : 1; } ); } const worker = this.workerPool[ this.workerPool.length - 1 ]; worker._taskCosts[ taskID ] = taskCost; worker._taskLoad += taskCost; return worker; } ); } _releaseTask( worker, taskID ) { worker._taskLoad -= worker._taskCosts[ taskID ]; delete worker._callbacks[ taskID ]; delete worker._taskCosts[ taskID ]; } debug() { console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); } dispose() { for ( let i = 0; i < this.workerPool.length; ++ i ) { this.workerPool[ i ].terminate(); } this.workerPool.length = 0; if ( this.workerSourceURL !== '' ) { URL.revokeObjectURL( this.workerSourceURL ); } return this; } } /* WEB WORKER */ function DRACOWorker() { let decoderConfig; let decoderPending; onmessage = function ( e ) { const message = e.data; switch ( message.type ) { case 'init': decoderConfig = message.decoderConfig; decoderPending = new Promise( function ( resolve/*, reject*/ ) { decoderConfig.onModuleLoaded = function ( draco ) { // Module is Promise-like. Wrap before resolving to avoid loop. resolve( { draco: draco } ); }; DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef } ); break; case 'decode': const buffer = message.buffer; const taskConfig = message.taskConfig; decoderPending.then( ( module ) => { const draco = module.draco; const decoder = new draco.Decoder(); try { const geometry = decodeGeometry( draco, decoder, new Int8Array( buffer ), taskConfig ); const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); if ( geometry.index ) buffers.push( geometry.index.array.buffer ); self.postMessage( { type: 'decode', id: message.id, geometry }, buffers ); } catch ( error ) { console.error( error ); self.postMessage( { type: 'error', id: message.id, error: error.message } ); } finally { draco.destroy( decoder ); } } ); break; } }; function decodeGeometry( draco, decoder, array, taskConfig ) { const attributeIDs = taskConfig.attributeIDs; const attributeTypes = taskConfig.attributeTypes; let dracoGeometry; let decodingStatus; const geometryType = decoder.GetEncodedGeometryType( array ); if ( geometryType === draco.TRIANGULAR_MESH ) { dracoGeometry = new draco.Mesh(); decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry ); } else if ( geometryType === draco.POINT_CLOUD ) { dracoGeometry = new draco.PointCloud(); decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry ); } else { throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' ); } if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) { throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() ); } const geometry = { index: null, attributes: [] }; // Gather all vertex attributes. for ( const attributeName in attributeIDs ) { const attributeType = self[ attributeTypes[ attributeName ] ]; let attribute; let attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, // a Draco file may contain a custom set of attributes, identified by known unique // IDs. glTF files always do the latter, and `.drc` files typically do the former. if ( taskConfig.useUniqueIDs ) { attributeID = attributeIDs[ attributeName ]; attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID ); } else { attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] ); if ( attributeID === - 1 ) continue; attribute = decoder.GetAttribute( dracoGeometry, attributeID ); } const attributeResult = decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ); if ( attributeName === 'color' ) { attributeResult.vertexColorSpace = taskConfig.vertexColorSpace; } geometry.attributes.push( attributeResult ); } // Add index. if ( geometryType === draco.TRIANGULAR_MESH ) { geometry.index = decodeIndex( draco, decoder, dracoGeometry ); } draco.destroy( dracoGeometry ); return geometry; } function decodeIndex( draco, decoder, dracoGeometry ) { const numFaces = dracoGeometry.num_faces(); const numIndices = numFaces * 3; const byteLength = numIndices * 4; const ptr = draco._malloc( byteLength ); decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr ); const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); draco._free( ptr ); return { array: index, itemSize: 1 }; } function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { const numComponents = attribute.num_components(); const numPoints = dracoGeometry.num_points(); const numValues = numPoints * numComponents; const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; const dataType = getDracoDataType( draco, attributeType ); const ptr = draco._malloc( byteLength ); decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr ); const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); draco._free( ptr ); return { name: attributeName, array: array, itemSize: numComponents }; } function getDracoDataType( draco, attributeType ) { switch ( attributeType ) { case Float32Array: return draco.DT_FLOAT32; case Int8Array: return draco.DT_INT8; case Int16Array: return draco.DT_INT16; case Int32Array: return draco.DT_INT32; case Uint8Array: return draco.DT_UINT8; case Uint16Array: return draco.DT_UINT16; case Uint32Array: return draco.DT_UINT32; } } } export { DRACOLoader };