From e77144b7f3556ded8ff9041cd3d84d269928a84b Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Fri, 15 Dec 2017 23:41:26 -0800 Subject: [PATCH] DRACOLoader: Simplify decoder loading. --- examples/js/loaders/draco/DRACOLoader.js | 444 ++++++++++------------- examples/webgl_loader_draco.html | 6 +- 2 files changed, 199 insertions(+), 251 deletions(-) diff --git a/examples/js/loaders/draco/DRACOLoader.js b/examples/js/loaders/draco/DRACOLoader.js index ab2e64678f..37e29353a0 100644 --- a/examples/js/loaders/draco/DRACOLoader.js +++ b/examples/js/loaders/draco/DRACOLoader.js @@ -14,23 +14,25 @@ // 'use strict'; -// |dracoPath| sets the path for the Draco decoder source files. The default -// path is "./". If |dracoDecoderType|.type is set to "js", then DRACOLoader -// will load the Draco JavaScript decoder. -THREE.DRACOLoader = function(dracoPath, dracoDecoderType, manager) { +/** + * @param {THREE.LoadingManager} manager + */ +THREE.DRACOLoader = function(manager) { this.timeLoaded = 0; - this.manager = (manager !== undefined) ? manager : - THREE.DefaultLoadingManager; + this.manager = manager || THREE.DefaultLoadingManager; this.materials = null; this.verbosity = 0; this.attributeOptions = {}; - this.dracoDecoderType = - (dracoDecoderType !== undefined) ? dracoDecoderType : {}; this.drawMode = THREE.TrianglesDrawMode; - this.dracoSrcPath = (dracoPath !== undefined) ? dracoPath : './'; - if (typeof DracoDecoderModule === 'undefined') { - THREE.DRACOLoader.loadDracoDecoder(this); - } + // User defined unique id for attributes. + this.attributeUniqueIdMap = {}; + // Native Draco attribute type to Three.JS attribute type. + this.nativeAttributeMap = { + 'position' : 'POSITION', + 'normal' : 'NORMAL', + 'color' : 'COLOR', + 'uv' : 'TEX_COORD' + }; }; THREE.DRACOLoader.prototype = { @@ -86,15 +88,30 @@ THREE.DRACOLoader.prototype = { skipDequantization; }, - decodeDracoFile: function(rawBuffer, callback) { + /** + * |attributeUniqueIdMap| specifies attribute unique id for an attribute in + * the geometry to be decoded. The name of the attribute must be one of the + * supported attribute type in Three.JS, including: + * 'position', + * 'color', + * 'normal', + * 'uv', + * 'uv2', + * 'skinIndex', + * 'skinWeight'. + * The format is: + * attributeUniqueIdMap[attributeName] = attributeId + */ + decodeDracoFile: function(rawBuffer, callback, attributeUniqueIdMap) { var scope = this; - THREE.DRACOLoader.getDecoder(this, - function(dracoDecoder) { - scope.decodeDracoFileInternal(rawBuffer, dracoDecoder, callback); - }); + this.attributeUniqueIdMap = attributeUniqueIdMap || {}; + THREE.DRACOLoader.getDecoderModule() + .then( function ( module ) { + scope.decodeDracoFileInternal( rawBuffer, module.decoder, callback ); + }); }, - decodeDracoFileInternal : function(rawBuffer, dracoDecoder, callback) { + decodeDracoFileInternal: function(rawBuffer, dracoDecoder, callback) { /* * Here is how to use Draco Javascript decoder and get the geometry. */ @@ -123,6 +140,33 @@ THREE.DRACOLoader.prototype = { geometryType, buffer)); }, + addAttributeToGeometry: function(dracoDecoder, decoder, dracoGeometry, + attributeName, 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 attributeData = new dracoDecoder.DracoFloat32Array(); + decoder.GetAttributeFloatForAllPoints( + dracoGeometry, attribute, attributeData); + var numPoints = dracoGeometry.num_points(); + var numValues = numPoints * numComponents; + // Allocate space for attribute. + geometryBuffer[attributeName] = new Float32Array(numValues); + // 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 THREE.Float32BufferAttribute(geometryBuffer[attributeName], + numComponents)); + dracoDecoder.destroy(attributeData); + }, + convertDracoGeometryTo3JS: function(dracoDecoder, decoder, geometryType, buffer) { if (this.getAttributeOptions('position').skipDequantization === true) { @@ -153,12 +197,7 @@ THREE.DRACOLoader.prototype = { /* * Example on how to retrieve mesh and attributes. */ - var numFaces, numPoints; - var numVertexCoordinates, numTextureCoordinates, numColorCoordinates; - var numAttributes; - var numColorCoordinateComponents = 3; - // For output basic geometry information. - var geometryInfoStr; + var numFaces; if (geometryType == dracoDecoder.TRIANGULAR_MESH) { numFaces = dracoGeometry.num_faces(); if (this.verbosity > 0) { @@ -167,20 +206,18 @@ THREE.DRACOLoader.prototype = { } else { numFaces = 0; } - numPoints = dracoGeometry.num_points(); - numVertexCoordinates = numPoints * 3; - numTextureCoordinates = numPoints * 2; - numColorCoordinates = numPoints * 3; - numAttributes = dracoGeometry.num_attributes(); + + 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()); } - // Get position attribute. Must exists. + // Verify if there is position attribute. var posAttId = decoder.GetAttributeId(dracoGeometry, - dracoDecoder.POSITION); + dracoDecoder.POSITION); if (posAttId == -1) { var errorMsg = 'THREE.DRACOLoader: No position attribute found.'; console.error(errorMsg); @@ -189,104 +226,39 @@ THREE.DRACOLoader.prototype = { throw new Error(errorMsg); } var posAttribute = decoder.GetAttribute(dracoGeometry, posAttId); - var posAttributeData = new dracoDecoder.DracoFloat32Array(); - decoder.GetAttributeFloatForAllPoints( - dracoGeometry, posAttribute, posAttributeData); - // Get color attributes if exists. - var colorAttId = decoder.GetAttributeId(dracoGeometry, - dracoDecoder.COLOR); - var colAttributeData; - if (colorAttId != -1) { - if (this.verbosity > 0) { - console.log('Loaded color attribute.'); - } - var colAttribute = decoder.GetAttribute(dracoGeometry, colorAttId); - if (colAttribute.num_components() === 4) { - numColorCoordinates = numPoints * 4; - numColorCoordinateComponents = 4; - } - colAttributeData = new dracoDecoder.DracoFloat32Array(); - decoder.GetAttributeFloatForAllPoints(dracoGeometry, colAttribute, - colAttributeData); - } - - // Get normal attributes if exists. - var normalAttId = - decoder.GetAttributeId(dracoGeometry, dracoDecoder.NORMAL); - var norAttributeData; - if (normalAttId != -1) { - if (this.verbosity > 0) { - console.log('Loaded normal attribute.'); - } - var norAttribute = decoder.GetAttribute(dracoGeometry, normalAttId); - norAttributeData = new dracoDecoder.DracoFloat32Array(); - decoder.GetAttributeFloatForAllPoints(dracoGeometry, norAttribute, - norAttributeData); - } - - // Get texture coord attributes if exists. - var texCoordAttId = - decoder.GetAttributeId(dracoGeometry, dracoDecoder.TEX_COORD); - var textCoordAttributeData; - if (texCoordAttId != -1) { - if (this.verbosity > 0) { - console.log('Loaded texture coordinate attribute.'); - } - var texCoordAttribute = decoder.GetAttribute(dracoGeometry, - texCoordAttId); - textCoordAttributeData = new dracoDecoder.DracoFloat32Array(); - decoder.GetAttributeFloatForAllPoints(dracoGeometry, - texCoordAttribute, - textCoordAttributeData); - } // Structure for converting to THREEJS geometry later. - var geometryBuffer = { - vertices: new Float32Array(numVertexCoordinates), - normals: new Float32Array(numVertexCoordinates), - uvs: new Float32Array(numTextureCoordinates), - colors: new Float32Array(numColorCoordinates) - }; - - for (var i = 0; i < numVertexCoordinates; i += 3) { - geometryBuffer.vertices[i] = posAttributeData.GetValue(i); - geometryBuffer.vertices[i + 1] = posAttributeData.GetValue(i + 1); - geometryBuffer.vertices[i + 2] = posAttributeData.GetValue(i + 2); - // Add normal. - if (normalAttId != -1) { - geometryBuffer.normals[i] = norAttributeData.GetValue(i); - geometryBuffer.normals[i + 1] = norAttributeData.GetValue(i + 1); - geometryBuffer.normals[i + 2] = norAttributeData.GetValue(i + 2); - } - } + var geometryBuffer = {}; + // Import data to Three JS geometry. + var geometry = new THREE.BufferGeometry(); - // Add color. - for (var i = 0; i < numColorCoordinates; i += 1) { - if (colorAttId != -1) { - // Draco colors are already normalized. - geometryBuffer.colors[i] = colAttributeData.GetValue(i); - } else { - // Default is white. This is faster than TypedArray.fill(). - geometryBuffer.colors[i] = 1.0; + // Add native Draco attribute type to geometry. + for (var attributeName in this.nativeAttributeMap) { + // The native attribute type is only used when no unique Id is + // provided. For example, loading .drc files. + if (this.attributeUniqueIdMap[attributeName] === undefined) { + 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, attribute, geometry, geometryBuffer); + } } } - // Add texture coordinates. - if (texCoordAttId != -1) { - for (var i = 0; i < numTextureCoordinates; i += 2) { - geometryBuffer.uvs[i] = textCoordAttributeData.GetValue(i); - geometryBuffer.uvs[i + 1] = textCoordAttributeData.GetValue(i + 1); - } + // Add attributes of user specified unique id. E.g. GLTF models. + for (var attributeName in this.attributeUniqueIdMap) { + var attributeId = this.attributeUniqueIdMap[attributeName]; + var attribute = decoder.GetAttributeByUniqueId(dracoGeometry, + attributeId); + this.addAttributeToGeometry(dracoDecoder, decoder, dracoGeometry, + attributeName, attribute, geometry, geometryBuffer); } - dracoDecoder.destroy(posAttributeData); - if (colorAttId != -1) - dracoDecoder.destroy(colAttributeData); - if (normalAttId != -1) - dracoDecoder.destroy(norAttributeData); - if (texCoordAttId != -1) - dracoDecoder.destroy(textCoordAttributeData); - // For mesh, we need to generate the faces. if (geometryType == dracoDecoder.TRIANGULAR_MESH) { if (this.drawMode === THREE.TriangleStripDrawMode) { @@ -313,16 +285,12 @@ THREE.DRACOLoader.prototype = { } } - // Import data to Three JS geometry. - var geometry = new THREE.BufferGeometry(); geometry.drawMode = this.drawMode; if (geometryType == dracoDecoder.TRIANGULAR_MESH) { geometry.setIndex(new(geometryBuffer.indices.length > 65535 ? THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute) (geometryBuffer.indices, 1)); } - geometry.addAttribute('position', - new THREE.Float32BufferAttribute(geometryBuffer.vertices, 3)); var posTransform = new dracoDecoder.AttributeQuantizationTransform(); if (posTransform.InitFromAttribute(posAttribute)) { // Quantized attribute. Store the quantization parameters into the @@ -338,18 +306,6 @@ THREE.DRACOLoader.prototype = { } } dracoDecoder.destroy(posTransform); - geometry.addAttribute('color', - new THREE.Float32BufferAttribute(geometryBuffer.colors, - numColorCoordinateComponents)); - if (normalAttId != -1) { - geometry.addAttribute('normal', - new THREE.Float32BufferAttribute(geometryBuffer.normals, 3)); - } - if (texCoordAttId != -1) { - geometry.addAttribute('uv', - new THREE.Float32BufferAttribute(geometryBuffer.uvs, 2)); - } - dracoDecoder.destroy(decoder); dracoDecoder.destroy(dracoGeometry); @@ -364,9 +320,9 @@ THREE.DRACOLoader.prototype = { }, isVersionSupported: function(version, callback) { - THREE.DRACOLoader.getDecoder(this, - function(decoder) { - callback(decoder.isVersionSupported(version)); + THREE.DRACOLoader.getDecoderModule() + .then( function ( module ) { + callback( module.decoder.isVersionSupported( version ) ); }); }, @@ -377,119 +333,109 @@ THREE.DRACOLoader.prototype = { } }; -// This function loads a JavaScript file and adds it to the page. "path" -// is the path to the JavaScript file. "onLoadFunc" is the function to be -// called when the JavaScript file has been loaded. -THREE.DRACOLoader.loadJavaScriptFile = function(path, onLoadFunc, - dracoDecoder) { - var head = document.getElementsByTagName('head')[0]; - var element = document.createElement('script'); - element.id = "decoder_script"; - element.type = 'text/javascript'; - element.src = path; - if (onLoadFunc !== null) { - element.onload = onLoadFunc(dracoDecoder); - } else { - element.onload = function(dracoDecoder) { - dracoDecoder.timeLoaded = performance.now(); - }; - } +THREE.DRACOLoader.decoderPath = './'; +THREE.DRACOLoader.decoderConfig = {}; +THREE.DRACOLoader.decoderModulePromise = null; - var previous_decoder_script = document.getElementById("decoder_script"); - if (previous_decoder_script !== null) { - previous_decoder_script.parentNode.removeChild(previous_decoder_script); - } - head.appendChild(element); -} - -THREE.DRACOLoader.loadWebAssemblyDecoder = function(dracoDecoder) { - dracoDecoder.dracoDecoderType['wasmBinaryFile'] = - dracoDecoder.dracoSrcPath + 'draco_decoder.wasm'; - var xhr = new XMLHttpRequest(); - xhr.open('GET', dracoDecoder.dracoSrcPath + 'draco_decoder.wasm', true); - xhr.responseType = 'arraybuffer'; - xhr.onload = function() { - // draco_wasm_wrapper.js must be loaded before DracoDecoderModule is - // created. The object passed into DracoDecoderModule() must contain a - // property with the name of wasmBinary and the value must be an - // ArrayBuffer containing the contents of the .wasm file. - dracoDecoder.dracoDecoderType['wasmBinary'] = xhr.response; - dracoDecoder.timeLoaded = performance.now(); - }; - xhr.send(null) -} - -// This function will test if the browser has support for WebAssembly. If -// it does it will download the WebAssembly Draco decoder, if not it will -// download the asmjs Draco decoder. -THREE.DRACOLoader.loadDracoDecoder = function(dracoDecoder) { - if (typeof WebAssembly !== 'object' || - dracoDecoder.dracoDecoderType.type === 'js') { - // No WebAssembly support - THREE.DRACOLoader.loadJavaScriptFile(dracoDecoder.dracoSrcPath + - 'draco_decoder.js', null, dracoDecoder); +/** + * 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.decoderModulePromise = null; + + // Reuse WASM binary. + if ( wasmBinary ) THREE.DRACOLoader.decoderConfig.wasmBinary = wasmBinary; +}; + + /** + * 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 { - THREE.DRACOLoader.loadJavaScriptFile(dracoDecoder.dracoSrcPath + - 'draco_wasm_wrapper.js', - function (dracoDecoder) { - THREE.DRACOLoader.loadWebAssemblyDecoder(dracoDecoder); - }, dracoDecoder); + // 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; +}; /** - * Creates and returns a singleton instance of the DracoDecoderModule. - * The module loading is done asynchronously for WebAssembly. Initialized module - * can be accessed through the callback function - * |onDracoDecoderModuleLoadedCallback|. + * @param {string} src + * @return {Promise} */ -THREE.DRACOLoader.getDecoder = (function() { - var decoder; - var decoderCreationCalled = false; - - return function(dracoDecoder, onDracoDecoderModuleLoadedCallback) { - if (typeof decoder !== 'undefined') { - // Module already initialized. - if (typeof onDracoDecoderModuleLoadedCallback !== 'undefined') { - onDracoDecoderModuleLoadedCallback(decoder); - } - } else { - if (typeof DracoDecoderModule === 'undefined') { - // Wait until the Draco decoder is loaded before starting the error - // timer. - if (dracoDecoder.timeLoaded > 0) { - var waitMs = performance.now() - dracoDecoder.timeLoaded; - - // After loading the Draco JavaScript decoder file, there is still - // some time before the DracoDecoderModule is defined. So start a - // loop to check when the DracoDecoderModule gets defined. If the - // time is hit throw an error. - if (waitMs > 5000) { - throw new Error( - 'THREE.DRACOLoader: DracoDecoderModule not found.'); - } - } - } else { - if (!decoderCreationCalled) { - decoderCreationCalled = true; - dracoDecoder.dracoDecoderType['onModuleLoaded'] = - function(module) { - if (typeof onDracoDecoderModuleLoadedCallback === - 'function') { - decoder = module; - } - }; - DracoDecoderModule(dracoDecoder.dracoDecoderType); - } - } - - // Either the DracoDecoderModule has not been defined or the decoder - // has not been created yet. Call getDecoder() again. - setTimeout(function() { - THREE.DRACOLoader.getDecoder(dracoDecoder, - onDracoDecoderModuleLoadedCallback); - }, 10); - } - }; +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 ); + }); +}; diff --git a/examples/webgl_loader_draco.html b/examples/webgl_loader_draco.html index c087eb196a..769485683b 100644 --- a/examples/webgl_loader_draco.html +++ b/examples/webgl_loader_draco.html @@ -43,8 +43,10 @@ var camera, scene, renderer; - // Global Draco decoder type. - var dracoLoader = new THREE.DRACOLoader('js/loaders/draco/'); + // Configure and create Draco decoder. + THREE.DRACOLoader.setDecoderPath('js/loaders/draco/'); + THREE.DRACOLoader.setDecoderConfig({type: 'js'}); + var dracoLoader = new THREE.DRACOLoader(); init(); animate(); -- GitLab