Spaces:
Running
Running
import { | |
BufferAttribute, | |
BufferGeometry, | |
Color, | |
ColorManagement, | |
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, onError ).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 ); | |
ColorManagement.toWorkingColorSpace( _color, SRGBColorSpace ); | |
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 }; | |