614 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			614 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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 };
 |