// 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. // 'use strict'; /** * @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(); var numStrips = 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 ); }); };