|
|
( function () {
const _taskCache = new WeakMap();
class DRACOLoader extends THREE.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 THREE.FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( this.requestHeader ); loader.setWithCredentials( this.withCredentials ); loader.load( url, buffer => {
const taskConfig = { attributeIDs: this.defaultAttributeIDs, attributeTypes: this.defaultAttributeTypes, useUniqueIDs: false }; this.decodeGeometry( buffer, taskConfig ).then( onLoad ).catch( onError );
}, onProgress, onError );
} /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */
decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) {
const taskConfig = { attributeIDs: attributeIDs || this.defaultAttributeIDs, attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !! attributeIDs }; this.decodeGeometry( buffer, taskConfig ).then( callback );
}
decodeGeometry( buffer, taskConfig ) {
// TODO: For backward-compatibility, support 'attributeTypes' objects containing
// references (rather than names) to typed array constructors. These must be
// serialized before sending them to the worker.
for ( const attribute in taskConfig.attributeTypes ) {
const type = taskConfig.attributeTypes[ attribute ];
if ( type.BYTES_PER_ELEMENT !== undefined ) {
taskConfig.attributeTypes[ attribute ] = type.name;
}
} //
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 THREE.BufferGeometry();
if ( geometryData.index ) {
geometry.setIndex( new THREE.BufferAttribute( geometryData.index.array, 1 ) );
}
for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
const attribute = geometryData.attributes[ i ]; const name = attribute.name; const array = attribute.array; const itemSize = attribute.itemSize; geometry.setAttribute( name, new THREE.BufferAttribute( array, itemSize ) );
}
return geometry;
}
_loadLibrary( url, responseType ) {
const loader = new THREE.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; 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(); const decoderBuffer = new draco.DecoderBuffer(); decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength );
try {
const geometry = decodeGeometry( draco, decoder, decoderBuffer, 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( decoderBuffer ); draco.destroy( decoder );
}
} ); break;
}
};
function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) {
const attributeIDs = taskConfig.attributeIDs; const attributeTypes = taskConfig.attributeTypes; let dracoGeometry; let decodingStatus; const geometryType = decoder.GetEncodedGeometryType( decoderBuffer );
if ( geometryType === draco.TRIANGULAR_MESH ) {
dracoGeometry = new draco.Mesh(); decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry );
} else if ( geometryType === draco.POINT_CLOUD ) {
dracoGeometry = new draco.PointCloud(); decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, 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 );
}
geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) );
} // 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;
}
}
}
THREE.DRACOLoader = DRACOLoader;
} )();
|