/** Copyright 2016 The Draco Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @param {THREE.LoadingManager} manager */ THREE.DRACOLoader = function ( manager ) { this.timeLoaded = 0; this.manager = manager || THREE.DefaultLoadingManager; this.materials = null; this.verbosity = 0; this.attributeOptions = {}; this.drawMode = THREE.TrianglesDrawMode; // Native Draco attribute type to Three.JS attribute type. this.nativeAttributeMap = { position: "POSITION", normal: "NORMAL", color: "COLOR", uv: "TEX_COORD" }; }; THREE.DRACOLoader.prototype = { constructor: THREE.DRACOLoader, load: function ( url, onLoad, onProgress, onError ) { var scope = this; var loader = new THREE.FileLoader( scope.manager ); loader.setPath( this.path ); loader.setResponseType( "arraybuffer" ); loader.load( url, function ( blob ) { scope.decodeDracoFile( blob, onLoad ); }, onProgress, onError ); }, setPath: function ( value ) { this.path = value; return this; }, setVerbosity: function ( level ) { this.verbosity = level; return this; }, /** * Sets desired mode for generated geometry indices. * Can be either: * THREE.TrianglesDrawMode * THREE.TriangleStripDrawMode */ setDrawMode: function ( drawMode ) { this.drawMode = drawMode; return this; }, /** * Skips dequantization for a specific attribute. * |attributeName| is the THREE.js name of the given attribute type. * The only currently supported |attributeName| is 'position', more may be * added in future. */ setSkipDequantization: function ( attributeName, skip ) { var skipDequantization = true; if ( typeof skip !== "undefined" ) skipDequantization = skip; this.getAttributeOptions( attributeName ).skipDequantization = skipDequantization; return this; }, /** * Decompresses a Draco buffer. Names of attributes (for ID and type maps) * must be one of the supported three.js types, including: position, color, * normal, uv, uv2, skinIndex, skinWeight. * * @param {ArrayBuffer} rawBuffer * @param {Function} callback * @param {Object|undefined} attributeUniqueIdMap Provides a pre-defined ID * for each attribute in the geometry to be decoded. If given, * `attributeTypeMap` is required and `nativeAttributeMap` will be * ignored. * @param {Object|undefined} attributeTypeMap Provides a predefined data * type (as a typed array constructor) for each attribute in the * geometry to be decoded. */ decodeDracoFile: function ( rawBuffer, callback, attributeUniqueIdMap, attributeTypeMap ) { var scope = this; THREE.DRACOLoader.getDecoderModule().then( function ( module ) { scope.decodeDracoFileInternal( rawBuffer, module.decoder, callback, attributeUniqueIdMap, attributeTypeMap ); } ); }, decodeDracoFileInternal: function ( rawBuffer, dracoDecoder, callback, attributeUniqueIdMap, attributeTypeMap ) { /* * Here is how to use Draco Javascript decoder and get the geometry. */ var buffer = new dracoDecoder.DecoderBuffer(); buffer.Init( new Int8Array( rawBuffer ), rawBuffer.byteLength ); var decoder = new dracoDecoder.Decoder(); /* * Determine what type is this file: mesh or point cloud. */ var geometryType = decoder.GetEncodedGeometryType( buffer ); if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) { if ( this.verbosity > 0 ) { console.log( "Loaded a mesh." ); } } else if ( geometryType == dracoDecoder.POINT_CLOUD ) { if ( this.verbosity > 0 ) { console.log( "Loaded a point cloud." ); } } else { var errorMsg = "THREE.DRACOLoader: Unknown geometry type."; console.error( errorMsg ); throw new Error( errorMsg ); } callback( this.convertDracoGeometryTo3JS( dracoDecoder, decoder, geometryType, buffer, attributeUniqueIdMap, attributeTypeMap ) ); }, addAttributeToGeometry: function ( dracoDecoder, decoder, dracoGeometry, attributeName, attributeType, attribute, geometry, geometryBuffer ) { if ( attribute.ptr === 0 ) { var errorMsg = "THREE.DRACOLoader: No attribute " + attributeName; console.error( errorMsg ); throw new Error( errorMsg ); } var numComponents = attribute.num_components(); var numPoints = dracoGeometry.num_points(); var numValues = numPoints * numComponents; var attributeData; var TypedBufferAttribute; switch ( attributeType ) { case Float32Array: attributeData = new dracoDecoder.DracoFloat32Array(); decoder.GetAttributeFloatForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Float32Array( numValues ); TypedBufferAttribute = THREE.Float32BufferAttribute; break; case Int8Array: attributeData = new dracoDecoder.DracoInt8Array(); decoder.GetAttributeInt8ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Int8Array( numValues ); TypedBufferAttribute = THREE.Int8BufferAttribute; break; case Int16Array: attributeData = new dracoDecoder.DracoInt16Array(); decoder.GetAttributeInt16ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Int16Array( numValues ); TypedBufferAttribute = THREE.Int16BufferAttribute; break; case Int32Array: attributeData = new dracoDecoder.DracoInt32Array(); decoder.GetAttributeInt32ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Int32Array( numValues ); TypedBufferAttribute = THREE.Int32BufferAttribute; break; case Uint8Array: attributeData = new dracoDecoder.DracoUInt8Array(); decoder.GetAttributeUInt8ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Uint8Array( numValues ); TypedBufferAttribute = THREE.Uint8BufferAttribute; break; case Uint16Array: attributeData = new dracoDecoder.DracoUInt16Array(); decoder.GetAttributeUInt16ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Uint16Array( numValues ); TypedBufferAttribute = THREE.Uint16BufferAttribute; break; case Uint32Array: attributeData = new dracoDecoder.DracoUInt32Array(); decoder.GetAttributeUInt32ForAllPoints( dracoGeometry, attribute, attributeData ); geometryBuffer[ attributeName ] = new Uint32Array( numValues ); TypedBufferAttribute = THREE.Uint32BufferAttribute; break; default: var errorMsg = "THREE.DRACOLoader: Unexpected attribute type."; console.error( errorMsg ); throw new Error( errorMsg ); } // Copy data from decoder. for ( var i = 0; i < numValues; i ++ ) { geometryBuffer[ attributeName ][ i ] = attributeData.GetValue( i ); } // Add attribute to THREEJS geometry for rendering. geometry.addAttribute( attributeName, new TypedBufferAttribute( geometryBuffer[ attributeName ], numComponents ) ); dracoDecoder.destroy( attributeData ); }, convertDracoGeometryTo3JS: function ( dracoDecoder, decoder, geometryType, buffer, attributeUniqueIdMap, attributeTypeMap ) { // TODO: Should not assume native Draco attribute IDs apply. if ( this.getAttributeOptions( "position" ).skipDequantization === true ) { decoder.SkipAttributeTransform( dracoDecoder.POSITION ); } var dracoGeometry; var decodingStatus; var start_time = performance.now(); if ( geometryType === dracoDecoder.TRIANGULAR_MESH ) { dracoGeometry = new dracoDecoder.Mesh(); decodingStatus = decoder.DecodeBufferToMesh( buffer, dracoGeometry ); } else { dracoGeometry = new dracoDecoder.PointCloud(); decodingStatus = decoder.DecodeBufferToPointCloud( buffer, dracoGeometry ); } if ( ! decodingStatus.ok() || dracoGeometry.ptr == 0 ) { var errorMsg = "THREE.DRACOLoader: Decoding failed: "; errorMsg += decodingStatus.error_msg(); console.error( errorMsg ); dracoDecoder.destroy( decoder ); dracoDecoder.destroy( dracoGeometry ); throw new Error( errorMsg ); } var decode_end = performance.now(); dracoDecoder.destroy( buffer ); /* * Example on how to retrieve mesh and attributes. */ var numFaces; if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) { numFaces = dracoGeometry.num_faces(); if ( this.verbosity > 0 ) { console.log( "Number of faces loaded: " + numFaces.toString() ); } } else { numFaces = 0; } var numPoints = dracoGeometry.num_points(); var numAttributes = dracoGeometry.num_attributes(); if ( this.verbosity > 0 ) { console.log( "Number of points loaded: " + numPoints.toString() ); console.log( "Number of attributes loaded: " + numAttributes.toString() ); } // Verify if there is position attribute. // TODO: Should not assume native Draco attribute IDs apply. var posAttId = decoder.GetAttributeId( dracoGeometry, dracoDecoder.POSITION ); if ( posAttId == - 1 ) { var errorMsg = "THREE.DRACOLoader: No position attribute found."; console.error( errorMsg ); dracoDecoder.destroy( decoder ); dracoDecoder.destroy( dracoGeometry ); throw new Error( errorMsg ); } var posAttribute = decoder.GetAttribute( dracoGeometry, posAttId ); // Structure for converting to THREEJS geometry later. var geometryBuffer = {}; // Import data to Three JS geometry. var geometry = new THREE.BufferGeometry(); // Do not use both the native attribute map and a provided (e.g. glTF) map. if ( attributeUniqueIdMap ) { // Add attributes of user specified unique id. E.g. GLTF models. for ( var attributeName in attributeUniqueIdMap ) { var attributeType = attributeTypeMap[ attributeName ]; var attributeId = attributeUniqueIdMap[ attributeName ]; var attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeId ); this.addAttributeToGeometry( dracoDecoder, decoder, dracoGeometry, attributeName, attributeType, attribute, geometry, geometryBuffer ); } } else { // Add native Draco attribute type to geometry. for ( var attributeName in this.nativeAttributeMap ) { var attId = decoder.GetAttributeId( dracoGeometry, dracoDecoder[ this.nativeAttributeMap[ attributeName ] ] ); if ( attId !== - 1 ) { if ( this.verbosity > 0 ) { console.log( "Loaded " + attributeName + " attribute." ); } var attribute = decoder.GetAttribute( dracoGeometry, attId ); this.addAttributeToGeometry( dracoDecoder, decoder, dracoGeometry, attributeName, Float32Array, attribute, geometry, geometryBuffer ); } } } // For mesh, we need to generate the faces. if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) { if ( this.drawMode === THREE.TriangleStripDrawMode ) { var stripsArray = new dracoDecoder.DracoInt32Array(); decoder.GetTriangleStripsFromMesh( dracoGeometry, stripsArray ); geometryBuffer.indices = new Uint32Array( stripsArray.size() ); for ( var i = 0; i < stripsArray.size(); ++ i ) { geometryBuffer.indices[ i ] = stripsArray.GetValue( i ); } dracoDecoder.destroy( stripsArray ); } else { var numIndices = numFaces * 3; geometryBuffer.indices = new Uint32Array( numIndices ); var ia = new dracoDecoder.DracoInt32Array(); for ( var i = 0; i < numFaces; ++ i ) { decoder.GetFaceFromMesh( dracoGeometry, i, ia ); var index = i * 3; geometryBuffer.indices[ index ] = ia.GetValue( 0 ); geometryBuffer.indices[ index + 1 ] = ia.GetValue( 1 ); geometryBuffer.indices[ index + 2 ] = ia.GetValue( 2 ); } dracoDecoder.destroy( ia ); } } geometry.drawMode = this.drawMode; if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) { geometry.setIndex( new ( geometryBuffer.indices.length > 65535 ? THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute )( geometryBuffer.indices, 1 ) ); } // TODO: Should not assume native Draco attribute IDs apply. // TODO: Can other attribute types be quantized? var posTransform = new dracoDecoder.AttributeQuantizationTransform(); if ( posTransform.InitFromAttribute( posAttribute ) ) { // Quantized attribute. Store the quantization parameters into the // THREE.js attribute. geometry.attributes[ "position" ].isQuantized = true; geometry.attributes[ "position" ].maxRange = posTransform.range(); geometry.attributes[ "position" ].numQuantizationBits = posTransform.quantization_bits(); geometry.attributes[ "position" ].minValues = new Float32Array( 3 ); for ( var i = 0; i < 3; ++ i ) { geometry.attributes[ "position" ].minValues[ i ] = posTransform.min_value( i ); } } dracoDecoder.destroy( posTransform ); dracoDecoder.destroy( decoder ); dracoDecoder.destroy( dracoGeometry ); this.decode_time = decode_end - start_time; this.import_time = performance.now() - decode_end; if ( this.verbosity > 0 ) { console.log( "Decode time: " + this.decode_time ); console.log( "Import time: " + this.import_time ); } return geometry; }, isVersionSupported: function ( version, callback ) { THREE.DRACOLoader.getDecoderModule().then( function ( module ) { callback( module.decoder.isVersionSupported( version ) ); } ); }, getAttributeOptions: function ( attributeName ) { if ( typeof this.attributeOptions[ attributeName ] === "undefined" ) this.attributeOptions[ attributeName ] = {}; return this.attributeOptions[ attributeName ]; } }; THREE.DRACOLoader.decoderPath = "./"; THREE.DRACOLoader.decoderConfig = {}; THREE.DRACOLoader.decoderModulePromise = null; /** * Sets the base path for decoder source files. * @param {string} path */ THREE.DRACOLoader.setDecoderPath = function ( path ) { THREE.DRACOLoader.decoderPath = path; }; /** * Sets decoder configuration and releases singleton decoder module. Module * will be recreated with the next decoding call. * @param {Object} config */ THREE.DRACOLoader.setDecoderConfig = function ( config ) { var wasmBinary = THREE.DRACOLoader.decoderConfig.wasmBinary; THREE.DRACOLoader.decoderConfig = config || {}; THREE.DRACOLoader.releaseDecoderModule(); // Reuse WASM binary. if ( wasmBinary ) THREE.DRACOLoader.decoderConfig.wasmBinary = wasmBinary; }; /** * Releases the singleton DracoDecoderModule instance. Module will be recreated * with the next decoding call. */ THREE.DRACOLoader.releaseDecoderModule = function () { THREE.DRACOLoader.decoderModulePromise = null; }; /** * Gets WebAssembly or asm.js singleton instance of DracoDecoderModule * after testing for browser support. Returns Promise that resolves when * module is available. * @return {Promise<{decoder: DracoDecoderModule}>} */ THREE.DRACOLoader.getDecoderModule = function () { var scope = this; var path = THREE.DRACOLoader.decoderPath; var config = THREE.DRACOLoader.decoderConfig; var promise = THREE.DRACOLoader.decoderModulePromise; if ( promise ) return promise; // Load source files. if ( typeof DracoDecoderModule !== "undefined" ) { // Loaded externally. promise = Promise.resolve(); } else if ( typeof WebAssembly !== "object" || config.type === "js" ) { // Load with asm.js. promise = THREE.DRACOLoader._loadScript( path + "draco_decoder.js" ); } else { // Load with WebAssembly. config.wasmBinaryFile = path + "draco_decoder.wasm"; promise = THREE.DRACOLoader._loadScript( path + "draco_wasm_wrapper.js" ) .then( function () { return THREE.DRACOLoader._loadArrayBuffer( config.wasmBinaryFile ); } ) .then( function ( wasmBinary ) { config.wasmBinary = wasmBinary; } ); } // Wait for source files, then create and return a decoder. promise = promise.then( function () { return new Promise( function ( resolve ) { config.onModuleLoaded = function ( decoder ) { scope.timeLoaded = performance.now(); // Module is Promise-like. Wrap before resolving to avoid loop. resolve( { decoder: decoder } ); }; DracoDecoderModule( config ); } ); } ); THREE.DRACOLoader.decoderModulePromise = promise; return promise; }; /** * @param {string} src * @return {Promise} */ THREE.DRACOLoader._loadScript = function ( src ) { var prevScript = document.getElementById( "decoder_script" ); if ( prevScript !== null ) { prevScript.parentNode.removeChild( prevScript ); } var head = document.getElementsByTagName( "head" )[ 0 ]; var script = document.createElement( "script" ); script.id = "decoder_script"; script.type = "text/javascript"; script.src = src; return new Promise( function ( resolve ) { script.onload = resolve; head.appendChild( script ); } ); }; /** * @param {string} src * @return {Promise} */ THREE.DRACOLoader._loadArrayBuffer = function ( src ) { var loader = new THREE.FileLoader(); loader.setResponseType( "arraybuffer" ); return new Promise( function ( resolve, reject ) { loader.load( src, resolve, undefined, reject ); } ); };