diff --git a/examples/js/loaders/3DMLoader.js b/examples/js/loaders/3DMLoader.js index e317a11f0c6f18..74016c81cca9f4 100644 --- a/examples/js/loaders/3DMLoader.js +++ b/examples/js/loaders/3DMLoader.js @@ -1,40 +1,43 @@ ( function () { - var Rhino3dmLoader = function ( manager ) { - - THREE.Loader.call( this, manager ); - this.libraryPath = ''; - this.libraryPending = null; - this.libraryBinary = null; - this.libraryConfig = {}; - this.url = ''; - this.workerLimit = 4; - this.workerPool = []; - this.workerNextTaskID = 1; - this.workerSourceURL = ''; - this.workerConfig = {}; - this.materials = []; - - }; - - Rhino3dmLoader.taskCache = new WeakMap(); - Rhino3dmLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: Rhino3dmLoader, - setLibraryPath: function ( path ) { + const _taskCache = new WeakMap(); + + class Rhino3dmLoader extends THREE.Loader { + + constructor( manager ) { + + super( manager ); + this.libraryPath = ''; + this.libraryPending = null; + this.libraryBinary = null; + this.libraryConfig = {}; + this.url = ''; + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ''; + this.workerConfig = {}; + this.materials = []; + + } + + setLibraryPath( path ) { this.libraryPath = path; return this; - }, - setWorkerLimit: function ( workerLimit ) { + } + + setWorkerLimit( workerLimit ) { this.workerLimit = workerLimit; return this; - }, - load: function ( url, onLoad, onProgress, onError ) { + } + + load( url, onLoad, onProgress, onError ) { - var loader = new THREE.FileLoader( this.manager ); + const loader = new THREE.FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( this.requestHeader ); @@ -43,9 +46,10 @@ // Check for an existing task using this buffer. A transferred buffer cannot be transferred // again from this thread. - if ( Rhino3dmLoader.taskCache.has( buffer ) ) { + if ( _taskCache.has( buffer ) ) { + + const cachedTask = _taskCache.get( buffer ); - var cachedTask = Rhino3dmLoader.taskCache.get( buffer ); return cachedTask.promise.then( onLoad ).catch( onError ); } @@ -54,13 +58,15 @@ }, onProgress, onError ); - }, - debug: function () { + } + + debug() { console.log( 'Task load: ', this.workerPool.map( worker => worker._taskLoad ) ); - }, - decodeObjects: function ( buffer, url ) { + } + + decodeObjects( buffer, url ) { var worker; var taskID; @@ -99,19 +105,22 @@ } ); // Cache the task result. - Rhino3dmLoader.taskCache.set( buffer, { + _taskCache.set( buffer, { url: url, promise: objectPending } ); + return objectPending; - }, - parse: function ( data, onLoad, onError ) { + } + + parse( data, onLoad, onError ) { this.decodeObjects( data, '' ).then( onLoad ).catch( onError ); - }, - _compareMaterials: function ( material ) { + } + + _compareMaterials( material ) { var mat = {}; mat.name = material.name; @@ -143,8 +152,9 @@ this.materials.push( material ); return material; - }, - _createMaterial: function ( material ) { + } + + _createMaterial( material ) { if ( material === undefined ) { @@ -213,8 +223,9 @@ return mat; - }, - _createGeometry: function ( data ) { + } + + _createGeometry( data ) { // console.log(data); var object = new THREE.Object3D(); @@ -342,8 +353,9 @@ object.userData[ 'materials' ] = this.materials; return object; - }, - _createObject: function ( obj, mat ) { + } + + _createObject( obj, mat ) { var loader = new THREE.BufferGeometryLoader(); var attributes = obj.attributes; @@ -545,8 +557,9 @@ } - }, - _initLibrary: function () { + } + + _initLibrary() { if ( ! this.libraryPending ) { @@ -571,7 +584,7 @@ //this.libraryBinary = binaryContent; this.libraryConfig.wasmBinary = binaryContent; - var fn = Rhino3dmLoader.Rhino3dmWorker.toString(); + var fn = Rhino3dmWorker.toString(); var body = [ '/* rhino3dm.js */', jsContent, '/* worker */', fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) ].join( '\n' ); this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) ); @@ -581,8 +594,9 @@ return this.libraryPending; - }, - _getWorker: function ( taskCost ) { + } + + _getWorker( taskCost ) { return this._initLibrary().then( () => { @@ -638,15 +652,17 @@ } ); - }, - _releaseTask: function ( worker, taskID ) { + } + + _releaseTask( worker, taskID ) { worker._taskLoad -= worker._taskCosts[ taskID ]; delete worker._callbacks[ taskID ]; delete worker._taskCosts[ taskID ]; - }, - dispose: function () { + } + + dispose() { for ( var i = 0; i < this.workerPool.length; ++ i ) { @@ -658,10 +674,12 @@ return this; } - } ); + + } /* WEB WORKER */ - Rhino3dmLoader.Rhino3dmWorker = function () { + + function Rhino3dmWorker() { var libraryPending; var libraryConfig; @@ -1257,7 +1275,7 @@ } - }; + } THREE.Rhino3dmLoader = Rhino3dmLoader; diff --git a/examples/js/loaders/DRACOLoader.js b/examples/js/loaders/DRACOLoader.js index 0227dea1f8b926..5ca41b903b4726 100644 --- a/examples/js/loaders/DRACOLoader.js +++ b/examples/js/loaders/DRACOLoader.js @@ -1,82 +1,66 @@ ( function () { - var DRACOLoader = function ( manager ) { - - THREE.Loader.call( this, 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' - }; + const _taskCache = new WeakMap(); + + class DRACOLoader extends THREE.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' + }; - }; + } - DRACOLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: DRACOLoader, - setDecoderPath: function ( path ) { + setDecoderPath( path ) { this.decoderPath = path; return this; - }, - setDecoderConfig: function ( config ) { + } + + setDecoderConfig( config ) { this.decoderConfig = config; return this; - }, - setWorkerLimit: function ( workerLimit ) { + } + + setWorkerLimit( workerLimit ) { this.workerLimit = workerLimit; return this; - }, - - /** @deprecated */ - setVerbosity: function () { - - console.warn( 'THREE.DRACOLoader: The .setVerbosity() method has been removed.' ); - - }, - - /** @deprecated */ - setDrawMode: function () { - - console.warn( 'THREE.DRACOLoader: The .setDrawMode() method has been removed.' ); - - }, - - /** @deprecated */ - setSkipDequantization: function () { - - console.warn( 'THREE.DRACOLoader: The .setSkipDequantization() method has been removed.' ); + } - }, - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var loader = new THREE.FileLoader( this.manager ); + const loader = new THREE.FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( this.requestHeader ); loader.setWithCredentials( this.withCredentials ); loader.load( url, buffer => { - var taskConfig = { + const taskConfig = { attributeIDs: this.defaultAttributeIDs, attributeTypes: this.defaultAttributeTypes, useUniqueIDs: false @@ -85,27 +69,29 @@ }, onProgress, onError ); - }, - + } /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ - decodeDracoFile: function ( buffer, callback, attributeIDs, attributeTypes ) { - var taskConfig = { + + decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) { + + const taskConfig = { attributeIDs: attributeIDs || this.defaultAttributeIDs, attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !! attributeIDs }; this.decodeGeometry( buffer, taskConfig ).then( callback ); - }, - decodeGeometry: function ( buffer, taskConfig ) { + } + + decodeGeometry( buffer, taskConfig ) { // TODO: For backward-compatibility, support 'attributeTypes' objects containing // references (rather than names) to typed array constructors. These must be // serialized before sending them to the worker. - for ( var attribute in taskConfig.attributeTypes ) { + for ( const attribute in taskConfig.attributeTypes ) { - var type = taskConfig.attributeTypes[ attribute ]; + const type = taskConfig.attributeTypes[ attribute ]; if ( type.BYTES_PER_ELEMENT !== undefined ) { @@ -116,12 +102,12 @@ } // - var taskKey = JSON.stringify( taskConfig ); // Check for an existing task using this buffer. A transferred buffer cannot be transferred + const taskKey = JSON.stringify( taskConfig ); // Check for an existing task using this buffer. A transferred buffer cannot be transferred // again from this thread. - if ( DRACOLoader.taskCache.has( buffer ) ) { + if ( _taskCache.has( buffer ) ) { - var cachedTask = DRACOLoader.taskCache.get( buffer ); + const cachedTask = _taskCache.get( buffer ); if ( cachedTask.key === taskKey ) { @@ -140,12 +126,12 @@ } // - var worker; - var taskID = this.workerNextTaskID ++; - var taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance + 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. - var geometryPending = this._getWorker( taskID, taskCost ).then( _worker => { + const geometryPending = this._getWorker( taskID, taskCost ).then( _worker => { worker = _worker; return new Promise( ( resolve, reject ) => { @@ -177,16 +163,18 @@ } ); // Cache the task result. - DRACOLoader.taskCache.set( buffer, { + _taskCache.set( buffer, { key: taskKey, promise: geometryPending } ); + return geometryPending; - }, - _createGeometry: function ( geometryData ) { + } + + _createGeometry( geometryData ) { - var geometry = new THREE.BufferGeometry(); + const geometry = new THREE.BufferGeometry(); if ( geometryData.index ) { @@ -194,22 +182,23 @@ } - for ( var i = 0; i < geometryData.attributes.length; i ++ ) { + for ( let i = 0; i < geometryData.attributes.length; i ++ ) { - var attribute = geometryData.attributes[ i ]; - var name = attribute.name; - var array = attribute.array; - var itemSize = attribute.itemSize; + const attribute = geometryData.attributes[ i ]; + const name = attribute.name; + const array = attribute.array; + const itemSize = attribute.itemSize; geometry.setAttribute( name, new THREE.BufferAttribute( array, itemSize ) ); } return geometry; - }, - _loadLibrary: function ( url, responseType ) { + } + + _loadLibrary( url, responseType ) { - var loader = new THREE.FileLoader( this.manager ); + const loader = new THREE.FileLoader( this.manager ); loader.setPath( this.decoderPath ); loader.setResponseType( responseType ); loader.setWithCredentials( this.withCredentials ); @@ -219,19 +208,21 @@ } ); - }, - preload: function () { + } + + preload() { this._initDecoder(); return this; - }, - _initDecoder: function () { + } + + _initDecoder() { if ( this.decoderPending ) return this.decoderPending; - var useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; - var librariesPending = []; + const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; + const librariesPending = []; if ( useJS ) { @@ -246,7 +237,7 @@ this.decoderPending = Promise.all( librariesPending ).then( libraries => { - var jsContent = libraries[ 0 ]; + const jsContent = libraries[ 0 ]; if ( ! useJS ) { @@ -254,21 +245,22 @@ } - var fn = DRACOLoader.DRACOWorker.toString(); - var body = [ '/* draco decoder */', jsContent, '', '/* worker */', fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) ].join( '\n' ); + 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: function ( taskID, taskCost ) { + } + + _getWorker( taskID, taskCost ) { return this._initDecoder().then( () => { if ( this.workerPool.length < this.workerLimit ) { - var worker = new Worker( this.workerSourceURL ); + const worker = new Worker( this.workerSourceURL ); worker._callbacks = {}; worker._taskCosts = {}; worker._taskLoad = 0; @@ -279,7 +271,7 @@ worker.onmessage = function ( e ) { - var message = e.data; + const message = e.data; switch ( message.type ) { @@ -312,29 +304,32 @@ } - var worker = this.workerPool[ this.workerPool.length - 1 ]; + const worker = this.workerPool[ this.workerPool.length - 1 ]; worker._taskCosts[ taskID ] = taskCost; worker._taskLoad += taskCost; return worker; } ); - }, - _releaseTask: function ( worker, taskID ) { + } + + _releaseTask( worker, taskID ) { worker._taskLoad -= worker._taskCosts[ taskID ]; delete worker._callbacks[ taskID ]; delete worker._taskCosts[ taskID ]; - }, - debug: function () { + } + + debug() { console.log( 'Task load: ', this.workerPool.map( worker => worker._taskLoad ) ); - }, - dispose: function () { + } + + dispose() { - for ( var i = 0; i < this.workerPool.length; ++ i ) { + for ( let i = 0; i < this.workerPool.length; ++ i ) { this.workerPool[ i ].terminate(); @@ -344,17 +339,19 @@ return this; } - } ); + + } /* WEB WORKER */ - DRACOLoader.DRACOWorker = function () { - var decoderConfig; - var decoderPending; + function DRACOWorker() { + + let decoderConfig; + let decoderPending; onmessage = function ( e ) { - var message = e.data; + const message = e.data; switch ( message.type ) { @@ -379,19 +376,19 @@ break; case 'decode': - var buffer = message.buffer; - var taskConfig = message.taskConfig; + const buffer = message.buffer; + const taskConfig = message.taskConfig; decoderPending.then( module => { - var draco = module.draco; - var decoder = new draco.Decoder(); - var decoderBuffer = new draco.DecoderBuffer(); + const draco = module.draco; + const decoder = new draco.Decoder(); + const decoderBuffer = new draco.DecoderBuffer(); decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength ); try { - var geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig ); - var buffers = geometry.attributes.map( attr => attr.array.buffer ); + const geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig ); + const buffers = geometry.attributes.map( attr => attr.array.buffer ); if ( geometry.index ) buffers.push( geometry.index.array.buffer ); self.postMessage( { type: 'decode', @@ -424,11 +421,11 @@ function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) { - var attributeIDs = taskConfig.attributeIDs; - var attributeTypes = taskConfig.attributeTypes; - var dracoGeometry; - var decodingStatus; - var geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; + let dracoGeometry; + let decodingStatus; + const geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); if ( geometryType === draco.TRIANGULAR_MESH ) { @@ -452,16 +449,16 @@ } - var geometry = { + const geometry = { index: null, attributes: [] }; // Gather all vertex attributes. - for ( var attributeName in attributeIDs ) { + for ( const attributeName in attributeIDs ) { - var attributeType = self[ attributeTypes[ attributeName ] ]; - var attribute; - var attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs + 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. @@ -497,14 +494,14 @@ function decodeIndex( draco, decoder, dracoGeometry ) { - var numFaces = dracoGeometry.num_faces(); - var numIndices = numFaces * 3; - var byteLength = numIndices * 4; + const numFaces = dracoGeometry.num_faces(); + const numIndices = numFaces * 3; + const byteLength = numIndices * 4; - var ptr = draco._malloc( byteLength ); + const ptr = draco._malloc( byteLength ); decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr ); - var index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); + const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); draco._free( ptr ); @@ -517,16 +514,16 @@ function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { - var numComponents = attribute.num_components(); - var numPoints = dracoGeometry.num_points(); - var numValues = numPoints * numComponents; - var byteLength = numValues * attributeType.BYTES_PER_ELEMENT; - var dataType = getDracoDataType( draco, attributeType ); + 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 ); - var ptr = draco._malloc( byteLength ); + const ptr = draco._malloc( byteLength ); decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr ); - var array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); + const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); draco._free( ptr ); @@ -567,42 +564,7 @@ } - }; - - DRACOLoader.taskCache = new WeakMap(); - /** Deprecated static methods */ - - /** @deprecated */ - - DRACOLoader.setDecoderPath = function () { - - console.warn( 'THREE.DRACOLoader: The .setDecoderPath() method has been removed. Use instance methods.' ); - - }; - /** @deprecated */ - - - DRACOLoader.setDecoderConfig = function () { - - console.warn( 'THREE.DRACOLoader: The .setDecoderConfig() method has been removed. Use instance methods.' ); - - }; - /** @deprecated */ - - - DRACOLoader.releaseDecoderModule = function () { - - console.warn( 'THREE.DRACOLoader: The .releaseDecoderModule() method has been removed. Use instance methods.' ); - - }; - /** @deprecated */ - - - DRACOLoader.getDecoderModule = function () { - - console.warn( 'THREE.DRACOLoader: The .getDecoderModule() method has been removed. Use instance methods.' ); - - }; + } THREE.DRACOLoader = DRACOLoader; diff --git a/examples/js/loaders/EXRLoader.js b/examples/js/loaders/EXRLoader.js index 07821d383913e0..20a574bf66b3f4 100644 --- a/examples/js/loaders/EXRLoader.js +++ b/examples/js/loaders/EXRLoader.js @@ -67,16 +67,16 @@ // /////////////////////////////////////////////////////////////////////////// // // End of OpenEXR license ------------------------------------------------- - var EXRLoader = function ( manager ) { + class EXRLoader extends THREE.DataTextureLoader { - THREE.DataTextureLoader.call( this, manager ); - this.type = THREE.FloatType; + constructor( manager ) { - }; + super( manager ); + this.type = THREE.FloatType; - EXRLoader.prototype = Object.assign( Object.create( THREE.DataTextureLoader.prototype ), { - constructor: EXRLoader, - parse: function ( buffer ) { + } + + parse( buffer ) { const USHORT_RANGE = 1 << 16; const BITMAP_SIZE = USHORT_RANGE >> 3; @@ -2155,14 +2155,16 @@ type: this.type }; - }, - setDataType: function ( value ) { + } + + setDataType( value ) { this.type = value; return this; - }, - load: function ( url, onLoad, onProgress, onError ) { + } + + load( url, onLoad, onProgress, onError ) { function onLoadCallback( texture, texData ) { @@ -2191,10 +2193,11 @@ } - return THREE.DataTextureLoader.prototype.load.call( this, url, onLoadCallback, onProgress, onError ); + return super.load( url, onLoadCallback, onProgress, onError ); } - } ); + + } THREE.EXRLoader = EXRLoader; diff --git a/examples/js/loaders/FBXLoader.js b/examples/js/loaders/FBXLoader.js index 962936a4dbbcf9..99219e9f577ced 100644 --- a/examples/js/loaders/FBXLoader.js +++ b/examples/js/loaders/FBXLoader.js @@ -16,189 +16,190 @@ * https://code.blender.org/2013/08/fbx-binary-file-format-specification/ */ - var FBXLoader = function () { + let fbxTree; + let connections; + let sceneGraph; - var fbxTree; - var connections; - var sceneGraph; + class FBXLoader extends THREE.Loader { - function FBXLoader( manager ) { + constructor( manager ) { - THREE.Loader.call( this, manager ); + super( manager ); } - FBXLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: FBXLoader, - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; - var path = scope.path === '' ? THREE.LoaderUtils.extractUrlBase( url ) : scope.path; - var loader = new THREE.FileLoader( this.manager ); - loader.setPath( scope.path ); - loader.setResponseType( 'arraybuffer' ); - loader.setRequestHeader( scope.requestHeader ); - loader.setWithCredentials( scope.withCredentials ); - loader.load( url, function ( buffer ) { + const scope = this; + const path = scope.path === '' ? THREE.LoaderUtils.extractUrlBase( url ) : scope.path; + const loader = new THREE.FileLoader( this.manager ); + loader.setPath( scope.path ); + loader.setResponseType( 'arraybuffer' ); + loader.setRequestHeader( scope.requestHeader ); + loader.setWithCredentials( scope.withCredentials ); + loader.load( url, function ( buffer ) { - try { + try { - onLoad( scope.parse( buffer, path ) ); + onLoad( scope.parse( buffer, path ) ); - } catch ( e ) { + } catch ( e ) { - if ( onError ) { + if ( onError ) { - onError( e ); + onError( e ); - } else { + } else { - console.error( e ); + console.error( e ); - } + } - scope.manager.itemError( url ); + scope.manager.itemError( url ); - } + } - }, onProgress, onError ); + }, onProgress, onError ); - }, - parse: function ( FBXBuffer, path ) { + } - if ( isFbxFormatBinary( FBXBuffer ) ) { + parse( FBXBuffer, path ) { - fbxTree = new BinaryParser().parse( FBXBuffer ); + if ( isFbxFormatBinary( FBXBuffer ) ) { - } else { + fbxTree = new BinaryParser().parse( FBXBuffer ); - var FBXText = convertArrayBufferToString( FBXBuffer ); + } else { - if ( ! isFbxFormatASCII( FBXText ) ) { + const FBXText = convertArrayBufferToString( FBXBuffer ); - throw new Error( 'THREE.FBXLoader: Unknown format.' ); + if ( ! isFbxFormatASCII( FBXText ) ) { - } + throw new Error( 'THREE.FBXLoader: Unknown format.' ); - if ( getFbxVersion( FBXText ) < 7000 ) { + } - throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); + if ( getFbxVersion( FBXText ) < 7000 ) { - } + throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); - fbxTree = new TextParser().parse( FBXText ); + } - } // console.log( fbxTree ); + fbxTree = new TextParser().parse( FBXText ); + } // console.log( fbxTree ); - var textureLoader = new THREE.TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); - return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree ); - } - } ); // Parse the FBXTree object returned by the BinaryParser or TextParser and return a THREE.Group + const textureLoader = new THREE.TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); + return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree ); + + } + + } // Parse the FBXTree object returned by the BinaryParser or TextParser and return a THREE.Group + + + class FBXTreeParser { - function FBXTreeParser( textureLoader, manager ) { + constructor( textureLoader, manager ) { this.textureLoader = textureLoader; this.manager = manager; } - FBXTreeParser.prototype = { - constructor: FBXTreeParser, - parse: function () { + parse() { - connections = this.parseConnections(); - var images = this.parseImages(); - var textures = this.parseTextures( images ); - var materials = this.parseMaterials( textures ); - var deformers = this.parseDeformers(); - var geometryMap = new GeometryParser().parse( deformers ); - this.parseScene( deformers, geometryMap, materials ); - return sceneGraph; + connections = this.parseConnections(); + const images = this.parseImages(); + const textures = this.parseTextures( images ); + const materials = this.parseMaterials( textures ); + const deformers = this.parseDeformers(); + const geometryMap = new GeometryParser().parse( deformers ); + this.parseScene( deformers, geometryMap, materials ); + return sceneGraph; - }, - // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) - // and details the connection type - parseConnections: function () { + } // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) + // and details the connection type - var connectionMap = new Map(); - if ( 'Connections' in fbxTree ) { + parseConnections() { - var rawConnections = fbxTree.Connections.connections; - rawConnections.forEach( function ( rawConnection ) { + const connectionMap = new Map(); - var fromID = rawConnection[ 0 ]; - var toID = rawConnection[ 1 ]; - var relationship = rawConnection[ 2 ]; + if ( 'Connections' in fbxTree ) { - if ( ! connectionMap.has( fromID ) ) { + const rawConnections = fbxTree.Connections.connections; + rawConnections.forEach( function ( rawConnection ) { - connectionMap.set( fromID, { - parents: [], - children: [] - } ); + const fromID = rawConnection[ 0 ]; + const toID = rawConnection[ 1 ]; + const relationship = rawConnection[ 2 ]; - } + if ( ! connectionMap.has( fromID ) ) { - var parentRelationship = { - ID: toID, - relationship: relationship - }; - connectionMap.get( fromID ).parents.push( parentRelationship ); + connectionMap.set( fromID, { + parents: [], + children: [] + } ); - if ( ! connectionMap.has( toID ) ) { + } - connectionMap.set( toID, { - parents: [], - children: [] - } ); + const parentRelationship = { + ID: toID, + relationship: relationship + }; + connectionMap.get( fromID ).parents.push( parentRelationship ); - } + if ( ! connectionMap.has( toID ) ) { - var childRelationship = { - ID: fromID, - relationship: relationship - }; - connectionMap.get( toID ).children.push( childRelationship ); + connectionMap.set( toID, { + parents: [], + children: [] + } ); - } ); + } - } + const childRelationship = { + ID: fromID, + relationship: relationship + }; + connectionMap.get( toID ).children.push( childRelationship ); + + } ); - return connectionMap; + } - }, - // Parse FBXTree.Objects.Video for embedded image data - // These images are connected to textures in FBXTree.Objects.Textures - // via FBXTree.Connections. - parseImages: function () { + return connectionMap; - var images = {}; - var blobs = {}; + } // Parse FBXTree.Objects.Video for embedded image data + // These images are connected to textures in FBXTree.Objects.Textures + // via FBXTree.Connections. - if ( 'Video' in fbxTree.Objects ) { - var videoNodes = fbxTree.Objects.Video; + parseImages() { - for ( var nodeID in videoNodes ) { + const images = {}; + const blobs = {}; - var videoNode = videoNodes[ nodeID ]; - var id = parseInt( nodeID ); - images[ id ] = videoNode.RelativeFilename || videoNode.Filename; // raw image data is in videoNode.Content + if ( 'Video' in fbxTree.Objects ) { - if ( 'Content' in videoNode ) { + const videoNodes = fbxTree.Objects.Video; - var arrayBufferContent = videoNode.Content instanceof ArrayBuffer && videoNode.Content.byteLength > 0; - var base64Content = typeof videoNode.Content === 'string' && videoNode.Content !== ''; + for ( const nodeID in videoNodes ) { - if ( arrayBufferContent || base64Content ) { + const videoNode = videoNodes[ nodeID ]; + const id = parseInt( nodeID ); + images[ id ] = videoNode.RelativeFilename || videoNode.Filename; // raw image data is in videoNode.Content - var image = this.parseImage( videoNodes[ nodeID ] ); - blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image; + if ( 'Content' in videoNode ) { - } + const arrayBufferContent = videoNode.Content instanceof ArrayBuffer && videoNode.Content.byteLength > 0; + const base64Content = typeof videoNode.Content === 'string' && videoNode.Content !== ''; + + if ( arrayBufferContent || base64Content ) { + + const image = this.parseImage( videoNodes[ nodeID ] ); + blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image; } @@ -206,3582 +207,3669 @@ } - for ( var id in images ) { + } - var filename = images[ id ]; - if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ]; else images[ id ] = images[ id ].split( '\\' ).pop(); + for ( const id in images ) { - } + const filename = images[ id ]; + if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ]; else images[ id ] = images[ id ].split( '\\' ).pop(); - return images; + } - }, - // Parse embedded image data in FBXTree.Video.Content - parseImage: function ( videoNode ) { + return images; - var content = videoNode.Content; - var fileName = videoNode.RelativeFilename || videoNode.Filename; - var extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); - var type; + } // Parse embedded image data in FBXTree.Video.Content - switch ( extension ) { - case 'bmp': - type = 'image/bmp'; - break; + parseImage( videoNode ) { - case 'jpg': - case 'jpeg': - type = 'image/jpeg'; - break; + const content = videoNode.Content; + const fileName = videoNode.RelativeFilename || videoNode.Filename; + const extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); + let type; - case 'png': - type = 'image/png'; - break; + switch ( extension ) { - case 'tif': - type = 'image/tiff'; - break; + case 'bmp': + type = 'image/bmp'; + break; - case 'tga': - if ( this.manager.getHandler( '.tga' ) === null ) { + case 'jpg': + case 'jpeg': + type = 'image/jpeg'; + break; - console.warn( 'FBXLoader: TGA loader not found, skipping ', fileName ); + case 'png': + type = 'image/png'; + break; - } + case 'tif': + type = 'image/tiff'; + break; - type = 'image/tga'; - break; + case 'tga': + if ( this.manager.getHandler( '.tga' ) === null ) { - default: - console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); - return; + console.warn( 'FBXLoader: TGA loader not found, skipping ', fileName ); - } + } - if ( typeof content === 'string' ) { + type = 'image/tga'; + break; - // ASCII format - return 'data:' + type + ';base64,' + content; + default: + console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); + return; - } else { + } - // Binary Format - var array = new Uint8Array( content ); - return window.URL.createObjectURL( new Blob( [ array ], { - type: type - } ) ); + if ( typeof content === 'string' ) { - } + // ASCII format + return 'data:' + type + ';base64,' + content; - }, - // Parse nodes in FBXTree.Objects.Texture - // These contain details such as UV scaling, cropping, rotation etc and are connected - // to images in FBXTree.Objects.Video - parseTextures: function ( images ) { + } else { + + // Binary Format + const array = new Uint8Array( content ); + return window.URL.createObjectURL( new Blob( [ array ], { + type: type + } ) ); - var textureMap = new Map(); + } - if ( 'Texture' in fbxTree.Objects ) { + } // Parse nodes in FBXTree.Objects.Texture + // These contain details such as UV scaling, cropping, rotation etc and are connected + // to images in FBXTree.Objects.Video - var textureNodes = fbxTree.Objects.Texture; - for ( var nodeID in textureNodes ) { + parseTextures( images ) { - var texture = this.parseTexture( textureNodes[ nodeID ], images ); - textureMap.set( parseInt( nodeID ), texture ); + const textureMap = new Map(); - } + if ( 'Texture' in fbxTree.Objects ) { + + const textureNodes = fbxTree.Objects.Texture; + + for ( const nodeID in textureNodes ) { + + const texture = this.parseTexture( textureNodes[ nodeID ], images ); + textureMap.set( parseInt( nodeID ), texture ); } - return textureMap; + } - }, - // Parse individual node in FBXTree.Objects.Texture - parseTexture: function ( textureNode, images ) { + return textureMap; - var texture = this.loadTexture( textureNode, images ); - texture.ID = textureNode.id; - texture.name = textureNode.attrName; - var wrapModeU = textureNode.WrapModeU; - var wrapModeV = textureNode.WrapModeV; - var valueU = wrapModeU !== undefined ? wrapModeU.value : 0; - var valueV = wrapModeV !== undefined ? wrapModeV.value : 0; // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a - // 0: repeat(default), 1: clamp + } // Parse individual node in FBXTree.Objects.Texture - texture.wrapS = valueU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; - texture.wrapT = valueV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; - if ( 'Scaling' in textureNode ) { + parseTexture( textureNode, images ) { - var values = textureNode.Scaling.value; - texture.repeat.x = values[ 0 ]; - texture.repeat.y = values[ 1 ]; + const texture = this.loadTexture( textureNode, images ); + texture.ID = textureNode.id; + texture.name = textureNode.attrName; + const wrapModeU = textureNode.WrapModeU; + const wrapModeV = textureNode.WrapModeV; + const valueU = wrapModeU !== undefined ? wrapModeU.value : 0; + const valueV = wrapModeV !== undefined ? wrapModeV.value : 0; // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a + // 0: repeat(default), 1: clamp - } + texture.wrapS = valueU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; + texture.wrapT = valueV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; - return texture; + if ( 'Scaling' in textureNode ) { - }, - // load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader - loadTexture: function ( textureNode, images ) { + const values = textureNode.Scaling.value; + texture.repeat.x = values[ 0 ]; + texture.repeat.y = values[ 1 ]; - var fileName; - var currentPath = this.textureLoader.path; - var children = connections.get( textureNode.id ).children; + } - if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) { + return texture; - fileName = images[ children[ 0 ].ID ]; + } // load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader - if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { - this.textureLoader.setPath( undefined ); + loadTexture( textureNode, images ) { - } + let fileName; + const currentPath = this.textureLoader.path; + const children = connections.get( textureNode.id ).children; - } + if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) { - var texture; - var extension = textureNode.FileName.slice( - 3 ).toLowerCase(); + fileName = images[ children[ 0 ].ID ]; - if ( extension === 'tga' ) { + if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { - var loader = this.manager.getHandler( '.tga' ); + this.textureLoader.setPath( undefined ); - if ( loader === null ) { + } - console.warn( 'FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename ); - texture = new THREE.Texture(); + } - } else { + let texture; + const extension = textureNode.FileName.slice( - 3 ).toLowerCase(); - texture = loader.load( fileName ); + if ( extension === 'tga' ) { - } + const loader = this.manager.getHandler( '.tga' ); - } else if ( extension === 'psd' ) { + if ( loader === null ) { - console.warn( 'FBXLoader: PSD textures are not supported, creating placeholder texture for', textureNode.RelativeFilename ); + console.warn( 'FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename ); texture = new THREE.Texture(); } else { - texture = this.textureLoader.load( fileName ); + texture = loader.load( fileName ); } - this.textureLoader.setPath( currentPath ); - return texture; + } else if ( extension === 'psd' ) { + + console.warn( 'FBXLoader: PSD textures are not supported, creating placeholder texture for', textureNode.RelativeFilename ); + texture = new THREE.Texture(); - }, - // Parse nodes in FBXTree.Objects.Material - parseMaterials: function ( textureMap ) { + } else { - var materialMap = new Map(); + texture = this.textureLoader.load( fileName ); - if ( 'Material' in fbxTree.Objects ) { + } - var materialNodes = fbxTree.Objects.Material; + this.textureLoader.setPath( currentPath ); + return texture; - for ( var nodeID in materialNodes ) { + } // Parse nodes in FBXTree.Objects.Material - var material = this.parseMaterial( materialNodes[ nodeID ], textureMap ); - if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); - } + parseMaterials( textureMap ) { + + const materialMap = new Map(); + + if ( 'Material' in fbxTree.Objects ) { + + const materialNodes = fbxTree.Objects.Material; + + for ( const nodeID in materialNodes ) { + + const material = this.parseMaterial( materialNodes[ nodeID ], textureMap ); + if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); } - return materialMap; + } + + return materialMap; + + } // Parse single node in FBXTree.Objects.Material + // Materials are connected to texture maps in FBXTree.Objects.Textures + // FBX format currently only supports Lambert and Phong shading models + + + parseMaterial( materialNode, textureMap ) { + + const ID = materialNode.id; + const name = materialNode.attrName; + let type = materialNode.ShadingModel; // Case where FBX wraps shading model in property object. + + if ( typeof type === 'object' ) { + + type = type.value; + + } // Ignore unused materials which don't have any connections. + - }, - // Parse single node in FBXTree.Objects.Material - // Materials are connected to texture maps in FBXTree.Objects.Textures - // FBX format currently only supports Lambert and Phong shading models - parseMaterial: function ( materialNode, textureMap ) { + if ( ! connections.has( ID ) ) return null; + const parameters = this.parseParameters( materialNode, textureMap, ID ); + let material; - var ID = materialNode.id; - var name = materialNode.attrName; - var type = materialNode.ShadingModel; // Case where FBX wraps shading model in property object. + switch ( type.toLowerCase() ) { - if ( typeof type === 'object' ) { + case 'phong': + material = new THREE.MeshPhongMaterial(); + break; + + case 'lambert': + material = new THREE.MeshLambertMaterial(); + break; + + default: + console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to THREE.MeshPhongMaterial.', type ); + material = new THREE.MeshPhongMaterial(); + break; + + } + + material.setValues( parameters ); + material.name = name; + return material; + + } // Parse FBX material and return parameters suitable for a three.js material + // Also parse the texture map and return any textures associated with the material + + + parseParameters( materialNode, textureMap, ID ) { + + const parameters = {}; - type = type.value; + if ( materialNode.BumpFactor ) { - } // Ignore unused materials which don't have any connections. + parameters.bumpScale = materialNode.BumpFactor.value; + + } + + if ( materialNode.Diffuse ) { + + parameters.color = new THREE.Color().fromArray( materialNode.Diffuse.value ); + + } else if ( materialNode.DiffuseColor && ( materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB' ) ) { + + // The blender exporter exports diffuse here instead of in materialNode.Diffuse + parameters.color = new THREE.Color().fromArray( materialNode.DiffuseColor.value ); + + } + + if ( materialNode.DisplacementFactor ) { + + parameters.displacementScale = materialNode.DisplacementFactor.value; + + } + if ( materialNode.Emissive ) { - if ( ! connections.has( ID ) ) return null; - var parameters = this.parseParameters( materialNode, textureMap, ID ); - var material; + parameters.emissive = new THREE.Color().fromArray( materialNode.Emissive.value ); + + } else if ( materialNode.EmissiveColor && ( materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB' ) ) { + + // The blender exporter exports emissive color here instead of in materialNode.Emissive + parameters.emissive = new THREE.Color().fromArray( materialNode.EmissiveColor.value ); + + } + + if ( materialNode.EmissiveFactor ) { + + parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value ); + + } + + if ( materialNode.Opacity ) { + + parameters.opacity = parseFloat( materialNode.Opacity.value ); + + } + + if ( parameters.opacity < 1.0 ) { + + parameters.transparent = true; + + } + + if ( materialNode.ReflectionFactor ) { + + parameters.reflectivity = materialNode.ReflectionFactor.value; + + } + + if ( materialNode.Shininess ) { + + parameters.shininess = materialNode.Shininess.value; + + } - switch ( type.toLowerCase() ) { + if ( materialNode.Specular ) { - case 'phong': - material = new THREE.MeshPhongMaterial(); + parameters.specular = new THREE.Color().fromArray( materialNode.Specular.value ); + + } else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) { + + // The blender exporter exports specular color here instead of in materialNode.Specular + parameters.specular = new THREE.Color().fromArray( materialNode.SpecularColor.value ); + + } + + const scope = this; + connections.get( ID ).children.forEach( function ( child ) { + + const type = child.relationship; + + switch ( type ) { + + case 'Bump': + parameters.bumpMap = scope.getTexture( textureMap, child.ID ); + break; + + case 'Maya|TEX_ao_map': + parameters.aoMap = scope.getTexture( textureMap, child.ID ); + break; + + case 'DiffuseColor': + case 'Maya|TEX_color_map': + parameters.map = scope.getTexture( textureMap, child.ID ); + parameters.map.encoding = THREE.sRGBEncoding; + break; + + case 'DisplacementColor': + parameters.displacementMap = scope.getTexture( textureMap, child.ID ); + break; + + case 'EmissiveColor': + parameters.emissiveMap = scope.getTexture( textureMap, child.ID ); + parameters.emissiveMap.encoding = THREE.sRGBEncoding; + break; + + case 'NormalMap': + case 'Maya|TEX_normal_map': + parameters.normalMap = scope.getTexture( textureMap, child.ID ); + break; + + case 'ReflectionColor': + parameters.envMap = scope.getTexture( textureMap, child.ID ); + parameters.envMap.mapping = THREE.EquirectangularReflectionMapping; + parameters.envMap.encoding = THREE.sRGBEncoding; break; - case 'lambert': - material = new THREE.MeshLambertMaterial(); + case 'SpecularColor': + parameters.specularMap = scope.getTexture( textureMap, child.ID ); + parameters.specularMap.encoding = THREE.sRGBEncoding; break; + case 'TransparentColor': + case 'TransparencyFactor': + parameters.alphaMap = scope.getTexture( textureMap, child.ID ); + parameters.transparent = true; + break; + + case 'AmbientColor': + case 'ShininessExponent': // AKA glossiness map + + case 'SpecularFactor': // AKA specularLevel + + case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor + default: - console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to THREE.MeshPhongMaterial.', type ); - material = new THREE.MeshPhongMaterial(); + console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type ); break; } - material.setValues( parameters ); - material.name = name; - return material; + } ); + return parameters; - }, - // Parse FBX material and return parameters suitable for a three.js material - // Also parse the texture map and return any textures associated with the material - parseParameters: function ( materialNode, textureMap, ID ) { + } // get a texture from the textureMap for use by a material. - var parameters = {}; - if ( materialNode.BumpFactor ) { + getTexture( textureMap, id ) { - parameters.bumpScale = materialNode.BumpFactor.value; + // if the texture is a layered texture, just use the first layer and issue a warning + if ( 'LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture ) { - } + console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); + id = connections.get( id ).children[ 0 ].ID; - if ( materialNode.Diffuse ) { + } - parameters.color = new THREE.Color().fromArray( materialNode.Diffuse.value ); + return textureMap.get( id ); - } else if ( materialNode.DiffuseColor && ( materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB' ) ) { + } // Parse nodes in FBXTree.Objects.Deformer + // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here + // Generates map of THREE.Skeleton-like objects for use later when generating and binding skeletons. - // The blender exporter exports diffuse here instead of in materialNode.Diffuse - parameters.color = new THREE.Color().fromArray( materialNode.DiffuseColor.value ); - } + parseDeformers() { - if ( materialNode.DisplacementFactor ) { + const skeletons = {}; + const morphTargets = {}; - parameters.displacementScale = materialNode.DisplacementFactor.value; + if ( 'Deformer' in fbxTree.Objects ) { - } + const DeformerNodes = fbxTree.Objects.Deformer; - if ( materialNode.Emissive ) { + for ( const nodeID in DeformerNodes ) { - parameters.emissive = new THREE.Color().fromArray( materialNode.Emissive.value ); + const deformerNode = DeformerNodes[ nodeID ]; + const relationships = connections.get( parseInt( nodeID ) ); - } else if ( materialNode.EmissiveColor && ( materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB' ) ) { + if ( deformerNode.attrType === 'Skin' ) { - // The blender exporter exports emissive color here instead of in materialNode.Emissive - parameters.emissive = new THREE.Color().fromArray( materialNode.EmissiveColor.value ); + const skeleton = this.parseSkeleton( relationships, DeformerNodes ); + skeleton.ID = nodeID; + if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' ); + skeleton.geometryID = relationships.parents[ 0 ].ID; + skeletons[ nodeID ] = skeleton; - } + } else if ( deformerNode.attrType === 'BlendShape' ) { - if ( materialNode.EmissiveFactor ) { + const morphTarget = { + id: nodeID + }; + morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes ); + morphTarget.id = nodeID; + if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' ); + morphTargets[ nodeID ] = morphTarget; - parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value ); + } } - if ( materialNode.Opacity ) { + } - parameters.opacity = parseFloat( materialNode.Opacity.value ); + return { + skeletons: skeletons, + morphTargets: morphTargets + }; - } + } // Parse single nodes in FBXTree.Objects.Deformer + // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster' + // Each skin node represents a skeleton and each cluster node represents a bone - if ( parameters.opacity < 1.0 ) { - parameters.transparent = true; + parseSkeleton( relationships, deformerNodes ) { - } + const rawBones = []; + relationships.children.forEach( function ( child ) { + + const boneNode = deformerNodes[ child.ID ]; + if ( boneNode.attrType !== 'Cluster' ) return; + const rawBone = { + ID: child.ID, + indices: [], + weights: [], + transformLink: new THREE.Matrix4().fromArray( boneNode.TransformLink.a ) // transform: new THREE.Matrix4().fromArray( boneNode.Transform.a ), + // linkMode: boneNode.Mode, + + }; - if ( materialNode.ReflectionFactor ) { + if ( 'Indexes' in boneNode ) { - parameters.reflectivity = materialNode.ReflectionFactor.value; + rawBone.indices = boneNode.Indexes.a; + rawBone.weights = boneNode.Weights.a; } - if ( materialNode.Shininess ) { + rawBones.push( rawBone ); + + } ); + return { + rawBones: rawBones, + bones: [] + }; + + } // The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel" + + + parseMorphTargets( relationships, deformerNodes ) { + + const rawMorphTargets = []; + + for ( let i = 0; i < relationships.children.length; i ++ ) { + + const child = relationships.children[ i ]; + const morphTargetNode = deformerNodes[ child.ID ]; + const rawMorphTarget = { + name: morphTargetNode.attrName, + initialWeight: morphTargetNode.DeformPercent, + id: morphTargetNode.id, + fullWeights: morphTargetNode.FullWeights.a + }; + if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return; + rawMorphTarget.geoID = connections.get( parseInt( child.ID ) ).children.filter( function ( child ) { + + return child.relationship === undefined; + + } )[ 0 ].ID; + rawMorphTargets.push( rawMorphTarget ); + + } + + return rawMorphTargets; + + } // create the main THREE.Group() to be returned by the loader - parameters.shininess = materialNode.Shininess.value; + + parseScene( deformers, geometryMap, materialMap ) { + + sceneGraph = new THREE.Group(); + const modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap ); + const modelNodes = fbxTree.Objects.Model; + const scope = this; + modelMap.forEach( function ( model ) { + + const modelNode = modelNodes[ model.ID ]; + scope.setLookAtProperties( model, modelNode ); + const parentConnections = connections.get( model.ID ).parents; + parentConnections.forEach( function ( connection ) { + + const parent = modelMap.get( connection.ID ); + if ( parent !== undefined ) parent.add( model ); + + } ); + + if ( model.parent === null ) { + + sceneGraph.add( model ); } - if ( materialNode.Specular ) { + } ); + this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); + this.createAmbientLight(); + this.setupMorphMaterials(); + sceneGraph.traverse( function ( node ) { + + if ( node.userData.transformData ) { + + if ( node.parent ) { - parameters.specular = new THREE.Color().fromArray( materialNode.Specular.value ); + node.userData.transformData.parentMatrix = node.parent.matrix; + node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld; - } else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) { + } - // The blender exporter exports specular color here instead of in materialNode.Specular - parameters.specular = new THREE.Color().fromArray( materialNode.SpecularColor.value ); + const transform = generateTransform( node.userData.transformData ); + node.applyMatrix4( transform ); + node.updateWorldMatrix(); } - var scope = this; - connections.get( ID ).children.forEach( function ( child ) { + } ); + const animations = new AnimationParser().parse(); // if all the models where already combined in a single group, just return that - var type = child.relationship; + if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) { - switch ( type ) { + sceneGraph.children[ 0 ].animations = animations; + sceneGraph = sceneGraph.children[ 0 ]; - case 'Bump': - parameters.bumpMap = scope.getTexture( textureMap, child.ID ); - break; + } - case 'Maya|TEX_ao_map': - parameters.aoMap = scope.getTexture( textureMap, child.ID ); - break; + sceneGraph.animations = animations; - case 'DiffuseColor': - case 'Maya|TEX_color_map': - parameters.map = scope.getTexture( textureMap, child.ID ); - parameters.map.encoding = THREE.sRGBEncoding; - break; + } // parse nodes in FBXTree.Objects.Model - case 'DisplacementColor': - parameters.displacementMap = scope.getTexture( textureMap, child.ID ); + + parseModels( skeletons, geometryMap, materialMap ) { + + const modelMap = new Map(); + const modelNodes = fbxTree.Objects.Model; + + for ( const nodeID in modelNodes ) { + + const id = parseInt( nodeID ); + const node = modelNodes[ nodeID ]; + const relationships = connections.get( id ); + let model = this.buildSkeleton( relationships, skeletons, id, node.attrName ); + + if ( ! model ) { + + switch ( node.attrType ) { + + case 'Camera': + model = this.createCamera( relationships ); break; - case 'EmissiveColor': - parameters.emissiveMap = scope.getTexture( textureMap, child.ID ); - parameters.emissiveMap.encoding = THREE.sRGBEncoding; + case 'Light': + model = this.createLight( relationships ); break; - case 'NormalMap': - case 'Maya|TEX_normal_map': - parameters.normalMap = scope.getTexture( textureMap, child.ID ); + case 'Mesh': + model = this.createMesh( relationships, geometryMap, materialMap ); break; - case 'ReflectionColor': - parameters.envMap = scope.getTexture( textureMap, child.ID ); - parameters.envMap.mapping = THREE.EquirectangularReflectionMapping; - parameters.envMap.encoding = THREE.sRGBEncoding; + case 'NurbsCurve': + model = this.createCurve( relationships, geometryMap ); break; - case 'SpecularColor': - parameters.specularMap = scope.getTexture( textureMap, child.ID ); - parameters.specularMap.encoding = THREE.sRGBEncoding; + case 'LimbNode': + case 'Root': + model = new THREE.Bone(); break; - case 'TransparentColor': - case 'TransparencyFactor': - parameters.alphaMap = scope.getTexture( textureMap, child.ID ); - parameters.transparent = true; + case 'Null': + default: + model = new THREE.Group(); break; - case 'AmbientColor': - case 'ShininessExponent': // AKA glossiness map + } - case 'SpecularFactor': // AKA specularLevel + model.name = node.attrName ? THREE.PropertyBinding.sanitizeNodeName( node.attrName ) : ''; + model.ID = id; - case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor + } - default: - console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type ); - break; + this.getTransformData( model, node ); + modelMap.set( id, model ); - } + } - } ); - return parameters; + return modelMap; + + } + + buildSkeleton( relationships, skeletons, id, name ) { + + let bone = null; + relationships.parents.forEach( function ( parent ) { + + for ( const ID in skeletons ) { + + const skeleton = skeletons[ ID ]; + skeleton.rawBones.forEach( function ( rawBone, i ) { + + if ( rawBone.ID === parent.ID ) { + + const subBone = bone; + bone = new THREE.Bone(); + bone.matrixWorld.copy( rawBone.transformLink ); // set name and id here - otherwise in cases where "subBone" is created it will not have a name / id - }, - // get a texture from the textureMap for use by a material. - getTexture: function ( textureMap, id ) { + bone.name = name ? THREE.PropertyBinding.sanitizeNodeName( name ) : ''; + bone.ID = id; + skeleton.bones[ i ] = bone; // In cases where a bone is shared between multiple meshes + // duplicate the bone here and and it as a child of the first bone - // if the texture is a layered texture, just use the first layer and issue a warning - if ( 'LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture ) { + if ( subBone !== null ) { - console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); - id = connections.get( id ).children[ 0 ].ID; + bone.add( subBone ); + + } + + } + + } ); } - return textureMap.get( id ); + } ); + return bone; - }, - // Parse nodes in FBXTree.Objects.Deformer - // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here - // Generates map of THREE.Skeleton-like objects for use later when generating and binding skeletons. - parseDeformers: function () { + } // create a THREE.PerspectiveCamera or THREE.OrthographicCamera - var skeletons = {}; - var morphTargets = {}; - if ( 'Deformer' in fbxTree.Objects ) { + createCamera( relationships ) { - var DeformerNodes = fbxTree.Objects.Deformer; + let model; + let cameraAttribute; + relationships.children.forEach( function ( child ) { - for ( var nodeID in DeformerNodes ) { + const attr = fbxTree.Objects.NodeAttribute[ child.ID ]; - var deformerNode = DeformerNodes[ nodeID ]; - var relationships = connections.get( parseInt( nodeID ) ); + if ( attr !== undefined ) { - if ( deformerNode.attrType === 'Skin' ) { + cameraAttribute = attr; - var skeleton = this.parseSkeleton( relationships, DeformerNodes ); - skeleton.ID = nodeID; - if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' ); - skeleton.geometryID = relationships.parents[ 0 ].ID; - skeletons[ nodeID ] = skeleton; + } - } else if ( deformerNode.attrType === 'BlendShape' ) { + } ); - var morphTarget = { - id: nodeID - }; - morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes ); - morphTarget.id = nodeID; - if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' ); - morphTargets[ nodeID ] = morphTarget; + if ( cameraAttribute === undefined ) { - } + model = new THREE.Object3D(); - } + } else { + + let type = 0; + + if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { + + type = 1; } - return { - skeletons: skeletons, - morphTargets: morphTargets - }; + let nearClippingPlane = 1; - }, - // Parse single nodes in FBXTree.Objects.Deformer - // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster' - // Each skin node represents a skeleton and each cluster node represents a bone - parseSkeleton: function ( relationships, deformerNodes ) { + if ( cameraAttribute.NearPlane !== undefined ) { - var rawBones = []; - relationships.children.forEach( function ( child ) { + nearClippingPlane = cameraAttribute.NearPlane.value / 1000; - var boneNode = deformerNodes[ child.ID ]; - if ( boneNode.attrType !== 'Cluster' ) return; - var rawBone = { - ID: child.ID, - indices: [], - weights: [], - transformLink: new THREE.Matrix4().fromArray( boneNode.TransformLink.a ) // transform: new THREE.Matrix4().fromArray( boneNode.Transform.a ), - // linkMode: boneNode.Mode, + } - }; + let farClippingPlane = 1000; - if ( 'Indexes' in boneNode ) { + if ( cameraAttribute.FarPlane !== undefined ) { - rawBone.indices = boneNode.Indexes.a; - rawBone.weights = boneNode.Weights.a; + farClippingPlane = cameraAttribute.FarPlane.value / 1000; - } + } - rawBones.push( rawBone ); + let width = window.innerWidth; + let height = window.innerHeight; - } ); - return { - rawBones: rawBones, - bones: [] - }; + if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { - }, - // The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel" - parseMorphTargets: function ( relationships, deformerNodes ) { + width = cameraAttribute.AspectWidth.value; + height = cameraAttribute.AspectHeight.value; - var rawMorphTargets = []; + } - for ( var i = 0; i < relationships.children.length; i ++ ) { + const aspect = width / height; + let fov = 45; - var child = relationships.children[ i ]; - var morphTargetNode = deformerNodes[ child.ID ]; - var rawMorphTarget = { - name: morphTargetNode.attrName, - initialWeight: morphTargetNode.DeformPercent, - id: morphTargetNode.id, - fullWeights: morphTargetNode.FullWeights.a - }; - if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return; - rawMorphTarget.geoID = connections.get( parseInt( child.ID ) ).children.filter( function ( child ) { + if ( cameraAttribute.FieldOfView !== undefined ) { + + fov = cameraAttribute.FieldOfView.value; + + } + + const focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null; - return child.relationship === undefined; + switch ( type ) { + + case 0: + // Perspective + model = new THREE.PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); + if ( focalLength !== null ) model.setFocalLength( focalLength ); + break; - } )[ 0 ].ID; - rawMorphTargets.push( rawMorphTarget ); + case 1: + // Orthographic + model = new THREE.OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); + break; + + default: + console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); + model = new THREE.Object3D(); + break; } - return rawMorphTargets; + } - }, - // create the main THREE.Group() to be returned by the loader - parseScene: function ( deformers, geometryMap, materialMap ) { + return model; - sceneGraph = new THREE.Group(); - var modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap ); - var modelNodes = fbxTree.Objects.Model; - var scope = this; - modelMap.forEach( function ( model ) { + } // Create a THREE.DirectionalLight, THREE.PointLight or THREE.SpotLight - var modelNode = modelNodes[ model.ID ]; - scope.setLookAtProperties( model, modelNode ); - var parentConnections = connections.get( model.ID ).parents; - parentConnections.forEach( function ( connection ) { - var parent = modelMap.get( connection.ID ); - if ( parent !== undefined ) parent.add( model ); + createLight( relationships ) { - } ); + let model; + let lightAttribute; + relationships.children.forEach( function ( child ) { - if ( model.parent === null ) { + const attr = fbxTree.Objects.NodeAttribute[ child.ID ]; - sceneGraph.add( model ); + if ( attr !== undefined ) { - } + lightAttribute = attr; - } ); - this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); - this.createAmbientLight(); - this.setupMorphMaterials(); - sceneGraph.traverse( function ( node ) { + } - if ( node.userData.transformData ) { + } ); - if ( node.parent ) { + if ( lightAttribute === undefined ) { - node.userData.transformData.parentMatrix = node.parent.matrix; - node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld; + model = new THREE.Object3D(); - } + } else { - var transform = generateTransform( node.userData.transformData ); - node.applyMatrix4( transform ); - node.updateWorldMatrix(); + let type; // LightType can be undefined for Point lights - } + if ( lightAttribute.LightType === undefined ) { - } ); - var animations = new AnimationParser().parse(); // if all the models where already combined in a single group, just return that + type = 0; + + } else { + + type = lightAttribute.LightType.value; + + } - if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) { + let color = 0xffffff; - sceneGraph.children[ 0 ].animations = animations; - sceneGraph = sceneGraph.children[ 0 ]; + if ( lightAttribute.Color !== undefined ) { + + color = new THREE.Color().fromArray( lightAttribute.Color.value ); } - sceneGraph.animations = animations; + let intensity = lightAttribute.Intensity === undefined ? 1 : lightAttribute.Intensity.value / 100; // light disabled - }, - // parse nodes in FBXTree.Objects.Model - parseModels: function ( skeletons, geometryMap, materialMap ) { + if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { - var modelMap = new Map(); - var modelNodes = fbxTree.Objects.Model; + intensity = 0; - for ( var nodeID in modelNodes ) { + } - var id = parseInt( nodeID ); - var node = modelNodes[ nodeID ]; - var relationships = connections.get( id ); - var model = this.buildSkeleton( relationships, skeletons, id, node.attrName ); + let distance = 0; - if ( ! model ) { + if ( lightAttribute.FarAttenuationEnd !== undefined ) { - switch ( node.attrType ) { + if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { - case 'Camera': - model = this.createCamera( relationships ); - break; + distance = 0; - case 'Light': - model = this.createLight( relationships ); - break; + } else { - case 'Mesh': - model = this.createMesh( relationships, geometryMap, materialMap ); - break; + distance = lightAttribute.FarAttenuationEnd.value; + + } + + } // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? - case 'NurbsCurve': - model = this.createCurve( relationships, geometryMap ); - break; - case 'LimbNode': - case 'Root': - model = new THREE.Bone(); - break; + const decay = 1; - case 'Null': - default: - model = new THREE.Group(); - break; + switch ( type ) { + + case 0: + // Point + model = new THREE.PointLight( color, intensity, distance, decay ); + break; + + case 1: + // Directional + model = new THREE.DirectionalLight( color, intensity ); + break; + + case 2: + // Spot + let angle = Math.PI / 3; + + if ( lightAttribute.InnerAngle !== undefined ) { + + angle = THREE.MathUtils.degToRad( lightAttribute.InnerAngle.value ); } - model.name = node.attrName ? THREE.PropertyBinding.sanitizeNodeName( node.attrName ) : ''; - model.ID = id; + let penumbra = 0; - } + if ( lightAttribute.OuterAngle !== undefined ) { - this.getTransformData( model, node ); - modelMap.set( id, model ); + // TODO: this is not correct - FBX calculates outer and inner angle in degrees + // with OuterAngle > InnerAngle && OuterAngle <= Math.PI + // while three.js uses a penumbra between (0, 1) to attenuate the inner angle + penumbra = THREE.MathUtils.degToRad( lightAttribute.OuterAngle.value ); + penumbra = Math.max( penumbra, 1 ); + + } + + model = new THREE.SpotLight( color, intensity, distance, angle, penumbra, decay ); + break; + + default: + console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a THREE.PointLight.' ); + model = new THREE.PointLight( color, intensity ); + break; } - return modelMap; + if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { - }, - buildSkeleton: function ( relationships, skeletons, id, name ) { + model.castShadow = true; - var bone = null; - relationships.parents.forEach( function ( parent ) { + } - for ( var ID in skeletons ) { + } - var skeleton = skeletons[ ID ]; - skeleton.rawBones.forEach( function ( rawBone, i ) { + return model; - if ( rawBone.ID === parent.ID ) { + } - var subBone = bone; - bone = new THREE.Bone(); - bone.matrixWorld.copy( rawBone.transformLink ); // set name and id here - otherwise in cases where "subBone" is created it will not have a name / id + createMesh( relationships, geometryMap, materialMap ) { - bone.name = name ? THREE.PropertyBinding.sanitizeNodeName( name ) : ''; - bone.ID = id; - skeleton.bones[ i ] = bone; // In cases where a bone is shared between multiple meshes - // duplicate the bone here and and it as a child of the first bone + let model; + let geometry = null; + let material = null; + const materials = []; // get geometry and materials(s) from connections - if ( subBone !== null ) { + relationships.children.forEach( function ( child ) { - bone.add( subBone ); + if ( geometryMap.has( child.ID ) ) { - } + geometry = geometryMap.get( child.ID ); - } + } - } ); + if ( materialMap.has( child.ID ) ) { - } + materials.push( materialMap.get( child.ID ) ); - } ); - return bone; + } - }, - // create a THREE.PerspectiveCamera or THREE.OrthographicCamera - createCamera: function ( relationships ) { + } ); - var model; - var cameraAttribute; - relationships.children.forEach( function ( child ) { + if ( materials.length > 1 ) { - var attr = fbxTree.Objects.NodeAttribute[ child.ID ]; + material = materials; - if ( attr !== undefined ) { + } else if ( materials.length > 0 ) { - cameraAttribute = attr; + material = materials[ 0 ]; - } + } else { + material = new THREE.MeshPhongMaterial( { + color: 0xcccccc } ); + materials.push( material ); - if ( cameraAttribute === undefined ) { + } - model = new THREE.Object3D(); + if ( 'color' in geometry.attributes ) { - } else { + materials.forEach( function ( material ) { - var type = 0; + material.vertexColors = true; - if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { + } ); - type = 1; + } - } + if ( geometry.FBX_Deformer ) { - var nearClippingPlane = 1; + materials.forEach( function ( material ) { - if ( cameraAttribute.NearPlane !== undefined ) { + material.skinning = true; - nearClippingPlane = cameraAttribute.NearPlane.value / 1000; + } ); + model = new THREE.SkinnedMesh( geometry, material ); + model.normalizeSkinWeights(); - } + } else { - var farClippingPlane = 1000; + model = new THREE.Mesh( geometry, material ); - if ( cameraAttribute.FarPlane !== undefined ) { + } - farClippingPlane = cameraAttribute.FarPlane.value / 1000; + return model; - } + } - var width = window.innerWidth; - var height = window.innerHeight; + createCurve( relationships, geometryMap ) { - if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { + const geometry = relationships.children.reduce( function ( geo, child ) { - width = cameraAttribute.AspectWidth.value; - height = cameraAttribute.AspectHeight.value; + if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); + return geo; - } + }, null ); // FBX does not list materials for Nurbs lines, so we'll just put our own in here. - var aspect = width / height; - var fov = 45; + const material = new THREE.LineBasicMaterial( { + color: 0x3300ff, + linewidth: 1 + } ); + return new THREE.Line( geometry, material ); - if ( cameraAttribute.FieldOfView !== undefined ) { + } // parse the model node for transform data - fov = cameraAttribute.FieldOfView.value; - } + getTransformData( model, modelNode ) { - var focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null; + const transformData = {}; + if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); + if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); else transformData.eulerOrder = 'ZYX'; + if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; + if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; + if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; + if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; + if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; + if ( 'ScalingOffset' in modelNode ) transformData.scalingOffset = modelNode.ScalingOffset.value; + if ( 'ScalingPivot' in modelNode ) transformData.scalingPivot = modelNode.ScalingPivot.value; + if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; + if ( 'RotationPivot' in modelNode ) transformData.rotationPivot = modelNode.RotationPivot.value; + model.userData.transformData = transformData; - switch ( type ) { + } - case 0: - // Perspective - model = new THREE.PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); - if ( focalLength !== null ) model.setFocalLength( focalLength ); - break; + setLookAtProperties( model, modelNode ) { - case 1: - // Orthographic - model = new THREE.OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); - break; + if ( 'LookAtProperty' in modelNode ) { - default: - console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); - model = new THREE.Object3D(); - break; + const children = connections.get( model.ID ).children; + children.forEach( function ( child ) { - } + if ( child.relationship === 'LookAtProperty' ) { - } + const lookAtTarget = fbxTree.Objects.Model[ child.ID ]; + + if ( 'Lcl_Translation' in lookAtTarget ) { + + const pos = lookAtTarget.Lcl_Translation.value; // THREE.DirectionalLight, THREE.SpotLight - return model; + if ( model.target !== undefined ) { - }, - // Create a THREE.DirectionalLight, THREE.PointLight or THREE.SpotLight - createLight: function ( relationships ) { + model.target.position.fromArray( pos ); + sceneGraph.add( model.target ); - var model; - var lightAttribute; - relationships.children.forEach( function ( child ) { + } else { - var attr = fbxTree.Objects.NodeAttribute[ child.ID ]; + // Cameras and other Object3Ds + model.lookAt( new THREE.Vector3().fromArray( pos ) ); - if ( attr !== undefined ) { + } - lightAttribute = attr; + } } } ); - if ( lightAttribute === undefined ) { + } - model = new THREE.Object3D(); + } - } else { + bindSkeleton( skeletons, geometryMap, modelMap ) { - var type; // LightType can be undefined for Point lights + const bindMatrices = this.parsePoseNodes(); - if ( lightAttribute.LightType === undefined ) { + for ( const ID in skeletons ) { - type = 0; + const skeleton = skeletons[ ID ]; + const parents = connections.get( parseInt( skeleton.ID ) ).parents; + parents.forEach( function ( parent ) { - } else { + if ( geometryMap.has( parent.ID ) ) { - type = lightAttribute.LightType.value; + const geoID = parent.ID; + const geoRelationships = connections.get( geoID ); + geoRelationships.parents.forEach( function ( geoConnParent ) { - } + if ( modelMap.has( geoConnParent.ID ) ) { - var color = 0xffffff; + const model = modelMap.get( geoConnParent.ID ); + model.bind( new THREE.Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); - if ( lightAttribute.Color !== undefined ) { + } - color = new THREE.Color().fromArray( lightAttribute.Color.value ); + } ); } - var intensity = lightAttribute.Intensity === undefined ? 1 : lightAttribute.Intensity.value / 100; // light disabled + } ); - if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { + } - intensity = 0; + } - } + parsePoseNodes() { - var distance = 0; + const bindMatrices = {}; - if ( lightAttribute.FarAttenuationEnd !== undefined ) { + if ( 'Pose' in fbxTree.Objects ) { - if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { + const BindPoseNode = fbxTree.Objects.Pose; - distance = 0; + for ( const nodeID in BindPoseNode ) { - } else { + if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { - distance = lightAttribute.FarAttenuationEnd.value; + const poseNodes = BindPoseNode[ nodeID ].PoseNode; - } + if ( Array.isArray( poseNodes ) ) { - } // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? + poseNodes.forEach( function ( poseNode ) { + bindMatrices[ poseNode.Node ] = new THREE.Matrix4().fromArray( poseNode.Matrix.a ); - var decay = 1; + } ); - switch ( type ) { + } else { - case 0: - // Point - model = new THREE.PointLight( color, intensity, distance, decay ); - break; + bindMatrices[ poseNodes.Node ] = new THREE.Matrix4().fromArray( poseNodes.Matrix.a ); - case 1: - // Directional - model = new THREE.DirectionalLight( color, intensity ); - break; + } - case 2: - // Spot - var angle = Math.PI / 3; + } - if ( lightAttribute.InnerAngle !== undefined ) { + } - angle = THREE.MathUtils.degToRad( lightAttribute.InnerAngle.value ); + } - } + return bindMatrices; - var penumbra = 0; + } // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light - if ( lightAttribute.OuterAngle !== undefined ) { - // TODO: this is not correct - FBX calculates outer and inner angle in degrees - // with OuterAngle > InnerAngle && OuterAngle <= Math.PI - // while three.js uses a penumbra between (0, 1) to attenuate the inner angle - penumbra = THREE.MathUtils.degToRad( lightAttribute.OuterAngle.value ); - penumbra = Math.max( penumbra, 1 ); + createAmbientLight() { - } + if ( 'GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings ) { - model = new THREE.SpotLight( color, intensity, distance, angle, penumbra, decay ); - break; + const ambientColor = fbxTree.GlobalSettings.AmbientColor.value; + const r = ambientColor[ 0 ]; + const g = ambientColor[ 1 ]; + const b = ambientColor[ 2 ]; - default: - console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a THREE.PointLight.' ); - model = new THREE.PointLight( color, intensity ); - break; + if ( r !== 0 || g !== 0 || b !== 0 ) { - } + const color = new THREE.Color( r, g, b ); + sceneGraph.add( new THREE.AmbientLight( color, 1 ) ); + + } - if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { + } - model.castShadow = true; + } - } + setupMorphMaterials() { - } + const scope = this; + sceneGraph.traverse( function ( child ) { - return model; + if ( child.isMesh ) { - }, - createMesh: function ( relationships, geometryMap, materialMap ) { + if ( child.geometry.morphAttributes.position && child.geometry.morphAttributes.position.length ) { - var model; - var geometry = null; - var material = null; - var materials = []; // get geometry and materials(s) from connections + if ( Array.isArray( child.material ) ) { - relationships.children.forEach( function ( child ) { + child.material.forEach( function ( material, i ) { - if ( geometryMap.has( child.ID ) ) { + scope.setupMorphMaterial( child, material, i ); - geometry = geometryMap.get( child.ID ); + } ); - } + } else { - if ( materialMap.has( child.ID ) ) { + scope.setupMorphMaterial( child, child.material ); - materials.push( materialMap.get( child.ID ) ); + } } - } ); + } - if ( materials.length > 1 ) { + } ); - material = materials; + } - } else if ( materials.length > 0 ) { + setupMorphMaterial( child, material, index ) { - material = materials[ 0 ]; + const uuid = child.uuid; + const matUuid = material.uuid; // if a geometry has morph targets, it cannot share the material with other geometries - } else { + let sharedMat = false; + sceneGraph.traverse( function ( node ) { - material = new THREE.MeshPhongMaterial( { - color: 0xcccccc - } ); - materials.push( material ); + if ( node.isMesh ) { - } + if ( Array.isArray( node.material ) ) { - if ( 'color' in geometry.attributes ) { + node.material.forEach( function ( mat ) { - materials.forEach( function ( material ) { + if ( mat.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; - material.vertexColors = true; + } ); - } ); + } else if ( node.material.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; } - if ( geometry.FBX_Deformer ) { + } ); - materials.forEach( function ( material ) { + if ( sharedMat === true ) { - material.skinning = true; + const clonedMat = material.clone(); + clonedMat.morphTargets = true; + if ( index === undefined ) child.material = clonedMat; else child.material[ index ] = clonedMat; - } ); - model = new THREE.SkinnedMesh( geometry, material ); - model.normalizeSkinWeights(); + } else material.morphTargets = true; - } else { + } - model = new THREE.Mesh( geometry, material ); + } // parse Geometry data from FBXTree and return map of BufferGeometries - } - return model; + class GeometryParser { - }, - createCurve: function ( relationships, geometryMap ) { + // Parse nodes in FBXTree.Objects.Geometry + parse( deformers ) { - var geometry = relationships.children.reduce( function ( geo, child ) { + const geometryMap = new Map(); - if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); - return geo; + if ( 'Geometry' in fbxTree.Objects ) { - }, null ); // FBX does not list materials for Nurbs lines, so we'll just put our own in here. + const geoNodes = fbxTree.Objects.Geometry; - var material = new THREE.LineBasicMaterial( { - color: 0x3300ff, - linewidth: 1 - } ); - return new THREE.Line( geometry, material ); + for ( const nodeID in geoNodes ) { - }, - // parse the model node for transform data - getTransformData: function ( model, modelNode ) { + const relationships = connections.get( parseInt( nodeID ) ); + const geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers ); + geometryMap.set( parseInt( nodeID ), geo ); - var transformData = {}; - if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); - if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); else transformData.eulerOrder = 'ZYX'; - if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; - if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; - if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; - if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; - if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; - if ( 'ScalingOffset' in modelNode ) transformData.scalingOffset = modelNode.ScalingOffset.value; - if ( 'ScalingPivot' in modelNode ) transformData.scalingPivot = modelNode.ScalingPivot.value; - if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; - if ( 'RotationPivot' in modelNode ) transformData.rotationPivot = modelNode.RotationPivot.value; - model.userData.transformData = transformData; + } - }, - setLookAtProperties: function ( model, modelNode ) { + } - if ( 'LookAtProperty' in modelNode ) { + return geometryMap; - var children = connections.get( model.ID ).children; - children.forEach( function ( child ) { + } // Parse single node in FBXTree.Objects.Geometry - if ( child.relationship === 'LookAtProperty' ) { - var lookAtTarget = fbxTree.Objects.Model[ child.ID ]; + parseGeometry( relationships, geoNode, deformers ) { - if ( 'Lcl_Translation' in lookAtTarget ) { + switch ( geoNode.attrType ) { - var pos = lookAtTarget.Lcl_Translation.value; // THREE.DirectionalLight, THREE.SpotLight + case 'Mesh': + return this.parseMeshGeometry( relationships, geoNode, deformers ); + break; - if ( model.target !== undefined ) { + case 'NurbsCurve': + return this.parseNurbsGeometry( geoNode ); + break; - model.target.position.fromArray( pos ); - sceneGraph.add( model.target ); + } - } else { + } // Parse single node mesh geometry in FBXTree.Objects.Geometry - // Cameras and other Object3Ds - model.lookAt( new THREE.Vector3().fromArray( pos ) ); - } + parseMeshGeometry( relationships, geoNode, deformers ) { - } + const skeletons = deformers.skeletons; + const morphTargets = []; + const modelNodes = relationships.parents.map( function ( parent ) { - } + return fbxTree.Objects.Model[ parent.ID ]; - } ); + } ); // don't create geometry if it is not associated with any models - } + if ( modelNodes.length === 0 ) return; + const skeleton = relationships.children.reduce( function ( skeleton, child ) { - }, - bindSkeleton: function ( skeletons, geometryMap, modelMap ) { + if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; + return skeleton; - var bindMatrices = this.parsePoseNodes(); + }, null ); + relationships.children.forEach( function ( child ) { - for ( var ID in skeletons ) { + if ( deformers.morphTargets[ child.ID ] !== undefined ) { - var skeleton = skeletons[ ID ]; - var parents = connections.get( parseInt( skeleton.ID ) ).parents; - parents.forEach( function ( parent ) { + morphTargets.push( deformers.morphTargets[ child.ID ] ); - if ( geometryMap.has( parent.ID ) ) { + } - var geoID = parent.ID; - var geoRelationships = connections.get( geoID ); - geoRelationships.parents.forEach( function ( geoConnParent ) { + } ); // Assume one model and get the preRotation from that + // if there is more than one model associated with the geometry this may cause problems - if ( modelMap.has( geoConnParent.ID ) ) { + const modelNode = modelNodes[ 0 ]; + const transformData = {}; + if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); + if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); + if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value; + if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value; + if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value; + const transform = generateTransform( transformData ); + return this.genGeometry( geoNode, skeleton, morphTargets, transform ); - var model = modelMap.get( geoConnParent.ID ); - model.bind( new THREE.Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); + } // Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry - } - } ); + genGeometry( geoNode, skeleton, morphTargets, preTransform ) { - } + const geo = new THREE.BufferGeometry(); + if ( geoNode.attrName ) geo.name = geoNode.attrName; + const geoInfo = this.parseGeoNode( geoNode, skeleton ); + const buffers = this.genBuffers( geoInfo ); + const positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 ); + positionAttribute.applyMatrix4( preTransform ); + geo.setAttribute( 'position', positionAttribute ); - } ); + if ( buffers.colors.length > 0 ) { - } + geo.setAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) ); - }, - parsePoseNodes: function () { + } - var bindMatrices = {}; + if ( skeleton ) { - if ( 'Pose' in fbxTree.Objects ) { + geo.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) ); + geo.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) ); // used later to bind the skeleton to the model - var BindPoseNode = fbxTree.Objects.Pose; + geo.FBX_Deformer = skeleton; - for ( var nodeID in BindPoseNode ) { + } - if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { + if ( buffers.normal.length > 0 ) { - var poseNodes = BindPoseNode[ nodeID ].PoseNode; + const normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform ); + const normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 ); + normalAttribute.applyNormalMatrix( normalMatrix ); + geo.setAttribute( 'normal', normalAttribute ); - if ( Array.isArray( poseNodes ) ) { + } - poseNodes.forEach( function ( poseNode ) { + buffers.uvs.forEach( function ( uvBuffer, i ) { - bindMatrices[ poseNode.Node ] = new THREE.Matrix4().fromArray( poseNode.Matrix.a ); + // subsequent uv buffers are called 'uv1', 'uv2', ... + let name = 'uv' + ( i + 1 ).toString(); // the first uv buffer is just called 'uv' - } ); + if ( i === 0 ) { - } else { + name = 'uv'; - bindMatrices[ poseNodes.Node ] = new THREE.Matrix4().fromArray( poseNodes.Matrix.a ); + } - } + geo.setAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) ); - } + } ); - } + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - } + // Convert the material indices of each vertex into rendering groups on the geometry. + let prevMaterialIndex = buffers.materialIndex[ 0 ]; + let startIndex = 0; + buffers.materialIndex.forEach( function ( currentIndex, i ) { + + if ( currentIndex !== prevMaterialIndex ) { - return bindMatrices; + geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); + prevMaterialIndex = currentIndex; + startIndex = i; + + } - }, - // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light - createAmbientLight: function () { + } ); // the loop above doesn't add the last group, do that here. - if ( 'GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings ) { + if ( geo.groups.length > 0 ) { - var ambientColor = fbxTree.GlobalSettings.AmbientColor.value; - var r = ambientColor[ 0 ]; - var g = ambientColor[ 1 ]; - var b = ambientColor[ 2 ]; + const lastGroup = geo.groups[ geo.groups.length - 1 ]; + const lastIndex = lastGroup.start + lastGroup.count; - if ( r !== 0 || g !== 0 || b !== 0 ) { + if ( lastIndex !== buffers.materialIndex.length ) { - var color = new THREE.Color( r, g, b ); - sceneGraph.add( new THREE.AmbientLight( color, 1 ) ); + geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex ); } - } + } // case where there are multiple materials but the whole geometry is only + // using one of them - }, - setupMorphMaterials: function () { - var scope = this; - sceneGraph.traverse( function ( child ) { + if ( geo.groups.length === 0 ) { - if ( child.isMesh ) { + geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] ); - if ( child.geometry.morphAttributes.position && child.geometry.morphAttributes.position.length ) { + } - if ( Array.isArray( child.material ) ) { + } - child.material.forEach( function ( material, i ) { + this.addMorphTargets( geo, geoNode, morphTargets, preTransform ); + return geo; - scope.setupMorphMaterial( child, material, i ); + } - } ); + parseGeoNode( geoNode, skeleton ) { - } else { + const geoInfo = {}; + geoInfo.vertexPositions = geoNode.Vertices !== undefined ? geoNode.Vertices.a : []; + geoInfo.vertexIndices = geoNode.PolygonVertexIndex !== undefined ? geoNode.PolygonVertexIndex.a : []; - scope.setupMorphMaterial( child, child.material ); + if ( geoNode.LayerElementColor ) { - } + geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] ); - } + } - } + if ( geoNode.LayerElementMaterial ) { - } ); + geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] ); - }, - setupMorphMaterial: function ( child, material, index ) { + } - var uuid = child.uuid; - var matUuid = material.uuid; // if a geometry has morph targets, it cannot share the material with other geometries + if ( geoNode.LayerElementNormal ) { - var sharedMat = false; - sceneGraph.traverse( function ( node ) { + geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] ); - if ( node.isMesh ) { + } - if ( Array.isArray( node.material ) ) { + if ( geoNode.LayerElementUV ) { - node.material.forEach( function ( mat ) { + geoInfo.uv = []; + let i = 0; - if ( mat.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; + while ( geoNode.LayerElementUV[ i ] ) { - } ); + if ( geoNode.LayerElementUV[ i ].UV ) { - } else if ( node.material.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; + geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) ); } - } ); + i ++; + + } - if ( sharedMat === true ) { + } - var clonedMat = material.clone(); - clonedMat.morphTargets = true; - if ( index === undefined ) child.material = clonedMat; else child.material[ index ] = clonedMat; + geoInfo.weightTable = {}; - } else material.morphTargets = true; + if ( skeleton !== null ) { - } - }; // parse Geometry data from FBXTree and return map of BufferGeometries + geoInfo.skeleton = skeleton; + skeleton.rawBones.forEach( function ( rawBone, i ) { - function GeometryParser() {} + // loop over the bone's vertex indices and weights + rawBone.indices.forEach( function ( index, j ) { - GeometryParser.prototype = { - constructor: GeometryParser, - // Parse nodes in FBXTree.Objects.Geometry - parse: function ( deformers ) { + if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = []; + geoInfo.weightTable[ index ].push( { + id: i, + weight: rawBone.weights[ j ] + } ); - var geometryMap = new Map(); + } ); - if ( 'Geometry' in fbxTree.Objects ) { + } ); - var geoNodes = fbxTree.Objects.Geometry; + } - for ( var nodeID in geoNodes ) { + return geoInfo; - var relationships = connections.get( parseInt( nodeID ) ); - var geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers ); - geometryMap.set( parseInt( nodeID ), geo ); + } - } + genBuffers( geoInfo ) { - } + const buffers = { + vertex: [], + normal: [], + colors: [], + uvs: [], + materialIndex: [], + vertexWeights: [], + weightsIndices: [] + }; + let polygonIndex = 0; + let faceLength = 0; + let displayedWeightsWarning = false; // these will hold data for a single face - return geometryMap; + let facePositionIndexes = []; + let faceNormals = []; + let faceColors = []; + let faceUVs = []; + let faceWeights = []; + let faceWeightIndices = []; + const scope = this; + geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { - }, - // Parse single node in FBXTree.Objects.Geometry - parseGeometry: function ( relationships, geoNode, deformers ) { + let materialIndex; + let endOfFace = false; // Face index and vertex index arrays are combined in a single array + // A cube with quad faces looks like this: + // PolygonVertexIndex: *24 { + // a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5 + // } + // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3 + // to find index of last vertex bit shift the index: ^ - 1 - switch ( geoNode.attrType ) { + if ( vertexIndex < 0 ) { - case 'Mesh': - return this.parseMeshGeometry( relationships, geoNode, deformers ); - break; + vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 - case 'NurbsCurve': - return this.parseNurbsGeometry( geoNode ); - break; + endOfFace = true; } - }, - // Parse single node mesh geometry in FBXTree.Objects.Geometry - parseMeshGeometry: function ( relationships, geoNode, deformers ) { + let weightIndices = []; + let weights = []; + facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); - var skeletons = deformers.skeletons; - var morphTargets = []; - var modelNodes = relationships.parents.map( function ( parent ) { + if ( geoInfo.color ) { - return fbxTree.Objects.Model[ parent.ID ]; + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color ); + faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - } ); // don't create geometry if it is not associated with any models + } - if ( modelNodes.length === 0 ) return; - var skeleton = relationships.children.reduce( function ( skeleton, child ) { + if ( geoInfo.skeleton ) { - if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; - return skeleton; + if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) { - }, null ); - relationships.children.forEach( function ( child ) { + geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) { - if ( deformers.morphTargets[ child.ID ] !== undefined ) { + weights.push( wt.weight ); + weightIndices.push( wt.id ); - morphTargets.push( deformers.morphTargets[ child.ID ] ); + } ); } - } ); // Assume one model and get the preRotation from that - // if there is more than one model associated with the geometry this may cause problems + if ( weights.length > 4 ) { - var modelNode = modelNodes[ 0 ]; - var transformData = {}; - if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); - if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); - if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value; - if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value; - if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value; - var transform = generateTransform( transformData ); - return this.genGeometry( geoNode, skeleton, morphTargets, transform ); + if ( ! displayedWeightsWarning ) { - }, - // Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry - genGeometry: function ( geoNode, skeleton, morphTargets, preTransform ) { + console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); + displayedWeightsWarning = true; - var geo = new THREE.BufferGeometry(); - if ( geoNode.attrName ) geo.name = geoNode.attrName; - var geoInfo = this.parseGeoNode( geoNode, skeleton ); - var buffers = this.genBuffers( geoInfo ); - var positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 ); - positionAttribute.applyMatrix4( preTransform ); - geo.setAttribute( 'position', positionAttribute ); + } - if ( buffers.colors.length > 0 ) { + const wIndex = [ 0, 0, 0, 0 ]; + const Weight = [ 0, 0, 0, 0 ]; + weights.forEach( function ( weight, weightIndex ) { - geo.setAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) ); + let currentWeight = weight; + let currentIndex = weightIndices[ weightIndex ]; + Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { - } + if ( currentWeight > comparedWeight ) { - if ( skeleton ) { + comparedWeightArray[ comparedWeightIndex ] = currentWeight; + currentWeight = comparedWeight; + const tmp = wIndex[ comparedWeightIndex ]; + wIndex[ comparedWeightIndex ] = currentIndex; + currentIndex = tmp; - geo.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) ); - geo.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) ); // used later to bind the skeleton to the model + } - geo.FBX_Deformer = skeleton; + } ); - } + } ); + weightIndices = wIndex; + weights = Weight; - if ( buffers.normal.length > 0 ) { + } // if the weight array is shorter than 4 pad with 0s - var normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform ); - var normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 ); - normalAttribute.applyNormalMatrix( normalMatrix ); - geo.setAttribute( 'normal', normalAttribute ); - } + while ( weights.length < 4 ) { - buffers.uvs.forEach( function ( uvBuffer, i ) { + weights.push( 0 ); + weightIndices.push( 0 ); - // subsequent uv buffers are called 'uv1', 'uv2', ... - var name = 'uv' + ( i + 1 ).toString(); // the first uv buffer is just called 'uv' + } - if ( i === 0 ) { + for ( let i = 0; i < 4; ++ i ) { - name = 'uv'; + faceWeights.push( weights[ i ] ); + faceWeightIndices.push( weightIndices[ i ] ); } - geo.setAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) ); + } - } ); + if ( geoInfo.normal ) { - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal ); + faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - // Convert the material indices of each vertex into rendering groups on the geometry. - var prevMaterialIndex = buffers.materialIndex[ 0 ]; - var startIndex = 0; - buffers.materialIndex.forEach( function ( currentIndex, i ) { + } - if ( currentIndex !== prevMaterialIndex ) { + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); - prevMaterialIndex = currentIndex; - startIndex = i; + materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ]; - } + } - } ); // the loop above doesn't add the last group, do that here. + if ( geoInfo.uv ) { - if ( geo.groups.length > 0 ) { + geoInfo.uv.forEach( function ( uv, i ) { - var lastGroup = geo.groups[ geo.groups.length - 1 ]; - var lastIndex = lastGroup.start + lastGroup.count; + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); - if ( lastIndex !== buffers.materialIndex.length ) { + if ( faceUVs[ i ] === undefined ) { - geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex ); + faceUVs[ i ] = []; } - } // case where there are multiple materials but the whole geometry is only - // using one of them + faceUVs[ i ].push( data[ 0 ] ); + faceUVs[ i ].push( data[ 1 ] ); - - if ( geo.groups.length === 0 ) { - - geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] ); - - } + } ); } - this.addMorphTargets( geo, geoNode, morphTargets, preTransform ); - return geo; - - }, - parseGeoNode: function ( geoNode, skeleton ) { + faceLength ++; - var geoInfo = {}; - geoInfo.vertexPositions = geoNode.Vertices !== undefined ? geoNode.Vertices.a : []; - geoInfo.vertexIndices = geoNode.PolygonVertexIndex !== undefined ? geoNode.PolygonVertexIndex.a : []; + if ( endOfFace ) { - if ( geoNode.LayerElementColor ) { + scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); + polygonIndex ++; + faceLength = 0; // reset arrays for the next face - geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] ); + facePositionIndexes = []; + faceNormals = []; + faceColors = []; + faceUVs = []; + faceWeights = []; + faceWeightIndices = []; } - if ( geoNode.LayerElementMaterial ) { - - geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] ); + } ); + return buffers; + + } // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris + + + genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { + + for ( let i = 2; i < faceLength; i ++ ) { + + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); + + if ( geoInfo.skeleton ) { + + buffers.vertexWeights.push( faceWeights[ 0 ] ); + buffers.vertexWeights.push( faceWeights[ 1 ] ); + buffers.vertexWeights.push( faceWeights[ 2 ] ); + buffers.vertexWeights.push( faceWeights[ 3 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); + + } + + if ( geoInfo.color ) { + + buffers.colors.push( faceColors[ 0 ] ); + buffers.colors.push( faceColors[ 1 ] ); + buffers.colors.push( faceColors[ 2 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); + buffers.colors.push( faceColors[ i * 3 ] ); + buffers.colors.push( faceColors[ i * 3 + 1 ] ); + buffers.colors.push( faceColors[ i * 3 + 2 ] ); } - if ( geoNode.LayerElementNormal ) { + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] ); + buffers.materialIndex.push( materialIndex ); + buffers.materialIndex.push( materialIndex ); + buffers.materialIndex.push( materialIndex ); } - if ( geoNode.LayerElementUV ) { + if ( geoInfo.normal ) { - geoInfo.uv = []; - var i = 0; + buffers.normal.push( faceNormals[ 0 ] ); + buffers.normal.push( faceNormals[ 1 ] ); + buffers.normal.push( faceNormals[ 2 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); + buffers.normal.push( faceNormals[ i * 3 ] ); + buffers.normal.push( faceNormals[ i * 3 + 1 ] ); + buffers.normal.push( faceNormals[ i * 3 + 2 ] ); - while ( geoNode.LayerElementUV[ i ] ) { - - if ( geoNode.LayerElementUV[ i ].UV ) { + } - geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) ); + if ( geoInfo.uv ) { - } + geoInfo.uv.forEach( function ( uv, j ) { - i ++; + if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; + buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); - } + } ); } - geoInfo.weightTable = {}; + } - if ( skeleton !== null ) { + } - geoInfo.skeleton = skeleton; - skeleton.rawBones.forEach( function ( rawBone, i ) { + addMorphTargets( parentGeo, parentGeoNode, morphTargets, preTransform ) { - // loop over the bone's vertex indices and weights - rawBone.indices.forEach( function ( index, j ) { + if ( morphTargets.length === 0 ) return; + parentGeo.morphTargetsRelative = true; + parentGeo.morphAttributes.position = []; // parentGeo.morphAttributes.normal = []; // not implemented - if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = []; - geoInfo.weightTable[ index ].push( { - id: i, - weight: rawBone.weights[ j ] - } ); + const scope = this; + morphTargets.forEach( function ( morphTarget ) { - } ); + morphTarget.rawTargets.forEach( function ( rawTarget ) { - } ); + const morphGeoNode = fbxTree.Objects.Geometry[ rawTarget.geoID ]; - } + if ( morphGeoNode !== undefined ) { - return geoInfo; + scope.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, rawTarget.name ); - }, - genBuffers: function ( geoInfo ) { + } - var buffers = { - vertex: [], - normal: [], - colors: [], - uvs: [], - materialIndex: [], - vertexWeights: [], - weightsIndices: [] - }; - var polygonIndex = 0; - var faceLength = 0; - var displayedWeightsWarning = false; // these will hold data for a single face + } ); - var facePositionIndexes = []; - var faceNormals = []; - var faceColors = []; - var faceUVs = []; - var faceWeights = []; - var faceWeightIndices = []; - var scope = this; - geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { + } ); - var endOfFace = false; // Face index and vertex index arrays are combined in a single array - // A cube with quad faces looks like this: - // PolygonVertexIndex: *24 { - // a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5 - // } - // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3 - // to find index of last vertex bit shift the index: ^ - 1 + } // a morph geometry node is similar to a standard node, and the node is also contained + // in FBXTree.Objects.Geometry, however it can only have attributes for position, normal + // and a special attribute Index defining which vertices of the original geometry are affected + // Normal and position attributes only have data for the vertices that are affected by the morph - if ( vertexIndex < 0 ) { - vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 + genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) { - endOfFace = true; + const vertexIndices = parentGeoNode.PolygonVertexIndex !== undefined ? parentGeoNode.PolygonVertexIndex.a : []; + const morphPositionsSparse = morphGeoNode.Vertices !== undefined ? morphGeoNode.Vertices.a : []; + const indices = morphGeoNode.Indexes !== undefined ? morphGeoNode.Indexes.a : []; + const length = parentGeo.attributes.position.count * 3; + const morphPositions = new Float32Array( length ); - } + for ( let i = 0; i < indices.length; i ++ ) { - var weightIndices = []; - var weights = []; - facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); + const morphIndex = indices[ i ] * 3; + morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ]; + morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ]; + morphPositions[ morphIndex + 2 ] = morphPositionsSparse[ i * 3 + 2 ]; - if ( geoInfo.color ) { + } // TODO: add morph normal support - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color ); - faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - } + const morphGeoInfo = { + vertexIndices: vertexIndices, + vertexPositions: morphPositions + }; + const morphBuffers = this.genBuffers( morphGeoInfo ); + const positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 ); + positionAttribute.name = name || morphGeoNode.attrName; + positionAttribute.applyMatrix4( preTransform ); + parentGeo.morphAttributes.position.push( positionAttribute ); - if ( geoInfo.skeleton ) { + } // Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists - if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) { - geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) { + parseNormals( NormalNode ) { - weights.push( wt.weight ); - weightIndices.push( wt.id ); + const mappingType = NormalNode.MappingInformationType; + const referenceType = NormalNode.ReferenceInformationType; + const buffer = NormalNode.Normals.a; + let indexBuffer = []; - } ); + if ( referenceType === 'IndexToDirect' ) { - } + if ( 'NormalIndex' in NormalNode ) { - if ( weights.length > 4 ) { + indexBuffer = NormalNode.NormalIndex.a; - if ( ! displayedWeightsWarning ) { + } else if ( 'NormalsIndex' in NormalNode ) { - console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); - displayedWeightsWarning = true; + indexBuffer = NormalNode.NormalsIndex.a; - } + } - var wIndex = [ 0, 0, 0, 0 ]; - var Weight = [ 0, 0, 0, 0 ]; - weights.forEach( function ( weight, weightIndex ) { + } - var currentWeight = weight; - var currentIndex = weightIndices[ weightIndex ]; - Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { + return { + dataSize: 3, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - if ( currentWeight > comparedWeight ) { + } // Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists - comparedWeightArray[ comparedWeightIndex ] = currentWeight; - currentWeight = comparedWeight; - var tmp = wIndex[ comparedWeightIndex ]; - wIndex[ comparedWeightIndex ] = currentIndex; - currentIndex = tmp; - } + parseUVs( UVNode ) { - } ); + const mappingType = UVNode.MappingInformationType; + const referenceType = UVNode.ReferenceInformationType; + const buffer = UVNode.UV.a; + let indexBuffer = []; - } ); - weightIndices = wIndex; - weights = Weight; + if ( referenceType === 'IndexToDirect' ) { - } // if the weight array is shorter than 4 pad with 0s + indexBuffer = UVNode.UVIndex.a; + } - while ( weights.length < 4 ) { + return { + dataSize: 2, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - weights.push( 0 ); - weightIndices.push( 0 ); + } // Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists - } - for ( var i = 0; i < 4; ++ i ) { + parseVertexColors( ColorNode ) { - faceWeights.push( weights[ i ] ); - faceWeightIndices.push( weightIndices[ i ] ); + const mappingType = ColorNode.MappingInformationType; + const referenceType = ColorNode.ReferenceInformationType; + const buffer = ColorNode.Colors.a; + let indexBuffer = []; - } + if ( referenceType === 'IndexToDirect' ) { - } + indexBuffer = ColorNode.ColorIndex.a; - if ( geoInfo.normal ) { + } - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal ); - faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); + return { + dataSize: 4, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - } + } // Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ]; + parseMaterialIndices( MaterialNode ) { - } + const mappingType = MaterialNode.MappingInformationType; + const referenceType = MaterialNode.ReferenceInformationType; - if ( geoInfo.uv ) { + if ( mappingType === 'NoMappingInformation' ) { - geoInfo.uv.forEach( function ( uv, i ) { + return { + dataSize: 1, + buffer: [ 0 ], + indices: [ 0 ], + mappingType: 'AllSame', + referenceType: referenceType + }; - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); + } - if ( faceUVs[ i ] === undefined ) { + const materialIndexBuffer = MaterialNode.Materials.a; // Since materials are stored as indices, there's a bit of a mismatch between FBX and what + // we expect.So we create an intermediate buffer that points to the index in the buffer, + // for conforming with the other functions we've written for other data. - faceUVs[ i ] = []; + const materialIndices = []; - } + for ( let i = 0; i < materialIndexBuffer.length; ++ i ) { - faceUVs[ i ].push( data[ 0 ] ); - faceUVs[ i ].push( data[ 1 ] ); + materialIndices.push( i ); - } ); + } - } + return { + dataSize: 1, + buffer: materialIndexBuffer, + indices: materialIndices, + mappingType: mappingType, + referenceType: referenceType + }; - faceLength ++; + } // Generate a NurbGeometry from a node in FBXTree.Objects.Geometry - if ( endOfFace ) { - scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); - polygonIndex ++; - faceLength = 0; // reset arrays for the next face + parseNurbsGeometry( geoNode ) { - facePositionIndexes = []; - faceNormals = []; - faceColors = []; - faceUVs = []; - faceWeights = []; - faceWeightIndices = []; + if ( THREE.NURBSCurve === undefined ) { - } + console.error( 'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' ); + return new THREE.BufferGeometry(); - } ); - return buffers; - - }, - // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris - genFace: function ( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { - - for ( var i = 2; i < faceLength; i ++ ) { - - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); - - if ( geoInfo.skeleton ) { - - buffers.vertexWeights.push( faceWeights[ 0 ] ); - buffers.vertexWeights.push( faceWeights[ 1 ] ); - buffers.vertexWeights.push( faceWeights[ 2 ] ); - buffers.vertexWeights.push( faceWeights[ 3 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); + } - } + const order = parseInt( geoNode.Order ); - if ( geoInfo.color ) { + if ( isNaN( order ) ) { - buffers.colors.push( faceColors[ 0 ] ); - buffers.colors.push( faceColors[ 1 ] ); - buffers.colors.push( faceColors[ 2 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); - buffers.colors.push( faceColors[ i * 3 ] ); - buffers.colors.push( faceColors[ i * 3 + 1 ] ); - buffers.colors.push( faceColors[ i * 3 + 2 ] ); + console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id ); + return new THREE.BufferGeometry(); - } + } - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { + const degree = order - 1; + const knots = geoNode.KnotVector.a; + const controlPoints = []; + const pointsValues = geoNode.Points.a; - buffers.materialIndex.push( materialIndex ); - buffers.materialIndex.push( materialIndex ); - buffers.materialIndex.push( materialIndex ); + for ( let i = 0, l = pointsValues.length; i < l; i += 4 ) { - } + controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) ); - if ( geoInfo.normal ) { + } - buffers.normal.push( faceNormals[ 0 ] ); - buffers.normal.push( faceNormals[ 1 ] ); - buffers.normal.push( faceNormals[ 2 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); - buffers.normal.push( faceNormals[ i * 3 ] ); - buffers.normal.push( faceNormals[ i * 3 + 1 ] ); - buffers.normal.push( faceNormals[ i * 3 + 2 ] ); + let startKnot, endKnot; - } + if ( geoNode.Form === 'Closed' ) { - if ( geoInfo.uv ) { + controlPoints.push( controlPoints[ 0 ] ); - geoInfo.uv.forEach( function ( uv, j ) { + } else if ( geoNode.Form === 'Periodic' ) { - if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; - buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); + startKnot = degree; + endKnot = knots.length - 1 - startKnot; - } ); + for ( let i = 0; i < degree; ++ i ) { - } + controlPoints.push( controlPoints[ i ] ); } - }, - addMorphTargets: function ( parentGeo, parentGeoNode, morphTargets, preTransform ) { + } + + const curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); + const vertices = curve.getPoints( controlPoints.length * 7 ); + const positions = new Float32Array( vertices.length * 3 ); + vertices.forEach( function ( vertex, i ) { - if ( morphTargets.length === 0 ) return; - parentGeo.morphTargetsRelative = true; - parentGeo.morphAttributes.position = []; // parentGeo.morphAttributes.normal = []; // not implemented + vertex.toArray( positions, i * 3 ); - var scope = this; - morphTargets.forEach( function ( morphTarget ) { + } ); + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); + return geometry; - morphTarget.rawTargets.forEach( function ( rawTarget ) { + } - var morphGeoNode = fbxTree.Objects.Geometry[ rawTarget.geoID ]; + } // parse animation data from FBXTree - if ( morphGeoNode !== undefined ) { - scope.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, rawTarget.name ); + class AnimationParser { - } + // take raw animation clips and turn them into three.js animation clips + parse() { - } ); + const animationClips = []; + const rawClips = this.parseClips(); - } ); + if ( rawClips !== undefined ) { + + for ( const key in rawClips ) { - }, - // a morph geometry node is similar to a standard node, and the node is also contained - // in FBXTree.Objects.Geometry, however it can only have attributes for position, normal - // and a special attribute Index defining which vertices of the original geometry are affected - // Normal and position attributes only have data for the vertices that are affected by the morph - genMorphGeometry: function ( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) { + const rawClip = rawClips[ key ]; + const clip = this.addClip( rawClip ); + animationClips.push( clip ); - var vertexIndices = parentGeoNode.PolygonVertexIndex !== undefined ? parentGeoNode.PolygonVertexIndex.a : []; - var morphPositionsSparse = morphGeoNode.Vertices !== undefined ? morphGeoNode.Vertices.a : []; - var indices = morphGeoNode.Indexes !== undefined ? morphGeoNode.Indexes.a : []; - var length = parentGeo.attributes.position.count * 3; - var morphPositions = new Float32Array( length ); + } - for ( var i = 0; i < indices.length; i ++ ) { + } - var morphIndex = indices[ i ] * 3; - morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ]; - morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ]; - morphPositions[ morphIndex + 2 ] = morphPositionsSparse[ i * 3 + 2 ]; + return animationClips; - } // TODO: add morph normal support + } + parseClips() { - var morphGeoInfo = { - vertexIndices: vertexIndices, - vertexPositions: morphPositions - }; - var morphBuffers = this.genBuffers( morphGeoInfo ); - var positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 ); - positionAttribute.name = name || morphGeoNode.attrName; - positionAttribute.applyMatrix4( preTransform ); - parentGeo.morphAttributes.position.push( positionAttribute ); + // since the actual transformation data is stored in FBXTree.Objects.AnimationCurve, + // if this is undefined we can safely assume there are no animations + if ( fbxTree.Objects.AnimationCurve === undefined ) return undefined; + const curveNodesMap = this.parseAnimationCurveNodes(); + this.parseAnimationCurves( curveNodesMap ); + const layersMap = this.parseAnimationLayers( curveNodesMap ); + const rawClips = this.parseAnimStacks( layersMap ); + return rawClips; - }, - // Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists - parseNormals: function ( NormalNode ) { + } // parse nodes in FBXTree.Objects.AnimationCurveNode + // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) + // and is referenced by an AnimationLayer - var mappingType = NormalNode.MappingInformationType; - var referenceType = NormalNode.ReferenceInformationType; - var buffer = NormalNode.Normals.a; - var indexBuffer = []; - if ( referenceType === 'IndexToDirect' ) { + parseAnimationCurveNodes() { - if ( 'NormalIndex' in NormalNode ) { + const rawCurveNodes = fbxTree.Objects.AnimationCurveNode; + const curveNodesMap = new Map(); - indexBuffer = NormalNode.NormalIndex.a; + for ( const nodeID in rawCurveNodes ) { - } else if ( 'NormalsIndex' in NormalNode ) { + const rawCurveNode = rawCurveNodes[ nodeID ]; - indexBuffer = NormalNode.NormalsIndex.a; + if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) { - } + const curveNode = { + id: rawCurveNode.id, + attr: rawCurveNode.attrName, + curves: {} + }; + curveNodesMap.set( curveNode.id, curveNode ); } - return { - dataSize: 3, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + } - }, - // Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists - parseUVs: function ( UVNode ) { + return curveNodesMap; - var mappingType = UVNode.MappingInformationType; - var referenceType = UVNode.ReferenceInformationType; - var buffer = UVNode.UV.a; - var indexBuffer = []; + } // parse nodes in FBXTree.Objects.AnimationCurve and connect them up to + // previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated + // axis ( e.g. times and values of x rotation) - if ( referenceType === 'IndexToDirect' ) { - indexBuffer = UVNode.UVIndex.a; + parseAnimationCurves( curveNodesMap ) { - } + const rawCurves = fbxTree.Objects.AnimationCurve; // TODO: Many values are identical up to roundoff error, but won't be optimised + // e.g. position times: [0, 0.4, 0. 8] + // position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809] + // clearly, this should be optimised to + // times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809] + // this shows up in nearly every FBX file, and generally time array is length > 100 - return { - dataSize: 2, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType + for ( const nodeID in rawCurves ) { + + const animationCurve = { + id: rawCurves[ nodeID ].id, + times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ), + values: rawCurves[ nodeID ].KeyValueFloat.a }; + const relationships = connections.get( animationCurve.id ); - }, - // Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists - parseVertexColors: function ( ColorNode ) { + if ( relationships !== undefined ) { - var mappingType = ColorNode.MappingInformationType; - var referenceType = ColorNode.ReferenceInformationType; - var buffer = ColorNode.Colors.a; - var indexBuffer = []; + const animationCurveID = relationships.parents[ 0 ].ID; + const animationCurveRelationship = relationships.parents[ 0 ].relationship; - if ( referenceType === 'IndexToDirect' ) { + if ( animationCurveRelationship.match( /X/ ) ) { - indexBuffer = ColorNode.ColorIndex.a; + curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve; - } + } else if ( animationCurveRelationship.match( /Y/ ) ) { - return { - dataSize: 4, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve; - }, - // Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists - parseMaterialIndices: function ( MaterialNode ) { + } else if ( animationCurveRelationship.match( /Z/ ) ) { - var mappingType = MaterialNode.MappingInformationType; - var referenceType = MaterialNode.ReferenceInformationType; + curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve; - if ( mappingType === 'NoMappingInformation' ) { + } else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) { - return { - dataSize: 1, - buffer: [ 0 ], - indices: [ 0 ], - mappingType: 'AllSame', - referenceType: referenceType - }; + curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve; + + } } - var materialIndexBuffer = MaterialNode.Materials.a; // Since materials are stored as indices, there's a bit of a mismatch between FBX and what - // we expect.So we create an intermediate buffer that points to the index in the buffer, - // for conforming with the other functions we've written for other data. + } - var materialIndices = []; + } // parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references + // to various AnimationCurveNodes and is referenced by an AnimationStack node + // note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack - for ( var i = 0; i < materialIndexBuffer.length; ++ i ) { - materialIndices.push( i ); + parseAnimationLayers( curveNodesMap ) { - } + const rawLayers = fbxTree.Objects.AnimationLayer; + const layersMap = new Map(); - return { - dataSize: 1, - buffer: materialIndexBuffer, - indices: materialIndices, - mappingType: mappingType, - referenceType: referenceType - }; + for ( const nodeID in rawLayers ) { - }, - // Generate a NurbGeometry from a node in FBXTree.Objects.Geometry - parseNurbsGeometry: function ( geoNode ) { + const layerCurveNodes = []; + const connection = connections.get( parseInt( nodeID ) ); - if ( THREE.NURBSCurve === undefined ) { + if ( connection !== undefined ) { - console.error( 'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' ); - return new THREE.BufferGeometry(); + // all the animationCurveNodes used in the layer + const children = connection.children; + children.forEach( function ( child, i ) { - } + if ( curveNodesMap.has( child.ID ) ) { - var order = parseInt( geoNode.Order ); + const curveNode = curveNodesMap.get( child.ID ); // check that the curves are defined for at least one axis, otherwise ignore the curveNode - if ( isNaN( order ) ) { + if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) { - console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id ); - return new THREE.BufferGeometry(); + if ( layerCurveNodes[ i ] === undefined ) { - } + const modelID = connections.get( child.ID ).parents.filter( function ( parent ) { - var degree = order - 1; - var knots = geoNode.KnotVector.a; - var controlPoints = []; - var pointsValues = geoNode.Points.a; + return parent.relationship !== undefined; - for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) { + } )[ 0 ].ID; - controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) ); + if ( modelID !== undefined ) { - } + const rawModel = fbxTree.Objects.Model[ modelID.toString() ]; - var startKnot, endKnot; + if ( rawModel === undefined ) { - if ( geoNode.Form === 'Closed' ) { + console.warn( 'THREE.FBXLoader: Encountered a unused curve.', child ); + return; - controlPoints.push( controlPoints[ 0 ] ); + } + + const node = { + modelName: rawModel.attrName ? THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', + ID: rawModel.id, + initialPosition: [ 0, 0, 0 ], + initialRotation: [ 0, 0, 0 ], + initialScale: [ 1, 1, 1 ] + }; + sceneGraph.traverse( function ( child ) { - } else if ( geoNode.Form === 'Periodic' ) { + if ( child.ID === rawModel.id ) { - startKnot = degree; - endKnot = knots.length - 1 - startKnot; + node.transform = child.matrix; + if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder; - for ( var i = 0; i < degree; ++ i ) { + } - controlPoints.push( controlPoints[ i ] ); + } ); + if ( ! node.transform ) node.transform = new THREE.Matrix4(); // if the animated model is pre rotated, we'll have to apply the pre rotations to every + // animation value as well - } + if ( 'PreRotation' in rawModel ) node.preRotation = rawModel.PreRotation.value; + if ( 'PostRotation' in rawModel ) node.postRotation = rawModel.PostRotation.value; + layerCurveNodes[ i ] = node; - } + } - var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); - var vertices = curve.getPoints( controlPoints.length * 7 ); - var positions = new Float32Array( vertices.length * 3 ); - vertices.forEach( function ( vertex, i ) { + } - vertex.toArray( positions, i * 3 ); + if ( layerCurveNodes[ i ] ) layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - } ); - var geometry = new THREE.BufferGeometry(); - geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); - return geometry; + } else if ( curveNode.curves.morph !== undefined ) { - } - }; // parse animation data from FBXTree + if ( layerCurveNodes[ i ] === undefined ) { - function AnimationParser() {} + const deformerID = connections.get( child.ID ).parents.filter( function ( parent ) { - AnimationParser.prototype = { - constructor: AnimationParser, - // take raw animation clips and turn them into three.js animation clips - parse: function () { + return parent.relationship !== undefined; - var animationClips = []; - var rawClips = this.parseClips(); + } )[ 0 ].ID; + const morpherID = connections.get( deformerID ).parents[ 0 ].ID; + const geoID = connections.get( morpherID ).parents[ 0 ].ID; // assuming geometry is not used in more than one model - if ( rawClips !== undefined ) { + const modelID = connections.get( geoID ).parents[ 0 ].ID; + const rawModel = fbxTree.Objects.Model[ modelID ]; + const node = { + modelName: rawModel.attrName ? THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', + morphName: fbxTree.Objects.Deformer[ deformerID ].attrName + }; + layerCurveNodes[ i ] = node; - for ( var key in rawClips ) { + } - var rawClip = rawClips[ key ]; - var clip = this.addClip( rawClip ); - animationClips.push( clip ); + layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - } + } - } + } + + } ); + layersMap.set( parseInt( nodeID ), layerCurveNodes ); - return animationClips; + } - }, - parseClips: function () { + } - // since the actual transformation data is stored in FBXTree.Objects.AnimationCurve, - // if this is undefined we can safely assume there are no animations - if ( fbxTree.Objects.AnimationCurve === undefined ) return undefined; - var curveNodesMap = this.parseAnimationCurveNodes(); - this.parseAnimationCurves( curveNodesMap ); - var layersMap = this.parseAnimationLayers( curveNodesMap ); - var rawClips = this.parseAnimStacks( layersMap ); - return rawClips; + return layersMap; - }, - // parse nodes in FBXTree.Objects.AnimationCurveNode - // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) - // and is referenced by an AnimationLayer - parseAnimationCurveNodes: function () { + } // parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation + // hierarchy. Each Stack node will be used to create a THREE.AnimationClip - var rawCurveNodes = fbxTree.Objects.AnimationCurveNode; - var curveNodesMap = new Map(); - for ( var nodeID in rawCurveNodes ) { + parseAnimStacks( layersMap ) { - var rawCurveNode = rawCurveNodes[ nodeID ]; + const rawStacks = fbxTree.Objects.AnimationStack; // connect the stacks (clips) up to the layers - if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) { + const rawClips = {}; - var curveNode = { - id: rawCurveNode.id, - attr: rawCurveNode.attrName, - curves: {} - }; - curveNodesMap.set( curveNode.id, curveNode ); + for ( const nodeID in rawStacks ) { - } + const children = connections.get( parseInt( nodeID ) ).children; - } + if ( children.length > 1 ) { - return curveNodesMap; + // it seems like stacks will always be associated with a single layer. But just in case there are files + // where there are multiple layers per stack, we'll display a warning + console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' ); - }, - // parse nodes in FBXTree.Objects.AnimationCurve and connect them up to - // previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated - // axis ( e.g. times and values of x rotation) - parseAnimationCurves: function ( curveNodesMap ) { + } - var rawCurves = fbxTree.Objects.AnimationCurve; // TODO: Many values are identical up to roundoff error, but won't be optimised - // e.g. position times: [0, 0.4, 0. 8] - // position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809] - // clearly, this should be optimised to - // times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809] - // this shows up in nearly every FBX file, and generally time array is length > 100 + const layer = layersMap.get( children[ 0 ].ID ); + rawClips[ nodeID ] = { + name: rawStacks[ nodeID ].attrName, + layer: layer + }; - for ( var nodeID in rawCurves ) { + } - var animationCurve = { - id: rawCurves[ nodeID ].id, - times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ), - values: rawCurves[ nodeID ].KeyValueFloat.a - }; - var relationships = connections.get( animationCurve.id ); + return rawClips; - if ( relationships !== undefined ) { + } - var animationCurveID = relationships.parents[ 0 ].ID; - var animationCurveRelationship = relationships.parents[ 0 ].relationship; + addClip( rawClip ) { - if ( animationCurveRelationship.match( /X/ ) ) { + let tracks = []; + const scope = this; + rawClip.layer.forEach( function ( rawTracks ) { - curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve; + tracks = tracks.concat( scope.generateTracks( rawTracks ) ); - } else if ( animationCurveRelationship.match( /Y/ ) ) { + } ); + return new THREE.AnimationClip( rawClip.name, - 1, tracks ); - curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve; + } - } else if ( animationCurveRelationship.match( /Z/ ) ) { + generateTracks( rawTracks ) { - curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve; + const tracks = []; + let initialPosition = new THREE.Vector3(); + let initialRotation = new THREE.Quaternion(); + let initialScale = new THREE.Vector3(); + if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale ); + initialPosition = initialPosition.toArray(); + initialRotation = new THREE.Euler().setFromQuaternion( initialRotation, rawTracks.eulerOrder ).toArray(); + initialScale = initialScale.toArray(); - } else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) { + if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) { - curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve; + const positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' ); + if ( positionTrack !== undefined ) tracks.push( positionTrack ); - } + } - } + if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { - } + const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder ); + if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); - }, - // parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references - // to various AnimationCurveNodes and is referenced by an AnimationStack node - // note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack - parseAnimationLayers: function ( curveNodesMap ) { + } - var rawLayers = fbxTree.Objects.AnimationLayer; - var layersMap = new Map(); + if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) { - for ( var nodeID in rawLayers ) { + const scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' ); + if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); - var layerCurveNodes = []; - var connection = connections.get( parseInt( nodeID ) ); + } - if ( connection !== undefined ) { + if ( rawTracks.DeformPercent !== undefined ) { - // all the animationCurveNodes used in the layer - var children = connection.children; - children.forEach( function ( child, i ) { + const morphTrack = this.generateMorphTrack( rawTracks ); + if ( morphTrack !== undefined ) tracks.push( morphTrack ); - if ( curveNodesMap.has( child.ID ) ) { + } - var curveNode = curveNodesMap.get( child.ID ); // check that the curves are defined for at least one axis, otherwise ignore the curveNode + return tracks; - if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) { + } - if ( layerCurveNodes[ i ] === undefined ) { + generateVectorTrack( modelName, curves, initialValue, type ) { - var modelID = connections.get( child.ID ).parents.filter( function ( parent ) { + const times = this.getTimesForAllAxes( curves ); + const values = this.getKeyframeTrackValues( times, curves, initialValue ); + return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values ); - return parent.relationship !== undefined; + } - } )[ 0 ].ID; + generateRotationTrack( modelName, curves, initialValue, preRotation, postRotation, eulerOrder ) { - if ( modelID !== undefined ) { + if ( curves.x !== undefined ) { - var rawModel = fbxTree.Objects.Model[ modelID.toString() ]; + this.interpolateRotations( curves.x ); + curves.x.values = curves.x.values.map( THREE.MathUtils.degToRad ); - if ( rawModel === undefined ) { + } - console.warn( 'THREE.FBXLoader: Encountered a unused curve.', child ); - return; + if ( curves.y !== undefined ) { - } + this.interpolateRotations( curves.y ); + curves.y.values = curves.y.values.map( THREE.MathUtils.degToRad ); - var node = { - modelName: rawModel.attrName ? THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', - ID: rawModel.id, - initialPosition: [ 0, 0, 0 ], - initialRotation: [ 0, 0, 0 ], - initialScale: [ 1, 1, 1 ] - }; - sceneGraph.traverse( function ( child ) { + } - if ( child.ID === rawModel.id ) { + if ( curves.z !== undefined ) { - node.transform = child.matrix; - if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder; + this.interpolateRotations( curves.z ); + curves.z.values = curves.z.values.map( THREE.MathUtils.degToRad ); - } + } - } ); - if ( ! node.transform ) node.transform = new THREE.Matrix4(); // if the animated model is pre rotated, we'll have to apply the pre rotations to every - // animation value as well + const times = this.getTimesForAllAxes( curves ); + const values = this.getKeyframeTrackValues( times, curves, initialValue ); - if ( 'PreRotation' in rawModel ) node.preRotation = rawModel.PreRotation.value; - if ( 'PostRotation' in rawModel ) node.postRotation = rawModel.PostRotation.value; - layerCurveNodes[ i ] = node; + if ( preRotation !== undefined ) { - } + preRotation = preRotation.map( THREE.MathUtils.degToRad ); + preRotation.push( eulerOrder ); + preRotation = new THREE.Euler().fromArray( preRotation ); + preRotation = new THREE.Quaternion().setFromEuler( preRotation ); - } + } - if ( layerCurveNodes[ i ] ) layerCurveNodes[ i ][ curveNode.attr ] = curveNode; + if ( postRotation !== undefined ) { - } else if ( curveNode.curves.morph !== undefined ) { + postRotation = postRotation.map( THREE.MathUtils.degToRad ); + postRotation.push( eulerOrder ); + postRotation = new THREE.Euler().fromArray( postRotation ); + postRotation = new THREE.Quaternion().setFromEuler( postRotation ).invert(); - if ( layerCurveNodes[ i ] === undefined ) { + } - var deformerID = connections.get( child.ID ).parents.filter( function ( parent ) { + const quaternion = new THREE.Quaternion(); + const euler = new THREE.Euler(); + const quaternionValues = []; - return parent.relationship !== undefined; + for ( let i = 0; i < values.length; i += 3 ) { - } )[ 0 ].ID; - var morpherID = connections.get( deformerID ).parents[ 0 ].ID; - var geoID = connections.get( morpherID ).parents[ 0 ].ID; // assuming geometry is not used in more than one model + euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], eulerOrder ); + quaternion.setFromEuler( euler ); + if ( preRotation !== undefined ) quaternion.premultiply( preRotation ); + if ( postRotation !== undefined ) quaternion.multiply( postRotation ); + quaternion.toArray( quaternionValues, i / 3 * 4 ); - var modelID = connections.get( geoID ).parents[ 0 ].ID; - var rawModel = fbxTree.Objects.Model[ modelID ]; - var node = { - modelName: rawModel.attrName ? THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', - morphName: fbxTree.Objects.Deformer[ deformerID ].attrName - }; - layerCurveNodes[ i ] = node; + } - } + return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); - layerCurveNodes[ i ][ curveNode.attr ] = curveNode; + } - } + generateMorphTrack( rawTracks ) { - } + const curves = rawTracks.DeformPercent.curves.morph; + const values = curves.values.map( function ( val ) { - } ); - layersMap.set( parseInt( nodeID ), layerCurveNodes ); + return val / 100; - } + } ); + const morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ]; + return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values ); - } + } // For all animated objects, times are defined separately for each axis + // Here we'll combine the times into one sorted array without duplicates - return layersMap; - }, - // parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation - // hierarchy. Each Stack node will be used to create a THREE.AnimationClip - parseAnimStacks: function ( layersMap ) { + getTimesForAllAxes( curves ) { - var rawStacks = fbxTree.Objects.AnimationStack; // connect the stacks (clips) up to the layers + let times = []; // first join together the times for each axis, if defined - var rawClips = {}; + if ( curves.x !== undefined ) times = times.concat( curves.x.times ); + if ( curves.y !== undefined ) times = times.concat( curves.y.times ); + if ( curves.z !== undefined ) times = times.concat( curves.z.times ); // then sort them - for ( var nodeID in rawStacks ) { + times = times.sort( function ( a, b ) { - var children = connections.get( parseInt( nodeID ) ).children; + return a - b; - if ( children.length > 1 ) { + } ); // and remove duplicates - // it seems like stacks will always be associated with a single layer. But just in case there are files - // where there are multiple layers per stack, we'll display a warning - console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' ); + if ( times.length > 1 ) { - } + let targetIndex = 1; + let lastValue = times[ 0 ]; - var layer = layersMap.get( children[ 0 ].ID ); - rawClips[ nodeID ] = { - name: rawStacks[ nodeID ].attrName, - layer: layer - }; + for ( let i = 1; i < times.length; i ++ ) { - } + const currentValue = times[ i ]; - return rawClips; + if ( currentValue !== lastValue ) { - }, - addClip: function ( rawClip ) { + times[ targetIndex ] = currentValue; + lastValue = currentValue; + targetIndex ++; - var tracks = []; - var scope = this; - rawClip.layer.forEach( function ( rawTracks ) { + } - tracks = tracks.concat( scope.generateTracks( rawTracks ) ); + } - } ); - return new THREE.AnimationClip( rawClip.name, - 1, tracks ); + times = times.slice( 0, targetIndex ); - }, - generateTracks: function ( rawTracks ) { + } - var tracks = []; - var initialPosition = new THREE.Vector3(); - var initialRotation = new THREE.Quaternion(); - var initialScale = new THREE.Vector3(); - if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale ); - initialPosition = initialPosition.toArray(); - initialRotation = new THREE.Euler().setFromQuaternion( initialRotation, rawTracks.eulerOrder ).toArray(); - initialScale = initialScale.toArray(); + return times; - if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) { + } - var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' ); - if ( positionTrack !== undefined ) tracks.push( positionTrack ); + getKeyframeTrackValues( times, curves, initialValue ) { - } + const prevValue = initialValue; + const values = []; + let xIndex = - 1; + let yIndex = - 1; + let zIndex = - 1; + times.forEach( function ( time ) { - if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { + if ( curves.x ) xIndex = curves.x.times.indexOf( time ); + if ( curves.y ) yIndex = curves.y.times.indexOf( time ); + if ( curves.z ) zIndex = curves.z.times.indexOf( time ); // if there is an x value defined for this frame, use that - var rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder ); - if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); + if ( xIndex !== - 1 ) { - } + const xValue = curves.x.values[ xIndex ]; + values.push( xValue ); + prevValue[ 0 ] = xValue; - if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) { + } else { - var scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' ); - if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); + // otherwise use the x value from the previous frame + values.push( prevValue[ 0 ] ); } - if ( rawTracks.DeformPercent !== undefined ) { + if ( yIndex !== - 1 ) { - var morphTrack = this.generateMorphTrack( rawTracks ); - if ( morphTrack !== undefined ) tracks.push( morphTrack ); + const yValue = curves.y.values[ yIndex ]; + values.push( yValue ); + prevValue[ 1 ] = yValue; - } + } else { - return tracks; + values.push( prevValue[ 1 ] ); - }, - generateVectorTrack: function ( modelName, curves, initialValue, type ) { + } - var times = this.getTimesForAllAxes( curves ); - var values = this.getKeyframeTrackValues( times, curves, initialValue ); - return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values ); + if ( zIndex !== - 1 ) { - }, - generateRotationTrack: function ( modelName, curves, initialValue, preRotation, postRotation, eulerOrder ) { + const zValue = curves.z.values[ zIndex ]; + values.push( zValue ); + prevValue[ 2 ] = zValue; - if ( curves.x !== undefined ) { + } else { - this.interpolateRotations( curves.x ); - curves.x.values = curves.x.values.map( THREE.MathUtils.degToRad ); + values.push( prevValue[ 2 ] ); } - if ( curves.y !== undefined ) { + } ); + return values; - this.interpolateRotations( curves.y ); - curves.y.values = curves.y.values.map( THREE.MathUtils.degToRad ); + } // Rotations are defined as THREE.Euler angles which can have values of any size + // These will be converted to quaternions which don't support values greater than + // PI, so we'll interpolate large rotations - } - if ( curves.z !== undefined ) { + interpolateRotations( curve ) { - this.interpolateRotations( curves.z ); - curves.z.values = curves.z.values.map( THREE.MathUtils.degToRad ); + for ( let i = 1; i < curve.values.length; i ++ ) { - } + const initialValue = curve.values[ i - 1 ]; + const valuesSpan = curve.values[ i ] - initialValue; + const absoluteSpan = Math.abs( valuesSpan ); - var times = this.getTimesForAllAxes( curves ); - var values = this.getKeyframeTrackValues( times, curves, initialValue ); + if ( absoluteSpan >= 180 ) { - if ( preRotation !== undefined ) { + const numSubIntervals = absoluteSpan / 180; + const step = valuesSpan / numSubIntervals; + let nextValue = initialValue + step; + const initialTime = curve.times[ i - 1 ]; + const timeSpan = curve.times[ i ] - initialTime; + const interval = timeSpan / numSubIntervals; + let nextTime = initialTime + interval; + const interpolatedTimes = []; + const interpolatedValues = []; - preRotation = preRotation.map( THREE.MathUtils.degToRad ); - preRotation.push( eulerOrder ); - preRotation = new THREE.Euler().fromArray( preRotation ); - preRotation = new THREE.Quaternion().setFromEuler( preRotation ); + while ( nextTime < curve.times[ i ] ) { - } + interpolatedTimes.push( nextTime ); + nextTime += interval; + interpolatedValues.push( nextValue ); + nextValue += step; - if ( postRotation !== undefined ) { + } - postRotation = postRotation.map( THREE.MathUtils.degToRad ); - postRotation.push( eulerOrder ); - postRotation = new THREE.Euler().fromArray( postRotation ); - postRotation = new THREE.Quaternion().setFromEuler( postRotation ).invert(); + curve.times = inject( curve.times, i, interpolatedTimes ); + curve.values = inject( curve.values, i, interpolatedValues ); } - var quaternion = new THREE.Quaternion(); - var euler = new THREE.Euler(); - var quaternionValues = []; + } - for ( var i = 0; i < values.length; i += 3 ) { + } - euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], eulerOrder ); - quaternion.setFromEuler( euler ); - if ( preRotation !== undefined ) quaternion.premultiply( preRotation ); - if ( postRotation !== undefined ) quaternion.multiply( postRotation ); - quaternion.toArray( quaternionValues, i / 3 * 4 ); + } // parse an FBX file in ASCII format - } - return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); + class TextParser { - }, - generateMorphTrack: function ( rawTracks ) { + getPrevNode() { - var curves = rawTracks.DeformPercent.curves.morph; - var values = curves.values.map( function ( val ) { + return this.nodeStack[ this.currentIndent - 2 ]; - return val / 100; + } - } ); - var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ]; - return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values ); + getCurrentNode() { - }, - // For all animated objects, times are defined separately for each axis - // Here we'll combine the times into one sorted array without duplicates - getTimesForAllAxes: function ( curves ) { + return this.nodeStack[ this.currentIndent - 1 ]; - var times = []; // first join together the times for each axis, if defined + } - if ( curves.x !== undefined ) times = times.concat( curves.x.times ); - if ( curves.y !== undefined ) times = times.concat( curves.y.times ); - if ( curves.z !== undefined ) times = times.concat( curves.z.times ); // then sort them + getCurrentProp() { - times = times.sort( function ( a, b ) { + return this.currentProp; - return a - b; + } - } ); // and remove duplicates + pushStack( node ) { - if ( times.length > 1 ) { + this.nodeStack.push( node ); + this.currentIndent += 1; - var targetIndex = 1; - var lastValue = times[ 0 ]; + } - for ( var i = 1; i < times.length; i ++ ) { + popStack() { - var currentValue = times[ i ]; + this.nodeStack.pop(); + this.currentIndent -= 1; - if ( currentValue !== lastValue ) { + } - times[ targetIndex ] = currentValue; - lastValue = currentValue; - targetIndex ++; + setCurrentProp( val, name ) { - } + this.currentProp = val; + this.currentPropName = name; - } + } - times = times.slice( 0, targetIndex ); + parse( text ) { - } + this.currentIndent = 0; + this.allNodes = new FBXTree(); + this.nodeStack = []; + this.currentProp = []; + this.currentPropName = ''; + const scope = this; + const split = text.split( /[\r\n]+/ ); + split.forEach( function ( line, i ) { - return times; + const matchComment = line.match( /^[\s\t]*;/ ); + const matchEmpty = line.match( /^[\s\t]*$/ ); + if ( matchComment || matchEmpty ) return; + const matchBeginning = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):(.*){', '' ); + const matchProperty = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):[\\s\\t\\r\\n](.*)' ); + const matchEnd = line.match( '^\\t{' + ( scope.currentIndent - 1 ) + '}}' ); - }, - getKeyframeTrackValues: function ( times, curves, initialValue ) { + if ( matchBeginning ) { - var prevValue = initialValue; - var values = []; - var xIndex = - 1; - var yIndex = - 1; - var zIndex = - 1; - times.forEach( function ( time ) { + scope.parseNodeBegin( line, matchBeginning ); - if ( curves.x ) xIndex = curves.x.times.indexOf( time ); - if ( curves.y ) yIndex = curves.y.times.indexOf( time ); - if ( curves.z ) zIndex = curves.z.times.indexOf( time ); // if there is an x value defined for this frame, use that + } else if ( matchProperty ) { - if ( xIndex !== - 1 ) { + scope.parseNodeProperty( line, matchProperty, split[ ++ i ] ); - var xValue = curves.x.values[ xIndex ]; - values.push( xValue ); - prevValue[ 0 ] = xValue; + } else if ( matchEnd ) { - } else { + scope.popStack(); - // otherwise use the x value from the previous frame - values.push( prevValue[ 0 ] ); + } else if ( line.match( /^[^\s\t}]/ ) ) { - } + // large arrays are split over multiple lines terminated with a ',' character + // if this is encountered the line needs to be joined to the previous line + scope.parseNodePropertyContinued( line ); - if ( yIndex !== - 1 ) { + } - var yValue = curves.y.values[ yIndex ]; - values.push( yValue ); - prevValue[ 1 ] = yValue; + } ); + return this.allNodes; - } else { + } - values.push( prevValue[ 1 ] ); + parseNodeBegin( line, property ) { - } + const nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); + const nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { - if ( zIndex !== - 1 ) { + return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); - var zValue = curves.z.values[ zIndex ]; - values.push( zValue ); - prevValue[ 2 ] = zValue; + } ); + const node = { + name: nodeName + }; + const attrs = this.parseNodeAttr( nodeAttrs ); + const currentNode = this.getCurrentNode(); // a top node - } else { + if ( this.currentIndent === 0 ) { - values.push( prevValue[ 2 ] ); + this.allNodes.add( nodeName, node ); - } + } else { - } ); - return values; + // a subnode + // if the subnode already exists, append it + if ( nodeName in currentNode ) { - }, - // Rotations are defined as THREE.Euler angles which can have values of any size - // These will be converted to quaternions which don't support values greater than - // PI, so we'll interpolate large rotations - interpolateRotations: function ( curve ) { + // special case Pose needs PoseNodes as an array + if ( nodeName === 'PoseNode' ) { - for ( var i = 1; i < curve.values.length; i ++ ) { + currentNode.PoseNode.push( node ); - var initialValue = curve.values[ i - 1 ]; - var valuesSpan = curve.values[ i ] - initialValue; - var absoluteSpan = Math.abs( valuesSpan ); + } else if ( currentNode[ nodeName ].id !== undefined ) { - if ( absoluteSpan >= 180 ) { + currentNode[ nodeName ] = {}; + currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ]; - var numSubIntervals = absoluteSpan / 180; - var step = valuesSpan / numSubIntervals; - var nextValue = initialValue + step; - var initialTime = curve.times[ i - 1 ]; - var timeSpan = curve.times[ i ] - initialTime; - var interval = timeSpan / numSubIntervals; - var nextTime = initialTime + interval; - var interpolatedTimes = []; - var interpolatedValues = []; + } - while ( nextTime < curve.times[ i ] ) { + if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node; - interpolatedTimes.push( nextTime ); - nextTime += interval; - interpolatedValues.push( nextValue ); - nextValue += step; + } else if ( typeof attrs.id === 'number' ) { - } + currentNode[ nodeName ] = {}; + currentNode[ nodeName ][ attrs.id ] = node; - curve.times = inject( curve.times, i, interpolatedTimes ); - curve.values = inject( curve.values, i, interpolatedValues ); + } else if ( nodeName !== 'Properties70' ) { - } + if ( nodeName === 'PoseNode' ) currentNode[ nodeName ] = [ node ]; else currentNode[ nodeName ] = node; } } - }; // parse an FBX file in ASCII format - function TextParser() {} + if ( typeof attrs.id === 'number' ) node.id = attrs.id; + if ( attrs.name !== '' ) node.attrName = attrs.name; + if ( attrs.type !== '' ) node.attrType = attrs.type; + this.pushStack( node ); - TextParser.prototype = { - constructor: TextParser, - getPrevNode: function () { + } - return this.nodeStack[ this.currentIndent - 2 ]; + parseNodeAttr( attrs ) { - }, - getCurrentNode: function () { + let id = attrs[ 0 ]; - return this.nodeStack[ this.currentIndent - 1 ]; + if ( attrs[ 0 ] !== '' ) { - }, - getCurrentProp: function () { + id = parseInt( attrs[ 0 ] ); - return this.currentProp; + if ( isNaN( id ) ) { - }, - pushStack: function ( node ) { + id = attrs[ 0 ]; - this.nodeStack.push( node ); - this.currentIndent += 1; + } - }, - popStack: function () { + } - this.nodeStack.pop(); - this.currentIndent -= 1; + let name = '', + type = ''; - }, - setCurrentProp: function ( val, name ) { + if ( attrs.length > 1 ) { - this.currentProp = val; - this.currentPropName = name; + name = attrs[ 1 ].replace( /^(\w+)::/, '' ); + type = attrs[ 2 ]; - }, - parse: function ( text ) { + } - this.currentIndent = 0; - this.allNodes = new FBXTree(); - this.nodeStack = []; - this.currentProp = []; - this.currentPropName = ''; - var scope = this; - var split = text.split( /[\r\n]+/ ); - split.forEach( function ( line, i ) { + return { + id: id, + name: name, + type: type + }; - var matchComment = line.match( /^[\s\t]*;/ ); - var matchEmpty = line.match( /^[\s\t]*$/ ); - if ( matchComment || matchEmpty ) return; - var matchBeginning = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):(.*){', '' ); - var matchProperty = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):[\\s\\t\\r\\n](.*)' ); - var matchEnd = line.match( '^\\t{' + ( scope.currentIndent - 1 ) + '}}' ); + } - if ( matchBeginning ) { + parseNodeProperty( line, property, contentLine ) { - scope.parseNodeBegin( line, matchBeginning ); + let propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); + let propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); // for special case: base64 image data follows "Content: ," line + // Content: , + // "/9j/4RDaRXhpZgAATU0A..." - } else if ( matchProperty ) { + if ( propName === 'Content' && propValue === ',' ) { - scope.parseNodeProperty( line, matchProperty, split[ ++ i ] ); + propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); - } else if ( matchEnd ) { + } - scope.popStack(); + const currentNode = this.getCurrentNode(); + const parentName = currentNode.name; - } else if ( line.match( /^[^\s\t}]/ ) ) { + if ( parentName === 'Properties70' ) { - // large arrays are split over multiple lines terminated with a ',' character - // if this is encountered the line needs to be joined to the previous line - scope.parseNodePropertyContinued( line ); + this.parseNodeSpecialProperty( line, propName, propValue ); + return; - } + } // Connections - } ); - return this.allNodes; - }, - parseNodeBegin: function ( line, property ) { + if ( propName === 'C' ) { - var nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); - var nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { + const connProps = propValue.split( ',' ).slice( 1 ); + const from = parseInt( connProps[ 0 ] ); + const to = parseInt( connProps[ 1 ] ); + let rest = propValue.split( ',' ).slice( 3 ); + rest = rest.map( function ( elem ) { - return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); + return elem.trim().replace( /^"/, '' ); } ); - var node = { - name: nodeName - }; - var attrs = this.parseNodeAttr( nodeAttrs ); - var currentNode = this.getCurrentNode(); // a top node - - if ( this.currentIndent === 0 ) { - - this.allNodes.add( nodeName, node ); + propName = 'connections'; + propValue = [ from, to ]; + append( propValue, rest ); - } else { - - // a subnode - // if the subnode already exists, append it - if ( nodeName in currentNode ) { + if ( currentNode[ propName ] === undefined ) { - // special case Pose needs PoseNodes as an array - if ( nodeName === 'PoseNode' ) { + currentNode[ propName ] = []; - currentNode.PoseNode.push( node ); + } - } else if ( currentNode[ nodeName ].id !== undefined ) { + } // Node - currentNode[ nodeName ] = {}; - currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ]; - } + if ( propName === 'Node' ) currentNode.id = propValue; // connections - if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node; + if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) { - } else if ( typeof attrs.id === 'number' ) { + currentNode[ propName ].push( propValue ); - currentNode[ nodeName ] = {}; - currentNode[ nodeName ][ attrs.id ] = node; + } else { - } else if ( nodeName !== 'Properties70' ) { + if ( propName !== 'a' ) currentNode[ propName ] = propValue; else currentNode.a = propValue; - if ( nodeName === 'PoseNode' ) currentNode[ nodeName ] = [ node ]; else currentNode[ nodeName ] = node; + } - } + this.setCurrentProp( currentNode, propName ); // convert string to array, unless it ends in ',' in which case more will be added to it - } + if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { - if ( typeof attrs.id === 'number' ) node.id = attrs.id; - if ( attrs.name !== '' ) node.attrName = attrs.name; - if ( attrs.type !== '' ) node.attrType = attrs.type; - this.pushStack( node ); + currentNode.a = parseNumberArray( propValue ); - }, - parseNodeAttr: function ( attrs ) { + } - var id = attrs[ 0 ]; + } - if ( attrs[ 0 ] !== '' ) { + parseNodePropertyContinued( line ) { - id = parseInt( attrs[ 0 ] ); + const currentNode = this.getCurrentNode(); + currentNode.a += line; // if the line doesn't end in ',' we have reached the end of the property value + // so convert the string to an array - if ( isNaN( id ) ) { + if ( line.slice( - 1 ) !== ',' ) { - id = attrs[ 0 ]; + currentNode.a = parseNumberArray( currentNode.a ); - } + } - } + } // parse "Property70" - var name = '', - type = ''; - if ( attrs.length > 1 ) { + parseNodeSpecialProperty( line, propName, propValue ) { - name = attrs[ 1 ].replace( /^(\w+)::/, '' ); - type = attrs[ 2 ]; + // split this + // P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 + // into array like below + // ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ] + const props = propValue.split( '",' ).map( function ( prop ) { - } + return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); - return { - id: id, - name: name, - type: type - }; + } ); + const innerPropName = props[ 0 ]; + const innerPropType1 = props[ 1 ]; + const innerPropType2 = props[ 2 ]; + const innerPropFlag = props[ 3 ]; + let innerPropValue = props[ 4 ]; // cast values where needed, otherwise leave as strings + + switch ( innerPropType1 ) { + + case 'int': + case 'enum': + case 'bool': + case 'ULongLong': + case 'double': + case 'Number': + case 'FieldOfView': + innerPropValue = parseFloat( innerPropValue ); + break; - }, - parseNodeProperty: function ( line, property, contentLine ) { + case 'Color': + case 'ColorRGB': + case 'Vector3D': + case 'Lcl_Translation': + case 'Lcl_Rotation': + case 'Lcl_Scaling': + innerPropValue = parseNumberArray( innerPropValue ); + break; - var propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); - var propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); // for special case: base64 image data follows "Content: ," line - // Content: , - // "/9j/4RDaRXhpZgAATU0A..." + } // CAUTION: these props must append to parent's parent - if ( propName === 'Content' && propValue === ',' ) { - propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); + this.getPrevNode()[ innerPropName ] = { + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue + }; + this.setCurrentProp( this.getPrevNode(), innerPropName ); - } + } - var currentNode = this.getCurrentNode(); - var parentName = currentNode.name; + } // Parse an FBX file in Binary format - if ( parentName === 'Properties70' ) { - this.parseNodeSpecialProperty( line, propName, propValue ); - return; + class BinaryParser { - } // Connections + parse( buffer ) { + const reader = new BinaryReader( buffer ); + reader.skip( 23 ); // skip magic 23 bytes - if ( propName === 'C' ) { + const version = reader.getUint32(); - var connProps = propValue.split( ',' ).slice( 1 ); - var from = parseInt( connProps[ 0 ] ); - var to = parseInt( connProps[ 1 ] ); - var rest = propValue.split( ',' ).slice( 3 ); - rest = rest.map( function ( elem ) { + if ( version < 6400 ) { - return elem.trim().replace( /^"/, '' ); + throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + version ); - } ); - propName = 'connections'; - propValue = [ from, to ]; - append( propValue, rest ); + } - if ( currentNode[ propName ] === undefined ) { + const allNodes = new FBXTree(); - currentNode[ propName ] = []; + while ( ! this.endOfContent( reader ) ) { - } + const node = this.parseNode( reader, version ); + if ( node !== null ) allNodes.add( node.name, node ); - } // Node + } + return allNodes; - if ( propName === 'Node' ) currentNode.id = propValue; // connections + } // Check if reader has reached the end of content. - if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) { - currentNode[ propName ].push( propValue ); + endOfContent( reader ) { - } else { + // footer size: 160bytes + 16-byte alignment padding + // - 16bytes: magic + // - padding til 16-byte alignment (at least 1byte?) + // (seems like some exporters embed fixed 15 or 16bytes?) + // - 4bytes: magic + // - 4bytes: version + // - 120bytes: zero + // - 16bytes: magic + if ( reader.size() % 16 === 0 ) { - if ( propName !== 'a' ) currentNode[ propName ] = propValue; else currentNode.a = propValue; + return ( reader.getOffset() + 160 + 16 & ~ 0xf ) >= reader.size(); - } + } else { - this.setCurrentProp( currentNode, propName ); // convert string to array, unless it ends in ',' in which case more will be added to it + return reader.getOffset() + 160 + 16 >= reader.size(); - if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { + } - currentNode.a = parseNumberArray( propValue ); + } // recursively parse nodes until the end of the file is reached - } - }, - parseNodePropertyContinued: function ( line ) { + parseNode( reader, version ) { - var currentNode = this.getCurrentNode(); - currentNode.a += line; // if the line doesn't end in ',' we have reached the end of the property value - // so convert the string to an array + const node = {}; // The first three data sizes depends on version. - if ( line.slice( - 1 ) !== ',' ) { + const endOffset = version >= 7500 ? reader.getUint64() : reader.getUint32(); + const numProperties = version >= 7500 ? reader.getUint64() : reader.getUint32(); + version >= 7500 ? reader.getUint64() : reader.getUint32(); // the returned propertyListLen is not used - currentNode.a = parseNumberArray( currentNode.a ); + const nameLen = reader.getUint8(); + const name = reader.getString( nameLen ); // Regards this node as NULL-record if endOffset is zero - } + if ( endOffset === 0 ) return null; + const propertyList = []; - }, - // parse "Property70" - parseNodeSpecialProperty: function ( line, propName, propValue ) { + for ( let i = 0; i < numProperties; i ++ ) { - // split this - // P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 - // into array like below - // ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ] - var props = propValue.split( '",' ).map( function ( prop ) { + propertyList.push( this.parseProperty( reader ) ); - return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); + } // Regards the first three elements in propertyList as id, attrName, and attrType - } ); - var innerPropName = props[ 0 ]; - var innerPropType1 = props[ 1 ]; - var innerPropType2 = props[ 2 ]; - var innerPropFlag = props[ 3 ]; - var innerPropValue = props[ 4 ]; // cast values where needed, otherwise leave as strings - - switch ( innerPropType1 ) { - - case 'int': - case 'enum': - case 'bool': - case 'ULongLong': - case 'double': - case 'Number': - case 'FieldOfView': - innerPropValue = parseFloat( innerPropValue ); - break; - case 'Color': - case 'ColorRGB': - case 'Vector3D': - case 'Lcl_Translation': - case 'Lcl_Rotation': - case 'Lcl_Scaling': - innerPropValue = parseNumberArray( innerPropValue ); - break; + const id = propertyList.length > 0 ? propertyList[ 0 ] : ''; + const attrName = propertyList.length > 1 ? propertyList[ 1 ] : ''; + const attrType = propertyList.length > 2 ? propertyList[ 2 ] : ''; // check if this node represents just a single property + // like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]} - } // CAUTION: these props must append to parent's parent + node.singleProperty = numProperties === 1 && reader.getOffset() === endOffset ? true : false; + while ( endOffset > reader.getOffset() ) { - this.getPrevNode()[ innerPropName ] = { - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue - }; - this.setCurrentProp( this.getPrevNode(), innerPropName ); + const subNode = this.parseNode( reader, version ); + if ( subNode !== null ) this.parseSubNode( name, node, subNode ); } - }; // Parse an FBX file in Binary format - function BinaryParser() {} + node.propertyList = propertyList; // raw property list used by parent - BinaryParser.prototype = { - constructor: BinaryParser, - parse: function ( buffer ) { + if ( typeof id === 'number' ) node.id = id; + if ( attrName !== '' ) node.attrName = attrName; + if ( attrType !== '' ) node.attrType = attrType; + if ( name !== '' ) node.name = name; + return node; - var reader = new BinaryReader( buffer ); - reader.skip( 23 ); // skip magic 23 bytes + } - var version = reader.getUint32(); + parseSubNode( name, node, subNode ) { - if ( version < 6400 ) { + // special case: child node is single property + if ( subNode.singleProperty === true ) { - throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + version ); + const value = subNode.propertyList[ 0 ]; - } + if ( Array.isArray( value ) ) { - var allNodes = new FBXTree(); + node[ subNode.name ] = subNode; + subNode.a = value; - while ( ! this.endOfContent( reader ) ) { + } else { - var node = this.parseNode( reader, version ); - if ( node !== null ) allNodes.add( node.name, node ); + node[ subNode.name ] = value; } - return allNodes; + } else if ( name === 'Connections' && subNode.name === 'C' ) { - }, - // Check if reader has reached the end of content. - endOfContent: function ( reader ) { + const array = []; + subNode.propertyList.forEach( function ( property, i ) { - // footer size: 160bytes + 16-byte alignment padding - // - 16bytes: magic - // - padding til 16-byte alignment (at least 1byte?) - // (seems like some exporters embed fixed 15 or 16bytes?) - // - 4bytes: magic - // - 4bytes: version - // - 120bytes: zero - // - 16bytes: magic - if ( reader.size() % 16 === 0 ) { + // first Connection is FBX type (OO, OP, etc.). We'll discard these + if ( i !== 0 ) array.push( property ); - return ( reader.getOffset() + 160 + 16 & ~ 0xf ) >= reader.size(); + } ); - } else { + if ( node.connections === undefined ) { - return reader.getOffset() + 160 + 16 >= reader.size(); + node.connections = []; } - }, - // recursively parse nodes until the end of the file is reached - parseNode: function ( reader, version ) { - - var node = {}; // The first three data sizes depends on version. - - var endOffset = version >= 7500 ? reader.getUint64() : reader.getUint32(); - var numProperties = version >= 7500 ? reader.getUint64() : reader.getUint32(); - version >= 7500 ? reader.getUint64() : reader.getUint32(); // the returned propertyListLen is not used - - var nameLen = reader.getUint8(); - var name = reader.getString( nameLen ); // Regards this node as NULL-record if endOffset is zero - - if ( endOffset === 0 ) return null; - var propertyList = []; - - for ( var i = 0; i < numProperties; i ++ ) { + node.connections.push( array ); - propertyList.push( this.parseProperty( reader ) ); + } else if ( subNode.name === 'Properties70' ) { - } // Regards the first three elements in propertyList as id, attrName, and attrType + const keys = Object.keys( subNode ); + keys.forEach( function ( key ) { + node[ key ] = subNode[ key ]; - var id = propertyList.length > 0 ? propertyList[ 0 ] : ''; - var attrName = propertyList.length > 1 ? propertyList[ 1 ] : ''; - var attrType = propertyList.length > 2 ? propertyList[ 2 ] : ''; // check if this node represents just a single property - // like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]} - - node.singleProperty = numProperties === 1 && reader.getOffset() === endOffset ? true : false; + } ); - while ( endOffset > reader.getOffset() ) { + } else if ( name === 'Properties70' && subNode.name === 'P' ) { - var subNode = this.parseNode( reader, version ); - if ( subNode !== null ) this.parseSubNode( name, node, subNode ); + let innerPropName = subNode.propertyList[ 0 ]; + let innerPropType1 = subNode.propertyList[ 1 ]; + const innerPropType2 = subNode.propertyList[ 2 ]; + const innerPropFlag = subNode.propertyList[ 3 ]; + let innerPropValue; + if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); + if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); - } + if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { - node.propertyList = propertyList; // raw property list used by parent + innerPropValue = [ subNode.propertyList[ 4 ], subNode.propertyList[ 5 ], subNode.propertyList[ 6 ] ]; - if ( typeof id === 'number' ) node.id = id; - if ( attrName !== '' ) node.attrName = attrName; - if ( attrType !== '' ) node.attrType = attrType; - if ( name !== '' ) node.name = name; - return node; + } else { - }, - parseSubNode: function ( name, node, subNode ) { + innerPropValue = subNode.propertyList[ 4 ]; - // special case: child node is single property - if ( subNode.singleProperty === true ) { + } // this will be copied to parent, see above - var value = subNode.propertyList[ 0 ]; - if ( Array.isArray( value ) ) { + node[ innerPropName ] = { + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue + }; - node[ subNode.name ] = subNode; - subNode.a = value; + } else if ( node[ subNode.name ] === undefined ) { - } else { + if ( typeof subNode.id === 'number' ) { - node[ subNode.name ] = value; + node[ subNode.name ] = {}; + node[ subNode.name ][ subNode.id ] = subNode; - } + } else { - } else if ( name === 'Connections' && subNode.name === 'C' ) { + node[ subNode.name ] = subNode; - var array = []; - subNode.propertyList.forEach( function ( property, i ) { + } - // first Connection is FBX type (OO, OP, etc.). We'll discard these - if ( i !== 0 ) array.push( property ); + } else { - } ); + if ( subNode.name === 'PoseNode' ) { - if ( node.connections === undefined ) { + if ( ! Array.isArray( node[ subNode.name ] ) ) { - node.connections = []; + node[ subNode.name ] = [ node[ subNode.name ] ]; } - node.connections.push( array ); - - } else if ( subNode.name === 'Properties70' ) { + node[ subNode.name ].push( subNode ); - var keys = Object.keys( subNode ); - keys.forEach( function ( key ) { + } else if ( node[ subNode.name ][ subNode.id ] === undefined ) { - node[ key ] = subNode[ key ]; + node[ subNode.name ][ subNode.id ] = subNode; - } ); + } - } else if ( name === 'Properties70' && subNode.name === 'P' ) { + } - var innerPropName = subNode.propertyList[ 0 ]; - var innerPropType1 = subNode.propertyList[ 1 ]; - var innerPropType2 = subNode.propertyList[ 2 ]; - var innerPropFlag = subNode.propertyList[ 3 ]; - var innerPropValue; - if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); - if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); + } - if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { + parseProperty( reader ) { - innerPropValue = [ subNode.propertyList[ 4 ], subNode.propertyList[ 5 ], subNode.propertyList[ 6 ] ]; + const type = reader.getString( 1 ); + let length; - } else { + switch ( type ) { - innerPropValue = subNode.propertyList[ 4 ]; + case 'C': + return reader.getBoolean(); - } // this will be copied to parent, see above + case 'D': + return reader.getFloat64(); + case 'F': + return reader.getFloat32(); - node[ innerPropName ] = { - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue - }; + case 'I': + return reader.getInt32(); - } else if ( node[ subNode.name ] === undefined ) { + case 'L': + return reader.getInt64(); - if ( typeof subNode.id === 'number' ) { + case 'R': + length = reader.getUint32(); + return reader.getArrayBuffer( length ); - node[ subNode.name ] = {}; - node[ subNode.name ][ subNode.id ] = subNode; + case 'S': + length = reader.getUint32(); + return reader.getString( length ); - } else { + case 'Y': + return reader.getInt16(); - node[ subNode.name ] = subNode; + case 'b': + case 'c': + case 'd': + case 'f': + case 'i': + case 'l': + const arrayLength = reader.getUint32(); + const encoding = reader.getUint32(); // 0: non-compressed, 1: compressed - } + const compressedLength = reader.getUint32(); - } else { + if ( encoding === 0 ) { - if ( subNode.name === 'PoseNode' ) { + switch ( type ) { - if ( ! Array.isArray( node[ subNode.name ] ) ) { + case 'b': + case 'c': + return reader.getBooleanArray( arrayLength ); - node[ subNode.name ] = [ node[ subNode.name ] ]; + case 'd': + return reader.getFloat64Array( arrayLength ); - } + case 'f': + return reader.getFloat32Array( arrayLength ); - node[ subNode.name ].push( subNode ); + case 'i': + return reader.getInt32Array( arrayLength ); - } else if ( node[ subNode.name ][ subNode.id ] === undefined ) { + case 'l': + return reader.getInt64Array( arrayLength ); - node[ subNode.name ][ subNode.id ] = subNode; + } } - } + if ( typeof fflate === 'undefined' ) { - }, - parseProperty: function ( reader ) { + console.error( 'THREE.FBXLoader: External library fflate.min.js required.' ); - var type = reader.getString( 1 ); + } - switch ( type ) { + const data = fflate.unzlibSync( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef - case 'C': - return reader.getBoolean(); + const reader2 = new BinaryReader( data.buffer ); - case 'D': - return reader.getFloat64(); + switch ( type ) { - case 'F': - return reader.getFloat32(); + case 'b': + case 'c': + return reader2.getBooleanArray( arrayLength ); - case 'I': - return reader.getInt32(); + case 'd': + return reader2.getFloat64Array( arrayLength ); - case 'L': - return reader.getInt64(); + case 'f': + return reader2.getFloat32Array( arrayLength ); - case 'R': - var length = reader.getUint32(); - return reader.getArrayBuffer( length ); + case 'i': + return reader2.getInt32Array( arrayLength ); - case 'S': - var length = reader.getUint32(); - return reader.getString( length ); + case 'l': + return reader2.getInt64Array( arrayLength ); - case 'Y': - return reader.getInt16(); + } - case 'b': - case 'c': - case 'd': - case 'f': - case 'i': - case 'l': - var arrayLength = reader.getUint32(); - var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed + default: + throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); - var compressedLength = reader.getUint32(); + } - if ( encoding === 0 ) { + } - switch ( type ) { + } - case 'b': - case 'c': - return reader.getBooleanArray( arrayLength ); + class BinaryReader { - case 'd': - return reader.getFloat64Array( arrayLength ); + constructor( buffer, littleEndian ) { - case 'f': - return reader.getFloat32Array( arrayLength ); + this.dv = new DataView( buffer ); + this.offset = 0; + this.littleEndian = littleEndian !== undefined ? littleEndian : true; - case 'i': - return reader.getInt32Array( arrayLength ); + } - case 'l': - return reader.getInt64Array( arrayLength ); + getOffset() { - } + return this.offset; - } + } - if ( typeof fflate === 'undefined' ) { + size() { - console.error( 'THREE.FBXLoader: External library fflate.min.js required.' ); + return this.dv.buffer.byteLength; - } + } - var data = fflate.unzlibSync( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef + skip( length ) { - var reader2 = new BinaryReader( data.buffer ); + this.offset += length; - switch ( type ) { + } // seems like true/false representation depends on exporter. + // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) + // then sees LSB. - case 'b': - case 'c': - return reader2.getBooleanArray( arrayLength ); - case 'd': - return reader2.getFloat64Array( arrayLength ); + getBoolean() { - case 'f': - return reader2.getFloat32Array( arrayLength ); + return ( this.getUint8() & 1 ) === 1; - case 'i': - return reader2.getInt32Array( arrayLength ); + } - case 'l': - return reader2.getInt64Array( arrayLength ); + getBooleanArray( size ) { - } + const a = []; - default: - throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); + for ( let i = 0; i < size; i ++ ) { - } + a.push( this.getBoolean() ); } - }; - function BinaryReader( buffer, littleEndian ) { - - this.dv = new DataView( buffer ); - this.offset = 0; - this.littleEndian = littleEndian !== undefined ? littleEndian : true; + return a; } - BinaryReader.prototype = { - constructor: BinaryReader, - getOffset: function () { - - return this.offset; + getUint8() { - }, - size: function () { + const value = this.dv.getUint8( this.offset ); + this.offset += 1; + return value; - return this.dv.buffer.byteLength; + } - }, - skip: function ( length ) { + getInt16() { - this.offset += length; + const value = this.dv.getInt16( this.offset, this.littleEndian ); + this.offset += 2; + return value; - }, - // seems like true/false representation depends on exporter. - // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) - // then sees LSB. - getBoolean: function () { + } - return ( this.getUint8() & 1 ) === 1; + getInt32() { - }, - getBooleanArray: function ( size ) { + const value = this.dv.getInt32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - var a = []; + } - for ( var i = 0; i < size; i ++ ) { + getInt32Array( size ) { - a.push( this.getBoolean() ); + const a = []; - } + for ( let i = 0; i < size; i ++ ) { - return a; + a.push( this.getInt32() ); - }, - getUint8: function () { + } - var value = this.dv.getUint8( this.offset ); - this.offset += 1; - return value; + return a; - }, - getInt16: function () { + } - var value = this.dv.getInt16( this.offset, this.littleEndian ); - this.offset += 2; - return value; + getUint32() { - }, - getInt32: function () { + const value = this.dv.getUint32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - var value = this.dv.getInt32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + } // JavaScript doesn't support 64-bit integer so calculate this here + // 1 << 32 will return 1 so using multiply operation instead here. + // There's a possibility that this method returns wrong value if the value + // is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. + // TODO: safely handle 64-bit integer - }, - getInt32Array: function ( size ) { - var a = []; + getInt64() { - for ( var i = 0; i < size; i ++ ) { + let low, high; - a.push( this.getInt32() ); + if ( this.littleEndian ) { - } + low = this.getUint32(); + high = this.getUint32(); - return a; + } else { - }, - getUint32: function () { + high = this.getUint32(); + low = this.getUint32(); - var value = this.dv.getUint32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + } // calculate negative value - }, - // JavaScript doesn't support 64-bit integer so calculate this here - // 1 << 32 will return 1 so using multiply operation instead here. - // There's a possibility that this method returns wrong value if the value - // is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. - // TODO: safely handle 64-bit integer - getInt64: function () { - var low, high; + if ( high & 0x80000000 ) { - if ( this.littleEndian ) { + high = ~ high & 0xFFFFFFFF; + low = ~ low & 0xFFFFFFFF; + if ( low === 0xFFFFFFFF ) high = high + 1 & 0xFFFFFFFF; + low = low + 1 & 0xFFFFFFFF; + return - ( high * 0x100000000 + low ); - low = this.getUint32(); - high = this.getUint32(); + } - } else { + return high * 0x100000000 + low; - high = this.getUint32(); - low = this.getUint32(); + } - } // calculate negative value + getInt64Array( size ) { + const a = []; - if ( high & 0x80000000 ) { + for ( let i = 0; i < size; i ++ ) { - high = ~ high & 0xFFFFFFFF; - low = ~ low & 0xFFFFFFFF; - if ( low === 0xFFFFFFFF ) high = high + 1 & 0xFFFFFFFF; - low = low + 1 & 0xFFFFFFFF; - return - ( high * 0x100000000 + low ); + a.push( this.getInt64() ); - } + } - return high * 0x100000000 + low; + return a; - }, - getInt64Array: function ( size ) { + } // Note: see getInt64() comment - var a = []; - for ( var i = 0; i < size; i ++ ) { + getUint64() { - a.push( this.getInt64() ); + let low, high; - } + if ( this.littleEndian ) { - return a; + low = this.getUint32(); + high = this.getUint32(); - }, - // Note: see getInt64() comment - getUint64: function () { + } else { - var low, high; + high = this.getUint32(); + low = this.getUint32(); - if ( this.littleEndian ) { + } - low = this.getUint32(); - high = this.getUint32(); + return high * 0x100000000 + low; - } else { + } - high = this.getUint32(); - low = this.getUint32(); + getFloat32() { - } + const value = this.dv.getFloat32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - return high * 0x100000000 + low; + } - }, - getFloat32: function () { + getFloat32Array( size ) { - var value = this.dv.getFloat32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + const a = []; - }, - getFloat32Array: function ( size ) { + for ( let i = 0; i < size; i ++ ) { - var a = []; + a.push( this.getFloat32() ); - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getFloat32() ); + return a; - } + } - return a; + getFloat64() { - }, - getFloat64: function () { + const value = this.dv.getFloat64( this.offset, this.littleEndian ); + this.offset += 8; + return value; - var value = this.dv.getFloat64( this.offset, this.littleEndian ); - this.offset += 8; - return value; + } - }, - getFloat64Array: function ( size ) { + getFloat64Array( size ) { - var a = []; + const a = []; - for ( var i = 0; i < size; i ++ ) { + for ( let i = 0; i < size; i ++ ) { - a.push( this.getFloat64() ); + a.push( this.getFloat64() ); - } + } - return a; + return a; - }, - getArrayBuffer: function ( size ) { + } - var value = this.dv.buffer.slice( this.offset, this.offset + size ); - this.offset += size; - return value; + getArrayBuffer( size ) { - }, - getString: function ( size ) { + const value = this.dv.buffer.slice( this.offset, this.offset + size ); + this.offset += size; + return value; - // note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead - var a = []; + } - for ( var i = 0; i < size; i ++ ) { + getString( size ) { - a[ i ] = this.getUint8(); + // note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead + let a = []; - } + for ( let i = 0; i < size; i ++ ) { - var nullByte = a.indexOf( 0 ); - if ( nullByte >= 0 ) a = a.slice( 0, nullByte ); - return THREE.LoaderUtils.decodeText( new Uint8Array( a ) ); + a[ i ] = this.getUint8(); } - }; // FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) - // and BinaryParser( FBX Binary format) - - function FBXTree() {} - FBXTree.prototype = { - constructor: FBXTree, - add: function ( key, val ) { + const nullByte = a.indexOf( 0 ); + if ( nullByte >= 0 ) a = a.slice( 0, nullByte ); + return THREE.LoaderUtils.decodeText( new Uint8Array( a ) ); - this[ key ] = val; - - } - }; // ************** UTILITY FUNCTIONS ************** + } - function isFbxFormatBinary( buffer ) { + } // FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) + // and BinaryParser( FBX Binary format) - var CORRECT = 'Kaydara FBX Binary \0'; - return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length ); - } + class FBXTree { - function isFbxFormatASCII( text ) { + add( key, val ) { - var CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ]; - var cursor = 0; + this[ key ] = val; - function read( offset ) { + } - var result = text[ offset - 1 ]; - text = text.slice( cursor + offset ); - cursor ++; - return result; + } // ************** UTILITY FUNCTIONS ************** - } - for ( var i = 0; i < CORRECT.length; ++ i ) { + function isFbxFormatBinary( buffer ) { - var num = read( 1 ); + const CORRECT = 'Kaydara FBX Binary \0'; + return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length ); - if ( num === CORRECT[ i ] ) { + } - return false; + function isFbxFormatASCII( text ) { - } + const CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ]; + let cursor = 0; - } + function read( offset ) { - return true; + const result = text[ offset - 1 ]; + text = text.slice( cursor + offset ); + cursor ++; + return result; } - function getFbxVersion( text ) { + for ( let i = 0; i < CORRECT.length; ++ i ) { - var versionRegExp = /FBXVersion: (\d+)/; - var match = text.match( versionRegExp ); + const num = read( 1 ); - if ( match ) { + if ( num === CORRECT[ i ] ) { - var version = parseInt( match[ 1 ] ); - return version; + return false; } - throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); + } - } // Converts FBX ticks into real time seconds. + return true; + } - function convertFBXTimeToSeconds( time ) { + function getFbxVersion( text ) { - return time / 46186158000; + const versionRegExp = /FBXVersion: (\d+)/; + const match = text.match( versionRegExp ); - } + if ( match ) { - var dataArray = []; // extracts the data from the correct position in the FBX array based on indexing type + const version = parseInt( match[ 1 ] ); + return version; - function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { + } - var index; + throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); - switch ( infoObject.mappingType ) { + } // Converts FBX ticks into real time seconds. - case 'ByPolygonVertex': - index = polygonVertexIndex; - break; - case 'ByPolygon': - index = polygonIndex; - break; + function convertFBXTimeToSeconds( time ) { - case 'ByVertice': - index = vertexIndex; - break; + return time / 46186158000; - case 'AllSame': - index = infoObject.indices[ 0 ]; - break; + } - default: - console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType ); + const dataArray = []; // extracts the data from the correct position in the FBX array based on indexing type - } + function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ]; - var from = index * infoObject.dataSize; - var to = from + infoObject.dataSize; - return slice( dataArray, infoObject.buffer, from, to ); + let index; - } + switch ( infoObject.mappingType ) { - var tempEuler = new THREE.Euler(); - var tempVec = new THREE.Vector3(); // generate transformation from FBX transform data - // ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm - // ref: http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_transformations_2main_8cxx-example.html,topicNumber=cpp_ref__transformations_2main_8cxx_example_htmlfc10a1e1-b18d-4e72-9dc0-70d0f1959f5e + case 'ByPolygonVertex': + index = polygonVertexIndex; + break; - function generateTransform( transformData ) { + case 'ByPolygon': + index = polygonIndex; + break; - var lTranslationM = new THREE.Matrix4(); - var lPreRotationM = new THREE.Matrix4(); - var lRotationM = new THREE.Matrix4(); - var lPostRotationM = new THREE.Matrix4(); - var lScalingM = new THREE.Matrix4(); - var lScalingPivotM = new THREE.Matrix4(); - var lScalingOffsetM = new THREE.Matrix4(); - var lRotationOffsetM = new THREE.Matrix4(); - var lRotationPivotM = new THREE.Matrix4(); - var lParentGX = new THREE.Matrix4(); - var lParentLX = new THREE.Matrix4(); - var lGlobalT = new THREE.Matrix4(); - var inheritType = transformData.inheritType ? transformData.inheritType : 0; - if ( transformData.translation ) lTranslationM.setPosition( tempVec.fromArray( transformData.translation ) ); + case 'ByVertice': + index = vertexIndex; + break; - if ( transformData.preRotation ) { + case 'AllSame': + index = infoObject.indices[ 0 ]; + break; - var array = transformData.preRotation.map( THREE.MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + default: + console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType ); - } + } - if ( transformData.rotation ) { + if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ]; + const from = index * infoObject.dataSize; + const to = from + infoObject.dataSize; + return slice( dataArray, infoObject.buffer, from, to ); + + } + + const tempEuler = new THREE.Euler(); + const tempVec = new THREE.Vector3(); // generate transformation from FBX transform data + // ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm + // ref: http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_transformations_2main_8cxx-example.html,topicNumber=cpp_ref__transformations_2main_8cxx_example_htmlfc10a1e1-b18d-4e72-9dc0-70d0f1959f5e + + function generateTransform( transformData ) { + + const lTranslationM = new THREE.Matrix4(); + const lPreRotationM = new THREE.Matrix4(); + const lRotationM = new THREE.Matrix4(); + const lPostRotationM = new THREE.Matrix4(); + const lScalingM = new THREE.Matrix4(); + const lScalingPivotM = new THREE.Matrix4(); + const lScalingOffsetM = new THREE.Matrix4(); + const lRotationOffsetM = new THREE.Matrix4(); + const lRotationPivotM = new THREE.Matrix4(); + const lParentGX = new THREE.Matrix4(); + const lParentLX = new THREE.Matrix4(); + const lGlobalT = new THREE.Matrix4(); + const inheritType = transformData.inheritType ? transformData.inheritType : 0; + if ( transformData.translation ) lTranslationM.setPosition( tempVec.fromArray( transformData.translation ) ); + + if ( transformData.preRotation ) { + + const array = transformData.preRotation.map( THREE.MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - var array = transformData.rotation.map( THREE.MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + } - } + if ( transformData.rotation ) { - if ( transformData.postRotation ) { + const array = transformData.rotation.map( THREE.MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - var array = transformData.postRotation.map( THREE.MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - lPostRotationM.invert(); + } - } + if ( transformData.postRotation ) { - if ( transformData.scale ) lScalingM.scale( tempVec.fromArray( transformData.scale ) ); // Pivots and offsets + const array = transformData.postRotation.map( THREE.MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + lPostRotationM.invert(); - if ( transformData.scalingOffset ) lScalingOffsetM.setPosition( tempVec.fromArray( transformData.scalingOffset ) ); - if ( transformData.scalingPivot ) lScalingPivotM.setPosition( tempVec.fromArray( transformData.scalingPivot ) ); - if ( transformData.rotationOffset ) lRotationOffsetM.setPosition( tempVec.fromArray( transformData.rotationOffset ) ); - if ( transformData.rotationPivot ) lRotationPivotM.setPosition( tempVec.fromArray( transformData.rotationPivot ) ); // parent transform + } - if ( transformData.parentMatrixWorld ) { + if ( transformData.scale ) lScalingM.scale( tempVec.fromArray( transformData.scale ) ); // Pivots and offsets - lParentLX.copy( transformData.parentMatrix ); - lParentGX.copy( transformData.parentMatrixWorld ); + if ( transformData.scalingOffset ) lScalingOffsetM.setPosition( tempVec.fromArray( transformData.scalingOffset ) ); + if ( transformData.scalingPivot ) lScalingPivotM.setPosition( tempVec.fromArray( transformData.scalingPivot ) ); + if ( transformData.rotationOffset ) lRotationOffsetM.setPosition( tempVec.fromArray( transformData.rotationOffset ) ); + if ( transformData.rotationPivot ) lRotationPivotM.setPosition( tempVec.fromArray( transformData.rotationPivot ) ); // parent transform - } + if ( transformData.parentMatrixWorld ) { - var lLRM = new THREE.Matrix4().copy( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ); // Global Rotation + lParentLX.copy( transformData.parentMatrix ); + lParentGX.copy( transformData.parentMatrixWorld ); - var lParentGRM = new THREE.Matrix4(); - lParentGRM.extractRotation( lParentGX ); // Global Shear*Scaling + } - var lParentTM = new THREE.Matrix4(); - lParentTM.copyPosition( lParentGX ); - var lParentGSM = new THREE.Matrix4(); - var lParentGRSM = new THREE.Matrix4().copy( lParentTM ).invert().multiply( lParentGX ); - lParentGSM.copy( lParentGRM ).invert().multiply( lParentGRSM ); - var lLSM = lScalingM; - var lGlobalRS = new THREE.Matrix4(); + const lLRM = new THREE.Matrix4().copy( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ); // Global Rotation - if ( inheritType === 0 ) { + const lParentGRM = new THREE.Matrix4(); + lParentGRM.extractRotation( lParentGX ); // Global Shear*Scaling - lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM ).multiply( lLSM ); + const lParentTM = new THREE.Matrix4(); + lParentTM.copyPosition( lParentGX ); + const lParentGSM = new THREE.Matrix4(); + const lParentGRSM = new THREE.Matrix4().copy( lParentTM ).invert().multiply( lParentGX ); + lParentGSM.copy( lParentGRM ).invert().multiply( lParentGRSM ); + const lLSM = lScalingM; + const lGlobalRS = new THREE.Matrix4(); - } else if ( inheritType === 1 ) { + if ( inheritType === 0 ) { - lGlobalRS.copy( lParentGRM ).multiply( lParentGSM ).multiply( lLRM ).multiply( lLSM ); + lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM ).multiply( lLSM ); - } else { + } else if ( inheritType === 1 ) { - var lParentLSM = new THREE.Matrix4().scale( new THREE.Vector3().setFromMatrixScale( lParentLX ) ); - var lParentLSM_inv = new THREE.Matrix4().copy( lParentLSM ).invert(); - var lParentGSM_noLocal = new THREE.Matrix4().copy( lParentGSM ).multiply( lParentLSM_inv ); - lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM_noLocal ).multiply( lLSM ); + lGlobalRS.copy( lParentGRM ).multiply( lParentGSM ).multiply( lLRM ).multiply( lLSM ); - } + } else { - var lRotationPivotM_inv = new THREE.Matrix4(); - lRotationPivotM_inv.copy( lRotationPivotM ).invert(); - var lScalingPivotM_inv = new THREE.Matrix4(); - lScalingPivotM_inv.copy( lScalingPivotM ).invert(); // Calculate the local transform matrix + const lParentLSM = new THREE.Matrix4().scale( new THREE.Vector3().setFromMatrixScale( lParentLX ) ); + const lParentLSM_inv = new THREE.Matrix4().copy( lParentLSM ).invert(); + const lParentGSM_noLocal = new THREE.Matrix4().copy( lParentGSM ).multiply( lParentLSM_inv ); + lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM_noLocal ).multiply( lLSM ); - var lTransform = new THREE.Matrix4(); - lTransform.copy( lTranslationM ).multiply( lRotationOffsetM ).multiply( lRotationPivotM ).multiply( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ).multiply( lRotationPivotM_inv ).multiply( lScalingOffsetM ).multiply( lScalingPivotM ).multiply( lScalingM ).multiply( lScalingPivotM_inv ); - var lLocalTWithAllPivotAndOffsetInfo = new THREE.Matrix4().copyPosition( lTransform ); - var lGlobalTranslation = new THREE.Matrix4().copy( lParentGX ).multiply( lLocalTWithAllPivotAndOffsetInfo ); - lGlobalT.copyPosition( lGlobalTranslation ); - lTransform = new THREE.Matrix4().copy( lGlobalT ).multiply( lGlobalRS ); // from global to local + } - lTransform.premultiply( lParentGX.invert() ); - return lTransform; + const lRotationPivotM_inv = new THREE.Matrix4(); + lRotationPivotM_inv.copy( lRotationPivotM ).invert(); + const lScalingPivotM_inv = new THREE.Matrix4(); + lScalingPivotM_inv.copy( lScalingPivotM ).invert(); // Calculate the local transform matrix - } // Returns the three.js intrinsic THREE.Euler order corresponding to FBX extrinsic THREE.Euler order - // ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html + let lTransform = new THREE.Matrix4(); + lTransform.copy( lTranslationM ).multiply( lRotationOffsetM ).multiply( lRotationPivotM ).multiply( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ).multiply( lRotationPivotM_inv ).multiply( lScalingOffsetM ).multiply( lScalingPivotM ).multiply( lScalingM ).multiply( lScalingPivotM_inv ); + const lLocalTWithAllPivotAndOffsetInfo = new THREE.Matrix4().copyPosition( lTransform ); + const lGlobalTranslation = new THREE.Matrix4().copy( lParentGX ).multiply( lLocalTWithAllPivotAndOffsetInfo ); + lGlobalT.copyPosition( lGlobalTranslation ); + lTransform = new THREE.Matrix4().copy( lGlobalT ).multiply( lGlobalRS ); // from global to local + lTransform.premultiply( lParentGX.invert() ); + return lTransform; - function getEulerOrder( order ) { + } // Returns the three.js intrinsic THREE.Euler order corresponding to FBX extrinsic THREE.Euler order + // ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html - order = order || 0; - var enums = [ 'ZYX', // -> XYZ extrinsic - 'YZX', // -> XZY extrinsic - 'XZY', // -> YZX extrinsic - 'ZXY', // -> YXZ extrinsic - 'YXZ', // -> ZXY extrinsic - 'XYZ' // -> ZYX extrinsic - //'SphericXYZ', // not possible to support - ]; - if ( order === 6 ) { + function getEulerOrder( order ) { - console.warn( 'THREE.FBXLoader: unsupported THREE.Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); - return enums[ 0 ]; + order = order || 0; + const enums = [ 'ZYX', // -> XYZ extrinsic + 'YZX', // -> XZY extrinsic + 'XZY', // -> YZX extrinsic + 'ZXY', // -> YXZ extrinsic + 'YXZ', // -> ZXY extrinsic + 'XYZ' // -> ZYX extrinsic + //'SphericXYZ', // not possible to support + ]; - } + if ( order === 6 ) { - return enums[ order ]; + console.warn( 'THREE.FBXLoader: unsupported THREE.Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); + return enums[ 0 ]; - } // Parses comma separated list of numbers and returns them an array. - // Used internally by the TextParser + } + return enums[ order ]; - function parseNumberArray( value ) { + } // Parses comma separated list of numbers and returns them an array. + // Used internally by the TextParser - var array = value.split( ',' ).map( function ( val ) { - return parseFloat( val ); + function parseNumberArray( value ) { - } ); - return array; + const array = value.split( ',' ).map( function ( val ) { - } + return parseFloat( val ); - function convertArrayBufferToString( buffer, from, to ) { + } ); + return array; - if ( from === undefined ) from = 0; - if ( to === undefined ) to = buffer.byteLength; - return THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); + } - } + function convertArrayBufferToString( buffer, from, to ) { - function append( a, b ) { + if ( from === undefined ) from = 0; + if ( to === undefined ) to = buffer.byteLength; + return THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); - for ( var i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { + } - a[ j ] = b[ i ]; + function append( a, b ) { - } + for ( let i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { - } + a[ j ] = b[ i ]; - function slice( a, b, from, to ) { + } - for ( var i = from, j = 0; i < to; i ++, j ++ ) { + } - a[ j ] = b[ i ]; + function slice( a, b, from, to ) { - } + for ( let i = from, j = 0; i < to; i ++, j ++ ) { - return a; + a[ j ] = b[ i ]; - } // inject array a2 into array a1 at index + } + return a; - function inject( a1, index, a2 ) { + } // inject array a2 into array a1 at index - return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) ); - } + function inject( a1, index, a2 ) { - return FBXLoader; + return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) ); - }(); + } THREE.FBXLoader = FBXLoader; diff --git a/examples/js/loaders/GCodeLoader.js b/examples/js/loaders/GCodeLoader.js index e74cc086ada110..7e8ceeaf02d18c 100644 --- a/examples/js/loaders/GCodeLoader.js +++ b/examples/js/loaders/GCodeLoader.js @@ -9,19 +9,19 @@ * @param {Manager} manager Loading manager. */ - var GCodeLoader = function ( manager ) { + class GCodeLoader extends THREE.Loader { - THREE.Loader.call( this, manager ); - this.splitLayer = false; + constructor( manager ) { - }; + super( manager ); + this.splitLayer = false; - GCodeLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: GCodeLoader, - load: function ( url, onLoad, onProgress, onError ) { + } + + load( url, onLoad, onProgress, onError ) { - var scope = this; - var loader = new THREE.FileLoader( scope.manager ); + const scope = this; + const loader = new THREE.FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setRequestHeader( scope.requestHeader ); loader.setWithCredentials( scope.withCredentials ); @@ -49,8 +49,9 @@ }, onProgress, onError ); - }, - parse: function ( data ) { + } + + parse( data ) { var state = { x: 0, @@ -248,7 +249,8 @@ return object; } - } ); + + } THREE.GCodeLoader = GCodeLoader; diff --git a/examples/js/loaders/LDrawLoader.js b/examples/js/loaders/LDrawLoader.js index 277f75ef616a24..7d6285957187ee 100644 --- a/examples/js/loaders/LDrawLoader.js +++ b/examples/js/loaders/LDrawLoader.js @@ -1,10 +1,25 @@ ( function () { - var LDrawLoader = function () { - - var conditionalLineVertShader = - /* glsl */ - ` + // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented + + const FINISH_TYPE_DEFAULT = 0; + const FINISH_TYPE_CHROME = 1; + const FINISH_TYPE_PEARLESCENT = 2; + const FINISH_TYPE_RUBBER = 3; + const FINISH_TYPE_MATTE_METALLIC = 4; + const FINISH_TYPE_METAL = 5; // State machine to search a subobject path. + // The LDraw standard establishes these various possible subfolders. + + const FILE_LOCATION_AS_IS = 0; + const FILE_LOCATION_TRY_PARTS = 1; + const FILE_LOCATION_TRY_P = 2; + const FILE_LOCATION_TRY_MODELS = 3; + const FILE_LOCATION_TRY_RELATIVE = 4; + const FILE_LOCATION_TRY_ABSOLUTE = 5; + const FILE_LOCATION_NOT_FOUND = 6; + const conditionalLineVertShader = +/* glsl */ +` attribute vec3 control0; attribute vec3 control1; attribute vec3 direction; @@ -51,9 +66,9 @@ #include } `; - var conditionalLineFragShader = - /* glsl */ - ` + const conditionalLineFragShader = +/* glsl */ +` uniform vec3 diffuse; uniform float opacity; varying float discardFlag; @@ -80,174 +95,174 @@ #include } `; - var tempVec0 = new THREE.Vector3(); - var tempVec1 = new THREE.Vector3(); - function smoothNormals( triangles, lineSegments ) { + const _tempVec0 = new THREE.Vector3(); - function hashVertex( v ) { + const _tempVec1 = new THREE.Vector3(); - // NOTE: 1e2 is pretty coarse but was chosen because it allows edges - // to be smoothed as expected (see minifig arms). The errors between edges - // could be due to matrix multiplication. - var x = ~ ~ ( v.x * 1e2 ); - var y = ~ ~ ( v.y * 1e2 ); - var z = ~ ~ ( v.z * 1e2 ); - return `${x},${y},${z}`; + function smoothNormals( triangles, lineSegments ) { - } + function hashVertex( v ) { - function hashEdge( v0, v1 ) { + // NOTE: 1e2 is pretty coarse but was chosen because it allows edges + // to be smoothed as expected (see minifig arms). The errors between edges + // could be due to matrix multiplication. + const x = ~ ~ ( v.x * 1e2 ); + const y = ~ ~ ( v.y * 1e2 ); + const z = ~ ~ ( v.z * 1e2 ); + return `${x},${y},${z}`; - return `${hashVertex( v0 )}_${hashVertex( v1 )}`; + } - } + function hashEdge( v0, v1 ) { - var hardEdges = new Set(); - var halfEdgeList = {}; - var fullHalfEdgeList = {}; - var normals = []; // Save the list of hard edges by hash + return `${hashVertex( v0 )}_${hashVertex( v1 )}`; - for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { + } - var ls = lineSegments[ i ]; - var v0 = ls.v0; - var v1 = ls.v1; - hardEdges.add( hashEdge( v0, v1 ) ); - hardEdges.add( hashEdge( v1, v0 ) ); + const hardEdges = new Set(); + const halfEdgeList = {}; + const fullHalfEdgeList = {}; + const normals = []; // Save the list of hard edges by hash - } // track the half edges associated with each triangle + for ( let i = 0, l = lineSegments.length; i < l; i ++ ) { + const ls = lineSegments[ i ]; + const v0 = ls.v0; + const v1 = ls.v1; + hardEdges.add( hashEdge( v0, v1 ) ); + hardEdges.add( hashEdge( v1, v0 ) ); - for ( var i = 0, l = triangles.length; i < l; i ++ ) { + } // track the half edges associated with each triangle - var tri = triangles[ i ]; - for ( var i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { + for ( let i = 0, l = triangles.length; i < l; i ++ ) { - var index = i2; - var next = ( i2 + 1 ) % 3; - var v0 = tri[ `v${index}` ]; - var v1 = tri[ `v${next}` ]; - var hash = hashEdge( v0, v1 ); // don't add the triangle if the edge is supposed to be hard + const tri = triangles[ i ]; - if ( hardEdges.has( hash ) ) continue; - halfEdgeList[ hash ] = tri; - fullHalfEdgeList[ hash ] = tri; + for ( let i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { - } + const index = i2; + const next = ( i2 + 1 ) % 3; + const v0 = tri[ `v${index}` ]; + const v1 = tri[ `v${next}` ]; + const hash = hashEdge( v0, v1 ); // don't add the triangle if the edge is supposed to be hard - } // NOTE: Some of the normals wind up being skewed in an unexpected way because - // quads provide more "influence" to some vertex normals than a triangle due to - // the fact that a quad is made up of two triangles and all triangles are weighted - // equally. To fix this quads could be tracked separately so their vertex normals - // are weighted appropriately or we could try only adding a normal direction - // once per normal. - // Iterate until we've tried to connect all triangles to share normals + if ( hardEdges.has( hash ) ) continue; + halfEdgeList[ hash ] = tri; + fullHalfEdgeList[ hash ] = tri; + } - while ( true ) { + } // NOTE: Some of the normals wind up being skewed in an unexpected way because + // quads provide more "influence" to some vertex normals than a triangle due to + // the fact that a quad is made up of two triangles and all triangles are weighted + // equally. To fix this quads could be tracked separately so their vertex normals + // are weighted appropriately or we could try only adding a normal direction + // once per normal. + // Iterate until we've tried to connect all triangles to share normals - // Stop if there are no more triangles left - var halfEdges = Object.keys( halfEdgeList ); - if ( halfEdges.length === 0 ) break; // Exhaustively find all connected triangles - var i = 0; - var queue = [ fullHalfEdgeList[ halfEdges[ 0 ] ] ]; + while ( true ) { - while ( i < queue.length ) { + // Stop if there are no more triangles left + const halfEdges = Object.keys( halfEdgeList ); + if ( halfEdges.length === 0 ) break; // Exhaustively find all connected triangles - // initialize all vertex normals in this triangle - var tri = queue[ i ]; - i ++; - var faceNormal = tri.faceNormal; + let i = 0; + const queue = [ fullHalfEdgeList[ halfEdges[ 0 ] ] ]; - if ( tri.n0 === null ) { + while ( i < queue.length ) { - tri.n0 = faceNormal.clone(); - normals.push( tri.n0 ); + // initialize all vertex normals in this triangle + const tri = queue[ i ]; + i ++; + const faceNormal = tri.faceNormal; - } + if ( tri.n0 === null ) { - if ( tri.n1 === null ) { + tri.n0 = faceNormal.clone(); + normals.push( tri.n0 ); - tri.n1 = faceNormal.clone(); - normals.push( tri.n1 ); + } - } + if ( tri.n1 === null ) { - if ( tri.n2 === null ) { + tri.n1 = faceNormal.clone(); + normals.push( tri.n1 ); - tri.n2 = faceNormal.clone(); - normals.push( tri.n2 ); + } - } // Check if any edge is connected to another triangle edge + if ( tri.n2 === null ) { + tri.n2 = faceNormal.clone(); + normals.push( tri.n2 ); - for ( var i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { + } // Check if any edge is connected to another triangle edge - var index = i2; - var next = ( i2 + 1 ) % 3; - var v0 = tri[ `v${index}` ]; - var v1 = tri[ `v${next}` ]; // delete this triangle from the list so it won't be found again - var hash = hashEdge( v0, v1 ); - delete halfEdgeList[ hash ]; - var reverseHash = hashEdge( v1, v0 ); - var otherTri = fullHalfEdgeList[ reverseHash ]; + for ( let i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { - if ( otherTri ) { + const index = i2; + const next = ( i2 + 1 ) % 3; + const v0 = tri[ `v${index}` ]; + const v1 = tri[ `v${next}` ]; // delete this triangle from the list so it won't be found again - // NOTE: If the angle between triangles is > 67.5 degrees then assume it's - // hard edge. There are some cases where the line segments do not line up exactly - // with or span multiple triangle edges (see Lunar Vehicle wheels). - if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) { + const hash = hashEdge( v0, v1 ); + delete halfEdgeList[ hash ]; + const reverseHash = hashEdge( v1, v0 ); + const otherTri = fullHalfEdgeList[ reverseHash ]; - continue; + if ( otherTri ) { - } // if this triangle has already been traversed then it won't be in - // the halfEdgeList. If it has not then add it to the queue and delete - // it so it won't be found again. + // NOTE: If the angle between triangles is > 67.5 degrees then assume it's + // hard edge. There are some cases where the line segments do not line up exactly + // with or span multiple triangle edges (see Lunar Vehicle wheels). + if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) { + continue; - if ( reverseHash in halfEdgeList ) { + } // if this triangle has already been traversed then it won't be in + // the halfEdgeList. If it has not then add it to the queue and delete + // it so it won't be found again. - queue.push( otherTri ); - delete halfEdgeList[ reverseHash ]; - } // Find the matching edge in this triangle and copy the normal vector over + if ( reverseHash in halfEdgeList ) { + queue.push( otherTri ); + delete halfEdgeList[ reverseHash ]; - for ( var i3 = 0, l3 = 3; i3 < l3; i3 ++ ) { + } // Find the matching edge in this triangle and copy the normal vector over - var otherIndex = i3; - var otherNext = ( i3 + 1 ) % 3; - var otherV0 = otherTri[ `v${otherIndex}` ]; - var otherV1 = otherTri[ `v${otherNext}` ]; - var otherHash = hashEdge( otherV0, otherV1 ); - if ( otherHash === reverseHash ) { + for ( let i3 = 0, l3 = 3; i3 < l3; i3 ++ ) { - if ( otherTri[ `n${otherIndex}` ] === null ) { + const otherIndex = i3; + const otherNext = ( i3 + 1 ) % 3; + const otherV0 = otherTri[ `v${otherIndex}` ]; + const otherV1 = otherTri[ `v${otherNext}` ]; + const otherHash = hashEdge( otherV0, otherV1 ); - var norm = tri[ `n${next}` ]; - otherTri[ `n${otherIndex}` ] = norm; - norm.add( otherTri.faceNormal ); + if ( otherHash === reverseHash ) { - } + if ( otherTri[ `n${otherIndex}` ] === null ) { - if ( otherTri[ `n${otherNext}` ] === null ) { + const norm = tri[ `n${next}` ]; + otherTri[ `n${otherIndex}` ] = norm; + norm.add( otherTri.faceNormal ); - var norm = tri[ `n${index}` ]; - otherTri[ `n${otherNext}` ] = norm; - norm.add( otherTri.faceNormal ); + } - } + if ( otherTri[ `n${otherNext}` ] === null ) { - break; + const norm = tri[ `n${index}` ]; + otherTri[ `n${otherNext}` ] = norm; + norm.add( otherTri.faceNormal ); } + break; + } } @@ -256,24 +271,28 @@ } - } // The normals of each face have been added up so now we average them by normalizing the vector. + } + } // The normals of each face have been added up so now we average them by normalizing the vector. - for ( var i = 0, l = normals.length; i < l; i ++ ) { - normals[ i ].normalize(); + for ( let i = 0, l = normals.length; i < l; i ++ ) { - } + normals[ i ].normalize(); } - function isPrimitiveType( type ) { + } - return /primitive/i.test( type ) || type === 'Subpart'; + function isPrimitiveType( type ) { - } + return /primitive/i.test( type ) || type === 'Subpart'; + + } - function LineParser( line, lineNumber ) { + class LineParser { + + constructor( line, lineNumber ) { this.line = line; this.lineLength = line.length; @@ -283,219 +302,225 @@ } - LineParser.prototype = { - constructor: LineParser, - seekNonSpace: function () { - - while ( this.currentCharIndex < this.lineLength ) { + seekNonSpace() { - this.currentChar = this.line.charAt( this.currentCharIndex ); + while ( this.currentCharIndex < this.lineLength ) { - if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) { + this.currentChar = this.line.charAt( this.currentCharIndex ); - return; - - } + if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) { - this.currentCharIndex ++; + return; } - }, - getToken: function () { + this.currentCharIndex ++; - var pos0 = this.currentCharIndex ++; // Seek space + } - while ( this.currentCharIndex < this.lineLength ) { + } - this.currentChar = this.line.charAt( this.currentCharIndex ); + getToken() { - if ( this.currentChar === ' ' || this.currentChar === '\t' ) { + const pos0 = this.currentCharIndex ++; // Seek space - break; + while ( this.currentCharIndex < this.lineLength ) { - } + this.currentChar = this.line.charAt( this.currentCharIndex ); + + if ( this.currentChar === ' ' || this.currentChar === '\t' ) { - this.currentCharIndex ++; + break; } - var pos1 = this.currentCharIndex; - this.seekNonSpace(); - return this.line.substring( pos0, pos1 ); + this.currentCharIndex ++; - }, - getRemainingString: function () { + } - return this.line.substring( this.currentCharIndex, this.lineLength ); + const pos1 = this.currentCharIndex; + this.seekNonSpace(); + return this.line.substring( pos0, pos1 ); - }, - isAtTheEnd: function () { + } - return this.currentCharIndex >= this.lineLength; + getRemainingString() { - }, - setToEnd: function () { + return this.line.substring( this.currentCharIndex, this.lineLength ); - this.currentCharIndex = this.lineLength; + } - }, - getLineNumberString: function () { + isAtTheEnd() { - return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''; + return this.currentCharIndex >= this.lineLength; - } - }; + } + + setToEnd() { - function sortByMaterial( a, b ) { + this.currentCharIndex = this.lineLength; - if ( a.colourCode === b.colourCode ) { + } - return 0; + getLineNumberString() { - } + return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''; - if ( a.colourCode < b.colourCode ) { + } - return - 1; + } - } + function sortByMaterial( a, b ) { - return 1; + if ( a.colourCode === b.colourCode ) { + + return 0; } - function createObject( elements, elementSize, isConditionalSegments ) { + if ( a.colourCode < b.colourCode ) { - // Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 ) - // With per face / segment material, implemented with mesh groups and materials array - // Sort the triangles or line segments by colour code to make later the mesh groups - elements.sort( sortByMaterial ); - var positions = []; - var normals = []; - var materials = []; - var bufferGeometry = new THREE.BufferGeometry(); - var prevMaterial = null; - var index0 = 0; - var numGroupVerts = 0; + return - 1; - for ( var iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) { + } - var elem = elements[ iElem ]; - var v0 = elem.v0; - var v1 = elem.v1; // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one + return 1; - positions.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z ); + } - if ( elementSize === 3 ) { + function createObject( elements, elementSize, isConditionalSegments ) { - positions.push( elem.v2.x, elem.v2.y, elem.v2.z ); - var n0 = elem.n0 || elem.faceNormal; - var n1 = elem.n1 || elem.faceNormal; - var n2 = elem.n2 || elem.faceNormal; - normals.push( n0.x, n0.y, n0.z ); - normals.push( n1.x, n1.y, n1.z ); - normals.push( n2.x, n2.y, n2.z ); + // Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 ) + // With per face / segment material, implemented with mesh groups and materials array + // Sort the triangles or line segments by colour code to make later the mesh groups + elements.sort( sortByMaterial ); + const positions = []; + const normals = []; + const materials = []; + const bufferGeometry = new THREE.BufferGeometry(); + let prevMaterial = null; + let index0 = 0; + let numGroupVerts = 0; - } + for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) { + + const elem = elements[ iElem ]; + const v0 = elem.v0; + const v1 = elem.v1; // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one - if ( prevMaterial !== elem.material ) { + positions.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z ); - if ( prevMaterial !== null ) { + if ( elementSize === 3 ) { - bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 ); + positions.push( elem.v2.x, elem.v2.y, elem.v2.z ); + const n0 = elem.n0 || elem.faceNormal; + const n1 = elem.n1 || elem.faceNormal; + const n2 = elem.n2 || elem.faceNormal; + normals.push( n0.x, n0.y, n0.z ); + normals.push( n1.x, n1.y, n1.z ); + normals.push( n2.x, n2.y, n2.z ); - } + } - materials.push( elem.material ); - prevMaterial = elem.material; - index0 = iElem * elementSize; - numGroupVerts = elementSize; + if ( prevMaterial !== elem.material ) { - } else { + if ( prevMaterial !== null ) { - numGroupVerts += elementSize; + bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 ); } - } + materials.push( elem.material ); + prevMaterial = elem.material; + index0 = iElem * elementSize; + numGroupVerts = elementSize; - if ( numGroupVerts > 0 ) { + } else { - bufferGeometry.addGroup( index0, Infinity, materials.length - 1 ); + numGroupVerts += elementSize; } - bufferGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); + } - if ( elementSize === 3 ) { + if ( numGroupVerts > 0 ) { - bufferGeometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); + bufferGeometry.addGroup( index0, Infinity, materials.length - 1 ); - } + } - var object3d = null; + bufferGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); - if ( elementSize === 2 ) { + if ( elementSize === 3 ) { - object3d = new THREE.LineSegments( bufferGeometry, materials ); + bufferGeometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); - } else if ( elementSize === 3 ) { + } - object3d = new THREE.Mesh( bufferGeometry, materials ); + let object3d = null; - } + if ( elementSize === 2 ) { - if ( isConditionalSegments ) { - - object3d.isConditionalLine = true; - var controlArray0 = new Float32Array( elements.length * 3 * 2 ); - var controlArray1 = new Float32Array( elements.length * 3 * 2 ); - var directionArray = new Float32Array( elements.length * 3 * 2 ); - - for ( var i = 0, l = elements.length; i < l; i ++ ) { - - var os = elements[ i ]; - var c0 = os.c0; - var c1 = os.c1; - var v0 = os.v0; - var v1 = os.v1; - var index = i * 3 * 2; - controlArray0[ index + 0 ] = c0.x; - controlArray0[ index + 1 ] = c0.y; - controlArray0[ index + 2 ] = c0.z; - controlArray0[ index + 3 ] = c0.x; - controlArray0[ index + 4 ] = c0.y; - controlArray0[ index + 5 ] = c0.z; - controlArray1[ index + 0 ] = c1.x; - controlArray1[ index + 1 ] = c1.y; - controlArray1[ index + 2 ] = c1.z; - controlArray1[ index + 3 ] = c1.x; - controlArray1[ index + 4 ] = c1.y; - controlArray1[ index + 5 ] = c1.z; - directionArray[ index + 0 ] = v1.x - v0.x; - directionArray[ index + 1 ] = v1.y - v0.y; - directionArray[ index + 2 ] = v1.z - v0.z; - directionArray[ index + 3 ] = v1.x - v0.x; - directionArray[ index + 4 ] = v1.y - v0.y; - directionArray[ index + 5 ] = v1.z - v0.z; + object3d = new THREE.LineSegments( bufferGeometry, materials ); - } + } else if ( elementSize === 3 ) { + + object3d = new THREE.Mesh( bufferGeometry, materials ); + + } - bufferGeometry.setAttribute( 'control0', new THREE.BufferAttribute( controlArray0, 3, false ) ); - bufferGeometry.setAttribute( 'control1', new THREE.BufferAttribute( controlArray1, 3, false ) ); - bufferGeometry.setAttribute( 'direction', new THREE.BufferAttribute( directionArray, 3, false ) ); + if ( isConditionalSegments ) { + + object3d.isConditionalLine = true; + const controlArray0 = new Float32Array( elements.length * 3 * 2 ); + const controlArray1 = new Float32Array( elements.length * 3 * 2 ); + const directionArray = new Float32Array( elements.length * 3 * 2 ); + + for ( let i = 0, l = elements.length; i < l; i ++ ) { + + const os = elements[ i ]; + const c0 = os.c0; + const c1 = os.c1; + const v0 = os.v0; + const v1 = os.v1; + const index = i * 3 * 2; + controlArray0[ index + 0 ] = c0.x; + controlArray0[ index + 1 ] = c0.y; + controlArray0[ index + 2 ] = c0.z; + controlArray0[ index + 3 ] = c0.x; + controlArray0[ index + 4 ] = c0.y; + controlArray0[ index + 5 ] = c0.z; + controlArray1[ index + 0 ] = c1.x; + controlArray1[ index + 1 ] = c1.y; + controlArray1[ index + 2 ] = c1.z; + controlArray1[ index + 3 ] = c1.x; + controlArray1[ index + 4 ] = c1.y; + controlArray1[ index + 5 ] = c1.z; + directionArray[ index + 0 ] = v1.x - v0.x; + directionArray[ index + 1 ] = v1.y - v0.y; + directionArray[ index + 2 ] = v1.z - v0.z; + directionArray[ index + 3 ] = v1.x - v0.x; + directionArray[ index + 4 ] = v1.y - v0.y; + directionArray[ index + 5 ] = v1.z - v0.z; } - return object3d; + bufferGeometry.setAttribute( 'control0', new THREE.BufferAttribute( controlArray0, 3, false ) ); + bufferGeometry.setAttribute( 'control1', new THREE.BufferAttribute( controlArray1, 3, false ) ); + bufferGeometry.setAttribute( 'direction', new THREE.BufferAttribute( directionArray, 3, false ) ); + + } + + return object3d; + + } // - } // + class LDrawLoader extends THREE.Loader { - function LDrawLoader( manager ) { + constructor( manager ) { - THREE.Loader.call( this, manager ); // This is a stack of 'parse scopes' with one level per subobject loaded file. + super( manager ); // This is a stack of 'parse scopes' with one level per subobject loaded file. // Each level contains a material lib and also other runtime variables passed between parent and child subobjects // When searching for a material code, the stack is read from top of the stack to bottom // Each material library is an object map keyed by colour codes. @@ -516,890 +541,911 @@ this.smoothNormals = true; - } // Special surface finish tag types. - // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented + } + load( url, onLoad, onProgress, onError ) { - LDrawLoader.FINISH_TYPE_DEFAULT = 0; - LDrawLoader.FINISH_TYPE_CHROME = 1; - LDrawLoader.FINISH_TYPE_PEARLESCENT = 2; - LDrawLoader.FINISH_TYPE_RUBBER = 3; - LDrawLoader.FINISH_TYPE_MATTE_METALLIC = 4; - LDrawLoader.FINISH_TYPE_METAL = 5; // State machine to search a subobject path. - // The LDraw standard establishes these various possible subfolders. + if ( ! this.fileMap ) { - LDrawLoader.FILE_LOCATION_AS_IS = 0; - LDrawLoader.FILE_LOCATION_TRY_PARTS = 1; - LDrawLoader.FILE_LOCATION_TRY_P = 2; - LDrawLoader.FILE_LOCATION_TRY_MODELS = 3; - LDrawLoader.FILE_LOCATION_TRY_RELATIVE = 4; - LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE = 5; - LDrawLoader.FILE_LOCATION_NOT_FOUND = 6; - LDrawLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: LDrawLoader, - load: function ( url, onLoad, onProgress, onError ) { + this.fileMap = {}; - if ( ! this.fileMap ) { + } - this.fileMap = {}; + const scope = this; + const fileLoader = new THREE.FileLoader( this.manager ); + fileLoader.setPath( this.path ); + fileLoader.setRequestHeader( this.requestHeader ); + fileLoader.setWithCredentials( this.withCredentials ); + fileLoader.load( url, function ( text ) { - } + scope.processObject( text, onLoad, null, url ); - var scope = this; - var fileLoader = new THREE.FileLoader( this.manager ); - fileLoader.setPath( this.path ); - fileLoader.setRequestHeader( this.requestHeader ); - fileLoader.setWithCredentials( this.withCredentials ); - fileLoader.load( url, function ( text ) { + }, onProgress, onError ); - scope.processObject( text, onLoad, null, url ); + } - }, onProgress, onError ); + parse( text, path, onLoad ) { - }, - parse: function ( text, path, onLoad ) { + // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter + this.processObject( text, onLoad, null, path ); - // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter - this.processObject( text, onLoad, null, path ); + } - }, - setMaterials: function ( materials ) { + setMaterials( materials ) { - // Clears parse scopes stack, adds new scope with material library - this.parseScopesStack = []; - this.newParseScopeLevel( materials ); - this.getCurrentParseScope().isFromParse = false; - this.materials = materials; - return this; + // Clears parse scopes stack, adds new scope with material library + this.parseScopesStack = []; + this.newParseScopeLevel( materials ); + this.getCurrentParseScope().isFromParse = false; + this.materials = materials; + return this; - }, - setFileMap: function ( fileMap ) { + } - this.fileMap = fileMap; - return this; + setFileMap( fileMap ) { - }, - newParseScopeLevel: function ( materials ) { + this.fileMap = fileMap; + return this; - // Adds a new scope level, assign materials to it and returns it - var matLib = {}; + } - if ( materials ) { + newParseScopeLevel( materials ) { - for ( var i = 0, n = materials.length; i < n; i ++ ) { + // Adds a new scope level, assign materials to it and returns it + const matLib = {}; - var material = materials[ i ]; - matLib[ material.userData.code ] = material; + if ( materials ) { - } + for ( let i = 0, n = materials.length; i < n; i ++ ) { - } - - var topParseScope = this.getCurrentParseScope(); - var newParseScope = { - lib: matLib, - url: null, - // Subobjects - subobjects: null, - numSubobjects: 0, - subobjectIndex: 0, - inverted: false, - category: null, - keywords: null, - // Current subobject - currentFileName: null, - mainColourCode: topParseScope ? topParseScope.mainColourCode : '16', - mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24', - currentMatrix: new THREE.Matrix4(), - matrix: new THREE.Matrix4(), - // If false, it is a root material scope previous to parse - isFromParse: true, - triangles: null, - lineSegments: null, - conditionalSegments: null, - // If true, this object is the start of a construction step - startingConstructionStep: false - }; - this.parseScopesStack.push( newParseScope ); - return newParseScope; - - }, - removeScopeLevel: function () { - - this.parseScopesStack.pop(); - return this; - - }, - addMaterial: function ( material ) { - - // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array - var matLib = this.getCurrentParseScope().lib; - - if ( ! matLib[ material.userData.code ] ) { - - this.materials.push( material ); + const material = materials[ i ]; + matLib[ material.userData.code ] = material; } - matLib[ material.userData.code ] = material; - return this; + } - }, - getMaterial: function ( colourCode ) { + const topParseScope = this.getCurrentParseScope(); + const newParseScope = { + lib: matLib, + url: null, + // Subobjects + subobjects: null, + numSubobjects: 0, + subobjectIndex: 0, + inverted: false, + category: null, + keywords: null, + // Current subobject + currentFileName: null, + mainColourCode: topParseScope ? topParseScope.mainColourCode : '16', + mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24', + currentMatrix: new THREE.Matrix4(), + matrix: new THREE.Matrix4(), + // If false, it is a root material scope previous to parse + isFromParse: true, + triangles: null, + lineSegments: null, + conditionalSegments: null, + // If true, this object is the start of a construction step + startingConstructionStep: false + }; + this.parseScopesStack.push( newParseScope ); + return newParseScope; - // Given a colour code search its material in the parse scopes stack - if ( colourCode.startsWith( '0x2' ) ) { + } - // Special 'direct' material value (RGB colour) - var colour = colourCode.substring( 3 ); - return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) ); + removeScopeLevel() { - } + this.parseScopesStack.pop(); + return this; - for ( var i = this.parseScopesStack.length - 1; i >= 0; i -- ) { + } - var material = this.parseScopesStack[ i ].lib[ colourCode ]; + addMaterial( material ) { - if ( material ) { + // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array + const matLib = this.getCurrentParseScope().lib; - return material; + if ( ! matLib[ material.userData.code ] ) { - } + this.materials.push( material ); - } // Material was not found + } + matLib[ material.userData.code ] = material; + return this; - return null; + } - }, - getParentParseScope: function () { + getMaterial( colourCode ) { - if ( this.parseScopesStack.length > 1 ) { + // Given a colour code search its material in the parse scopes stack + if ( colourCode.startsWith( '0x2' ) ) { - return this.parseScopesStack[ this.parseScopesStack.length - 2 ]; + // Special 'direct' material value (RGB colour) + const colour = colourCode.substring( 3 ); + return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) ); - } + } - return null; + for ( let i = this.parseScopesStack.length - 1; i >= 0; i -- ) { - }, - getCurrentParseScope: function () { + const material = this.parseScopesStack[ i ].lib[ colourCode ]; - if ( this.parseScopesStack.length > 0 ) { + if ( material ) { - return this.parseScopesStack[ this.parseScopesStack.length - 1 ]; + return material; } - return null; + } // Material was not found - }, - parseColourMetaDirective: function ( lineParser ) { - // Parses a colour definition and returns a THREE.Material or null if error - var code = null; // Triangle and line colours + return null; - var colour = 0xFF00FF; - var edgeColour = 0xFF00FF; // Transparency + } - var alpha = 1; - var isTransparent = false; // Self-illumination: + getParentParseScope() { - var luminance = 0; - var finishType = LDrawLoader.FINISH_TYPE_DEFAULT; - var canHaveEnvMap = true; - var edgeMaterial = null; - var name = lineParser.getToken(); + if ( this.parseScopesStack.length > 1 ) { - if ( ! name ) { + return this.parseScopesStack[ this.parseScopesStack.length - 2 ]; - throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.'; + } - } // Parse tag tokens and their parameters + return null; + } - var token = null; + getCurrentParseScope() { - while ( true ) { + if ( this.parseScopesStack.length > 0 ) { - token = lineParser.getToken(); + return this.parseScopesStack[ this.parseScopesStack.length - 1 ]; - if ( ! token ) { + } - break; + return null; - } + } - switch ( token.toUpperCase() ) { + parseColourMetaDirective( lineParser ) { - case 'CODE': - code = lineParser.getToken(); - break; + // Parses a colour definition and returns a THREE.Material or null if error + var code = null; // Triangle and line colours - case 'VALUE': - colour = lineParser.getToken(); + var colour = 0xFF00FF; + var edgeColour = 0xFF00FF; // Transparency - if ( colour.startsWith( '0x' ) ) { + var alpha = 1; + var isTransparent = false; // Self-illumination: - colour = '#' + colour.substring( 2 ); + var luminance = 0; + var finishType = FINISH_TYPE_DEFAULT; + var canHaveEnvMap = true; + var edgeMaterial = null; + var name = lineParser.getToken(); - } else if ( ! colour.startsWith( '#' ) ) { + if ( ! name ) { - throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.'; + throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.'; - } + } // Parse tag tokens and their parameters - break; - case 'EDGE': - edgeColour = lineParser.getToken(); + var token = null; - if ( edgeColour.startsWith( '0x' ) ) { + while ( true ) { - edgeColour = '#' + edgeColour.substring( 2 ); + token = lineParser.getToken(); - } else if ( ! edgeColour.startsWith( '#' ) ) { + if ( ! token ) { - // Try to see if edge colour is a colour code - edgeMaterial = this.getMaterial( edgeColour ); + break; - if ( ! edgeMaterial ) { + } - throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.'; + switch ( token.toUpperCase() ) { - } // Get the edge material for this triangle material + case 'CODE': + code = lineParser.getToken(); + break; + case 'VALUE': + colour = lineParser.getToken(); - edgeMaterial = edgeMaterial.userData.edgeMaterial; + if ( colour.startsWith( '0x' ) ) { - } + colour = '#' + colour.substring( 2 ); - break; + } else if ( ! colour.startsWith( '#' ) ) { - case 'ALPHA': - alpha = parseInt( lineParser.getToken() ); + throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.'; - if ( isNaN( alpha ) ) { + } - throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.'; + break; - } + case 'EDGE': + edgeColour = lineParser.getToken(); - alpha = Math.max( 0, Math.min( 1, alpha / 255 ) ); + if ( edgeColour.startsWith( '0x' ) ) { - if ( alpha < 1 ) { + edgeColour = '#' + edgeColour.substring( 2 ); - isTransparent = true; + } else if ( ! edgeColour.startsWith( '#' ) ) { - } + // Try to see if edge colour is a colour code + edgeMaterial = this.getMaterial( edgeColour ); - break; + if ( ! edgeMaterial ) { - case 'LUMINANCE': - luminance = parseInt( lineParser.getToken() ); + throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.'; - if ( isNaN( luminance ) ) { + } // Get the edge material for this triangle material - throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.'; - } + edgeMaterial = edgeMaterial.userData.edgeMaterial; - luminance = Math.max( 0, Math.min( 1, luminance / 255 ) ); - break; + } - case 'CHROME': - finishType = LDrawLoader.FINISH_TYPE_CHROME; - break; + break; - case 'PEARLESCENT': - finishType = LDrawLoader.FINISH_TYPE_PEARLESCENT; - break; + case 'ALPHA': + alpha = parseInt( lineParser.getToken() ); - case 'RUBBER': - finishType = LDrawLoader.FINISH_TYPE_RUBBER; - break; + if ( isNaN( alpha ) ) { - case 'MATTE_METALLIC': - finishType = LDrawLoader.FINISH_TYPE_MATTE_METALLIC; - break; + throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.'; - case 'METAL': - finishType = LDrawLoader.FINISH_TYPE_METAL; - break; + } - case 'MATERIAL': - // Not implemented - lineParser.setToEnd(); - break; + alpha = Math.max( 0, Math.min( 1, alpha / 255 ) ); - default: - throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.'; - break; + if ( alpha < 1 ) { - } + isTransparent = true; - } + } - var material = null; + break; - switch ( finishType ) { + case 'LUMINANCE': + luminance = parseInt( lineParser.getToken() ); - case LDrawLoader.FINISH_TYPE_DEFAULT: - material = new THREE.MeshStandardMaterial( { - color: colour, - roughness: 0.3, - envMapIntensity: 0.3, - metalness: 0 - } ); + if ( isNaN( luminance ) ) { + + throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.'; + + } + + luminance = Math.max( 0, Math.min( 1, luminance / 255 ) ); break; - case LDrawLoader.FINISH_TYPE_PEARLESCENT: - // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess - var specular = new THREE.Color( colour ); - var hsl = specular.getHSL( { - h: 0, - s: 0, - l: 0 - } ); - hsl.h = ( hsl.h + 0.5 ) % 1; - hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 ); - specular.setHSL( hsl.h, hsl.s, hsl.l ); - material = new THREE.MeshPhongMaterial( { - color: colour, - specular: specular, - shininess: 10, - reflectivity: 0.3 - } ); + case 'CHROME': + finishType = FINISH_TYPE_CHROME; break; - case LDrawLoader.FINISH_TYPE_CHROME: - // Mirror finish surface - material = new THREE.MeshStandardMaterial( { - color: colour, - roughness: 0, - metalness: 1 - } ); + case 'PEARLESCENT': + finishType = FINISH_TYPE_PEARLESCENT; break; - case LDrawLoader.FINISH_TYPE_RUBBER: - // Rubber finish - material = new THREE.MeshStandardMaterial( { - color: colour, - roughness: 0.9, - metalness: 0 - } ); - canHaveEnvMap = false; + case 'RUBBER': + finishType = FINISH_TYPE_RUBBER; break; - case LDrawLoader.FINISH_TYPE_MATTE_METALLIC: - // Brushed metal finish - material = new THREE.MeshStandardMaterial( { - color: colour, - roughness: 0.8, - metalness: 0.4 - } ); + case 'MATTE_METALLIC': + finishType = FINISH_TYPE_MATTE_METALLIC; break; - case LDrawLoader.FINISH_TYPE_METAL: - // Average metal finish - material = new THREE.MeshStandardMaterial( { - color: colour, - roughness: 0.2, - metalness: 0.85 - } ); + case 'METAL': + finishType = FINISH_TYPE_METAL; + break; + + case 'MATERIAL': + // Not implemented + lineParser.setToEnd(); break; default: - // Should not happen + throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.'; break; } - material.transparent = isTransparent; - material.premultipliedAlpha = true; - material.opacity = alpha; - material.depthWrite = ! isTransparent; - material.polygonOffset = true; - material.polygonOffsetFactor = 1; - material.userData.canHaveEnvMap = canHaveEnvMap; - - if ( luminance !== 0 ) { - - material.emissive.set( material.color ).multiplyScalar( luminance ); + } - } + var material = null; - if ( ! edgeMaterial ) { + switch ( finishType ) { - // This is the material used for edges - edgeMaterial = new THREE.LineBasicMaterial( { - color: edgeColour, - transparent: isTransparent, - opacity: alpha, - depthWrite: ! isTransparent + case FINISH_TYPE_DEFAULT: + material = new THREE.MeshStandardMaterial( { + color: colour, + roughness: 0.3, + envMapIntensity: 0.3, + metalness: 0 } ); - edgeMaterial.userData.code = code; - edgeMaterial.name = name + ' - Edge'; - edgeMaterial.userData.canHaveEnvMap = false; // This is the material used for conditional edges - - edgeMaterial.userData.conditionalEdgeMaterial = new THREE.ShaderMaterial( { - vertexShader: conditionalLineVertShader, - fragmentShader: conditionalLineFragShader, - uniforms: THREE.UniformsUtils.merge( [ THREE.UniformsLib.fog, { - diffuse: { - value: new THREE.Color( edgeColour ) - }, - opacity: { - value: alpha - } - } ] ), - fog: true, - transparent: isTransparent, - depthWrite: ! isTransparent + break; + + case FINISH_TYPE_PEARLESCENT: + // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess + var specular = new THREE.Color( colour ); + var hsl = specular.getHSL( { + h: 0, + s: 0, + l: 0 + } ); + hsl.h = ( hsl.h + 0.5 ) % 1; + hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 ); + specular.setHSL( hsl.h, hsl.s, hsl.l ); + material = new THREE.MeshPhongMaterial( { + color: colour, + specular: specular, + shininess: 10, + reflectivity: 0.3 + } ); + break; + + case FINISH_TYPE_CHROME: + // Mirror finish surface + material = new THREE.MeshStandardMaterial( { + color: colour, + roughness: 0, + metalness: 1 + } ); + break; + + case FINISH_TYPE_RUBBER: + // Rubber finish + material = new THREE.MeshStandardMaterial( { + color: colour, + roughness: 0.9, + metalness: 0 + } ); + canHaveEnvMap = false; + break; + + case FINISH_TYPE_MATTE_METALLIC: + // Brushed metal finish + material = new THREE.MeshStandardMaterial( { + color: colour, + roughness: 0.8, + metalness: 0.4 + } ); + break; + + case FINISH_TYPE_METAL: + // Average metal finish + material = new THREE.MeshStandardMaterial( { + color: colour, + roughness: 0.2, + metalness: 0.85 } ); - edgeMaterial.userData.conditionalEdgeMaterial.userData.canHaveEnvMap = false; + break; - } + default: + // Should not happen + break; - material.userData.code = code; - material.name = name; - material.userData.edgeMaterial = edgeMaterial; - return material; + } - }, - // - objectParse: function ( text ) { + material.transparent = isTransparent; + material.premultipliedAlpha = true; + material.opacity = alpha; + material.depthWrite = ! isTransparent; + material.polygonOffset = true; + material.polygonOffsetFactor = 1; + material.userData.canHaveEnvMap = canHaveEnvMap; - // Retrieve data from the parent parse scope - var parentParseScope = this.getParentParseScope(); // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object) + if ( luminance !== 0 ) { - var mainColourCode = parentParseScope.mainColourCode; - var mainEdgeColourCode = parentParseScope.mainEdgeColourCode; - var currentParseScope = this.getCurrentParseScope(); // Parse result variables + material.emissive.set( material.color ).multiplyScalar( luminance ); - var triangles; - var lineSegments; - var conditionalSegments; - var subobjects = []; - var category = null; - var keywords = null; + } - if ( text.indexOf( '\r\n' ) !== - 1 ) { + if ( ! edgeMaterial ) { - // This is faster than String.split with regex that splits on both - text = text.replace( /\r\n/g, '\n' ); + // This is the material used for edges + edgeMaterial = new THREE.LineBasicMaterial( { + color: edgeColour, + transparent: isTransparent, + opacity: alpha, + depthWrite: ! isTransparent + } ); + edgeMaterial.userData.code = code; + edgeMaterial.name = name + ' - Edge'; + edgeMaterial.userData.canHaveEnvMap = false; // This is the material used for conditional edges + + edgeMaterial.userData.conditionalEdgeMaterial = new THREE.ShaderMaterial( { + vertexShader: conditionalLineVertShader, + fragmentShader: conditionalLineFragShader, + uniforms: THREE.UniformsUtils.merge( [ THREE.UniformsLib.fog, { + diffuse: { + value: new THREE.Color( edgeColour ) + }, + opacity: { + value: alpha + } + } ] ), + fog: true, + transparent: isTransparent, + depthWrite: ! isTransparent + } ); + edgeMaterial.userData.conditionalEdgeMaterial.userData.canHaveEnvMap = false; - } + } - var lines = text.split( '\n' ); - var numLines = lines.length; - var lineIndex = 0; - var parsingEmbeddedFiles = false; - var currentEmbeddedFileName = null; - var currentEmbeddedText = null; - var bfcCertified = false; - var bfcCCW = true; - var bfcInverted = false; - var bfcCull = true; - var type = ''; - var startingConstructionStep = false; - var scope = this; + material.userData.code = code; + material.name = name; + material.userData.edgeMaterial = edgeMaterial; + return material; - function parseColourCode( lineParser, forEdge ) { + } // - // Parses next colour code and returns a THREE.Material - var colourCode = lineParser.getToken(); - if ( ! forEdge && colourCode === '16' ) { + objectParse( text ) { - colourCode = mainColourCode; + // Retrieve data from the parent parse scope + var parentParseScope = this.getParentParseScope(); // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object) - } + var mainColourCode = parentParseScope.mainColourCode; + var mainEdgeColourCode = parentParseScope.mainEdgeColourCode; + var currentParseScope = this.getCurrentParseScope(); // Parse result variables - if ( forEdge && colourCode === '24' ) { + var triangles; + var lineSegments; + var conditionalSegments; + var subobjects = []; + var category = null; + var keywords = null; - colourCode = mainEdgeColourCode; + if ( text.indexOf( '\r\n' ) !== - 1 ) { - } + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); - var material = scope.getMaterial( colourCode ); + } - if ( ! material ) { + var lines = text.split( '\n' ); + var numLines = lines.length; + var lineIndex = 0; + var parsingEmbeddedFiles = false; + var currentEmbeddedFileName = null; + var currentEmbeddedText = null; + var bfcCertified = false; + var bfcCCW = true; + var bfcInverted = false; + var bfcCull = true; + var type = ''; + var startingConstructionStep = false; + var scope = this; - throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.'; + function parseColourCode( lineParser, forEdge ) { - } + // Parses next colour code and returns a THREE.Material + var colourCode = lineParser.getToken(); - return material; + if ( ! forEdge && colourCode === '16' ) { + + colourCode = mainColourCode; } - function parseVector( lp ) { + if ( forEdge && colourCode === '24' ) { + + colourCode = mainEdgeColourCode; - var v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) ); + } - if ( ! scope.separateObjects ) { + var material = scope.getMaterial( colourCode ); - v.applyMatrix4( currentParseScope.currentMatrix ); + if ( ! material ) { - } + throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.'; - return v; + } - } // Parse all line commands + return material; + } - for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) { + function parseVector( lp ) { - var line = lines[ lineIndex ]; - if ( line.length === 0 ) continue; + var v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) ); - if ( parsingEmbeddedFiles ) { + if ( ! scope.separateObjects ) { - if ( line.startsWith( '0 FILE ' ) ) { + v.applyMatrix4( currentParseScope.currentMatrix ); - // Save previous embedded file in the cache - this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; // New embedded text file + } - currentEmbeddedFileName = line.substring( 7 ); - currentEmbeddedText = ''; + return v; - } else { + } // Parse all line commands - currentEmbeddedText += line + '\n'; - } + for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) { - continue; + var line = lines[ lineIndex ]; + if ( line.length === 0 ) continue; - } + if ( parsingEmbeddedFiles ) { - var lp = new LineParser( line, lineIndex + 1 ); - lp.seekNonSpace(); + if ( line.startsWith( '0 FILE ' ) ) { - if ( lp.isAtTheEnd() ) { + // Save previous embedded file in the cache + this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; // New embedded text file - // Empty line - continue; + currentEmbeddedFileName = line.substring( 7 ); + currentEmbeddedText = ''; - } // Parse the line type + } else { + currentEmbeddedText += line + '\n'; - var lineType = lp.getToken(); + } - switch ( lineType ) { + continue; - // Line type 0: Comment or META - case '0': - // Parse meta directive - var meta = lp.getToken(); + } - if ( meta ) { + var lp = new LineParser( line, lineIndex + 1 ); + lp.seekNonSpace(); - switch ( meta ) { + if ( lp.isAtTheEnd() ) { - case '!LDRAW_ORG': - type = lp.getToken(); - currentParseScope.triangles = []; - currentParseScope.lineSegments = []; - currentParseScope.conditionalSegments = []; - currentParseScope.type = type; - var isRoot = ! parentParseScope.isFromParse; + // Empty line + continue; - if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) { + } // Parse the line type - currentParseScope.groupObject = new THREE.Group(); - currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep; - } // If the scale of the object is negated then the triangle winding order - // needs to be flipped. + var lineType = lp.getToken(); + switch ( lineType ) { - var matrix = currentParseScope.matrix; + // Line type 0: Comment or META + case '0': + // Parse meta directive + var meta = lp.getToken(); - if ( matrix.determinant() < 0 && ( scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects ) ) { + if ( meta ) { - currentParseScope.inverted = ! currentParseScope.inverted; + switch ( meta ) { - } + case '!LDRAW_ORG': + type = lp.getToken(); + currentParseScope.triangles = []; + currentParseScope.lineSegments = []; + currentParseScope.conditionalSegments = []; + currentParseScope.type = type; + var isRoot = ! parentParseScope.isFromParse; - triangles = currentParseScope.triangles; - lineSegments = currentParseScope.lineSegments; - conditionalSegments = currentParseScope.conditionalSegments; - break; + if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) { - case '!COLOUR': - var material = this.parseColourMetaDirective( lp ); + currentParseScope.groupObject = new THREE.Group(); + currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep; - if ( material ) { + } // If the scale of the object is negated then the triangle winding order + // needs to be flipped. - this.addMaterial( material ); - } else { + var matrix = currentParseScope.matrix; - console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() ); + if ( matrix.determinant() < 0 && ( scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects ) ) { - } + currentParseScope.inverted = ! currentParseScope.inverted; - break; + } - case '!CATEGORY': - category = lp.getToken(); - break; + triangles = currentParseScope.triangles; + lineSegments = currentParseScope.lineSegments; + conditionalSegments = currentParseScope.conditionalSegments; + break; - case '!KEYWORDS': - var newKeywords = lp.getRemainingString().split( ',' ); + case '!COLOUR': + var material = this.parseColourMetaDirective( lp ); - if ( newKeywords.length > 0 ) { + if ( material ) { - if ( ! keywords ) { + this.addMaterial( material ); - keywords = []; + } else { - } + console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() ); - newKeywords.forEach( function ( keyword ) { + } - keywords.push( keyword.trim() ); + break; - } ); + case '!CATEGORY': + category = lp.getToken(); + break; - } + case '!KEYWORDS': + var newKeywords = lp.getRemainingString().split( ',' ); - break; + if ( newKeywords.length > 0 ) { - case 'FILE': - if ( lineIndex > 0 ) { + if ( ! keywords ) { - // Start embedded text files parsing - parsingEmbeddedFiles = true; - currentEmbeddedFileName = lp.getRemainingString(); - currentEmbeddedText = ''; - bfcCertified = false; - bfcCCW = true; + keywords = []; } - break; + newKeywords.forEach( function ( keyword ) { - case 'BFC': - // Changes to the backface culling state - while ( ! lp.isAtTheEnd() ) { + keywords.push( keyword.trim() ); - var token = lp.getToken(); + } ); - switch ( token ) { + } - case 'CERTIFY': - case 'NOCERTIFY': - bfcCertified = token === 'CERTIFY'; - bfcCCW = true; - break; + break; + + case 'FILE': + if ( lineIndex > 0 ) { + + // Start embedded text files parsing + parsingEmbeddedFiles = true; + currentEmbeddedFileName = lp.getRemainingString(); + currentEmbeddedText = ''; + bfcCertified = false; + bfcCCW = true; + + } + + break; + + case 'BFC': + // Changes to the backface culling state + while ( ! lp.isAtTheEnd() ) { + + var token = lp.getToken(); + + switch ( token ) { - case 'CW': - case 'CCW': - bfcCCW = token === 'CCW'; - break; + case 'CERTIFY': + case 'NOCERTIFY': + bfcCertified = token === 'CERTIFY'; + bfcCCW = true; + break; - case 'INVERTNEXT': - bfcInverted = true; - break; + case 'CW': + case 'CCW': + bfcCCW = token === 'CCW'; + break; - case 'CLIP': - case 'NOCLIP': - bfcCull = token === 'CLIP'; - break; + case 'INVERTNEXT': + bfcInverted = true; + break; - default: - console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' ); - break; + case 'CLIP': + case 'NOCLIP': + bfcCull = token === 'CLIP'; + break; - } + default: + console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' ); + break; } - break; + } - case 'STEP': - startingConstructionStep = true; - break; + break; - default: - // Other meta directives are not implemented - break; + case 'STEP': + startingConstructionStep = true; + break; - } + default: + // Other meta directives are not implemented + break; } - break; - // Line type 1: Sub-object file + } + + break; + // Line type 1: Sub-object file + + case '1': + var material = parseColourCode( lp ); + var posX = parseFloat( lp.getToken() ); + var posY = parseFloat( lp.getToken() ); + var posZ = parseFloat( lp.getToken() ); + var m0 = parseFloat( lp.getToken() ); + var m1 = parseFloat( lp.getToken() ); + var m2 = parseFloat( lp.getToken() ); + var m3 = parseFloat( lp.getToken() ); + var m4 = parseFloat( lp.getToken() ); + var m5 = parseFloat( lp.getToken() ); + var m6 = parseFloat( lp.getToken() ); + var m7 = parseFloat( lp.getToken() ); + var m8 = parseFloat( lp.getToken() ); + var matrix = new THREE.Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 ); + var fileName = lp.getRemainingString().trim().replace( /\\/g, '/' ); + + if ( scope.fileMap[ fileName ] ) { + + // Found the subobject path in the preloaded file path map + fileName = scope.fileMap[ fileName ]; + + } else { - case '1': - var material = parseColourCode( lp ); - var posX = parseFloat( lp.getToken() ); - var posY = parseFloat( lp.getToken() ); - var posZ = parseFloat( lp.getToken() ); - var m0 = parseFloat( lp.getToken() ); - var m1 = parseFloat( lp.getToken() ); - var m2 = parseFloat( lp.getToken() ); - var m3 = parseFloat( lp.getToken() ); - var m4 = parseFloat( lp.getToken() ); - var m5 = parseFloat( lp.getToken() ); - var m6 = parseFloat( lp.getToken() ); - var m7 = parseFloat( lp.getToken() ); - var m8 = parseFloat( lp.getToken() ); - var matrix = new THREE.Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 ); - var fileName = lp.getRemainingString().trim().replace( /\\/g, '/' ); + // Standardized subfolders + if ( fileName.startsWith( 's/' ) ) { - if ( scope.fileMap[ fileName ] ) { + fileName = 'parts/' + fileName; - // Found the subobject path in the preloaded file path map - fileName = scope.fileMap[ fileName ]; + } else if ( fileName.startsWith( '48/' ) ) { - } else { + fileName = 'p/' + fileName; - // Standardized subfolders - if ( fileName.startsWith( 's/' ) ) { + } - fileName = 'parts/' + fileName; + } - } else if ( fileName.startsWith( '48/' ) ) { + subobjects.push( { + material: material, + matrix: matrix, + fileName: fileName, + originalFileName: fileName, + locationState: FILE_LOCATION_AS_IS, + url: null, + triedLowerCase: false, + inverted: bfcInverted !== currentParseScope.inverted, + startingConstructionStep: startingConstructionStep + } ); + bfcInverted = false; + break; + // Line type 2: Line segment + + case '2': + var material = parseColourCode( lp, true ); + var segment = { + material: material.userData.edgeMaterial, + colourCode: material.userData.code, + v0: parseVector( lp ), + v1: parseVector( lp ) + }; + lineSegments.push( segment ); + break; + // Line type 5: Conditional Line segment + + case '5': + var material = parseColourCode( lp, true ); + var segment = { + material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial, + colourCode: material.userData.code, + v0: parseVector( lp ), + v1: parseVector( lp ), + c0: parseVector( lp ), + c1: parseVector( lp ) + }; + conditionalSegments.push( segment ); + break; + // Line type 3: Triangle - fileName = 'p/' + fileName; + case '3': + var material = parseColourCode( lp ); + var inverted = currentParseScope.inverted; + var ccw = bfcCCW !== inverted; + var doubleSided = ! bfcCertified || ! bfcCull; + var v0, v1, v2, faceNormal; - } + if ( ccw === true ) { - } + v0 = parseVector( lp ); + v1 = parseVector( lp ); + v2 = parseVector( lp ); - subobjects.push( { - material: material, - matrix: matrix, - fileName: fileName, - originalFileName: fileName, - locationState: LDrawLoader.FILE_LOCATION_AS_IS, - url: null, - triedLowerCase: false, - inverted: bfcInverted !== currentParseScope.inverted, - startingConstructionStep: startingConstructionStep - } ); - bfcInverted = false; - break; - // Line type 2: Line segment - - case '2': - var material = parseColourCode( lp, true ); - var segment = { - material: material.userData.edgeMaterial, - colourCode: material.userData.code, - v0: parseVector( lp ), - v1: parseVector( lp ) - }; - lineSegments.push( segment ); - break; - // Line type 5: Conditional Line segment - - case '5': - var material = parseColourCode( lp, true ); - var segment = { - material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial, - colourCode: material.userData.code, - v0: parseVector( lp ), - v1: parseVector( lp ), - c0: parseVector( lp ), - c1: parseVector( lp ) - }; - conditionalSegments.push( segment ); - break; - // Line type 3: Triangle - - case '3': - var material = parseColourCode( lp ); - var inverted = currentParseScope.inverted; - var ccw = bfcCCW !== inverted; - var doubleSided = ! bfcCertified || ! bfcCull; - var v0, v1, v2, faceNormal; - - if ( ccw === true ) { - - v0 = parseVector( lp ); - v1 = parseVector( lp ); - v2 = parseVector( lp ); - - } else { - - v2 = parseVector( lp ); - v1 = parseVector( lp ); - v0 = parseVector( lp ); + } else { - } + v2 = parseVector( lp ); + v1 = parseVector( lp ); + v0 = parseVector( lp ); + + } + + _tempVec0.subVectors( v1, v0 ); + + _tempVec1.subVectors( v2, v1 ); + + faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize(); + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v1, + v2: v2, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); + + if ( doubleSided === true ) { - tempVec0.subVectors( v1, v0 ); - tempVec1.subVectors( v2, v1 ); - faceNormal = new THREE.Vector3().crossVectors( tempVec0, tempVec1 ).normalize(); triangles.push( { material: material, colourCode: material.userData.code, v0: v0, - v1: v1, - v2: v2, + v1: v2, + v2: v1, faceNormal: faceNormal, n0: null, n1: null, n2: null } ); - if ( doubleSided === true ) { + } - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v2, - v2: v1, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); + break; + // Line type 4: Quadrilateral - } + case '4': + var material = parseColourCode( lp ); + var inverted = currentParseScope.inverted; + var ccw = bfcCCW !== inverted; + var doubleSided = ! bfcCertified || ! bfcCull; + var v0, v1, v2, v3, faceNormal; - break; - // Line type 4: Quadrilateral + if ( ccw === true ) { - case '4': - var material = parseColourCode( lp ); - var inverted = currentParseScope.inverted; - var ccw = bfcCCW !== inverted; - var doubleSided = ! bfcCertified || ! bfcCull; - var v0, v1, v2, v3, faceNormal; + v0 = parseVector( lp ); + v1 = parseVector( lp ); + v2 = parseVector( lp ); + v3 = parseVector( lp ); - if ( ccw === true ) { + } else { - v0 = parseVector( lp ); - v1 = parseVector( lp ); - v2 = parseVector( lp ); - v3 = parseVector( lp ); + v3 = parseVector( lp ); + v2 = parseVector( lp ); + v1 = parseVector( lp ); + v0 = parseVector( lp ); - } else { + } - v3 = parseVector( lp ); - v2 = parseVector( lp ); - v1 = parseVector( lp ); - v0 = parseVector( lp ); + _tempVec0.subVectors( v1, v0 ); + + _tempVec1.subVectors( v2, v1 ); + + faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize(); + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v1, + v2: v2, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v2, + v2: v3, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); - } + if ( doubleSided === true ) { - tempVec0.subVectors( v1, v0 ); - tempVec1.subVectors( v2, v1 ); - faceNormal = new THREE.Vector3().crossVectors( tempVec0, tempVec1 ).normalize(); triangles.push( { material: material, colourCode: material.userData.code, v0: v0, - v1: v1, - v2: v2, + v1: v2, + v2: v1, faceNormal: faceNormal, n0: null, n1: null, @@ -1409,399 +1455,377 @@ material: material, colourCode: material.userData.code, v0: v0, - v1: v2, - v2: v3, + v1: v3, + v2: v2, faceNormal: faceNormal, n0: null, n1: null, n2: null } ); - if ( doubleSided === true ) { - - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v2, - v2: v1, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v3, - v2: v2, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); - - } - - break; + } - default: - throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.'; - break; + break; - } + default: + throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.'; + break; } - if ( parsingEmbeddedFiles ) { + } - this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; + if ( parsingEmbeddedFiles ) { - } + this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; - currentParseScope.category = category; - currentParseScope.keywords = keywords; - currentParseScope.subobjects = subobjects; - currentParseScope.numSubobjects = subobjects.length; - currentParseScope.subobjectIndex = 0; + } - }, - computeConstructionSteps: function ( model ) { + currentParseScope.category = category; + currentParseScope.keywords = keywords; + currentParseScope.subobjects = subobjects; + currentParseScope.numSubobjects = subobjects.length; + currentParseScope.subobjectIndex = 0; - // Sets userdata.constructionStep number in THREE.Group objects and userData.numConstructionSteps number in the root THREE.Group object. - var stepNumber = 0; - model.traverse( c => { + } - if ( c.isGroup ) { + computeConstructionSteps( model ) { - if ( c.userData.startingConstructionStep ) { + // Sets userdata.constructionStep number in THREE.Group objects and userData.numConstructionSteps number in the root THREE.Group object. + var stepNumber = 0; + model.traverse( c => { - stepNumber ++; + if ( c.isGroup ) { - } + if ( c.userData.startingConstructionStep ) { - c.userData.constructionStep = stepNumber; + stepNumber ++; } - } ); - model.userData.numConstructionSteps = stepNumber + 1; + c.userData.constructionStep = stepNumber; - }, - processObject: function ( text, onProcessed, subobject, url ) { + } - var scope = this; - var parseScope = scope.newParseScopeLevel(); - parseScope.url = url; - var parentParseScope = scope.getParentParseScope(); // Set current matrix + } ); + model.userData.numConstructionSteps = stepNumber + 1; - if ( subobject ) { + } - parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix ); - parseScope.matrix.copy( subobject.matrix ); - parseScope.inverted = subobject.inverted; - parseScope.startingConstructionStep = subobject.startingConstructionStep; + processObject( text, onProcessed, subobject, url ) { - } // Add to cache + var scope = this; + var parseScope = scope.newParseScopeLevel(); + parseScope.url = url; + var parentParseScope = scope.getParentParseScope(); // Set current matrix + if ( subobject ) { - var currentFileName = parentParseScope.currentFileName; + parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix ); + parseScope.matrix.copy( subobject.matrix ); + parseScope.inverted = subobject.inverted; + parseScope.startingConstructionStep = subobject.startingConstructionStep; - if ( currentFileName !== null ) { + } // Add to cache - currentFileName = parentParseScope.currentFileName.toLowerCase(); - } + var currentFileName = parentParseScope.currentFileName; - if ( scope.subobjectCache[ currentFileName ] === undefined ) { + if ( currentFileName !== null ) { - scope.subobjectCache[ currentFileName ] = text; + currentFileName = parentParseScope.currentFileName.toLowerCase(); - } // Parse the object (returns a THREE.Group) + } + if ( scope.subobjectCache[ currentFileName ] === undefined ) { - scope.objectParse( text ); - var finishedCount = 0; - onSubobjectFinish(); + scope.subobjectCache[ currentFileName ] = text; - function onSubobjectFinish() { + } // Parse the object (returns a THREE.Group) - finishedCount ++; - if ( finishedCount === parseScope.subobjects.length + 1 ) { + scope.objectParse( text ); + var finishedCount = 0; + onSubobjectFinish(); - finalizeObject(); + function onSubobjectFinish() { - } else { + finishedCount ++; - // Once the previous subobject has finished we can start processing the next one in the list. - // The subobject processing shares scope in processing so it's important that they be loaded serially - // to avoid race conditions. - // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to - // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame - // will work but causes the load to happen after the next frame which causes the load to take significantly longer. - var subobject = parseScope.subobjects[ parseScope.subobjectIndex ]; - Promise.resolve().then( function () { + if ( finishedCount === parseScope.subobjects.length + 1 ) { - loadSubobject( subobject ); + finalizeObject(); - } ); - parseScope.subobjectIndex ++; + } else { - } + // Once the previous subobject has finished we can start processing the next one in the list. + // The subobject processing shares scope in processing so it's important that they be loaded serially + // to avoid race conditions. + // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to + // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame + // will work but causes the load to happen after the next frame which causes the load to take significantly longer. + var subobject = parseScope.subobjects[ parseScope.subobjectIndex ]; + Promise.resolve().then( function () { - } + loadSubobject( subobject ); - function finalizeObject() { + } ); + parseScope.subobjectIndex ++; - if ( scope.smoothNormals && parseScope.type === 'Part' ) { + } - smoothNormals( parseScope.triangles, parseScope.lineSegments ); + } - } + function finalizeObject() { - var isRoot = ! parentParseScope.isFromParse; + if ( scope.smoothNormals && parseScope.type === 'Part' ) { - if ( scope.separateObjects && ! isPrimitiveType( parseScope.type ) || isRoot ) { + smoothNormals( parseScope.triangles, parseScope.lineSegments ); - const objGroup = parseScope.groupObject; + } - if ( parseScope.triangles.length > 0 ) { + var isRoot = ! parentParseScope.isFromParse; - objGroup.add( createObject( parseScope.triangles, 3 ) ); + if ( scope.separateObjects && ! isPrimitiveType( parseScope.type ) || isRoot ) { - } + const objGroup = parseScope.groupObject; - if ( parseScope.lineSegments.length > 0 ) { + if ( parseScope.triangles.length > 0 ) { - objGroup.add( createObject( parseScope.lineSegments, 2 ) ); + objGroup.add( createObject( parseScope.triangles, 3 ) ); - } + } - if ( parseScope.conditionalSegments.length > 0 ) { + if ( parseScope.lineSegments.length > 0 ) { - objGroup.add( createObject( parseScope.conditionalSegments, 2, true ) ); + objGroup.add( createObject( parseScope.lineSegments, 2 ) ); - } + } - if ( parentParseScope.groupObject ) { + if ( parseScope.conditionalSegments.length > 0 ) { - objGroup.name = parseScope.fileName; - objGroup.userData.category = parseScope.category; - objGroup.userData.keywords = parseScope.keywords; - parseScope.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale ); - parentParseScope.groupObject.add( objGroup ); + objGroup.add( createObject( parseScope.conditionalSegments, 2, true ) ); - } + } - } else { + if ( parentParseScope.groupObject ) { - var separateObjects = scope.separateObjects; - var parentLineSegments = parentParseScope.lineSegments; - var parentConditionalSegments = parentParseScope.conditionalSegments; - var parentTriangles = parentParseScope.triangles; - var lineSegments = parseScope.lineSegments; - var conditionalSegments = parseScope.conditionalSegments; - var triangles = parseScope.triangles; + objGroup.name = parseScope.fileName; + objGroup.userData.category = parseScope.category; + objGroup.userData.keywords = parseScope.keywords; + parseScope.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale ); + parentParseScope.groupObject.add( objGroup ); - for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { + } - var ls = lineSegments[ i ]; + } else { - if ( separateObjects ) { + var separateObjects = scope.separateObjects; + var parentLineSegments = parentParseScope.lineSegments; + var parentConditionalSegments = parentParseScope.conditionalSegments; + var parentTriangles = parentParseScope.triangles; + var lineSegments = parseScope.lineSegments; + var conditionalSegments = parseScope.conditionalSegments; + var triangles = parseScope.triangles; - ls.v0.applyMatrix4( parseScope.matrix ); - ls.v1.applyMatrix4( parseScope.matrix ); + for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { - } + var ls = lineSegments[ i ]; + + if ( separateObjects ) { - parentLineSegments.push( ls ); + ls.v0.applyMatrix4( parseScope.matrix ); + ls.v1.applyMatrix4( parseScope.matrix ); } - for ( var i = 0, l = conditionalSegments.length; i < l; i ++ ) { + parentLineSegments.push( ls ); - var os = conditionalSegments[ i ]; + } - if ( separateObjects ) { + for ( var i = 0, l = conditionalSegments.length; i < l; i ++ ) { - os.v0.applyMatrix4( parseScope.matrix ); - os.v1.applyMatrix4( parseScope.matrix ); - os.c0.applyMatrix4( parseScope.matrix ); - os.c1.applyMatrix4( parseScope.matrix ); + var os = conditionalSegments[ i ]; - } + if ( separateObjects ) { - parentConditionalSegments.push( os ); + os.v0.applyMatrix4( parseScope.matrix ); + os.v1.applyMatrix4( parseScope.matrix ); + os.c0.applyMatrix4( parseScope.matrix ); + os.c1.applyMatrix4( parseScope.matrix ); } - for ( var i = 0, l = triangles.length; i < l; i ++ ) { + parentConditionalSegments.push( os ); - var tri = triangles[ i ]; + } - if ( separateObjects ) { + for ( var i = 0, l = triangles.length; i < l; i ++ ) { - tri.v0 = tri.v0.clone().applyMatrix4( parseScope.matrix ); - tri.v1 = tri.v1.clone().applyMatrix4( parseScope.matrix ); - tri.v2 = tri.v2.clone().applyMatrix4( parseScope.matrix ); - tempVec0.subVectors( tri.v1, tri.v0 ); - tempVec1.subVectors( tri.v2, tri.v1 ); - tri.faceNormal.crossVectors( tempVec0, tempVec1 ).normalize(); + var tri = triangles[ i ]; - } + if ( separateObjects ) { - parentTriangles.push( tri ); + tri.v0 = tri.v0.clone().applyMatrix4( parseScope.matrix ); + tri.v1 = tri.v1.clone().applyMatrix4( parseScope.matrix ); + tri.v2 = tri.v2.clone().applyMatrix4( parseScope.matrix ); - } + _tempVec0.subVectors( tri.v1, tri.v0 ); - } + _tempVec1.subVectors( tri.v2, tri.v1 ); - scope.removeScopeLevel(); // If it is root object, compute construction steps + tri.faceNormal.crossVectors( _tempVec0, _tempVec1 ).normalize(); - if ( ! parentParseScope.isFromParse ) { + } - scope.computeConstructionSteps( parseScope.groupObject ); + parentTriangles.push( tri ); } - if ( onProcessed ) { + } - onProcessed( parseScope.groupObject ); + scope.removeScopeLevel(); // If it is root object, compute construction steps - } + if ( ! parentParseScope.isFromParse ) { - } + scope.computeConstructionSteps( parseScope.groupObject ); - function loadSubobject( subobject ) { + } - parseScope.mainColourCode = subobject.material.userData.code; - parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code; - parseScope.currentFileName = subobject.originalFileName; // If subobject was cached previously, use the cached one + if ( onProcessed ) { - var cached = scope.subobjectCache[ subobject.originalFileName.toLowerCase() ]; + onProcessed( parseScope.groupObject ); - if ( cached ) { + } - scope.processObject( cached, function ( subobjectGroup ) { + } - onSubobjectLoaded( subobjectGroup, subobject ); - onSubobjectFinish(); + function loadSubobject( subobject ) { - }, subobject, url ); - return; + parseScope.mainColourCode = subobject.material.userData.code; + parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code; + parseScope.currentFileName = subobject.originalFileName; // If subobject was cached previously, use the cached one - } // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path) - // Update also subobject.locationState for the next try if this load fails. + var cached = scope.subobjectCache[ subobject.originalFileName.toLowerCase() ]; + if ( cached ) { - var subobjectURL = subobject.fileName; - var newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND; + scope.processObject( cached, function ( subobjectGroup ) { - switch ( subobject.locationState ) { + onSubobjectLoaded( subobjectGroup, subobject ); + onSubobjectFinish(); - case LDrawLoader.FILE_LOCATION_AS_IS: - newLocationState = subobject.locationState + 1; - break; + }, subobject, url ); + return; - case LDrawLoader.FILE_LOCATION_TRY_PARTS: - subobjectURL = 'parts/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + } // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path) + // Update also subobject.locationState for the next try if this load fails. - case LDrawLoader.FILE_LOCATION_TRY_P: - subobjectURL = 'p/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; - case LDrawLoader.FILE_LOCATION_TRY_MODELS: - subobjectURL = 'models/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + var subobjectURL = subobject.fileName; + var newLocationState = FILE_LOCATION_NOT_FOUND; - case LDrawLoader.FILE_LOCATION_TRY_RELATIVE: - subobjectURL = url.substring( 0, url.lastIndexOf( '/' ) + 1 ) + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + switch ( subobject.locationState ) { - case LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE: - if ( subobject.triedLowerCase ) { + case FILE_LOCATION_AS_IS: + newLocationState = subobject.locationState + 1; + break; - // Try absolute path - newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND; + case FILE_LOCATION_TRY_PARTS: + subobjectURL = 'parts/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - } else { + case FILE_LOCATION_TRY_P: + subobjectURL = 'p/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - // Next attempt is lower case - subobject.fileName = subobject.fileName.toLowerCase(); - subobjectURL = subobject.fileName; - subobject.triedLowerCase = true; - newLocationState = LDrawLoader.FILE_LOCATION_AS_IS; + case FILE_LOCATION_TRY_MODELS: + subobjectURL = 'models/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - } + case FILE_LOCATION_TRY_RELATIVE: + subobjectURL = url.substring( 0, url.lastIndexOf( '/' ) + 1 ) + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - break; + case FILE_LOCATION_TRY_ABSOLUTE: + if ( subobject.triedLowerCase ) { - case LDrawLoader.FILE_LOCATION_NOT_FOUND: - // All location possibilities have been tried, give up loading this object - console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' ); - return; + // Try absolute path + newLocationState = FILE_LOCATION_NOT_FOUND; - } + } else { - subobject.locationState = newLocationState; - subobject.url = subobjectURL; // Load the subobject - // Use another file loader here so we can keep track of the subobject information - // and use it when processing the next model. + // Next attempt is lower case + subobject.fileName = subobject.fileName.toLowerCase(); + subobjectURL = subobject.fileName; + subobject.triedLowerCase = true; + newLocationState = FILE_LOCATION_AS_IS; - var fileLoader = new THREE.FileLoader( scope.manager ); - fileLoader.setPath( scope.path ); - fileLoader.setRequestHeader( scope.requestHeader ); - fileLoader.setWithCredentials( scope.withCredentials ); - fileLoader.load( subobjectURL, function ( text ) { + } - scope.processObject( text, function ( subobjectGroup ) { + break; - onSubobjectLoaded( subobjectGroup, subobject ); - onSubobjectFinish(); + case FILE_LOCATION_NOT_FOUND: + // All location possibilities have been tried, give up loading this object + console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' ); + return; - }, subobject, url ); + } - }, undefined, function ( err ) { + subobject.locationState = newLocationState; + subobject.url = subobjectURL; // Load the subobject + // Use another file loader here so we can keep track of the subobject information + // and use it when processing the next model. - onSubobjectError( err, subobject ); + var fileLoader = new THREE.FileLoader( scope.manager ); + fileLoader.setPath( scope.path ); + fileLoader.setRequestHeader( scope.requestHeader ); + fileLoader.setWithCredentials( scope.withCredentials ); + fileLoader.load( subobjectURL, function ( text ) { - }, subobject ); + scope.processObject( text, function ( subobjectGroup ) { - } + onSubobjectLoaded( subobjectGroup, subobject ); + onSubobjectFinish(); - function onSubobjectLoaded( subobjectGroup, subobject ) { + }, subobject, url ); - if ( subobjectGroup === null ) { + }, undefined, function ( err ) { - // Try to reload - loadSubobject( subobject ); - return; + onSubobjectError( err, subobject ); - } + }, subobject ); - scope.fileMap[ subobject.originalFileName ] = subobject.url; + } - } + function onSubobjectLoaded( subobjectGroup, subobject ) { - function onSubobjectError( err, subobject ) { + if ( subobjectGroup === null ) { - // Retry download from a different default possible location + // Try to reload loadSubobject( subobject ); + return; } + scope.fileMap[ subobject.originalFileName ] = subobject.url; + } - } ); - return LDrawLoader; - }(); + function onSubobjectError( err, subobject ) { + + // Retry download from a different default possible location + loadSubobject( subobject ); + + } + + } + + } THREE.LDrawLoader = LDrawLoader; diff --git a/examples/js/loaders/MMDLoader.js b/examples/js/loaders/MMDLoader.js index 64ed3b838d4d43..49bb50316a1b7a 100644 --- a/examples/js/loaders/MMDLoader.js +++ b/examples/js/loaders/MMDLoader.js @@ -29,14 +29,15 @@ * - shadow support. */ - var MMDLoader = function () { + /** + * @param {THREE.LoadingManager} manager + */ - /** - * @param {THREE.LoadingManager} manager - */ - function MMDLoader( manager ) { + class MMDLoader extends THREE.Loader { + + constructor( manager ) { - THREE.Loader.call( this, manager ); + super( manager ); this.loader = new THREE.FileLoader( this.manager ); this.parser = null; // lazy generation @@ -44,890 +45,901 @@ this.animationBuilder = new AnimationBuilder(); } + /** + * @param {string} animationPath + * @return {MMDLoader} + */ - MMDLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: MMDLoader, - /** - * @param {string} animationPath - * @return {MMDLoader} - */ - setAnimationPath: function ( animationPath ) { + setAnimationPath( animationPath ) { - this.animationPath = animationPath; - return this; + this.animationPath = animationPath; + return this; - }, - // Load MMD assets as Three.js Object + } // Load MMD assets as Three.js Object - /** - * Loads Model file (.pmd or .pmx) as a THREE.SkinnedMesh. - * - * @param {string} url - url to Model(.pmd or .pmx) file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - load: function ( url, onLoad, onProgress, onError ) { + /** + * Loads Model file (.pmd or .pmx) as a THREE.SkinnedMesh. + * + * @param {string} url - url to Model(.pmd or .pmx) file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - var builder = this.meshBuilder.setCrossOrigin( this.crossOrigin ); // resource path - var resourcePath; + load( url, onLoad, onProgress, onError ) { - if ( this.resourcePath !== '' ) { + const builder = this.meshBuilder.setCrossOrigin( this.crossOrigin ); // resource path - resourcePath = this.resourcePath; + let resourcePath; - } else if ( this.path !== '' ) { + if ( this.resourcePath !== '' ) { - resourcePath = this.path; + resourcePath = this.resourcePath; - } else { + } else if ( this.path !== '' ) { - resourcePath = THREE.LoaderUtils.extractUrlBase( url ); + resourcePath = this.path; - } + } else { - var modelExtension = this._extractExtension( url ).toLowerCase(); // Should I detect by seeing header? + resourcePath = THREE.LoaderUtils.extractUrlBase( url ); + } - if ( modelExtension !== 'pmd' && modelExtension !== 'pmx' ) { + const modelExtension = this._extractExtension( url ).toLowerCase(); // Should I detect by seeing header? - if ( onError ) onError( new Error( 'THREE.MMDLoader: Unknown model file extension .' + modelExtension + '.' ) ); - return; - } + if ( modelExtension !== 'pmd' && modelExtension !== 'pmx' ) { - this[ modelExtension === 'pmd' ? 'loadPMD' : 'loadPMX' ]( url, function ( data ) { + if ( onError ) onError( new Error( 'THREE.MMDLoader: Unknown model file extension .' + modelExtension + '.' ) ); + return; - onLoad( builder.build( data, resourcePath, onProgress, onError ) ); + } - }, onProgress, onError ); + this[ modelExtension === 'pmd' ? 'loadPMD' : 'loadPMX' ]( url, function ( data ) { - }, + onLoad( builder.build( data, resourcePath, onProgress, onError ) ); - /** - * Loads Motion file(s) (.vmd) as a THREE.AnimationClip. - * If two or more files are specified, they'll be merged. - * - * @param {string|Array} url - url(s) to animation(.vmd) file(s) - * @param {SkinnedMesh|THREE.Camera} object - tracks will be fitting to this object - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadAnimation: function ( url, object, onLoad, onProgress, onError ) { + }, onProgress, onError ); - var builder = this.animationBuilder; - this.loadVMD( url, function ( vmd ) { + } + /** + * Loads Motion file(s) (.vmd) as a THREE.AnimationClip. + * If two or more files are specified, they'll be merged. + * + * @param {string|Array} url - url(s) to animation(.vmd) file(s) + * @param {SkinnedMesh|THREE.Camera} object - tracks will be fitting to this object + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - onLoad( object.isCamera ? builder.buildCameraAnimation( vmd ) : builder.build( vmd, object ) ); - }, onProgress, onError ); + loadAnimation( url, object, onLoad, onProgress, onError ) { + + const builder = this.animationBuilder; + this.loadVMD( url, function ( vmd ) { + + onLoad( object.isCamera ? builder.buildCameraAnimation( vmd ) : builder.build( vmd, object ) ); + + }, onProgress, onError ); - }, + } + /** + * Loads mode file and motion file(s) as an object containing + * a THREE.SkinnedMesh and a THREE.AnimationClip. + * Tracks of THREE.AnimationClip are fitting to the model. + * + * @param {string} modelUrl - url to Model(.pmd or .pmx) file + * @param {string|Array{string}} vmdUrl - url(s) to animation(.vmd) file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - /** - * Loads mode file and motion file(s) as an object containing - * a THREE.SkinnedMesh and a THREE.AnimationClip. - * Tracks of THREE.AnimationClip are fitting to the model. - * - * @param {string} modelUrl - url to Model(.pmd or .pmx) file - * @param {string|Array{string}} vmdUrl - url(s) to animation(.vmd) file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadWithAnimation: function ( modelUrl, vmdUrl, onLoad, onProgress, onError ) { - var scope = this; - this.load( modelUrl, function ( mesh ) { + loadWithAnimation( modelUrl, vmdUrl, onLoad, onProgress, onError ) { - scope.loadAnimation( vmdUrl, mesh, function ( animation ) { + const scope = this; + this.load( modelUrl, function ( mesh ) { - onLoad( { - mesh: mesh, - animation: animation - } ); + scope.loadAnimation( vmdUrl, mesh, function ( animation ) { - }, onProgress, onError ); + onLoad( { + mesh: mesh, + animation: animation + } ); }, onProgress, onError ); - }, - // Load MMD assets as Object data parsed by MMDParser + }, onProgress, onError ); - /** - * Loads .pmd file as an Object. - * - * @param {string} url - url to .pmd file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadPMD: function ( url, onLoad, onProgress, onError ) { + } // Load MMD assets as Object data parsed by MMDParser - var parser = this._getParser(); + /** + * Loads .pmd file as an Object. + * + * @param {string} url - url to .pmd file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - this.loader.setMimeType( undefined ).setPath( this.path ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( buffer ) { - onLoad( parser.parsePmd( buffer, true ) ); + loadPMD( url, onLoad, onProgress, onError ) { - }, onProgress, onError ); + const parser = this._getParser(); - }, + this.loader.setMimeType( undefined ).setPath( this.path ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( buffer ) { - /** - * Loads .pmx file as an Object. - * - * @param {string} url - url to .pmx file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadPMX: function ( url, onLoad, onProgress, onError ) { + onLoad( parser.parsePmd( buffer, true ) ); - var parser = this._getParser(); + }, onProgress, onError ); - this.loader.setMimeType( undefined ).setPath( this.path ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( buffer ) { + } + /** + * Loads .pmx file as an Object. + * + * @param {string} url - url to .pmx file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - onLoad( parser.parsePmx( buffer, true ) ); - }, onProgress, onError ); + loadPMX( url, onLoad, onProgress, onError ) { - }, + const parser = this._getParser(); - /** - * Loads .vmd file as an Object. If two or more files are specified - * they'll be merged. - * - * @param {string|Array} url - url(s) to .vmd file(s) - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadVMD: function ( url, onLoad, onProgress, onError ) { + this.loader.setMimeType( undefined ).setPath( this.path ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( buffer ) { - var urls = Array.isArray( url ) ? url : [ url ]; - var vmds = []; - var vmdNum = urls.length; + onLoad( parser.parsePmx( buffer, true ) ); - var parser = this._getParser(); + }, onProgress, onError ); - this.loader.setMimeType( undefined ).setPath( this.animationPath ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ); + } + /** + * Loads .vmd file as an Object. If two or more files are specified + * they'll be merged. + * + * @param {string|Array} url - url(s) to .vmd file(s) + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - for ( var i = 0, il = urls.length; i < il; i ++ ) { - this.loader.load( urls[ i ], function ( buffer ) { + loadVMD( url, onLoad, onProgress, onError ) { - vmds.push( parser.parseVmd( buffer, true ) ); - if ( vmds.length === vmdNum ) onLoad( parser.mergeVmds( vmds ) ); + const urls = Array.isArray( url ) ? url : [ url ]; + const vmds = []; + const vmdNum = urls.length; - }, onProgress, onError ); + const parser = this._getParser(); - } + this.loader.setMimeType( undefined ).setPath( this.animationPath ).setResponseType( 'arraybuffer' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ); - }, + for ( let i = 0, il = urls.length; i < il; i ++ ) { - /** - * Loads .vpd file as an Object. - * - * @param {string} url - url to .vpd file - * @param {boolean} isUnicode - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadVPD: function ( url, isUnicode, onLoad, onProgress, onError ) { + this.loader.load( urls[ i ], function ( buffer ) { - var parser = this._getParser(); + vmds.push( parser.parseVmd( buffer, true ) ); + if ( vmds.length === vmdNum ) onLoad( parser.mergeVmds( vmds ) ); - this.loader.setMimeType( isUnicode ? undefined : 'text/plain; charset=shift_jis' ).setPath( this.animationPath ).setResponseType( 'text' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( text ) { + }, onProgress, onError ); - onLoad( parser.parseVpd( text, true ) ); + } - }, onProgress, onError ); + } + /** + * Loads .vpd file as an Object. + * + * @param {string} url - url to .vpd file + * @param {boolean} isUnicode + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ - }, - // private methods - _extractExtension: function ( url ) { - var index = url.lastIndexOf( '.' ); - return index < 0 ? '' : url.slice( index + 1 ); + loadVPD( url, isUnicode, onLoad, onProgress, onError ) { - }, - _getParser: function () { + const parser = this._getParser(); - if ( this.parser === null ) { + this.loader.setMimeType( isUnicode ? undefined : 'text/plain; charset=shift_jis' ).setPath( this.animationPath ).setResponseType( 'text' ).setRequestHeader( this.requestHeader ).setWithCredentials( this.withCredentials ).load( url, function ( text ) { - if ( typeof MMDParser === 'undefined' ) { + onLoad( parser.parseVpd( text, true ) ); - throw new Error( 'THREE.MMDLoader: Import MMDParser https://github.com/takahirox/mmd-parser' ); + }, onProgress, onError ); - } + } // private methods - this.parser = new MMDParser.Parser(); // eslint-disable-line no-undef + + _extractExtension( url ) { + + const index = url.lastIndexOf( '.' ); + return index < 0 ? '' : url.slice( index + 1 ); + + } + + _getParser() { + + if ( this.parser === null ) { + + if ( typeof MMDParser === 'undefined' ) { + + throw new Error( 'THREE.MMDLoader: Import MMDParser https://github.com/takahirox/mmd-parser' ); } - return this.parser; + this.parser = new MMDParser.Parser(); // eslint-disable-line no-undef } - } ); // Utilities - /* + return this.parser; + + } + + } // Utilities + + /* * base64 encoded defalut toon textures toon00.bmp - toon10.bmp. * We don't need to request external toon image files. * This idea is from http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js */ - var DEFAULT_TOON_TEXTURES = [ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/bWiiMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh8aBHZBl14e8wAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOUlEQVRYR+3WMREAMAwDsYY/yoDI7MLwIiP40+RJklfcCCBAgAABAgTqArfb/QMCCBAgQIAAgbbAB3z/e0F3js2cAAAAAElFTkSuQmCC', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/B5ilMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh81dWyx0gFwKAAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOklEQVRYR+3WoREAMAwDsWb/UQtCy9wxTOQJ/oQ8SXKKGwEECBAgQIBAXeDt7f4BAQQIECBAgEBb4AOz8Hzx7WLY4wAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABPUlEQVRYR+1XwW7CMAy1+f9fZOMysSEOEweEOPRNdm3HbdOyIhAcklPrOs/PLy9RygBALxzcCDQFmgJNgaZAU6Ap0BR4PwX8gsRMVLssMRH5HcpzJEaWL7EVg9F1IHRlyqQohgVr4FGUlUcMJSjcUlDw0zvjeun70cLWmneoyf7NgBTQSniBTQQSuJAZsOnnaczjIMb5hCiuHKxokCrJfVnrctyZL0PkJAJe1HMil4nxeyi3Ypfn1kX51jpPvo/JeCNC4PhVdHdJw2XjBR8brF8PEIhNVn12AgP7uHsTBguBn53MUZCqv7Lp07Pn5k1Ro+uWmUNn7D+M57rtk7aG0Vo73xyF/fbFf0bPJjDXngnGocDTdFhygZjwUQrMNrDcmZlQT50VJ/g/UwNyHpu778+yW+/ksOz/BFo54P4AsUXMfRq7XWsAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACMElEQVRYR+2Xv4pTQRTGf2dubhLdICiii2KnYKHVolhauKWPoGAnNr6BD6CvIVaihYuI2i1ia0BY0MZGRHQXjZj/mSPnnskfNWiWZUlzJ5k7M2cm833nO5Mziej2DWWJRUoCpQKlAntSQCqgw39/iUWAGmh37jrRnVsKlgpiqmkoGVABA7E57fvY+pJDdgKqF6HzFCSADkDq+F6AHABtQ+UMVE5D7zXod7fFNhTEckTbj5XQgHzNN+5tQvc5NG7C6BNkp6D3EmpXHDR+dQAjFLchW3VS9rlw3JBh+B7ys5Cf9z0GW1C/7P32AyBAOAz1q4jGliIH3YPuBnSfQX4OGreTIgEYQb/pBDtPnEQ4CivXYPAWBk13oHrB54yA9QuSn2H4AcKRpEILDt0BUzj+RLR1V5EqjD66NPRBVpLcQwjHoHYJOhsQv6U4mnzmrIXJCFr4LDwm/xBUoboG9XX4cc9VKdYoSA2yk5NQLJaKDUjTBoveG3Z2TElTxwjNK4M3LEZgUdDdruvcXzKBpStgp2NPiWi3ks9ZXxIoFVi+AvHLdc9TqtjL3/aYjpPlrzOcEnK62Szhimdd7xX232zFDTgtxezOu3WNMRLjiKgjtOhHVMd1loynVHvOgjuIIJMaELEqhJAV/RCSLbWTcfPFakFgFlALTRRvx+ok6Hlp/Q+v3fmx90bMyUzaEAhmM3KvHlXTL5DxnbGf/1M8RNNACLL5MNtPxP/mypJAqcDSFfgFhpYqWUzhTEAAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=' ]; // Builders. They build Three.js object from Object data parsed by MMDParser. - /** - * @param {THREE.LoadingManager} manager - */ + const DEFAULT_TOON_TEXTURES = [ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/bWiiMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh8aBHZBl14e8wAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOUlEQVRYR+3WMREAMAwDsYY/yoDI7MLwIiP40+RJklfcCCBAgAABAgTqArfb/QMCCBAgQIAAgbbAB3z/e0F3js2cAAAAAElFTkSuQmCC', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/B5ilMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh81dWyx0gFwKAAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOklEQVRYR+3WoREAMAwDsWb/UQtCy9wxTOQJ/oQ8SXKKGwEECBAgQIBAXeDt7f4BAQQIECBAgEBb4AOz8Hzx7WLY4wAAAABJRU5ErkJggg==', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABPUlEQVRYR+1XwW7CMAy1+f9fZOMysSEOEweEOPRNdm3HbdOyIhAcklPrOs/PLy9RygBALxzcCDQFmgJNgaZAU6Ap0BR4PwX8gsRMVLssMRH5HcpzJEaWL7EVg9F1IHRlyqQohgVr4FGUlUcMJSjcUlDw0zvjeun70cLWmneoyf7NgBTQSniBTQQSuJAZsOnnaczjIMb5hCiuHKxokCrJfVnrctyZL0PkJAJe1HMil4nxeyi3Ypfn1kX51jpPvo/JeCNC4PhVdHdJw2XjBR8brF8PEIhNVn12AgP7uHsTBguBn53MUZCqv7Lp07Pn5k1Ro+uWmUNn7D+M57rtk7aG0Vo73xyF/fbFf0bPJjDXngnGocDTdFhygZjwUQrMNrDcmZlQT50VJ/g/UwNyHpu778+yW+/ksOz/BFo54P4AsUXMfRq7XWsAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACMElEQVRYR+2Xv4pTQRTGf2dubhLdICiii2KnYKHVolhauKWPoGAnNr6BD6CvIVaihYuI2i1ia0BY0MZGRHQXjZj/mSPnnskfNWiWZUlzJ5k7M2cm833nO5Mziej2DWWJRUoCpQKlAntSQCqgw39/iUWAGmh37jrRnVsKlgpiqmkoGVABA7E57fvY+pJDdgKqF6HzFCSADkDq+F6AHABtQ+UMVE5D7zXod7fFNhTEckTbj5XQgHzNN+5tQvc5NG7C6BNkp6D3EmpXHDR+dQAjFLchW3VS9rlw3JBh+B7ys5Cf9z0GW1C/7P32AyBAOAz1q4jGliIH3YPuBnSfQX4OGreTIgEYQb/pBDtPnEQ4CivXYPAWBk13oHrB54yA9QuSn2H4AcKRpEILDt0BUzj+RLR1V5EqjD66NPRBVpLcQwjHoHYJOhsQv6U4mnzmrIXJCFr4LDwm/xBUoboG9XX4cc9VKdYoSA2yk5NQLJaKDUjTBoveG3Z2TElTxwjNK4M3LEZgUdDdruvcXzKBpStgp2NPiWi3ks9ZXxIoFVi+AvHLdc9TqtjL3/aYjpPlrzOcEnK62Szhimdd7xX232zFDTgtxezOu3WNMRLjiKgjtOhHVMd1loynVHvOgjuIIJMaELEqhJAV/RCSLbWTcfPFakFgFlALTRRvx+ok6Hlp/Q+v3fmx90bMyUzaEAhmM3KvHlXTL5DxnbGf/1M8RNNACLL5MNtPxP/mypJAqcDSFfgFhpYqWUzhTEAAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=' ]; // Builders. They build Three.js object from Object data parsed by MMDParser. + + /** + * @param {THREE.LoadingManager} manager + */ - function MeshBuilder( manager ) { + class MeshBuilder { + constructor( manager ) { + + this.crossOrigin = 'anonymous'; this.geometryBuilder = new GeometryBuilder(); this.materialBuilder = new MaterialBuilder( manager ); } + /** + * @param {string} crossOrigin + * @return {MeshBuilder} + */ - MeshBuilder.prototype = { - constructor: MeshBuilder, - crossOrigin: 'anonymous', - - /** - * @param {string} crossOrigin - * @return {MeshBuilder} - */ - setCrossOrigin: function ( crossOrigin ) { - this.crossOrigin = crossOrigin; - return this; + setCrossOrigin( crossOrigin ) { - }, + this.crossOrigin = crossOrigin; + return this; - /** - * @param {Object} data - parsed PMD/PMX data - * @param {string} resourcePath - * @param {function} onProgress - * @param {function} onError - * @return {SkinnedMesh} - */ - build: function ( data, resourcePath, onProgress, onError ) { + } + /** + * @param {Object} data - parsed PMD/PMX data + * @param {string} resourcePath + * @param {function} onProgress + * @param {function} onError + * @return {SkinnedMesh} + */ - var geometry = this.geometryBuilder.build( data ); - var material = this.materialBuilder.setCrossOrigin( this.crossOrigin ).setResourcePath( resourcePath ).build( data, geometry, onProgress, onError ); - var mesh = new THREE.SkinnedMesh( geometry, material ); - var skeleton = new THREE.Skeleton( initBones( mesh ) ); - mesh.bind( skeleton ); // console.log( mesh ); // for console debug - return mesh; + build( data, resourcePath, onProgress, onError ) { - } - }; // TODO: Try to remove this function + const geometry = this.geometryBuilder.build( data ); + const material = this.materialBuilder.setCrossOrigin( this.crossOrigin ).setResourcePath( resourcePath ).build( data, geometry, onProgress, onError ); + const mesh = new THREE.SkinnedMesh( geometry, material ); + const skeleton = new THREE.Skeleton( initBones( mesh ) ); + mesh.bind( skeleton ); // console.log( mesh ); // for console debug - function initBones( mesh ) { + return mesh; - var geometry = mesh.geometry; - var bones = [], - bone, - gbone; - var i, il; + } - if ( geometry && geometry.bones !== undefined ) { + } // TODO: Try to remove this function - // first, create array of 'Bone' objects from geometry data - for ( i = 0, il = geometry.bones.length; i < il; i ++ ) { - gbone = geometry.bones[ i ]; // create new 'Bone' object + function initBones( mesh ) { - bone = new THREE.Bone(); - bones.push( bone ); // apply values + const geometry = mesh.geometry; + const bones = []; - bone.name = gbone.name; - bone.position.fromArray( gbone.pos ); - bone.quaternion.fromArray( gbone.rotq ); - if ( gbone.scl !== undefined ) bone.scale.fromArray( gbone.scl ); + if ( geometry && geometry.bones !== undefined ) { - } // second, create bone hierarchy + // first, create array of 'Bone' objects from geometry data + for ( let i = 0, il = geometry.bones.length; i < il; i ++ ) { + const gbone = geometry.bones[ i ]; // create new 'Bone' object - for ( i = 0, il = geometry.bones.length; i < il; i ++ ) { + const bone = new THREE.Bone(); + bones.push( bone ); // apply values - gbone = geometry.bones[ i ]; + bone.name = gbone.name; + bone.position.fromArray( gbone.pos ); + bone.quaternion.fromArray( gbone.rotq ); + if ( gbone.scl !== undefined ) bone.scale.fromArray( gbone.scl ); - if ( gbone.parent !== - 1 && gbone.parent !== null && bones[ gbone.parent ] !== undefined ) { + } // second, create bone hierarchy - // subsequent bones in the hierarchy - bones[ gbone.parent ].add( bones[ i ] ); - } else { + for ( let i = 0, il = geometry.bones.length; i < il; i ++ ) { - // topmost bone, immediate child of the skinned mesh - mesh.add( bones[ i ] ); + const gbone = geometry.bones[ i ]; - } + if ( gbone.parent !== - 1 && gbone.parent !== null && bones[ gbone.parent ] !== undefined ) { - } + // subsequent bones in the hierarchy + bones[ gbone.parent ].add( bones[ i ] ); - } // now the bones are part of the scene graph and children of the skinned mesh. - // let's update the corresponding matrices + } else { + // topmost bone, immediate child of the skinned mesh + mesh.add( bones[ i ] ); - mesh.updateMatrixWorld( true ); - return bones; + } - } // + } + } // now the bones are part of the scene graph and children of the skinned mesh. + // let's update the corresponding matrices - function GeometryBuilder() {} - GeometryBuilder.prototype = { - constructor: GeometryBuilder, + mesh.updateMatrixWorld( true ); + return bones; - /** - * @param {Object} data - parsed PMD/PMX data - * @return {BufferGeometry} - */ - build: function ( data ) { + } // - // for geometry - var positions = []; - var uvs = []; - var normals = []; - var indices = []; - var groups = []; - var bones = []; - var skinIndices = []; - var skinWeights = []; - var morphTargets = []; - var morphPositions = []; - var iks = []; - var grants = []; - var rigidBodies = []; - var constraints = []; // for work - var offset = 0; - var boneTypeTable = {}; // positions, normals, uvs, skinIndices, skinWeights + class GeometryBuilder { - for ( var i = 0; i < data.metadata.vertexCount; i ++ ) { + /** + * @param {Object} data - parsed PMD/PMX data + * @return {BufferGeometry} + */ + build( data ) { - var v = data.vertices[ i ]; + // for geometry + const positions = []; + const uvs = []; + const normals = []; + const indices = []; + const groups = []; + const bones = []; + const skinIndices = []; + const skinWeights = []; + const morphTargets = []; + const morphPositions = []; + const iks = []; + const grants = []; + const rigidBodies = []; + const constraints = []; // for work - for ( var j = 0, jl = v.position.length; j < jl; j ++ ) { + let offset = 0; + const boneTypeTable = {}; // positions, normals, uvs, skinIndices, skinWeights - positions.push( v.position[ j ] ); + for ( let i = 0; i < data.metadata.vertexCount; i ++ ) { - } + const v = data.vertices[ i ]; - for ( var j = 0, jl = v.normal.length; j < jl; j ++ ) { + for ( let j = 0, jl = v.position.length; j < jl; j ++ ) { - normals.push( v.normal[ j ] ); + positions.push( v.position[ j ] ); - } + } - for ( var j = 0, jl = v.uv.length; j < jl; j ++ ) { + for ( let j = 0, jl = v.normal.length; j < jl; j ++ ) { - uvs.push( v.uv[ j ] ); + normals.push( v.normal[ j ] ); - } + } - for ( var j = 0; j < 4; j ++ ) { + for ( let j = 0, jl = v.uv.length; j < jl; j ++ ) { - skinIndices.push( v.skinIndices.length - 1 >= j ? v.skinIndices[ j ] : 0.0 ); + uvs.push( v.uv[ j ] ); - } + } - for ( var j = 0; j < 4; j ++ ) { + for ( let j = 0; j < 4; j ++ ) { - skinWeights.push( v.skinWeights.length - 1 >= j ? v.skinWeights[ j ] : 0.0 ); + skinIndices.push( v.skinIndices.length - 1 >= j ? v.skinIndices[ j ] : 0.0 ); - } + } - } // indices + for ( let j = 0; j < 4; j ++ ) { + skinWeights.push( v.skinWeights.length - 1 >= j ? v.skinWeights[ j ] : 0.0 ); - for ( var i = 0; i < data.metadata.faceCount; i ++ ) { + } - var face = data.faces[ i ]; + } // indices - for ( var j = 0, jl = face.indices.length; j < jl; j ++ ) { - indices.push( face.indices[ j ] ); + for ( let i = 0; i < data.metadata.faceCount; i ++ ) { - } + const face = data.faces[ i ]; - } // groups + for ( let j = 0, jl = face.indices.length; j < jl; j ++ ) { + indices.push( face.indices[ j ] ); - for ( var i = 0; i < data.metadata.materialCount; i ++ ) { + } - var material = data.materials[ i ]; - groups.push( { - offset: offset * 3, - count: material.faceCount * 3 - } ); - offset += material.faceCount; + } // groups - } // bones + for ( let i = 0; i < data.metadata.materialCount; i ++ ) { - for ( var i = 0; i < data.metadata.rigidBodyCount; i ++ ) { + const material = data.materials[ i ]; + groups.push( { + offset: offset * 3, + count: material.faceCount * 3 + } ); + offset += material.faceCount; - var body = data.rigidBodies[ i ]; - var value = boneTypeTable[ body.boneIndex ]; // keeps greater number if already value is set without any special reasons + } // bones - value = value === undefined ? body.type : Math.max( body.type, value ); - boneTypeTable[ body.boneIndex ] = value; - } + for ( let i = 0; i < data.metadata.rigidBodyCount; i ++ ) { - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { + const body = data.rigidBodies[ i ]; + let value = boneTypeTable[ body.boneIndex ]; // keeps greater number if already value is set without any special reasons - var boneData = data.bones[ i ]; - var bone = { - index: i, - transformationClass: boneData.transformationClass, - parent: boneData.parentIndex, - name: boneData.name, - pos: boneData.position.slice( 0, 3 ), - rotq: [ 0, 0, 0, 1 ], - scl: [ 1, 1, 1 ], - rigidBodyType: boneTypeTable[ i ] !== undefined ? boneTypeTable[ i ] : - 1 - }; + value = value === undefined ? body.type : Math.max( body.type, value ); + boneTypeTable[ body.boneIndex ] = value; - if ( bone.parent !== - 1 ) { + } - bone.pos[ 0 ] -= data.bones[ bone.parent ].position[ 0 ]; - bone.pos[ 1 ] -= data.bones[ bone.parent ].position[ 1 ]; - bone.pos[ 2 ] -= data.bones[ bone.parent ].position[ 2 ]; + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { + + const boneData = data.bones[ i ]; + const bone = { + index: i, + transformationClass: boneData.transformationClass, + parent: boneData.parentIndex, + name: boneData.name, + pos: boneData.position.slice( 0, 3 ), + rotq: [ 0, 0, 0, 1 ], + scl: [ 1, 1, 1 ], + rigidBodyType: boneTypeTable[ i ] !== undefined ? boneTypeTable[ i ] : - 1 + }; - } + if ( bone.parent !== - 1 ) { - bones.push( bone ); + bone.pos[ 0 ] -= data.bones[ bone.parent ].position[ 0 ]; + bone.pos[ 1 ] -= data.bones[ bone.parent ].position[ 1 ]; + bone.pos[ 2 ] -= data.bones[ bone.parent ].position[ 2 ]; - } // iks - // TODO: remove duplicated codes between PMD and PMX + } + bones.push( bone ); - if ( data.metadata.format === 'pmd' ) { + } // iks + // TODO: remove duplicated codes between PMD and PMX - for ( var i = 0; i < data.metadata.ikCount; i ++ ) { - var ik = data.iks[ i ]; - var param = { - target: ik.target, - effector: ik.effector, - iteration: ik.iteration, - maxAngle: ik.maxAngle * 4, - links: [] - }; + if ( data.metadata.format === 'pmd' ) { - for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) { + for ( let i = 0; i < data.metadata.ikCount; i ++ ) { - var link = {}; - link.index = ik.links[ j ].index; - link.enabled = true; + const ik = data.iks[ i ]; + const param = { + target: ik.target, + effector: ik.effector, + iteration: ik.iteration, + maxAngle: ik.maxAngle * 4, + links: [] + }; - if ( data.bones[ link.index ].name.indexOf( 'ひざ' ) >= 0 ) { + for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) { - link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); + const link = {}; + link.index = ik.links[ j ].index; + link.enabled = true; - } + if ( data.bones[ link.index ].name.indexOf( 'ひざ' ) >= 0 ) { - param.links.push( link ); + link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); } - iks.push( param ); + param.links.push( link ); } - } else { + iks.push( param ); - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { + } - var ik = data.bones[ i ].ik; - if ( ik === undefined ) continue; - var param = { - target: i, - effector: ik.effector, - iteration: ik.iteration, - maxAngle: ik.maxAngle, - links: [] - }; + } else { - for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) { + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { - var link = {}; - link.index = ik.links[ j ].index; - link.enabled = true; + const ik = data.bones[ i ].ik; + if ( ik === undefined ) continue; + const param = { + target: i, + effector: ik.effector, + iteration: ik.iteration, + maxAngle: ik.maxAngle, + links: [] + }; - if ( ik.links[ j ].angleLimitation === 1 ) { + for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) { - // Revert if rotationMin/Max doesn't work well - // link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); - var rotationMin = ik.links[ j ].lowerLimitationAngle; - var rotationMax = ik.links[ j ].upperLimitationAngle; // Convert Left to Right coordinate by myself because - // MMDParser doesn't convert. It's a MMDParser's bug + const link = {}; + link.index = ik.links[ j ].index; + link.enabled = true; - var tmp1 = - rotationMax[ 0 ]; - var tmp2 = - rotationMax[ 1 ]; - rotationMax[ 0 ] = - rotationMin[ 0 ]; - rotationMax[ 1 ] = - rotationMin[ 1 ]; - rotationMin[ 0 ] = tmp1; - rotationMin[ 1 ] = tmp2; - link.rotationMin = new THREE.Vector3().fromArray( rotationMin ); - link.rotationMax = new THREE.Vector3().fromArray( rotationMax ); + if ( ik.links[ j ].angleLimitation === 1 ) { - } + // Revert if rotationMin/Max doesn't work well + // link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); + const rotationMin = ik.links[ j ].lowerLimitationAngle; + const rotationMax = ik.links[ j ].upperLimitationAngle; // Convert Left to Right coordinate by myself because + // MMDParser doesn't convert. It's a MMDParser's bug - param.links.push( link ); + const tmp1 = - rotationMax[ 0 ]; + const tmp2 = - rotationMax[ 1 ]; + rotationMax[ 0 ] = - rotationMin[ 0 ]; + rotationMax[ 1 ] = - rotationMin[ 1 ]; + rotationMin[ 0 ] = tmp1; + rotationMin[ 1 ] = tmp2; + link.rotationMin = new THREE.Vector3().fromArray( rotationMin ); + link.rotationMax = new THREE.Vector3().fromArray( rotationMax ); } - iks.push( param ); // Save the reference even from bone data for efficiently - // simulating PMX animation system - - bones[ i ].ik = param; + param.links.push( link ); } - } // grants + iks.push( param ); // Save the reference even from bone data for efficiently + // simulating PMX animation system + bones[ i ].ik = param; - if ( data.metadata.format === 'pmx' ) { + } - // bone index -> grant entry map - var grantEntryMap = {}; - - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { - - var boneData = data.bones[ i ]; - var grant = boneData.grant; - if ( grant === undefined ) continue; - var param = { - index: i, - parentIndex: grant.parentIndex, - ratio: grant.ratio, - isLocal: grant.isLocal, - affectRotation: grant.affectRotation, - affectPosition: grant.affectPosition, - transformationClass: boneData.transformationClass - }; - grantEntryMap[ i ] = { - parent: null, - children: [], - param: param, - visited: false - }; + } // grants - } - var rootEntry = { + if ( data.metadata.format === 'pmx' ) { + + // bone index -> grant entry map + const grantEntryMap = {}; + + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { + + const boneData = data.bones[ i ]; + const grant = boneData.grant; + if ( grant === undefined ) continue; + const param = { + index: i, + parentIndex: grant.parentIndex, + ratio: grant.ratio, + isLocal: grant.isLocal, + affectRotation: grant.affectRotation, + affectPosition: grant.affectPosition, + transformationClass: boneData.transformationClass + }; + grantEntryMap[ i ] = { parent: null, children: [], - param: null, + param: param, visited: false - }; // Build a tree representing grant hierarchy + }; - for ( var boneIndex in grantEntryMap ) { + } - var grantEntry = grantEntryMap[ boneIndex ]; - var parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry; - grantEntry.parent = parentGrantEntry; - parentGrantEntry.children.push( grantEntry ); + const rootEntry = { + parent: null, + children: [], + param: null, + visited: false + }; // Build a tree representing grant hierarchy - } // Sort grant parameters from parents to children because - // grant uses parent's transform that parent's grant is already applied - // so grant should be applied in order from parents to children + for ( const boneIndex in grantEntryMap ) { + const grantEntry = grantEntryMap[ boneIndex ]; + const parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry; + grantEntry.parent = parentGrantEntry; + parentGrantEntry.children.push( grantEntry ); - function traverse( entry ) { + } // Sort grant parameters from parents to children because + // grant uses parent's transform that parent's grant is already applied + // so grant should be applied in order from parents to children - if ( entry.param ) { - grants.push( entry.param ); // Save the reference even from bone data for efficiently - // simulating PMX animation system + function traverse( entry ) { - bones[ entry.param.index ].grant = entry.param; + if ( entry.param ) { - } + grants.push( entry.param ); // Save the reference even from bone data for efficiently + // simulating PMX animation system + + bones[ entry.param.index ].grant = entry.param; - entry.visited = true; + } - for ( var i = 0, il = entry.children.length; i < il; i ++ ) { + entry.visited = true; - var child = entry.children[ i ]; // Cut off a loop if exists. (Is a grant loop invalid?) + for ( let i = 0, il = entry.children.length; i < il; i ++ ) { - if ( ! child.visited ) traverse( child ); + const child = entry.children[ i ]; // Cut off a loop if exists. (Is a grant loop invalid?) - } + if ( ! child.visited ) traverse( child ); } - traverse( rootEntry ); - - } // morph + } + traverse( rootEntry ); - function updateAttributes( attribute, morph, ratio ) { + } // morph - for ( var i = 0; i < morph.elementCount; i ++ ) { - var element = morph.elements[ i ]; - var index; + function updateAttributes( attribute, morph, ratio ) { - if ( data.metadata.format === 'pmd' ) { + for ( let i = 0; i < morph.elementCount; i ++ ) { - index = data.morphs[ 0 ].elements[ element.index ].index; + const element = morph.elements[ i ]; + let index; - } else { + if ( data.metadata.format === 'pmd' ) { - index = element.index; + index = data.morphs[ 0 ].elements[ element.index ].index; - } + } else { - attribute.array[ index * 3 + 0 ] += element.position[ 0 ] * ratio; - attribute.array[ index * 3 + 1 ] += element.position[ 1 ] * ratio; - attribute.array[ index * 3 + 2 ] += element.position[ 2 ] * ratio; + index = element.index; } + attribute.array[ index * 3 + 0 ] += element.position[ 0 ] * ratio; + attribute.array[ index * 3 + 1 ] += element.position[ 1 ] * ratio; + attribute.array[ index * 3 + 2 ] += element.position[ 2 ] * ratio; + } - for ( var i = 0; i < data.metadata.morphCount; i ++ ) { + } - var morph = data.morphs[ i ]; - var params = { - name: morph.name - }; - var attribute = new THREE.Float32BufferAttribute( data.metadata.vertexCount * 3, 3 ); - attribute.name = morph.name; + for ( let i = 0; i < data.metadata.morphCount; i ++ ) { - for ( var j = 0; j < data.metadata.vertexCount * 3; j ++ ) { + const morph = data.morphs[ i ]; + const params = { + name: morph.name + }; + const attribute = new THREE.Float32BufferAttribute( data.metadata.vertexCount * 3, 3 ); + attribute.name = morph.name; - attribute.array[ j ] = positions[ j ]; + for ( let j = 0; j < data.metadata.vertexCount * 3; j ++ ) { - } + attribute.array[ j ] = positions[ j ]; - if ( data.metadata.format === 'pmd' ) { + } - if ( i !== 0 ) { + if ( data.metadata.format === 'pmd' ) { - updateAttributes( attribute, morph, 1.0 ); + if ( i !== 0 ) { - } + updateAttributes( attribute, morph, 1.0 ); - } else { + } - if ( morph.type === 0 ) { + } else { - // group - for ( var j = 0; j < morph.elementCount; j ++ ) { + if ( morph.type === 0 ) { - var morph2 = data.morphs[ morph.elements[ j ].index ]; - var ratio = morph.elements[ j ].ratio; + // group + for ( let j = 0; j < morph.elementCount; j ++ ) { - if ( morph2.type === 1 ) { + const morph2 = data.morphs[ morph.elements[ j ].index ]; + const ratio = morph.elements[ j ].ratio; - updateAttributes( attribute, morph2, ratio ); + if ( morph2.type === 1 ) { - } else { // TODO: implement - } + updateAttributes( attribute, morph2, ratio ); + } else { // TODO: implement } - } else if ( morph.type === 1 ) { - - // vertex - updateAttributes( attribute, morph, 1.0 ); - - } else if ( morph.type === 2 ) { // bone - // TODO: implement - } else if ( morph.type === 3 ) { // uv - // TODO: implement - } else if ( morph.type === 4 ) { // additional uv1 - // TODO: implement - } else if ( morph.type === 5 ) { // additional uv2 - // TODO: implement - } else if ( morph.type === 6 ) { // additional uv3 - // TODO: implement - } else if ( morph.type === 7 ) { // additional uv4 - // TODO: implement - } else if ( morph.type === 8 ) { // material - // TODO: implement } + } else if ( morph.type === 1 ) { + + // vertex + updateAttributes( attribute, morph, 1.0 ); + + } else if ( morph.type === 2 ) { // bone + // TODO: implement + } else if ( morph.type === 3 ) { // uv + // TODO: implement + } else if ( morph.type === 4 ) { // additional uv1 + // TODO: implement + } else if ( morph.type === 5 ) { // additional uv2 + // TODO: implement + } else if ( morph.type === 6 ) { // additional uv3 + // TODO: implement + } else if ( morph.type === 7 ) { // additional uv4 + // TODO: implement + } else if ( morph.type === 8 ) { // material + // TODO: implement } - morphTargets.push( params ); - morphPositions.push( attribute ); + } - } // rigid bodies from rigidBodies field. + morphTargets.push( params ); + morphPositions.push( attribute ); + } // rigid bodies from rigidBodies field. - for ( var i = 0; i < data.metadata.rigidBodyCount; i ++ ) { - var rigidBody = data.rigidBodies[ i ]; - var params = {}; + for ( let i = 0; i < data.metadata.rigidBodyCount; i ++ ) { - for ( var key in rigidBody ) { + const rigidBody = data.rigidBodies[ i ]; + const params = {}; - params[ key ] = rigidBody[ key ]; + for ( const key in rigidBody ) { - } - /* + params[ key ] = rigidBody[ key ]; + + } + /* * RigidBody position parameter in PMX seems global position * while the one in PMD seems offset from corresponding bone. * So unify being offset. */ - if ( data.metadata.format === 'pmx' ) { - - if ( params.boneIndex !== - 1 ) { + if ( data.metadata.format === 'pmx' ) { - var bone = data.bones[ params.boneIndex ]; - params.position[ 0 ] -= bone.position[ 0 ]; - params.position[ 1 ] -= bone.position[ 1 ]; - params.position[ 2 ] -= bone.position[ 2 ]; + if ( params.boneIndex !== - 1 ) { - } + const bone = data.bones[ params.boneIndex ]; + params.position[ 0 ] -= bone.position[ 0 ]; + params.position[ 1 ] -= bone.position[ 1 ]; + params.position[ 2 ] -= bone.position[ 2 ]; } - rigidBodies.push( params ); + } - } // constraints from constraints field. + rigidBodies.push( params ); + } // constraints from constraints field. - for ( var i = 0; i < data.metadata.constraintCount; i ++ ) { - var constraint = data.constraints[ i ]; - var params = {}; + for ( let i = 0; i < data.metadata.constraintCount; i ++ ) { - for ( var key in constraint ) { + const constraint = data.constraints[ i ]; + const params = {}; - params[ key ] = constraint[ key ]; + for ( const key in constraint ) { - } + params[ key ] = constraint[ key ]; - var bodyA = rigidBodies[ params.rigidBodyIndex1 ]; - var bodyB = rigidBodies[ params.rigidBodyIndex2 ]; // Refer to http://www20.atpages.jp/katwat/wp/?p=4135 + } - if ( bodyA.type !== 0 && bodyB.type === 2 ) { + const bodyA = rigidBodies[ params.rigidBodyIndex1 ]; + const bodyB = rigidBodies[ params.rigidBodyIndex2 ]; // Refer to http://www20.atpages.jp/katwat/wp/?p=4135 - if ( bodyA.boneIndex !== - 1 && bodyB.boneIndex !== - 1 && data.bones[ bodyB.boneIndex ].parentIndex === bodyA.boneIndex ) { + if ( bodyA.type !== 0 && bodyB.type === 2 ) { - bodyB.type = 1; + if ( bodyA.boneIndex !== - 1 && bodyB.boneIndex !== - 1 && data.bones[ bodyB.boneIndex ].parentIndex === bodyA.boneIndex ) { - } + bodyB.type = 1; } - constraints.push( params ); + } - } // build THREE.BufferGeometry. + constraints.push( params ); + } // build THREE.BufferGeometry. - var geometry = new THREE.BufferGeometry(); - geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); - geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); - geometry.setAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); - geometry.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( skinIndices, 4 ) ); - geometry.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeights, 4 ) ); - geometry.setIndex( indices ); - for ( var i = 0, il = groups.length; i < il; i ++ ) { + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); + geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); + geometry.setAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); + geometry.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( skinIndices, 4 ) ); + geometry.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeights, 4 ) ); + geometry.setIndex( indices ); - geometry.addGroup( groups[ i ].offset, groups[ i ].count, i ); + for ( let i = 0, il = groups.length; i < il; i ++ ) { - } - - geometry.bones = bones; - geometry.morphTargets = morphTargets; - geometry.morphAttributes.position = morphPositions; - geometry.morphTargetsRelative = false; - geometry.userData.MMD = { - bones: bones, - iks: iks, - grants: grants, - rigidBodies: rigidBodies, - constraints: constraints, - format: data.metadata.format - }; - geometry.computeBoundingSphere(); - return geometry; + geometry.addGroup( groups[ i ].offset, groups[ i ].count, i ); } - }; // - /** - * @param {THREE.LoadingManager} manager - */ + geometry.bones = bones; + geometry.morphTargets = morphTargets; + geometry.morphAttributes.position = morphPositions; + geometry.morphTargetsRelative = false; + geometry.userData.MMD = { + bones: bones, + iks: iks, + grants: grants, + rigidBodies: rigidBodies, + constraints: constraints, + format: data.metadata.format + }; + geometry.computeBoundingSphere(); + return geometry; - function MaterialBuilder( manager ) { + } + + } // + + /** + * @param {THREE.LoadingManager} manager + */ + + + class MaterialBuilder { + + constructor( manager ) { this.manager = manager; this.textureLoader = new THREE.TextureLoader( this.manager ); this.tgaLoader = null; // lazy generation + this.crossOrigin = 'anonymous'; + this.resourcePath = undefined; + + } + /** + * @param {string} crossOrigin + * @return {MaterialBuilder} + */ + + + setCrossOrigin( crossOrigin ) { + + this.crossOrigin = crossOrigin; + return this; + } + /** + * @param {string} resourcePath + * @return {MaterialBuilder} + */ - MaterialBuilder.prototype = { - constructor: MaterialBuilder, - crossOrigin: 'anonymous', - resourcePath: undefined, - - /** - * @param {string} crossOrigin - * @return {MaterialBuilder} - */ - setCrossOrigin: function ( crossOrigin ) { - - this.crossOrigin = crossOrigin; - return this; - - }, - - /** - * @param {string} resourcePath - * @return {MaterialBuilder} - */ - setResourcePath: function ( resourcePath ) { - - this.resourcePath = resourcePath; - return this; - - }, - - /** - * @param {Object} data - parsed PMD/PMX data - * @param {BufferGeometry} geometry - some properties are dependend on geometry - * @param {function} onProgress - * @param {function} onError - * @return {Array} - */ - build: function ( data, geometry - /*, onProgress, onError */ - ) { - - var materials = []; - var textures = {}; - this.textureLoader.setCrossOrigin( this.crossOrigin ); // materials - - for ( var i = 0; i < data.metadata.materialCount; i ++ ) { - - var material = data.materials[ i ]; - var params = { - userData: {} - }; - if ( material.name !== undefined ) params.name = material.name; - /* + + setResourcePath( resourcePath ) { + + this.resourcePath = resourcePath; + return this; + + } + /** + * @param {Object} data - parsed PMD/PMX data + * @param {BufferGeometry} geometry - some properties are dependend on geometry + * @param {function} onProgress + * @param {function} onError + * @return {Array} + */ + + + build( data, geometry + /*, onProgress, onError */ + ) { + + const materials = []; + const textures = {}; + this.textureLoader.setCrossOrigin( this.crossOrigin ); // materials + + for ( let i = 0; i < data.metadata.materialCount; i ++ ) { + + const material = data.materials[ i ]; + const params = { + userData: {} + }; + if ( material.name !== undefined ) params.name = material.name; + /* * THREE.Color * * MMD THREE.MeshToonMaterial @@ -939,343 +951,348 @@ * It'll be too bright if material has map texture so using coef 0.2. */ - params.color = new THREE.Color().fromArray( material.diffuse ); - params.opacity = material.diffuse[ 3 ]; - params.emissive = new THREE.Color().fromArray( material.ambient ); - params.transparent = params.opacity !== 1.0; // - - params.skinning = geometry.bones.length > 0 ? true : false; - params.morphTargets = geometry.morphTargets.length > 0 ? true : false; - params.fog = true; // blend + params.color = new THREE.Color().fromArray( material.diffuse ); + params.opacity = material.diffuse[ 3 ]; + params.emissive = new THREE.Color().fromArray( material.ambient ); + params.transparent = params.opacity !== 1.0; // - params.blending = THREE.CustomBlending; - params.blendSrc = THREE.SrcAlphaFactor; - params.blendDst = THREE.OneMinusSrcAlphaFactor; - params.blendSrcAlpha = THREE.SrcAlphaFactor; - params.blendDstAlpha = THREE.DstAlphaFactor; // side + params.skinning = geometry.bones.length > 0 ? true : false; + params.morphTargets = geometry.morphTargets.length > 0 ? true : false; + params.fog = true; // blend - if ( data.metadata.format === 'pmx' && ( material.flag & 0x1 ) === 1 ) { + params.blending = THREE.CustomBlending; + params.blendSrc = THREE.SrcAlphaFactor; + params.blendDst = THREE.OneMinusSrcAlphaFactor; + params.blendSrcAlpha = THREE.SrcAlphaFactor; + params.blendDstAlpha = THREE.DstAlphaFactor; // side - params.side = THREE.DoubleSide; + if ( data.metadata.format === 'pmx' && ( material.flag & 0x1 ) === 1 ) { - } else { - - params.side = params.opacity === 1.0 ? THREE.FrontSide : THREE.DoubleSide; + params.side = THREE.DoubleSide; - } + } else { - if ( data.metadata.format === 'pmd' ) { + params.side = params.opacity === 1.0 ? THREE.FrontSide : THREE.DoubleSide; - // map, envMap - if ( material.fileName ) { + } - var fileName = material.fileName; - var fileNames = fileName.split( '*' ); // fileNames[ 0 ]: mapFileName - // fileNames[ 1 ]: envMapFileName( optional ) + if ( data.metadata.format === 'pmd' ) { - params.map = this._loadTexture( fileNames[ 0 ], textures ); + // map, envMap + if ( material.fileName ) { - if ( fileNames.length > 1 ) { + const fileName = material.fileName; + const fileNames = fileName.split( '*' ); // fileNames[ 0 ]: mapFileName + // fileNames[ 1 ]: envMapFileName( optional ) - var extension = fileNames[ 1 ].slice( - 4 ).toLowerCase(); - params.envMap = this._loadTexture( fileNames[ 1 ], textures ); - params.combine = extension === '.sph' ? THREE.MultiplyOperation : THREE.AddOperation; + params.map = this._loadTexture( fileNames[ 0 ], textures ); - } + if ( fileNames.length > 1 ) { - } // gradientMap + const extension = fileNames[ 1 ].slice( - 4 ).toLowerCase(); + params.envMap = this._loadTexture( fileNames[ 1 ], textures ); + params.combine = extension === '.sph' ? THREE.MultiplyOperation : THREE.AddOperation; + } - var toonFileName = material.toonIndex === - 1 ? 'toon00.bmp' : data.toonTextures[ material.toonIndex ].fileName; - params.gradientMap = this._loadTexture( toonFileName, textures, { - isToonTexture: true, - isDefaultToonTexture: this._isDefaultToonTexture( toonFileName ) - } ); // parameters for OutlineEffect + } // gradientMap - params.userData.outlineParameters = { - thickness: material.edgeFlag === 1 ? 0.003 : 0.0, - color: [ 0, 0, 0 ], - alpha: 1.0, - visible: material.edgeFlag === 1 - }; - } else { + const toonFileName = material.toonIndex === - 1 ? 'toon00.bmp' : data.toonTextures[ material.toonIndex ].fileName; + params.gradientMap = this._loadTexture( toonFileName, textures, { + isToonTexture: true, + isDefaultToonTexture: this._isDefaultToonTexture( toonFileName ) + } ); // parameters for OutlineEffect - // map - if ( material.textureIndex !== - 1 ) { + params.userData.outlineParameters = { + thickness: material.edgeFlag === 1 ? 0.003 : 0.0, + color: [ 0, 0, 0 ], + alpha: 1.0, + visible: material.edgeFlag === 1 + }; - params.map = this._loadTexture( data.textures[ material.textureIndex ], textures ); + } else { - } // envMap TODO: support m.envFlag === 3 + // map + if ( material.textureIndex !== - 1 ) { + params.map = this._loadTexture( data.textures[ material.textureIndex ], textures ); - if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) { + } // envMap TODO: support m.envFlag === 3 - params.envMap = this._loadTexture( data.textures[ material.envTextureIndex ], textures ); - params.combine = material.envFlag === 1 ? THREE.MultiplyOperation : THREE.AddOperation; - } // gradientMap + if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) { + params.envMap = this._loadTexture( data.textures[ material.envTextureIndex ], textures ); + params.combine = material.envFlag === 1 ? THREE.MultiplyOperation : THREE.AddOperation; - var toonFileName, isDefaultToon; + } // gradientMap - if ( material.toonIndex === - 1 || material.toonFlag !== 0 ) { - toonFileName = 'toon' + ( '0' + ( material.toonIndex + 1 ) ).slice( - 2 ) + '.bmp'; - isDefaultToon = true; + let toonFileName, isDefaultToon; - } else { + if ( material.toonIndex === - 1 || material.toonFlag !== 0 ) { - toonFileName = data.textures[ material.toonIndex ]; - isDefaultToon = false; + toonFileName = 'toon' + ( '0' + ( material.toonIndex + 1 ) ).slice( - 2 ) + '.bmp'; + isDefaultToon = true; - } + } else { - params.gradientMap = this._loadTexture( toonFileName, textures, { - isToonTexture: true, - isDefaultToonTexture: isDefaultToon - } ); // parameters for OutlineEffect - - params.userData.outlineParameters = { - thickness: material.edgeSize / 300, - // TODO: better calculation? - color: material.edgeColor.slice( 0, 3 ), - alpha: material.edgeColor[ 3 ], - visible: ( material.flag & 0x10 ) !== 0 && material.edgeSize > 0.0 - }; + toonFileName = data.textures[ material.toonIndex ]; + isDefaultToon = false; } - if ( params.map !== undefined ) { + params.gradientMap = this._loadTexture( toonFileName, textures, { + isToonTexture: true, + isDefaultToonTexture: isDefaultToon + } ); // parameters for OutlineEffect + + params.userData.outlineParameters = { + thickness: material.edgeSize / 300, + // TODO: better calculation? + color: material.edgeColor.slice( 0, 3 ), + alpha: material.edgeColor[ 3 ], + visible: ( material.flag & 0x10 ) !== 0 && material.edgeSize > 0.0 + }; - if ( ! params.transparent ) { + } - this._checkImageTransparency( params.map, geometry, i ); + if ( params.map !== undefined ) { - } + if ( ! params.transparent ) { - params.emissive.multiplyScalar( 0.2 ); + this._checkImageTransparency( params.map, geometry, i ); } - materials.push( new THREE.MeshToonMaterial( params ) ); + params.emissive.multiplyScalar( 0.2 ); } - if ( data.metadata.format === 'pmx' ) { + materials.push( new THREE.MeshToonMaterial( params ) ); - // set transparent true if alpha morph is defined. - function checkAlphaMorph( elements, materials ) { + } - for ( var i = 0, il = elements.length; i < il; i ++ ) { + if ( data.metadata.format === 'pmx' ) { - var element = elements[ i ]; - if ( element.index === - 1 ) continue; - var material = materials[ element.index ]; + // set transparent true if alpha morph is defined. + function checkAlphaMorph( elements, materials ) { - if ( material.opacity !== element.diffuse[ 3 ] ) { + for ( let i = 0, il = elements.length; i < il; i ++ ) { - material.transparent = true; + const element = elements[ i ]; + if ( element.index === - 1 ) continue; + const material = materials[ element.index ]; - } + if ( material.opacity !== element.diffuse[ 3 ] ) { + + material.transparent = true; } } - for ( var i = 0, il = data.morphs.length; i < il; i ++ ) { + } - var morph = data.morphs[ i ]; - var elements = morph.elements; + for ( let i = 0, il = data.morphs.length; i < il; i ++ ) { - if ( morph.type === 0 ) { + const morph = data.morphs[ i ]; + const elements = morph.elements; - for ( var j = 0, jl = elements.length; j < jl; j ++ ) { + if ( morph.type === 0 ) { - var morph2 = data.morphs[ elements[ j ].index ]; - if ( morph2.type !== 8 ) continue; - checkAlphaMorph( morph2.elements, materials ); + for ( let j = 0, jl = elements.length; j < jl; j ++ ) { - } + const morph2 = data.morphs[ elements[ j ].index ]; + if ( morph2.type !== 8 ) continue; + checkAlphaMorph( morph2.elements, materials ); - } else if ( morph.type === 8 ) { + } - checkAlphaMorph( elements, materials ); + } else if ( morph.type === 8 ) { - } + checkAlphaMorph( elements, materials ); } } - return materials; + } - }, - // private methods - _getTGALoader: function () { + return materials; - if ( this.tgaLoader === null ) { + } // private methods - if ( THREE.TGALoader === undefined ) { - throw new Error( 'THREE.MMDLoader: Import THREE.TGALoader' ); + _getTGALoader() { - } + if ( this.tgaLoader === null ) { + + if ( THREE.TGALoader === undefined ) { - this.tgaLoader = new THREE.TGALoader( this.manager ); + throw new Error( 'THREE.MMDLoader: Import THREE.TGALoader' ); } - return this.tgaLoader; + this.tgaLoader = new THREE.TGALoader( this.manager ); - }, - _isDefaultToonTexture: function ( name ) { + } - if ( name.length !== 10 ) return false; - return /toon(10|0[0-9])\.bmp/.test( name ); + return this.tgaLoader; - }, - _loadTexture: function ( filePath, textures, params, onProgress, onError ) { + } - params = params || {}; - var scope = this; - var fullPath; + _isDefaultToonTexture( name ) { - if ( params.isDefaultToonTexture === true ) { + if ( name.length !== 10 ) return false; + return /toon(10|0[0-9])\.bmp/.test( name ); - var index; + } - try { + _loadTexture( filePath, textures, params, onProgress, onError ) { - index = parseInt( filePath.match( /toon([0-9]{2})\.bmp$/ )[ 1 ] ); + params = params || {}; + const scope = this; + let fullPath; - } catch ( e ) { + if ( params.isDefaultToonTexture === true ) { - console.warn( 'THREE.MMDLoader: ' + filePath + ' seems like a ' + 'not right default texture path. Using toon00.bmp instead.' ); - index = 0; + let index; - } + try { - fullPath = DEFAULT_TOON_TEXTURES[ index ]; + index = parseInt( filePath.match( /toon([0-9]{2})\.bmp$/ )[ 1 ] ); - } else { + } catch ( e ) { - fullPath = this.resourcePath + filePath; + console.warn( 'THREE.MMDLoader: ' + filePath + ' seems like a ' + 'not right default texture path. Using toon00.bmp instead.' ); + index = 0; } - if ( textures[ fullPath ] !== undefined ) return textures[ fullPath ]; - var loader = this.manager.getHandler( fullPath ); + fullPath = DEFAULT_TOON_TEXTURES[ index ]; + + } else { + + fullPath = this.resourcePath + filePath; + + } + + if ( textures[ fullPath ] !== undefined ) return textures[ fullPath ]; + let loader = this.manager.getHandler( fullPath ); + + if ( loader === null ) { + + loader = filePath.slice( - 4 ).toLowerCase() === '.tga' ? this._getTGALoader() : this.textureLoader; + + } - if ( loader === null ) { + const texture = loader.load( fullPath, function ( t ) { - loader = filePath.slice( - 4 ).toLowerCase() === '.tga' ? this._getTGALoader() : this.textureLoader; + // MMD toon texture is Axis-Y oriented + // but Three.js gradient map is Axis-X oriented. + // So here replaces the toon texture image with the rotated one. + if ( params.isToonTexture === true ) { + + t.image = scope._getRotatedImage( t.image ); + t.magFilter = THREE.NearestFilter; + t.minFilter = THREE.NearestFilter; } - var texture = loader.load( fullPath, function ( t ) { + t.flipY = false; + t.wrapS = THREE.RepeatWrapping; + t.wrapT = THREE.RepeatWrapping; - // MMD toon texture is Axis-Y oriented - // but Three.js gradient map is Axis-X oriented. - // So here replaces the toon texture image with the rotated one. - if ( params.isToonTexture === true ) { + for ( let i = 0; i < texture.readyCallbacks.length; i ++ ) { - t.image = scope._getRotatedImage( t.image ); - t.magFilter = THREE.NearestFilter; - t.minFilter = THREE.NearestFilter; + texture.readyCallbacks[ i ]( texture ); - } + } - t.flipY = false; - t.wrapS = THREE.RepeatWrapping; - t.wrapT = THREE.RepeatWrapping; + delete texture.readyCallbacks; - for ( var i = 0; i < texture.readyCallbacks.length; i ++ ) { + }, onProgress, onError ); + texture.readyCallbacks = []; + textures[ fullPath ] = texture; + return texture; - texture.readyCallbacks[ i ]( texture ); + } - } + _getRotatedImage( image ) { - delete texture.readyCallbacks; + const canvas = document.createElement( 'canvas' ); + const context = canvas.getContext( '2d' ); + const width = image.width; + const height = image.height; + canvas.width = width; + canvas.height = height; + context.clearRect( 0, 0, width, height ); + context.translate( width / 2.0, height / 2.0 ); + context.rotate( 0.5 * Math.PI ); // 90.0 * Math.PI / 180.0 - }, onProgress, onError ); - texture.readyCallbacks = []; - textures[ fullPath ] = texture; - return texture; - - }, - _getRotatedImage: function ( image ) { - - var canvas = document.createElement( 'canvas' ); - var context = canvas.getContext( '2d' ); - var width = image.width; - var height = image.height; - canvas.width = width; - canvas.height = height; - context.clearRect( 0, 0, width, height ); - context.translate( width / 2.0, height / 2.0 ); - context.rotate( 0.5 * Math.PI ); // 90.0 * Math.PI / 180.0 - - context.translate( - width / 2.0, - height / 2.0 ); - context.drawImage( image, 0, 0 ); - return context.getImageData( 0, 0, width, height ); - - }, - // Check if the partial image area used by the texture is transparent. - _checkImageTransparency: function ( map, geometry, groupIndex ) { - - map.readyCallbacks.push( function ( texture ) { - - // Is there any efficient ways? - function createImageData( image ) { - - var canvas = document.createElement( 'canvas' ); - canvas.width = image.width; - canvas.height = image.height; - var context = canvas.getContext( '2d' ); - context.drawImage( image, 0, 0 ); - return context.getImageData( 0, 0, canvas.width, canvas.height ); + context.translate( - width / 2.0, - height / 2.0 ); + context.drawImage( image, 0, 0 ); + return context.getImageData( 0, 0, width, height ); - } + } // Check if the partial image area used by the texture is transparent. - function detectImageTransparency( image, uvs, indices ) { - var width = image.width; - var height = image.height; - var data = image.data; - var threshold = 253; - if ( data.length / ( width * height ) !== 4 ) return false; + _checkImageTransparency( map, geometry, groupIndex ) { - for ( var i = 0; i < indices.length; i += 3 ) { + map.readyCallbacks.push( function ( texture ) { - var centerUV = { - x: 0.0, - y: 0.0 - }; + // Is there any efficient ways? + function createImageData( image ) { - for ( var j = 0; j < 3; j ++ ) { + const canvas = document.createElement( 'canvas' ); + canvas.width = image.width; + canvas.height = image.height; + const context = canvas.getContext( '2d' ); + context.drawImage( image, 0, 0 ); + return context.getImageData( 0, 0, canvas.width, canvas.height ); - var index = indices[ i * 3 + j ]; - var uv = { - x: uvs[ index * 2 + 0 ], - y: uvs[ index * 2 + 1 ] - }; - if ( getAlphaByUv( image, uv ) < threshold ) return true; - centerUV.x += uv.x; - centerUV.y += uv.y; + } - } + function detectImageTransparency( image, uvs, indices ) { + + const width = image.width; + const height = image.height; + const data = image.data; + const threshold = 253; + if ( data.length / ( width * height ) !== 4 ) return false; - centerUV.x /= 3; - centerUV.y /= 3; - if ( getAlphaByUv( image, centerUV ) < threshold ) return true; + for ( let i = 0; i < indices.length; i += 3 ) { + + const centerUV = { + x: 0.0, + y: 0.0 + }; + + for ( let j = 0; j < 3; j ++ ) { + + const index = indices[ i * 3 + j ]; + const uv = { + x: uvs[ index * 2 + 0 ], + y: uvs[ index * 2 + 1 ] + }; + if ( getAlphaByUv( image, uv ) < threshold ) return true; + centerUV.x += uv.x; + centerUV.y += uv.y; } - return false; + centerUV.x /= 3; + centerUV.y /= 3; + if ( getAlphaByUv( image, centerUV ) < threshold ) return true; } - /* + + return false; + + } + /* * This method expects * texture.flipY = false * texture.wrapS = THREE.RepeatWrapping @@ -1284,432 +1301,437 @@ */ - function getAlphaByUv( image, uv ) { + function getAlphaByUv( image, uv ) { - var width = image.width; - var height = image.height; - var x = Math.round( uv.x * width ) % width; - var y = Math.round( uv.y * height ) % height; - if ( x < 0 ) x += width; - if ( y < 0 ) y += height; - var index = y * width + x; - return image.data[ index * 4 + 3 ]; + const width = image.width; + const height = image.height; + let x = Math.round( uv.x * width ) % width; + let y = Math.round( uv.y * height ) % height; + if ( x < 0 ) x += width; + if ( y < 0 ) y += height; + const index = y * width + x; + return image.data[ index * 4 + 3 ]; - } - - var imageData = texture.image.data !== undefined ? texture.image : createImageData( texture.image ); - var group = geometry.groups[ groupIndex ]; - - if ( detectImageTransparency( imageData, geometry.attributes.uv.array, geometry.index.array.slice( group.start, group.start + group.count ) ) ) { + } - map.transparent = true; + const imageData = texture.image.data !== undefined ? texture.image : createImageData( texture.image ); + const group = geometry.groups[ groupIndex ]; - } + if ( detectImageTransparency( imageData, geometry.attributes.uv.array, geometry.index.array.slice( group.start, group.start + group.count ) ) ) { - } ); + map.transparent = true; - } - }; // + } - function AnimationBuilder() {} + } ); - AnimationBuilder.prototype = { - constructor: AnimationBuilder, + } - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - build: function ( vmd, mesh ) { + } // - // combine skeletal and morph animations - var tracks = this.buildSkeletalAnimation( vmd, mesh ).tracks; - var tracks2 = this.buildMorphAnimation( vmd, mesh ).tracks; - for ( var i = 0, il = tracks2.length; i < il; i ++ ) { + class AnimationBuilder { - tracks.push( tracks2[ i ] ); + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ + build( vmd, mesh ) { - } + // combine skeletal and morph animations + const tracks = this.buildSkeletalAnimation( vmd, mesh ).tracks; + const tracks2 = this.buildMorphAnimation( vmd, mesh ).tracks; - return new THREE.AnimationClip( '', - 1, tracks ); + for ( let i = 0, il = tracks2.length; i < il; i ++ ) { - }, + tracks.push( tracks2[ i ] ); - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - buildSkeletalAnimation: function ( vmd, mesh ) { + } - function pushInterpolation( array, interpolation, index ) { + return new THREE.AnimationClip( '', - 1, tracks ); - array.push( interpolation[ index + 0 ] / 127 ); // x1 + } + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ - array.push( interpolation[ index + 8 ] / 127 ); // x2 - array.push( interpolation[ index + 4 ] / 127 ); // y1 + buildSkeletalAnimation( vmd, mesh ) { - array.push( interpolation[ index + 12 ] / 127 ); // y2 + function pushInterpolation( array, interpolation, index ) { - } + array.push( interpolation[ index + 0 ] / 127 ); // x1 - var tracks = []; - var motions = {}; - var bones = mesh.skeleton.bones; - var boneNameDictionary = {}; + array.push( interpolation[ index + 8 ] / 127 ); // x2 - for ( var i = 0, il = bones.length; i < il; i ++ ) { + array.push( interpolation[ index + 4 ] / 127 ); // y1 - boneNameDictionary[ bones[ i ].name ] = true; + array.push( interpolation[ index + 12 ] / 127 ); // y2 - } + } - for ( var i = 0; i < vmd.metadata.motionCount; i ++ ) { + const tracks = []; + const motions = {}; + const bones = mesh.skeleton.bones; + const boneNameDictionary = {}; - var motion = vmd.motions[ i ]; - var boneName = motion.boneName; - if ( boneNameDictionary[ boneName ] === undefined ) continue; - motions[ boneName ] = motions[ boneName ] || []; - motions[ boneName ].push( motion ); + for ( let i = 0, il = bones.length; i < il; i ++ ) { - } + boneNameDictionary[ bones[ i ].name ] = true; - for ( var key in motions ) { + } - var array = motions[ key ]; - array.sort( function ( a, b ) { + for ( let i = 0; i < vmd.metadata.motionCount; i ++ ) { - return a.frameNum - b.frameNum; + const motion = vmd.motions[ i ]; + const boneName = motion.boneName; + if ( boneNameDictionary[ boneName ] === undefined ) continue; + motions[ boneName ] = motions[ boneName ] || []; + motions[ boneName ].push( motion ); - } ); - var times = []; - var positions = []; - var rotations = []; - var pInterpolations = []; - var rInterpolations = []; - var basePosition = mesh.skeleton.getBoneByName( key ).position.toArray(); + } - for ( var i = 0, il = array.length; i < il; i ++ ) { + for ( const key in motions ) { - var time = array[ i ].frameNum / 30; - var position = array[ i ].position; - var rotation = array[ i ].rotation; - var interpolation = array[ i ].interpolation; - times.push( time ); + const array = motions[ key ]; + array.sort( function ( a, b ) { - for ( var j = 0; j < 3; j ++ ) positions.push( basePosition[ j ] + position[ j ] ); + return a.frameNum - b.frameNum; - for ( var j = 0; j < 4; j ++ ) rotations.push( rotation[ j ] ); + } ); + const times = []; + const positions = []; + const rotations = []; + const pInterpolations = []; + const rInterpolations = []; + const basePosition = mesh.skeleton.getBoneByName( key ).position.toArray(); + + for ( let i = 0, il = array.length; i < il; i ++ ) { + + const time = array[ i ].frameNum / 30; + const position = array[ i ].position; + const rotation = array[ i ].rotation; + const interpolation = array[ i ].interpolation; + times.push( time ); - for ( var j = 0; j < 3; j ++ ) pushInterpolation( pInterpolations, interpolation, j ); + for ( let j = 0; j < 3; j ++ ) positions.push( basePosition[ j ] + position[ j ] ); - pushInterpolation( rInterpolations, interpolation, 3 ); + for ( let j = 0; j < 4; j ++ ) rotations.push( rotation[ j ] ); - } + for ( let j = 0; j < 3; j ++ ) pushInterpolation( pInterpolations, interpolation, j ); - var targetName = '.bones[' + key + ']'; - tracks.push( this._createTrack( targetName + '.position', THREE.VectorKeyframeTrack, times, positions, pInterpolations ) ); - tracks.push( this._createTrack( targetName + '.quaternion', THREE.QuaternionKeyframeTrack, times, rotations, rInterpolations ) ); + pushInterpolation( rInterpolations, interpolation, 3 ); } - return new THREE.AnimationClip( '', - 1, tracks ); + const targetName = '.bones[' + key + ']'; + tracks.push( this._createTrack( targetName + '.position', THREE.VectorKeyframeTrack, times, positions, pInterpolations ) ); + tracks.push( this._createTrack( targetName + '.quaternion', THREE.QuaternionKeyframeTrack, times, rotations, rInterpolations ) ); - }, + } - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - buildMorphAnimation: function ( vmd, mesh ) { + return new THREE.AnimationClip( '', - 1, tracks ); - var tracks = []; - var morphs = {}; - var morphTargetDictionary = mesh.morphTargetDictionary; + } + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ - for ( var i = 0; i < vmd.metadata.morphCount; i ++ ) { - var morph = vmd.morphs[ i ]; - var morphName = morph.morphName; - if ( morphTargetDictionary[ morphName ] === undefined ) continue; - morphs[ morphName ] = morphs[ morphName ] || []; - morphs[ morphName ].push( morph ); + buildMorphAnimation( vmd, mesh ) { - } + const tracks = []; + const morphs = {}; + const morphTargetDictionary = mesh.morphTargetDictionary; - for ( var key in morphs ) { + for ( let i = 0; i < vmd.metadata.morphCount; i ++ ) { - var array = morphs[ key ]; - array.sort( function ( a, b ) { + const morph = vmd.morphs[ i ]; + const morphName = morph.morphName; + if ( morphTargetDictionary[ morphName ] === undefined ) continue; + morphs[ morphName ] = morphs[ morphName ] || []; + morphs[ morphName ].push( morph ); - return a.frameNum - b.frameNum; + } - } ); - var times = []; - var values = []; + for ( const key in morphs ) { - for ( var i = 0, il = array.length; i < il; i ++ ) { + const array = morphs[ key ]; + array.sort( function ( a, b ) { - times.push( array[ i ].frameNum / 30 ); - values.push( array[ i ].weight ); + return a.frameNum - b.frameNum; - } + } ); + const times = []; + const values = []; - tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetDictionary[ key ] + ']', times, values ) ); + for ( let i = 0, il = array.length; i < il; i ++ ) { + + times.push( array[ i ].frameNum / 30 ); + values.push( array[ i ].weight ); } - return new THREE.AnimationClip( '', - 1, tracks ); + tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetDictionary[ key ] + ']', times, values ) ); - }, + } - /** - * @param {Object} vmd - parsed VMD data - * @return {AnimationClip} - */ - buildCameraAnimation: function ( vmd ) { + return new THREE.AnimationClip( '', - 1, tracks ); - function pushVector3( array, vec ) { + } + /** + * @param {Object} vmd - parsed VMD data + * @return {AnimationClip} + */ - array.push( vec.x ); - array.push( vec.y ); - array.push( vec.z ); - } + buildCameraAnimation( vmd ) { - function pushQuaternion( array, q ) { + function pushVector3( array, vec ) { - array.push( q.x ); - array.push( q.y ); - array.push( q.z ); - array.push( q.w ); + array.push( vec.x ); + array.push( vec.y ); + array.push( vec.z ); - } + } - function pushInterpolation( array, interpolation, index ) { + function pushQuaternion( array, q ) { - array.push( interpolation[ index * 4 + 0 ] / 127 ); // x1 + array.push( q.x ); + array.push( q.y ); + array.push( q.z ); + array.push( q.w ); - array.push( interpolation[ index * 4 + 1 ] / 127 ); // x2 + } - array.push( interpolation[ index * 4 + 2 ] / 127 ); // y1 + function pushInterpolation( array, interpolation, index ) { - array.push( interpolation[ index * 4 + 3 ] / 127 ); // y2 + array.push( interpolation[ index * 4 + 0 ] / 127 ); // x1 - } + array.push( interpolation[ index * 4 + 1 ] / 127 ); // x2 - var tracks = []; - var cameras = vmd.cameras === undefined ? [] : vmd.cameras.slice(); - cameras.sort( function ( a, b ) { + array.push( interpolation[ index * 4 + 2 ] / 127 ); // y1 - return a.frameNum - b.frameNum; + array.push( interpolation[ index * 4 + 3 ] / 127 ); // y2 - } ); - var times = []; - var centers = []; - var quaternions = []; - var positions = []; - var fovs = []; - var cInterpolations = []; - var qInterpolations = []; - var pInterpolations = []; - var fInterpolations = []; - var quaternion = new THREE.Quaternion(); - var euler = new THREE.Euler(); - var position = new THREE.Vector3(); - var center = new THREE.Vector3(); - - for ( var i = 0, il = cameras.length; i < il; i ++ ) { - - var motion = cameras[ i ]; - var time = motion.frameNum / 30; - var pos = motion.position; - var rot = motion.rotation; - var distance = motion.distance; - var fov = motion.fov; - var interpolation = motion.interpolation; - times.push( time ); - position.set( 0, 0, - distance ); - center.set( pos[ 0 ], pos[ 1 ], pos[ 2 ] ); - euler.set( - rot[ 0 ], - rot[ 1 ], - rot[ 2 ] ); - quaternion.setFromEuler( euler ); - position.add( center ); - position.applyQuaternion( quaternion ); - pushVector3( centers, center ); - pushQuaternion( quaternions, quaternion ); - pushVector3( positions, position ); - fovs.push( fov ); + } - for ( var j = 0; j < 3; j ++ ) { + const cameras = vmd.cameras === undefined ? [] : vmd.cameras.slice(); + cameras.sort( function ( a, b ) { + + return a.frameNum - b.frameNum; + + } ); + const times = []; + const centers = []; + const quaternions = []; + const positions = []; + const fovs = []; + const cInterpolations = []; + const qInterpolations = []; + const pInterpolations = []; + const fInterpolations = []; + const quaternion = new THREE.Quaternion(); + const euler = new THREE.Euler(); + const position = new THREE.Vector3(); + const center = new THREE.Vector3(); + + for ( let i = 0, il = cameras.length; i < il; i ++ ) { + + const motion = cameras[ i ]; + const time = motion.frameNum / 30; + const pos = motion.position; + const rot = motion.rotation; + const distance = motion.distance; + const fov = motion.fov; + const interpolation = motion.interpolation; + times.push( time ); + position.set( 0, 0, - distance ); + center.set( pos[ 0 ], pos[ 1 ], pos[ 2 ] ); + euler.set( - rot[ 0 ], - rot[ 1 ], - rot[ 2 ] ); + quaternion.setFromEuler( euler ); + position.add( center ); + position.applyQuaternion( quaternion ); + pushVector3( centers, center ); + pushQuaternion( quaternions, quaternion ); + pushVector3( positions, position ); + fovs.push( fov ); + + for ( let j = 0; j < 3; j ++ ) { + + pushInterpolation( cInterpolations, interpolation, j ); - pushInterpolation( cInterpolations, interpolation, j ); + } - } + pushInterpolation( qInterpolations, interpolation, 3 ); // use the same parameter for x, y, z axis. - pushInterpolation( qInterpolations, interpolation, 3 ); // use the same parameter for x, y, z axis. + for ( let j = 0; j < 3; j ++ ) { - for ( var j = 0; j < 3; j ++ ) { + pushInterpolation( pInterpolations, interpolation, 4 ); - pushInterpolation( pInterpolations, interpolation, 4 ); + } - } + pushInterpolation( fInterpolations, interpolation, 5 ); - pushInterpolation( fInterpolations, interpolation, 5 ); + } - } + const tracks = []; // I expect an object whose name 'target' exists under THREE.Camera - var tracks = []; // I expect an object whose name 'target' exists under THREE.Camera + tracks.push( this._createTrack( 'target.position', THREE.VectorKeyframeTrack, times, centers, cInterpolations ) ); + tracks.push( this._createTrack( '.quaternion', THREE.QuaternionKeyframeTrack, times, quaternions, qInterpolations ) ); + tracks.push( this._createTrack( '.position', THREE.VectorKeyframeTrack, times, positions, pInterpolations ) ); + tracks.push( this._createTrack( '.fov', THREE.NumberKeyframeTrack, times, fovs, fInterpolations ) ); + return new THREE.AnimationClip( '', - 1, tracks ); - tracks.push( this._createTrack( 'target.position', THREE.VectorKeyframeTrack, times, centers, cInterpolations ) ); - tracks.push( this._createTrack( '.quaternion', THREE.QuaternionKeyframeTrack, times, quaternions, qInterpolations ) ); - tracks.push( this._createTrack( '.position', THREE.VectorKeyframeTrack, times, positions, pInterpolations ) ); - tracks.push( this._createTrack( '.fov', THREE.NumberKeyframeTrack, times, fovs, fInterpolations ) ); - return new THREE.AnimationClip( '', - 1, tracks ); + } // private method - }, - // private method - _createTrack: function ( node, typedKeyframeTrack, times, values, interpolations ) { - /* + _createTrack( node, typedKeyframeTrack, times, values, interpolations ) { + + /* * optimizes here not to let KeyframeTrackPrototype optimize * because KeyframeTrackPrototype optimizes times and values but * doesn't optimize interpolations. */ - if ( times.length > 2 ) { - - times = times.slice(); - values = values.slice(); - interpolations = interpolations.slice(); - var stride = values.length / times.length; - var interpolateStride = interpolations.length / times.length; - var index = 1; + if ( times.length > 2 ) { - for ( var aheadIndex = 2, endIndex = times.length; aheadIndex < endIndex; aheadIndex ++ ) { + times = times.slice(); + values = values.slice(); + interpolations = interpolations.slice(); + const stride = values.length / times.length; + const interpolateStride = interpolations.length / times.length; + let index = 1; - for ( var i = 0; i < stride; i ++ ) { + for ( let aheadIndex = 2, endIndex = times.length; aheadIndex < endIndex; aheadIndex ++ ) { - if ( values[ index * stride + i ] !== values[ ( index - 1 ) * stride + i ] || values[ index * stride + i ] !== values[ aheadIndex * stride + i ] ) { + for ( let i = 0; i < stride; i ++ ) { - index ++; - break; + if ( values[ index * stride + i ] !== values[ ( index - 1 ) * stride + i ] || values[ index * stride + i ] !== values[ aheadIndex * stride + i ] ) { - } + index ++; + break; } - if ( aheadIndex > index ) { + } - times[ index ] = times[ aheadIndex ]; + if ( aheadIndex > index ) { - for ( var i = 0; i < stride; i ++ ) { + times[ index ] = times[ aheadIndex ]; - values[ index * stride + i ] = values[ aheadIndex * stride + i ]; + for ( let i = 0; i < stride; i ++ ) { - } + values[ index * stride + i ] = values[ aheadIndex * stride + i ]; - for ( var i = 0; i < interpolateStride; i ++ ) { + } - interpolations[ index * interpolateStride + i ] = interpolations[ aheadIndex * interpolateStride + i ]; + for ( let i = 0; i < interpolateStride; i ++ ) { - } + interpolations[ index * interpolateStride + i ] = interpolations[ aheadIndex * interpolateStride + i ]; } } - times.length = index + 1; - values.length = ( index + 1 ) * stride; - interpolations.length = ( index + 1 ) * interpolateStride; - } - var track = new typedKeyframeTrack( node, times, values ); + times.length = index + 1; + values.length = ( index + 1 ) * stride; + interpolations.length = ( index + 1 ) * interpolateStride; - track.createInterpolant = function InterpolantFactoryMethodCubicBezier( result ) { + } - return new CubicBezierInterpolation( this.times, this.values, this.getValueSize(), result, new Float32Array( interpolations ) ); + const track = new typedKeyframeTrack( node, times, values ); - }; + track.createInterpolant = function InterpolantFactoryMethodCubicBezier( result ) { - return track; + return new CubicBezierInterpolation( this.times, this.values, this.getValueSize(), result, new Float32Array( interpolations ) ); - } - }; // interpolation + }; + + return track; + + } - function CubicBezierInterpolation( parameterPositions, sampleValues, sampleSize, resultBuffer, params ) { + } // interpolation - THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); + + class CubicBezierInterpolation extends THREE.Interpolant { + + constructor( parameterPositions, sampleValues, sampleSize, resultBuffer, params ) { + + super( parameterPositions, sampleValues, sampleSize, resultBuffer ); this.interpolationParams = params; } - CubicBezierInterpolation.prototype = Object.assign( Object.create( THREE.Interpolant.prototype ), { - constructor: CubicBezierInterpolation, - interpolate_: function ( i1, t0, t, t1 ) { + interpolate_( i1, t0, t, t1 ) { - var result = this.resultBuffer; - var values = this.sampleValues; - var stride = this.valueSize; - var params = this.interpolationParams; - var offset1 = i1 * stride; - var offset0 = offset1 - stride; // No interpolation if next key frame is in one frame in 30fps. - // This is from MMD animation spec. - // '1.5' is for precision loss. times are Float32 in Three.js Animation system. + const result = this.resultBuffer; + const values = this.sampleValues; + const stride = this.valueSize; + const params = this.interpolationParams; + const offset1 = i1 * stride; + const offset0 = offset1 - stride; // No interpolation if next key frame is in one frame in 30fps. + // This is from MMD animation spec. + // '1.5' is for precision loss. times are Float32 in Three.js Animation system. - var weight1 = t1 - t0 < 1 / 30 * 1.5 ? 0.0 : ( t - t0 ) / ( t1 - t0 ); + const weight1 = t1 - t0 < 1 / 30 * 1.5 ? 0.0 : ( t - t0 ) / ( t1 - t0 ); - if ( stride === 4 ) { + if ( stride === 4 ) { - // THREE.Quaternion - var x1 = params[ i1 * 4 + 0 ]; - var x2 = params[ i1 * 4 + 1 ]; - var y1 = params[ i1 * 4 + 2 ]; - var y2 = params[ i1 * 4 + 3 ]; + // THREE.Quaternion + const x1 = params[ i1 * 4 + 0 ]; + const x2 = params[ i1 * 4 + 1 ]; + const y1 = params[ i1 * 4 + 2 ]; + const y2 = params[ i1 * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - THREE.Quaternion.slerpFlat( result, 0, values, offset0, values, offset1, ratio ); + THREE.Quaternion.slerpFlat( result, 0, values, offset0, values, offset1, ratio ); - } else if ( stride === 3 ) { + } else if ( stride === 3 ) { - // THREE.Vector3 - for ( var i = 0; i !== stride; ++ i ) { + // THREE.Vector3 + for ( let i = 0; i !== stride; ++ i ) { - var x1 = params[ i1 * 12 + i * 4 + 0 ]; - var x2 = params[ i1 * 12 + i * 4 + 1 ]; - var y1 = params[ i1 * 12 + i * 4 + 2 ]; - var y2 = params[ i1 * 12 + i * 4 + 3 ]; + const x1 = params[ i1 * 12 + i * 4 + 0 ]; + const x2 = params[ i1 * 12 + i * 4 + 1 ]; + const y1 = params[ i1 * 12 + i * 4 + 2 ]; + const y2 = params[ i1 * 12 + i * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - result[ i ] = values[ offset0 + i ] * ( 1 - ratio ) + values[ offset1 + i ] * ratio; + result[ i ] = values[ offset0 + i ] * ( 1 - ratio ) + values[ offset1 + i ] * ratio; - } + } - } else { + } else { - // Number - var x1 = params[ i1 * 4 + 0 ]; - var x2 = params[ i1 * 4 + 1 ]; - var y1 = params[ i1 * 4 + 2 ]; - var y2 = params[ i1 * 4 + 3 ]; + // Number + const x1 = params[ i1 * 4 + 0 ]; + const x2 = params[ i1 * 4 + 1 ]; + const y1 = params[ i1 * 4 + 2 ]; + const y2 = params[ i1 * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - result[ 0 ] = values[ offset0 ] * ( 1 - ratio ) + values[ offset1 ] * ratio; + result[ 0 ] = values[ offset0 ] * ( 1 - ratio ) + values[ offset1 ] * ratio; - } + } - return result; + return result; - }, - _calculate: function ( x1, x2, y1, y2, x ) { + } - /* + _calculate( x1, x2, y1, y2, x ) { + + /* * Cubic Bezier curves * https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves * @@ -1745,34 +1767,32 @@ * (Another option: Newton's method * https://en.wikipedia.org/wiki/Newton%27s_method) */ - var c = 0.5; - var t = c; - var s = 1.0 - t; - var loop = 15; - var eps = 1e-5; - var math = Math; - var sst3, stt3, ttt; - - for ( var i = 0; i < loop; i ++ ) { - - sst3 = 3.0 * s * s * t; - stt3 = 3.0 * s * t * t; - ttt = t * t * t; - var ft = sst3 * x1 + stt3 * x2 + ttt - x; - if ( math.abs( ft ) < eps ) break; - c /= 2.0; - t += ft < 0 ? c : - c; - s = 1.0 - t; + let c = 0.5; + let t = c; + let s = 1.0 - t; + const loop = 15; + const eps = 1e-5; + const math = Math; + let sst3, stt3, ttt; + + for ( let i = 0; i < loop; i ++ ) { + + sst3 = 3.0 * s * s * t; + stt3 = 3.0 * s * t * t; + ttt = t * t * t; + const ft = sst3 * x1 + stt3 * x2 + ttt - x; + if ( math.abs( ft ) < eps ) break; + c /= 2.0; + t += ft < 0 ? c : - c; + s = 1.0 - t; - } + } - return sst3 * y1 + stt3 * y2 + ttt; + return sst3 * y1 + stt3 * y2 + ttt; - } - } ); - return MMDLoader; + } - }(); + } THREE.MMDLoader = MMDLoader; diff --git a/examples/js/loaders/NRRDLoader.js b/examples/js/loaders/NRRDLoader.js index 964a62eef8694f..237944cb08fd31 100644 --- a/examples/js/loaders/NRRDLoader.js +++ b/examples/js/loaders/NRRDLoader.js @@ -1,17 +1,17 @@ ( function () { - var NRRDLoader = function ( manager ) { + class NRRDLoader extends THREE.Loader { - THREE.Loader.call( this, manager ); + constructor( manager ) { - }; + super( manager ); + + } - NRRDLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: NRRDLoader, - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; - var loader = new THREE.FileLoader( scope.manager ); + const scope = this; + const loader = new THREE.FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( scope.requestHeader ); @@ -40,17 +40,18 @@ }, onProgress, onError ); - }, - parse: function ( data ) { + } + + parse( data ) { // this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X - var _data = data; - var _dataPointer = 0; + let _data = data; + let _dataPointer = 0; - var _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0; + const _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0; - var _littleEndian = true; - var headerObject = {}; + const _littleEndian = true; + const headerObject = {}; function scan( type, chunks ) { @@ -60,8 +61,8 @@ } - var _chunkSize = 1; - var _array_type = Uint8Array; + let _chunkSize = 1; + let _array_type = Uint8Array; switch ( type ) { @@ -113,7 +114,7 @@ } // increase the data pointer in-place - var _bytes = new _array_type( _data.slice( _dataPointer, _dataPointer += chunks * _chunkSize ) ); // if required, flip the endianness of the bytes + let _bytes = new _array_type( _data.slice( _dataPointer, _dataPointer += chunks * _chunkSize ) ); // if required, flip the endianness of the bytes if ( _nativeLittleEndian != _littleEndian ) { @@ -138,13 +139,13 @@ function flipEndianness( array, chunkSize ) { - var u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength ); + const u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength ); - for ( var i = 0; i < array.byteLength; i += chunkSize ) { + for ( let i = 0; i < array.byteLength; i += chunkSize ) { - for ( var j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) { + for ( let j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) { - var tmp = u8[ k ]; + const tmp = u8[ k ]; u8[ k ] = u8[ j ]; u8[ j ] = tmp; @@ -159,9 +160,9 @@ function parseHeader( header ) { - var data, field, fn, i, l, lines, m, _i, _len; + let data, field, fn, i, l, m, _i, _len; - lines = header.split( /\r?\n/ ); + const lines = header.split( /\r?\n/ ); for ( _i = 0, _len = lines.length; _i < _len; _i ++ ) { @@ -175,7 +176,7 @@ field = m[ 1 ].trim(); data = m[ 2 ].trim(); - fn = NRRDLoader.prototype.fieldFunctions[ field ]; + fn = _fieldFunctions[ field ]; if ( fn ) { @@ -229,17 +230,17 @@ function parseDataAsText( data, start, end ) { - var number = ''; + let number = ''; start = start || 0; end = end || data.length; - var value; //length of the result is the product of the sizes + let value; //length of the result is the product of the sizes - var lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) { + const lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) { return previous * current; }, 1 ); - var base = 10; + let base = 10; if ( headerObject.encoding === 'hex' ) { @@ -247,9 +248,9 @@ } - var result = new headerObject.__array( lengthOfTheResult ); - var resultIndex = 0; - var parsingFunction = parseInt; + const result = new headerObject.__array( lengthOfTheResult ); + let resultIndex = 0; + let parsingFunction = parseInt; if ( headerObject.__array === Float32Array || headerObject.__array === Float64Array ) { @@ -257,7 +258,7 @@ } - for ( var i = start; i < end; i ++ ) { + for ( let i = start; i < end; i ++ ) { value = data[ i ]; //if value is not a space @@ -291,12 +292,12 @@ } - var _bytes = scan( 'uchar', data.byteLength ); + const _bytes = scan( 'uchar', data.byteLength ); - var _length = _bytes.length; - var _header = null; - var _data_start = 0; - var i; + const _length = _bytes.length; + let _header = null; + let _data_start = 0; + let i; for ( i = 1; i < _length; i ++ ) { @@ -315,9 +316,7 @@ parseHeader( _header ); - - var _data = _bytes.subarray( _data_start ); // the data without header - + _data = _bytes.subarray( _data_start ); // the data without header if ( headerObject.encoding.substring( 0, 2 ) === 'gz' ) { @@ -332,9 +331,9 @@ } else if ( headerObject.encoding === 'raw' ) { //we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header - var _copy = new Uint8Array( _data.length ); + const _copy = new Uint8Array( _data.length ); - for ( var i = 0; i < _data.length; i ++ ) { + for ( let i = 0; i < _data.length; i ++ ) { _copy[ i ] = _data[ i ]; @@ -346,16 +345,16 @@ _data = _data.buffer; - var volume = new THREE.Volume(); + const volume = new THREE.Volume(); volume.header = headerObject; // // parse the (unzipped) data to a datastream of the correct type // volume.data = new headerObject.__array( _data ); // get the min and max intensities - var min_max = volume.computeMinMax(); - var min = min_max[ 0 ]; - var max = min_max[ 1 ]; // attach the scalar range to the volume + const min_max = volume.computeMinMax(); + const min = min_max[ 0 ]; + const max = min_max[ 1 ]; // attach the scalar range to the volume volume.windowLow = min; volume.windowHigh = max; // get the image dimensions @@ -365,15 +364,15 @@ volume.yLength = volume.dimensions[ 1 ]; volume.zLength = volume.dimensions[ 2 ]; // spacing - var spacingX = new THREE.Vector3( headerObject.vectors[ 0 ][ 0 ], headerObject.vectors[ 0 ][ 1 ], headerObject.vectors[ 0 ][ 2 ] ).length(); - var spacingY = new THREE.Vector3( headerObject.vectors[ 1 ][ 0 ], headerObject.vectors[ 1 ][ 1 ], headerObject.vectors[ 1 ][ 2 ] ).length(); - var spacingZ = new THREE.Vector3( headerObject.vectors[ 2 ][ 0 ], headerObject.vectors[ 2 ][ 1 ], headerObject.vectors[ 2 ][ 2 ] ).length(); + const spacingX = new THREE.Vector3( headerObject.vectors[ 0 ][ 0 ], headerObject.vectors[ 0 ][ 1 ], headerObject.vectors[ 0 ][ 2 ] ).length(); + const spacingY = new THREE.Vector3( headerObject.vectors[ 1 ][ 0 ], headerObject.vectors[ 1 ][ 1 ], headerObject.vectors[ 1 ][ 2 ] ).length(); + const spacingZ = new THREE.Vector3( headerObject.vectors[ 2 ][ 0 ], headerObject.vectors[ 2 ][ 1 ], headerObject.vectors[ 2 ][ 2 ] ).length(); volume.spacing = [ spacingX, spacingY, spacingZ ]; // Create IJKtoRAS matrix volume.matrix = new THREE.Matrix4(); - var _spaceX = 1; - var _spaceY = 1; - var _spaceZ = 1; + let _spaceX = 1; + let _spaceY = 1; + const _spaceZ = 1; if ( headerObject.space == 'left-posterior-superior' ) { @@ -392,7 +391,7 @@ } else { - var v = headerObject.vectors; + const v = headerObject.vectors; volume.matrix.set( _spaceX * v[ 0 ][ 0 ], _spaceX * v[ 1 ][ 0 ], _spaceX * v[ 2 ][ 0 ], 0, _spaceY * v[ 0 ][ 1 ], _spaceY * v[ 1 ][ 1 ], _spaceY * v[ 2 ][ 1 ], 0, _spaceZ * v[ 0 ][ 2 ], _spaceZ * v[ 1 ][ 2 ], _spaceZ * v[ 2 ][ 2 ], 0, 0, 0, 0, 1 ); } @@ -416,8 +415,9 @@ return volume; - }, - parseChars: function ( array, start, end ) { + } + + parseChars( array, start, end ) { // without borders, use the whole array if ( start === undefined ) { @@ -432,9 +432,9 @@ } - var output = ''; // create and append the chars + let output = ''; // create and append the chars - var i = 0; + let i = 0; for ( i = start; i < end; ++ i ) { @@ -444,185 +444,181 @@ return output; - }, - fieldFunctions: { - type: function ( data ) { + } - switch ( data ) { + } - case 'uchar': - case 'unsigned char': - case 'uint8': - case 'uint8_t': - this.__array = Uint8Array; - break; + const _fieldFunctions = { + type: function ( data ) { - case 'signed char': - case 'int8': - case 'int8_t': - this.__array = Int8Array; - break; + switch ( data ) { - case 'short': - case 'short int': - case 'signed short': - case 'signed short int': - case 'int16': - case 'int16_t': - this.__array = Int16Array; - break; + case 'uchar': + case 'unsigned char': + case 'uint8': + case 'uint8_t': + this.__array = Uint8Array; + break; - case 'ushort': - case 'unsigned short': - case 'unsigned short int': - case 'uint16': - case 'uint16_t': - this.__array = Uint16Array; - break; + case 'signed char': + case 'int8': + case 'int8_t': + this.__array = Int8Array; + break; - case 'int': - case 'signed int': - case 'int32': - case 'int32_t': - this.__array = Int32Array; - break; + case 'short': + case 'short int': + case 'signed short': + case 'signed short int': + case 'int16': + case 'int16_t': + this.__array = Int16Array; + break; - case 'uint': - case 'unsigned int': - case 'uint32': - case 'uint32_t': - this.__array = Uint32Array; - break; + case 'ushort': + case 'unsigned short': + case 'unsigned short int': + case 'uint16': + case 'uint16_t': + this.__array = Uint16Array; + break; - case 'float': - this.__array = Float32Array; - break; + case 'int': + case 'signed int': + case 'int32': + case 'int32_t': + this.__array = Int32Array; + break; - case 'double': - this.__array = Float64Array; - break; + case 'uint': + case 'unsigned int': + case 'uint32': + case 'uint32_t': + this.__array = Uint32Array; + break; - default: - throw new Error( 'Unsupported NRRD data type: ' + data ); + case 'float': + this.__array = Float32Array; + break; - } + case 'double': + this.__array = Float64Array; + break; - return this.type = data; + default: + throw new Error( 'Unsupported NRRD data type: ' + data ); - }, - endian: function ( data ) { + } - return this.endian = data; + return this.type = data; - }, - encoding: function ( data ) { + }, + endian: function ( data ) { - return this.encoding = data; + return this.endian = data; - }, - dimension: function ( data ) { + }, + encoding: function ( data ) { - return this.dim = parseInt( data, 10 ); + return this.encoding = data; - }, - sizes: function ( data ) { + }, + dimension: function ( data ) { - var i; - return this.sizes = function () { + return this.dim = parseInt( data, 10 ); - var _i, _len, _ref, _results; + }, + sizes: function ( data ) { - _ref = data.split( /\s+/ ); - _results = []; + let i; + return this.sizes = function () { - for ( _i = 0, _len = _ref.length; _i < _len; _i ++ ) { + const _ref = data.split( /\s+/ ); - i = _ref[ _i ]; + const _results = []; - _results.push( parseInt( i, 10 ) ); + for ( let _i = 0, _len = _ref.length; _i < _len; _i ++ ) { - } + i = _ref[ _i ]; - return _results; + _results.push( parseInt( i, 10 ) ); - }(); + } - }, - space: function ( data ) { + return _results; - return this.space = data; + }(); - }, - 'space origin': function ( data ) { + }, + space: function ( data ) { - return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' ); + return this.space = data; - }, - 'space directions': function ( data ) { + }, + 'space origin': function ( data ) { - var f, parts, v; - parts = data.match( /\(.*?\)/g ); - return this.vectors = function () { + return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' ); - var _i, _len, _results; + }, + 'space directions': function ( data ) { - _results = []; + let f, v; + const parts = data.match( /\(.*?\)/g ); + return this.vectors = function () { - for ( _i = 0, _len = parts.length; _i < _len; _i ++ ) { + const _results = []; - v = parts[ _i ]; + for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { - _results.push( function () { + v = parts[ _i ]; - var _j, _len2, _ref, _results2; + _results.push( function () { - _ref = v.slice( 1, - 1 ).split( /,/ ); - _results2 = []; + const _ref = v.slice( 1, - 1 ).split( /,/ ); - for ( _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) { + const _results2 = []; - f = _ref[ _j ]; + for ( let _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) { - _results2.push( parseFloat( f ) ); + f = _ref[ _j ]; - } + _results2.push( parseFloat( f ) ); - return _results2; + } - }() ); + return _results2; - } + }() ); - return _results; + } - }(); + return _results; - }, - spacings: function ( data ) { + }(); - var f, parts; - parts = data.split( /\s+/ ); - return this.spacings = function () { + }, + spacings: function ( data ) { - var _i, - _len, - _results = []; + let f; + const parts = data.split( /\s+/ ); + return this.spacings = function () { - for ( _i = 0, _len = parts.length; _i < _len; _i ++ ) { + const _results = []; - f = parts[ _i ]; + for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { - _results.push( parseFloat( f ) ); + f = parts[ _i ]; - } + _results.push( parseFloat( f ) ); + + } - return _results; + return _results; - }(); + }(); - } } - } ); + }; THREE.NRRDLoader = NRRDLoader; diff --git a/examples/js/loaders/VTKLoader.js b/examples/js/loaders/VTKLoader.js index c90daa6d568f95..d2698266d58906 100644 --- a/examples/js/loaders/VTKLoader.js +++ b/examples/js/loaders/VTKLoader.js @@ -1,17 +1,17 @@ ( function () { - var VTKLoader = function ( manager ) { + class VTKLoader extends THREE.Loader { - THREE.Loader.call( this, manager ); + constructor( manager ) { - }; + super( manager ); - VTKLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { - constructor: VTKLoader, - load: function ( url, onLoad, onProgress, onError ) { + } + + load( url, onLoad, onProgress, onError ) { - var scope = this; - var loader = new THREE.FileLoader( scope.manager ); + const scope = this; + const loader = new THREE.FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( scope.requestHeader ); @@ -40,8 +40,9 @@ }, onProgress, onError ); - }, - parse: function ( data ) { + } + + parse( data ) { function parseASCII( data ) { @@ -511,7 +512,7 @@ function Float32Concat( first, second ) { - var firstLength = first.length, + const firstLength = first.length, result = new Float32Array( firstLength + second.length ); result.set( first ); result.set( second, firstLength ); @@ -1121,7 +1122,8 @@ } } - } ); + + } THREE.VTKLoader = VTKLoader; diff --git a/examples/jsm/loaders/3DMLoader.js b/examples/jsm/loaders/3DMLoader.js index 2f9842127021b5..8dba9cd7b01705 100644 --- a/examples/jsm/loaders/3DMLoader.js +++ b/examples/jsm/loaders/3DMLoader.js @@ -24,52 +24,50 @@ import { TextureLoader } from '../../../build/three.module.js'; -var Rhino3dmLoader = function ( manager ) { +const _taskCache = new WeakMap(); - Loader.call( this, manager ); +class Rhino3dmLoader extends Loader { - this.libraryPath = ''; - this.libraryPending = null; - this.libraryBinary = null; - this.libraryConfig = {}; + constructor( manager ) { - this.url = ''; + super( manager ); - this.workerLimit = 4; - this.workerPool = []; - this.workerNextTaskID = 1; - this.workerSourceURL = ''; - this.workerConfig = {}; + this.libraryPath = ''; + this.libraryPending = null; + this.libraryBinary = null; + this.libraryConfig = {}; - this.materials = []; + this.url = ''; -}; + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ''; + this.workerConfig = {}; -Rhino3dmLoader.taskCache = new WeakMap(); + this.materials = []; -Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { - - constructor: Rhino3dmLoader, + } - setLibraryPath: function ( path ) { + setLibraryPath( path ) { this.libraryPath = path; return this; - }, + } - setWorkerLimit: function ( workerLimit ) { + setWorkerLimit( workerLimit ) { this.workerLimit = workerLimit; return this; - }, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var loader = new FileLoader( this.manager ); + const loader = new FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); @@ -81,9 +79,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // Check for an existing task using this buffer. A transferred buffer cannot be transferred // again from this thread. - if ( Rhino3dmLoader.taskCache.has( buffer ) ) { + if ( _taskCache.has( buffer ) ) { - var cachedTask = Rhino3dmLoader.taskCache.get( buffer ); + const cachedTask = _taskCache.get( buffer ); return cachedTask.promise.then( onLoad ).catch( onError ); @@ -96,15 +94,15 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { }, onProgress, onError ); - }, + } - debug: function () { + debug() { console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); - }, + } - decodeObjects: function ( buffer, url ) { + decodeObjects( buffer, url ) { var worker; var taskID; @@ -147,7 +145,7 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } ); // Cache the task result. - Rhino3dmLoader.taskCache.set( buffer, { + _taskCache.set( buffer, { url: url, promise: objectPending @@ -156,17 +154,17 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return objectPending; - }, + } - parse: function ( data, onLoad, onError ) { + parse( data, onLoad, onError ) { this.decodeObjects( data, '' ) .then( onLoad ) .catch( onError ); - }, + } - _compareMaterials: function ( material ) { + _compareMaterials( material ) { var mat = {}; mat.name = material.name; @@ -199,9 +197,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return material; - }, + } - _createMaterial: function ( material ) { + _createMaterial( material ) { if ( material === undefined ) { @@ -281,9 +279,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return mat; - }, + } - _createGeometry: function ( data ) { + _createGeometry( data ) { // console.log(data); @@ -420,9 +418,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { object.userData[ 'materials' ] = this.materials; return object; - }, + } - _createObject: function ( obj, mat ) { + _createObject( obj, mat ) { var loader = new BufferGeometryLoader(); @@ -635,9 +633,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - }, + } - _initLibrary: function () { + _initLibrary() { if ( ! this.libraryPending ) { @@ -666,7 +664,7 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { //this.libraryBinary = binaryContent; this.libraryConfig.wasmBinary = binaryContent; - var fn = Rhino3dmLoader.Rhino3dmWorker.toString(); + var fn = Rhino3dmWorker.toString(); var body = [ '/* rhino3dm.js */', @@ -683,9 +681,9 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return this.libraryPending; - }, + } - _getWorker: function ( taskCost ) { + _getWorker( taskCost ) { return this._initLibrary().then( () => { @@ -743,17 +741,17 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } ); - }, + } - _releaseTask: function ( worker, taskID ) { + _releaseTask( worker, taskID ) { worker._taskLoad -= worker._taskCosts[ taskID ]; delete worker._callbacks[ taskID ]; delete worker._taskCosts[ taskID ]; - }, + } - dispose: function () { + dispose() { for ( var i = 0; i < this.workerPool.length; ++ i ) { @@ -767,11 +765,11 @@ Rhino3dmLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } -} ); +} /* WEB WORKER */ -Rhino3dmLoader.Rhino3dmWorker = function () { +function Rhino3dmWorker() { var libraryPending; var libraryConfig; @@ -1416,6 +1414,6 @@ Rhino3dmLoader.Rhino3dmWorker = function () { } -}; +} export { Rhino3dmLoader }; diff --git a/examples/jsm/loaders/DRACOLoader.js b/examples/jsm/loaders/DRACOLoader.js index 4b2c6b3b966d12..e9f3ca48a39f8a 100644 --- a/examples/jsm/loaders/DRACOLoader.js +++ b/examples/jsm/loaders/DRACOLoader.js @@ -5,87 +5,66 @@ import { Loader } from '../../../build/three.module.js'; -var DRACOLoader = function ( manager ) { +const _taskCache = new WeakMap(); - Loader.call( this, manager ); +class DRACOLoader extends Loader { - this.decoderPath = ''; - this.decoderConfig = {}; - this.decoderBinary = null; - this.decoderPending = null; + constructor( manager ) { - this.workerLimit = 4; - this.workerPool = []; - this.workerNextTaskID = 1; - this.workerSourceURL = ''; + super( manager ); - this.defaultAttributeIDs = { - position: 'POSITION', - normal: 'NORMAL', - color: 'COLOR', - uv: 'TEX_COORD' - }; - this.defaultAttributeTypes = { - position: 'Float32Array', - normal: 'Float32Array', - color: 'Float32Array', - uv: 'Float32Array' - }; + this.decoderPath = ''; + this.decoderConfig = {}; + this.decoderBinary = null; + this.decoderPending = null; -}; + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ''; -DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { + this.defaultAttributeIDs = { + position: 'POSITION', + normal: 'NORMAL', + color: 'COLOR', + uv: 'TEX_COORD' + }; + this.defaultAttributeTypes = { + position: 'Float32Array', + normal: 'Float32Array', + color: 'Float32Array', + uv: 'Float32Array' + }; - constructor: DRACOLoader, + } - setDecoderPath: function ( path ) { + setDecoderPath( path ) { this.decoderPath = path; return this; - }, + } - setDecoderConfig: function ( config ) { + setDecoderConfig( config ) { this.decoderConfig = config; return this; - }, + } - setWorkerLimit: function ( workerLimit ) { + setWorkerLimit( workerLimit ) { this.workerLimit = workerLimit; return this; - }, - - /** @deprecated */ - setVerbosity: function () { - - console.warn( 'THREE.DRACOLoader: The .setVerbosity() method has been removed.' ); - - }, - - /** @deprecated */ - setDrawMode: function () { - - console.warn( 'THREE.DRACOLoader: The .setDrawMode() method has been removed.' ); - - }, - - /** @deprecated */ - setSkipDequantization: function () { - - console.warn( 'THREE.DRACOLoader: The .setSkipDequantization() method has been removed.' ); - - }, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var loader = new FileLoader( this.manager ); + const loader = new FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); @@ -94,7 +73,7 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { loader.load( url, ( buffer ) => { - var taskConfig = { + const taskConfig = { attributeIDs: this.defaultAttributeIDs, attributeTypes: this.defaultAttributeTypes, useUniqueIDs: false @@ -106,12 +85,12 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { }, onProgress, onError ); - }, + } /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ - decodeDracoFile: function ( buffer, callback, attributeIDs, attributeTypes ) { + decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) { - var taskConfig = { + const taskConfig = { attributeIDs: attributeIDs || this.defaultAttributeIDs, attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !! attributeIDs @@ -119,16 +98,16 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { this.decodeGeometry( buffer, taskConfig ).then( callback ); - }, + } - decodeGeometry: function ( buffer, taskConfig ) { + decodeGeometry( buffer, taskConfig ) { // TODO: For backward-compatibility, support 'attributeTypes' objects containing // references (rather than names) to typed array constructors. These must be // serialized before sending them to the worker. - for ( var attribute in taskConfig.attributeTypes ) { + for ( const attribute in taskConfig.attributeTypes ) { - var type = taskConfig.attributeTypes[ attribute ]; + const type = taskConfig.attributeTypes[ attribute ]; if ( type.BYTES_PER_ELEMENT !== undefined ) { @@ -140,13 +119,13 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // - var taskKey = JSON.stringify( 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 ( DRACOLoader.taskCache.has( buffer ) ) { + if ( _taskCache.has( buffer ) ) { - var cachedTask = DRACOLoader.taskCache.get( buffer ); + const cachedTask = _taskCache.get( buffer ); if ( cachedTask.key === taskKey ) { @@ -171,13 +150,13 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // - var worker; - var taskID = this.workerNextTaskID ++; - var taskCost = buffer.byteLength; + 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. - var geometryPending = this._getWorker( taskID, taskCost ) + const geometryPending = this._getWorker( taskID, taskCost ) .then( ( _worker ) => { worker = _worker; @@ -212,7 +191,7 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } ); // Cache the task result. - DRACOLoader.taskCache.set( buffer, { + _taskCache.set( buffer, { key: taskKey, promise: geometryPending @@ -221,11 +200,11 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return geometryPending; - }, + } - _createGeometry: function ( geometryData ) { + _createGeometry( geometryData ) { - var geometry = new BufferGeometry(); + const geometry = new BufferGeometry(); if ( geometryData.index ) { @@ -233,12 +212,12 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - for ( var i = 0; i < geometryData.attributes.length; i ++ ) { + for ( let i = 0; i < geometryData.attributes.length; i ++ ) { - var attribute = geometryData.attributes[ i ]; - var name = attribute.name; - var array = attribute.array; - var itemSize = attribute.itemSize; + const attribute = geometryData.attributes[ i ]; + const name = attribute.name; + const array = attribute.array; + const itemSize = attribute.itemSize; geometry.setAttribute( name, new BufferAttribute( array, itemSize ) ); @@ -246,11 +225,11 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return geometry; - }, + } - _loadLibrary: function ( url, responseType ) { + _loadLibrary( url, responseType ) { - var loader = new FileLoader( this.manager ); + const loader = new FileLoader( this.manager ); loader.setPath( this.decoderPath ); loader.setResponseType( responseType ); loader.setWithCredentials( this.withCredentials ); @@ -261,22 +240,22 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } ); - }, + } - preload: function () { + preload() { this._initDecoder(); return this; - }, + } - _initDecoder: function () { + _initDecoder() { if ( this.decoderPending ) return this.decoderPending; - var useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; - var librariesPending = []; + const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; + const librariesPending = []; if ( useJS ) { @@ -292,7 +271,7 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { this.decoderPending = Promise.all( librariesPending ) .then( ( libraries ) => { - var jsContent = libraries[ 0 ]; + const jsContent = libraries[ 0 ]; if ( ! useJS ) { @@ -300,9 +279,9 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - var fn = DRACOLoader.DRACOWorker.toString(); + const fn = DRACOWorker.toString(); - var body = [ + const body = [ '/* draco decoder */', jsContent, '', @@ -316,15 +295,15 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return this.decoderPending; - }, + } - _getWorker: function ( taskID, taskCost ) { + _getWorker( taskID, taskCost ) { return this._initDecoder().then( () => { if ( this.workerPool.length < this.workerLimit ) { - var worker = new Worker( this.workerSourceURL ); + const worker = new Worker( this.workerSourceURL ); worker._callbacks = {}; worker._taskCosts = {}; @@ -334,7 +313,7 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { worker.onmessage = function ( e ) { - var message = e.data; + const message = e.data; switch ( message.type ) { @@ -365,32 +344,32 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - var worker = this.workerPool[ this.workerPool.length - 1 ]; + const worker = this.workerPool[ this.workerPool.length - 1 ]; worker._taskCosts[ taskID ] = taskCost; worker._taskLoad += taskCost; return worker; } ); - }, + } - _releaseTask: function ( worker, taskID ) { + _releaseTask( worker, taskID ) { worker._taskLoad -= worker._taskCosts[ taskID ]; delete worker._callbacks[ taskID ]; delete worker._taskCosts[ taskID ]; - }, + } - debug: function () { + debug() { console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); - }, + } - dispose: function () { + dispose() { - for ( var i = 0; i < this.workerPool.length; ++ i ) { + for ( let i = 0; i < this.workerPool.length; ++ i ) { this.workerPool[ i ].terminate(); @@ -402,18 +381,18 @@ DRACOLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } -} ); +} /* WEB WORKER */ -DRACOLoader.DRACOWorker = function () { +function DRACOWorker() { - var decoderConfig; - var decoderPending; + let decoderConfig; + let decoderPending; onmessage = function ( e ) { - var message = e.data; + const message = e.data; switch ( message.type ) { @@ -434,20 +413,20 @@ DRACOLoader.DRACOWorker = function () { break; case 'decode': - var buffer = message.buffer; - var taskConfig = message.taskConfig; + const buffer = message.buffer; + const taskConfig = message.taskConfig; decoderPending.then( ( module ) => { - var draco = module.draco; - var decoder = new draco.Decoder(); - var decoderBuffer = new draco.DecoderBuffer(); + const draco = module.draco; + const decoder = new draco.Decoder(); + const decoderBuffer = new draco.DecoderBuffer(); decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength ); try { - var geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig ); + const geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig ); - var buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); + const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); if ( geometry.index ) buffers.push( geometry.index.array.buffer ); @@ -475,13 +454,13 @@ DRACOLoader.DRACOWorker = function () { function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) { - var attributeIDs = taskConfig.attributeIDs; - var attributeTypes = taskConfig.attributeTypes; + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; - var dracoGeometry; - var decodingStatus; + let dracoGeometry; + let decodingStatus; - var geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); + const geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); if ( geometryType === draco.TRIANGULAR_MESH ) { @@ -505,15 +484,15 @@ DRACOLoader.DRACOWorker = function () { } - var geometry = { index: null, attributes: [] }; + const geometry = { index: null, attributes: [] }; // Gather all vertex attributes. - for ( var attributeName in attributeIDs ) { + for ( const attributeName in attributeIDs ) { - var attributeType = self[ attributeTypes[ attributeName ] ]; + const attributeType = self[ attributeTypes[ attributeName ] ]; - var attribute; - var attributeID; + 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, @@ -553,13 +532,13 @@ DRACOLoader.DRACOWorker = function () { function decodeIndex( draco, decoder, dracoGeometry ) { - var numFaces = dracoGeometry.num_faces(); - var numIndices = numFaces * 3; - var byteLength = numIndices * 4; + const numFaces = dracoGeometry.num_faces(); + const numIndices = numFaces * 3; + const byteLength = numIndices * 4; - var ptr = draco._malloc( byteLength ); + const ptr = draco._malloc( byteLength ); decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr ); - var index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); + const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); draco._free( ptr ); return { array: index, itemSize: 1 }; @@ -568,15 +547,15 @@ DRACOLoader.DRACOWorker = function () { function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { - var numComponents = attribute.num_components(); - var numPoints = dracoGeometry.num_points(); - var numValues = numPoints * numComponents; - var byteLength = numValues * attributeType.BYTES_PER_ELEMENT; - var dataType = getDracoDataType( draco, attributeType ); + 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 ); - var ptr = draco._malloc( byteLength ); + const ptr = draco._malloc( byteLength ); decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr ); - var array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); + const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); draco._free( ptr ); return { @@ -603,38 +582,6 @@ DRACOLoader.DRACOWorker = function () { } -}; - -DRACOLoader.taskCache = new WeakMap(); - -/** Deprecated static methods */ - -/** @deprecated */ -DRACOLoader.setDecoderPath = function () { - - console.warn( 'THREE.DRACOLoader: The .setDecoderPath() method has been removed. Use instance methods.' ); - -}; - -/** @deprecated */ -DRACOLoader.setDecoderConfig = function () { - - console.warn( 'THREE.DRACOLoader: The .setDecoderConfig() method has been removed. Use instance methods.' ); - -}; - -/** @deprecated */ -DRACOLoader.releaseDecoderModule = function () { - - console.warn( 'THREE.DRACOLoader: The .releaseDecoderModule() method has been removed. Use instance methods.' ); - -}; - -/** @deprecated */ -DRACOLoader.getDecoderModule = function () { - - console.warn( 'THREE.DRACOLoader: The .getDecoderModule() method has been removed. Use instance methods.' ); - -}; +} export { DRACOLoader }; diff --git a/examples/jsm/loaders/EXRLoader.js b/examples/jsm/loaders/EXRLoader.js index 93312a5d1d96ea..986cab7667377d 100644 --- a/examples/jsm/loaders/EXRLoader.js +++ b/examples/jsm/loaders/EXRLoader.js @@ -87,19 +87,17 @@ import * as fflate from '../libs/fflate.module.min.js'; // // End of OpenEXR license ------------------------------------------------- -var EXRLoader = function ( manager ) { +class EXRLoader extends DataTextureLoader { - DataTextureLoader.call( this, manager ); + constructor( manager ) { - this.type = FloatType; + super( manager ); -}; + this.type = FloatType; -EXRLoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype ), { - - constructor: EXRLoader, + } - parse: function ( buffer ) { + parse( buffer ) { const USHORT_RANGE = ( 1 << 16 ); const BITMAP_SIZE = ( USHORT_RANGE >> 3 ); @@ -2366,16 +2364,16 @@ EXRLoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype type: this.type }; - }, + } - setDataType: function ( value ) { + setDataType( value ) { this.type = value; return this; - }, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { function onLoadCallback( texture, texData ) { @@ -2406,10 +2404,10 @@ EXRLoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype } - return DataTextureLoader.prototype.load.call( this, url, onLoadCallback, onProgress, onError ); + return super.load( url, onLoadCallback, onProgress, onError ); } -} ); +} export { EXRLoader }; diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 8b851bb9125918..f505b44ad805a5 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -62,205 +62,197 @@ import { NURBSCurve } from '../curves/NURBSCurve.js'; */ -var FBXLoader = ( function () { +let fbxTree; +let connections; +let sceneGraph; - var fbxTree; - var connections; - var sceneGraph; +class FBXLoader extends Loader { - function FBXLoader( manager ) { + constructor( manager ) { - Loader.call( this, manager ); + super( manager ); } - FBXLoader.prototype = Object.assign( Object.create( Loader.prototype ), { + load( url, onLoad, onProgress, onError ) { - constructor: FBXLoader, + const scope = this; - load: function ( url, onLoad, onProgress, onError ) { + const path = ( scope.path === '' ) ? LoaderUtils.extractUrlBase( url ) : scope.path; - var scope = this; + const loader = new FileLoader( this.manager ); + loader.setPath( scope.path ); + loader.setResponseType( 'arraybuffer' ); + loader.setRequestHeader( scope.requestHeader ); + loader.setWithCredentials( scope.withCredentials ); - var path = ( scope.path === '' ) ? LoaderUtils.extractUrlBase( url ) : scope.path; + loader.load( url, function ( buffer ) { - var loader = new FileLoader( this.manager ); - loader.setPath( scope.path ); - loader.setResponseType( 'arraybuffer' ); - loader.setRequestHeader( scope.requestHeader ); - loader.setWithCredentials( scope.withCredentials ); + try { - loader.load( url, function ( buffer ) { + onLoad( scope.parse( buffer, path ) ); - try { + } catch ( e ) { - onLoad( scope.parse( buffer, path ) ); + if ( onError ) { - } catch ( e ) { + onError( e ); - if ( onError ) { - - onError( e ); + } else { - } else { + console.error( e ); - console.error( e ); + } - } + scope.manager.itemError( url ); - scope.manager.itemError( url ); + } - } + }, onProgress, onError ); - }, onProgress, onError ); + } - }, + parse( FBXBuffer, path ) { - parse: function ( FBXBuffer, path ) { + if ( isFbxFormatBinary( FBXBuffer ) ) { - if ( isFbxFormatBinary( FBXBuffer ) ) { + fbxTree = new BinaryParser().parse( FBXBuffer ); - fbxTree = new BinaryParser().parse( FBXBuffer ); + } else { - } else { + const FBXText = convertArrayBufferToString( FBXBuffer ); - var FBXText = convertArrayBufferToString( FBXBuffer ); + if ( ! isFbxFormatASCII( FBXText ) ) { - if ( ! isFbxFormatASCII( FBXText ) ) { + throw new Error( 'THREE.FBXLoader: Unknown format.' ); - throw new Error( 'THREE.FBXLoader: Unknown format.' ); + } - } + if ( getFbxVersion( FBXText ) < 7000 ) { - if ( getFbxVersion( FBXText ) < 7000 ) { + throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); - throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); + } - } + fbxTree = new TextParser().parse( FBXText ); - fbxTree = new TextParser().parse( FBXText ); + } - } + // console.log( fbxTree ); - // console.log( fbxTree ); + const textureLoader = new TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); - var textureLoader = new TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); + return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree ); - return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree ); + } - } +} - } ); +// Parse the FBXTree object returned by the BinaryParser or TextParser and return a Group +class FBXTreeParser { - // Parse the FBXTree object returned by the BinaryParser or TextParser and return a Group - function FBXTreeParser( textureLoader, manager ) { + constructor( textureLoader, manager ) { this.textureLoader = textureLoader; this.manager = manager; } - FBXTreeParser.prototype = { - - constructor: FBXTreeParser, + parse() { - parse: function () { + connections = this.parseConnections(); - connections = this.parseConnections(); + const images = this.parseImages(); + const textures = this.parseTextures( images ); + const materials = this.parseMaterials( textures ); + const deformers = this.parseDeformers(); + const geometryMap = new GeometryParser().parse( deformers ); - var images = this.parseImages(); - var textures = this.parseTextures( images ); - var materials = this.parseMaterials( textures ); - var deformers = this.parseDeformers(); - var geometryMap = new GeometryParser().parse( deformers ); + this.parseScene( deformers, geometryMap, materials ); - this.parseScene( deformers, geometryMap, materials ); + return sceneGraph; - return sceneGraph; - - }, + } - // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) - // and details the connection type - parseConnections: function () { + // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) + // and details the connection type + parseConnections() { - var connectionMap = new Map(); + const connectionMap = new Map(); - if ( 'Connections' in fbxTree ) { + if ( 'Connections' in fbxTree ) { - var rawConnections = fbxTree.Connections.connections; + const rawConnections = fbxTree.Connections.connections; - rawConnections.forEach( function ( rawConnection ) { + rawConnections.forEach( function ( rawConnection ) { - var fromID = rawConnection[ 0 ]; - var toID = rawConnection[ 1 ]; - var relationship = rawConnection[ 2 ]; + const fromID = rawConnection[ 0 ]; + const toID = rawConnection[ 1 ]; + const relationship = rawConnection[ 2 ]; - if ( ! connectionMap.has( fromID ) ) { + if ( ! connectionMap.has( fromID ) ) { - connectionMap.set( fromID, { - parents: [], - children: [] - } ); - - } + connectionMap.set( fromID, { + parents: [], + children: [] + } ); - var parentRelationship = { ID: toID, relationship: relationship }; - connectionMap.get( fromID ).parents.push( parentRelationship ); + } - if ( ! connectionMap.has( toID ) ) { + const parentRelationship = { ID: toID, relationship: relationship }; + connectionMap.get( fromID ).parents.push( parentRelationship ); - connectionMap.set( toID, { - parents: [], - children: [] - } ); + if ( ! connectionMap.has( toID ) ) { - } + connectionMap.set( toID, { + parents: [], + children: [] + } ); - var childRelationship = { ID: fromID, relationship: relationship }; - connectionMap.get( toID ).children.push( childRelationship ); + } - } ); + const childRelationship = { ID: fromID, relationship: relationship }; + connectionMap.get( toID ).children.push( childRelationship ); - } + } ); - return connectionMap; + } - }, + return connectionMap; - // Parse FBXTree.Objects.Video for embedded image data - // These images are connected to textures in FBXTree.Objects.Textures - // via FBXTree.Connections. - parseImages: function () { + } - var images = {}; - var blobs = {}; + // Parse FBXTree.Objects.Video for embedded image data + // These images are connected to textures in FBXTree.Objects.Textures + // via FBXTree.Connections. + parseImages() { - if ( 'Video' in fbxTree.Objects ) { + const images = {}; + const blobs = {}; - var videoNodes = fbxTree.Objects.Video; + if ( 'Video' in fbxTree.Objects ) { - for ( var nodeID in videoNodes ) { + const videoNodes = fbxTree.Objects.Video; - var videoNode = videoNodes[ nodeID ]; + for ( const nodeID in videoNodes ) { - var id = parseInt( nodeID ); + const videoNode = videoNodes[ nodeID ]; - images[ id ] = videoNode.RelativeFilename || videoNode.Filename; + const id = parseInt( nodeID ); - // raw image data is in videoNode.Content - if ( 'Content' in videoNode ) { + images[ id ] = videoNode.RelativeFilename || videoNode.Filename; - var arrayBufferContent = ( videoNode.Content instanceof ArrayBuffer ) && ( videoNode.Content.byteLength > 0 ); - var base64Content = ( typeof videoNode.Content === 'string' ) && ( videoNode.Content !== '' ); + // raw image data is in videoNode.Content + if ( 'Content' in videoNode ) { - if ( arrayBufferContent || base64Content ) { + const arrayBufferContent = ( videoNode.Content instanceof ArrayBuffer ) && ( videoNode.Content.byteLength > 0 ); + const base64Content = ( typeof videoNode.Content === 'string' ) && ( videoNode.Content !== '' ); - var image = this.parseImage( videoNodes[ nodeID ] ); + if ( arrayBufferContent || base64Content ) { - blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image; + const image = this.parseImage( videoNodes[ nodeID ] ); - } + blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image; } @@ -268,1170 +260,1170 @@ var FBXLoader = ( function () { } - for ( var id in images ) { + } - var filename = images[ id ]; + for ( const id in images ) { - if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ]; - else images[ id ] = images[ id ].split( '\\' ).pop(); + const filename = images[ id ]; - } + if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ]; + else images[ id ] = images[ id ].split( '\\' ).pop(); - return images; + } - }, + return images; - // Parse embedded image data in FBXTree.Video.Content - parseImage: function ( videoNode ) { + } - var content = videoNode.Content; - var fileName = videoNode.RelativeFilename || videoNode.Filename; - var extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); + // Parse embedded image data in FBXTree.Video.Content + parseImage( videoNode ) { - var type; + const content = videoNode.Content; + const fileName = videoNode.RelativeFilename || videoNode.Filename; + const extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); - switch ( extension ) { + let type; - case 'bmp': + switch ( extension ) { - type = 'image/bmp'; - break; + case 'bmp': - case 'jpg': - case 'jpeg': + type = 'image/bmp'; + break; - type = 'image/jpeg'; - break; + case 'jpg': + case 'jpeg': - case 'png': + type = 'image/jpeg'; + break; - type = 'image/png'; - break; + case 'png': - case 'tif': + type = 'image/png'; + break; - type = 'image/tiff'; - break; + case 'tif': - case 'tga': + type = 'image/tiff'; + break; - if ( this.manager.getHandler( '.tga' ) === null ) { + case 'tga': - console.warn( 'FBXLoader: TGA loader not found, skipping ', fileName ); + if ( this.manager.getHandler( '.tga' ) === null ) { - } + console.warn( 'FBXLoader: TGA loader not found, skipping ', fileName ); - type = 'image/tga'; - break; + } - default: + type = 'image/tga'; + break; - console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); - return; + default: - } + console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); + return; - if ( typeof content === 'string' ) { // ASCII format + } - return 'data:' + type + ';base64,' + content; + if ( typeof content === 'string' ) { // ASCII format - } else { // Binary Format + return 'data:' + type + ';base64,' + content; - var array = new Uint8Array( content ); - return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) ); + } else { // Binary Format - } + const array = new Uint8Array( content ); + return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) ); - }, + } - // Parse nodes in FBXTree.Objects.Texture - // These contain details such as UV scaling, cropping, rotation etc and are connected - // to images in FBXTree.Objects.Video - parseTextures: function ( images ) { + } - var textureMap = new Map(); + // Parse nodes in FBXTree.Objects.Texture + // These contain details such as UV scaling, cropping, rotation etc and are connected + // to images in FBXTree.Objects.Video + parseTextures( images ) { - if ( 'Texture' in fbxTree.Objects ) { + const textureMap = new Map(); - var textureNodes = fbxTree.Objects.Texture; - for ( var nodeID in textureNodes ) { + if ( 'Texture' in fbxTree.Objects ) { - var texture = this.parseTexture( textureNodes[ nodeID ], images ); - textureMap.set( parseInt( nodeID ), texture ); + const textureNodes = fbxTree.Objects.Texture; + for ( const nodeID in textureNodes ) { - } + const texture = this.parseTexture( textureNodes[ nodeID ], images ); + textureMap.set( parseInt( nodeID ), texture ); } - return textureMap; + } - }, + return textureMap; - // Parse individual node in FBXTree.Objects.Texture - parseTexture: function ( textureNode, images ) { + } - var texture = this.loadTexture( textureNode, images ); + // Parse individual node in FBXTree.Objects.Texture + parseTexture( textureNode, images ) { - texture.ID = textureNode.id; + const texture = this.loadTexture( textureNode, images ); - texture.name = textureNode.attrName; + texture.ID = textureNode.id; - var wrapModeU = textureNode.WrapModeU; - var wrapModeV = textureNode.WrapModeV; + texture.name = textureNode.attrName; - var valueU = wrapModeU !== undefined ? wrapModeU.value : 0; - var valueV = wrapModeV !== undefined ? wrapModeV.value : 0; + const wrapModeU = textureNode.WrapModeU; + const wrapModeV = textureNode.WrapModeV; - // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a - // 0: repeat(default), 1: clamp + const valueU = wrapModeU !== undefined ? wrapModeU.value : 0; + const valueV = wrapModeV !== undefined ? wrapModeV.value : 0; - texture.wrapS = valueU === 0 ? RepeatWrapping : ClampToEdgeWrapping; - texture.wrapT = valueV === 0 ? RepeatWrapping : ClampToEdgeWrapping; + // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a + // 0: repeat(default), 1: clamp - if ( 'Scaling' in textureNode ) { + texture.wrapS = valueU === 0 ? RepeatWrapping : ClampToEdgeWrapping; + texture.wrapT = valueV === 0 ? RepeatWrapping : ClampToEdgeWrapping; - var values = textureNode.Scaling.value; + if ( 'Scaling' in textureNode ) { - texture.repeat.x = values[ 0 ]; - texture.repeat.y = values[ 1 ]; + const values = textureNode.Scaling.value; - } + texture.repeat.x = values[ 0 ]; + texture.repeat.y = values[ 1 ]; - return texture; + } - }, + return texture; - // load a texture specified as a blob or data URI, or via an external URL using TextureLoader - loadTexture: function ( textureNode, images ) { + } - var fileName; + // load a texture specified as a blob or data URI, or via an external URL using TextureLoader + loadTexture( textureNode, images ) { - var currentPath = this.textureLoader.path; + let fileName; - var children = connections.get( textureNode.id ).children; + const currentPath = this.textureLoader.path; - if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) { + const children = connections.get( textureNode.id ).children; - fileName = images[ children[ 0 ].ID ]; + if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) { - if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { + fileName = images[ children[ 0 ].ID ]; - this.textureLoader.setPath( undefined ); + if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { - } + this.textureLoader.setPath( undefined ); } - var texture; + } - var extension = textureNode.FileName.slice( - 3 ).toLowerCase(); + let texture; - if ( extension === 'tga' ) { + const extension = textureNode.FileName.slice( - 3 ).toLowerCase(); - var loader = this.manager.getHandler( '.tga' ); + if ( extension === 'tga' ) { - if ( loader === null ) { + const loader = this.manager.getHandler( '.tga' ); - console.warn( 'FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename ); - texture = new Texture(); + if ( loader === null ) { - } else { + console.warn( 'FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename ); + texture = new Texture(); - texture = loader.load( fileName ); + } else { - } + texture = loader.load( fileName ); - } else if ( extension === 'psd' ) { + } - console.warn( 'FBXLoader: PSD textures are not supported, creating placeholder texture for', textureNode.RelativeFilename ); - texture = new Texture(); + } else if ( extension === 'psd' ) { - } else { + console.warn( 'FBXLoader: PSD textures are not supported, creating placeholder texture for', textureNode.RelativeFilename ); + texture = new Texture(); - texture = this.textureLoader.load( fileName ); + } else { - } + texture = this.textureLoader.load( fileName ); - this.textureLoader.setPath( currentPath ); + } - return texture; + this.textureLoader.setPath( currentPath ); - }, + return texture; - // Parse nodes in FBXTree.Objects.Material - parseMaterials: function ( textureMap ) { + } - var materialMap = new Map(); + // Parse nodes in FBXTree.Objects.Material + parseMaterials( textureMap ) { - if ( 'Material' in fbxTree.Objects ) { + const materialMap = new Map(); - var materialNodes = fbxTree.Objects.Material; + if ( 'Material' in fbxTree.Objects ) { - for ( var nodeID in materialNodes ) { + const materialNodes = fbxTree.Objects.Material; - var material = this.parseMaterial( materialNodes[ nodeID ], textureMap ); + for ( const nodeID in materialNodes ) { - if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); + const material = this.parseMaterial( materialNodes[ nodeID ], textureMap ); - } + if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); } - return materialMap; + } - }, + return materialMap; - // Parse single node in FBXTree.Objects.Material - // Materials are connected to texture maps in FBXTree.Objects.Textures - // FBX format currently only supports Lambert and Phong shading models - parseMaterial: function ( materialNode, textureMap ) { + } - var ID = materialNode.id; - var name = materialNode.attrName; - var type = materialNode.ShadingModel; + // Parse single node in FBXTree.Objects.Material + // Materials are connected to texture maps in FBXTree.Objects.Textures + // FBX format currently only supports Lambert and Phong shading models + parseMaterial( materialNode, textureMap ) { - // Case where FBX wraps shading model in property object. - if ( typeof type === 'object' ) { + const ID = materialNode.id; + const name = materialNode.attrName; + let type = materialNode.ShadingModel; - type = type.value; + // Case where FBX wraps shading model in property object. + if ( typeof type === 'object' ) { - } + type = type.value; - // Ignore unused materials which don't have any connections. - if ( ! connections.has( ID ) ) return null; + } - var parameters = this.parseParameters( materialNode, textureMap, ID ); + // Ignore unused materials which don't have any connections. + if ( ! connections.has( ID ) ) return null; - var material; + const parameters = this.parseParameters( materialNode, textureMap, ID ); - switch ( type.toLowerCase() ) { + let material; - case 'phong': - material = new MeshPhongMaterial(); - break; - case 'lambert': - material = new MeshLambertMaterial(); - break; - default: - console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.', type ); - material = new MeshPhongMaterial(); - break; + switch ( type.toLowerCase() ) { - } + case 'phong': + material = new MeshPhongMaterial(); + break; + case 'lambert': + material = new MeshLambertMaterial(); + break; + default: + console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.', type ); + material = new MeshPhongMaterial(); + break; - material.setValues( parameters ); - material.name = name; + } - return material; + material.setValues( parameters ); + material.name = name; - }, + return material; - // Parse FBX material and return parameters suitable for a three.js material - // Also parse the texture map and return any textures associated with the material - parseParameters: function ( materialNode, textureMap, ID ) { + } - var parameters = {}; + // Parse FBX material and return parameters suitable for a three.js material + // Also parse the texture map and return any textures associated with the material + parseParameters( materialNode, textureMap, ID ) { - if ( materialNode.BumpFactor ) { + const parameters = {}; - parameters.bumpScale = materialNode.BumpFactor.value; + if ( materialNode.BumpFactor ) { - } + parameters.bumpScale = materialNode.BumpFactor.value; - if ( materialNode.Diffuse ) { + } - parameters.color = new Color().fromArray( materialNode.Diffuse.value ); + if ( materialNode.Diffuse ) { - } else if ( materialNode.DiffuseColor && ( materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB' ) ) { + parameters.color = new Color().fromArray( materialNode.Diffuse.value ); - // The blender exporter exports diffuse here instead of in materialNode.Diffuse - parameters.color = new Color().fromArray( materialNode.DiffuseColor.value ); + } else if ( materialNode.DiffuseColor && ( materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB' ) ) { - } + // The blender exporter exports diffuse here instead of in materialNode.Diffuse + parameters.color = new Color().fromArray( materialNode.DiffuseColor.value ); - if ( materialNode.DisplacementFactor ) { + } - parameters.displacementScale = materialNode.DisplacementFactor.value; + if ( materialNode.DisplacementFactor ) { - } + parameters.displacementScale = materialNode.DisplacementFactor.value; - if ( materialNode.Emissive ) { + } - parameters.emissive = new Color().fromArray( materialNode.Emissive.value ); + if ( materialNode.Emissive ) { - } else if ( materialNode.EmissiveColor && ( materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB' ) ) { + parameters.emissive = new Color().fromArray( materialNode.Emissive.value ); - // The blender exporter exports emissive color here instead of in materialNode.Emissive - parameters.emissive = new Color().fromArray( materialNode.EmissiveColor.value ); + } else if ( materialNode.EmissiveColor && ( materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB' ) ) { - } + // The blender exporter exports emissive color here instead of in materialNode.Emissive + parameters.emissive = new Color().fromArray( materialNode.EmissiveColor.value ); - if ( materialNode.EmissiveFactor ) { + } - parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value ); + if ( materialNode.EmissiveFactor ) { - } + parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value ); - if ( materialNode.Opacity ) { + } - parameters.opacity = parseFloat( materialNode.Opacity.value ); + if ( materialNode.Opacity ) { - } + parameters.opacity = parseFloat( materialNode.Opacity.value ); - if ( parameters.opacity < 1.0 ) { + } - parameters.transparent = true; + if ( parameters.opacity < 1.0 ) { - } + parameters.transparent = true; - if ( materialNode.ReflectionFactor ) { + } - parameters.reflectivity = materialNode.ReflectionFactor.value; + if ( materialNode.ReflectionFactor ) { - } + parameters.reflectivity = materialNode.ReflectionFactor.value; - if ( materialNode.Shininess ) { + } - parameters.shininess = materialNode.Shininess.value; + if ( materialNode.Shininess ) { - } + parameters.shininess = materialNode.Shininess.value; - if ( materialNode.Specular ) { + } - parameters.specular = new Color().fromArray( materialNode.Specular.value ); + if ( materialNode.Specular ) { - } else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) { + parameters.specular = new Color().fromArray( materialNode.Specular.value ); - // The blender exporter exports specular color here instead of in materialNode.Specular - parameters.specular = new Color().fromArray( materialNode.SpecularColor.value ); + } else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) { - } + // The blender exporter exports specular color here instead of in materialNode.Specular + parameters.specular = new Color().fromArray( materialNode.SpecularColor.value ); - var scope = this; - connections.get( ID ).children.forEach( function ( child ) { + } - var type = child.relationship; + const scope = this; + connections.get( ID ).children.forEach( function ( child ) { - switch ( type ) { + const type = child.relationship; - case 'Bump': - parameters.bumpMap = scope.getTexture( textureMap, child.ID ); - break; + switch ( type ) { - case 'Maya|TEX_ao_map': - parameters.aoMap = scope.getTexture( textureMap, child.ID ); - break; + case 'Bump': + parameters.bumpMap = scope.getTexture( textureMap, child.ID ); + break; - case 'DiffuseColor': - case 'Maya|TEX_color_map': - parameters.map = scope.getTexture( textureMap, child.ID ); - parameters.map.encoding = sRGBEncoding; - break; + case 'Maya|TEX_ao_map': + parameters.aoMap = scope.getTexture( textureMap, child.ID ); + break; - case 'DisplacementColor': - parameters.displacementMap = scope.getTexture( textureMap, child.ID ); - break; + case 'DiffuseColor': + case 'Maya|TEX_color_map': + parameters.map = scope.getTexture( textureMap, child.ID ); + parameters.map.encoding = sRGBEncoding; + break; - case 'EmissiveColor': - parameters.emissiveMap = scope.getTexture( textureMap, child.ID ); - parameters.emissiveMap.encoding = sRGBEncoding; - break; + case 'DisplacementColor': + parameters.displacementMap = scope.getTexture( textureMap, child.ID ); + break; - case 'NormalMap': - case 'Maya|TEX_normal_map': - parameters.normalMap = scope.getTexture( textureMap, child.ID ); - break; + case 'EmissiveColor': + parameters.emissiveMap = scope.getTexture( textureMap, child.ID ); + parameters.emissiveMap.encoding = sRGBEncoding; + break; - case 'ReflectionColor': - parameters.envMap = scope.getTexture( textureMap, child.ID ); - parameters.envMap.mapping = EquirectangularReflectionMapping; - parameters.envMap.encoding = sRGBEncoding; - break; + case 'NormalMap': + case 'Maya|TEX_normal_map': + parameters.normalMap = scope.getTexture( textureMap, child.ID ); + break; - case 'SpecularColor': - parameters.specularMap = scope.getTexture( textureMap, child.ID ); - parameters.specularMap.encoding = sRGBEncoding; - break; + case 'ReflectionColor': + parameters.envMap = scope.getTexture( textureMap, child.ID ); + parameters.envMap.mapping = EquirectangularReflectionMapping; + parameters.envMap.encoding = sRGBEncoding; + break; - case 'TransparentColor': - case 'TransparencyFactor': - parameters.alphaMap = scope.getTexture( textureMap, child.ID ); - parameters.transparent = true; - break; + case 'SpecularColor': + parameters.specularMap = scope.getTexture( textureMap, child.ID ); + parameters.specularMap.encoding = sRGBEncoding; + break; - case 'AmbientColor': - case 'ShininessExponent': // AKA glossiness map - case 'SpecularFactor': // AKA specularLevel - case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor - default: - console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type ); - break; + case 'TransparentColor': + case 'TransparencyFactor': + parameters.alphaMap = scope.getTexture( textureMap, child.ID ); + parameters.transparent = true; + break; - } + case 'AmbientColor': + case 'ShininessExponent': // AKA glossiness map + case 'SpecularFactor': // AKA specularLevel + case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor + default: + console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type ); + break; - } ); + } - return parameters; + } ); - }, + return parameters; - // get a texture from the textureMap for use by a material. - getTexture: function ( textureMap, id ) { + } - // if the texture is a layered texture, just use the first layer and issue a warning - if ( 'LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture ) { + // get a texture from the textureMap for use by a material. + getTexture( textureMap, id ) { - console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); - id = connections.get( id ).children[ 0 ].ID; + // if the texture is a layered texture, just use the first layer and issue a warning + if ( 'LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture ) { - } + console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); + id = connections.get( id ).children[ 0 ].ID; - return textureMap.get( id ); + } - }, + return textureMap.get( id ); - // Parse nodes in FBXTree.Objects.Deformer - // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here - // Generates map of Skeleton-like objects for use later when generating and binding skeletons. - parseDeformers: function () { + } - var skeletons = {}; - var morphTargets = {}; + // Parse nodes in FBXTree.Objects.Deformer + // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here + // Generates map of Skeleton-like objects for use later when generating and binding skeletons. + parseDeformers() { - if ( 'Deformer' in fbxTree.Objects ) { + const skeletons = {}; + const morphTargets = {}; - var DeformerNodes = fbxTree.Objects.Deformer; + if ( 'Deformer' in fbxTree.Objects ) { - for ( var nodeID in DeformerNodes ) { + const DeformerNodes = fbxTree.Objects.Deformer; - var deformerNode = DeformerNodes[ nodeID ]; + for ( const nodeID in DeformerNodes ) { - var relationships = connections.get( parseInt( nodeID ) ); + const deformerNode = DeformerNodes[ nodeID ]; - if ( deformerNode.attrType === 'Skin' ) { + const relationships = connections.get( parseInt( nodeID ) ); - var skeleton = this.parseSkeleton( relationships, DeformerNodes ); - skeleton.ID = nodeID; + if ( deformerNode.attrType === 'Skin' ) { - if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' ); - skeleton.geometryID = relationships.parents[ 0 ].ID; + const skeleton = this.parseSkeleton( relationships, DeformerNodes ); + skeleton.ID = nodeID; - skeletons[ nodeID ] = skeleton; + if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' ); + skeleton.geometryID = relationships.parents[ 0 ].ID; - } else if ( deformerNode.attrType === 'BlendShape' ) { + skeletons[ nodeID ] = skeleton; - var morphTarget = { - id: nodeID, - }; + } else if ( deformerNode.attrType === 'BlendShape' ) { - morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes ); - morphTarget.id = nodeID; + const morphTarget = { + id: nodeID, + }; - if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' ); + morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes ); + morphTarget.id = nodeID; - morphTargets[ nodeID ] = morphTarget; + if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' ); - } + morphTargets[ nodeID ] = morphTarget; } } - return { - - skeletons: skeletons, - morphTargets: morphTargets, - - }; + } - }, + return { - // Parse single nodes in FBXTree.Objects.Deformer - // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster' - // Each skin node represents a skeleton and each cluster node represents a bone - parseSkeleton: function ( relationships, deformerNodes ) { + skeletons: skeletons, + morphTargets: morphTargets, - var rawBones = []; + }; - relationships.children.forEach( function ( child ) { + } - var boneNode = deformerNodes[ child.ID ]; + // Parse single nodes in FBXTree.Objects.Deformer + // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster' + // Each skin node represents a skeleton and each cluster node represents a bone + parseSkeleton( relationships, deformerNodes ) { - if ( boneNode.attrType !== 'Cluster' ) return; + const rawBones = []; - var rawBone = { + relationships.children.forEach( function ( child ) { - ID: child.ID, - indices: [], - weights: [], - transformLink: new Matrix4().fromArray( boneNode.TransformLink.a ), - // transform: new Matrix4().fromArray( boneNode.Transform.a ), - // linkMode: boneNode.Mode, + const boneNode = deformerNodes[ child.ID ]; - }; + if ( boneNode.attrType !== 'Cluster' ) return; - if ( 'Indexes' in boneNode ) { + const rawBone = { - rawBone.indices = boneNode.Indexes.a; - rawBone.weights = boneNode.Weights.a; + ID: child.ID, + indices: [], + weights: [], + transformLink: new Matrix4().fromArray( boneNode.TransformLink.a ), + // transform: new Matrix4().fromArray( boneNode.Transform.a ), + // linkMode: boneNode.Mode, - } + }; - rawBones.push( rawBone ); + if ( 'Indexes' in boneNode ) { - } ); + rawBone.indices = boneNode.Indexes.a; + rawBone.weights = boneNode.Weights.a; - return { + } - rawBones: rawBones, - bones: [] + rawBones.push( rawBone ); - }; + } ); - }, + return { - // The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel" - parseMorphTargets: function ( relationships, deformerNodes ) { + rawBones: rawBones, + bones: [] - var rawMorphTargets = []; + }; - for ( var i = 0; i < relationships.children.length; i ++ ) { + } - var child = relationships.children[ i ]; + // The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel" + parseMorphTargets( relationships, deformerNodes ) { - var morphTargetNode = deformerNodes[ child.ID ]; + const rawMorphTargets = []; - var rawMorphTarget = { + for ( let i = 0; i < relationships.children.length; i ++ ) { - name: morphTargetNode.attrName, - initialWeight: morphTargetNode.DeformPercent, - id: morphTargetNode.id, - fullWeights: morphTargetNode.FullWeights.a + const child = relationships.children[ i ]; - }; + const morphTargetNode = deformerNodes[ child.ID ]; - if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return; + const rawMorphTarget = { - rawMorphTarget.geoID = connections.get( parseInt( child.ID ) ).children.filter( function ( child ) { + name: morphTargetNode.attrName, + initialWeight: morphTargetNode.DeformPercent, + id: morphTargetNode.id, + fullWeights: morphTargetNode.FullWeights.a - return child.relationship === undefined; + }; - } )[ 0 ].ID; + if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return; - rawMorphTargets.push( rawMorphTarget ); + rawMorphTarget.geoID = connections.get( parseInt( child.ID ) ).children.filter( function ( child ) { - } + return child.relationship === undefined; - return rawMorphTargets; + } )[ 0 ].ID; - }, + rawMorphTargets.push( rawMorphTarget ); - // create the main Group() to be returned by the loader - parseScene: function ( deformers, geometryMap, materialMap ) { + } - sceneGraph = new Group(); + return rawMorphTargets; - var modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap ); + } - var modelNodes = fbxTree.Objects.Model; + // create the main Group() to be returned by the loader + parseScene( deformers, geometryMap, materialMap ) { - var scope = this; - modelMap.forEach( function ( model ) { + sceneGraph = new Group(); - var modelNode = modelNodes[ model.ID ]; - scope.setLookAtProperties( model, modelNode ); + const modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap ); - var parentConnections = connections.get( model.ID ).parents; + const modelNodes = fbxTree.Objects.Model; - parentConnections.forEach( function ( connection ) { + const scope = this; + modelMap.forEach( function ( model ) { - var parent = modelMap.get( connection.ID ); - if ( parent !== undefined ) parent.add( model ); + const modelNode = modelNodes[ model.ID ]; + scope.setLookAtProperties( model, modelNode ); - } ); + const parentConnections = connections.get( model.ID ).parents; - if ( model.parent === null ) { + parentConnections.forEach( function ( connection ) { - sceneGraph.add( model ); + const parent = modelMap.get( connection.ID ); + if ( parent !== undefined ) parent.add( model ); - } + } ); + if ( model.parent === null ) { - } ); + sceneGraph.add( model ); - this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); + } - this.createAmbientLight(); - this.setupMorphMaterials(); + } ); - sceneGraph.traverse( function ( node ) { + this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); - if ( node.userData.transformData ) { + this.createAmbientLight(); - if ( node.parent ) { + this.setupMorphMaterials(); - node.userData.transformData.parentMatrix = node.parent.matrix; - node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld; + sceneGraph.traverse( function ( node ) { - } + if ( node.userData.transformData ) { - var transform = generateTransform( node.userData.transformData ); + if ( node.parent ) { - node.applyMatrix4( transform ); - node.updateWorldMatrix(); + node.userData.transformData.parentMatrix = node.parent.matrix; + node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld; } - } ); + const transform = generateTransform( node.userData.transformData ); - var animations = new AnimationParser().parse(); + node.applyMatrix4( transform ); + node.updateWorldMatrix(); - // if all the models where already combined in a single group, just return that - if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) { + } - sceneGraph.children[ 0 ].animations = animations; - sceneGraph = sceneGraph.children[ 0 ]; + } ); - } + const animations = new AnimationParser().parse(); - sceneGraph.animations = animations; + // if all the models where already combined in a single group, just return that + if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) { - }, + sceneGraph.children[ 0 ].animations = animations; + sceneGraph = sceneGraph.children[ 0 ]; - // parse nodes in FBXTree.Objects.Model - parseModels: function ( skeletons, geometryMap, materialMap ) { + } - var modelMap = new Map(); - var modelNodes = fbxTree.Objects.Model; + sceneGraph.animations = animations; - for ( var nodeID in modelNodes ) { + } - var id = parseInt( nodeID ); - var node = modelNodes[ nodeID ]; - var relationships = connections.get( id ); + // parse nodes in FBXTree.Objects.Model + parseModels( skeletons, geometryMap, materialMap ) { - var model = this.buildSkeleton( relationships, skeletons, id, node.attrName ); + const modelMap = new Map(); + const modelNodes = fbxTree.Objects.Model; - if ( ! model ) { + for ( const nodeID in modelNodes ) { - switch ( node.attrType ) { + const id = parseInt( nodeID ); + const node = modelNodes[ nodeID ]; + const relationships = connections.get( id ); - case 'Camera': - model = this.createCamera( relationships ); - break; - case 'Light': - model = this.createLight( relationships ); - break; - case 'Mesh': - model = this.createMesh( relationships, geometryMap, materialMap ); - break; - case 'NurbsCurve': - model = this.createCurve( relationships, geometryMap ); - break; - case 'LimbNode': - case 'Root': - model = new Bone(); - break; - case 'Null': - default: - model = new Group(); - break; + let model = this.buildSkeleton( relationships, skeletons, id, node.attrName ); - } + if ( ! model ) { - model.name = node.attrName ? PropertyBinding.sanitizeNodeName( node.attrName ) : ''; + switch ( node.attrType ) { - model.ID = id; + case 'Camera': + model = this.createCamera( relationships ); + break; + case 'Light': + model = this.createLight( relationships ); + break; + case 'Mesh': + model = this.createMesh( relationships, geometryMap, materialMap ); + break; + case 'NurbsCurve': + model = this.createCurve( relationships, geometryMap ); + break; + case 'LimbNode': + case 'Root': + model = new Bone(); + break; + case 'Null': + default: + model = new Group(); + break; } - this.getTransformData( model, node ); - modelMap.set( id, model ); + model.name = node.attrName ? PropertyBinding.sanitizeNodeName( node.attrName ) : ''; + + model.ID = id; } - return modelMap; + this.getTransformData( model, node ); + modelMap.set( id, model ); - }, + } - buildSkeleton: function ( relationships, skeletons, id, name ) { + return modelMap; - var bone = null; + } - relationships.parents.forEach( function ( parent ) { + buildSkeleton( relationships, skeletons, id, name ) { - for ( var ID in skeletons ) { + let bone = null; - var skeleton = skeletons[ ID ]; + relationships.parents.forEach( function ( parent ) { - skeleton.rawBones.forEach( function ( rawBone, i ) { + for ( const ID in skeletons ) { - if ( rawBone.ID === parent.ID ) { + const skeleton = skeletons[ ID ]; - var subBone = bone; - bone = new Bone(); + skeleton.rawBones.forEach( function ( rawBone, i ) { - bone.matrixWorld.copy( rawBone.transformLink ); + if ( rawBone.ID === parent.ID ) { - // set name and id here - otherwise in cases where "subBone" is created it will not have a name / id + const subBone = bone; + bone = new Bone(); - bone.name = name ? PropertyBinding.sanitizeNodeName( name ) : ''; - bone.ID = id; + bone.matrixWorld.copy( rawBone.transformLink ); - skeleton.bones[ i ] = bone; + // set name and id here - otherwise in cases where "subBone" is created it will not have a name / id - // In cases where a bone is shared between multiple meshes - // duplicate the bone here and and it as a child of the first bone - if ( subBone !== null ) { + bone.name = name ? PropertyBinding.sanitizeNodeName( name ) : ''; + bone.ID = id; - bone.add( subBone ); + skeleton.bones[ i ] = bone; - } + // In cases where a bone is shared between multiple meshes + // duplicate the bone here and and it as a child of the first bone + if ( subBone !== null ) { + + bone.add( subBone ); } - } ); + } - } + } ); - } ); + } - return bone; + } ); - }, + return bone; - // create a PerspectiveCamera or OrthographicCamera - createCamera: function ( relationships ) { + } - var model; - var cameraAttribute; + // create a PerspectiveCamera or OrthographicCamera + createCamera( relationships ) { - relationships.children.forEach( function ( child ) { + let model; + let cameraAttribute; - var attr = fbxTree.Objects.NodeAttribute[ child.ID ]; + relationships.children.forEach( function ( child ) { - if ( attr !== undefined ) { + const attr = fbxTree.Objects.NodeAttribute[ child.ID ]; - cameraAttribute = attr; + if ( attr !== undefined ) { - } + cameraAttribute = attr; - } ); + } - if ( cameraAttribute === undefined ) { + } ); - model = new Object3D(); + if ( cameraAttribute === undefined ) { - } else { + model = new Object3D(); - var type = 0; - if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { + } else { - type = 1; + let type = 0; + if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { - } + type = 1; - var nearClippingPlane = 1; - if ( cameraAttribute.NearPlane !== undefined ) { + } - nearClippingPlane = cameraAttribute.NearPlane.value / 1000; + let nearClippingPlane = 1; + if ( cameraAttribute.NearPlane !== undefined ) { - } + nearClippingPlane = cameraAttribute.NearPlane.value / 1000; - var farClippingPlane = 1000; - if ( cameraAttribute.FarPlane !== undefined ) { + } - farClippingPlane = cameraAttribute.FarPlane.value / 1000; + let farClippingPlane = 1000; + if ( cameraAttribute.FarPlane !== undefined ) { - } + farClippingPlane = cameraAttribute.FarPlane.value / 1000; + } - var width = window.innerWidth; - var height = window.innerHeight; - if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { + let width = window.innerWidth; + let height = window.innerHeight; - width = cameraAttribute.AspectWidth.value; - height = cameraAttribute.AspectHeight.value; + if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { - } + width = cameraAttribute.AspectWidth.value; + height = cameraAttribute.AspectHeight.value; - var aspect = width / height; + } - var fov = 45; - if ( cameraAttribute.FieldOfView !== undefined ) { + const aspect = width / height; - fov = cameraAttribute.FieldOfView.value; + let fov = 45; + if ( cameraAttribute.FieldOfView !== undefined ) { - } + fov = cameraAttribute.FieldOfView.value; - var focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null; + } - switch ( type ) { + const focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null; - case 0: // Perspective - model = new PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); - if ( focalLength !== null ) model.setFocalLength( focalLength ); - break; + switch ( type ) { - case 1: // Orthographic - model = new OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); - break; + case 0: // Perspective + model = new PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); + if ( focalLength !== null ) model.setFocalLength( focalLength ); + break; - default: - console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); - model = new Object3D(); - break; + case 1: // Orthographic + model = new OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); + break; - } + default: + console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); + model = new Object3D(); + break; } - return model; + } - }, + return model; - // Create a DirectionalLight, PointLight or SpotLight - createLight: function ( relationships ) { + } - var model; - var lightAttribute; + // Create a DirectionalLight, PointLight or SpotLight + createLight( relationships ) { - relationships.children.forEach( function ( child ) { + let model; + let lightAttribute; - var attr = fbxTree.Objects.NodeAttribute[ child.ID ]; + relationships.children.forEach( function ( child ) { - if ( attr !== undefined ) { + const attr = fbxTree.Objects.NodeAttribute[ child.ID ]; - lightAttribute = attr; + if ( attr !== undefined ) { - } + lightAttribute = attr; - } ); + } - if ( lightAttribute === undefined ) { + } ); - model = new Object3D(); + if ( lightAttribute === undefined ) { - } else { + model = new Object3D(); - var type; + } else { - // LightType can be undefined for Point lights - if ( lightAttribute.LightType === undefined ) { + let type; - type = 0; + // LightType can be undefined for Point lights + if ( lightAttribute.LightType === undefined ) { - } else { + type = 0; - type = lightAttribute.LightType.value; + } else { - } + type = lightAttribute.LightType.value; - var color = 0xffffff; + } - if ( lightAttribute.Color !== undefined ) { + let color = 0xffffff; - color = new Color().fromArray( lightAttribute.Color.value ); + if ( lightAttribute.Color !== undefined ) { - } + color = new Color().fromArray( lightAttribute.Color.value ); - var intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100; + } - // light disabled - if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { + let intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100; - intensity = 0; + // light disabled + if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { - } + intensity = 0; - var distance = 0; - if ( lightAttribute.FarAttenuationEnd !== undefined ) { + } - if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { + let distance = 0; + if ( lightAttribute.FarAttenuationEnd !== undefined ) { - distance = 0; + if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { - } else { + distance = 0; - distance = lightAttribute.FarAttenuationEnd.value; + } else { - } + distance = lightAttribute.FarAttenuationEnd.value; } - // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? - var decay = 1; + } - switch ( type ) { + // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? + const decay = 1; - case 0: // Point - model = new PointLight( color, intensity, distance, decay ); - break; + switch ( type ) { - case 1: // Directional - model = new DirectionalLight( color, intensity ); - break; + case 0: // Point + model = new PointLight( color, intensity, distance, decay ); + break; - case 2: // Spot - var angle = Math.PI / 3; + case 1: // Directional + model = new DirectionalLight( color, intensity ); + break; - if ( lightAttribute.InnerAngle !== undefined ) { + case 2: // Spot + let angle = Math.PI / 3; - angle = MathUtils.degToRad( lightAttribute.InnerAngle.value ); + if ( lightAttribute.InnerAngle !== undefined ) { - } + angle = MathUtils.degToRad( lightAttribute.InnerAngle.value ); - var penumbra = 0; - if ( lightAttribute.OuterAngle !== undefined ) { + } - // TODO: this is not correct - FBX calculates outer and inner angle in degrees - // with OuterAngle > InnerAngle && OuterAngle <= Math.PI - // while three.js uses a penumbra between (0, 1) to attenuate the inner angle - penumbra = MathUtils.degToRad( lightAttribute.OuterAngle.value ); - penumbra = Math.max( penumbra, 1 ); + let penumbra = 0; + if ( lightAttribute.OuterAngle !== undefined ) { - } + // TODO: this is not correct - FBX calculates outer and inner angle in degrees + // with OuterAngle > InnerAngle && OuterAngle <= Math.PI + // while three.js uses a penumbra between (0, 1) to attenuate the inner angle + penumbra = MathUtils.degToRad( lightAttribute.OuterAngle.value ); + penumbra = Math.max( penumbra, 1 ); - model = new SpotLight( color, intensity, distance, angle, penumbra, decay ); - break; + } - default: - console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a PointLight.' ); - model = new PointLight( color, intensity ); - break; + model = new SpotLight( color, intensity, distance, angle, penumbra, decay ); + break; - } + default: + console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a PointLight.' ); + model = new PointLight( color, intensity ); + break; - if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { + } - model.castShadow = true; + if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { - } + model.castShadow = true; } - return model; + } - }, + return model; - createMesh: function ( relationships, geometryMap, materialMap ) { + } - var model; - var geometry = null; - var material = null; - var materials = []; + createMesh( relationships, geometryMap, materialMap ) { - // get geometry and materials(s) from connections - relationships.children.forEach( function ( child ) { + let model; + let geometry = null; + let material = null; + const materials = []; - if ( geometryMap.has( child.ID ) ) { + // get geometry and materials(s) from connections + relationships.children.forEach( function ( child ) { - geometry = geometryMap.get( child.ID ); + if ( geometryMap.has( child.ID ) ) { - } + geometry = geometryMap.get( child.ID ); - if ( materialMap.has( child.ID ) ) { + } - materials.push( materialMap.get( child.ID ) ); + if ( materialMap.has( child.ID ) ) { - } + materials.push( materialMap.get( child.ID ) ); - } ); + } - if ( materials.length > 1 ) { + } ); - material = materials; + if ( materials.length > 1 ) { - } else if ( materials.length > 0 ) { + material = materials; - material = materials[ 0 ]; + } else if ( materials.length > 0 ) { - } else { + material = materials[ 0 ]; - material = new MeshPhongMaterial( { color: 0xcccccc } ); - materials.push( material ); + } else { - } + material = new MeshPhongMaterial( { color: 0xcccccc } ); + materials.push( material ); - if ( 'color' in geometry.attributes ) { + } - materials.forEach( function ( material ) { + if ( 'color' in geometry.attributes ) { - material.vertexColors = true; + materials.forEach( function ( material ) { - } ); + material.vertexColors = true; - } + } ); - if ( geometry.FBX_Deformer ) { + } - materials.forEach( function ( material ) { + if ( geometry.FBX_Deformer ) { - material.skinning = true; + materials.forEach( function ( material ) { - } ); + material.skinning = true; - model = new SkinnedMesh( geometry, material ); - model.normalizeSkinWeights(); + } ); - } else { + model = new SkinnedMesh( geometry, material ); + model.normalizeSkinWeights(); - model = new Mesh( geometry, material ); + } else { - } + model = new Mesh( geometry, material ); - return model; + } - }, + return model; - createCurve: function ( relationships, geometryMap ) { + } - var geometry = relationships.children.reduce( function ( geo, child ) { + createCurve( relationships, geometryMap ) { - if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); + const geometry = relationships.children.reduce( function ( geo, child ) { - return geo; + if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); - }, null ); + return geo; - // FBX does not list materials for Nurbs lines, so we'll just put our own in here. - var material = new LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } ); - return new Line( geometry, material ); + }, null ); - }, + // FBX does not list materials for Nurbs lines, so we'll just put our own in here. + const material = new LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } ); + return new Line( geometry, material ); - // parse the model node for transform data - getTransformData: function ( model, modelNode ) { + } - var transformData = {}; + // parse the model node for transform data + getTransformData( model, modelNode ) { - if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); + const transformData = {}; - if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); - else transformData.eulerOrder = 'ZYX'; + if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); - if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; + if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); + else transformData.eulerOrder = 'ZYX'; - if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; - if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; - if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; + if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; - if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; + if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; + if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; + if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; - if ( 'ScalingOffset' in modelNode ) transformData.scalingOffset = modelNode.ScalingOffset.value; - if ( 'ScalingPivot' in modelNode ) transformData.scalingPivot = modelNode.ScalingPivot.value; + if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; - if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; - if ( 'RotationPivot' in modelNode ) transformData.rotationPivot = modelNode.RotationPivot.value; + if ( 'ScalingOffset' in modelNode ) transformData.scalingOffset = modelNode.ScalingOffset.value; + if ( 'ScalingPivot' in modelNode ) transformData.scalingPivot = modelNode.ScalingPivot.value; - model.userData.transformData = transformData; + if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; + if ( 'RotationPivot' in modelNode ) transformData.rotationPivot = modelNode.RotationPivot.value; - }, + model.userData.transformData = transformData; - setLookAtProperties: function ( model, modelNode ) { + } - if ( 'LookAtProperty' in modelNode ) { + setLookAtProperties( model, modelNode ) { - var children = connections.get( model.ID ).children; + if ( 'LookAtProperty' in modelNode ) { - children.forEach( function ( child ) { + const children = connections.get( model.ID ).children; - if ( child.relationship === 'LookAtProperty' ) { + children.forEach( function ( child ) { - var lookAtTarget = fbxTree.Objects.Model[ child.ID ]; + if ( child.relationship === 'LookAtProperty' ) { - if ( 'Lcl_Translation' in lookAtTarget ) { + const lookAtTarget = fbxTree.Objects.Model[ child.ID ]; - var pos = lookAtTarget.Lcl_Translation.value; + if ( 'Lcl_Translation' in lookAtTarget ) { - // DirectionalLight, SpotLight - if ( model.target !== undefined ) { + const pos = lookAtTarget.Lcl_Translation.value; - model.target.position.fromArray( pos ); - sceneGraph.add( model.target ); + // DirectionalLight, SpotLight + if ( model.target !== undefined ) { - } else { // Cameras and other Object3Ds + model.target.position.fromArray( pos ); + sceneGraph.add( model.target ); - model.lookAt( new Vector3().fromArray( pos ) ); + } else { // Cameras and other Object3Ds - } + model.lookAt( new Vector3().fromArray( pos ) ); } } - } ); + } - } + } ); - }, + } - bindSkeleton: function ( skeletons, geometryMap, modelMap ) { + } - var bindMatrices = this.parsePoseNodes(); + bindSkeleton( skeletons, geometryMap, modelMap ) { - for ( var ID in skeletons ) { + const bindMatrices = this.parsePoseNodes(); - var skeleton = skeletons[ ID ]; + for ( const ID in skeletons ) { - var parents = connections.get( parseInt( skeleton.ID ) ).parents; + const skeleton = skeletons[ ID ]; - parents.forEach( function ( parent ) { + const parents = connections.get( parseInt( skeleton.ID ) ).parents; - if ( geometryMap.has( parent.ID ) ) { + parents.forEach( function ( parent ) { - var geoID = parent.ID; - var geoRelationships = connections.get( geoID ); + if ( geometryMap.has( parent.ID ) ) { - geoRelationships.parents.forEach( function ( geoConnParent ) { + const geoID = parent.ID; + const geoRelationships = connections.get( geoID ); - if ( modelMap.has( geoConnParent.ID ) ) { + geoRelationships.parents.forEach( function ( geoConnParent ) { - var model = modelMap.get( geoConnParent.ID ); + if ( modelMap.has( geoConnParent.ID ) ) { - model.bind( new Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); + const model = modelMap.get( geoConnParent.ID ); - } + model.bind( new Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); - } ); + } - } + } ); - } ); + } - } + } ); - }, + } - parsePoseNodes: function () { + } - var bindMatrices = {}; + parsePoseNodes() { - if ( 'Pose' in fbxTree.Objects ) { + const bindMatrices = {}; - var BindPoseNode = fbxTree.Objects.Pose; + if ( 'Pose' in fbxTree.Objects ) { - for ( var nodeID in BindPoseNode ) { + const BindPoseNode = fbxTree.Objects.Pose; - if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { + for ( const nodeID in BindPoseNode ) { - var poseNodes = BindPoseNode[ nodeID ].PoseNode; + if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { - if ( Array.isArray( poseNodes ) ) { + const poseNodes = BindPoseNode[ nodeID ].PoseNode; - poseNodes.forEach( function ( poseNode ) { + if ( Array.isArray( poseNodes ) ) { - bindMatrices[ poseNode.Node ] = new Matrix4().fromArray( poseNode.Matrix.a ); + poseNodes.forEach( function ( poseNode ) { - } ); + bindMatrices[ poseNode.Node ] = new Matrix4().fromArray( poseNode.Matrix.a ); - } else { + } ); - bindMatrices[ poseNodes.Node ] = new Matrix4().fromArray( poseNodes.Matrix.a ); + } else { - } + bindMatrices[ poseNodes.Node ] = new Matrix4().fromArray( poseNodes.Matrix.a ); } @@ -1439,2785 +1431,2762 @@ var FBXLoader = ( function () { } - return bindMatrices; + } - }, + return bindMatrices; - // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light - createAmbientLight: function () { + } - if ( 'GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings ) { + // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light + createAmbientLight() { - var ambientColor = fbxTree.GlobalSettings.AmbientColor.value; - var r = ambientColor[ 0 ]; - var g = ambientColor[ 1 ]; - var b = ambientColor[ 2 ]; + if ( 'GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings ) { - if ( r !== 0 || g !== 0 || b !== 0 ) { + const ambientColor = fbxTree.GlobalSettings.AmbientColor.value; + const r = ambientColor[ 0 ]; + const g = ambientColor[ 1 ]; + const b = ambientColor[ 2 ]; - var color = new Color( r, g, b ); - sceneGraph.add( new AmbientLight( color, 1 ) ); + if ( r !== 0 || g !== 0 || b !== 0 ) { - } + const color = new Color( r, g, b ); + sceneGraph.add( new AmbientLight( color, 1 ) ); } - }, + } - setupMorphMaterials: function () { + } - var scope = this; - sceneGraph.traverse( function ( child ) { + setupMorphMaterials() { - if ( child.isMesh ) { + const scope = this; + sceneGraph.traverse( function ( child ) { - if ( child.geometry.morphAttributes.position && child.geometry.morphAttributes.position.length ) { + if ( child.isMesh ) { - if ( Array.isArray( child.material ) ) { + if ( child.geometry.morphAttributes.position && child.geometry.morphAttributes.position.length ) { - child.material.forEach( function ( material, i ) { + if ( Array.isArray( child.material ) ) { - scope.setupMorphMaterial( child, material, i ); + child.material.forEach( function ( material, i ) { - } ); + scope.setupMorphMaterial( child, material, i ); - } else { + } ); - scope.setupMorphMaterial( child, child.material ); + } else { - } + scope.setupMorphMaterial( child, child.material ); } } - } ); - - }, - - setupMorphMaterial: function ( child, material, index ) { + } - var uuid = child.uuid; - var matUuid = material.uuid; + } ); - // if a geometry has morph targets, it cannot share the material with other geometries - var sharedMat = false; + } - sceneGraph.traverse( function ( node ) { + setupMorphMaterial( child, material, index ) { - if ( node.isMesh ) { + const uuid = child.uuid; + const matUuid = material.uuid; - if ( Array.isArray( node.material ) ) { + // if a geometry has morph targets, it cannot share the material with other geometries + let sharedMat = false; - node.material.forEach( function ( mat ) { + sceneGraph.traverse( function ( node ) { - if ( mat.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; + if ( node.isMesh ) { - } ); + if ( Array.isArray( node.material ) ) { - } else if ( node.material.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; + node.material.forEach( function ( mat ) { - } + if ( mat.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; - } ); + } ); - if ( sharedMat === true ) { + } else if ( node.material.uuid === matUuid && node.uuid !== uuid ) sharedMat = true; - var clonedMat = material.clone(); - clonedMat.morphTargets = true; + } - if ( index === undefined ) child.material = clonedMat; - else child.material[ index ] = clonedMat; + } ); - } else material.morphTargets = true; + if ( sharedMat === true ) { - } + const clonedMat = material.clone(); + clonedMat.morphTargets = true; - }; + if ( index === undefined ) child.material = clonedMat; + else child.material[ index ] = clonedMat; - // parse Geometry data from FBXTree and return map of BufferGeometries - function GeometryParser() {} + } else material.morphTargets = true; - GeometryParser.prototype = { + } - constructor: GeometryParser, +} - // Parse nodes in FBXTree.Objects.Geometry - parse: function ( deformers ) { +// parse Geometry data from FBXTree and return map of BufferGeometries +class GeometryParser { - var geometryMap = new Map(); + // Parse nodes in FBXTree.Objects.Geometry + parse( deformers ) { - if ( 'Geometry' in fbxTree.Objects ) { + const geometryMap = new Map(); - var geoNodes = fbxTree.Objects.Geometry; + if ( 'Geometry' in fbxTree.Objects ) { - for ( var nodeID in geoNodes ) { + const geoNodes = fbxTree.Objects.Geometry; - var relationships = connections.get( parseInt( nodeID ) ); - var geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers ); + for ( const nodeID in geoNodes ) { - geometryMap.set( parseInt( nodeID ), geo ); + const relationships = connections.get( parseInt( nodeID ) ); + const geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers ); - } + geometryMap.set( parseInt( nodeID ), geo ); } - return geometryMap; - - }, + } - // Parse single node in FBXTree.Objects.Geometry - parseGeometry: function ( relationships, geoNode, deformers ) { + return geometryMap; - switch ( geoNode.attrType ) { + } - case 'Mesh': - return this.parseMeshGeometry( relationships, geoNode, deformers ); - break; + // Parse single node in FBXTree.Objects.Geometry + parseGeometry( relationships, geoNode, deformers ) { - case 'NurbsCurve': - return this.parseNurbsGeometry( geoNode ); - break; + switch ( geoNode.attrType ) { - } + case 'Mesh': + return this.parseMeshGeometry( relationships, geoNode, deformers ); + break; - }, + case 'NurbsCurve': + return this.parseNurbsGeometry( geoNode ); + break; + } - // Parse single node mesh geometry in FBXTree.Objects.Geometry - parseMeshGeometry: function ( relationships, geoNode, deformers ) { + } - var skeletons = deformers.skeletons; - var morphTargets = []; + // Parse single node mesh geometry in FBXTree.Objects.Geometry + parseMeshGeometry( relationships, geoNode, deformers ) { - var modelNodes = relationships.parents.map( function ( parent ) { + const skeletons = deformers.skeletons; + const morphTargets = []; - return fbxTree.Objects.Model[ parent.ID ]; + const modelNodes = relationships.parents.map( function ( parent ) { - } ); + return fbxTree.Objects.Model[ parent.ID ]; - // don't create geometry if it is not associated with any models - if ( modelNodes.length === 0 ) return; + } ); - var skeleton = relationships.children.reduce( function ( skeleton, child ) { + // don't create geometry if it is not associated with any models + if ( modelNodes.length === 0 ) return; - if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; + const skeleton = relationships.children.reduce( function ( skeleton, child ) { - return skeleton; + if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; - }, null ); + return skeleton; - relationships.children.forEach( function ( child ) { + }, null ); - if ( deformers.morphTargets[ child.ID ] !== undefined ) { + relationships.children.forEach( function ( child ) { - morphTargets.push( deformers.morphTargets[ child.ID ] ); + if ( deformers.morphTargets[ child.ID ] !== undefined ) { - } + morphTargets.push( deformers.morphTargets[ child.ID ] ); - } ); + } - // Assume one model and get the preRotation from that - // if there is more than one model associated with the geometry this may cause problems - var modelNode = modelNodes[ 0 ]; + } ); - var transformData = {}; + // Assume one model and get the preRotation from that + // if there is more than one model associated with the geometry this may cause problems + const modelNode = modelNodes[ 0 ]; - if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); - if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); + const transformData = {}; - if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value; - if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value; - if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value; + if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value ); + if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value ); - var transform = generateTransform( transformData ); + if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value; + if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value; + if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value; - return this.genGeometry( geoNode, skeleton, morphTargets, transform ); + const transform = generateTransform( transformData ); - }, + return this.genGeometry( geoNode, skeleton, morphTargets, transform ); - // Generate a BufferGeometry from a node in FBXTree.Objects.Geometry - genGeometry: function ( geoNode, skeleton, morphTargets, preTransform ) { + } - var geo = new BufferGeometry(); - if ( geoNode.attrName ) geo.name = geoNode.attrName; + // Generate a BufferGeometry from a node in FBXTree.Objects.Geometry + genGeometry( geoNode, skeleton, morphTargets, preTransform ) { - var geoInfo = this.parseGeoNode( geoNode, skeleton ); - var buffers = this.genBuffers( geoInfo ); + const geo = new BufferGeometry(); + if ( geoNode.attrName ) geo.name = geoNode.attrName; - var positionAttribute = new Float32BufferAttribute( buffers.vertex, 3 ); + const geoInfo = this.parseGeoNode( geoNode, skeleton ); + const buffers = this.genBuffers( geoInfo ); - positionAttribute.applyMatrix4( preTransform ); + const positionAttribute = new Float32BufferAttribute( buffers.vertex, 3 ); - geo.setAttribute( 'position', positionAttribute ); + positionAttribute.applyMatrix4( preTransform ); - if ( buffers.colors.length > 0 ) { + geo.setAttribute( 'position', positionAttribute ); - geo.setAttribute( 'color', new Float32BufferAttribute( buffers.colors, 3 ) ); + if ( buffers.colors.length > 0 ) { - } + geo.setAttribute( 'color', new Float32BufferAttribute( buffers.colors, 3 ) ); - if ( skeleton ) { + } - geo.setAttribute( 'skinIndex', new Uint16BufferAttribute( buffers.weightsIndices, 4 ) ); + if ( skeleton ) { - geo.setAttribute( 'skinWeight', new Float32BufferAttribute( buffers.vertexWeights, 4 ) ); + geo.setAttribute( 'skinIndex', new Uint16BufferAttribute( buffers.weightsIndices, 4 ) ); - // used later to bind the skeleton to the model - geo.FBX_Deformer = skeleton; + geo.setAttribute( 'skinWeight', new Float32BufferAttribute( buffers.vertexWeights, 4 ) ); - } + // used later to bind the skeleton to the model + geo.FBX_Deformer = skeleton; - if ( buffers.normal.length > 0 ) { + } - var normalMatrix = new Matrix3().getNormalMatrix( preTransform ); + if ( buffers.normal.length > 0 ) { - var normalAttribute = new Float32BufferAttribute( buffers.normal, 3 ); - normalAttribute.applyNormalMatrix( normalMatrix ); + const normalMatrix = new Matrix3().getNormalMatrix( preTransform ); - geo.setAttribute( 'normal', normalAttribute ); + const normalAttribute = new Float32BufferAttribute( buffers.normal, 3 ); + normalAttribute.applyNormalMatrix( normalMatrix ); - } + geo.setAttribute( 'normal', normalAttribute ); - buffers.uvs.forEach( function ( uvBuffer, i ) { + } - // subsequent uv buffers are called 'uv1', 'uv2', ... - var name = 'uv' + ( i + 1 ).toString(); + buffers.uvs.forEach( function ( uvBuffer, i ) { - // the first uv buffer is just called 'uv' - if ( i === 0 ) { + // subsequent uv buffers are called 'uv1', 'uv2', ... + let name = 'uv' + ( i + 1 ).toString(); - name = 'uv'; + // the first uv buffer is just called 'uv' + if ( i === 0 ) { - } + name = 'uv'; - geo.setAttribute( name, new Float32BufferAttribute( buffers.uvs[ i ], 2 ) ); + } - } ); + geo.setAttribute( name, new Float32BufferAttribute( buffers.uvs[ i ], 2 ) ); - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { + } ); - // Convert the material indices of each vertex into rendering groups on the geometry. - var prevMaterialIndex = buffers.materialIndex[ 0 ]; - var startIndex = 0; + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - buffers.materialIndex.forEach( function ( currentIndex, i ) { + // Convert the material indices of each vertex into rendering groups on the geometry. + let prevMaterialIndex = buffers.materialIndex[ 0 ]; + let startIndex = 0; - if ( currentIndex !== prevMaterialIndex ) { + buffers.materialIndex.forEach( function ( currentIndex, i ) { - geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); + if ( currentIndex !== prevMaterialIndex ) { - prevMaterialIndex = currentIndex; - startIndex = i; + geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); - } + prevMaterialIndex = currentIndex; + startIndex = i; - } ); + } - // the loop above doesn't add the last group, do that here. - if ( geo.groups.length > 0 ) { + } ); - var lastGroup = geo.groups[ geo.groups.length - 1 ]; - var lastIndex = lastGroup.start + lastGroup.count; + // the loop above doesn't add the last group, do that here. + if ( geo.groups.length > 0 ) { - if ( lastIndex !== buffers.materialIndex.length ) { + const lastGroup = geo.groups[ geo.groups.length - 1 ]; + const lastIndex = lastGroup.start + lastGroup.count; - geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex ); + if ( lastIndex !== buffers.materialIndex.length ) { - } + geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex ); } - // case where there are multiple materials but the whole geometry is only - // using one of them - if ( geo.groups.length === 0 ) { + } - geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] ); + // case where there are multiple materials but the whole geometry is only + // using one of them + if ( geo.groups.length === 0 ) { - } + geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] ); } - this.addMorphTargets( geo, geoNode, morphTargets, preTransform ); - - return geo; + } - }, + this.addMorphTargets( geo, geoNode, morphTargets, preTransform ); - parseGeoNode: function ( geoNode, skeleton ) { + return geo; - var geoInfo = {}; + } - geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : []; - geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : []; + parseGeoNode( geoNode, skeleton ) { - if ( geoNode.LayerElementColor ) { + const geoInfo = {}; - geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] ); + geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : []; + geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : []; - } + if ( geoNode.LayerElementColor ) { - if ( geoNode.LayerElementMaterial ) { + geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] ); - geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] ); + } - } + if ( geoNode.LayerElementMaterial ) { - if ( geoNode.LayerElementNormal ) { + geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] ); - geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] ); + } - } + if ( geoNode.LayerElementNormal ) { - if ( geoNode.LayerElementUV ) { + geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] ); - geoInfo.uv = []; + } - var i = 0; - while ( geoNode.LayerElementUV[ i ] ) { + if ( geoNode.LayerElementUV ) { - if ( geoNode.LayerElementUV[ i ].UV ) { + geoInfo.uv = []; - geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) ); + let i = 0; + while ( geoNode.LayerElementUV[ i ] ) { - } + if ( geoNode.LayerElementUV[ i ].UV ) { - i ++; + geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) ); } + i ++; + } - geoInfo.weightTable = {}; + } - if ( skeleton !== null ) { + geoInfo.weightTable = {}; - geoInfo.skeleton = skeleton; + if ( skeleton !== null ) { - skeleton.rawBones.forEach( function ( rawBone, i ) { + geoInfo.skeleton = skeleton; - // loop over the bone's vertex indices and weights - rawBone.indices.forEach( function ( index, j ) { + skeleton.rawBones.forEach( function ( rawBone, i ) { - if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = []; + // loop over the bone's vertex indices and weights + rawBone.indices.forEach( function ( index, j ) { - geoInfo.weightTable[ index ].push( { + if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = []; - id: i, - weight: rawBone.weights[ j ], + geoInfo.weightTable[ index ].push( { - } ); + id: i, + weight: rawBone.weights[ j ], } ); } ); - } - - return geoInfo; - - }, + } ); - genBuffers: function ( geoInfo ) { + } - var buffers = { - vertex: [], - normal: [], - colors: [], - uvs: [], - materialIndex: [], - vertexWeights: [], - weightsIndices: [], - }; + return geoInfo; - var polygonIndex = 0; - var faceLength = 0; - var displayedWeightsWarning = false; + } - // these will hold data for a single face - var facePositionIndexes = []; - var faceNormals = []; - var faceColors = []; - var faceUVs = []; - var faceWeights = []; - var faceWeightIndices = []; + genBuffers( geoInfo ) { + + const buffers = { + vertex: [], + normal: [], + colors: [], + uvs: [], + materialIndex: [], + vertexWeights: [], + weightsIndices: [], + }; + + let polygonIndex = 0; + let faceLength = 0; + let displayedWeightsWarning = false; + + // these will hold data for a single face + let facePositionIndexes = []; + let faceNormals = []; + let faceColors = []; + let faceUVs = []; + let faceWeights = []; + let faceWeightIndices = []; + + const scope = this; + geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { + + let materialIndex; + let endOfFace = false; + + // Face index and vertex index arrays are combined in a single array + // A cube with quad faces looks like this: + // PolygonVertexIndex: *24 { + // a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5 + // } + // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3 + // to find index of last vertex bit shift the index: ^ - 1 + if ( vertexIndex < 0 ) { + + vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 + endOfFace = true; - var scope = this; - geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { + } - var endOfFace = false; + let weightIndices = []; + let weights = []; - // Face index and vertex index arrays are combined in a single array - // A cube with quad faces looks like this: - // PolygonVertexIndex: *24 { - // a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5 - // } - // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3 - // to find index of last vertex bit shift the index: ^ - 1 - if ( vertexIndex < 0 ) { + facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); - vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 - endOfFace = true; + if ( geoInfo.color ) { - } + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color ); - var weightIndices = []; - var weights = []; + faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); + } - if ( geoInfo.color ) { + if ( geoInfo.skeleton ) { - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color ); + if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) { - faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); + geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) { - } + weights.push( wt.weight ); + weightIndices.push( wt.id ); - if ( geoInfo.skeleton ) { + } ); - if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) { - geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) { + } - weights.push( wt.weight ); - weightIndices.push( wt.id ); + if ( weights.length > 4 ) { - } ); + if ( ! displayedWeightsWarning ) { + console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); + displayedWeightsWarning = true; } - if ( weights.length > 4 ) { - - if ( ! displayedWeightsWarning ) { - - console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); - displayedWeightsWarning = true; - - } - - var wIndex = [ 0, 0, 0, 0 ]; - var Weight = [ 0, 0, 0, 0 ]; + const wIndex = [ 0, 0, 0, 0 ]; + const Weight = [ 0, 0, 0, 0 ]; - weights.forEach( function ( weight, weightIndex ) { + weights.forEach( function ( weight, weightIndex ) { - var currentWeight = weight; - var currentIndex = weightIndices[ weightIndex ]; + let currentWeight = weight; + let currentIndex = weightIndices[ weightIndex ]; - Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { + Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { - if ( currentWeight > comparedWeight ) { + if ( currentWeight > comparedWeight ) { - comparedWeightArray[ comparedWeightIndex ] = currentWeight; - currentWeight = comparedWeight; + comparedWeightArray[ comparedWeightIndex ] = currentWeight; + currentWeight = comparedWeight; - var tmp = wIndex[ comparedWeightIndex ]; - wIndex[ comparedWeightIndex ] = currentIndex; - currentIndex = tmp; + const tmp = wIndex[ comparedWeightIndex ]; + wIndex[ comparedWeightIndex ] = currentIndex; + currentIndex = tmp; - } - - } ); + } } ); - weightIndices = wIndex; - weights = Weight; + } ); - } + weightIndices = wIndex; + weights = Weight; - // if the weight array is shorter than 4 pad with 0s - while ( weights.length < 4 ) { + } - weights.push( 0 ); - weightIndices.push( 0 ); + // if the weight array is shorter than 4 pad with 0s + while ( weights.length < 4 ) { - } + weights.push( 0 ); + weightIndices.push( 0 ); - for ( var i = 0; i < 4; ++ i ) { + } - faceWeights.push( weights[ i ] ); - faceWeightIndices.push( weightIndices[ i ] ); + for ( let i = 0; i < 4; ++ i ) { - } + faceWeights.push( weights[ i ] ); + faceWeightIndices.push( weightIndices[ i ] ); } - if ( geoInfo.normal ) { + } - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal ); + if ( geoInfo.normal ) { - faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal ); - } + faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { + } - var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ]; + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - } + materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ]; - if ( geoInfo.uv ) { + } - geoInfo.uv.forEach( function ( uv, i ) { + if ( geoInfo.uv ) { - var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); + geoInfo.uv.forEach( function ( uv, i ) { - if ( faceUVs[ i ] === undefined ) { + const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); - faceUVs[ i ] = []; + if ( faceUVs[ i ] === undefined ) { - } + faceUVs[ i ] = []; - faceUVs[ i ].push( data[ 0 ] ); - faceUVs[ i ].push( data[ 1 ] ); + } - } ); + faceUVs[ i ].push( data[ 0 ] ); + faceUVs[ i ].push( data[ 1 ] ); - } + } ); - faceLength ++; + } - if ( endOfFace ) { + faceLength ++; - scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); + if ( endOfFace ) { - polygonIndex ++; - faceLength = 0; + scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); - // reset arrays for the next face - facePositionIndexes = []; - faceNormals = []; - faceColors = []; - faceUVs = []; - faceWeights = []; - faceWeightIndices = []; + polygonIndex ++; + faceLength = 0; - } + // reset arrays for the next face + facePositionIndexes = []; + faceNormals = []; + faceColors = []; + faceUVs = []; + faceWeights = []; + faceWeightIndices = []; - } ); + } - return buffers; + } ); - }, + return buffers; - // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris - genFace: function ( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { + } - for ( var i = 2; i < faceLength; i ++ ) { + // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris + genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); + for ( let i = 2; i < faceLength; i ++ ) { - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); - buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); - if ( geoInfo.skeleton ) { + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); + buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); - buffers.vertexWeights.push( faceWeights[ 0 ] ); - buffers.vertexWeights.push( faceWeights[ 1 ] ); - buffers.vertexWeights.push( faceWeights[ 2 ] ); - buffers.vertexWeights.push( faceWeights[ 3 ] ); + if ( geoInfo.skeleton ) { - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); + buffers.vertexWeights.push( faceWeights[ 0 ] ); + buffers.vertexWeights.push( faceWeights[ 1 ] ); + buffers.vertexWeights.push( faceWeights[ 2 ] ); + buffers.vertexWeights.push( faceWeights[ 3 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); - buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); + buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); - buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); - } + buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); + buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); - if ( geoInfo.color ) { + } - buffers.colors.push( faceColors[ 0 ] ); - buffers.colors.push( faceColors[ 1 ] ); - buffers.colors.push( faceColors[ 2 ] ); + if ( geoInfo.color ) { - buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); - buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); + buffers.colors.push( faceColors[ 0 ] ); + buffers.colors.push( faceColors[ 1 ] ); + buffers.colors.push( faceColors[ 2 ] ); - buffers.colors.push( faceColors[ i * 3 ] ); - buffers.colors.push( faceColors[ i * 3 + 1 ] ); - buffers.colors.push( faceColors[ i * 3 + 2 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); + buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); - } + buffers.colors.push( faceColors[ i * 3 ] ); + buffers.colors.push( faceColors[ i * 3 + 1 ] ); + buffers.colors.push( faceColors[ i * 3 + 2 ] ); - if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { + } - buffers.materialIndex.push( materialIndex ); - buffers.materialIndex.push( materialIndex ); - buffers.materialIndex.push( materialIndex ); + if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { - } + buffers.materialIndex.push( materialIndex ); + buffers.materialIndex.push( materialIndex ); + buffers.materialIndex.push( materialIndex ); - if ( geoInfo.normal ) { + } - buffers.normal.push( faceNormals[ 0 ] ); - buffers.normal.push( faceNormals[ 1 ] ); - buffers.normal.push( faceNormals[ 2 ] ); + if ( geoInfo.normal ) { - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); - buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); + buffers.normal.push( faceNormals[ 0 ] ); + buffers.normal.push( faceNormals[ 1 ] ); + buffers.normal.push( faceNormals[ 2 ] ); - buffers.normal.push( faceNormals[ i * 3 ] ); - buffers.normal.push( faceNormals[ i * 3 + 1 ] ); - buffers.normal.push( faceNormals[ i * 3 + 2 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); + buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); - } + buffers.normal.push( faceNormals[ i * 3 ] ); + buffers.normal.push( faceNormals[ i * 3 + 1 ] ); + buffers.normal.push( faceNormals[ i * 3 + 2 ] ); - if ( geoInfo.uv ) { + } - geoInfo.uv.forEach( function ( uv, j ) { + if ( geoInfo.uv ) { - if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; + geoInfo.uv.forEach( function ( uv, j ) { - buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); + if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); - buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); - } ); + buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); + buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); - } + } ); } - }, + } - addMorphTargets: function ( parentGeo, parentGeoNode, morphTargets, preTransform ) { + } - if ( morphTargets.length === 0 ) return; + addMorphTargets( parentGeo, parentGeoNode, morphTargets, preTransform ) { - parentGeo.morphTargetsRelative = true; + if ( morphTargets.length === 0 ) return; - parentGeo.morphAttributes.position = []; - // parentGeo.morphAttributes.normal = []; // not implemented + parentGeo.morphTargetsRelative = true; - var scope = this; - morphTargets.forEach( function ( morphTarget ) { + parentGeo.morphAttributes.position = []; + // parentGeo.morphAttributes.normal = []; // not implemented - morphTarget.rawTargets.forEach( function ( rawTarget ) { + const scope = this; + morphTargets.forEach( function ( morphTarget ) { - var morphGeoNode = fbxTree.Objects.Geometry[ rawTarget.geoID ]; + morphTarget.rawTargets.forEach( function ( rawTarget ) { - if ( morphGeoNode !== undefined ) { + const morphGeoNode = fbxTree.Objects.Geometry[ rawTarget.geoID ]; - scope.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, rawTarget.name ); + if ( morphGeoNode !== undefined ) { - } + scope.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, rawTarget.name ); - } ); + } } ); - }, - - // a morph geometry node is similar to a standard node, and the node is also contained - // in FBXTree.Objects.Geometry, however it can only have attributes for position, normal - // and a special attribute Index defining which vertices of the original geometry are affected - // Normal and position attributes only have data for the vertices that are affected by the morph - genMorphGeometry: function ( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) { - - var vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : []; - - var morphPositionsSparse = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : []; - var indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : []; - - var length = parentGeo.attributes.position.count * 3; - var morphPositions = new Float32Array( length ); - - for ( var i = 0; i < indices.length; i ++ ) { - - var morphIndex = indices[ i ] * 3; + } ); - morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ]; - morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ]; - morphPositions[ morphIndex + 2 ] = morphPositionsSparse[ i * 3 + 2 ]; + } - } + // a morph geometry node is similar to a standard node, and the node is also contained + // in FBXTree.Objects.Geometry, however it can only have attributes for position, normal + // and a special attribute Index defining which vertices of the original geometry are affected + // Normal and position attributes only have data for the vertices that are affected by the morph + genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) { - // TODO: add morph normal support - var morphGeoInfo = { - vertexIndices: vertexIndices, - vertexPositions: morphPositions, + const vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : []; - }; + const morphPositionsSparse = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : []; + const indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : []; - var morphBuffers = this.genBuffers( morphGeoInfo ); + const length = parentGeo.attributes.position.count * 3; + const morphPositions = new Float32Array( length ); - var positionAttribute = new Float32BufferAttribute( morphBuffers.vertex, 3 ); - positionAttribute.name = name || morphGeoNode.attrName; + for ( let i = 0; i < indices.length; i ++ ) { - positionAttribute.applyMatrix4( preTransform ); + const morphIndex = indices[ i ] * 3; - parentGeo.morphAttributes.position.push( positionAttribute ); + morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ]; + morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ]; + morphPositions[ morphIndex + 2 ] = morphPositionsSparse[ i * 3 + 2 ]; - }, + } - // Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists - parseNormals: function ( NormalNode ) { + // TODO: add morph normal support + const morphGeoInfo = { + vertexIndices: vertexIndices, + vertexPositions: morphPositions, - var mappingType = NormalNode.MappingInformationType; - var referenceType = NormalNode.ReferenceInformationType; - var buffer = NormalNode.Normals.a; - var indexBuffer = []; - if ( referenceType === 'IndexToDirect' ) { + }; - if ( 'NormalIndex' in NormalNode ) { + const morphBuffers = this.genBuffers( morphGeoInfo ); - indexBuffer = NormalNode.NormalIndex.a; + const positionAttribute = new Float32BufferAttribute( morphBuffers.vertex, 3 ); + positionAttribute.name = name || morphGeoNode.attrName; - } else if ( 'NormalsIndex' in NormalNode ) { + positionAttribute.applyMatrix4( preTransform ); - indexBuffer = NormalNode.NormalsIndex.a; + parentGeo.morphAttributes.position.push( positionAttribute ); - } + } - } + // Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists + parseNormals( NormalNode ) { - return { - dataSize: 3, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + const mappingType = NormalNode.MappingInformationType; + const referenceType = NormalNode.ReferenceInformationType; + const buffer = NormalNode.Normals.a; + let indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - }, + if ( 'NormalIndex' in NormalNode ) { - // Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists - parseUVs: function ( UVNode ) { + indexBuffer = NormalNode.NormalIndex.a; - var mappingType = UVNode.MappingInformationType; - var referenceType = UVNode.ReferenceInformationType; - var buffer = UVNode.UV.a; - var indexBuffer = []; - if ( referenceType === 'IndexToDirect' ) { + } else if ( 'NormalsIndex' in NormalNode ) { - indexBuffer = UVNode.UVIndex.a; + indexBuffer = NormalNode.NormalsIndex.a; } - return { - dataSize: 2, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + } - }, + return { + dataSize: 3, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - // Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists - parseVertexColors: function ( ColorNode ) { + } - var mappingType = ColorNode.MappingInformationType; - var referenceType = ColorNode.ReferenceInformationType; - var buffer = ColorNode.Colors.a; - var indexBuffer = []; - if ( referenceType === 'IndexToDirect' ) { + // Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists + parseUVs( UVNode ) { - indexBuffer = ColorNode.ColorIndex.a; + const mappingType = UVNode.MappingInformationType; + const referenceType = UVNode.ReferenceInformationType; + const buffer = UVNode.UV.a; + let indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - } + indexBuffer = UVNode.UVIndex.a; - return { - dataSize: 4, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + } - }, + return { + dataSize: 2, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - // Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists - parseMaterialIndices: function ( MaterialNode ) { + } - var mappingType = MaterialNode.MappingInformationType; - var referenceType = MaterialNode.ReferenceInformationType; + // Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists + parseVertexColors( ColorNode ) { - if ( mappingType === 'NoMappingInformation' ) { + const mappingType = ColorNode.MappingInformationType; + const referenceType = ColorNode.ReferenceInformationType; + const buffer = ColorNode.Colors.a; + let indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - return { - dataSize: 1, - buffer: [ 0 ], - indices: [ 0 ], - mappingType: 'AllSame', - referenceType: referenceType - }; + indexBuffer = ColorNode.ColorIndex.a; - } + } - var materialIndexBuffer = MaterialNode.Materials.a; + return { + dataSize: 4, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - // Since materials are stored as indices, there's a bit of a mismatch between FBX and what - // we expect.So we create an intermediate buffer that points to the index in the buffer, - // for conforming with the other functions we've written for other data. - var materialIndices = []; + } - for ( var i = 0; i < materialIndexBuffer.length; ++ i ) { + // Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists + parseMaterialIndices( MaterialNode ) { - materialIndices.push( i ); + const mappingType = MaterialNode.MappingInformationType; + const referenceType = MaterialNode.ReferenceInformationType; - } + if ( mappingType === 'NoMappingInformation' ) { return { dataSize: 1, - buffer: materialIndexBuffer, - indices: materialIndices, - mappingType: mappingType, + buffer: [ 0 ], + indices: [ 0 ], + mappingType: 'AllSame', referenceType: referenceType }; - }, - - // Generate a NurbGeometry from a node in FBXTree.Objects.Geometry - parseNurbsGeometry: function ( geoNode ) { - - if ( NURBSCurve === undefined ) { - - console.error( 'THREE.FBXLoader: The loader relies on NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' ); - return new BufferGeometry(); - - } - - var order = parseInt( geoNode.Order ); - - if ( isNaN( order ) ) { - - console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id ); - return new BufferGeometry(); + } - } + const materialIndexBuffer = MaterialNode.Materials.a; - var degree = order - 1; + // Since materials are stored as indices, there's a bit of a mismatch between FBX and what + // we expect.So we create an intermediate buffer that points to the index in the buffer, + // for conforming with the other functions we've written for other data. + const materialIndices = []; - var knots = geoNode.KnotVector.a; - var controlPoints = []; - var pointsValues = geoNode.Points.a; + for ( let i = 0; i < materialIndexBuffer.length; ++ i ) { - for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) { + materialIndices.push( i ); - controlPoints.push( new Vector4().fromArray( pointsValues, i ) ); + } - } + return { + dataSize: 1, + buffer: materialIndexBuffer, + indices: materialIndices, + mappingType: mappingType, + referenceType: referenceType + }; - var startKnot, endKnot; + } - if ( geoNode.Form === 'Closed' ) { + // Generate a NurbGeometry from a node in FBXTree.Objects.Geometry + parseNurbsGeometry( geoNode ) { - controlPoints.push( controlPoints[ 0 ] ); + if ( NURBSCurve === undefined ) { - } else if ( geoNode.Form === 'Periodic' ) { + console.error( 'THREE.FBXLoader: The loader relies on NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' ); + return new BufferGeometry(); - startKnot = degree; - endKnot = knots.length - 1 - startKnot; + } - for ( var i = 0; i < degree; ++ i ) { + const order = parseInt( geoNode.Order ); - controlPoints.push( controlPoints[ i ] ); + if ( isNaN( order ) ) { - } + console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id ); + return new BufferGeometry(); - } + } - var curve = new NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); - var vertices = curve.getPoints( controlPoints.length * 7 ); + const degree = order - 1; - var positions = new Float32Array( vertices.length * 3 ); + const knots = geoNode.KnotVector.a; + const controlPoints = []; + const pointsValues = geoNode.Points.a; - vertices.forEach( function ( vertex, i ) { + for ( let i = 0, l = pointsValues.length; i < l; i += 4 ) { - vertex.toArray( positions, i * 3 ); + controlPoints.push( new Vector4().fromArray( pointsValues, i ) ); - } ); + } - var geometry = new BufferGeometry(); - geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) ); + let startKnot, endKnot; - return geometry; + if ( geoNode.Form === 'Closed' ) { - }, + controlPoints.push( controlPoints[ 0 ] ); - }; + } else if ( geoNode.Form === 'Periodic' ) { - // parse animation data from FBXTree - function AnimationParser() {} + startKnot = degree; + endKnot = knots.length - 1 - startKnot; - AnimationParser.prototype = { + for ( let i = 0; i < degree; ++ i ) { - constructor: AnimationParser, + controlPoints.push( controlPoints[ i ] ); - // take raw animation clips and turn them into three.js animation clips - parse: function () { + } - var animationClips = []; + } - var rawClips = this.parseClips(); + const curve = new NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); + const vertices = curve.getPoints( controlPoints.length * 7 ); - if ( rawClips !== undefined ) { + const positions = new Float32Array( vertices.length * 3 ); - for ( var key in rawClips ) { + vertices.forEach( function ( vertex, i ) { - var rawClip = rawClips[ key ]; + vertex.toArray( positions, i * 3 ); - var clip = this.addClip( rawClip ); + } ); - animationClips.push( clip ); + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) ); - } + return geometry; - } + } - return animationClips; +} - }, +// parse animation data from FBXTree +class AnimationParser { - parseClips: function () { + // take raw animation clips and turn them into three.js animation clips + parse() { - // since the actual transformation data is stored in FBXTree.Objects.AnimationCurve, - // if this is undefined we can safely assume there are no animations - if ( fbxTree.Objects.AnimationCurve === undefined ) return undefined; + const animationClips = []; - var curveNodesMap = this.parseAnimationCurveNodes(); + const rawClips = this.parseClips(); - this.parseAnimationCurves( curveNodesMap ); + if ( rawClips !== undefined ) { - var layersMap = this.parseAnimationLayers( curveNodesMap ); - var rawClips = this.parseAnimStacks( layersMap ); + for ( const key in rawClips ) { - return rawClips; + const rawClip = rawClips[ key ]; - }, + const clip = this.addClip( rawClip ); - // parse nodes in FBXTree.Objects.AnimationCurveNode - // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) - // and is referenced by an AnimationLayer - parseAnimationCurveNodes: function () { + animationClips.push( clip ); - var rawCurveNodes = fbxTree.Objects.AnimationCurveNode; + } - var curveNodesMap = new Map(); + } - for ( var nodeID in rawCurveNodes ) { + return animationClips; - var rawCurveNode = rawCurveNodes[ nodeID ]; + } - if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) { + parseClips() { - var curveNode = { + // since the actual transformation data is stored in FBXTree.Objects.AnimationCurve, + // if this is undefined we can safely assume there are no animations + if ( fbxTree.Objects.AnimationCurve === undefined ) return undefined; - id: rawCurveNode.id, - attr: rawCurveNode.attrName, - curves: {}, + const curveNodesMap = this.parseAnimationCurveNodes(); - }; + this.parseAnimationCurves( curveNodesMap ); - curveNodesMap.set( curveNode.id, curveNode ); + const layersMap = this.parseAnimationLayers( curveNodesMap ); + const rawClips = this.parseAnimStacks( layersMap ); - } + return rawClips; - } + } - return curveNodesMap; + // parse nodes in FBXTree.Objects.AnimationCurveNode + // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) + // and is referenced by an AnimationLayer + parseAnimationCurveNodes() { - }, + const rawCurveNodes = fbxTree.Objects.AnimationCurveNode; - // parse nodes in FBXTree.Objects.AnimationCurve and connect them up to - // previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated - // axis ( e.g. times and values of x rotation) - parseAnimationCurves: function ( curveNodesMap ) { + const curveNodesMap = new Map(); - var rawCurves = fbxTree.Objects.AnimationCurve; + for ( const nodeID in rawCurveNodes ) { - // TODO: Many values are identical up to roundoff error, but won't be optimised - // e.g. position times: [0, 0.4, 0. 8] - // position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809] - // clearly, this should be optimised to - // times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809] - // this shows up in nearly every FBX file, and generally time array is length > 100 + const rawCurveNode = rawCurveNodes[ nodeID ]; - for ( var nodeID in rawCurves ) { + if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) { - var animationCurve = { + const curveNode = { - id: rawCurves[ nodeID ].id, - times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ), - values: rawCurves[ nodeID ].KeyValueFloat.a, + id: rawCurveNode.id, + attr: rawCurveNode.attrName, + curves: {}, }; - var relationships = connections.get( animationCurve.id ); + curveNodesMap.set( curveNode.id, curveNode ); - if ( relationships !== undefined ) { - - var animationCurveID = relationships.parents[ 0 ].ID; - var animationCurveRelationship = relationships.parents[ 0 ].relationship; + } - if ( animationCurveRelationship.match( /X/ ) ) { + } - curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve; + return curveNodesMap; - } else if ( animationCurveRelationship.match( /Y/ ) ) { + } - curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve; + // parse nodes in FBXTree.Objects.AnimationCurve and connect them up to + // previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated + // axis ( e.g. times and values of x rotation) + parseAnimationCurves( curveNodesMap ) { - } else if ( animationCurveRelationship.match( /Z/ ) ) { + const rawCurves = fbxTree.Objects.AnimationCurve; - curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve; + // TODO: Many values are identical up to roundoff error, but won't be optimised + // e.g. position times: [0, 0.4, 0. 8] + // position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809] + // clearly, this should be optimised to + // times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809] + // this shows up in nearly every FBX file, and generally time array is length > 100 - } else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) { + for ( const nodeID in rawCurves ) { - curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve; + const animationCurve = { - } + id: rawCurves[ nodeID ].id, + times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ), + values: rawCurves[ nodeID ].KeyValueFloat.a, - } + }; - } + const relationships = connections.get( animationCurve.id ); - }, + if ( relationships !== undefined ) { - // parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references - // to various AnimationCurveNodes and is referenced by an AnimationStack node - // note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack - parseAnimationLayers: function ( curveNodesMap ) { + const animationCurveID = relationships.parents[ 0 ].ID; + const animationCurveRelationship = relationships.parents[ 0 ].relationship; - var rawLayers = fbxTree.Objects.AnimationLayer; + if ( animationCurveRelationship.match( /X/ ) ) { - var layersMap = new Map(); + curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve; - for ( var nodeID in rawLayers ) { + } else if ( animationCurveRelationship.match( /Y/ ) ) { - var layerCurveNodes = []; + curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve; - var connection = connections.get( parseInt( nodeID ) ); + } else if ( animationCurveRelationship.match( /Z/ ) ) { - if ( connection !== undefined ) { + curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve; - // all the animationCurveNodes used in the layer - var children = connection.children; + } else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) { - children.forEach( function ( child, i ) { + curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve; - if ( curveNodesMap.has( child.ID ) ) { + } - var curveNode = curveNodesMap.get( child.ID ); + } - // check that the curves are defined for at least one axis, otherwise ignore the curveNode - if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) { + } - if ( layerCurveNodes[ i ] === undefined ) { + } - var modelID = connections.get( child.ID ).parents.filter( function ( parent ) { + // parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references + // to various AnimationCurveNodes and is referenced by an AnimationStack node + // note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack + parseAnimationLayers( curveNodesMap ) { - return parent.relationship !== undefined; + const rawLayers = fbxTree.Objects.AnimationLayer; - } )[ 0 ].ID; + const layersMap = new Map(); - if ( modelID !== undefined ) { + for ( const nodeID in rawLayers ) { - var rawModel = fbxTree.Objects.Model[ modelID.toString() ]; + const layerCurveNodes = []; - if ( rawModel === undefined ) { + const connection = connections.get( parseInt( nodeID ) ); - console.warn( 'THREE.FBXLoader: Encountered a unused curve.', child ); - return; + if ( connection !== undefined ) { - } + // all the animationCurveNodes used in the layer + const children = connection.children; - var node = { + children.forEach( function ( child, i ) { - modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', - ID: rawModel.id, - initialPosition: [ 0, 0, 0 ], - initialRotation: [ 0, 0, 0 ], - initialScale: [ 1, 1, 1 ], + if ( curveNodesMap.has( child.ID ) ) { - }; + const curveNode = curveNodesMap.get( child.ID ); - sceneGraph.traverse( function ( child ) { + // check that the curves are defined for at least one axis, otherwise ignore the curveNode + if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) { - if ( child.ID === rawModel.id ) { + if ( layerCurveNodes[ i ] === undefined ) { - node.transform = child.matrix; + const modelID = connections.get( child.ID ).parents.filter( function ( parent ) { - if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder; + return parent.relationship !== undefined; - } + } )[ 0 ].ID; - } ); + if ( modelID !== undefined ) { - if ( ! node.transform ) node.transform = new Matrix4(); + const rawModel = fbxTree.Objects.Model[ modelID.toString() ]; - // if the animated model is pre rotated, we'll have to apply the pre rotations to every - // animation value as well - if ( 'PreRotation' in rawModel ) node.preRotation = rawModel.PreRotation.value; - if ( 'PostRotation' in rawModel ) node.postRotation = rawModel.PostRotation.value; + if ( rawModel === undefined ) { - layerCurveNodes[ i ] = node; + console.warn( 'THREE.FBXLoader: Encountered a unused curve.', child ); + return; } - } - - if ( layerCurveNodes[ i ] ) layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - - } else if ( curveNode.curves.morph !== undefined ) { + const node = { - if ( layerCurveNodes[ i ] === undefined ) { + modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', + ID: rawModel.id, + initialPosition: [ 0, 0, 0 ], + initialRotation: [ 0, 0, 0 ], + initialScale: [ 1, 1, 1 ], - var deformerID = connections.get( child.ID ).parents.filter( function ( parent ) { + }; - return parent.relationship !== undefined; + sceneGraph.traverse( function ( child ) { - } )[ 0 ].ID; + if ( child.ID === rawModel.id ) { - var morpherID = connections.get( deformerID ).parents[ 0 ].ID; - var geoID = connections.get( morpherID ).parents[ 0 ].ID; + node.transform = child.matrix; - // assuming geometry is not used in more than one model - var modelID = connections.get( geoID ).parents[ 0 ].ID; + if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder; - var rawModel = fbxTree.Objects.Model[ modelID ]; + } - var node = { + } ); - modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', - morphName: fbxTree.Objects.Deformer[ deformerID ].attrName, + if ( ! node.transform ) node.transform = new Matrix4(); - }; + // if the animated model is pre rotated, we'll have to apply the pre rotations to every + // animation value as well + if ( 'PreRotation' in rawModel ) node.preRotation = rawModel.PreRotation.value; + if ( 'PostRotation' in rawModel ) node.postRotation = rawModel.PostRotation.value; layerCurveNodes[ i ] = node; } - layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - } - } + if ( layerCurveNodes[ i ] ) layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - } ); + } else if ( curveNode.curves.morph !== undefined ) { - layersMap.set( parseInt( nodeID ), layerCurveNodes ); + if ( layerCurveNodes[ i ] === undefined ) { - } + const deformerID = connections.get( child.ID ).parents.filter( function ( parent ) { - } + return parent.relationship !== undefined; - return layersMap; + } )[ 0 ].ID; - }, + const morpherID = connections.get( deformerID ).parents[ 0 ].ID; + const geoID = connections.get( morpherID ).parents[ 0 ].ID; - // parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation - // hierarchy. Each Stack node will be used to create a AnimationClip - parseAnimStacks: function ( layersMap ) { + // assuming geometry is not used in more than one model + const modelID = connections.get( geoID ).parents[ 0 ].ID; - var rawStacks = fbxTree.Objects.AnimationStack; + const rawModel = fbxTree.Objects.Model[ modelID ]; - // connect the stacks (clips) up to the layers - var rawClips = {}; + const node = { - for ( var nodeID in rawStacks ) { + modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '', + morphName: fbxTree.Objects.Deformer[ deformerID ].attrName, - var children = connections.get( parseInt( nodeID ) ).children; + }; - if ( children.length > 1 ) { + layerCurveNodes[ i ] = node; - // it seems like stacks will always be associated with a single layer. But just in case there are files - // where there are multiple layers per stack, we'll display a warning - console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' ); + } - } + layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - var layer = layersMap.get( children[ 0 ].ID ); + } - rawClips[ nodeID ] = { + } - name: rawStacks[ nodeID ].attrName, - layer: layer, + } ); - }; + layersMap.set( parseInt( nodeID ), layerCurveNodes ); } - return rawClips; - - }, - - addClip: function ( rawClip ) { - - var tracks = []; + } - var scope = this; - rawClip.layer.forEach( function ( rawTracks ) { + return layersMap; - tracks = tracks.concat( scope.generateTracks( rawTracks ) ); + } - } ); + // parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation + // hierarchy. Each Stack node will be used to create a AnimationClip + parseAnimStacks( layersMap ) { - return new AnimationClip( rawClip.name, - 1, tracks ); + const rawStacks = fbxTree.Objects.AnimationStack; - }, + // connect the stacks (clips) up to the layers + const rawClips = {}; - generateTracks: function ( rawTracks ) { + for ( const nodeID in rawStacks ) { - var tracks = []; + const children = connections.get( parseInt( nodeID ) ).children; - var initialPosition = new Vector3(); - var initialRotation = new Quaternion(); - var initialScale = new Vector3(); + if ( children.length > 1 ) { - if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale ); + // it seems like stacks will always be associated with a single layer. But just in case there are files + // where there are multiple layers per stack, we'll display a warning + console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' ); - initialPosition = initialPosition.toArray(); - initialRotation = new Euler().setFromQuaternion( initialRotation, rawTracks.eulerOrder ).toArray(); - initialScale = initialScale.toArray(); + } - if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) { + const layer = layersMap.get( children[ 0 ].ID ); - var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' ); - if ( positionTrack !== undefined ) tracks.push( positionTrack ); + rawClips[ nodeID ] = { - } + name: rawStacks[ nodeID ].attrName, + layer: layer, - if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { + }; - var rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder ); - if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); + } - } + return rawClips; - if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) { + } - var scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' ); - if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); + addClip( rawClip ) { - } + let tracks = []; - if ( rawTracks.DeformPercent !== undefined ) { + const scope = this; + rawClip.layer.forEach( function ( rawTracks ) { - var morphTrack = this.generateMorphTrack( rawTracks ); - if ( morphTrack !== undefined ) tracks.push( morphTrack ); + tracks = tracks.concat( scope.generateTracks( rawTracks ) ); - } + } ); - return tracks; + return new AnimationClip( rawClip.name, - 1, tracks ); - }, + } - generateVectorTrack: function ( modelName, curves, initialValue, type ) { + generateTracks( rawTracks ) { - var times = this.getTimesForAllAxes( curves ); - var values = this.getKeyframeTrackValues( times, curves, initialValue ); + const tracks = []; - return new VectorKeyframeTrack( modelName + '.' + type, times, values ); + let initialPosition = new Vector3(); + let initialRotation = new Quaternion(); + let initialScale = new Vector3(); - }, + if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale ); - generateRotationTrack: function ( modelName, curves, initialValue, preRotation, postRotation, eulerOrder ) { + initialPosition = initialPosition.toArray(); + initialRotation = new Euler().setFromQuaternion( initialRotation, rawTracks.eulerOrder ).toArray(); + initialScale = initialScale.toArray(); - if ( curves.x !== undefined ) { + if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) { - this.interpolateRotations( curves.x ); - curves.x.values = curves.x.values.map( MathUtils.degToRad ); + const positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' ); + if ( positionTrack !== undefined ) tracks.push( positionTrack ); - } + } - if ( curves.y !== undefined ) { + if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { - this.interpolateRotations( curves.y ); - curves.y.values = curves.y.values.map( MathUtils.degToRad ); + const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder ); + if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); - } + } - if ( curves.z !== undefined ) { + if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) { - this.interpolateRotations( curves.z ); - curves.z.values = curves.z.values.map( MathUtils.degToRad ); + const scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' ); + if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); - } + } - var times = this.getTimesForAllAxes( curves ); - var values = this.getKeyframeTrackValues( times, curves, initialValue ); + if ( rawTracks.DeformPercent !== undefined ) { - if ( preRotation !== undefined ) { + const morphTrack = this.generateMorphTrack( rawTracks ); + if ( morphTrack !== undefined ) tracks.push( morphTrack ); - preRotation = preRotation.map( MathUtils.degToRad ); - preRotation.push( eulerOrder ); + } - preRotation = new Euler().fromArray( preRotation ); - preRotation = new Quaternion().setFromEuler( preRotation ); + return tracks; - } + } - if ( postRotation !== undefined ) { + generateVectorTrack( modelName, curves, initialValue, type ) { - postRotation = postRotation.map( MathUtils.degToRad ); - postRotation.push( eulerOrder ); + const times = this.getTimesForAllAxes( curves ); + const values = this.getKeyframeTrackValues( times, curves, initialValue ); - postRotation = new Euler().fromArray( postRotation ); - postRotation = new Quaternion().setFromEuler( postRotation ).invert(); + return new VectorKeyframeTrack( modelName + '.' + type, times, values ); - } + } - var quaternion = new Quaternion(); - var euler = new Euler(); + generateRotationTrack( modelName, curves, initialValue, preRotation, postRotation, eulerOrder ) { - var quaternionValues = []; + if ( curves.x !== undefined ) { - for ( var i = 0; i < values.length; i += 3 ) { + this.interpolateRotations( curves.x ); + curves.x.values = curves.x.values.map( MathUtils.degToRad ); - euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], eulerOrder ); + } - quaternion.setFromEuler( euler ); + if ( curves.y !== undefined ) { - if ( preRotation !== undefined ) quaternion.premultiply( preRotation ); - if ( postRotation !== undefined ) quaternion.multiply( postRotation ); + this.interpolateRotations( curves.y ); + curves.y.values = curves.y.values.map( MathUtils.degToRad ); - quaternion.toArray( quaternionValues, ( i / 3 ) * 4 ); + } - } + if ( curves.z !== undefined ) { - return new QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); + this.interpolateRotations( curves.z ); + curves.z.values = curves.z.values.map( MathUtils.degToRad ); - }, + } - generateMorphTrack: function ( rawTracks ) { + const times = this.getTimesForAllAxes( curves ); + const values = this.getKeyframeTrackValues( times, curves, initialValue ); - var curves = rawTracks.DeformPercent.curves.morph; - var values = curves.values.map( function ( val ) { + if ( preRotation !== undefined ) { - return val / 100; + preRotation = preRotation.map( MathUtils.degToRad ); + preRotation.push( eulerOrder ); - } ); + preRotation = new Euler().fromArray( preRotation ); + preRotation = new Quaternion().setFromEuler( preRotation ); - var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ]; + } - return new NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values ); + if ( postRotation !== undefined ) { - }, + postRotation = postRotation.map( MathUtils.degToRad ); + postRotation.push( eulerOrder ); - // For all animated objects, times are defined separately for each axis - // Here we'll combine the times into one sorted array without duplicates - getTimesForAllAxes: function ( curves ) { + postRotation = new Euler().fromArray( postRotation ); + postRotation = new Quaternion().setFromEuler( postRotation ).invert(); - var times = []; + } - // first join together the times for each axis, if defined - if ( curves.x !== undefined ) times = times.concat( curves.x.times ); - if ( curves.y !== undefined ) times = times.concat( curves.y.times ); - if ( curves.z !== undefined ) times = times.concat( curves.z.times ); + const quaternion = new Quaternion(); + const euler = new Euler(); - // then sort them - times = times.sort( function ( a, b ) { + const quaternionValues = []; - return a - b; + for ( let i = 0; i < values.length; i += 3 ) { - } ); + euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], eulerOrder ); - // and remove duplicates - if ( times.length > 1 ) { + quaternion.setFromEuler( euler ); - var targetIndex = 1; - var lastValue = times[ 0 ]; - for ( var i = 1; i < times.length; i ++ ) { + if ( preRotation !== undefined ) quaternion.premultiply( preRotation ); + if ( postRotation !== undefined ) quaternion.multiply( postRotation ); - var currentValue = times[ i ]; - if ( currentValue !== lastValue ) { + quaternion.toArray( quaternionValues, ( i / 3 ) * 4 ); - times[ targetIndex ] = currentValue; - lastValue = currentValue; - targetIndex ++; + } - } + return new QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); - } + } - times = times.slice( 0, targetIndex ); + generateMorphTrack( rawTracks ) { - } + const curves = rawTracks.DeformPercent.curves.morph; + const values = curves.values.map( function ( val ) { - return times; + return val / 100; - }, + } ); - getKeyframeTrackValues: function ( times, curves, initialValue ) { + const morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ]; - var prevValue = initialValue; + return new NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values ); - var values = []; + } - var xIndex = - 1; - var yIndex = - 1; - var zIndex = - 1; + // For all animated objects, times are defined separately for each axis + // Here we'll combine the times into one sorted array without duplicates + getTimesForAllAxes( curves ) { - times.forEach( function ( time ) { + let times = []; - if ( curves.x ) xIndex = curves.x.times.indexOf( time ); - if ( curves.y ) yIndex = curves.y.times.indexOf( time ); - if ( curves.z ) zIndex = curves.z.times.indexOf( time ); + // first join together the times for each axis, if defined + if ( curves.x !== undefined ) times = times.concat( curves.x.times ); + if ( curves.y !== undefined ) times = times.concat( curves.y.times ); + if ( curves.z !== undefined ) times = times.concat( curves.z.times ); - // if there is an x value defined for this frame, use that - if ( xIndex !== - 1 ) { + // then sort them + times = times.sort( function ( a, b ) { - var xValue = curves.x.values[ xIndex ]; - values.push( xValue ); - prevValue[ 0 ] = xValue; + return a - b; - } else { + } ); - // otherwise use the x value from the previous frame - values.push( prevValue[ 0 ] ); + // and remove duplicates + if ( times.length > 1 ) { - } + let targetIndex = 1; + let lastValue = times[ 0 ]; + for ( let i = 1; i < times.length; i ++ ) { - if ( yIndex !== - 1 ) { + const currentValue = times[ i ]; + if ( currentValue !== lastValue ) { - var yValue = curves.y.values[ yIndex ]; - values.push( yValue ); - prevValue[ 1 ] = yValue; + times[ targetIndex ] = currentValue; + lastValue = currentValue; + targetIndex ++; - } else { + } - values.push( prevValue[ 1 ] ); + } - } + times = times.slice( 0, targetIndex ); - if ( zIndex !== - 1 ) { + } - var zValue = curves.z.values[ zIndex ]; - values.push( zValue ); - prevValue[ 2 ] = zValue; + return times; - } else { + } - values.push( prevValue[ 2 ] ); + getKeyframeTrackValues( times, curves, initialValue ) { - } + const prevValue = initialValue; - } ); + const values = []; - return values; + let xIndex = - 1; + let yIndex = - 1; + let zIndex = - 1; - }, + times.forEach( function ( time ) { - // Rotations are defined as Euler angles which can have values of any size - // These will be converted to quaternions which don't support values greater than - // PI, so we'll interpolate large rotations - interpolateRotations: function ( curve ) { + if ( curves.x ) xIndex = curves.x.times.indexOf( time ); + if ( curves.y ) yIndex = curves.y.times.indexOf( time ); + if ( curves.z ) zIndex = curves.z.times.indexOf( time ); - for ( var i = 1; i < curve.values.length; i ++ ) { + // if there is an x value defined for this frame, use that + if ( xIndex !== - 1 ) { - var initialValue = curve.values[ i - 1 ]; - var valuesSpan = curve.values[ i ] - initialValue; + const xValue = curves.x.values[ xIndex ]; + values.push( xValue ); + prevValue[ 0 ] = xValue; - var absoluteSpan = Math.abs( valuesSpan ); + } else { - if ( absoluteSpan >= 180 ) { + // otherwise use the x value from the previous frame + values.push( prevValue[ 0 ] ); - var numSubIntervals = absoluteSpan / 180; + } - var step = valuesSpan / numSubIntervals; - var nextValue = initialValue + step; + if ( yIndex !== - 1 ) { - var initialTime = curve.times[ i - 1 ]; - var timeSpan = curve.times[ i ] - initialTime; - var interval = timeSpan / numSubIntervals; - var nextTime = initialTime + interval; + const yValue = curves.y.values[ yIndex ]; + values.push( yValue ); + prevValue[ 1 ] = yValue; - var interpolatedTimes = []; - var interpolatedValues = []; + } else { - while ( nextTime < curve.times[ i ] ) { + values.push( prevValue[ 1 ] ); - interpolatedTimes.push( nextTime ); - nextTime += interval; + } - interpolatedValues.push( nextValue ); - nextValue += step; + if ( zIndex !== - 1 ) { - } + const zValue = curves.z.values[ zIndex ]; + values.push( zValue ); + prevValue[ 2 ] = zValue; - curve.times = inject( curve.times, i, interpolatedTimes ); - curve.values = inject( curve.values, i, interpolatedValues ); + } else { - } + values.push( prevValue[ 2 ] ); } - }, + } ); - }; + return values; - // parse an FBX file in ASCII format - function TextParser() {} + } - TextParser.prototype = { + // Rotations are defined as Euler angles which can have values of any size + // These will be converted to quaternions which don't support values greater than + // PI, so we'll interpolate large rotations + interpolateRotations( curve ) { - constructor: TextParser, + for ( let i = 1; i < curve.values.length; i ++ ) { - getPrevNode: function () { + const initialValue = curve.values[ i - 1 ]; + const valuesSpan = curve.values[ i ] - initialValue; - return this.nodeStack[ this.currentIndent - 2 ]; + const absoluteSpan = Math.abs( valuesSpan ); - }, + if ( absoluteSpan >= 180 ) { - getCurrentNode: function () { + const numSubIntervals = absoluteSpan / 180; - return this.nodeStack[ this.currentIndent - 1 ]; + const step = valuesSpan / numSubIntervals; + let nextValue = initialValue + step; - }, + const initialTime = curve.times[ i - 1 ]; + const timeSpan = curve.times[ i ] - initialTime; + const interval = timeSpan / numSubIntervals; + let nextTime = initialTime + interval; - getCurrentProp: function () { + const interpolatedTimes = []; + const interpolatedValues = []; - return this.currentProp; + while ( nextTime < curve.times[ i ] ) { - }, + interpolatedTimes.push( nextTime ); + nextTime += interval; - pushStack: function ( node ) { + interpolatedValues.push( nextValue ); + nextValue += step; - this.nodeStack.push( node ); - this.currentIndent += 1; + } - }, + curve.times = inject( curve.times, i, interpolatedTimes ); + curve.values = inject( curve.values, i, interpolatedValues ); - popStack: function () { + } - this.nodeStack.pop(); - this.currentIndent -= 1; + } - }, + } - setCurrentProp: function ( val, name ) { +} - this.currentProp = val; - this.currentPropName = name; +// parse an FBX file in ASCII format +class TextParser { - }, + getPrevNode() { - parse: function ( text ) { + return this.nodeStack[ this.currentIndent - 2 ]; - this.currentIndent = 0; + } - this.allNodes = new FBXTree(); - this.nodeStack = []; - this.currentProp = []; - this.currentPropName = ''; + getCurrentNode() { - var scope = this; + return this.nodeStack[ this.currentIndent - 1 ]; - var split = text.split( /[\r\n]+/ ); + } - split.forEach( function ( line, i ) { + getCurrentProp() { - var matchComment = line.match( /^[\s\t]*;/ ); - var matchEmpty = line.match( /^[\s\t]*$/ ); + return this.currentProp; - if ( matchComment || matchEmpty ) return; + } - var matchBeginning = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):(.*){', '' ); - var matchProperty = line.match( '^\\t{' + ( scope.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' ); - var matchEnd = line.match( '^\\t{' + ( scope.currentIndent - 1 ) + '}}' ); + pushStack( node ) { - if ( matchBeginning ) { + this.nodeStack.push( node ); + this.currentIndent += 1; - scope.parseNodeBegin( line, matchBeginning ); + } - } else if ( matchProperty ) { + popStack() { - scope.parseNodeProperty( line, matchProperty, split[ ++ i ] ); + this.nodeStack.pop(); + this.currentIndent -= 1; - } else if ( matchEnd ) { + } - scope.popStack(); + setCurrentProp( val, name ) { - } else if ( line.match( /^[^\s\t}]/ ) ) { + this.currentProp = val; + this.currentPropName = name; - // large arrays are split over multiple lines terminated with a ',' character - // if this is encountered the line needs to be joined to the previous line - scope.parseNodePropertyContinued( line ); + } - } + parse( text ) { - } ); + this.currentIndent = 0; - return this.allNodes; + this.allNodes = new FBXTree(); + this.nodeStack = []; + this.currentProp = []; + this.currentPropName = ''; - }, + const scope = this; - parseNodeBegin: function ( line, property ) { + const split = text.split( /[\r\n]+/ ); - var nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); + split.forEach( function ( line, i ) { - var nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { + const matchComment = line.match( /^[\s\t]*;/ ); + const matchEmpty = line.match( /^[\s\t]*$/ ); - return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); + if ( matchComment || matchEmpty ) return; - } ); + const matchBeginning = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):(.*){', '' ); + const matchProperty = line.match( '^\\t{' + ( scope.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' ); + const matchEnd = line.match( '^\\t{' + ( scope.currentIndent - 1 ) + '}}' ); - var node = { name: nodeName }; - var attrs = this.parseNodeAttr( nodeAttrs ); + if ( matchBeginning ) { - var currentNode = this.getCurrentNode(); + scope.parseNodeBegin( line, matchBeginning ); - // a top node - if ( this.currentIndent === 0 ) { + } else if ( matchProperty ) { - this.allNodes.add( nodeName, node ); + scope.parseNodeProperty( line, matchProperty, split[ ++ i ] ); - } else { // a subnode + } else if ( matchEnd ) { - // if the subnode already exists, append it - if ( nodeName in currentNode ) { + scope.popStack(); - // special case Pose needs PoseNodes as an array - if ( nodeName === 'PoseNode' ) { + } else if ( line.match( /^[^\s\t}]/ ) ) { - currentNode.PoseNode.push( node ); + // large arrays are split over multiple lines terminated with a ',' character + // if this is encountered the line needs to be joined to the previous line + scope.parseNodePropertyContinued( line ); - } else if ( currentNode[ nodeName ].id !== undefined ) { + } - currentNode[ nodeName ] = {}; - currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ]; + } ); - } + return this.allNodes; - if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node; + } - } else if ( typeof attrs.id === 'number' ) { + parseNodeBegin( line, property ) { - currentNode[ nodeName ] = {}; - currentNode[ nodeName ][ attrs.id ] = node; + const nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); - } else if ( nodeName !== 'Properties70' ) { + const nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { - if ( nodeName === 'PoseNode' ) currentNode[ nodeName ] = [ node ]; - else currentNode[ nodeName ] = node; + return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); - } + } ); - } + const node = { name: nodeName }; + const attrs = this.parseNodeAttr( nodeAttrs ); - if ( typeof attrs.id === 'number' ) node.id = attrs.id; - if ( attrs.name !== '' ) node.attrName = attrs.name; - if ( attrs.type !== '' ) node.attrType = attrs.type; + const currentNode = this.getCurrentNode(); - this.pushStack( node ); + // a top node + if ( this.currentIndent === 0 ) { - }, + this.allNodes.add( nodeName, node ); - parseNodeAttr: function ( attrs ) { + } else { // a subnode - var id = attrs[ 0 ]; + // if the subnode already exists, append it + if ( nodeName in currentNode ) { - if ( attrs[ 0 ] !== '' ) { + // special case Pose needs PoseNodes as an array + if ( nodeName === 'PoseNode' ) { - id = parseInt( attrs[ 0 ] ); + currentNode.PoseNode.push( node ); - if ( isNaN( id ) ) { + } else if ( currentNode[ nodeName ].id !== undefined ) { - id = attrs[ 0 ]; + currentNode[ nodeName ] = {}; + currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ]; } - } + if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node; - var name = '', type = ''; + } else if ( typeof attrs.id === 'number' ) { - if ( attrs.length > 1 ) { + currentNode[ nodeName ] = {}; + currentNode[ nodeName ][ attrs.id ] = node; - name = attrs[ 1 ].replace( /^(\w+)::/, '' ); - type = attrs[ 2 ]; + } else if ( nodeName !== 'Properties70' ) { + + if ( nodeName === 'PoseNode' ) currentNode[ nodeName ] = [ node ]; + else currentNode[ nodeName ] = node; } - return { id: id, name: name, type: type }; + } - }, + if ( typeof attrs.id === 'number' ) node.id = attrs.id; + if ( attrs.name !== '' ) node.attrName = attrs.name; + if ( attrs.type !== '' ) node.attrType = attrs.type; - parseNodeProperty: function ( line, property, contentLine ) { + this.pushStack( node ); - var propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); - var propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); + } - // for special case: base64 image data follows "Content: ," line - // Content: , - // "/9j/4RDaRXhpZgAATU0A..." - if ( propName === 'Content' && propValue === ',' ) { + parseNodeAttr( attrs ) { - propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); + let id = attrs[ 0 ]; - } + if ( attrs[ 0 ] !== '' ) { - var currentNode = this.getCurrentNode(); - var parentName = currentNode.name; + id = parseInt( attrs[ 0 ] ); - if ( parentName === 'Properties70' ) { + if ( isNaN( id ) ) { - this.parseNodeSpecialProperty( line, propName, propValue ); - return; + id = attrs[ 0 ]; } - // Connections - if ( propName === 'C' ) { + } - var connProps = propValue.split( ',' ).slice( 1 ); - var from = parseInt( connProps[ 0 ] ); - var to = parseInt( connProps[ 1 ] ); + let name = '', type = ''; - var rest = propValue.split( ',' ).slice( 3 ); + if ( attrs.length > 1 ) { - rest = rest.map( function ( elem ) { + name = attrs[ 1 ].replace( /^(\w+)::/, '' ); + type = attrs[ 2 ]; - return elem.trim().replace( /^"/, '' ); + } - } ); + return { id: id, name: name, type: type }; - propName = 'connections'; - propValue = [ from, to ]; - append( propValue, rest ); + } - if ( currentNode[ propName ] === undefined ) { + parseNodeProperty( line, property, contentLine ) { - currentNode[ propName ] = []; + let propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); + let propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); - } + // for special case: base64 image data follows "Content: ," line + // Content: , + // "/9j/4RDaRXhpZgAATU0A..." + if ( propName === 'Content' && propValue === ',' ) { - } + propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); - // Node - if ( propName === 'Node' ) currentNode.id = propValue; + } - // connections - if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) { + const currentNode = this.getCurrentNode(); + const parentName = currentNode.name; - currentNode[ propName ].push( propValue ); + if ( parentName === 'Properties70' ) { - } else { + this.parseNodeSpecialProperty( line, propName, propValue ); + return; - if ( propName !== 'a' ) currentNode[ propName ] = propValue; - else currentNode.a = propValue; + } - } + // Connections + if ( propName === 'C' ) { - this.setCurrentProp( currentNode, propName ); + const connProps = propValue.split( ',' ).slice( 1 ); + const from = parseInt( connProps[ 0 ] ); + const to = parseInt( connProps[ 1 ] ); - // convert string to array, unless it ends in ',' in which case more will be added to it - if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { + let rest = propValue.split( ',' ).slice( 3 ); - currentNode.a = parseNumberArray( propValue ); + rest = rest.map( function ( elem ) { - } + return elem.trim().replace( /^"/, '' ); - }, + } ); - parseNodePropertyContinued: function ( line ) { + propName = 'connections'; + propValue = [ from, to ]; + append( propValue, rest ); - var currentNode = this.getCurrentNode(); + if ( currentNode[ propName ] === undefined ) { - currentNode.a += line; + currentNode[ propName ] = []; - // if the line doesn't end in ',' we have reached the end of the property value - // so convert the string to an array - if ( line.slice( - 1 ) !== ',' ) { + } - currentNode.a = parseNumberArray( currentNode.a ); + } - } + // Node + if ( propName === 'Node' ) currentNode.id = propValue; - }, + // connections + if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) { - // parse "Property70" - parseNodeSpecialProperty: function ( line, propName, propValue ) { + currentNode[ propName ].push( propValue ); - // split this - // P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 - // into array like below - // ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ] - var props = propValue.split( '",' ).map( function ( prop ) { + } else { - return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); + if ( propName !== 'a' ) currentNode[ propName ] = propValue; + else currentNode.a = propValue; - } ); + } - var innerPropName = props[ 0 ]; - var innerPropType1 = props[ 1 ]; - var innerPropType2 = props[ 2 ]; - var innerPropFlag = props[ 3 ]; - var innerPropValue = props[ 4 ]; - - // cast values where needed, otherwise leave as strings - switch ( innerPropType1 ) { - - case 'int': - case 'enum': - case 'bool': - case 'ULongLong': - case 'double': - case 'Number': - case 'FieldOfView': - innerPropValue = parseFloat( innerPropValue ); - break; + this.setCurrentProp( currentNode, propName ); - case 'Color': - case 'ColorRGB': - case 'Vector3D': - case 'Lcl_Translation': - case 'Lcl_Rotation': - case 'Lcl_Scaling': - innerPropValue = parseNumberArray( innerPropValue ); - break; + // convert string to array, unless it ends in ',' in which case more will be added to it + if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { - } + currentNode.a = parseNumberArray( propValue ); - // CAUTION: these props must append to parent's parent - this.getPrevNode()[ innerPropName ] = { + } - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue + } - }; + parseNodePropertyContinued( line ) { - this.setCurrentProp( this.getPrevNode(), innerPropName ); + const currentNode = this.getCurrentNode(); - }, + currentNode.a += line; - }; + // if the line doesn't end in ',' we have reached the end of the property value + // so convert the string to an array + if ( line.slice( - 1 ) !== ',' ) { - // Parse an FBX file in Binary format - function BinaryParser() {} + currentNode.a = parseNumberArray( currentNode.a ); - BinaryParser.prototype = { + } - constructor: BinaryParser, + } - parse: function ( buffer ) { + // parse "Property70" + parseNodeSpecialProperty( line, propName, propValue ) { - var reader = new BinaryReader( buffer ); - reader.skip( 23 ); // skip magic 23 bytes + // split this + // P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 + // into array like below + // ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ] + const props = propValue.split( '",' ).map( function ( prop ) { - var version = reader.getUint32(); + return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); - if ( version < 6400 ) { + } ); - throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + version ); + const innerPropName = props[ 0 ]; + const innerPropType1 = props[ 1 ]; + const innerPropType2 = props[ 2 ]; + const innerPropFlag = props[ 3 ]; + let innerPropValue = props[ 4 ]; + + // cast values where needed, otherwise leave as strings + switch ( innerPropType1 ) { + + case 'int': + case 'enum': + case 'bool': + case 'ULongLong': + case 'double': + case 'Number': + case 'FieldOfView': + innerPropValue = parseFloat( innerPropValue ); + break; - } + case 'Color': + case 'ColorRGB': + case 'Vector3D': + case 'Lcl_Translation': + case 'Lcl_Rotation': + case 'Lcl_Scaling': + innerPropValue = parseNumberArray( innerPropValue ); + break; - var allNodes = new FBXTree(); + } - while ( ! this.endOfContent( reader ) ) { + // CAUTION: these props must append to parent's parent + this.getPrevNode()[ innerPropName ] = { - var node = this.parseNode( reader, version ); - if ( node !== null ) allNodes.add( node.name, node ); + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue - } + }; - return allNodes; + this.setCurrentProp( this.getPrevNode(), innerPropName ); - }, + } - // Check if reader has reached the end of content. - endOfContent: function ( reader ) { +} - // footer size: 160bytes + 16-byte alignment padding - // - 16bytes: magic - // - padding til 16-byte alignment (at least 1byte?) - // (seems like some exporters embed fixed 15 or 16bytes?) - // - 4bytes: magic - // - 4bytes: version - // - 120bytes: zero - // - 16bytes: magic - if ( reader.size() % 16 === 0 ) { +// Parse an FBX file in Binary format +class BinaryParser { - return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size(); + parse( buffer ) { - } else { + const reader = new BinaryReader( buffer ); + reader.skip( 23 ); // skip magic 23 bytes - return reader.getOffset() + 160 + 16 >= reader.size(); + const version = reader.getUint32(); - } + if ( version < 6400 ) { - }, + throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + version ); - // recursively parse nodes until the end of the file is reached - parseNode: function ( reader, version ) { + } - var node = {}; + const allNodes = new FBXTree(); - // The first three data sizes depends on version. - var endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); - var numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); + while ( ! this.endOfContent( reader ) ) { - ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); // the returned propertyListLen is not used + const node = this.parseNode( reader, version ); + if ( node !== null ) allNodes.add( node.name, node ); - var nameLen = reader.getUint8(); - var name = reader.getString( nameLen ); + } - // Regards this node as NULL-record if endOffset is zero - if ( endOffset === 0 ) return null; + return allNodes; - var propertyList = []; + } - for ( var i = 0; i < numProperties; i ++ ) { + // Check if reader has reached the end of content. + endOfContent( reader ) { - propertyList.push( this.parseProperty( reader ) ); + // footer size: 160bytes + 16-byte alignment padding + // - 16bytes: magic + // - padding til 16-byte alignment (at least 1byte?) + // (seems like some exporters embed fixed 15 or 16bytes?) + // - 4bytes: magic + // - 4bytes: version + // - 120bytes: zero + // - 16bytes: magic + if ( reader.size() % 16 === 0 ) { - } + return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size(); - // Regards the first three elements in propertyList as id, attrName, and attrType - var id = propertyList.length > 0 ? propertyList[ 0 ] : ''; - var attrName = propertyList.length > 1 ? propertyList[ 1 ] : ''; - var attrType = propertyList.length > 2 ? propertyList[ 2 ] : ''; + } else { - // check if this node represents just a single property - // like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]} - node.singleProperty = ( numProperties === 1 && reader.getOffset() === endOffset ) ? true : false; + return reader.getOffset() + 160 + 16 >= reader.size(); - while ( endOffset > reader.getOffset() ) { + } - var subNode = this.parseNode( reader, version ); + } - if ( subNode !== null ) this.parseSubNode( name, node, subNode ); + // recursively parse nodes until the end of the file is reached + parseNode( reader, version ) { - } + const node = {}; - node.propertyList = propertyList; // raw property list used by parent + // The first three data sizes depends on version. + const endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); + const numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); - if ( typeof id === 'number' ) node.id = id; - if ( attrName !== '' ) node.attrName = attrName; - if ( attrType !== '' ) node.attrType = attrType; - if ( name !== '' ) node.name = name; + ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); // the returned propertyListLen is not used - return node; + const nameLen = reader.getUint8(); + const name = reader.getString( nameLen ); - }, + // Regards this node as NULL-record if endOffset is zero + if ( endOffset === 0 ) return null; - parseSubNode: function ( name, node, subNode ) { + const propertyList = []; - // special case: child node is single property - if ( subNode.singleProperty === true ) { + for ( let i = 0; i < numProperties; i ++ ) { - var value = subNode.propertyList[ 0 ]; + propertyList.push( this.parseProperty( reader ) ); - if ( Array.isArray( value ) ) { + } - node[ subNode.name ] = subNode; + // Regards the first three elements in propertyList as id, attrName, and attrType + const id = propertyList.length > 0 ? propertyList[ 0 ] : ''; + const attrName = propertyList.length > 1 ? propertyList[ 1 ] : ''; + const attrType = propertyList.length > 2 ? propertyList[ 2 ] : ''; - subNode.a = value; + // check if this node represents just a single property + // like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]} + node.singleProperty = ( numProperties === 1 && reader.getOffset() === endOffset ) ? true : false; - } else { + while ( endOffset > reader.getOffset() ) { - node[ subNode.name ] = value; + const subNode = this.parseNode( reader, version ); - } + if ( subNode !== null ) this.parseSubNode( name, node, subNode ); - } else if ( name === 'Connections' && subNode.name === 'C' ) { + } - var array = []; + node.propertyList = propertyList; // raw property list used by parent - subNode.propertyList.forEach( function ( property, i ) { + if ( typeof id === 'number' ) node.id = id; + if ( attrName !== '' ) node.attrName = attrName; + if ( attrType !== '' ) node.attrType = attrType; + if ( name !== '' ) node.name = name; - // first Connection is FBX type (OO, OP, etc.). We'll discard these - if ( i !== 0 ) array.push( property ); + return node; - } ); + } - if ( node.connections === undefined ) { + parseSubNode( name, node, subNode ) { - node.connections = []; + // special case: child node is single property + if ( subNode.singleProperty === true ) { - } + const value = subNode.propertyList[ 0 ]; - node.connections.push( array ); + if ( Array.isArray( value ) ) { - } else if ( subNode.name === 'Properties70' ) { + node[ subNode.name ] = subNode; - var keys = Object.keys( subNode ); + subNode.a = value; - keys.forEach( function ( key ) { + } else { - node[ key ] = subNode[ key ]; + node[ subNode.name ] = value; - } ); + } - } else if ( name === 'Properties70' && subNode.name === 'P' ) { + } else if ( name === 'Connections' && subNode.name === 'C' ) { - var innerPropName = subNode.propertyList[ 0 ]; - var innerPropType1 = subNode.propertyList[ 1 ]; - var innerPropType2 = subNode.propertyList[ 2 ]; - var innerPropFlag = subNode.propertyList[ 3 ]; - var innerPropValue; + const array = []; - if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); - if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); + subNode.propertyList.forEach( function ( property, i ) { - if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { + // first Connection is FBX type (OO, OP, etc.). We'll discard these + if ( i !== 0 ) array.push( property ); - innerPropValue = [ - subNode.propertyList[ 4 ], - subNode.propertyList[ 5 ], - subNode.propertyList[ 6 ] - ]; + } ); - } else { + if ( node.connections === undefined ) { - innerPropValue = subNode.propertyList[ 4 ]; + node.connections = []; - } + } - // this will be copied to parent, see above - node[ innerPropName ] = { + node.connections.push( array ); - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue + } else if ( subNode.name === 'Properties70' ) { - }; + const keys = Object.keys( subNode ); - } else if ( node[ subNode.name ] === undefined ) { + keys.forEach( function ( key ) { - if ( typeof subNode.id === 'number' ) { + node[ key ] = subNode[ key ]; - node[ subNode.name ] = {}; - node[ subNode.name ][ subNode.id ] = subNode; + } ); - } else { + } else if ( name === 'Properties70' && subNode.name === 'P' ) { - node[ subNode.name ] = subNode; + let innerPropName = subNode.propertyList[ 0 ]; + let innerPropType1 = subNode.propertyList[ 1 ]; + const innerPropType2 = subNode.propertyList[ 2 ]; + const innerPropFlag = subNode.propertyList[ 3 ]; + let innerPropValue; - } + if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); + if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); + + if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { + + innerPropValue = [ + subNode.propertyList[ 4 ], + subNode.propertyList[ 5 ], + subNode.propertyList[ 6 ] + ]; } else { - if ( subNode.name === 'PoseNode' ) { + innerPropValue = subNode.propertyList[ 4 ]; - if ( ! Array.isArray( node[ subNode.name ] ) ) { + } - node[ subNode.name ] = [ node[ subNode.name ] ]; + // this will be copied to parent, see above + node[ innerPropName ] = { - } + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue - node[ subNode.name ].push( subNode ); + }; - } else if ( node[ subNode.name ][ subNode.id ] === undefined ) { + } else if ( node[ subNode.name ] === undefined ) { - node[ subNode.name ][ subNode.id ] = subNode; + if ( typeof subNode.id === 'number' ) { - } + node[ subNode.name ] = {}; + node[ subNode.name ][ subNode.id ] = subNode; - } + } else { - }, + node[ subNode.name ] = subNode; - parseProperty: function ( reader ) { + } - var type = reader.getString( 1 ); + } else { - switch ( type ) { + if ( subNode.name === 'PoseNode' ) { - case 'C': - return reader.getBoolean(); + if ( ! Array.isArray( node[ subNode.name ] ) ) { - case 'D': - return reader.getFloat64(); + node[ subNode.name ] = [ node[ subNode.name ] ]; - case 'F': - return reader.getFloat32(); + } - case 'I': - return reader.getInt32(); + node[ subNode.name ].push( subNode ); - case 'L': - return reader.getInt64(); + } else if ( node[ subNode.name ][ subNode.id ] === undefined ) { - case 'R': - var length = reader.getUint32(); - return reader.getArrayBuffer( length ); + node[ subNode.name ][ subNode.id ] = subNode; - case 'S': - var length = reader.getUint32(); - return reader.getString( length ); + } - case 'Y': - return reader.getInt16(); + } - case 'b': - case 'c': - case 'd': - case 'f': - case 'i': - case 'l': + } - var arrayLength = reader.getUint32(); - var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed - var compressedLength = reader.getUint32(); + parseProperty( reader ) { - if ( encoding === 0 ) { + const type = reader.getString( 1 ); + let length; - switch ( type ) { + switch ( type ) { - case 'b': - case 'c': - return reader.getBooleanArray( arrayLength ); + case 'C': + return reader.getBoolean(); - case 'd': - return reader.getFloat64Array( arrayLength ); + case 'D': + return reader.getFloat64(); - case 'f': - return reader.getFloat32Array( arrayLength ); + case 'F': + return reader.getFloat32(); - case 'i': - return reader.getInt32Array( arrayLength ); + case 'I': + return reader.getInt32(); - case 'l': - return reader.getInt64Array( arrayLength ); + case 'L': + return reader.getInt64(); - } + case 'R': + length = reader.getUint32(); + return reader.getArrayBuffer( length ); - } + case 'S': + length = reader.getUint32(); + return reader.getString( length ); - if ( typeof fflate === 'undefined' ) { + case 'Y': + return reader.getInt16(); - console.error( 'THREE.FBXLoader: External library fflate.min.js required.' ); + case 'b': + case 'c': + case 'd': + case 'f': + case 'i': + case 'l': - } + const arrayLength = reader.getUint32(); + const encoding = reader.getUint32(); // 0: non-compressed, 1: compressed + const compressedLength = reader.getUint32(); - var data = fflate.unzlibSync( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef - var reader2 = new BinaryReader( data.buffer ); + if ( encoding === 0 ) { switch ( type ) { case 'b': case 'c': - return reader2.getBooleanArray( arrayLength ); + return reader.getBooleanArray( arrayLength ); case 'd': - return reader2.getFloat64Array( arrayLength ); + return reader.getFloat64Array( arrayLength ); case 'f': - return reader2.getFloat32Array( arrayLength ); + return reader.getFloat32Array( arrayLength ); case 'i': - return reader2.getInt32Array( arrayLength ); + return reader.getInt32Array( arrayLength ); case 'l': - return reader2.getInt64Array( arrayLength ); + return reader.getInt64Array( arrayLength ); } - default: - throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); + } - } + if ( typeof fflate === 'undefined' ) { - } + console.error( 'THREE.FBXLoader: External library fflate.min.js required.' ); - }; + } - function BinaryReader( buffer, littleEndian ) { + const data = fflate.unzlibSync( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef + const reader2 = new BinaryReader( data.buffer ); - this.dv = new DataView( buffer ); - this.offset = 0; - this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true; + switch ( type ) { + + case 'b': + case 'c': + return reader2.getBooleanArray( arrayLength ); + + case 'd': + return reader2.getFloat64Array( arrayLength ); + + case 'f': + return reader2.getFloat32Array( arrayLength ); + + case 'i': + return reader2.getInt32Array( arrayLength ); + + case 'l': + return reader2.getInt64Array( arrayLength ); + + } + + default: + throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); + + } } - BinaryReader.prototype = { +} - constructor: BinaryReader, +class BinaryReader { - getOffset: function () { + constructor( buffer, littleEndian ) { - return this.offset; + this.dv = new DataView( buffer ); + this.offset = 0; + this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true; - }, + } - size: function () { + getOffset() { - return this.dv.buffer.byteLength; + return this.offset; - }, + } - skip: function ( length ) { + size() { - this.offset += length; + return this.dv.buffer.byteLength; - }, + } - // seems like true/false representation depends on exporter. - // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) - // then sees LSB. - getBoolean: function () { + skip( length ) { - return ( this.getUint8() & 1 ) === 1; + this.offset += length; - }, + } - getBooleanArray: function ( size ) { + // seems like true/false representation depends on exporter. + // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) + // then sees LSB. + getBoolean() { - var a = []; + return ( this.getUint8() & 1 ) === 1; - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getBoolean() ); + getBooleanArray( size ) { - } + const a = []; - return a; + for ( let i = 0; i < size; i ++ ) { - }, + a.push( this.getBoolean() ); - getUint8: function () { + } - var value = this.dv.getUint8( this.offset ); - this.offset += 1; - return value; + return a; - }, + } - getInt16: function () { + getUint8() { - var value = this.dv.getInt16( this.offset, this.littleEndian ); - this.offset += 2; - return value; + const value = this.dv.getUint8( this.offset ); + this.offset += 1; + return value; - }, + } - getInt32: function () { + getInt16() { - var value = this.dv.getInt32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + const value = this.dv.getInt16( this.offset, this.littleEndian ); + this.offset += 2; + return value; - }, + } - getInt32Array: function ( size ) { + getInt32() { - var a = []; + const value = this.dv.getInt32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getInt32() ); + getInt32Array( size ) { - } + const a = []; - return a; + for ( let i = 0; i < size; i ++ ) { - }, + a.push( this.getInt32() ); - getUint32: function () { + } - var value = this.dv.getUint32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + return a; - }, + } - // JavaScript doesn't support 64-bit integer so calculate this here - // 1 << 32 will return 1 so using multiply operation instead here. - // There's a possibility that this method returns wrong value if the value - // is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. - // TODO: safely handle 64-bit integer - getInt64: function () { + getUint32() { - var low, high; + const value = this.dv.getUint32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - if ( this.littleEndian ) { + } - low = this.getUint32(); - high = this.getUint32(); + // JavaScript doesn't support 64-bit integer so calculate this here + // 1 << 32 will return 1 so using multiply operation instead here. + // There's a possibility that this method returns wrong value if the value + // is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. + // TODO: safely handle 64-bit integer + getInt64() { - } else { + let low, high; - high = this.getUint32(); - low = this.getUint32(); + if ( this.littleEndian ) { - } + low = this.getUint32(); + high = this.getUint32(); - // calculate negative value - if ( high & 0x80000000 ) { + } else { - high = ~ high & 0xFFFFFFFF; - low = ~ low & 0xFFFFFFFF; + high = this.getUint32(); + low = this.getUint32(); - if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF; + } - low = ( low + 1 ) & 0xFFFFFFFF; + // calculate negative value + if ( high & 0x80000000 ) { - return - ( high * 0x100000000 + low ); + high = ~ high & 0xFFFFFFFF; + low = ~ low & 0xFFFFFFFF; - } + if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF; - return high * 0x100000000 + low; + low = ( low + 1 ) & 0xFFFFFFFF; - }, + return - ( high * 0x100000000 + low ); - getInt64Array: function ( size ) { + } - var a = []; + return high * 0x100000000 + low; - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getInt64() ); + getInt64Array( size ) { - } + const a = []; - return a; + for ( let i = 0; i < size; i ++ ) { - }, + a.push( this.getInt64() ); - // Note: see getInt64() comment - getUint64: function () { + } - var low, high; + return a; - if ( this.littleEndian ) { + } - low = this.getUint32(); - high = this.getUint32(); + // Note: see getInt64() comment + getUint64() { - } else { + let low, high; - high = this.getUint32(); - low = this.getUint32(); + if ( this.littleEndian ) { - } + low = this.getUint32(); + high = this.getUint32(); - return high * 0x100000000 + low; + } else { - }, + high = this.getUint32(); + low = this.getUint32(); - getFloat32: function () { + } - var value = this.dv.getFloat32( this.offset, this.littleEndian ); - this.offset += 4; - return value; + return high * 0x100000000 + low; - }, + } - getFloat32Array: function ( size ) { + getFloat32() { - var a = []; + const value = this.dv.getFloat32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getFloat32() ); + getFloat32Array( size ) { - } + const a = []; - return a; + for ( let i = 0; i < size; i ++ ) { - }, + a.push( this.getFloat32() ); - getFloat64: function () { + } - var value = this.dv.getFloat64( this.offset, this.littleEndian ); - this.offset += 8; - return value; + return a; - }, + } - getFloat64Array: function ( size ) { + getFloat64() { - var a = []; + const value = this.dv.getFloat64( this.offset, this.littleEndian ); + this.offset += 8; + return value; - for ( var i = 0; i < size; i ++ ) { + } - a.push( this.getFloat64() ); + getFloat64Array( size ) { - } + const a = []; - return a; + for ( let i = 0; i < size; i ++ ) { - }, + a.push( this.getFloat64() ); - getArrayBuffer: function ( size ) { + } - var value = this.dv.buffer.slice( this.offset, this.offset + size ); - this.offset += size; - return value; + return a; - }, + } - getString: function ( size ) { + getArrayBuffer( size ) { - // note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead - var a = []; + const value = this.dv.buffer.slice( this.offset, this.offset + size ); + this.offset += size; + return value; - for ( var i = 0; i < size; i ++ ) { + } - a[ i ] = this.getUint8(); + getString( size ) { - } + // note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead + let a = []; - var nullByte = a.indexOf( 0 ); - if ( nullByte >= 0 ) a = a.slice( 0, nullByte ); + for ( let i = 0; i < size; i ++ ) { - return LoaderUtils.decodeText( new Uint8Array( a ) ); + a[ i ] = this.getUint8(); } - }; + const nullByte = a.indexOf( 0 ); + if ( nullByte >= 0 ) a = a.slice( 0, nullByte ); - // FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) - // and BinaryParser( FBX Binary format) - function FBXTree() {} + return LoaderUtils.decodeText( new Uint8Array( a ) ); - FBXTree.prototype = { + } - constructor: FBXTree, +} - add: function ( key, val ) { +// FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) +// and BinaryParser( FBX Binary format) +class FBXTree { - this[ key ] = val; + add( key, val ) { - }, + this[ key ] = val; - }; + } - // ************** UTILITY FUNCTIONS ************** +} - function isFbxFormatBinary( buffer ) { +// ************** UTILITY FUNCTIONS ************** - var CORRECT = 'Kaydara FBX Binary \0'; +function isFbxFormatBinary( buffer ) { - return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length ); + const CORRECT = 'Kaydara FBX Binary \0'; - } + return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length ); - function isFbxFormatASCII( text ) { +} - var CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ]; +function isFbxFormatASCII( text ) { - var cursor = 0; + const CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ]; - function read( offset ) { + let cursor = 0; - var result = text[ offset - 1 ]; - text = text.slice( cursor + offset ); - cursor ++; - return result; + function read( offset ) { - } + const result = text[ offset - 1 ]; + text = text.slice( cursor + offset ); + cursor ++; + return result; - for ( var i = 0; i < CORRECT.length; ++ i ) { + } - var num = read( 1 ); - if ( num === CORRECT[ i ] ) { + for ( let i = 0; i < CORRECT.length; ++ i ) { - return false; + const num = read( 1 ); + if ( num === CORRECT[ i ] ) { - } + return false; } - return true; - } - function getFbxVersion( text ) { + return true; - var versionRegExp = /FBXVersion: (\d+)/; - var match = text.match( versionRegExp ); +} - if ( match ) { +function getFbxVersion( text ) { - var version = parseInt( match[ 1 ] ); - return version; + const versionRegExp = /FBXVersion: (\d+)/; + const match = text.match( versionRegExp ); - } + if ( match ) { - throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); + const version = parseInt( match[ 1 ] ); + return version; } - // Converts FBX ticks into real time seconds. - function convertFBXTimeToSeconds( time ) { + throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); - return time / 46186158000; +} - } +// Converts FBX ticks into real time seconds. +function convertFBXTimeToSeconds( time ) { - var dataArray = []; + return time / 46186158000; - // extracts the data from the correct position in the FBX array based on indexing type - function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { +} - var index; +const dataArray = []; - switch ( infoObject.mappingType ) { +// extracts the data from the correct position in the FBX array based on indexing type +function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - case 'ByPolygonVertex' : - index = polygonVertexIndex; - break; - case 'ByPolygon' : - index = polygonIndex; - break; - case 'ByVertice' : - index = vertexIndex; - break; - case 'AllSame' : - index = infoObject.indices[ 0 ]; - break; - default : - console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType ); + let index; - } + switch ( infoObject.mappingType ) { - if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ]; + case 'ByPolygonVertex' : + index = polygonVertexIndex; + break; + case 'ByPolygon' : + index = polygonIndex; + break; + case 'ByVertice' : + index = vertexIndex; + break; + case 'AllSame' : + index = infoObject.indices[ 0 ]; + break; + default : + console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType ); - var from = index * infoObject.dataSize; - var to = from + infoObject.dataSize; + } - return slice( dataArray, infoObject.buffer, from, to ); + if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ]; - } + const from = index * infoObject.dataSize; + const to = from + infoObject.dataSize; - var tempEuler = new Euler(); - var tempVec = new Vector3(); + return slice( dataArray, infoObject.buffer, from, to ); - // generate transformation from FBX transform data - // ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm - // ref: http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_transformations_2main_8cxx-example.html,topicNumber=cpp_ref__transformations_2main_8cxx_example_htmlfc10a1e1-b18d-4e72-9dc0-70d0f1959f5e - function generateTransform( transformData ) { +} - var lTranslationM = new Matrix4(); - var lPreRotationM = new Matrix4(); - var lRotationM = new Matrix4(); - var lPostRotationM = new Matrix4(); +const tempEuler = new Euler(); +const tempVec = new Vector3(); - var lScalingM = new Matrix4(); - var lScalingPivotM = new Matrix4(); - var lScalingOffsetM = new Matrix4(); - var lRotationOffsetM = new Matrix4(); - var lRotationPivotM = new Matrix4(); +// generate transformation from FBX transform data +// ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm +// ref: http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_transformations_2main_8cxx-example.html,topicNumber=cpp_ref__transformations_2main_8cxx_example_htmlfc10a1e1-b18d-4e72-9dc0-70d0f1959f5e +function generateTransform( transformData ) { - var lParentGX = new Matrix4(); - var lParentLX = new Matrix4(); - var lGlobalT = new Matrix4(); + const lTranslationM = new Matrix4(); + const lPreRotationM = new Matrix4(); + const lRotationM = new Matrix4(); + const lPostRotationM = new Matrix4(); - var inheritType = ( transformData.inheritType ) ? transformData.inheritType : 0; + const lScalingM = new Matrix4(); + const lScalingPivotM = new Matrix4(); + const lScalingOffsetM = new Matrix4(); + const lRotationOffsetM = new Matrix4(); + const lRotationPivotM = new Matrix4(); - if ( transformData.translation ) lTranslationM.setPosition( tempVec.fromArray( transformData.translation ) ); + const lParentGX = new Matrix4(); + const lParentLX = new Matrix4(); + const lGlobalT = new Matrix4(); - if ( transformData.preRotation ) { + const inheritType = ( transformData.inheritType ) ? transformData.inheritType : 0; - var array = transformData.preRotation.map( MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + if ( transformData.translation ) lTranslationM.setPosition( tempVec.fromArray( transformData.translation ) ); - } + if ( transformData.preRotation ) { - if ( transformData.rotation ) { + const array = transformData.preRotation.map( MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - var array = transformData.rotation.map( MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + } - } + if ( transformData.rotation ) { - if ( transformData.postRotation ) { + const array = transformData.rotation.map( MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - var array = transformData.postRotation.map( MathUtils.degToRad ); - array.push( transformData.eulerOrder ); - lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); - lPostRotationM.invert(); + } - } + if ( transformData.postRotation ) { - if ( transformData.scale ) lScalingM.scale( tempVec.fromArray( transformData.scale ) ); + const array = transformData.postRotation.map( MathUtils.degToRad ); + array.push( transformData.eulerOrder ); + lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) ); + lPostRotationM.invert(); - // Pivots and offsets - if ( transformData.scalingOffset ) lScalingOffsetM.setPosition( tempVec.fromArray( transformData.scalingOffset ) ); - if ( transformData.scalingPivot ) lScalingPivotM.setPosition( tempVec.fromArray( transformData.scalingPivot ) ); - if ( transformData.rotationOffset ) lRotationOffsetM.setPosition( tempVec.fromArray( transformData.rotationOffset ) ); - if ( transformData.rotationPivot ) lRotationPivotM.setPosition( tempVec.fromArray( transformData.rotationPivot ) ); + } - // parent transform - if ( transformData.parentMatrixWorld ) { + if ( transformData.scale ) lScalingM.scale( tempVec.fromArray( transformData.scale ) ); - lParentLX.copy( transformData.parentMatrix ); - lParentGX.copy( transformData.parentMatrixWorld ); + // Pivots and offsets + if ( transformData.scalingOffset ) lScalingOffsetM.setPosition( tempVec.fromArray( transformData.scalingOffset ) ); + if ( transformData.scalingPivot ) lScalingPivotM.setPosition( tempVec.fromArray( transformData.scalingPivot ) ); + if ( transformData.rotationOffset ) lRotationOffsetM.setPosition( tempVec.fromArray( transformData.rotationOffset ) ); + if ( transformData.rotationPivot ) lRotationPivotM.setPosition( tempVec.fromArray( transformData.rotationPivot ) ); - } + // parent transform + if ( transformData.parentMatrixWorld ) { - var lLRM = new Matrix4().copy( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ); - // Global Rotation - var lParentGRM = new Matrix4(); - lParentGRM.extractRotation( lParentGX ); + lParentLX.copy( transformData.parentMatrix ); + lParentGX.copy( transformData.parentMatrixWorld ); - // Global Shear*Scaling - var lParentTM = new Matrix4(); - lParentTM.copyPosition( lParentGX ); + } - var lParentGSM = new Matrix4(); - var lParentGRSM = new Matrix4().copy( lParentTM ).invert().multiply( lParentGX ); - lParentGSM.copy( lParentGRM ).invert().multiply( lParentGRSM ); - var lLSM = lScalingM; + const lLRM = new Matrix4().copy( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ); + // Global Rotation + const lParentGRM = new Matrix4(); + lParentGRM.extractRotation( lParentGX ); - var lGlobalRS = new Matrix4(); + // Global Shear*Scaling + const lParentTM = new Matrix4(); + lParentTM.copyPosition( lParentGX ); - if ( inheritType === 0 ) { + const lParentGSM = new Matrix4(); + const lParentGRSM = new Matrix4().copy( lParentTM ).invert().multiply( lParentGX ); + lParentGSM.copy( lParentGRM ).invert().multiply( lParentGRSM ); + const lLSM = lScalingM; - lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM ).multiply( lLSM ); + const lGlobalRS = new Matrix4(); - } else if ( inheritType === 1 ) { + if ( inheritType === 0 ) { - lGlobalRS.copy( lParentGRM ).multiply( lParentGSM ).multiply( lLRM ).multiply( lLSM ); + lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM ).multiply( lLSM ); - } else { + } else if ( inheritType === 1 ) { - var lParentLSM = new Matrix4().scale( new Vector3().setFromMatrixScale( lParentLX ) ); - var lParentLSM_inv = new Matrix4().copy( lParentLSM ).invert(); - var lParentGSM_noLocal = new Matrix4().copy( lParentGSM ).multiply( lParentLSM_inv ); + lGlobalRS.copy( lParentGRM ).multiply( lParentGSM ).multiply( lLRM ).multiply( lLSM ); - lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM_noLocal ).multiply( lLSM ); + } else { - } + const lParentLSM = new Matrix4().scale( new Vector3().setFromMatrixScale( lParentLX ) ); + const lParentLSM_inv = new Matrix4().copy( lParentLSM ).invert(); + const lParentGSM_noLocal = new Matrix4().copy( lParentGSM ).multiply( lParentLSM_inv ); - var lRotationPivotM_inv = new Matrix4(); - lRotationPivotM_inv.copy( lRotationPivotM ).invert(); - var lScalingPivotM_inv = new Matrix4(); - lScalingPivotM_inv.copy( lScalingPivotM ).invert(); - // Calculate the local transform matrix - var lTransform = new Matrix4(); - lTransform.copy( lTranslationM ).multiply( lRotationOffsetM ).multiply( lRotationPivotM ).multiply( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ).multiply( lRotationPivotM_inv ).multiply( lScalingOffsetM ).multiply( lScalingPivotM ).multiply( lScalingM ).multiply( lScalingPivotM_inv ); + lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM_noLocal ).multiply( lLSM ); - var lLocalTWithAllPivotAndOffsetInfo = new Matrix4().copyPosition( lTransform ); + } - var lGlobalTranslation = new Matrix4().copy( lParentGX ).multiply( lLocalTWithAllPivotAndOffsetInfo ); - lGlobalT.copyPosition( lGlobalTranslation ); + const lRotationPivotM_inv = new Matrix4(); + lRotationPivotM_inv.copy( lRotationPivotM ).invert(); + const lScalingPivotM_inv = new Matrix4(); + lScalingPivotM_inv.copy( lScalingPivotM ).invert(); + // Calculate the local transform matrix + let lTransform = new Matrix4(); + lTransform.copy( lTranslationM ).multiply( lRotationOffsetM ).multiply( lRotationPivotM ).multiply( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ).multiply( lRotationPivotM_inv ).multiply( lScalingOffsetM ).multiply( lScalingPivotM ).multiply( lScalingM ).multiply( lScalingPivotM_inv ); - lTransform = new Matrix4().copy( lGlobalT ).multiply( lGlobalRS ); + const lLocalTWithAllPivotAndOffsetInfo = new Matrix4().copyPosition( lTransform ); - // from global to local - lTransform.premultiply( lParentGX.invert() ); + const lGlobalTranslation = new Matrix4().copy( lParentGX ).multiply( lLocalTWithAllPivotAndOffsetInfo ); + lGlobalT.copyPosition( lGlobalTranslation ); - return lTransform; + lTransform = new Matrix4().copy( lGlobalT ).multiply( lGlobalRS ); - } + // from global to local + lTransform.premultiply( lParentGX.invert() ); - // Returns the three.js intrinsic Euler order corresponding to FBX extrinsic Euler order - // ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html - function getEulerOrder( order ) { + return lTransform; - order = order || 0; +} - var enums = [ - 'ZYX', // -> XYZ extrinsic - 'YZX', // -> XZY extrinsic - 'XZY', // -> YZX extrinsic - 'ZXY', // -> YXZ extrinsic - 'YXZ', // -> ZXY extrinsic - 'XYZ', // -> ZYX extrinsic - //'SphericXYZ', // not possible to support - ]; +// Returns the three.js intrinsic Euler order corresponding to FBX extrinsic Euler order +// ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html +function getEulerOrder( order ) { - if ( order === 6 ) { + order = order || 0; - console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); - return enums[ 0 ]; + const enums = [ + 'ZYX', // -> XYZ extrinsic + 'YZX', // -> XZY extrinsic + 'XZY', // -> YZX extrinsic + 'ZXY', // -> YXZ extrinsic + 'YXZ', // -> ZXY extrinsic + 'XYZ', // -> ZYX extrinsic + //'SphericXYZ', // not possible to support + ]; - } + if ( order === 6 ) { - return enums[ order ]; + console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); + return enums[ 0 ]; } - // Parses comma separated list of numbers and returns them an array. - // Used internally by the TextParser - function parseNumberArray( value ) { + return enums[ order ]; - var array = value.split( ',' ).map( function ( val ) { +} - return parseFloat( val ); +// Parses comma separated list of numbers and returns them an array. +// Used internally by the TextParser +function parseNumberArray( value ) { - } ); + const array = value.split( ',' ).map( function ( val ) { - return array; + return parseFloat( val ); - } + } ); - function convertArrayBufferToString( buffer, from, to ) { + return array; - if ( from === undefined ) from = 0; - if ( to === undefined ) to = buffer.byteLength; +} - return LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); +function convertArrayBufferToString( buffer, from, to ) { - } + if ( from === undefined ) from = 0; + if ( to === undefined ) to = buffer.byteLength; - function append( a, b ) { + return LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); - for ( var i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { +} - a[ j ] = b[ i ]; +function append( a, b ) { - } + for ( let i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { - } + a[ j ] = b[ i ]; - function slice( a, b, from, to ) { + } - for ( var i = from, j = 0; i < to; i ++, j ++ ) { +} - a[ j ] = b[ i ]; +function slice( a, b, from, to ) { - } + for ( let i = from, j = 0; i < to; i ++, j ++ ) { - return a; + a[ j ] = b[ i ]; } - // inject array a2 into array a1 at index - function inject( a1, index, a2 ) { + return a; - return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) ); +} - } +// inject array a2 into array a1 at index +function inject( a1, index, a2 ) { - return FBXLoader; + return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) ); -} )(); +} export { FBXLoader }; diff --git a/examples/jsm/loaders/GCodeLoader.js b/examples/jsm/loaders/GCodeLoader.js index d1c4745abe08d4..41bc46ee36f89d 100644 --- a/examples/jsm/loaders/GCodeLoader.js +++ b/examples/jsm/loaders/GCodeLoader.js @@ -18,23 +18,21 @@ import { * @param {Manager} manager Loading manager. */ -var GCodeLoader = function ( manager ) { +class GCodeLoader extends Loader { - Loader.call( this, manager ); + constructor( manager ) { - this.splitLayer = false; + super( manager ); -}; + this.splitLayer = false; -GCodeLoader.prototype = Object.assign( Object.create( Loader.prototype ), { - - constructor: GCodeLoader, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; + const scope = this; - var loader = new FileLoader( scope.manager ); + const loader = new FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setRequestHeader( scope.requestHeader ); loader.setWithCredentials( scope.withCredentials ); @@ -62,9 +60,9 @@ GCodeLoader.prototype = Object.assign( Object.create( Loader.prototype ), { }, onProgress, onError ); - }, + } - parse: function ( data ) { + parse( data ) { var state = { x: 0, y: 0, z: 0, e: 0, f: 0, extruding: false, relative: false }; var layers = []; @@ -260,6 +258,6 @@ GCodeLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } -} ); +} export { GCodeLoader }; diff --git a/examples/jsm/loaders/LDrawLoader.js b/examples/jsm/loaders/LDrawLoader.js index 8add49ebdffcb9..86132172de251e 100644 --- a/examples/jsm/loaders/LDrawLoader.js +++ b/examples/jsm/loaders/LDrawLoader.js @@ -18,9 +18,26 @@ import { Vector3 } from '../../../build/three.module.js'; -var LDrawLoader = ( function () { - - var conditionalLineVertShader = /* glsl */` +// Special surface finish tag types. +// Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented +const FINISH_TYPE_DEFAULT = 0; +const FINISH_TYPE_CHROME = 1; +const FINISH_TYPE_PEARLESCENT = 2; +const FINISH_TYPE_RUBBER = 3; +const FINISH_TYPE_MATTE_METALLIC = 4; +const FINISH_TYPE_METAL = 5; + +// State machine to search a subobject path. +// The LDraw standard establishes these various possible subfolders. +const FILE_LOCATION_AS_IS = 0; +const FILE_LOCATION_TRY_PARTS = 1; +const FILE_LOCATION_TRY_P = 2; +const FILE_LOCATION_TRY_MODELS = 3; +const FILE_LOCATION_TRY_RELATIVE = 4; +const FILE_LOCATION_TRY_ABSOLUTE = 5; +const FILE_LOCATION_NOT_FOUND = 6; + +const conditionalLineVertShader = /* glsl */` attribute vec3 control0; attribute vec3 control1; attribute vec3 direction; @@ -68,7 +85,7 @@ var LDrawLoader = ( function () { } `; - var conditionalLineFragShader = /* glsl */` +const conditionalLineFragShader = /* glsl */` uniform vec3 diffuse; uniform float opacity; varying float discardFlag; @@ -96,178 +113,175 @@ var LDrawLoader = ( function () { } `; +const _tempVec0 = new Vector3(); +const _tempVec1 = new Vector3(); +function smoothNormals( triangles, lineSegments ) { - var tempVec0 = new Vector3(); - var tempVec1 = new Vector3(); - function smoothNormals( triangles, lineSegments ) { - - function hashVertex( v ) { - - // NOTE: 1e2 is pretty coarse but was chosen because it allows edges - // to be smoothed as expected (see minifig arms). The errors between edges - // could be due to matrix multiplication. - var x = ~ ~ ( v.x * 1e2 ); - var y = ~ ~ ( v.y * 1e2 ); - var z = ~ ~ ( v.z * 1e2 ); - return `${ x },${ y },${ z }`; + function hashVertex( v ) { - } + // NOTE: 1e2 is pretty coarse but was chosen because it allows edges + // to be smoothed as expected (see minifig arms). The errors between edges + // could be due to matrix multiplication. + const x = ~ ~ ( v.x * 1e2 ); + const y = ~ ~ ( v.y * 1e2 ); + const z = ~ ~ ( v.z * 1e2 ); + return `${ x },${ y },${ z }`; - function hashEdge( v0, v1 ) { + } - return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`; + function hashEdge( v0, v1 ) { - } + return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`; - var hardEdges = new Set(); - var halfEdgeList = {}; - var fullHalfEdgeList = {}; - var normals = []; + } - // Save the list of hard edges by hash - for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { + const hardEdges = new Set(); + const halfEdgeList = {}; + const fullHalfEdgeList = {}; + const normals = []; - var ls = lineSegments[ i ]; - var v0 = ls.v0; - var v1 = ls.v1; - hardEdges.add( hashEdge( v0, v1 ) ); - hardEdges.add( hashEdge( v1, v0 ) ); + // Save the list of hard edges by hash + for ( let i = 0, l = lineSegments.length; i < l; i ++ ) { - } + const ls = lineSegments[ i ]; + const v0 = ls.v0; + const v1 = ls.v1; + hardEdges.add( hashEdge( v0, v1 ) ); + hardEdges.add( hashEdge( v1, v0 ) ); - // track the half edges associated with each triangle - for ( var i = 0, l = triangles.length; i < l; i ++ ) { + } - var tri = triangles[ i ]; - for ( var i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { + // track the half edges associated with each triangle + for ( let i = 0, l = triangles.length; i < l; i ++ ) { - var index = i2; - var next = ( i2 + 1 ) % 3; - var v0 = tri[ `v${ index }` ]; - var v1 = tri[ `v${ next }` ]; - var hash = hashEdge( v0, v1 ); + const tri = triangles[ i ]; + for ( let i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { - // don't add the triangle if the edge is supposed to be hard - if ( hardEdges.has( hash ) ) continue; - halfEdgeList[ hash ] = tri; - fullHalfEdgeList[ hash ] = tri; + const index = i2; + const next = ( i2 + 1 ) % 3; + const v0 = tri[ `v${ index }` ]; + const v1 = tri[ `v${ next }` ]; + const hash = hashEdge( v0, v1 ); - } + // don't add the triangle if the edge is supposed to be hard + if ( hardEdges.has( hash ) ) continue; + halfEdgeList[ hash ] = tri; + fullHalfEdgeList[ hash ] = tri; } - // NOTE: Some of the normals wind up being skewed in an unexpected way because - // quads provide more "influence" to some vertex normals than a triangle due to - // the fact that a quad is made up of two triangles and all triangles are weighted - // equally. To fix this quads could be tracked separately so their vertex normals - // are weighted appropriately or we could try only adding a normal direction - // once per normal. + } - // Iterate until we've tried to connect all triangles to share normals - while ( true ) { + // NOTE: Some of the normals wind up being skewed in an unexpected way because + // quads provide more "influence" to some vertex normals than a triangle due to + // the fact that a quad is made up of two triangles and all triangles are weighted + // equally. To fix this quads could be tracked separately so their vertex normals + // are weighted appropriately or we could try only adding a normal direction + // once per normal. - // Stop if there are no more triangles left - var halfEdges = Object.keys( halfEdgeList ); - if ( halfEdges.length === 0 ) break; + // Iterate until we've tried to connect all triangles to share normals + while ( true ) { - // Exhaustively find all connected triangles - var i = 0; - var queue = [ fullHalfEdgeList[ halfEdges[ 0 ] ] ]; - while ( i < queue.length ) { + // Stop if there are no more triangles left + const halfEdges = Object.keys( halfEdgeList ); + if ( halfEdges.length === 0 ) break; - // initialize all vertex normals in this triangle - var tri = queue[ i ]; - i ++; + // Exhaustively find all connected triangles + let i = 0; + const queue = [ fullHalfEdgeList[ halfEdges[ 0 ] ] ]; + while ( i < queue.length ) { - var faceNormal = tri.faceNormal; - if ( tri.n0 === null ) { + // initialize all vertex normals in this triangle + const tri = queue[ i ]; + i ++; - tri.n0 = faceNormal.clone(); - normals.push( tri.n0 ); + const faceNormal = tri.faceNormal; + if ( tri.n0 === null ) { - } + tri.n0 = faceNormal.clone(); + normals.push( tri.n0 ); - if ( tri.n1 === null ) { + } - tri.n1 = faceNormal.clone(); - normals.push( tri.n1 ); + if ( tri.n1 === null ) { - } + tri.n1 = faceNormal.clone(); + normals.push( tri.n1 ); - if ( tri.n2 === null ) { + } - tri.n2 = faceNormal.clone(); - normals.push( tri.n2 ); + if ( tri.n2 === null ) { - } + tri.n2 = faceNormal.clone(); + normals.push( tri.n2 ); - // Check if any edge is connected to another triangle edge - for ( var i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { + } - var index = i2; - var next = ( i2 + 1 ) % 3; - var v0 = tri[ `v${ index }` ]; - var v1 = tri[ `v${ next }` ]; + // Check if any edge is connected to another triangle edge + for ( let i2 = 0, l2 = 3; i2 < l2; i2 ++ ) { - // delete this triangle from the list so it won't be found again - var hash = hashEdge( v0, v1 ); - delete halfEdgeList[ hash ]; + const index = i2; + const next = ( i2 + 1 ) % 3; + const v0 = tri[ `v${ index }` ]; + const v1 = tri[ `v${ next }` ]; - var reverseHash = hashEdge( v1, v0 ); - var otherTri = fullHalfEdgeList[ reverseHash ]; - if ( otherTri ) { + // delete this triangle from the list so it won't be found again + const hash = hashEdge( v0, v1 ); + delete halfEdgeList[ hash ]; - // NOTE: If the angle between triangles is > 67.5 degrees then assume it's - // hard edge. There are some cases where the line segments do not line up exactly - // with or span multiple triangle edges (see Lunar Vehicle wheels). - if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) { + const reverseHash = hashEdge( v1, v0 ); + const otherTri = fullHalfEdgeList[ reverseHash ]; + if ( otherTri ) { - continue; + // NOTE: If the angle between triangles is > 67.5 degrees then assume it's + // hard edge. There are some cases where the line segments do not line up exactly + // with or span multiple triangle edges (see Lunar Vehicle wheels). + if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) { - } + continue; - // if this triangle has already been traversed then it won't be in - // the halfEdgeList. If it has not then add it to the queue and delete - // it so it won't be found again. - if ( reverseHash in halfEdgeList ) { + } - queue.push( otherTri ); - delete halfEdgeList[ reverseHash ]; + // if this triangle has already been traversed then it won't be in + // the halfEdgeList. If it has not then add it to the queue and delete + // it so it won't be found again. + if ( reverseHash in halfEdgeList ) { - } + queue.push( otherTri ); + delete halfEdgeList[ reverseHash ]; - // Find the matching edge in this triangle and copy the normal vector over - for ( var i3 = 0, l3 = 3; i3 < l3; i3 ++ ) { - - var otherIndex = i3; - var otherNext = ( i3 + 1 ) % 3; - var otherV0 = otherTri[ `v${ otherIndex }` ]; - var otherV1 = otherTri[ `v${ otherNext }` ]; + } - var otherHash = hashEdge( otherV0, otherV1 ); - if ( otherHash === reverseHash ) { + // Find the matching edge in this triangle and copy the normal vector over + for ( let i3 = 0, l3 = 3; i3 < l3; i3 ++ ) { - if ( otherTri[ `n${ otherIndex }` ] === null ) { + const otherIndex = i3; + const otherNext = ( i3 + 1 ) % 3; + const otherV0 = otherTri[ `v${ otherIndex }` ]; + const otherV1 = otherTri[ `v${ otherNext }` ]; - var norm = tri[ `n${ next }` ]; - otherTri[ `n${ otherIndex }` ] = norm; - norm.add( otherTri.faceNormal ); + const otherHash = hashEdge( otherV0, otherV1 ); + if ( otherHash === reverseHash ) { - } + if ( otherTri[ `n${ otherIndex }` ] === null ) { - if ( otherTri[ `n${ otherNext }` ] === null ) { + const norm = tri[ `n${ next }` ]; + otherTri[ `n${ otherIndex }` ] = norm; + norm.add( otherTri.faceNormal ); - var norm = tri[ `n${ index }` ]; - otherTri[ `n${ otherNext }` ] = norm; - norm.add( otherTri.faceNormal ); + } - } + if ( otherTri[ `n${ otherNext }` ] === null ) { - break; + const norm = tri[ `n${ index }` ]; + otherTri[ `n${ otherNext }` ] = norm; + norm.add( otherTri.faceNormal ); } + break; + } } @@ -278,22 +292,26 @@ var LDrawLoader = ( function () { } - // The normals of each face have been added up so now we average them by normalizing the vector. - for ( var i = 0, l = normals.length; i < l; i ++ ) { + } - normals[ i ].normalize(); + // The normals of each face have been added up so now we average them by normalizing the vector. + for ( let i = 0, l = normals.length; i < l; i ++ ) { - } + normals[ i ].normalize(); } - function isPrimitiveType( type ) { +} - return /primitive/i.test( type ) || type === 'Subpart'; +function isPrimitiveType( type ) { - } + return /primitive/i.test( type ) || type === 'Subpart'; - function LineParser( line, lineNumber ) { +} + +class LineParser { + + constructor( line, lineNumber ) { this.line = line; this.lineLength = line.length; @@ -303,238 +321,235 @@ var LDrawLoader = ( function () { } - LineParser.prototype = { - - constructor: LineParser, - - seekNonSpace: function () { - - while ( this.currentCharIndex < this.lineLength ) { + seekNonSpace() { - this.currentChar = this.line.charAt( this.currentCharIndex ); + while ( this.currentCharIndex < this.lineLength ) { - if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) { + this.currentChar = this.line.charAt( this.currentCharIndex ); - return; - - } + if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) { - this.currentCharIndex ++; + return; } - }, + this.currentCharIndex ++; - getToken: function () { + } - var pos0 = this.currentCharIndex ++; + } - // Seek space - while ( this.currentCharIndex < this.lineLength ) { + getToken() { - this.currentChar = this.line.charAt( this.currentCharIndex ); + const pos0 = this.currentCharIndex ++; - if ( this.currentChar === ' ' || this.currentChar === '\t' ) { + // Seek space + while ( this.currentCharIndex < this.lineLength ) { - break; + this.currentChar = this.line.charAt( this.currentCharIndex ); - } + if ( this.currentChar === ' ' || this.currentChar === '\t' ) { - this.currentCharIndex ++; + break; } - var pos1 = this.currentCharIndex; - - this.seekNonSpace(); + this.currentCharIndex ++; - return this.line.substring( pos0, pos1 ); + } - }, + const pos1 = this.currentCharIndex; - getRemainingString: function () { + this.seekNonSpace(); - return this.line.substring( this.currentCharIndex, this.lineLength ); + return this.line.substring( pos0, pos1 ); - }, + } - isAtTheEnd: function () { + getRemainingString() { - return this.currentCharIndex >= this.lineLength; + return this.line.substring( this.currentCharIndex, this.lineLength ); - }, + } - setToEnd: function () { + isAtTheEnd() { - this.currentCharIndex = this.lineLength; + return this.currentCharIndex >= this.lineLength; - }, + } - getLineNumberString: function () { + setToEnd() { - return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''; + this.currentCharIndex = this.lineLength; - } + } + getLineNumberString() { - }; + return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''; - function sortByMaterial( a, b ) { + } - if ( a.colourCode === b.colourCode ) { +} - return 0; +function sortByMaterial( a, b ) { - } + if ( a.colourCode === b.colourCode ) { - if ( a.colourCode < b.colourCode ) { + return 0; - return - 1; + } - } + if ( a.colourCode < b.colourCode ) { - return 1; + return - 1; } - function createObject( elements, elementSize, isConditionalSegments ) { + return 1; - // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 ) - // With per face / segment material, implemented with mesh groups and materials array +} - // Sort the triangles or line segments by colour code to make later the mesh groups - elements.sort( sortByMaterial ); +function createObject( elements, elementSize, isConditionalSegments ) { - var positions = []; - var normals = []; - var materials = []; + // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 ) + // With per face / segment material, implemented with mesh groups and materials array - var bufferGeometry = new BufferGeometry(); - var prevMaterial = null; - var index0 = 0; - var numGroupVerts = 0; + // Sort the triangles or line segments by colour code to make later the mesh groups + elements.sort( sortByMaterial ); - for ( var iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) { + const positions = []; + const normals = []; + const materials = []; - var elem = elements[ iElem ]; - var v0 = elem.v0; - var v1 = elem.v1; - // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one - positions.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z ); - if ( elementSize === 3 ) { + const bufferGeometry = new BufferGeometry(); + let prevMaterial = null; + let index0 = 0; + let numGroupVerts = 0; - positions.push( elem.v2.x, elem.v2.y, elem.v2.z ); + for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) { - var n0 = elem.n0 || elem.faceNormal; - var n1 = elem.n1 || elem.faceNormal; - var n2 = elem.n2 || elem.faceNormal; - normals.push( n0.x, n0.y, n0.z ); - normals.push( n1.x, n1.y, n1.z ); - normals.push( n2.x, n2.y, n2.z ); + const elem = elements[ iElem ]; + const v0 = elem.v0; + const v1 = elem.v1; + // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one + positions.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z ); + if ( elementSize === 3 ) { - } + positions.push( elem.v2.x, elem.v2.y, elem.v2.z ); - if ( prevMaterial !== elem.material ) { + const n0 = elem.n0 || elem.faceNormal; + const n1 = elem.n1 || elem.faceNormal; + const n2 = elem.n2 || elem.faceNormal; + normals.push( n0.x, n0.y, n0.z ); + normals.push( n1.x, n1.y, n1.z ); + normals.push( n2.x, n2.y, n2.z ); - if ( prevMaterial !== null ) { + } - bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 ); + if ( prevMaterial !== elem.material ) { - } + if ( prevMaterial !== null ) { - materials.push( elem.material ); + bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 ); - prevMaterial = elem.material; - index0 = iElem * elementSize; - numGroupVerts = elementSize; + } - } else { + materials.push( elem.material ); - numGroupVerts += elementSize; + prevMaterial = elem.material; + index0 = iElem * elementSize; + numGroupVerts = elementSize; - } + } else { - } + numGroupVerts += elementSize; - if ( numGroupVerts > 0 ) { + } - bufferGeometry.addGroup( index0, Infinity, materials.length - 1 ); + } - } + if ( numGroupVerts > 0 ) { - bufferGeometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); + bufferGeometry.addGroup( index0, Infinity, materials.length - 1 ); - if ( elementSize === 3 ) { + } - bufferGeometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); + bufferGeometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); - } + if ( elementSize === 3 ) { - var object3d = null; + bufferGeometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); - if ( elementSize === 2 ) { + } - object3d = new LineSegments( bufferGeometry, materials ); + let object3d = null; - } else if ( elementSize === 3 ) { + if ( elementSize === 2 ) { - object3d = new Mesh( bufferGeometry, materials ); + object3d = new LineSegments( bufferGeometry, materials ); - } + } else if ( elementSize === 3 ) { - if ( isConditionalSegments ) { - - object3d.isConditionalLine = true; - - var controlArray0 = new Float32Array( elements.length * 3 * 2 ); - var controlArray1 = new Float32Array( elements.length * 3 * 2 ); - var directionArray = new Float32Array( elements.length * 3 * 2 ); - for ( var i = 0, l = elements.length; i < l; i ++ ) { - - var os = elements[ i ]; - var c0 = os.c0; - var c1 = os.c1; - var v0 = os.v0; - var v1 = os.v1; - var index = i * 3 * 2; - controlArray0[ index + 0 ] = c0.x; - controlArray0[ index + 1 ] = c0.y; - controlArray0[ index + 2 ] = c0.z; - controlArray0[ index + 3 ] = c0.x; - controlArray0[ index + 4 ] = c0.y; - controlArray0[ index + 5 ] = c0.z; - - controlArray1[ index + 0 ] = c1.x; - controlArray1[ index + 1 ] = c1.y; - controlArray1[ index + 2 ] = c1.z; - controlArray1[ index + 3 ] = c1.x; - controlArray1[ index + 4 ] = c1.y; - controlArray1[ index + 5 ] = c1.z; - - directionArray[ index + 0 ] = v1.x - v0.x; - directionArray[ index + 1 ] = v1.y - v0.y; - directionArray[ index + 2 ] = v1.z - v0.z; - directionArray[ index + 3 ] = v1.x - v0.x; - directionArray[ index + 4 ] = v1.y - v0.y; - directionArray[ index + 5 ] = v1.z - v0.z; + object3d = new Mesh( bufferGeometry, materials ); - } + } - bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) ); - bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) ); - bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) ); + if ( isConditionalSegments ) { + + object3d.isConditionalLine = true; + + const controlArray0 = new Float32Array( elements.length * 3 * 2 ); + const controlArray1 = new Float32Array( elements.length * 3 * 2 ); + const directionArray = new Float32Array( elements.length * 3 * 2 ); + for ( let i = 0, l = elements.length; i < l; i ++ ) { + + const os = elements[ i ]; + const c0 = os.c0; + const c1 = os.c1; + const v0 = os.v0; + const v1 = os.v1; + const index = i * 3 * 2; + controlArray0[ index + 0 ] = c0.x; + controlArray0[ index + 1 ] = c0.y; + controlArray0[ index + 2 ] = c0.z; + controlArray0[ index + 3 ] = c0.x; + controlArray0[ index + 4 ] = c0.y; + controlArray0[ index + 5 ] = c0.z; + + controlArray1[ index + 0 ] = c1.x; + controlArray1[ index + 1 ] = c1.y; + controlArray1[ index + 2 ] = c1.z; + controlArray1[ index + 3 ] = c1.x; + controlArray1[ index + 4 ] = c1.y; + controlArray1[ index + 5 ] = c1.z; + + directionArray[ index + 0 ] = v1.x - v0.x; + directionArray[ index + 1 ] = v1.y - v0.y; + directionArray[ index + 2 ] = v1.z - v0.z; + directionArray[ index + 3 ] = v1.x - v0.x; + directionArray[ index + 4 ] = v1.y - v0.y; + directionArray[ index + 5 ] = v1.z - v0.z; } - return object3d; + bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) ); + bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) ); + bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) ); } - // + return object3d; + +} + +// - function LDrawLoader( manager ) { +class LDrawLoader extends Loader { - Loader.call( this, manager ); + constructor( manager ) { + + super( manager ); // This is a stack of 'parse scopes' with one level per subobject loaded file. // Each level contains a material lib and also other runtime variables passed between parent and child subobjects @@ -567,984 +582,987 @@ var LDrawLoader = ( function () { } - // Special surface finish tag types. - // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented - LDrawLoader.FINISH_TYPE_DEFAULT = 0; - LDrawLoader.FINISH_TYPE_CHROME = 1; - LDrawLoader.FINISH_TYPE_PEARLESCENT = 2; - LDrawLoader.FINISH_TYPE_RUBBER = 3; - LDrawLoader.FINISH_TYPE_MATTE_METALLIC = 4; - LDrawLoader.FINISH_TYPE_METAL = 5; - - // State machine to search a subobject path. - // The LDraw standard establishes these various possible subfolders. - LDrawLoader.FILE_LOCATION_AS_IS = 0; - LDrawLoader.FILE_LOCATION_TRY_PARTS = 1; - LDrawLoader.FILE_LOCATION_TRY_P = 2; - LDrawLoader.FILE_LOCATION_TRY_MODELS = 3; - LDrawLoader.FILE_LOCATION_TRY_RELATIVE = 4; - LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE = 5; - LDrawLoader.FILE_LOCATION_NOT_FOUND = 6; + load( url, onLoad, onProgress, onError ) { - LDrawLoader.prototype = Object.assign( Object.create( Loader.prototype ), { + if ( ! this.fileMap ) { - constructor: LDrawLoader, - - load: function ( url, onLoad, onProgress, onError ) { - - if ( ! this.fileMap ) { - - this.fileMap = {}; - - } + this.fileMap = {}; - var scope = this; + } - var fileLoader = new FileLoader( this.manager ); - fileLoader.setPath( this.path ); - fileLoader.setRequestHeader( this.requestHeader ); - fileLoader.setWithCredentials( this.withCredentials ); - fileLoader.load( url, function ( text ) { + const scope = this; - scope.processObject( text, onLoad, null, url ); + const fileLoader = new FileLoader( this.manager ); + fileLoader.setPath( this.path ); + fileLoader.setRequestHeader( this.requestHeader ); + fileLoader.setWithCredentials( this.withCredentials ); + fileLoader.load( url, function ( text ) { - }, onProgress, onError ); + scope.processObject( text, onLoad, null, url ); - }, + }, onProgress, onError ); - parse: function ( text, path, onLoad ) { + } - // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter + parse( text, path, onLoad ) { - this.processObject( text, onLoad, null, path ); + // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter - }, + this.processObject( text, onLoad, null, path ); - setMaterials: function ( materials ) { + } - // Clears parse scopes stack, adds new scope with material library + setMaterials( materials ) { - this.parseScopesStack = []; + // Clears parse scopes stack, adds new scope with material library - this.newParseScopeLevel( materials ); + this.parseScopesStack = []; - this.getCurrentParseScope().isFromParse = false; + this.newParseScopeLevel( materials ); - this.materials = materials; + this.getCurrentParseScope().isFromParse = false; - return this; + this.materials = materials; - }, + return this; - setFileMap: function ( fileMap ) { + } - this.fileMap = fileMap; + setFileMap( fileMap ) { - return this; + this.fileMap = fileMap; - }, + return this; - newParseScopeLevel: function ( materials ) { + } - // Adds a new scope level, assign materials to it and returns it + newParseScopeLevel( materials ) { - var matLib = {}; + // Adds a new scope level, assign materials to it and returns it - if ( materials ) { + const matLib = {}; - for ( var i = 0, n = materials.length; i < n; i ++ ) { + if ( materials ) { - var material = materials[ i ]; - matLib[ material.userData.code ] = material; + for ( let i = 0, n = materials.length; i < n; i ++ ) { - } + const material = materials[ i ]; + matLib[ material.userData.code ] = material; } - var topParseScope = this.getCurrentParseScope(); - var newParseScope = { + } - lib: matLib, - url: null, + const topParseScope = this.getCurrentParseScope(); + const newParseScope = { - // Subobjects - subobjects: null, - numSubobjects: 0, - subobjectIndex: 0, - inverted: false, - category: null, - keywords: null, + lib: matLib, + url: null, - // Current subobject - currentFileName: null, - mainColourCode: topParseScope ? topParseScope.mainColourCode : '16', - mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24', - currentMatrix: new Matrix4(), - matrix: new Matrix4(), + // Subobjects + subobjects: null, + numSubobjects: 0, + subobjectIndex: 0, + inverted: false, + category: null, + keywords: null, - // If false, it is a root material scope previous to parse - isFromParse: true, + // Current subobject + currentFileName: null, + mainColourCode: topParseScope ? topParseScope.mainColourCode : '16', + mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24', + currentMatrix: new Matrix4(), + matrix: new Matrix4(), - triangles: null, - lineSegments: null, - conditionalSegments: null, + // If false, it is a root material scope previous to parse + isFromParse: true, - // If true, this object is the start of a construction step - startingConstructionStep: false - }; + triangles: null, + lineSegments: null, + conditionalSegments: null, - this.parseScopesStack.push( newParseScope ); + // If true, this object is the start of a construction step + startingConstructionStep: false + }; - return newParseScope; + this.parseScopesStack.push( newParseScope ); - }, + return newParseScope; - removeScopeLevel: function () { + } - this.parseScopesStack.pop(); + removeScopeLevel() { - return this; + this.parseScopesStack.pop(); - }, + return this; - addMaterial: function ( material ) { + } - // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array + addMaterial( material ) { - var matLib = this.getCurrentParseScope().lib; + // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array - if ( ! matLib[ material.userData.code ] ) { + const matLib = this.getCurrentParseScope().lib; - this.materials.push( material ); + if ( ! matLib[ material.userData.code ] ) { - } + this.materials.push( material ); - matLib[ material.userData.code ] = material; + } - return this; + matLib[ material.userData.code ] = material; - }, + return this; - getMaterial: function ( colourCode ) { + } - // Given a colour code search its material in the parse scopes stack + getMaterial( colourCode ) { - if ( colourCode.startsWith( '0x2' ) ) { + // Given a colour code search its material in the parse scopes stack - // Special 'direct' material value (RGB colour) + if ( colourCode.startsWith( '0x2' ) ) { - var colour = colourCode.substring( 3 ); + // Special 'direct' material value (RGB colour) - return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) ); + const colour = colourCode.substring( 3 ); - } + return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) ); - for ( var i = this.parseScopesStack.length - 1; i >= 0; i -- ) { + } - var material = this.parseScopesStack[ i ].lib[ colourCode ]; + for ( let i = this.parseScopesStack.length - 1; i >= 0; i -- ) { - if ( material ) { + const material = this.parseScopesStack[ i ].lib[ colourCode ]; - return material; + if ( material ) { - } + return material; } - // Material was not found - return null; + } - }, + // Material was not found + return null; - getParentParseScope: function () { + } - if ( this.parseScopesStack.length > 1 ) { + getParentParseScope() { - return this.parseScopesStack[ this.parseScopesStack.length - 2 ]; + if ( this.parseScopesStack.length > 1 ) { - } + return this.parseScopesStack[ this.parseScopesStack.length - 2 ]; - return null; - - }, + } - getCurrentParseScope: function () { + return null; - if ( this.parseScopesStack.length > 0 ) { + } - return this.parseScopesStack[ this.parseScopesStack.length - 1 ]; + getCurrentParseScope() { - } + if ( this.parseScopesStack.length > 0 ) { - return null; + return this.parseScopesStack[ this.parseScopesStack.length - 1 ]; - }, + } - parseColourMetaDirective: function ( lineParser ) { + return null; - // Parses a colour definition and returns a THREE.Material or null if error + } - var code = null; + parseColourMetaDirective( lineParser ) { - // Triangle and line colours - var colour = 0xFF00FF; - var edgeColour = 0xFF00FF; + // Parses a colour definition and returns a THREE.Material or null if error - // Transparency - var alpha = 1; - var isTransparent = false; - // Self-illumination: - var luminance = 0; + var code = null; - var finishType = LDrawLoader.FINISH_TYPE_DEFAULT; - var canHaveEnvMap = true; + // Triangle and line colours + var colour = 0xFF00FF; + var edgeColour = 0xFF00FF; - var edgeMaterial = null; + // Transparency + var alpha = 1; + var isTransparent = false; + // Self-illumination: + var luminance = 0; - var name = lineParser.getToken(); - if ( ! name ) { + var finishType = FINISH_TYPE_DEFAULT; + var canHaveEnvMap = true; - throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.'; + var edgeMaterial = null; - } + var name = lineParser.getToken(); + if ( ! name ) { - // Parse tag tokens and their parameters - var token = null; - while ( true ) { + throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.'; - token = lineParser.getToken(); + } - if ( ! token ) { + // Parse tag tokens and their parameters + var token = null; + while ( true ) { - break; + token = lineParser.getToken(); - } + if ( ! token ) { - switch ( token.toUpperCase() ) { + break; - case 'CODE': + } - code = lineParser.getToken(); - break; + switch ( token.toUpperCase() ) { - case 'VALUE': + case 'CODE': - colour = lineParser.getToken(); - if ( colour.startsWith( '0x' ) ) { + code = lineParser.getToken(); + break; - colour = '#' + colour.substring( 2 ); + case 'VALUE': - } else if ( ! colour.startsWith( '#' ) ) { + colour = lineParser.getToken(); + if ( colour.startsWith( '0x' ) ) { - throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.'; + colour = '#' + colour.substring( 2 ); - } + } else if ( ! colour.startsWith( '#' ) ) { - break; + throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.'; - case 'EDGE': + } - edgeColour = lineParser.getToken(); - if ( edgeColour.startsWith( '0x' ) ) { + break; - edgeColour = '#' + edgeColour.substring( 2 ); + case 'EDGE': - } else if ( ! edgeColour.startsWith( '#' ) ) { + edgeColour = lineParser.getToken(); + if ( edgeColour.startsWith( '0x' ) ) { - // Try to see if edge colour is a colour code - edgeMaterial = this.getMaterial( edgeColour ); - if ( ! edgeMaterial ) { + edgeColour = '#' + edgeColour.substring( 2 ); - throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.'; + } else if ( ! edgeColour.startsWith( '#' ) ) { - } + // Try to see if edge colour is a colour code + edgeMaterial = this.getMaterial( edgeColour ); + if ( ! edgeMaterial ) { - // Get the edge material for this triangle material - edgeMaterial = edgeMaterial.userData.edgeMaterial; + throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.'; } - break; + // Get the edge material for this triangle material + edgeMaterial = edgeMaterial.userData.edgeMaterial; - case 'ALPHA': + } - alpha = parseInt( lineParser.getToken() ); + break; - if ( isNaN( alpha ) ) { + case 'ALPHA': - throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.'; + alpha = parseInt( lineParser.getToken() ); - } + if ( isNaN( alpha ) ) { - alpha = Math.max( 0, Math.min( 1, alpha / 255 ) ); + throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.'; - if ( alpha < 1 ) { + } - isTransparent = true; + alpha = Math.max( 0, Math.min( 1, alpha / 255 ) ); - } + if ( alpha < 1 ) { - break; + isTransparent = true; - case 'LUMINANCE': + } - luminance = parseInt( lineParser.getToken() ); + break; - if ( isNaN( luminance ) ) { + case 'LUMINANCE': - throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.'; + luminance = parseInt( lineParser.getToken() ); - } + if ( isNaN( luminance ) ) { - luminance = Math.max( 0, Math.min( 1, luminance / 255 ) ); + throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.'; - break; + } - case 'CHROME': - finishType = LDrawLoader.FINISH_TYPE_CHROME; - break; + luminance = Math.max( 0, Math.min( 1, luminance / 255 ) ); - case 'PEARLESCENT': - finishType = LDrawLoader.FINISH_TYPE_PEARLESCENT; - break; + break; - case 'RUBBER': - finishType = LDrawLoader.FINISH_TYPE_RUBBER; - break; + case 'CHROME': + finishType = FINISH_TYPE_CHROME; + break; - case 'MATTE_METALLIC': - finishType = LDrawLoader.FINISH_TYPE_MATTE_METALLIC; - break; + case 'PEARLESCENT': + finishType = FINISH_TYPE_PEARLESCENT; + break; - case 'METAL': - finishType = LDrawLoader.FINISH_TYPE_METAL; - break; + case 'RUBBER': + finishType = FINISH_TYPE_RUBBER; + break; - case 'MATERIAL': - // Not implemented - lineParser.setToEnd(); - break; + case 'MATTE_METALLIC': + finishType = FINISH_TYPE_MATTE_METALLIC; + break; - default: - throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.'; - break; + case 'METAL': + finishType = FINISH_TYPE_METAL; + break; - } + case 'MATERIAL': + // Not implemented + lineParser.setToEnd(); + break; + + default: + throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.'; + break; } - var material = null; + } - switch ( finishType ) { + var material = null; - case LDrawLoader.FINISH_TYPE_DEFAULT: + switch ( finishType ) { - material = new MeshStandardMaterial( { color: colour, roughness: 0.3, envMapIntensity: 0.3, metalness: 0 } ); - break; + case FINISH_TYPE_DEFAULT: - case LDrawLoader.FINISH_TYPE_PEARLESCENT: + material = new MeshStandardMaterial( { color: colour, roughness: 0.3, envMapIntensity: 0.3, metalness: 0 } ); + break; - // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess - var specular = new Color( colour ); - var hsl = specular.getHSL( { h: 0, s: 0, l: 0 } ); - hsl.h = ( hsl.h + 0.5 ) % 1; - hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 ); - specular.setHSL( hsl.h, hsl.s, hsl.l ); + case FINISH_TYPE_PEARLESCENT: - material = new MeshPhongMaterial( { color: colour, specular: specular, shininess: 10, reflectivity: 0.3 } ); - break; + // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess + var specular = new Color( colour ); + var hsl = specular.getHSL( { h: 0, s: 0, l: 0 } ); + hsl.h = ( hsl.h + 0.5 ) % 1; + hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 ); + specular.setHSL( hsl.h, hsl.s, hsl.l ); - case LDrawLoader.FINISH_TYPE_CHROME: + material = new MeshPhongMaterial( { color: colour, specular: specular, shininess: 10, reflectivity: 0.3 } ); + break; - // Mirror finish surface - material = new MeshStandardMaterial( { color: colour, roughness: 0, metalness: 1 } ); - break; + case FINISH_TYPE_CHROME: - case LDrawLoader.FINISH_TYPE_RUBBER: + // Mirror finish surface + material = new MeshStandardMaterial( { color: colour, roughness: 0, metalness: 1 } ); + break; - // Rubber finish - material = new MeshStandardMaterial( { color: colour, roughness: 0.9, metalness: 0 } ); - canHaveEnvMap = false; - break; + case FINISH_TYPE_RUBBER: - case LDrawLoader.FINISH_TYPE_MATTE_METALLIC: + // Rubber finish + material = new MeshStandardMaterial( { color: colour, roughness: 0.9, metalness: 0 } ); + canHaveEnvMap = false; + break; - // Brushed metal finish - material = new MeshStandardMaterial( { color: colour, roughness: 0.8, metalness: 0.4 } ); - break; + case FINISH_TYPE_MATTE_METALLIC: - case LDrawLoader.FINISH_TYPE_METAL: + // Brushed metal finish + material = new MeshStandardMaterial( { color: colour, roughness: 0.8, metalness: 0.4 } ); + break; - // Average metal finish - material = new MeshStandardMaterial( { color: colour, roughness: 0.2, metalness: 0.85 } ); - break; + case FINISH_TYPE_METAL: - default: - // Should not happen - break; - - } + // Average metal finish + material = new MeshStandardMaterial( { color: colour, roughness: 0.2, metalness: 0.85 } ); + break; - material.transparent = isTransparent; - material.premultipliedAlpha = true; - material.opacity = alpha; - material.depthWrite = ! isTransparent; + default: + // Should not happen + break; - material.polygonOffset = true; - material.polygonOffsetFactor = 1; + } - material.userData.canHaveEnvMap = canHaveEnvMap; + material.transparent = isTransparent; + material.premultipliedAlpha = true; + material.opacity = alpha; + material.depthWrite = ! isTransparent; - if ( luminance !== 0 ) { + material.polygonOffset = true; + material.polygonOffsetFactor = 1; - material.emissive.set( material.color ).multiplyScalar( luminance ); + material.userData.canHaveEnvMap = canHaveEnvMap; - } + if ( luminance !== 0 ) { - if ( ! edgeMaterial ) { + material.emissive.set( material.color ).multiplyScalar( luminance ); - // This is the material used for edges - edgeMaterial = new LineBasicMaterial( { - color: edgeColour, - transparent: isTransparent, - opacity: alpha, - depthWrite: ! isTransparent - } ); - edgeMaterial.userData.code = code; - edgeMaterial.name = name + ' - Edge'; - edgeMaterial.userData.canHaveEnvMap = false; - - // This is the material used for conditional edges - edgeMaterial.userData.conditionalEdgeMaterial = new ShaderMaterial( { - vertexShader: conditionalLineVertShader, - fragmentShader: conditionalLineFragShader, - uniforms: UniformsUtils.merge( [ - UniformsLib.fog, - { - diffuse: { - value: new Color( edgeColour ) - }, - opacity: { - value: alpha - } - } - ] ), - fog: true, - transparent: isTransparent, - depthWrite: ! isTransparent - } ); - edgeMaterial.userData.conditionalEdgeMaterial.userData.canHaveEnvMap = false; + } - } + if ( ! edgeMaterial ) { - material.userData.code = code; - material.name = name; + // This is the material used for edges + edgeMaterial = new LineBasicMaterial( { + color: edgeColour, + transparent: isTransparent, + opacity: alpha, + depthWrite: ! isTransparent + } ); + edgeMaterial.userData.code = code; + edgeMaterial.name = name + ' - Edge'; + edgeMaterial.userData.canHaveEnvMap = false; + + // This is the material used for conditional edges + edgeMaterial.userData.conditionalEdgeMaterial = new ShaderMaterial( { + vertexShader: conditionalLineVertShader, + fragmentShader: conditionalLineFragShader, + uniforms: UniformsUtils.merge( [ + UniformsLib.fog, + { + diffuse: { + value: new Color( edgeColour ) + }, + opacity: { + value: alpha + } + } + ] ), + fog: true, + transparent: isTransparent, + depthWrite: ! isTransparent + } ); + edgeMaterial.userData.conditionalEdgeMaterial.userData.canHaveEnvMap = false; - material.userData.edgeMaterial = edgeMaterial; + } - return material; + material.userData.code = code; + material.name = name; - }, + material.userData.edgeMaterial = edgeMaterial; - // + return material; - objectParse: function ( text ) { + } - // Retrieve data from the parent parse scope - var parentParseScope = this.getParentParseScope(); + // - // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object) - var mainColourCode = parentParseScope.mainColourCode; - var mainEdgeColourCode = parentParseScope.mainEdgeColourCode; + objectParse( text ) { - var currentParseScope = this.getCurrentParseScope(); + // Retrieve data from the parent parse scope + var parentParseScope = this.getParentParseScope(); - // Parse result variables - var triangles; - var lineSegments; - var conditionalSegments; + // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object) + var mainColourCode = parentParseScope.mainColourCode; + var mainEdgeColourCode = parentParseScope.mainEdgeColourCode; - var subobjects = []; + var currentParseScope = this.getCurrentParseScope(); - var category = null; - var keywords = null; + // Parse result variables + var triangles; + var lineSegments; + var conditionalSegments; - if ( text.indexOf( '\r\n' ) !== - 1 ) { + var subobjects = []; - // This is faster than String.split with regex that splits on both - text = text.replace( /\r\n/g, '\n' ); + var category = null; + var keywords = null; - } + if ( text.indexOf( '\r\n' ) !== - 1 ) { - var lines = text.split( '\n' ); - var numLines = lines.length; - var lineIndex = 0; + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); - var parsingEmbeddedFiles = false; - var currentEmbeddedFileName = null; - var currentEmbeddedText = null; + } - var bfcCertified = false; - var bfcCCW = true; - var bfcInverted = false; - var bfcCull = true; - var type = ''; + var lines = text.split( '\n' ); + var numLines = lines.length; + var lineIndex = 0; - var startingConstructionStep = false; + var parsingEmbeddedFiles = false; + var currentEmbeddedFileName = null; + var currentEmbeddedText = null; - var scope = this; - function parseColourCode( lineParser, forEdge ) { + var bfcCertified = false; + var bfcCCW = true; + var bfcInverted = false; + var bfcCull = true; + var type = ''; - // Parses next colour code and returns a THREE.Material + var startingConstructionStep = false; - var colourCode = lineParser.getToken(); + var scope = this; + function parseColourCode( lineParser, forEdge ) { - if ( ! forEdge && colourCode === '16' ) { + // Parses next colour code and returns a THREE.Material - colourCode = mainColourCode; + var colourCode = lineParser.getToken(); - } + if ( ! forEdge && colourCode === '16' ) { - if ( forEdge && colourCode === '24' ) { + colourCode = mainColourCode; - colourCode = mainEdgeColourCode; + } - } + if ( forEdge && colourCode === '24' ) { - var material = scope.getMaterial( colourCode ); + colourCode = mainEdgeColourCode; - if ( ! material ) { + } - throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.'; + var material = scope.getMaterial( colourCode ); - } + if ( ! material ) { - return material; + throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.'; } - function parseVector( lp ) { + return material; - var v = new Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) ); + } - if ( ! scope.separateObjects ) { + function parseVector( lp ) { - v.applyMatrix4( currentParseScope.currentMatrix ); + var v = new Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) ); - } + if ( ! scope.separateObjects ) { - return v; + v.applyMatrix4( currentParseScope.currentMatrix ); } - // Parse all line commands - for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) { + return v; - var line = lines[ lineIndex ]; + } - if ( line.length === 0 ) continue; + // Parse all line commands + for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) { - if ( parsingEmbeddedFiles ) { + var line = lines[ lineIndex ]; - if ( line.startsWith( '0 FILE ' ) ) { + if ( line.length === 0 ) continue; - // Save previous embedded file in the cache - this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; + if ( parsingEmbeddedFiles ) { - // New embedded text file - currentEmbeddedFileName = line.substring( 7 ); - currentEmbeddedText = ''; + if ( line.startsWith( '0 FILE ' ) ) { - } else { + // Save previous embedded file in the cache + this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; - currentEmbeddedText += line + '\n'; + // New embedded text file + currentEmbeddedFileName = line.substring( 7 ); + currentEmbeddedText = ''; - } + } else { - continue; + currentEmbeddedText += line + '\n'; } - var lp = new LineParser( line, lineIndex + 1 ); + continue; - lp.seekNonSpace(); + } - if ( lp.isAtTheEnd() ) { + var lp = new LineParser( line, lineIndex + 1 ); - // Empty line - continue; + lp.seekNonSpace(); - } + if ( lp.isAtTheEnd() ) { - // Parse the line type - var lineType = lp.getToken(); + // Empty line + continue; - switch ( lineType ) { + } - // Line type 0: Comment or META - case '0': + // Parse the line type + var lineType = lp.getToken(); - // Parse meta directive - var meta = lp.getToken(); + switch ( lineType ) { - if ( meta ) { + // Line type 0: Comment or META + case '0': - switch ( meta ) { + // Parse meta directive + var meta = lp.getToken(); - case '!LDRAW_ORG': + if ( meta ) { - type = lp.getToken(); + switch ( meta ) { - currentParseScope.triangles = []; - currentParseScope.lineSegments = []; - currentParseScope.conditionalSegments = []; - currentParseScope.type = type; + case '!LDRAW_ORG': - var isRoot = ! parentParseScope.isFromParse; - if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) { + type = lp.getToken(); - currentParseScope.groupObject = new Group(); + currentParseScope.triangles = []; + currentParseScope.lineSegments = []; + currentParseScope.conditionalSegments = []; + currentParseScope.type = type; - currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep; + var isRoot = ! parentParseScope.isFromParse; + if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) { - } + currentParseScope.groupObject = new Group(); - // If the scale of the object is negated then the triangle winding order - // needs to be flipped. - var matrix = currentParseScope.matrix; - if ( - matrix.determinant() < 0 && ( - scope.separateObjects && isPrimitiveType( type ) || + currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep; + + } + + // If the scale of the object is negated then the triangle winding order + // needs to be flipped. + var matrix = currentParseScope.matrix; + if ( + matrix.determinant() < 0 && ( + scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects - ) ) { + ) ) { - currentParseScope.inverted = ! currentParseScope.inverted; + currentParseScope.inverted = ! currentParseScope.inverted; - } + } - triangles = currentParseScope.triangles; - lineSegments = currentParseScope.lineSegments; - conditionalSegments = currentParseScope.conditionalSegments; + triangles = currentParseScope.triangles; + lineSegments = currentParseScope.lineSegments; + conditionalSegments = currentParseScope.conditionalSegments; - break; + break; - case '!COLOUR': + case '!COLOUR': - var material = this.parseColourMetaDirective( lp ); - if ( material ) { + var material = this.parseColourMetaDirective( lp ); + if ( material ) { - this.addMaterial( material ); + this.addMaterial( material ); - } else { + } else { - console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() ); + console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() ); - } + } - break; + break; - case '!CATEGORY': + case '!CATEGORY': - category = lp.getToken(); - break; + category = lp.getToken(); + break; - case '!KEYWORDS': + case '!KEYWORDS': - var newKeywords = lp.getRemainingString().split( ',' ); - if ( newKeywords.length > 0 ) { + var newKeywords = lp.getRemainingString().split( ',' ); + if ( newKeywords.length > 0 ) { - if ( ! keywords ) { + if ( ! keywords ) { - keywords = []; + keywords = []; - } + } - newKeywords.forEach( function ( keyword ) { + newKeywords.forEach( function ( keyword ) { - keywords.push( keyword.trim() ); + keywords.push( keyword.trim() ); - } ); + } ); - } + } - break; + break; - case 'FILE': + case 'FILE': - if ( lineIndex > 0 ) { + if ( lineIndex > 0 ) { - // Start embedded text files parsing - parsingEmbeddedFiles = true; - currentEmbeddedFileName = lp.getRemainingString(); - currentEmbeddedText = ''; + // Start embedded text files parsing + parsingEmbeddedFiles = true; + currentEmbeddedFileName = lp.getRemainingString(); + currentEmbeddedText = ''; - bfcCertified = false; - bfcCCW = true; + bfcCertified = false; + bfcCCW = true; - } + } - break; + break; - case 'BFC': + case 'BFC': - // Changes to the backface culling state - while ( ! lp.isAtTheEnd() ) { + // Changes to the backface culling state + while ( ! lp.isAtTheEnd() ) { - var token = lp.getToken(); + var token = lp.getToken(); - switch ( token ) { + switch ( token ) { - case 'CERTIFY': - case 'NOCERTIFY': + case 'CERTIFY': + case 'NOCERTIFY': - bfcCertified = token === 'CERTIFY'; - bfcCCW = true; + bfcCertified = token === 'CERTIFY'; + bfcCCW = true; - break; + break; - case 'CW': - case 'CCW': + case 'CW': + case 'CCW': - bfcCCW = token === 'CCW'; + bfcCCW = token === 'CCW'; - break; + break; - case 'INVERTNEXT': + case 'INVERTNEXT': - bfcInverted = true; + bfcInverted = true; - break; + break; - case 'CLIP': - case 'NOCLIP': + case 'CLIP': + case 'NOCLIP': bfcCull = token === 'CLIP'; - break; - - default: + break; - console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' ); + default: - break; + console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' ); - } + break; } - break; + } - case 'STEP': + break; - startingConstructionStep = true; + case 'STEP': - break; + startingConstructionStep = true; - default: - // Other meta directives are not implemented - break; + break; - } + default: + // Other meta directives are not implemented + break; } - break; + } - // Line type 1: Sub-object file - case '1': + break; - var material = parseColourCode( lp ); + // Line type 1: Sub-object file + case '1': - var posX = parseFloat( lp.getToken() ); - var posY = parseFloat( lp.getToken() ); - var posZ = parseFloat( lp.getToken() ); - var m0 = parseFloat( lp.getToken() ); - var m1 = parseFloat( lp.getToken() ); - var m2 = parseFloat( lp.getToken() ); - var m3 = parseFloat( lp.getToken() ); - var m4 = parseFloat( lp.getToken() ); - var m5 = parseFloat( lp.getToken() ); - var m6 = parseFloat( lp.getToken() ); - var m7 = parseFloat( lp.getToken() ); - var m8 = parseFloat( lp.getToken() ); + var material = parseColourCode( lp ); - var matrix = new Matrix4().set( - m0, m1, m2, posX, - m3, m4, m5, posY, - m6, m7, m8, posZ, - 0, 0, 0, 1 - ); + var posX = parseFloat( lp.getToken() ); + var posY = parseFloat( lp.getToken() ); + var posZ = parseFloat( lp.getToken() ); + var m0 = parseFloat( lp.getToken() ); + var m1 = parseFloat( lp.getToken() ); + var m2 = parseFloat( lp.getToken() ); + var m3 = parseFloat( lp.getToken() ); + var m4 = parseFloat( lp.getToken() ); + var m5 = parseFloat( lp.getToken() ); + var m6 = parseFloat( lp.getToken() ); + var m7 = parseFloat( lp.getToken() ); + var m8 = parseFloat( lp.getToken() ); - var fileName = lp.getRemainingString().trim().replace( /\\/g, '/' ); + var matrix = new Matrix4().set( + m0, m1, m2, posX, + m3, m4, m5, posY, + m6, m7, m8, posZ, + 0, 0, 0, 1 + ); - if ( scope.fileMap[ fileName ] ) { + var fileName = lp.getRemainingString().trim().replace( /\\/g, '/' ); - // Found the subobject path in the preloaded file path map - fileName = scope.fileMap[ fileName ]; + if ( scope.fileMap[ fileName ] ) { - } else { + // Found the subobject path in the preloaded file path map + fileName = scope.fileMap[ fileName ]; - // Standardized subfolders - if ( fileName.startsWith( 's/' ) ) { + } else { - fileName = 'parts/' + fileName; + // Standardized subfolders + if ( fileName.startsWith( 's/' ) ) { - } else if ( fileName.startsWith( '48/' ) ) { + fileName = 'parts/' + fileName; - fileName = 'p/' + fileName; + } else if ( fileName.startsWith( '48/' ) ) { - } + fileName = 'p/' + fileName; } - subobjects.push( { - material: material, - matrix: matrix, - fileName: fileName, - originalFileName: fileName, - locationState: LDrawLoader.FILE_LOCATION_AS_IS, - url: null, - triedLowerCase: false, - inverted: bfcInverted !== currentParseScope.inverted, - startingConstructionStep: startingConstructionStep - } ); + } - bfcInverted = false; + subobjects.push( { + material: material, + matrix: matrix, + fileName: fileName, + originalFileName: fileName, + locationState: FILE_LOCATION_AS_IS, + url: null, + triedLowerCase: false, + inverted: bfcInverted !== currentParseScope.inverted, + startingConstructionStep: startingConstructionStep + } ); + + bfcInverted = false; - break; + break; // Line type 2: Line segment - case '2': + case '2': - var material = parseColourCode( lp, true ); + var material = parseColourCode( lp, true ); - var segment = { - material: material.userData.edgeMaterial, - colourCode: material.userData.code, - v0: parseVector( lp ), - v1: parseVector( lp ) - }; + var segment = { + material: material.userData.edgeMaterial, + colourCode: material.userData.code, + v0: parseVector( lp ), + v1: parseVector( lp ) + }; - lineSegments.push( segment ); + lineSegments.push( segment ); - break; + break; // Line type 5: Conditional Line segment - case '5': + case '5': - var material = parseColourCode( lp, true ); + var material = parseColourCode( lp, true ); - var segment = { - material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial, - colourCode: material.userData.code, - v0: parseVector( lp ), - v1: parseVector( lp ), - c0: parseVector( lp ), - c1: parseVector( lp ) - }; + var segment = { + material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial, + colourCode: material.userData.code, + v0: parseVector( lp ), + v1: parseVector( lp ), + c0: parseVector( lp ), + c1: parseVector( lp ) + }; - conditionalSegments.push( segment ); + conditionalSegments.push( segment ); - break; + break; // Line type 3: Triangle - case '3': + case '3': - var material = parseColourCode( lp ); + var material = parseColourCode( lp ); - var inverted = currentParseScope.inverted; - var ccw = bfcCCW !== inverted; - var doubleSided = ! bfcCertified || ! bfcCull; - var v0, v1, v2, faceNormal; + var inverted = currentParseScope.inverted; + var ccw = bfcCCW !== inverted; + var doubleSided = ! bfcCertified || ! bfcCull; + var v0, v1, v2, faceNormal; - if ( ccw === true ) { + if ( ccw === true ) { - v0 = parseVector( lp ); - v1 = parseVector( lp ); - v2 = parseVector( lp ); + v0 = parseVector( lp ); + v1 = parseVector( lp ); + v2 = parseVector( lp ); - } else { + } else { - v2 = parseVector( lp ); - v1 = parseVector( lp ); - v0 = parseVector( lp ); + v2 = parseVector( lp ); + v1 = parseVector( lp ); + v0 = parseVector( lp ); - } + } - tempVec0.subVectors( v1, v0 ); - tempVec1.subVectors( v2, v1 ); - faceNormal = new Vector3() - .crossVectors( tempVec0, tempVec1 ) - .normalize(); + _tempVec0.subVectors( v1, v0 ); + _tempVec1.subVectors( v2, v1 ); + faceNormal = new Vector3() + .crossVectors( _tempVec0, _tempVec1 ) + .normalize(); + + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v1, + v2: v2, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); + + if ( doubleSided === true ) { triangles.push( { material: material, colourCode: material.userData.code, v0: v0, - v1: v1, - v2: v2, + v1: v2, + v2: v1, faceNormal: faceNormal, n0: null, n1: null, n2: null } ); - if ( doubleSided === true ) { + } - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v2, - v2: v1, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); + break; - } + // Line type 4: Quadrilateral + case '4': - break; + var material = parseColourCode( lp ); - // Line type 4: Quadrilateral - case '4': + var inverted = currentParseScope.inverted; + var ccw = bfcCCW !== inverted; + var doubleSided = ! bfcCertified || ! bfcCull; + var v0, v1, v2, v3, faceNormal; - var material = parseColourCode( lp ); + if ( ccw === true ) { - var inverted = currentParseScope.inverted; - var ccw = bfcCCW !== inverted; - var doubleSided = ! bfcCertified || ! bfcCull; - var v0, v1, v2, v3, faceNormal; + v0 = parseVector( lp ); + v1 = parseVector( lp ); + v2 = parseVector( lp ); + v3 = parseVector( lp ); - if ( ccw === true ) { + } else { - v0 = parseVector( lp ); - v1 = parseVector( lp ); - v2 = parseVector( lp ); - v3 = parseVector( lp ); + v3 = parseVector( lp ); + v2 = parseVector( lp ); + v1 = parseVector( lp ); + v0 = parseVector( lp ); - } else { + } - v3 = parseVector( lp ); - v2 = parseVector( lp ); - v1 = parseVector( lp ); - v0 = parseVector( lp ); + _tempVec0.subVectors( v1, v0 ); + _tempVec1.subVectors( v2, v1 ); + faceNormal = new Vector3() + .crossVectors( _tempVec0, _tempVec1 ) + .normalize(); + + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v1, + v2: v2, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); - } + triangles.push( { + material: material, + colourCode: material.userData.code, + v0: v0, + v1: v2, + v2: v3, + faceNormal: faceNormal, + n0: null, + n1: null, + n2: null + } ); - tempVec0.subVectors( v1, v0 ); - tempVec1.subVectors( v2, v1 ); - faceNormal = new Vector3() - .crossVectors( tempVec0, tempVec1 ) - .normalize(); + if ( doubleSided === true ) { triangles.push( { material: material, colourCode: material.userData.code, v0: v0, - v1: v1, - v2: v2, + v1: v2, + v2: v1, faceNormal: faceNormal, n0: null, n1: null, @@ -1555,418 +1573,388 @@ var LDrawLoader = ( function () { material: material, colourCode: material.userData.code, v0: v0, - v1: v2, - v2: v3, + v1: v3, + v2: v2, faceNormal: faceNormal, n0: null, n1: null, n2: null } ); - if ( doubleSided === true ) { - - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v2, - v2: v1, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); - - triangles.push( { - material: material, - colourCode: material.userData.code, - v0: v0, - v1: v3, - v2: v2, - faceNormal: faceNormal, - n0: null, - n1: null, - n2: null - } ); - - } - - break; + } - default: - throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.'; - break; + break; - } + default: + throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.'; + break; } - if ( parsingEmbeddedFiles ) { + } - this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; + if ( parsingEmbeddedFiles ) { - } + this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText; - currentParseScope.category = category; - currentParseScope.keywords = keywords; - currentParseScope.subobjects = subobjects; - currentParseScope.numSubobjects = subobjects.length; - currentParseScope.subobjectIndex = 0; - - }, + } - computeConstructionSteps: function ( model ) { + currentParseScope.category = category; + currentParseScope.keywords = keywords; + currentParseScope.subobjects = subobjects; + currentParseScope.numSubobjects = subobjects.length; + currentParseScope.subobjectIndex = 0; - // Sets userdata.constructionStep number in Group objects and userData.numConstructionSteps number in the root Group object. + } - var stepNumber = 0; + computeConstructionSteps( model ) { - model.traverse( c => { + // Sets userdata.constructionStep number in Group objects and userData.numConstructionSteps number in the root Group object. - if ( c.isGroup ) { + var stepNumber = 0; - if ( c.userData.startingConstructionStep ) { + model.traverse( c => { - stepNumber ++; + if ( c.isGroup ) { - } + if ( c.userData.startingConstructionStep ) { - c.userData.constructionStep = stepNumber; + stepNumber ++; } - } ); + c.userData.constructionStep = stepNumber; - model.userData.numConstructionSteps = stepNumber + 1; + } - }, + } ); - processObject: function ( text, onProcessed, subobject, url ) { + model.userData.numConstructionSteps = stepNumber + 1; - var scope = this; + } - var parseScope = scope.newParseScopeLevel(); - parseScope.url = url; + processObject( text, onProcessed, subobject, url ) { - var parentParseScope = scope.getParentParseScope(); + var scope = this; - // Set current matrix - if ( subobject ) { + var parseScope = scope.newParseScopeLevel(); + parseScope.url = url; - parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix ); - parseScope.matrix.copy( subobject.matrix ); - parseScope.inverted = subobject.inverted; - parseScope.startingConstructionStep = subobject.startingConstructionStep; + var parentParseScope = scope.getParentParseScope(); - } + // Set current matrix + if ( subobject ) { - // Add to cache - var currentFileName = parentParseScope.currentFileName; - if ( currentFileName !== null ) { + parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix ); + parseScope.matrix.copy( subobject.matrix ); + parseScope.inverted = subobject.inverted; + parseScope.startingConstructionStep = subobject.startingConstructionStep; - currentFileName = parentParseScope.currentFileName.toLowerCase(); + } - } + // Add to cache + var currentFileName = parentParseScope.currentFileName; + if ( currentFileName !== null ) { - if ( scope.subobjectCache[ currentFileName ] === undefined ) { + currentFileName = parentParseScope.currentFileName.toLowerCase(); - scope.subobjectCache[ currentFileName ] = text; + } - } + if ( scope.subobjectCache[ currentFileName ] === undefined ) { + scope.subobjectCache[ currentFileName ] = text; - // Parse the object (returns a Group) - scope.objectParse( text ); - var finishedCount = 0; - onSubobjectFinish(); + } - function onSubobjectFinish() { - finishedCount ++; + // Parse the object (returns a Group) + scope.objectParse( text ); + var finishedCount = 0; + onSubobjectFinish(); - if ( finishedCount === parseScope.subobjects.length + 1 ) { + function onSubobjectFinish() { - finalizeObject(); + finishedCount ++; - } else { + if ( finishedCount === parseScope.subobjects.length + 1 ) { - // Once the previous subobject has finished we can start processing the next one in the list. - // The subobject processing shares scope in processing so it's important that they be loaded serially - // to avoid race conditions. - // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to - // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame - // will work but causes the load to happen after the next frame which causes the load to take significantly longer. - var subobject = parseScope.subobjects[ parseScope.subobjectIndex ]; - Promise.resolve().then( function () { + finalizeObject(); - loadSubobject( subobject ); + } else { - } ); - parseScope.subobjectIndex ++; + // Once the previous subobject has finished we can start processing the next one in the list. + // The subobject processing shares scope in processing so it's important that they be loaded serially + // to avoid race conditions. + // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to + // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame + // will work but causes the load to happen after the next frame which causes the load to take significantly longer. + var subobject = parseScope.subobjects[ parseScope.subobjectIndex ]; + Promise.resolve().then( function () { - } + loadSubobject( subobject ); - } + } ); + parseScope.subobjectIndex ++; - function finalizeObject() { + } - if ( scope.smoothNormals && parseScope.type === 'Part' ) { + } - smoothNormals( parseScope.triangles, parseScope.lineSegments ); + function finalizeObject() { - } + if ( scope.smoothNormals && parseScope.type === 'Part' ) { - var isRoot = ! parentParseScope.isFromParse; - if ( scope.separateObjects && ! isPrimitiveType( parseScope.type ) || isRoot ) { + smoothNormals( parseScope.triangles, parseScope.lineSegments ); - const objGroup = parseScope.groupObject; + } - if ( parseScope.triangles.length > 0 ) { + var isRoot = ! parentParseScope.isFromParse; + if ( scope.separateObjects && ! isPrimitiveType( parseScope.type ) || isRoot ) { - objGroup.add( createObject( parseScope.triangles, 3 ) ); + const objGroup = parseScope.groupObject; - } + if ( parseScope.triangles.length > 0 ) { - if ( parseScope.lineSegments.length > 0 ) { + objGroup.add( createObject( parseScope.triangles, 3 ) ); - objGroup.add( createObject( parseScope.lineSegments, 2 ) ); + } - } + if ( parseScope.lineSegments.length > 0 ) { - if ( parseScope.conditionalSegments.length > 0 ) { + objGroup.add( createObject( parseScope.lineSegments, 2 ) ); - objGroup.add( createObject( parseScope.conditionalSegments, 2, true ) ); + } - } + if ( parseScope.conditionalSegments.length > 0 ) { - if ( parentParseScope.groupObject ) { + objGroup.add( createObject( parseScope.conditionalSegments, 2, true ) ); - objGroup.name = parseScope.fileName; - objGroup.userData.category = parseScope.category; - objGroup.userData.keywords = parseScope.keywords; - parseScope.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale ); + } - parentParseScope.groupObject.add( objGroup ); + if ( parentParseScope.groupObject ) { - } + objGroup.name = parseScope.fileName; + objGroup.userData.category = parseScope.category; + objGroup.userData.keywords = parseScope.keywords; + parseScope.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale ); - } else { + parentParseScope.groupObject.add( objGroup ); - var separateObjects = scope.separateObjects; - var parentLineSegments = parentParseScope.lineSegments; - var parentConditionalSegments = parentParseScope.conditionalSegments; - var parentTriangles = parentParseScope.triangles; + } - var lineSegments = parseScope.lineSegments; - var conditionalSegments = parseScope.conditionalSegments; - var triangles = parseScope.triangles; + } else { - for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { + var separateObjects = scope.separateObjects; + var parentLineSegments = parentParseScope.lineSegments; + var parentConditionalSegments = parentParseScope.conditionalSegments; + var parentTriangles = parentParseScope.triangles; - var ls = lineSegments[ i ]; + var lineSegments = parseScope.lineSegments; + var conditionalSegments = parseScope.conditionalSegments; + var triangles = parseScope.triangles; - if ( separateObjects ) { + for ( var i = 0, l = lineSegments.length; i < l; i ++ ) { - ls.v0.applyMatrix4( parseScope.matrix ); - ls.v1.applyMatrix4( parseScope.matrix ); + var ls = lineSegments[ i ]; - } + if ( separateObjects ) { - parentLineSegments.push( ls ); + ls.v0.applyMatrix4( parseScope.matrix ); + ls.v1.applyMatrix4( parseScope.matrix ); } - for ( var i = 0, l = conditionalSegments.length; i < l; i ++ ) { + parentLineSegments.push( ls ); - var os = conditionalSegments[ i ]; + } - if ( separateObjects ) { + for ( var i = 0, l = conditionalSegments.length; i < l; i ++ ) { - os.v0.applyMatrix4( parseScope.matrix ); - os.v1.applyMatrix4( parseScope.matrix ); - os.c0.applyMatrix4( parseScope.matrix ); - os.c1.applyMatrix4( parseScope.matrix ); + var os = conditionalSegments[ i ]; - } + if ( separateObjects ) { - parentConditionalSegments.push( os ); + os.v0.applyMatrix4( parseScope.matrix ); + os.v1.applyMatrix4( parseScope.matrix ); + os.c0.applyMatrix4( parseScope.matrix ); + os.c1.applyMatrix4( parseScope.matrix ); } - for ( var i = 0, l = triangles.length; i < l; i ++ ) { + parentConditionalSegments.push( os ); - var tri = triangles[ i ]; + } - if ( separateObjects ) { + for ( var i = 0, l = triangles.length; i < l; i ++ ) { - tri.v0 = tri.v0.clone().applyMatrix4( parseScope.matrix ); - tri.v1 = tri.v1.clone().applyMatrix4( parseScope.matrix ); - tri.v2 = tri.v2.clone().applyMatrix4( parseScope.matrix ); + var tri = triangles[ i ]; - tempVec0.subVectors( tri.v1, tri.v0 ); - tempVec1.subVectors( tri.v2, tri.v1 ); - tri.faceNormal.crossVectors( tempVec0, tempVec1 ).normalize(); + if ( separateObjects ) { - } + tri.v0 = tri.v0.clone().applyMatrix4( parseScope.matrix ); + tri.v1 = tri.v1.clone().applyMatrix4( parseScope.matrix ); + tri.v2 = tri.v2.clone().applyMatrix4( parseScope.matrix ); - parentTriangles.push( tri ); + _tempVec0.subVectors( tri.v1, tri.v0 ); + _tempVec1.subVectors( tri.v2, tri.v1 ); + tri.faceNormal.crossVectors( _tempVec0, _tempVec1 ).normalize(); } + parentTriangles.push( tri ); + } - scope.removeScopeLevel(); + } - // If it is root object, compute construction steps - if ( ! parentParseScope.isFromParse ) { + scope.removeScopeLevel(); - scope.computeConstructionSteps( parseScope.groupObject ); + // If it is root object, compute construction steps + if ( ! parentParseScope.isFromParse ) { - } + scope.computeConstructionSteps( parseScope.groupObject ); - if ( onProcessed ) { + } - onProcessed( parseScope.groupObject ); + if ( onProcessed ) { - } + onProcessed( parseScope.groupObject ); } - function loadSubobject( subobject ) { + } - parseScope.mainColourCode = subobject.material.userData.code; - parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code; - parseScope.currentFileName = subobject.originalFileName; + function loadSubobject( subobject ) { + parseScope.mainColourCode = subobject.material.userData.code; + parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code; + parseScope.currentFileName = subobject.originalFileName; - // If subobject was cached previously, use the cached one - var cached = scope.subobjectCache[ subobject.originalFileName.toLowerCase() ]; - if ( cached ) { - scope.processObject( cached, function ( subobjectGroup ) { + // If subobject was cached previously, use the cached one + var cached = scope.subobjectCache[ subobject.originalFileName.toLowerCase() ]; + if ( cached ) { - onSubobjectLoaded( subobjectGroup, subobject ); - onSubobjectFinish(); + scope.processObject( cached, function ( subobjectGroup ) { - }, subobject, url ); + onSubobjectLoaded( subobjectGroup, subobject ); + onSubobjectFinish(); - return; - - } + }, subobject, url ); - // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path) - // Update also subobject.locationState for the next try if this load fails. - var subobjectURL = subobject.fileName; - var newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND; + return; - switch ( subobject.locationState ) { + } - case LDrawLoader.FILE_LOCATION_AS_IS: - newLocationState = subobject.locationState + 1; - break; + // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path) + // Update also subobject.locationState for the next try if this load fails. + var subobjectURL = subobject.fileName; + var newLocationState = FILE_LOCATION_NOT_FOUND; - case LDrawLoader.FILE_LOCATION_TRY_PARTS: - subobjectURL = 'parts/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + switch ( subobject.locationState ) { - case LDrawLoader.FILE_LOCATION_TRY_P: - subobjectURL = 'p/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + case FILE_LOCATION_AS_IS: + newLocationState = subobject.locationState + 1; + break; - case LDrawLoader.FILE_LOCATION_TRY_MODELS: - subobjectURL = 'models/' + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + case FILE_LOCATION_TRY_PARTS: + subobjectURL = 'parts/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - case LDrawLoader.FILE_LOCATION_TRY_RELATIVE: - subobjectURL = url.substring( 0, url.lastIndexOf( '/' ) + 1 ) + subobjectURL; - newLocationState = subobject.locationState + 1; - break; + case FILE_LOCATION_TRY_P: + subobjectURL = 'p/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - case LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE: + case FILE_LOCATION_TRY_MODELS: + subobjectURL = 'models/' + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - if ( subobject.triedLowerCase ) { + case FILE_LOCATION_TRY_RELATIVE: + subobjectURL = url.substring( 0, url.lastIndexOf( '/' ) + 1 ) + subobjectURL; + newLocationState = subobject.locationState + 1; + break; - // Try absolute path - newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND; + case FILE_LOCATION_TRY_ABSOLUTE: - } else { + if ( subobject.triedLowerCase ) { - // Next attempt is lower case - subobject.fileName = subobject.fileName.toLowerCase(); - subobjectURL = subobject.fileName; - subobject.triedLowerCase = true; - newLocationState = LDrawLoader.FILE_LOCATION_AS_IS; + // Try absolute path + newLocationState = FILE_LOCATION_NOT_FOUND; - } + } else { - break; + // Next attempt is lower case + subobject.fileName = subobject.fileName.toLowerCase(); + subobjectURL = subobject.fileName; + subobject.triedLowerCase = true; + newLocationState = FILE_LOCATION_AS_IS; - case LDrawLoader.FILE_LOCATION_NOT_FOUND: + } - // All location possibilities have been tried, give up loading this object - console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' ); + break; - return; + case FILE_LOCATION_NOT_FOUND: - } + // All location possibilities have been tried, give up loading this object + console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' ); - subobject.locationState = newLocationState; - subobject.url = subobjectURL; + return; - // Load the subobject - // Use another file loader here so we can keep track of the subobject information - // and use it when processing the next model. - var fileLoader = new FileLoader( scope.manager ); - fileLoader.setPath( scope.path ); - fileLoader.setRequestHeader( scope.requestHeader ); - fileLoader.setWithCredentials( scope.withCredentials ); - fileLoader.load( subobjectURL, function ( text ) { + } - scope.processObject( text, function ( subobjectGroup ) { + subobject.locationState = newLocationState; + subobject.url = subobjectURL; - onSubobjectLoaded( subobjectGroup, subobject ); - onSubobjectFinish(); + // Load the subobject + // Use another file loader here so we can keep track of the subobject information + // and use it when processing the next model. + var fileLoader = new FileLoader( scope.manager ); + fileLoader.setPath( scope.path ); + fileLoader.setRequestHeader( scope.requestHeader ); + fileLoader.setWithCredentials( scope.withCredentials ); + fileLoader.load( subobjectURL, function ( text ) { - }, subobject, url ); + scope.processObject( text, function ( subobjectGroup ) { - }, undefined, function ( err ) { + onSubobjectLoaded( subobjectGroup, subobject ); + onSubobjectFinish(); - onSubobjectError( err, subobject ); + }, subobject, url ); - }, subobject ); + }, undefined, function ( err ) { - } + onSubobjectError( err, subobject ); - function onSubobjectLoaded( subobjectGroup, subobject ) { + }, subobject ); - if ( subobjectGroup === null ) { + } - // Try to reload - loadSubobject( subobject ); - return; + function onSubobjectLoaded( subobjectGroup, subobject ) { - } + if ( subobjectGroup === null ) { - scope.fileMap[ subobject.originalFileName ] = subobject.url; + // Try to reload + loadSubobject( subobject ); + return; } - function onSubobjectError( err, subobject ) { + scope.fileMap[ subobject.originalFileName ] = subobject.url; - // Retry download from a different default possible location - loadSubobject( subobject ); + } - } + function onSubobjectError( err, subobject ) { - } + // Retry download from a different default possible location + loadSubobject( subobject ); - } ); + } - return LDrawLoader; + } -} )(); +} export { LDrawLoader }; diff --git a/examples/jsm/loaders/MMDLoader.js b/examples/jsm/loaders/MMDLoader.js index 63662a8582efc8..4870e806a0b392 100644 --- a/examples/jsm/loaders/MMDLoader.js +++ b/examples/jsm/loaders/MMDLoader.js @@ -62,14 +62,14 @@ import { MMDParser } from '../libs/mmdparser.module.js'; * - shadow support. */ -var MMDLoader = ( function () { +/** + * @param {THREE.LoadingManager} manager + */ +class MMDLoader extends Loader { - /** - * @param {THREE.LoadingManager} manager - */ - function MMDLoader( manager ) { + constructor( manager ) { - Loader.call( this, manager ); + super( manager ); this.loader = new FileLoader( this.manager ); @@ -79,1027 +79,1010 @@ var MMDLoader = ( function () { } - MMDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { - - constructor: MMDLoader, - - /** - * @param {string} animationPath - * @return {MMDLoader} - */ - setAnimationPath: function ( animationPath ) { + /** + * @param {string} animationPath + * @return {MMDLoader} + */ + setAnimationPath( animationPath ) { - this.animationPath = animationPath; - return this; + this.animationPath = animationPath; + return this; - }, + } - // Load MMD assets as Three.js Object + // Load MMD assets as Three.js Object - /** - * Loads Model file (.pmd or .pmx) as a SkinnedMesh. - * - * @param {string} url - url to Model(.pmd or .pmx) file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - load: function ( url, onLoad, onProgress, onError ) { + /** + * Loads Model file (.pmd or .pmx) as a SkinnedMesh. + * + * @param {string} url - url to Model(.pmd or .pmx) file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + load( url, onLoad, onProgress, onError ) { - var builder = this.meshBuilder.setCrossOrigin( this.crossOrigin ); + const builder = this.meshBuilder.setCrossOrigin( this.crossOrigin ); - // resource path + // resource path - var resourcePath; + let resourcePath; - if ( this.resourcePath !== '' ) { + if ( this.resourcePath !== '' ) { - resourcePath = this.resourcePath; + resourcePath = this.resourcePath; - } else if ( this.path !== '' ) { + } else if ( this.path !== '' ) { - resourcePath = this.path; + resourcePath = this.path; - } else { + } else { - resourcePath = LoaderUtils.extractUrlBase( url ); + resourcePath = LoaderUtils.extractUrlBase( url ); - } - - var modelExtension = this._extractExtension( url ).toLowerCase(); + } - // Should I detect by seeing header? - if ( modelExtension !== 'pmd' && modelExtension !== 'pmx' ) { + const modelExtension = this._extractExtension( url ).toLowerCase(); - if ( onError ) onError( new Error( 'THREE.MMDLoader: Unknown model file extension .' + modelExtension + '.' ) ); + // Should I detect by seeing header? + if ( modelExtension !== 'pmd' && modelExtension !== 'pmx' ) { - return; + if ( onError ) onError( new Error( 'THREE.MMDLoader: Unknown model file extension .' + modelExtension + '.' ) ); - } + return; - this[ modelExtension === 'pmd' ? 'loadPMD' : 'loadPMX' ]( url, function ( data ) { + } - onLoad( builder.build( data, resourcePath, onProgress, onError ) ); + this[ modelExtension === 'pmd' ? 'loadPMD' : 'loadPMX' ]( url, function ( data ) { - }, onProgress, onError ); + onLoad( builder.build( data, resourcePath, onProgress, onError ) ); - }, + }, onProgress, onError ); - /** - * Loads Motion file(s) (.vmd) as a AnimationClip. - * If two or more files are specified, they'll be merged. - * - * @param {string|Array} url - url(s) to animation(.vmd) file(s) - * @param {SkinnedMesh|THREE.Camera} object - tracks will be fitting to this object - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadAnimation: function ( url, object, onLoad, onProgress, onError ) { + } - var builder = this.animationBuilder; + /** + * Loads Motion file(s) (.vmd) as a AnimationClip. + * If two or more files are specified, they'll be merged. + * + * @param {string|Array} url - url(s) to animation(.vmd) file(s) + * @param {SkinnedMesh|THREE.Camera} object - tracks will be fitting to this object + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadAnimation( url, object, onLoad, onProgress, onError ) { - this.loadVMD( url, function ( vmd ) { + const builder = this.animationBuilder; - onLoad( object.isCamera - ? builder.buildCameraAnimation( vmd ) - : builder.build( vmd, object ) ); + this.loadVMD( url, function ( vmd ) { - }, onProgress, onError ); + onLoad( object.isCamera + ? builder.buildCameraAnimation( vmd ) + : builder.build( vmd, object ) ); - }, + }, onProgress, onError ); - /** - * Loads mode file and motion file(s) as an object containing - * a SkinnedMesh and a AnimationClip. - * Tracks of AnimationClip are fitting to the model. - * - * @param {string} modelUrl - url to Model(.pmd or .pmx) file - * @param {string|Array{string}} vmdUrl - url(s) to animation(.vmd) file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadWithAnimation: function ( modelUrl, vmdUrl, onLoad, onProgress, onError ) { + } - var scope = this; + /** + * Loads mode file and motion file(s) as an object containing + * a SkinnedMesh and a AnimationClip. + * Tracks of AnimationClip are fitting to the model. + * + * @param {string} modelUrl - url to Model(.pmd or .pmx) file + * @param {string|Array{string}} vmdUrl - url(s) to animation(.vmd) file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadWithAnimation( modelUrl, vmdUrl, onLoad, onProgress, onError ) { - this.load( modelUrl, function ( mesh ) { + const scope = this; - scope.loadAnimation( vmdUrl, mesh, function ( animation ) { + this.load( modelUrl, function ( mesh ) { - onLoad( { - mesh: mesh, - animation: animation - } ); + scope.loadAnimation( vmdUrl, mesh, function ( animation ) { - }, onProgress, onError ); + onLoad( { + mesh: mesh, + animation: animation + } ); }, onProgress, onError ); - }, + }, onProgress, onError ); - // Load MMD assets as Object data parsed by MMDParser - - /** - * Loads .pmd file as an Object. - * - * @param {string} url - url to .pmd file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadPMD: function ( url, onLoad, onProgress, onError ) { + } - var parser = this._getParser(); + // Load MMD assets as Object data parsed by MMDParser - this.loader - .setMimeType( undefined ) - .setPath( this.path ) - .setResponseType( 'arraybuffer' ) - .setRequestHeader( this.requestHeader ) - .setWithCredentials( this.withCredentials ) - .load( url, function ( buffer ) { + /** + * Loads .pmd file as an Object. + * + * @param {string} url - url to .pmd file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadPMD( url, onLoad, onProgress, onError ) { - onLoad( parser.parsePmd( buffer, true ) ); + const parser = this._getParser(); - }, onProgress, onError ); + this.loader + .setMimeType( undefined ) + .setPath( this.path ) + .setResponseType( 'arraybuffer' ) + .setRequestHeader( this.requestHeader ) + .setWithCredentials( this.withCredentials ) + .load( url, function ( buffer ) { - }, + onLoad( parser.parsePmd( buffer, true ) ); - /** - * Loads .pmx file as an Object. - * - * @param {string} url - url to .pmx file - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadPMX: function ( url, onLoad, onProgress, onError ) { + }, onProgress, onError ); - var parser = this._getParser(); + } - this.loader - .setMimeType( undefined ) - .setPath( this.path ) - .setResponseType( 'arraybuffer' ) - .setRequestHeader( this.requestHeader ) - .setWithCredentials( this.withCredentials ) - .load( url, function ( buffer ) { + /** + * Loads .pmx file as an Object. + * + * @param {string} url - url to .pmx file + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadPMX( url, onLoad, onProgress, onError ) { - onLoad( parser.parsePmx( buffer, true ) ); + const parser = this._getParser(); - }, onProgress, onError ); + this.loader + .setMimeType( undefined ) + .setPath( this.path ) + .setResponseType( 'arraybuffer' ) + .setRequestHeader( this.requestHeader ) + .setWithCredentials( this.withCredentials ) + .load( url, function ( buffer ) { - }, + onLoad( parser.parsePmx( buffer, true ) ); - /** - * Loads .vmd file as an Object. If two or more files are specified - * they'll be merged. - * - * @param {string|Array} url - url(s) to .vmd file(s) - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadVMD: function ( url, onLoad, onProgress, onError ) { + }, onProgress, onError ); - var urls = Array.isArray( url ) ? url : [ url ]; + } - var vmds = []; - var vmdNum = urls.length; + /** + * Loads .vmd file as an Object. If two or more files are specified + * they'll be merged. + * + * @param {string|Array} url - url(s) to .vmd file(s) + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadVMD( url, onLoad, onProgress, onError ) { - var parser = this._getParser(); + const urls = Array.isArray( url ) ? url : [ url ]; - this.loader - .setMimeType( undefined ) - .setPath( this.animationPath ) - .setResponseType( 'arraybuffer' ) - .setRequestHeader( this.requestHeader ) - .setWithCredentials( this.withCredentials ); + const vmds = []; + const vmdNum = urls.length; - for ( var i = 0, il = urls.length; i < il; i ++ ) { + const parser = this._getParser(); - this.loader.load( urls[ i ], function ( buffer ) { + this.loader + .setMimeType( undefined ) + .setPath( this.animationPath ) + .setResponseType( 'arraybuffer' ) + .setRequestHeader( this.requestHeader ) + .setWithCredentials( this.withCredentials ); - vmds.push( parser.parseVmd( buffer, true ) ); + for ( let i = 0, il = urls.length; i < il; i ++ ) { - if ( vmds.length === vmdNum ) onLoad( parser.mergeVmds( vmds ) ); + this.loader.load( urls[ i ], function ( buffer ) { - }, onProgress, onError ); + vmds.push( parser.parseVmd( buffer, true ) ); - } + if ( vmds.length === vmdNum ) onLoad( parser.mergeVmds( vmds ) ); - }, + }, onProgress, onError ); - /** - * Loads .vpd file as an Object. - * - * @param {string} url - url to .vpd file - * @param {boolean} isUnicode - * @param {function} onLoad - * @param {function} onProgress - * @param {function} onError - */ - loadVPD: function ( url, isUnicode, onLoad, onProgress, onError ) { + } - var parser = this._getParser(); + } - this.loader - .setMimeType( isUnicode ? undefined : 'text/plain; charset=shift_jis' ) - .setPath( this.animationPath ) - .setResponseType( 'text' ) - .setRequestHeader( this.requestHeader ) - .setWithCredentials( this.withCredentials ) - .load( url, function ( text ) { + /** + * Loads .vpd file as an Object. + * + * @param {string} url - url to .vpd file + * @param {boolean} isUnicode + * @param {function} onLoad + * @param {function} onProgress + * @param {function} onError + */ + loadVPD( url, isUnicode, onLoad, onProgress, onError ) { - onLoad( parser.parseVpd( text, true ) ); + const parser = this._getParser(); - }, onProgress, onError ); + this.loader + .setMimeType( isUnicode ? undefined : 'text/plain; charset=shift_jis' ) + .setPath( this.animationPath ) + .setResponseType( 'text' ) + .setRequestHeader( this.requestHeader ) + .setWithCredentials( this.withCredentials ) + .load( url, function ( text ) { - }, + onLoad( parser.parseVpd( text, true ) ); - // private methods + }, onProgress, onError ); - _extractExtension: function ( url ) { + } - var index = url.lastIndexOf( '.' ); - return index < 0 ? '' : url.slice( index + 1 ); + // private methods - }, + _extractExtension( url ) { - _getParser: function () { + const index = url.lastIndexOf( '.' ); + return index < 0 ? '' : url.slice( index + 1 ); - if ( this.parser === null ) { + } - if ( typeof MMDParser === 'undefined' ) { + _getParser() { - throw new Error( 'THREE.MMDLoader: Import MMDParser https://github.com/takahirox/mmd-parser' ); + if ( this.parser === null ) { - } + if ( typeof MMDParser === 'undefined' ) { - this.parser = new MMDParser.Parser(); // eslint-disable-line no-undef + throw new Error( 'THREE.MMDLoader: Import MMDParser https://github.com/takahirox/mmd-parser' ); } - return this.parser; + this.parser = new MMDParser.Parser(); // eslint-disable-line no-undef } - } ); + return this.parser; + + } + +} - // Utilities +// Utilities - /* +/* * base64 encoded defalut toon textures toon00.bmp - toon10.bmp. * We don't need to request external toon image files. * This idea is from http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js */ - var DEFAULT_TOON_TEXTURES = [ - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/bWiiMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh8aBHZBl14e8wAAAABJRU5ErkJggg==', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOUlEQVRYR+3WMREAMAwDsYY/yoDI7MLwIiP40+RJklfcCCBAgAABAgTqArfb/QMCCBAgQIAAgbbAB3z/e0F3js2cAAAAAElFTkSuQmCC', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/B5ilMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh81dWyx0gFwKAAAAABJRU5ErkJggg==', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOklEQVRYR+3WoREAMAwDsWb/UQtCy9wxTOQJ/oQ8SXKKGwEECBAgQIBAXeDt7f4BAQQIECBAgEBb4AOz8Hzx7WLY4wAAAABJRU5ErkJggg==', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABPUlEQVRYR+1XwW7CMAy1+f9fZOMysSEOEweEOPRNdm3HbdOyIhAcklPrOs/PLy9RygBALxzcCDQFmgJNgaZAU6Ap0BR4PwX8gsRMVLssMRH5HcpzJEaWL7EVg9F1IHRlyqQohgVr4FGUlUcMJSjcUlDw0zvjeun70cLWmneoyf7NgBTQSniBTQQSuJAZsOnnaczjIMb5hCiuHKxokCrJfVnrctyZL0PkJAJe1HMil4nxeyi3Ypfn1kX51jpPvo/JeCNC4PhVdHdJw2XjBR8brF8PEIhNVn12AgP7uHsTBguBn53MUZCqv7Lp07Pn5k1Ro+uWmUNn7D+M57rtk7aG0Vo73xyF/fbFf0bPJjDXngnGocDTdFhygZjwUQrMNrDcmZlQT50VJ/g/UwNyHpu778+yW+/ksOz/BFo54P4AsUXMfRq7XWsAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACMElEQVRYR+2Xv4pTQRTGf2dubhLdICiii2KnYKHVolhauKWPoGAnNr6BD6CvIVaihYuI2i1ia0BY0MZGRHQXjZj/mSPnnskfNWiWZUlzJ5k7M2cm833nO5Mziej2DWWJRUoCpQKlAntSQCqgw39/iUWAGmh37jrRnVsKlgpiqmkoGVABA7E57fvY+pJDdgKqF6HzFCSADkDq+F6AHABtQ+UMVE5D7zXod7fFNhTEckTbj5XQgHzNN+5tQvc5NG7C6BNkp6D3EmpXHDR+dQAjFLchW3VS9rlw3JBh+B7ys5Cf9z0GW1C/7P32AyBAOAz1q4jGliIH3YPuBnSfQX4OGreTIgEYQb/pBDtPnEQ4CivXYPAWBk13oHrB54yA9QuSn2H4AcKRpEILDt0BUzj+RLR1V5EqjD66NPRBVpLcQwjHoHYJOhsQv6U4mnzmrIXJCFr4LDwm/xBUoboG9XX4cc9VKdYoSA2yk5NQLJaKDUjTBoveG3Z2TElTxwjNK4M3LEZgUdDdruvcXzKBpStgp2NPiWi3ks9ZXxIoFVi+AvHLdc9TqtjL3/aYjpPlrzOcEnK62Szhimdd7xX232zFDTgtxezOu3WNMRLjiKgjtOhHVMd1loynVHvOgjuIIJMaELEqhJAV/RCSLbWTcfPFakFgFlALTRRvx+ok6Hlp/Q+v3fmx90bMyUzaEAhmM3KvHlXTL5DxnbGf/1M8RNNACLL5MNtPxP/mypJAqcDSFfgFhpYqWUzhTEAAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=' - ]; - - // Builders. They build Three.js object from Object data parsed by MMDParser. +const DEFAULT_TOON_TEXTURES = [ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/bWiiMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh8aBHZBl14e8wAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOUlEQVRYR+3WMREAMAwDsYY/yoDI7MLwIiP40+RJklfcCCBAgAABAgTqArfb/QMCCBAgQIAAgbbAB3z/e0F3js2cAAAAAElFTkSuQmCC', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/B5ilMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh81dWyx0gFwKAAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOklEQVRYR+3WoREAMAwDsWb/UQtCy9wxTOQJ/oQ8SXKKGwEECBAgQIBAXeDt7f4BAQQIECBAgEBb4AOz8Hzx7WLY4wAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABPUlEQVRYR+1XwW7CMAy1+f9fZOMysSEOEweEOPRNdm3HbdOyIhAcklPrOs/PLy9RygBALxzcCDQFmgJNgaZAU6Ap0BR4PwX8gsRMVLssMRH5HcpzJEaWL7EVg9F1IHRlyqQohgVr4FGUlUcMJSjcUlDw0zvjeun70cLWmneoyf7NgBTQSniBTQQSuJAZsOnnaczjIMb5hCiuHKxokCrJfVnrctyZL0PkJAJe1HMil4nxeyi3Ypfn1kX51jpPvo/JeCNC4PhVdHdJw2XjBR8brF8PEIhNVn12AgP7uHsTBguBn53MUZCqv7Lp07Pn5k1Ro+uWmUNn7D+M57rtk7aG0Vo73xyF/fbFf0bPJjDXngnGocDTdFhygZjwUQrMNrDcmZlQT50VJ/g/UwNyHpu778+yW+/ksOz/BFo54P4AsUXMfRq7XWsAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACMElEQVRYR+2Xv4pTQRTGf2dubhLdICiii2KnYKHVolhauKWPoGAnNr6BD6CvIVaihYuI2i1ia0BY0MZGRHQXjZj/mSPnnskfNWiWZUlzJ5k7M2cm833nO5Mziej2DWWJRUoCpQKlAntSQCqgw39/iUWAGmh37jrRnVsKlgpiqmkoGVABA7E57fvY+pJDdgKqF6HzFCSADkDq+F6AHABtQ+UMVE5D7zXod7fFNhTEckTbj5XQgHzNN+5tQvc5NG7C6BNkp6D3EmpXHDR+dQAjFLchW3VS9rlw3JBh+B7ys5Cf9z0GW1C/7P32AyBAOAz1q4jGliIH3YPuBnSfQX4OGreTIgEYQb/pBDtPnEQ4CivXYPAWBk13oHrB54yA9QuSn2H4AcKRpEILDt0BUzj+RLR1V5EqjD66NPRBVpLcQwjHoHYJOhsQv6U4mnzmrIXJCFr4LDwm/xBUoboG9XX4cc9VKdYoSA2yk5NQLJaKDUjTBoveG3Z2TElTxwjNK4M3LEZgUdDdruvcXzKBpStgp2NPiWi3ks9ZXxIoFVi+AvHLdc9TqtjL3/aYjpPlrzOcEnK62Szhimdd7xX232zFDTgtxezOu3WNMRLjiKgjtOhHVMd1loynVHvOgjuIIJMaELEqhJAV/RCSLbWTcfPFakFgFlALTRRvx+ok6Hlp/Q+v3fmx90bMyUzaEAhmM3KvHlXTL5DxnbGf/1M8RNNACLL5MNtPxP/mypJAqcDSFfgFhpYqWUzhTEAAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=' +]; + +// Builders. They build Three.js object from Object data parsed by MMDParser. - /** - * @param {THREE.LoadingManager} manager - */ - function MeshBuilder( manager ) { +/** + * @param {THREE.LoadingManager} manager + */ +class MeshBuilder { + + constructor( manager ) { + this.crossOrigin = 'anonymous'; this.geometryBuilder = new GeometryBuilder(); this.materialBuilder = new MaterialBuilder( manager ); } - MeshBuilder.prototype = { - - constructor: MeshBuilder, - - crossOrigin: 'anonymous', - - /** - * @param {string} crossOrigin - * @return {MeshBuilder} - */ - setCrossOrigin: function ( crossOrigin ) { - - this.crossOrigin = crossOrigin; - return this; + /** + * @param {string} crossOrigin + * @return {MeshBuilder} + */ + setCrossOrigin( crossOrigin ) { - }, + this.crossOrigin = crossOrigin; + return this; - /** - * @param {Object} data - parsed PMD/PMX data - * @param {string} resourcePath - * @param {function} onProgress - * @param {function} onError - * @return {SkinnedMesh} - */ - build: function ( data, resourcePath, onProgress, onError ) { + } - var geometry = this.geometryBuilder.build( data ); - var material = this.materialBuilder - .setCrossOrigin( this.crossOrigin ) - .setResourcePath( resourcePath ) - .build( data, geometry, onProgress, onError ); + /** + * @param {Object} data - parsed PMD/PMX data + * @param {string} resourcePath + * @param {function} onProgress + * @param {function} onError + * @return {SkinnedMesh} + */ + build( data, resourcePath, onProgress, onError ) { - var mesh = new SkinnedMesh( geometry, material ); + const geometry = this.geometryBuilder.build( data ); + const material = this.materialBuilder + .setCrossOrigin( this.crossOrigin ) + .setResourcePath( resourcePath ) + .build( data, geometry, onProgress, onError ); - var skeleton = new Skeleton( initBones( mesh ) ); - mesh.bind( skeleton ); + const mesh = new SkinnedMesh( geometry, material ); - // console.log( mesh ); // for console debug + const skeleton = new Skeleton( initBones( mesh ) ); + mesh.bind( skeleton ); - return mesh; + // console.log( mesh ); // for console debug - } + return mesh; - }; + } - // TODO: Try to remove this function +} - function initBones( mesh ) { +// TODO: Try to remove this function - var geometry = mesh.geometry; +function initBones( mesh ) { - var bones = [], bone, gbone; - var i, il; + const geometry = mesh.geometry; - if ( geometry && geometry.bones !== undefined ) { + const bones = []; - // first, create array of 'Bone' objects from geometry data + if ( geometry && geometry.bones !== undefined ) { - for ( i = 0, il = geometry.bones.length; i < il; i ++ ) { + // first, create array of 'Bone' objects from geometry data - gbone = geometry.bones[ i ]; + for ( let i = 0, il = geometry.bones.length; i < il; i ++ ) { - // create new 'Bone' object + const gbone = geometry.bones[ i ]; - bone = new Bone(); - bones.push( bone ); + // create new 'Bone' object - // apply values + const bone = new Bone(); + bones.push( bone ); - bone.name = gbone.name; - bone.position.fromArray( gbone.pos ); - bone.quaternion.fromArray( gbone.rotq ); - if ( gbone.scl !== undefined ) bone.scale.fromArray( gbone.scl ); + // apply values - } + bone.name = gbone.name; + bone.position.fromArray( gbone.pos ); + bone.quaternion.fromArray( gbone.rotq ); + if ( gbone.scl !== undefined ) bone.scale.fromArray( gbone.scl ); - // second, create bone hierarchy + } - for ( i = 0, il = geometry.bones.length; i < il; i ++ ) { + // second, create bone hierarchy - gbone = geometry.bones[ i ]; + for ( let i = 0, il = geometry.bones.length; i < il; i ++ ) { - if ( ( gbone.parent !== - 1 ) && ( gbone.parent !== null ) && ( bones[ gbone.parent ] !== undefined ) ) { + const gbone = geometry.bones[ i ]; - // subsequent bones in the hierarchy + if ( ( gbone.parent !== - 1 ) && ( gbone.parent !== null ) && ( bones[ gbone.parent ] !== undefined ) ) { - bones[ gbone.parent ].add( bones[ i ] ); + // subsequent bones in the hierarchy - } else { + bones[ gbone.parent ].add( bones[ i ] ); - // topmost bone, immediate child of the skinned mesh + } else { - mesh.add( bones[ i ] ); + // topmost bone, immediate child of the skinned mesh - } + mesh.add( bones[ i ] ); } } - // now the bones are part of the scene graph and children of the skinned mesh. - // let's update the corresponding matrices - - mesh.updateMatrixWorld( true ); - - return bones; - } - // + // now the bones are part of the scene graph and children of the skinned mesh. + // let's update the corresponding matrices - function GeometryBuilder() { + mesh.updateMatrixWorld( true ); - } + return bones; - GeometryBuilder.prototype = { +} - constructor: GeometryBuilder, +// - /** - * @param {Object} data - parsed PMD/PMX data - * @return {BufferGeometry} - */ - build: function ( data ) { +class GeometryBuilder { - // for geometry - var positions = []; - var uvs = []; - var normals = []; + /** + * @param {Object} data - parsed PMD/PMX data + * @return {BufferGeometry} + */ + build( data ) { - var indices = []; + // for geometry + const positions = []; + const uvs = []; + const normals = []; - var groups = []; + const indices = []; - var bones = []; - var skinIndices = []; - var skinWeights = []; + const groups = []; - var morphTargets = []; - var morphPositions = []; + const bones = []; + const skinIndices = []; + const skinWeights = []; - var iks = []; - var grants = []; + const morphTargets = []; + const morphPositions = []; - var rigidBodies = []; - var constraints = []; + const iks = []; + const grants = []; - // for work - var offset = 0; - var boneTypeTable = {}; + const rigidBodies = []; + const constraints = []; - // positions, normals, uvs, skinIndices, skinWeights + // for work + let offset = 0; + const boneTypeTable = {}; - for ( var i = 0; i < data.metadata.vertexCount; i ++ ) { + // positions, normals, uvs, skinIndices, skinWeights - var v = data.vertices[ i ]; + for ( let i = 0; i < data.metadata.vertexCount; i ++ ) { - for ( var j = 0, jl = v.position.length; j < jl; j ++ ) { + const v = data.vertices[ i ]; - positions.push( v.position[ j ] ); + for ( let j = 0, jl = v.position.length; j < jl; j ++ ) { - } + positions.push( v.position[ j ] ); - for ( var j = 0, jl = v.normal.length; j < jl; j ++ ) { + } - normals.push( v.normal[ j ] ); + for ( let j = 0, jl = v.normal.length; j < jl; j ++ ) { - } + normals.push( v.normal[ j ] ); - for ( var j = 0, jl = v.uv.length; j < jl; j ++ ) { + } - uvs.push( v.uv[ j ] ); + for ( let j = 0, jl = v.uv.length; j < jl; j ++ ) { - } + uvs.push( v.uv[ j ] ); - for ( var j = 0; j < 4; j ++ ) { + } - skinIndices.push( v.skinIndices.length - 1 >= j ? v.skinIndices[ j ] : 0.0 ); + for ( let j = 0; j < 4; j ++ ) { - } + skinIndices.push( v.skinIndices.length - 1 >= j ? v.skinIndices[ j ] : 0.0 ); - for ( var j = 0; j < 4; j ++ ) { + } - skinWeights.push( v.skinWeights.length - 1 >= j ? v.skinWeights[ j ] : 0.0 ); + for ( let j = 0; j < 4; j ++ ) { - } + skinWeights.push( v.skinWeights.length - 1 >= j ? v.skinWeights[ j ] : 0.0 ); } - // indices + } - for ( var i = 0; i < data.metadata.faceCount; i ++ ) { + // indices - var face = data.faces[ i ]; + for ( let i = 0; i < data.metadata.faceCount; i ++ ) { - for ( var j = 0, jl = face.indices.length; j < jl; j ++ ) { + const face = data.faces[ i ]; - indices.push( face.indices[ j ] ); + for ( let j = 0, jl = face.indices.length; j < jl; j ++ ) { - } + indices.push( face.indices[ j ] ); } - // groups - - for ( var i = 0; i < data.metadata.materialCount; i ++ ) { + } - var material = data.materials[ i ]; + // groups - groups.push( { - offset: offset * 3, - count: material.faceCount * 3 - } ); + for ( let i = 0; i < data.metadata.materialCount; i ++ ) { - offset += material.faceCount; + const material = data.materials[ i ]; - } + groups.push( { + offset: offset * 3, + count: material.faceCount * 3 + } ); - // bones + offset += material.faceCount; - for ( var i = 0; i < data.metadata.rigidBodyCount; i ++ ) { + } - var body = data.rigidBodies[ i ]; - var value = boneTypeTable[ body.boneIndex ]; + // bones - // keeps greater number if already value is set without any special reasons - value = value === undefined ? body.type : Math.max( body.type, value ); + for ( let i = 0; i < data.metadata.rigidBodyCount; i ++ ) { - boneTypeTable[ body.boneIndex ] = value; + const body = data.rigidBodies[ i ]; + let value = boneTypeTable[ body.boneIndex ]; - } + // keeps greater number if already value is set without any special reasons + value = value === undefined ? body.type : Math.max( body.type, value ); - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { + boneTypeTable[ body.boneIndex ] = value; - var boneData = data.bones[ i ]; + } - var bone = { - index: i, - transformationClass: boneData.transformationClass, - parent: boneData.parentIndex, - name: boneData.name, - pos: boneData.position.slice( 0, 3 ), - rotq: [ 0, 0, 0, 1 ], - scl: [ 1, 1, 1 ], - rigidBodyType: boneTypeTable[ i ] !== undefined ? boneTypeTable[ i ] : - 1 - }; + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { - if ( bone.parent !== - 1 ) { + const boneData = data.bones[ i ]; - bone.pos[ 0 ] -= data.bones[ bone.parent ].position[ 0 ]; - bone.pos[ 1 ] -= data.bones[ bone.parent ].position[ 1 ]; - bone.pos[ 2 ] -= data.bones[ bone.parent ].position[ 2 ]; + const bone = { + index: i, + transformationClass: boneData.transformationClass, + parent: boneData.parentIndex, + name: boneData.name, + pos: boneData.position.slice( 0, 3 ), + rotq: [ 0, 0, 0, 1 ], + scl: [ 1, 1, 1 ], + rigidBodyType: boneTypeTable[ i ] !== undefined ? boneTypeTable[ i ] : - 1 + }; - } + if ( bone.parent !== - 1 ) { - bones.push( bone ); + bone.pos[ 0 ] -= data.bones[ bone.parent ].position[ 0 ]; + bone.pos[ 1 ] -= data.bones[ bone.parent ].position[ 1 ]; + bone.pos[ 2 ] -= data.bones[ bone.parent ].position[ 2 ]; } - // iks + bones.push( bone ); - // TODO: remove duplicated codes between PMD and PMX - if ( data.metadata.format === 'pmd' ) { + } - for ( var i = 0; i < data.metadata.ikCount; i ++ ) { + // iks - var ik = data.iks[ i ]; + // TODO: remove duplicated codes between PMD and PMX + if ( data.metadata.format === 'pmd' ) { - var param = { - target: ik.target, - effector: ik.effector, - iteration: ik.iteration, - maxAngle: ik.maxAngle * 4, - links: [] - }; + for ( let i = 0; i < data.metadata.ikCount; i ++ ) { - for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) { + const ik = data.iks[ i ]; - var link = {}; - link.index = ik.links[ j ].index; - link.enabled = true; + const param = { + target: ik.target, + effector: ik.effector, + iteration: ik.iteration, + maxAngle: ik.maxAngle * 4, + links: [] + }; - if ( data.bones[ link.index ].name.indexOf( 'ひざ' ) >= 0 ) { + for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) { - link.limitation = new Vector3( 1.0, 0.0, 0.0 ); + const link = {}; + link.index = ik.links[ j ].index; + link.enabled = true; - } + if ( data.bones[ link.index ].name.indexOf( 'ひざ' ) >= 0 ) { - param.links.push( link ); + link.limitation = new Vector3( 1.0, 0.0, 0.0 ); } - iks.push( param ); + param.links.push( link ); } - } else { + iks.push( param ); - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { + } - var ik = data.bones[ i ].ik; + } else { - if ( ik === undefined ) continue; + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { - var param = { - target: i, - effector: ik.effector, - iteration: ik.iteration, - maxAngle: ik.maxAngle, - links: [] - }; + const ik = data.bones[ i ].ik; - for ( var j = 0, jl = ik.links.length; j < jl; j ++ ) { + if ( ik === undefined ) continue; - var link = {}; - link.index = ik.links[ j ].index; - link.enabled = true; + const param = { + target: i, + effector: ik.effector, + iteration: ik.iteration, + maxAngle: ik.maxAngle, + links: [] + }; - if ( ik.links[ j ].angleLimitation === 1 ) { + for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) { - // Revert if rotationMin/Max doesn't work well - // link.limitation = new Vector3( 1.0, 0.0, 0.0 ); + const link = {}; + link.index = ik.links[ j ].index; + link.enabled = true; - var rotationMin = ik.links[ j ].lowerLimitationAngle; - var rotationMax = ik.links[ j ].upperLimitationAngle; + if ( ik.links[ j ].angleLimitation === 1 ) { - // Convert Left to Right coordinate by myself because - // MMDParser doesn't convert. It's a MMDParser's bug + // Revert if rotationMin/Max doesn't work well + // link.limitation = new Vector3( 1.0, 0.0, 0.0 ); - var tmp1 = - rotationMax[ 0 ]; - var tmp2 = - rotationMax[ 1 ]; - rotationMax[ 0 ] = - rotationMin[ 0 ]; - rotationMax[ 1 ] = - rotationMin[ 1 ]; - rotationMin[ 0 ] = tmp1; - rotationMin[ 1 ] = tmp2; + const rotationMin = ik.links[ j ].lowerLimitationAngle; + const rotationMax = ik.links[ j ].upperLimitationAngle; - link.rotationMin = new Vector3().fromArray( rotationMin ); - link.rotationMax = new Vector3().fromArray( rotationMax ); + // Convert Left to Right coordinate by myself because + // MMDParser doesn't convert. It's a MMDParser's bug - } + const tmp1 = - rotationMax[ 0 ]; + const tmp2 = - rotationMax[ 1 ]; + rotationMax[ 0 ] = - rotationMin[ 0 ]; + rotationMax[ 1 ] = - rotationMin[ 1 ]; + rotationMin[ 0 ] = tmp1; + rotationMin[ 1 ] = tmp2; - param.links.push( link ); + link.rotationMin = new Vector3().fromArray( rotationMin ); + link.rotationMax = new Vector3().fromArray( rotationMax ); } - iks.push( param ); - - // Save the reference even from bone data for efficiently - // simulating PMX animation system - bones[ i ].ik = param; + param.links.push( link ); } + iks.push( param ); + + // Save the reference even from bone data for efficiently + // simulating PMX animation system + bones[ i ].ik = param; + } - // grants + } - if ( data.metadata.format === 'pmx' ) { + // grants - // bone index -> grant entry map - var grantEntryMap = {}; + if ( data.metadata.format === 'pmx' ) { - for ( var i = 0; i < data.metadata.boneCount; i ++ ) { + // bone index -> grant entry map + const grantEntryMap = {}; - var boneData = data.bones[ i ]; - var grant = boneData.grant; + for ( let i = 0; i < data.metadata.boneCount; i ++ ) { - if ( grant === undefined ) continue; + const boneData = data.bones[ i ]; + const grant = boneData.grant; - var param = { - index: i, - parentIndex: grant.parentIndex, - ratio: grant.ratio, - isLocal: grant.isLocal, - affectRotation: grant.affectRotation, - affectPosition: grant.affectPosition, - transformationClass: boneData.transformationClass - }; + if ( grant === undefined ) continue; - grantEntryMap[ i ] = { parent: null, children: [], param: param, visited: false }; + const param = { + index: i, + parentIndex: grant.parentIndex, + ratio: grant.ratio, + isLocal: grant.isLocal, + affectRotation: grant.affectRotation, + affectPosition: grant.affectPosition, + transformationClass: boneData.transformationClass + }; - } + grantEntryMap[ i ] = { parent: null, children: [], param: param, visited: false }; - var rootEntry = { parent: null, children: [], param: null, visited: false }; + } - // Build a tree representing grant hierarchy + const rootEntry = { parent: null, children: [], param: null, visited: false }; - for ( var boneIndex in grantEntryMap ) { + // Build a tree representing grant hierarchy - var grantEntry = grantEntryMap[ boneIndex ]; - var parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry; + for ( const boneIndex in grantEntryMap ) { - grantEntry.parent = parentGrantEntry; - parentGrantEntry.children.push( grantEntry ); + const grantEntry = grantEntryMap[ boneIndex ]; + const parentGrantEntry = grantEntryMap[ grantEntry.parentIndex ] || rootEntry; - } + grantEntry.parent = parentGrantEntry; + parentGrantEntry.children.push( grantEntry ); - // Sort grant parameters from parents to children because - // grant uses parent's transform that parent's grant is already applied - // so grant should be applied in order from parents to children + } - function traverse( entry ) { + // Sort grant parameters from parents to children because + // grant uses parent's transform that parent's grant is already applied + // so grant should be applied in order from parents to children - if ( entry.param ) { + function traverse( entry ) { - grants.push( entry.param ); + if ( entry.param ) { - // Save the reference even from bone data for efficiently - // simulating PMX animation system - bones[ entry.param.index ].grant = entry.param; + grants.push( entry.param ); - } + // Save the reference even from bone data for efficiently + // simulating PMX animation system + bones[ entry.param.index ].grant = entry.param; - entry.visited = true; + } - for ( var i = 0, il = entry.children.length; i < il; i ++ ) { + entry.visited = true; - var child = entry.children[ i ]; + for ( let i = 0, il = entry.children.length; i < il; i ++ ) { - // Cut off a loop if exists. (Is a grant loop invalid?) - if ( ! child.visited ) traverse( child ); + const child = entry.children[ i ]; - } + // Cut off a loop if exists. (Is a grant loop invalid?) + if ( ! child.visited ) traverse( child ); } - traverse( rootEntry ); - } - // morph + traverse( rootEntry ); - function updateAttributes( attribute, morph, ratio ) { + } - for ( var i = 0; i < morph.elementCount; i ++ ) { + // morph - var element = morph.elements[ i ]; + function updateAttributes( attribute, morph, ratio ) { - var index; + for ( let i = 0; i < morph.elementCount; i ++ ) { - if ( data.metadata.format === 'pmd' ) { + const element = morph.elements[ i ]; - index = data.morphs[ 0 ].elements[ element.index ].index; + let index; - } else { + if ( data.metadata.format === 'pmd' ) { - index = element.index; + index = data.morphs[ 0 ].elements[ element.index ].index; - } + } else { - attribute.array[ index * 3 + 0 ] += element.position[ 0 ] * ratio; - attribute.array[ index * 3 + 1 ] += element.position[ 1 ] * ratio; - attribute.array[ index * 3 + 2 ] += element.position[ 2 ] * ratio; + index = element.index; } + attribute.array[ index * 3 + 0 ] += element.position[ 0 ] * ratio; + attribute.array[ index * 3 + 1 ] += element.position[ 1 ] * ratio; + attribute.array[ index * 3 + 2 ] += element.position[ 2 ] * ratio; + } - for ( var i = 0; i < data.metadata.morphCount; i ++ ) { + } - var morph = data.morphs[ i ]; - var params = { name: morph.name }; + for ( let i = 0; i < data.metadata.morphCount; i ++ ) { - var attribute = new Float32BufferAttribute( data.metadata.vertexCount * 3, 3 ); - attribute.name = morph.name; + const morph = data.morphs[ i ]; + const params = { name: morph.name }; - for ( var j = 0; j < data.metadata.vertexCount * 3; j ++ ) { + const attribute = new Float32BufferAttribute( data.metadata.vertexCount * 3, 3 ); + attribute.name = morph.name; - attribute.array[ j ] = positions[ j ]; + for ( let j = 0; j < data.metadata.vertexCount * 3; j ++ ) { - } + attribute.array[ j ] = positions[ j ]; - if ( data.metadata.format === 'pmd' ) { + } - if ( i !== 0 ) { + if ( data.metadata.format === 'pmd' ) { - updateAttributes( attribute, morph, 1.0 ); + if ( i !== 0 ) { - } + updateAttributes( attribute, morph, 1.0 ); - } else { + } - if ( morph.type === 0 ) { // group + } else { - for ( var j = 0; j < morph.elementCount; j ++ ) { + if ( morph.type === 0 ) { // group - var morph2 = data.morphs[ morph.elements[ j ].index ]; - var ratio = morph.elements[ j ].ratio; + for ( let j = 0; j < morph.elementCount; j ++ ) { - if ( morph2.type === 1 ) { + const morph2 = data.morphs[ morph.elements[ j ].index ]; + const ratio = morph.elements[ j ].ratio; - updateAttributes( attribute, morph2, ratio ); + if ( morph2.type === 1 ) { - } else { + updateAttributes( attribute, morph2, ratio ); - // TODO: implement + } else { - } + // TODO: implement } - } else if ( morph.type === 1 ) { // vertex + } - updateAttributes( attribute, morph, 1.0 ); + } else if ( morph.type === 1 ) { // vertex - } else if ( morph.type === 2 ) { // bone + updateAttributes( attribute, morph, 1.0 ); - // TODO: implement + } else if ( morph.type === 2 ) { // bone - } else if ( morph.type === 3 ) { // uv + // TODO: implement - // TODO: implement + } else if ( morph.type === 3 ) { // uv - } else if ( morph.type === 4 ) { // additional uv1 + // TODO: implement - // TODO: implement + } else if ( morph.type === 4 ) { // additional uv1 - } else if ( morph.type === 5 ) { // additional uv2 + // TODO: implement - // TODO: implement + } else if ( morph.type === 5 ) { // additional uv2 - } else if ( morph.type === 6 ) { // additional uv3 + // TODO: implement - // TODO: implement + } else if ( morph.type === 6 ) { // additional uv3 - } else if ( morph.type === 7 ) { // additional uv4 + // TODO: implement - // TODO: implement + } else if ( morph.type === 7 ) { // additional uv4 - } else if ( morph.type === 8 ) { // material + // TODO: implement - // TODO: implement + } else if ( morph.type === 8 ) { // material - } + // TODO: implement } - morphTargets.push( params ); - morphPositions.push( attribute ); - } - // rigid bodies from rigidBodies field. + morphTargets.push( params ); + morphPositions.push( attribute ); - for ( var i = 0; i < data.metadata.rigidBodyCount; i ++ ) { + } - var rigidBody = data.rigidBodies[ i ]; - var params = {}; + // rigid bodies from rigidBodies field. - for ( var key in rigidBody ) { + for ( let i = 0; i < data.metadata.rigidBodyCount; i ++ ) { - params[ key ] = rigidBody[ key ]; + const rigidBody = data.rigidBodies[ i ]; + const params = {}; - } + for ( const key in rigidBody ) { - /* + params[ key ] = rigidBody[ key ]; + + } + + /* * RigidBody position parameter in PMX seems global position * while the one in PMD seems offset from corresponding bone. * So unify being offset. */ - if ( data.metadata.format === 'pmx' ) { + if ( data.metadata.format === 'pmx' ) { - if ( params.boneIndex !== - 1 ) { + if ( params.boneIndex !== - 1 ) { - var bone = data.bones[ params.boneIndex ]; - params.position[ 0 ] -= bone.position[ 0 ]; - params.position[ 1 ] -= bone.position[ 1 ]; - params.position[ 2 ] -= bone.position[ 2 ]; - - } + const bone = data.bones[ params.boneIndex ]; + params.position[ 0 ] -= bone.position[ 0 ]; + params.position[ 1 ] -= bone.position[ 1 ]; + params.position[ 2 ] -= bone.position[ 2 ]; } - rigidBodies.push( params ); - } - // constraints from constraints field. + rigidBodies.push( params ); - for ( var i = 0; i < data.metadata.constraintCount; i ++ ) { + } - var constraint = data.constraints[ i ]; - var params = {}; + // constraints from constraints field. - for ( var key in constraint ) { + for ( let i = 0; i < data.metadata.constraintCount; i ++ ) { - params[ key ] = constraint[ key ]; + const constraint = data.constraints[ i ]; + const params = {}; - } + for ( const key in constraint ) { - var bodyA = rigidBodies[ params.rigidBodyIndex1 ]; - var bodyB = rigidBodies[ params.rigidBodyIndex2 ]; + params[ key ] = constraint[ key ]; - // Refer to http://www20.atpages.jp/katwat/wp/?p=4135 - if ( bodyA.type !== 0 && bodyB.type === 2 ) { + } - if ( bodyA.boneIndex !== - 1 && bodyB.boneIndex !== - 1 && - data.bones[ bodyB.boneIndex ].parentIndex === bodyA.boneIndex ) { + const bodyA = rigidBodies[ params.rigidBodyIndex1 ]; + const bodyB = rigidBodies[ params.rigidBodyIndex2 ]; - bodyB.type = 1; + // Refer to http://www20.atpages.jp/katwat/wp/?p=4135 + if ( bodyA.type !== 0 && bodyB.type === 2 ) { - } + if ( bodyA.boneIndex !== - 1 && bodyB.boneIndex !== - 1 && + data.bones[ bodyB.boneIndex ].parentIndex === bodyA.boneIndex ) { - } + bodyB.type = 1; - constraints.push( params ); + } } - // build BufferGeometry. + constraints.push( params ); - var geometry = new BufferGeometry(); + } - geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); - geometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); - geometry.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) ); - geometry.setAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) ); - geometry.setAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) ); - geometry.setIndex( indices ); + // build BufferGeometry. - for ( var i = 0, il = groups.length; i < il; i ++ ) { + const geometry = new BufferGeometry(); - geometry.addGroup( groups[ i ].offset, groups[ i ].count, i ); + geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); + geometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); + geometry.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) ); + geometry.setAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) ); + geometry.setAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) ); + geometry.setIndex( indices ); - } + for ( let i = 0, il = groups.length; i < il; i ++ ) { - geometry.bones = bones; + geometry.addGroup( groups[ i ].offset, groups[ i ].count, i ); - geometry.morphTargets = morphTargets; - geometry.morphAttributes.position = morphPositions; - geometry.morphTargetsRelative = false; + } - geometry.userData.MMD = { - bones: bones, - iks: iks, - grants: grants, - rigidBodies: rigidBodies, - constraints: constraints, - format: data.metadata.format - }; + geometry.bones = bones; - geometry.computeBoundingSphere(); + geometry.morphTargets = morphTargets; + geometry.morphAttributes.position = morphPositions; + geometry.morphTargetsRelative = false; - return geometry; + geometry.userData.MMD = { + bones: bones, + iks: iks, + grants: grants, + rigidBodies: rigidBodies, + constraints: constraints, + format: data.metadata.format + }; - } + geometry.computeBoundingSphere(); + + return geometry; - }; + } - // +} - /** - * @param {THREE.LoadingManager} manager - */ - function MaterialBuilder( manager ) { +// + +/** + * @param {THREE.LoadingManager} manager + */ +class MaterialBuilder { + + constructor( manager ) { this.manager = manager; this.textureLoader = new TextureLoader( this.manager ); this.tgaLoader = null; // lazy generation - } - - MaterialBuilder.prototype = { - - constructor: MaterialBuilder, - - crossOrigin: 'anonymous', + this.crossOrigin = 'anonymous'; + this.resourcePath = undefined; - resourcePath: undefined, + } - /** - * @param {string} crossOrigin - * @return {MaterialBuilder} - */ - setCrossOrigin: function ( crossOrigin ) { + /** + * @param {string} crossOrigin + * @return {MaterialBuilder} + */ + setCrossOrigin( crossOrigin ) { - this.crossOrigin = crossOrigin; - return this; + this.crossOrigin = crossOrigin; + return this; - }, + } - /** - * @param {string} resourcePath - * @return {MaterialBuilder} - */ - setResourcePath: function ( resourcePath ) { + /** + * @param {string} resourcePath + * @return {MaterialBuilder} + */ + setResourcePath( resourcePath ) { - this.resourcePath = resourcePath; - return this; + this.resourcePath = resourcePath; + return this; - }, + } - /** - * @param {Object} data - parsed PMD/PMX data - * @param {BufferGeometry} geometry - some properties are dependend on geometry - * @param {function} onProgress - * @param {function} onError - * @return {Array} - */ - build: function ( data, geometry /*, onProgress, onError */ ) { + /** + * @param {Object} data - parsed PMD/PMX data + * @param {BufferGeometry} geometry - some properties are dependend on geometry + * @param {function} onProgress + * @param {function} onError + * @return {Array} + */ + build( data, geometry /*, onProgress, onError */ ) { - var materials = []; + const materials = []; - var textures = {}; + const textures = {}; - this.textureLoader.setCrossOrigin( this.crossOrigin ); + this.textureLoader.setCrossOrigin( this.crossOrigin ); - // materials + // materials - for ( var i = 0; i < data.metadata.materialCount; i ++ ) { + for ( let i = 0; i < data.metadata.materialCount; i ++ ) { - var material = data.materials[ i ]; + const material = data.materials[ i ]; - var params = { userData: {} }; + const params = { userData: {} }; - if ( material.name !== undefined ) params.name = material.name; + if ( material.name !== undefined ) params.name = material.name; - /* + /* * Color * * MMD MeshToonMaterial @@ -1110,890 +1093,880 @@ var MMDLoader = ( function () { * MeshToonMaterial doesn't have ambient. Set it to emissive instead. * It'll be too bright if material has map texture so using coef 0.2. */ - params.color = new Color().fromArray( material.diffuse ); - params.opacity = material.diffuse[ 3 ]; - params.emissive = new Color().fromArray( material.ambient ); - params.transparent = params.opacity !== 1.0; - - // + params.color = new Color().fromArray( material.diffuse ); + params.opacity = material.diffuse[ 3 ]; + params.emissive = new Color().fromArray( material.ambient ); + params.transparent = params.opacity !== 1.0; - params.skinning = geometry.bones.length > 0 ? true : false; - params.morphTargets = geometry.morphTargets.length > 0 ? true : false; - params.fog = true; + // - // blend + params.skinning = geometry.bones.length > 0 ? true : false; + params.morphTargets = geometry.morphTargets.length > 0 ? true : false; + params.fog = true; - params.blending = CustomBlending; - params.blendSrc = SrcAlphaFactor; - params.blendDst = OneMinusSrcAlphaFactor; - params.blendSrcAlpha = SrcAlphaFactor; - params.blendDstAlpha = DstAlphaFactor; + // blend - // side + params.blending = CustomBlending; + params.blendSrc = SrcAlphaFactor; + params.blendDst = OneMinusSrcAlphaFactor; + params.blendSrcAlpha = SrcAlphaFactor; + params.blendDstAlpha = DstAlphaFactor; - if ( data.metadata.format === 'pmx' && ( material.flag & 0x1 ) === 1 ) { + // side - params.side = DoubleSide; + if ( data.metadata.format === 'pmx' && ( material.flag & 0x1 ) === 1 ) { - } else { + params.side = DoubleSide; - params.side = params.opacity === 1.0 ? FrontSide : DoubleSide; + } else { - } + params.side = params.opacity === 1.0 ? FrontSide : DoubleSide; - if ( data.metadata.format === 'pmd' ) { + } - // map, envMap + if ( data.metadata.format === 'pmd' ) { - if ( material.fileName ) { + // map, envMap - var fileName = material.fileName; - var fileNames = fileName.split( '*' ); + if ( material.fileName ) { - // fileNames[ 0 ]: mapFileName - // fileNames[ 1 ]: envMapFileName( optional ) + const fileName = material.fileName; + const fileNames = fileName.split( '*' ); - params.map = this._loadTexture( fileNames[ 0 ], textures ); + // fileNames[ 0 ]: mapFileName + // fileNames[ 1 ]: envMapFileName( optional ) - if ( fileNames.length > 1 ) { + params.map = this._loadTexture( fileNames[ 0 ], textures ); - var extension = fileNames[ 1 ].slice( - 4 ).toLowerCase(); + if ( fileNames.length > 1 ) { - params.envMap = this._loadTexture( - fileNames[ 1 ], - textures - ); + const extension = fileNames[ 1 ].slice( - 4 ).toLowerCase(); - params.combine = extension === '.sph' - ? MultiplyOperation - : AddOperation; + params.envMap = this._loadTexture( + fileNames[ 1 ], + textures + ); - } + params.combine = extension === '.sph' + ? MultiplyOperation + : AddOperation; } - // gradientMap - - var toonFileName = ( material.toonIndex === - 1 ) - ? 'toon00.bmp' - : data.toonTextures[ material.toonIndex ].fileName; - - params.gradientMap = this._loadTexture( - toonFileName, - textures, - { - isToonTexture: true, - isDefaultToonTexture: this._isDefaultToonTexture( toonFileName ) - } - ); + } - // parameters for OutlineEffect + // gradientMap - params.userData.outlineParameters = { - thickness: material.edgeFlag === 1 ? 0.003 : 0.0, - color: [ 0, 0, 0 ], - alpha: 1.0, - visible: material.edgeFlag === 1 - }; + const toonFileName = ( material.toonIndex === - 1 ) + ? 'toon00.bmp' + : data.toonTextures[ material.toonIndex ].fileName; - } else { + params.gradientMap = this._loadTexture( + toonFileName, + textures, + { + isToonTexture: true, + isDefaultToonTexture: this._isDefaultToonTexture( toonFileName ) + } + ); - // map + // parameters for OutlineEffect - if ( material.textureIndex !== - 1 ) { + params.userData.outlineParameters = { + thickness: material.edgeFlag === 1 ? 0.003 : 0.0, + color: [ 0, 0, 0 ], + alpha: 1.0, + visible: material.edgeFlag === 1 + }; - params.map = this._loadTexture( data.textures[ material.textureIndex ], textures ); + } else { - } + // map - // envMap TODO: support m.envFlag === 3 + if ( material.textureIndex !== - 1 ) { - if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) { + params.map = this._loadTexture( data.textures[ material.textureIndex ], textures ); - params.envMap = this._loadTexture( - data.textures[ material.envTextureIndex ], - textures - ); + } - params.combine = material.envFlag === 1 - ? MultiplyOperation - : AddOperation; + // envMap TODO: support m.envFlag === 3 - } + if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) { - // gradientMap + params.envMap = this._loadTexture( + data.textures[ material.envTextureIndex ], + textures + ); - var toonFileName, isDefaultToon; + params.combine = material.envFlag === 1 + ? MultiplyOperation + : AddOperation; - if ( material.toonIndex === - 1 || material.toonFlag !== 0 ) { + } - toonFileName = 'toon' + ( '0' + ( material.toonIndex + 1 ) ).slice( - 2 ) + '.bmp'; - isDefaultToon = true; + // gradientMap - } else { + let toonFileName, isDefaultToon; - toonFileName = data.textures[ material.toonIndex ]; - isDefaultToon = false; + if ( material.toonIndex === - 1 || material.toonFlag !== 0 ) { - } + toonFileName = 'toon' + ( '0' + ( material.toonIndex + 1 ) ).slice( - 2 ) + '.bmp'; + isDefaultToon = true; - params.gradientMap = this._loadTexture( - toonFileName, - textures, - { - isToonTexture: true, - isDefaultToonTexture: isDefaultToon - } - ); + } else { - // parameters for OutlineEffect - params.userData.outlineParameters = { - thickness: material.edgeSize / 300, // TODO: better calculation? - color: material.edgeColor.slice( 0, 3 ), - alpha: material.edgeColor[ 3 ], - visible: ( material.flag & 0x10 ) !== 0 && material.edgeSize > 0.0 - }; + toonFileName = data.textures[ material.toonIndex ]; + isDefaultToon = false; } - if ( params.map !== undefined ) { + params.gradientMap = this._loadTexture( + toonFileName, + textures, + { + isToonTexture: true, + isDefaultToonTexture: isDefaultToon + } + ); + + // parameters for OutlineEffect + params.userData.outlineParameters = { + thickness: material.edgeSize / 300, // TODO: better calculation? + color: material.edgeColor.slice( 0, 3 ), + alpha: material.edgeColor[ 3 ], + visible: ( material.flag & 0x10 ) !== 0 && material.edgeSize > 0.0 + }; - if ( ! params.transparent ) { + } - this._checkImageTransparency( params.map, geometry, i ); + if ( params.map !== undefined ) { - } + if ( ! params.transparent ) { - params.emissive.multiplyScalar( 0.2 ); + this._checkImageTransparency( params.map, geometry, i ); } - materials.push( new MeshToonMaterial( params ) ); + params.emissive.multiplyScalar( 0.2 ); } - if ( data.metadata.format === 'pmx' ) { + materials.push( new MeshToonMaterial( params ) ); - // set transparent true if alpha morph is defined. + } - function checkAlphaMorph( elements, materials ) { + if ( data.metadata.format === 'pmx' ) { - for ( var i = 0, il = elements.length; i < il; i ++ ) { + // set transparent true if alpha morph is defined. - var element = elements[ i ]; + function checkAlphaMorph( elements, materials ) { - if ( element.index === - 1 ) continue; + for ( let i = 0, il = elements.length; i < il; i ++ ) { - var material = materials[ element.index ]; + const element = elements[ i ]; - if ( material.opacity !== element.diffuse[ 3 ] ) { + if ( element.index === - 1 ) continue; - material.transparent = true; + const material = materials[ element.index ]; - } + if ( material.opacity !== element.diffuse[ 3 ] ) { + + material.transparent = true; } } - for ( var i = 0, il = data.morphs.length; i < il; i ++ ) { + } - var morph = data.morphs[ i ]; - var elements = morph.elements; + for ( let i = 0, il = data.morphs.length; i < il; i ++ ) { - if ( morph.type === 0 ) { + const morph = data.morphs[ i ]; + const elements = morph.elements; - for ( var j = 0, jl = elements.length; j < jl; j ++ ) { + if ( morph.type === 0 ) { - var morph2 = data.morphs[ elements[ j ].index ]; + for ( let j = 0, jl = elements.length; j < jl; j ++ ) { - if ( morph2.type !== 8 ) continue; + const morph2 = data.morphs[ elements[ j ].index ]; - checkAlphaMorph( morph2.elements, materials ); + if ( morph2.type !== 8 ) continue; - } + checkAlphaMorph( morph2.elements, materials ); - } else if ( morph.type === 8 ) { + } - checkAlphaMorph( elements, materials ); + } else if ( morph.type === 8 ) { - } + checkAlphaMorph( elements, materials ); } } - return materials; - - }, + } - // private methods + return materials; - _getTGALoader: function () { + } - if ( this.tgaLoader === null ) { + // private methods - if ( TGALoader === undefined ) { + _getTGALoader() { - throw new Error( 'THREE.MMDLoader: Import TGALoader' ); + if ( this.tgaLoader === null ) { - } + if ( TGALoader === undefined ) { - this.tgaLoader = new TGALoader( this.manager ); + throw new Error( 'THREE.MMDLoader: Import TGALoader' ); } - return this.tgaLoader; - - }, + this.tgaLoader = new TGALoader( this.manager ); - _isDefaultToonTexture: function ( name ) { + } - if ( name.length !== 10 ) return false; + return this.tgaLoader; - return /toon(10|0[0-9])\.bmp/.test( name ); + } - }, + _isDefaultToonTexture( name ) { - _loadTexture: function ( filePath, textures, params, onProgress, onError ) { + if ( name.length !== 10 ) return false; - params = params || {}; + return /toon(10|0[0-9])\.bmp/.test( name ); - var scope = this; + } - var fullPath; + _loadTexture( filePath, textures, params, onProgress, onError ) { - if ( params.isDefaultToonTexture === true ) { + params = params || {}; - var index; + const scope = this; - try { + let fullPath; - index = parseInt( filePath.match( /toon([0-9]{2})\.bmp$/ )[ 1 ] ); + if ( params.isDefaultToonTexture === true ) { - } catch ( e ) { + let index; - console.warn( 'THREE.MMDLoader: ' + filePath + ' seems like a ' - + 'not right default texture path. Using toon00.bmp instead.' ); + try { - index = 0; + index = parseInt( filePath.match( /toon([0-9]{2})\.bmp$/ )[ 1 ] ); - } + } catch ( e ) { - fullPath = DEFAULT_TOON_TEXTURES[ index ]; - - } else { + console.warn( 'THREE.MMDLoader: ' + filePath + ' seems like a ' + + 'not right default texture path. Using toon00.bmp instead.' ); - fullPath = this.resourcePath + filePath; + index = 0; } - if ( textures[ fullPath ] !== undefined ) return textures[ fullPath ]; + fullPath = DEFAULT_TOON_TEXTURES[ index ]; - var loader = this.manager.getHandler( fullPath ); + } else { - if ( loader === null ) { + fullPath = this.resourcePath + filePath; - loader = ( filePath.slice( - 4 ).toLowerCase() === '.tga' ) - ? this._getTGALoader() - : this.textureLoader; + } - } + if ( textures[ fullPath ] !== undefined ) return textures[ fullPath ]; - var texture = loader.load( fullPath, function ( t ) { + let loader = this.manager.getHandler( fullPath ); - // MMD toon texture is Axis-Y oriented - // but Three.js gradient map is Axis-X oriented. - // So here replaces the toon texture image with the rotated one. - if ( params.isToonTexture === true ) { + if ( loader === null ) { - t.image = scope._getRotatedImage( t.image ); + loader = ( filePath.slice( - 4 ).toLowerCase() === '.tga' ) + ? this._getTGALoader() + : this.textureLoader; - t.magFilter = NearestFilter; - t.minFilter = NearestFilter; + } - } + const texture = loader.load( fullPath, function ( t ) { - t.flipY = false; - t.wrapS = RepeatWrapping; - t.wrapT = RepeatWrapping; + // MMD toon texture is Axis-Y oriented + // but Three.js gradient map is Axis-X oriented. + // So here replaces the toon texture image with the rotated one. + if ( params.isToonTexture === true ) { - for ( var i = 0; i < texture.readyCallbacks.length; i ++ ) { + t.image = scope._getRotatedImage( t.image ); - texture.readyCallbacks[ i ]( texture ); + t.magFilter = NearestFilter; + t.minFilter = NearestFilter; - } + } - delete texture.readyCallbacks; + t.flipY = false; + t.wrapS = RepeatWrapping; + t.wrapT = RepeatWrapping; - }, onProgress, onError ); + for ( let i = 0; i < texture.readyCallbacks.length; i ++ ) { + + texture.readyCallbacks[ i ]( texture ); - texture.readyCallbacks = []; + } - textures[ fullPath ] = texture; + delete texture.readyCallbacks; - return texture; + }, onProgress, onError ); - }, + texture.readyCallbacks = []; - _getRotatedImage: function ( image ) { + textures[ fullPath ] = texture; - var canvas = document.createElement( 'canvas' ); - var context = canvas.getContext( '2d' ); + return texture; - var width = image.width; - var height = image.height; + } - canvas.width = width; - canvas.height = height; + _getRotatedImage( image ) { - context.clearRect( 0, 0, width, height ); - context.translate( width / 2.0, height / 2.0 ); - context.rotate( 0.5 * Math.PI ); // 90.0 * Math.PI / 180.0 - context.translate( - width / 2.0, - height / 2.0 ); - context.drawImage( image, 0, 0 ); + const canvas = document.createElement( 'canvas' ); + const context = canvas.getContext( '2d' ); - return context.getImageData( 0, 0, width, height ); + const width = image.width; + const height = image.height; - }, + canvas.width = width; + canvas.height = height; - // Check if the partial image area used by the texture is transparent. - _checkImageTransparency: function ( map, geometry, groupIndex ) { + context.clearRect( 0, 0, width, height ); + context.translate( width / 2.0, height / 2.0 ); + context.rotate( 0.5 * Math.PI ); // 90.0 * Math.PI / 180.0 + context.translate( - width / 2.0, - height / 2.0 ); + context.drawImage( image, 0, 0 ); - map.readyCallbacks.push( function ( texture ) { + return context.getImageData( 0, 0, width, height ); - // Is there any efficient ways? - function createImageData( image ) { + } - var canvas = document.createElement( 'canvas' ); - canvas.width = image.width; - canvas.height = image.height; + // Check if the partial image area used by the texture is transparent. + _checkImageTransparency( map, geometry, groupIndex ) { - var context = canvas.getContext( '2d' ); - context.drawImage( image, 0, 0 ); + map.readyCallbacks.push( function ( texture ) { - return context.getImageData( 0, 0, canvas.width, canvas.height ); + // Is there any efficient ways? + function createImageData( image ) { - } + const canvas = document.createElement( 'canvas' ); + canvas.width = image.width; + canvas.height = image.height; - function detectImageTransparency( image, uvs, indices ) { + const context = canvas.getContext( '2d' ); + context.drawImage( image, 0, 0 ); - var width = image.width; - var height = image.height; - var data = image.data; - var threshold = 253; + return context.getImageData( 0, 0, canvas.width, canvas.height ); - if ( data.length / ( width * height ) !== 4 ) return false; + } - for ( var i = 0; i < indices.length; i += 3 ) { + function detectImageTransparency( image, uvs, indices ) { - var centerUV = { x: 0.0, y: 0.0 }; + const width = image.width; + const height = image.height; + const data = image.data; + const threshold = 253; - for ( var j = 0; j < 3; j ++ ) { + if ( data.length / ( width * height ) !== 4 ) return false; - var index = indices[ i * 3 + j ]; - var uv = { x: uvs[ index * 2 + 0 ], y: uvs[ index * 2 + 1 ] }; + for ( let i = 0; i < indices.length; i += 3 ) { - if ( getAlphaByUv( image, uv ) < threshold ) return true; + const centerUV = { x: 0.0, y: 0.0 }; - centerUV.x += uv.x; - centerUV.y += uv.y; + for ( let j = 0; j < 3; j ++ ) { - } + const index = indices[ i * 3 + j ]; + const uv = { x: uvs[ index * 2 + 0 ], y: uvs[ index * 2 + 1 ] }; - centerUV.x /= 3; - centerUV.y /= 3; + if ( getAlphaByUv( image, uv ) < threshold ) return true; - if ( getAlphaByUv( image, centerUV ) < threshold ) return true; + centerUV.x += uv.x; + centerUV.y += uv.y; } - return false; + centerUV.x /= 3; + centerUV.y /= 3; + + if ( getAlphaByUv( image, centerUV ) < threshold ) return true; } - /* + return false; + + } + + /* * This method expects * texture.flipY = false * texture.wrapS = RepeatWrapping * texture.wrapT = RepeatWrapping * TODO: more precise */ - function getAlphaByUv( image, uv ) { + function getAlphaByUv( image, uv ) { - var width = image.width; - var height = image.height; + const width = image.width; + const height = image.height; - var x = Math.round( uv.x * width ) % width; - var y = Math.round( uv.y * height ) % height; + let x = Math.round( uv.x * width ) % width; + let y = Math.round( uv.y * height ) % height; - if ( x < 0 ) x += width; - if ( y < 0 ) y += height; + if ( x < 0 ) x += width; + if ( y < 0 ) y += height; - var index = y * width + x; + const index = y * width + x; - return image.data[ index * 4 + 3 ]; + return image.data[ index * 4 + 3 ]; - } - - var imageData = texture.image.data !== undefined - ? texture.image - : createImageData( texture.image ); - - var group = geometry.groups[ groupIndex ]; - - if ( detectImageTransparency( - imageData, - geometry.attributes.uv.array, - geometry.index.array.slice( group.start, group.start + group.count ) ) ) { - - map.transparent = true; + } - } + const imageData = texture.image.data !== undefined + ? texture.image + : createImageData( texture.image ); - } ); + const group = geometry.groups[ groupIndex ]; - } + if ( detectImageTransparency( + imageData, + geometry.attributes.uv.array, + geometry.index.array.slice( group.start, group.start + group.count ) ) ) { - }; + map.transparent = true; - // + } - function AnimationBuilder() { + } ); } - AnimationBuilder.prototype = { - - constructor: AnimationBuilder, +} - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - build: function ( vmd, mesh ) { +// - // combine skeletal and morph animations +class AnimationBuilder { - var tracks = this.buildSkeletalAnimation( vmd, mesh ).tracks; - var tracks2 = this.buildMorphAnimation( vmd, mesh ).tracks; + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ + build( vmd, mesh ) { - for ( var i = 0, il = tracks2.length; i < il; i ++ ) { + // combine skeletal and morph animations - tracks.push( tracks2[ i ] ); + const tracks = this.buildSkeletalAnimation( vmd, mesh ).tracks; + const tracks2 = this.buildMorphAnimation( vmd, mesh ).tracks; - } + for ( let i = 0, il = tracks2.length; i < il; i ++ ) { - return new AnimationClip( '', - 1, tracks ); + tracks.push( tracks2[ i ] ); - }, + } - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - buildSkeletalAnimation: function ( vmd, mesh ) { + return new AnimationClip( '', - 1, tracks ); - function pushInterpolation( array, interpolation, index ) { + } - array.push( interpolation[ index + 0 ] / 127 ); // x1 - array.push( interpolation[ index + 8 ] / 127 ); // x2 - array.push( interpolation[ index + 4 ] / 127 ); // y1 - array.push( interpolation[ index + 12 ] / 127 ); // y2 + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ + buildSkeletalAnimation( vmd, mesh ) { - } + function pushInterpolation( array, interpolation, index ) { - var tracks = []; + array.push( interpolation[ index + 0 ] / 127 ); // x1 + array.push( interpolation[ index + 8 ] / 127 ); // x2 + array.push( interpolation[ index + 4 ] / 127 ); // y1 + array.push( interpolation[ index + 12 ] / 127 ); // y2 - var motions = {}; - var bones = mesh.skeleton.bones; - var boneNameDictionary = {}; + } - for ( var i = 0, il = bones.length; i < il; i ++ ) { + const tracks = []; - boneNameDictionary[ bones[ i ].name ] = true; + const motions = {}; + const bones = mesh.skeleton.bones; + const boneNameDictionary = {}; - } + for ( let i = 0, il = bones.length; i < il; i ++ ) { - for ( var i = 0; i < vmd.metadata.motionCount; i ++ ) { + boneNameDictionary[ bones[ i ].name ] = true; - var motion = vmd.motions[ i ]; - var boneName = motion.boneName; + } - if ( boneNameDictionary[ boneName ] === undefined ) continue; + for ( let i = 0; i < vmd.metadata.motionCount; i ++ ) { - motions[ boneName ] = motions[ boneName ] || []; - motions[ boneName ].push( motion ); + const motion = vmd.motions[ i ]; + const boneName = motion.boneName; - } + if ( boneNameDictionary[ boneName ] === undefined ) continue; - for ( var key in motions ) { + motions[ boneName ] = motions[ boneName ] || []; + motions[ boneName ].push( motion ); - var array = motions[ key ]; - - array.sort( function ( a, b ) { + } - return a.frameNum - b.frameNum; + for ( const key in motions ) { - } ); + const array = motions[ key ]; - var times = []; - var positions = []; - var rotations = []; - var pInterpolations = []; - var rInterpolations = []; + array.sort( function ( a, b ) { - var basePosition = mesh.skeleton.getBoneByName( key ).position.toArray(); + return a.frameNum - b.frameNum; - for ( var i = 0, il = array.length; i < il; i ++ ) { + } ); - var time = array[ i ].frameNum / 30; - var position = array[ i ].position; - var rotation = array[ i ].rotation; - var interpolation = array[ i ].interpolation; + const times = []; + const positions = []; + const rotations = []; + const pInterpolations = []; + const rInterpolations = []; - times.push( time ); + const basePosition = mesh.skeleton.getBoneByName( key ).position.toArray(); - for ( var j = 0; j < 3; j ++ ) positions.push( basePosition[ j ] + position[ j ] ); - for ( var j = 0; j < 4; j ++ ) rotations.push( rotation[ j ] ); - for ( var j = 0; j < 3; j ++ ) pushInterpolation( pInterpolations, interpolation, j ); + for ( let i = 0, il = array.length; i < il; i ++ ) { - pushInterpolation( rInterpolations, interpolation, 3 ); + const time = array[ i ].frameNum / 30; + const position = array[ i ].position; + const rotation = array[ i ].rotation; + const interpolation = array[ i ].interpolation; - } + times.push( time ); - var targetName = '.bones[' + key + ']'; + for ( let j = 0; j < 3; j ++ ) positions.push( basePosition[ j ] + position[ j ] ); + for ( let j = 0; j < 4; j ++ ) rotations.push( rotation[ j ] ); + for ( let j = 0; j < 3; j ++ ) pushInterpolation( pInterpolations, interpolation, j ); - tracks.push( this._createTrack( targetName + '.position', VectorKeyframeTrack, times, positions, pInterpolations ) ); - tracks.push( this._createTrack( targetName + '.quaternion', QuaternionKeyframeTrack, times, rotations, rInterpolations ) ); + pushInterpolation( rInterpolations, interpolation, 3 ); } - return new AnimationClip( '', - 1, tracks ); + const targetName = '.bones[' + key + ']'; - }, + tracks.push( this._createTrack( targetName + '.position', VectorKeyframeTrack, times, positions, pInterpolations ) ); + tracks.push( this._createTrack( targetName + '.quaternion', QuaternionKeyframeTrack, times, rotations, rInterpolations ) ); - /** - * @param {Object} vmd - parsed VMD data - * @param {SkinnedMesh} mesh - tracks will be fitting to mesh - * @return {AnimationClip} - */ - buildMorphAnimation: function ( vmd, mesh ) { + } - var tracks = []; + return new AnimationClip( '', - 1, tracks ); - var morphs = {}; - var morphTargetDictionary = mesh.morphTargetDictionary; + } + + /** + * @param {Object} vmd - parsed VMD data + * @param {SkinnedMesh} mesh - tracks will be fitting to mesh + * @return {AnimationClip} + */ + buildMorphAnimation( vmd, mesh ) { - for ( var i = 0; i < vmd.metadata.morphCount; i ++ ) { + const tracks = []; - var morph = vmd.morphs[ i ]; - var morphName = morph.morphName; + const morphs = {}; + const morphTargetDictionary = mesh.morphTargetDictionary; - if ( morphTargetDictionary[ morphName ] === undefined ) continue; + for ( let i = 0; i < vmd.metadata.morphCount; i ++ ) { - morphs[ morphName ] = morphs[ morphName ] || []; - morphs[ morphName ].push( morph ); + const morph = vmd.morphs[ i ]; + const morphName = morph.morphName; - } + if ( morphTargetDictionary[ morphName ] === undefined ) continue; - for ( var key in morphs ) { + morphs[ morphName ] = morphs[ morphName ] || []; + morphs[ morphName ].push( morph ); - var array = morphs[ key ]; + } - array.sort( function ( a, b ) { + for ( const key in morphs ) { - return a.frameNum - b.frameNum; + const array = morphs[ key ]; - } ); + array.sort( function ( a, b ) { - var times = []; - var values = []; + return a.frameNum - b.frameNum; - for ( var i = 0, il = array.length; i < il; i ++ ) { + } ); - times.push( array[ i ].frameNum / 30 ); - values.push( array[ i ].weight ); + const times = []; + const values = []; - } + for ( let i = 0, il = array.length; i < il; i ++ ) { - tracks.push( new NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetDictionary[ key ] + ']', times, values ) ); + times.push( array[ i ].frameNum / 30 ); + values.push( array[ i ].weight ); } - return new AnimationClip( '', - 1, tracks ); - - }, + tracks.push( new NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetDictionary[ key ] + ']', times, values ) ); - /** - * @param {Object} vmd - parsed VMD data - * @return {AnimationClip} - */ - buildCameraAnimation: function ( vmd ) { + } - function pushVector3( array, vec ) { + return new AnimationClip( '', - 1, tracks ); - array.push( vec.x ); - array.push( vec.y ); - array.push( vec.z ); + } - } + /** + * @param {Object} vmd - parsed VMD data + * @return {AnimationClip} + */ + buildCameraAnimation( vmd ) { - function pushQuaternion( array, q ) { + function pushVector3( array, vec ) { - array.push( q.x ); - array.push( q.y ); - array.push( q.z ); - array.push( q.w ); + array.push( vec.x ); + array.push( vec.y ); + array.push( vec.z ); - } + } - function pushInterpolation( array, interpolation, index ) { + function pushQuaternion( array, q ) { - array.push( interpolation[ index * 4 + 0 ] / 127 ); // x1 - array.push( interpolation[ index * 4 + 1 ] / 127 ); // x2 - array.push( interpolation[ index * 4 + 2 ] / 127 ); // y1 - array.push( interpolation[ index * 4 + 3 ] / 127 ); // y2 + array.push( q.x ); + array.push( q.y ); + array.push( q.z ); + array.push( q.w ); - } + } - var tracks = []; + function pushInterpolation( array, interpolation, index ) { - var cameras = vmd.cameras === undefined ? [] : vmd.cameras.slice(); + array.push( interpolation[ index * 4 + 0 ] / 127 ); // x1 + array.push( interpolation[ index * 4 + 1 ] / 127 ); // x2 + array.push( interpolation[ index * 4 + 2 ] / 127 ); // y1 + array.push( interpolation[ index * 4 + 3 ] / 127 ); // y2 - cameras.sort( function ( a, b ) { + } - return a.frameNum - b.frameNum; + const cameras = vmd.cameras === undefined ? [] : vmd.cameras.slice(); - } ); + cameras.sort( function ( a, b ) { - var times = []; - var centers = []; - var quaternions = []; - var positions = []; - var fovs = []; + return a.frameNum - b.frameNum; - var cInterpolations = []; - var qInterpolations = []; - var pInterpolations = []; - var fInterpolations = []; + } ); - var quaternion = new Quaternion(); - var euler = new Euler(); - var position = new Vector3(); - var center = new Vector3(); + const times = []; + const centers = []; + const quaternions = []; + const positions = []; + const fovs = []; - for ( var i = 0, il = cameras.length; i < il; i ++ ) { + const cInterpolations = []; + const qInterpolations = []; + const pInterpolations = []; + const fInterpolations = []; - var motion = cameras[ i ]; + const quaternion = new Quaternion(); + const euler = new Euler(); + const position = new Vector3(); + const center = new Vector3(); - var time = motion.frameNum / 30; - var pos = motion.position; - var rot = motion.rotation; - var distance = motion.distance; - var fov = motion.fov; - var interpolation = motion.interpolation; + for ( let i = 0, il = cameras.length; i < il; i ++ ) { - times.push( time ); + const motion = cameras[ i ]; - position.set( 0, 0, - distance ); - center.set( pos[ 0 ], pos[ 1 ], pos[ 2 ] ); + const time = motion.frameNum / 30; + const pos = motion.position; + const rot = motion.rotation; + const distance = motion.distance; + const fov = motion.fov; + const interpolation = motion.interpolation; - euler.set( - rot[ 0 ], - rot[ 1 ], - rot[ 2 ] ); - quaternion.setFromEuler( euler ); + times.push( time ); - position.add( center ); - position.applyQuaternion( quaternion ); + position.set( 0, 0, - distance ); + center.set( pos[ 0 ], pos[ 1 ], pos[ 2 ] ); - pushVector3( centers, center ); - pushQuaternion( quaternions, quaternion ); - pushVector3( positions, position ); + euler.set( - rot[ 0 ], - rot[ 1 ], - rot[ 2 ] ); + quaternion.setFromEuler( euler ); - fovs.push( fov ); + position.add( center ); + position.applyQuaternion( quaternion ); - for ( var j = 0; j < 3; j ++ ) { + pushVector3( centers, center ); + pushQuaternion( quaternions, quaternion ); + pushVector3( positions, position ); - pushInterpolation( cInterpolations, interpolation, j ); + fovs.push( fov ); - } + for ( let j = 0; j < 3; j ++ ) { - pushInterpolation( qInterpolations, interpolation, 3 ); + pushInterpolation( cInterpolations, interpolation, j ); - // use the same parameter for x, y, z axis. - for ( var j = 0; j < 3; j ++ ) { + } - pushInterpolation( pInterpolations, interpolation, 4 ); + pushInterpolation( qInterpolations, interpolation, 3 ); - } + // use the same parameter for x, y, z axis. + for ( let j = 0; j < 3; j ++ ) { - pushInterpolation( fInterpolations, interpolation, 5 ); + pushInterpolation( pInterpolations, interpolation, 4 ); } - var tracks = []; + pushInterpolation( fInterpolations, interpolation, 5 ); + + } - // I expect an object whose name 'target' exists under THREE.Camera - tracks.push( this._createTrack( 'target.position', VectorKeyframeTrack, times, centers, cInterpolations ) ); + const tracks = []; - tracks.push( this._createTrack( '.quaternion', QuaternionKeyframeTrack, times, quaternions, qInterpolations ) ); - tracks.push( this._createTrack( '.position', VectorKeyframeTrack, times, positions, pInterpolations ) ); - tracks.push( this._createTrack( '.fov', NumberKeyframeTrack, times, fovs, fInterpolations ) ); + // I expect an object whose name 'target' exists under THREE.Camera + tracks.push( this._createTrack( 'target.position', VectorKeyframeTrack, times, centers, cInterpolations ) ); - return new AnimationClip( '', - 1, tracks ); + tracks.push( this._createTrack( '.quaternion', QuaternionKeyframeTrack, times, quaternions, qInterpolations ) ); + tracks.push( this._createTrack( '.position', VectorKeyframeTrack, times, positions, pInterpolations ) ); + tracks.push( this._createTrack( '.fov', NumberKeyframeTrack, times, fovs, fInterpolations ) ); - }, + return new AnimationClip( '', - 1, tracks ); - // private method + } - _createTrack: function ( node, typedKeyframeTrack, times, values, interpolations ) { + // private method - /* + _createTrack( node, typedKeyframeTrack, times, values, interpolations ) { + + /* * optimizes here not to let KeyframeTrackPrototype optimize * because KeyframeTrackPrototype optimizes times and values but * doesn't optimize interpolations. */ - if ( times.length > 2 ) { + if ( times.length > 2 ) { - times = times.slice(); - values = values.slice(); - interpolations = interpolations.slice(); + times = times.slice(); + values = values.slice(); + interpolations = interpolations.slice(); - var stride = values.length / times.length; - var interpolateStride = interpolations.length / times.length; + const stride = values.length / times.length; + const interpolateStride = interpolations.length / times.length; - var index = 1; + let index = 1; - for ( var aheadIndex = 2, endIndex = times.length; aheadIndex < endIndex; aheadIndex ++ ) { + for ( let aheadIndex = 2, endIndex = times.length; aheadIndex < endIndex; aheadIndex ++ ) { - for ( var i = 0; i < stride; i ++ ) { + for ( let i = 0; i < stride; i ++ ) { - if ( values[ index * stride + i ] !== values[ ( index - 1 ) * stride + i ] || + if ( values[ index * stride + i ] !== values[ ( index - 1 ) * stride + i ] || values[ index * stride + i ] !== values[ aheadIndex * stride + i ] ) { - index ++; - break; - - } + index ++; + break; } - if ( aheadIndex > index ) { + } - times[ index ] = times[ aheadIndex ]; + if ( aheadIndex > index ) { - for ( var i = 0; i < stride; i ++ ) { + times[ index ] = times[ aheadIndex ]; - values[ index * stride + i ] = values[ aheadIndex * stride + i ]; + for ( let i = 0; i < stride; i ++ ) { - } + values[ index * stride + i ] = values[ aheadIndex * stride + i ]; - for ( var i = 0; i < interpolateStride; i ++ ) { + } - interpolations[ index * interpolateStride + i ] = interpolations[ aheadIndex * interpolateStride + i ]; + for ( let i = 0; i < interpolateStride; i ++ ) { - } + interpolations[ index * interpolateStride + i ] = interpolations[ aheadIndex * interpolateStride + i ]; } } - times.length = index + 1; - values.length = ( index + 1 ) * stride; - interpolations.length = ( index + 1 ) * interpolateStride; - } - var track = new typedKeyframeTrack( node, times, values ); + times.length = index + 1; + values.length = ( index + 1 ) * stride; + interpolations.length = ( index + 1 ) * interpolateStride; - track.createInterpolant = function InterpolantFactoryMethodCubicBezier( result ) { + } - return new CubicBezierInterpolation( this.times, this.values, this.getValueSize(), result, new Float32Array( interpolations ) ); + const track = new typedKeyframeTrack( node, times, values ); - }; + track.createInterpolant = function InterpolantFactoryMethodCubicBezier( result ) { - return track; + return new CubicBezierInterpolation( this.times, this.values, this.getValueSize(), result, new Float32Array( interpolations ) ); - } + }; - }; + return track; - // interpolation + } - function CubicBezierInterpolation( parameterPositions, sampleValues, sampleSize, resultBuffer, params ) { +} - Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); +// interpolation - this.interpolationParams = params; +class CubicBezierInterpolation extends Interpolant { - } + constructor( parameterPositions, sampleValues, sampleSize, resultBuffer, params ) { - CubicBezierInterpolation.prototype = Object.assign( Object.create( Interpolant.prototype ), { + super( parameterPositions, sampleValues, sampleSize, resultBuffer ); + + this.interpolationParams = params; - constructor: CubicBezierInterpolation, + } - interpolate_: function ( i1, t0, t, t1 ) { + interpolate_( i1, t0, t, t1 ) { - var result = this.resultBuffer; - var values = this.sampleValues; - var stride = this.valueSize; - var params = this.interpolationParams; + const result = this.resultBuffer; + const values = this.sampleValues; + const stride = this.valueSize; + const params = this.interpolationParams; - var offset1 = i1 * stride; - var offset0 = offset1 - stride; + const offset1 = i1 * stride; + const offset0 = offset1 - stride; - // No interpolation if next key frame is in one frame in 30fps. - // This is from MMD animation spec. - // '1.5' is for precision loss. times are Float32 in Three.js Animation system. - var weight1 = ( ( t1 - t0 ) < 1 / 30 * 1.5 ) ? 0.0 : ( t - t0 ) / ( t1 - t0 ); + // No interpolation if next key frame is in one frame in 30fps. + // This is from MMD animation spec. + // '1.5' is for precision loss. times are Float32 in Three.js Animation system. + const weight1 = ( ( t1 - t0 ) < 1 / 30 * 1.5 ) ? 0.0 : ( t - t0 ) / ( t1 - t0 ); - if ( stride === 4 ) { // Quaternion + if ( stride === 4 ) { // Quaternion - var x1 = params[ i1 * 4 + 0 ]; - var x2 = params[ i1 * 4 + 1 ]; - var y1 = params[ i1 * 4 + 2 ]; - var y2 = params[ i1 * 4 + 3 ]; + const x1 = params[ i1 * 4 + 0 ]; + const x2 = params[ i1 * 4 + 1 ]; + const y1 = params[ i1 * 4 + 2 ]; + const y2 = params[ i1 * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - Quaternion.slerpFlat( result, 0, values, offset0, values, offset1, ratio ); + Quaternion.slerpFlat( result, 0, values, offset0, values, offset1, ratio ); - } else if ( stride === 3 ) { // Vector3 + } else if ( stride === 3 ) { // Vector3 - for ( var i = 0; i !== stride; ++ i ) { + for ( let i = 0; i !== stride; ++ i ) { - var x1 = params[ i1 * 12 + i * 4 + 0 ]; - var x2 = params[ i1 * 12 + i * 4 + 1 ]; - var y1 = params[ i1 * 12 + i * 4 + 2 ]; - var y2 = params[ i1 * 12 + i * 4 + 3 ]; + const x1 = params[ i1 * 12 + i * 4 + 0 ]; + const x2 = params[ i1 * 12 + i * 4 + 1 ]; + const y1 = params[ i1 * 12 + i * 4 + 2 ]; + const y2 = params[ i1 * 12 + i * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - result[ i ] = values[ offset0 + i ] * ( 1 - ratio ) + values[ offset1 + i ] * ratio; + result[ i ] = values[ offset0 + i ] * ( 1 - ratio ) + values[ offset1 + i ] * ratio; - } + } - } else { // Number + } else { // Number - var x1 = params[ i1 * 4 + 0 ]; - var x2 = params[ i1 * 4 + 1 ]; - var y1 = params[ i1 * 4 + 2 ]; - var y2 = params[ i1 * 4 + 3 ]; + const x1 = params[ i1 * 4 + 0 ]; + const x2 = params[ i1 * 4 + 1 ]; + const y1 = params[ i1 * 4 + 2 ]; + const y2 = params[ i1 * 4 + 3 ]; - var ratio = this._calculate( x1, x2, y1, y2, weight1 ); + const ratio = this._calculate( x1, x2, y1, y2, weight1 ); - result[ 0 ] = values[ offset0 ] * ( 1 - ratio ) + values[ offset1 ] * ratio; + result[ 0 ] = values[ offset0 ] * ( 1 - ratio ) + values[ offset1 ] * ratio; - } + } - return result; + return result; - }, + } - _calculate: function ( x1, x2, y1, y2, x ) { + _calculate( x1, x2, y1, y2, x ) { - /* + /* * Cubic Bezier curves * https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves * @@ -2030,40 +2003,36 @@ var MMDLoader = ( function () { * https://en.wikipedia.org/wiki/Newton%27s_method) */ - var c = 0.5; - var t = c; - var s = 1.0 - t; - var loop = 15; - var eps = 1e-5; - var math = Math; - - var sst3, stt3, ttt; + let c = 0.5; + let t = c; + let s = 1.0 - t; + const loop = 15; + const eps = 1e-5; + const math = Math; - for ( var i = 0; i < loop; i ++ ) { + let sst3, stt3, ttt; - sst3 = 3.0 * s * s * t; - stt3 = 3.0 * s * t * t; - ttt = t * t * t; + for ( let i = 0; i < loop; i ++ ) { - var ft = ( sst3 * x1 ) + ( stt3 * x2 ) + ( ttt ) - x; + sst3 = 3.0 * s * s * t; + stt3 = 3.0 * s * t * t; + ttt = t * t * t; - if ( math.abs( ft ) < eps ) break; + const ft = ( sst3 * x1 ) + ( stt3 * x2 ) + ( ttt ) - x; - c /= 2.0; + if ( math.abs( ft ) < eps ) break; - t += ( ft < 0 ) ? c : - c; - s = 1.0 - t; + c /= 2.0; - } - - return ( sst3 * y1 ) + ( stt3 * y2 ) + ttt; + t += ( ft < 0 ) ? c : - c; + s = 1.0 - t; } - } ); + return ( sst3 * y1 ) + ( stt3 * y2 ) + ttt; - return MMDLoader; + } -} )(); +} export { MMDLoader }; diff --git a/examples/jsm/loaders/NRRDLoader.js b/examples/jsm/loaders/NRRDLoader.js index 59aa50ca24ca0e..9adf5c6916dc88 100644 --- a/examples/jsm/loaders/NRRDLoader.js +++ b/examples/jsm/loaders/NRRDLoader.js @@ -7,21 +7,19 @@ import { import * as fflate from '../libs/fflate.module.min.js'; import { Volume } from '../misc/Volume.js'; -var NRRDLoader = function ( manager ) { +class NRRDLoader extends Loader { - Loader.call( this, manager ); + constructor( manager ) { -}; - -NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { + super( manager ); - constructor: NRRDLoader, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; + const scope = this; - var loader = new FileLoader( scope.manager ); + const loader = new FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( scope.requestHeader ); @@ -50,21 +48,21 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { }, onProgress, onError ); - }, + } - parse: function ( data ) { + parse( data ) { // this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X - var _data = data; + let _data = data; - var _dataPointer = 0; + let _dataPointer = 0; - var _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0; + const _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0; - var _littleEndian = true; + const _littleEndian = true; - var headerObject = {}; + const headerObject = {}; function scan( type, chunks ) { @@ -74,8 +72,8 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - var _chunkSize = 1; - var _array_type = Uint8Array; + let _chunkSize = 1; + let _array_type = Uint8Array; switch ( type ) { @@ -119,7 +117,7 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } // increase the data pointer in-place - var _bytes = new _array_type( _data.slice( _dataPointer, + let _bytes = new _array_type( _data.slice( _dataPointer, _dataPointer += chunks * _chunkSize ) ); // if required, flip the endianness of the bytes @@ -146,12 +144,12 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { function flipEndianness( array, chunkSize ) { - var u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength ); - for ( var i = 0; i < array.byteLength; i += chunkSize ) { + const u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength ); + for ( let i = 0; i < array.byteLength; i += chunkSize ) { - for ( var j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) { + for ( let j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) { - var tmp = u8[ k ]; + const tmp = u8[ k ]; u8[ k ] = u8[ j ]; u8[ j ] = tmp; @@ -166,8 +164,8 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { //parse the header function parseHeader( header ) { - var data, field, fn, i, l, lines, m, _i, _len; - lines = header.split( /\r?\n/ ); + let data, field, fn, i, l, m, _i, _len; + const lines = header.split( /\r?\n/ ); for ( _i = 0, _len = lines.length; _i < _len; _i ++ ) { l = lines[ _i ]; @@ -180,7 +178,7 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { field = m[ 1 ].trim(); data = m[ 2 ].trim(); - fn = NRRDLoader.prototype.fieldFunctions[ field ]; + fn = _fieldFunctions[ field ]; if ( fn ) { fn.call( headerObject, data ); @@ -233,34 +231,34 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { //parse the data when registred as one of this type : 'text', 'ascii', 'txt' function parseDataAsText( data, start, end ) { - var number = ''; + let number = ''; start = start || 0; end = end || data.length; - var value; + let value; //length of the result is the product of the sizes - var lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) { + const lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) { return previous * current; }, 1 ); - var base = 10; + let base = 10; if ( headerObject.encoding === 'hex' ) { base = 16; } - var result = new headerObject.__array( lengthOfTheResult ); - var resultIndex = 0; - var parsingFunction = parseInt; + const result = new headerObject.__array( lengthOfTheResult ); + let resultIndex = 0; + let parsingFunction = parseInt; if ( headerObject.__array === Float32Array || headerObject.__array === Float64Array ) { parsingFunction = parseFloat; } - for ( var i = start; i < end; i ++ ) { + for ( let i = start; i < end; i ++ ) { value = data[ i ]; //if value is not a space @@ -294,11 +292,11 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - var _bytes = scan( 'uchar', data.byteLength ); - var _length = _bytes.length; - var _header = null; - var _data_start = 0; - var i; + const _bytes = scan( 'uchar', data.byteLength ); + const _length = _bytes.length; + let _header = null; + let _data_start = 0; + let i; for ( i = 1; i < _length; i ++ ) { if ( _bytes[ i - 1 ] == 10 && _bytes[ i ] == 10 ) { @@ -317,7 +315,7 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // parse the header parseHeader( _header ); - var _data = _bytes.subarray( _data_start ); // the data without header + _data = _bytes.subarray( _data_start ); // the data without header if ( headerObject.encoding.substring( 0, 2 ) === 'gz' ) { // we need to decompress the datastream @@ -331,9 +329,9 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } else if ( headerObject.encoding === 'raw' ) { //we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header - var _copy = new Uint8Array( _data.length ); + const _copy = new Uint8Array( _data.length ); - for ( var i = 0; i < _data.length; i ++ ) { + for ( let i = 0; i < _data.length; i ++ ) { _copy[ i ] = _data[ i ]; @@ -346,16 +344,16 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // .. let's use the underlying array buffer _data = _data.buffer; - var volume = new Volume(); + const volume = new Volume(); volume.header = headerObject; // // parse the (unzipped) data to a datastream of the correct type // volume.data = new headerObject.__array( _data ); // get the min and max intensities - var min_max = volume.computeMinMax(); - var min = min_max[ 0 ]; - var max = min_max[ 1 ]; + const min_max = volume.computeMinMax(); + const min = min_max[ 0 ]; + const max = min_max[ 1 ]; // attach the scalar range to the volume volume.windowLow = min; volume.windowHigh = max; @@ -366,11 +364,11 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { volume.yLength = volume.dimensions[ 1 ]; volume.zLength = volume.dimensions[ 2 ]; // spacing - var spacingX = ( new Vector3( headerObject.vectors[ 0 ][ 0 ], headerObject.vectors[ 0 ][ 1 ], + const spacingX = ( new Vector3( headerObject.vectors[ 0 ][ 0 ], headerObject.vectors[ 0 ][ 1 ], headerObject.vectors[ 0 ][ 2 ] ) ).length(); - var spacingY = ( new Vector3( headerObject.vectors[ 1 ][ 0 ], headerObject.vectors[ 1 ][ 1 ], + const spacingY = ( new Vector3( headerObject.vectors[ 1 ][ 0 ], headerObject.vectors[ 1 ][ 1 ], headerObject.vectors[ 1 ][ 2 ] ) ).length(); - var spacingZ = ( new Vector3( headerObject.vectors[ 2 ][ 0 ], headerObject.vectors[ 2 ][ 1 ], + const spacingZ = ( new Vector3( headerObject.vectors[ 2 ][ 0 ], headerObject.vectors[ 2 ][ 1 ], headerObject.vectors[ 2 ][ 2 ] ) ).length(); volume.spacing = [ spacingX, spacingY, spacingZ ]; @@ -378,9 +376,9 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { // Create IJKtoRAS matrix volume.matrix = new Matrix4(); - var _spaceX = 1; - var _spaceY = 1; - var _spaceZ = 1; + let _spaceX = 1; + let _spaceY = 1; + const _spaceZ = 1; if ( headerObject.space == 'left-posterior-superior' ) { @@ -404,7 +402,7 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } else { - var v = headerObject.vectors; + const v = headerObject.vectors; volume.matrix.set( _spaceX * v[ 0 ][ 0 ], _spaceX * v[ 1 ][ 0 ], _spaceX * v[ 2 ][ 0 ], 0, @@ -434,9 +432,9 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return volume; - }, + } - parseChars: function ( array, start, end ) { + parseChars( array, start, end ) { // without borders, use the whole array if ( start === undefined ) { @@ -451,9 +449,9 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } - var output = ''; + let output = ''; // create and append the chars - var i = 0; + let i = 0; for ( i = start; i < end; ++ i ) { output += String.fromCharCode( array[ i ] ); @@ -462,178 +460,176 @@ NRRDLoader.prototype = Object.assign( Object.create( Loader.prototype ), { return output; - }, + } - fieldFunctions: { +} - type: function ( data ) { +const _fieldFunctions = { - switch ( data ) { + type: function ( data ) { - case 'uchar': - case 'unsigned char': - case 'uint8': - case 'uint8_t': - this.__array = Uint8Array; - break; - case 'signed char': - case 'int8': - case 'int8_t': - this.__array = Int8Array; - break; - case 'short': - case 'short int': - case 'signed short': - case 'signed short int': - case 'int16': - case 'int16_t': - this.__array = Int16Array; - break; - case 'ushort': - case 'unsigned short': - case 'unsigned short int': - case 'uint16': - case 'uint16_t': - this.__array = Uint16Array; - break; - case 'int': - case 'signed int': - case 'int32': - case 'int32_t': - this.__array = Int32Array; - break; - case 'uint': - case 'unsigned int': - case 'uint32': - case 'uint32_t': - this.__array = Uint32Array; - break; - case 'float': - this.__array = Float32Array; - break; - case 'double': - this.__array = Float64Array; - break; - default: - throw new Error( 'Unsupported NRRD data type: ' + data ); + switch ( data ) { - } + case 'uchar': + case 'unsigned char': + case 'uint8': + case 'uint8_t': + this.__array = Uint8Array; + break; + case 'signed char': + case 'int8': + case 'int8_t': + this.__array = Int8Array; + break; + case 'short': + case 'short int': + case 'signed short': + case 'signed short int': + case 'int16': + case 'int16_t': + this.__array = Int16Array; + break; + case 'ushort': + case 'unsigned short': + case 'unsigned short int': + case 'uint16': + case 'uint16_t': + this.__array = Uint16Array; + break; + case 'int': + case 'signed int': + case 'int32': + case 'int32_t': + this.__array = Int32Array; + break; + case 'uint': + case 'unsigned int': + case 'uint32': + case 'uint32_t': + this.__array = Uint32Array; + break; + case 'float': + this.__array = Float32Array; + break; + case 'double': + this.__array = Float64Array; + break; + default: + throw new Error( 'Unsupported NRRD data type: ' + data ); - return this.type = data; + } - }, + return this.type = data; - endian: function ( data ) { + }, - return this.endian = data; + endian: function ( data ) { - }, + return this.endian = data; - encoding: function ( data ) { + }, - return this.encoding = data; + encoding: function ( data ) { - }, + return this.encoding = data; - dimension: function ( data ) { + }, - return this.dim = parseInt( data, 10 ); + dimension: function ( data ) { - }, + return this.dim = parseInt( data, 10 ); - sizes: function ( data ) { + }, - var i; - return this.sizes = ( function () { + sizes: function ( data ) { - var _i, _len, _ref, _results; - _ref = data.split( /\s+/ ); - _results = []; + let i; + return this.sizes = ( function () { - for ( _i = 0, _len = _ref.length; _i < _len; _i ++ ) { + const _ref = data.split( /\s+/ ); + const _results = []; - i = _ref[ _i ]; - _results.push( parseInt( i, 10 ) ); + for ( let _i = 0, _len = _ref.length; _i < _len; _i ++ ) { - } + i = _ref[ _i ]; + _results.push( parseInt( i, 10 ) ); - return _results; + } - } )(); + return _results; - }, + } )(); - space: function ( data ) { + }, - return this.space = data; + space: function ( data ) { - }, + return this.space = data; - 'space origin': function ( data ) { + }, - return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' ); + 'space origin': function ( data ) { - }, + return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' ); - 'space directions': function ( data ) { + }, - var f, parts, v; - parts = data.match( /\(.*?\)/g ); - return this.vectors = ( function () { + 'space directions': function ( data ) { - var _i, _len, _results; - _results = []; + let f, v; + const parts = data.match( /\(.*?\)/g ); + return this.vectors = ( function () { - for ( _i = 0, _len = parts.length; _i < _len; _i ++ ) { + const _results = []; - v = parts[ _i ]; - _results.push( ( function () { + for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { - var _j, _len2, _ref, _results2; - _ref = v.slice( 1, - 1 ).split( /,/ ); - _results2 = []; + v = parts[ _i ]; + _results.push( ( function () { - for ( _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) { + const _ref = v.slice( 1, - 1 ).split( /,/ ); + const _results2 = []; - f = _ref[ _j ]; - _results2.push( parseFloat( f ) ); + for ( let _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) { - } + f = _ref[ _j ]; + _results2.push( parseFloat( f ) ); - return _results2; + } - } )() ); + return _results2; - } + } )() ); + + } - return _results; + return _results; - } )(); + } )(); - }, + }, - spacings: function ( data ) { + spacings: function ( data ) { - var f, parts; - parts = data.split( /\s+/ ); - return this.spacings = ( function () { + let f; + const parts = data.split( /\s+/ ); + return this.spacings = ( function () { - var _i, _len, _results = []; + const _results = []; - for ( _i = 0, _len = parts.length; _i < _len; _i ++ ) { + for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { - f = parts[ _i ]; - _results.push( parseFloat( f ) ); + f = parts[ _i ]; + _results.push( parseFloat( f ) ); - } + } - return _results; + return _results; - } )(); + } )(); - } } -} ); +}; export { NRRDLoader }; diff --git a/examples/jsm/loaders/NodeMaterialLoader.js b/examples/jsm/loaders/NodeMaterialLoader.js index e03c29369f521a..8a64d60db6b3f3 100644 --- a/examples/jsm/loaders/NodeMaterialLoader.js +++ b/examples/jsm/loaders/NodeMaterialLoader.js @@ -1,81 +1,29 @@ import { - DefaultLoadingManager, + Loader, FileLoader } from '../../../build/three.module.js'; import * as Nodes from '../nodes/Nodes.js'; -var NodeMaterialLoader = function ( manager, library ) { +class NodeMaterialLoader extends Loader { - this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; + constructor( manager, library = {} ) { - this.nodes = {}; - this.materials = {}; - this.passes = {}; - this.names = {}; - this.library = library || {}; + super( manager ); -}; - -var NodeMaterialLoaderUtils = { - - replaceUUIDObject: function ( object, uuid, value, recursive ) { - - recursive = recursive !== undefined ? recursive : true; - - if ( typeof uuid === 'object' ) uuid = uuid.uuid; - - if ( typeof object === 'object' ) { - - var keys = Object.keys( object ); - - for ( var i = 0; i < keys.length; i ++ ) { - - var key = keys[ i ]; - - if ( recursive ) { - - object[ key ] = this.replaceUUIDObject( object[ key ], uuid, value ); - - } - - if ( key === uuid ) { - - object[ uuid ] = object[ key ]; - - delete object[ key ]; - - } - - } - - } - - return object === uuid ? value : object; - - }, - - replaceUUID: function ( json, uuid, value ) { - - this.replaceUUIDObject( json, uuid, value, false ); - this.replaceUUIDObject( json.nodes, uuid, value ); - this.replaceUUIDObject( json.materials, uuid, value ); - this.replaceUUIDObject( json.passes, uuid, value ); - this.replaceUUIDObject( json.library, uuid, value, false ); - - return json; + this.nodes = {}; + this.materials = {}; + this.passes = {}; + this.names = {}; + this.library = library; } -}; - -Object.assign( NodeMaterialLoader.prototype, { - - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; + const scope = this; - var loader = new FileLoader( scope.manager ); + const loader = new FileLoader( scope.manager ); loader.setPath( scope.path ); loader.load( url, function ( text ) { @@ -85,22 +33,15 @@ Object.assign( NodeMaterialLoader.prototype, { return this; - }, - - setPath: function ( value ) { - - this.path = value; - return this; - - }, + } - getObjectByName: function ( uuid ) { + getObjectByName( uuid ) { return this.names[ uuid ]; - }, + } - getObjectById: function ( uuid ) { + getObjectById( uuid ) { return this.library[ uuid ] || this.nodes[ uuid ] || @@ -108,11 +49,11 @@ Object.assign( NodeMaterialLoader.prototype, { this.passes[ uuid ] || this.names[ uuid ]; - }, + } - getNode: function ( uuid ) { + getNode( uuid ) { - var object = this.getObjectById( uuid ); + const object = this.getObjectById( uuid ); if ( ! object ) { @@ -122,9 +63,9 @@ Object.assign( NodeMaterialLoader.prototype, { return object; - }, + } - resolve: function ( json ) { + resolve( json ) { switch ( typeof json ) { @@ -147,7 +88,7 @@ Object.assign( NodeMaterialLoader.prototype, { if ( Array.isArray( json ) ) { - for ( var i = 0; i < json.length; i ++ ) { + for ( let i = 0; i < json.length; i ++ ) { json[ i ] = this.resolve( json[ i ] ); @@ -155,7 +96,7 @@ Object.assign( NodeMaterialLoader.prototype, { } else { - for ( var prop in json ) { + for ( const prop in json ) { if ( prop === 'uuid' ) continue; @@ -169,11 +110,11 @@ Object.assign( NodeMaterialLoader.prototype, { return json; - }, + } - declare: function ( json ) { + declare( json ) { - var uuid, node, object; + let uuid, node, object; for ( uuid in json.nodes ) { @@ -235,11 +176,11 @@ Object.assign( NodeMaterialLoader.prototype, { return json; - }, + } - parse: function ( json ) { + parse( json ) { - var uuid; + let uuid; json = this.resolve( this.declare( json ) ); @@ -265,6 +206,58 @@ Object.assign( NodeMaterialLoader.prototype, { } -} ); +} + +class NodeMaterialLoaderUtils { + + static replaceUUIDObject( object, uuid, value, recursive ) { + + recursive = recursive !== undefined ? recursive : true; + + if ( typeof uuid === 'object' ) uuid = uuid.uuid; + + if ( typeof object === 'object' ) { + + const keys = Object.keys( object ); + + for ( let i = 0; i < keys.length; i ++ ) { + + const key = keys[ i ]; + + if ( recursive ) { + + object[ key ] = this.replaceUUIDObject( object[ key ], uuid, value ); + + } + + if ( key === uuid ) { + + object[ uuid ] = object[ key ]; + + delete object[ key ]; + + } + + } + + } + + return object === uuid ? value : object; + + } + + static replaceUUID( json, uuid, value ) { + + this.replaceUUIDObject( json, uuid, value, false ); + this.replaceUUIDObject( json.nodes, uuid, value ); + this.replaceUUIDObject( json.materials, uuid, value ); + this.replaceUUIDObject( json.passes, uuid, value ); + this.replaceUUIDObject( json.library, uuid, value, false ); + + return json; + + } + +} export { NodeMaterialLoader, NodeMaterialLoaderUtils }; diff --git a/examples/jsm/loaders/VTKLoader.js b/examples/jsm/loaders/VTKLoader.js index 6ebba6c9823020..d4f63666d992d9 100644 --- a/examples/jsm/loaders/VTKLoader.js +++ b/examples/jsm/loaders/VTKLoader.js @@ -8,21 +8,19 @@ import { } from '../../../build/three.module.js'; import * as fflate from '../libs/fflate.module.min.js'; -var VTKLoader = function ( manager ) { +class VTKLoader extends Loader { - Loader.call( this, manager ); + constructor( manager ) { -}; + super( manager ); -VTKLoader.prototype = Object.assign( Object.create( Loader.prototype ), { - - constructor: VTKLoader, + } - load: function ( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError ) { - var scope = this; + const scope = this; - var loader = new FileLoader( scope.manager ); + const loader = new FileLoader( scope.manager ); loader.setPath( scope.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( scope.requestHeader ); @@ -51,9 +49,9 @@ VTKLoader.prototype = Object.assign( Object.create( Loader.prototype ), { }, onProgress, onError ); - }, + } - parse: function ( data ) { + parse( data ) { function parseASCII( data ) { @@ -553,23 +551,23 @@ VTKLoader.prototype = Object.assign( Object.create( Loader.prototype ), { function Float32Concat( first, second ) { - var firstLength = first.length, result = new Float32Array( firstLength + second.length ); + const firstLength = first.length, result = new Float32Array( firstLength + second.length ); - result.set( first ); - result.set( second, firstLength ); + result.set( first ); + result.set( second, firstLength ); - return result; + return result; } function Int32Concat( first, second ) { - var firstLength = first.length, result = new Int32Array( firstLength + second.length ); + var firstLength = first.length, result = new Int32Array( firstLength + second.length ); - result.set( first ); - result.set( second, firstLength ); + result.set( first ); + result.set( second, firstLength ); - return result; + return result; } @@ -1179,6 +1177,6 @@ VTKLoader.prototype = Object.assign( Object.create( Loader.prototype ), { } -} ); +} export { VTKLoader };