diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..096746c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ \ No newline at end of file diff --git a/index.js b/index.js index f3e6ee4..3632c06 100644 --- a/index.js +++ b/index.js @@ -19,5024 +19,3765 @@ * Indexed Buffers * PreRotation support. */ - +var pako = require('pako'); module.exports = function (THREE) { + THREE.FBXLoader = function ( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + + }; + + Object.assign( THREE.FBXLoader.prototype, { + + load: function ( url, onLoad, onProgress, onError ) { + + var self = this; + + var resourceDirectory = THREE.LoaderUtils.extractUrlBase( url ); + + var loader = new THREE.FileLoader( this.manager ); + loader.setResponseType( 'arraybuffer' ); + loader.load( url, function ( buffer ) { + + try { + + var scene = self.parse( buffer, resourceDirectory ); + onLoad( scene ); + + } catch ( error ) { + + setTimeout( function () { + + if ( onError ) onError( error ); + + self.manager.itemError( url ); + + }, 0 ); + + } + + }, onProgress, onError ); + + }, + + parse: function ( FBXBuffer, resourceDirectory ) { + + var FBXTree; + + if ( isFbxFormatBinary( FBXBuffer ) ) { + + FBXTree = new BinaryParser().parse( FBXBuffer ); - /** - * Generates a loader for loading FBX files from URL and parsing into - * a THREE.Group. - * @param {THREE.LoadingManager} manager - Loading Manager for loader to use. - */ - THREE.FBXLoader = function (manager) { + } else { - this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager; + var FBXText = convertArrayBufferToString( FBXBuffer ); - }; + if ( ! isFbxFormatASCII( FBXText ) ) { - Object.assign(THREE.FBXLoader.prototype, { + throw new Error( 'THREE.FBXLoader: Unknown format.' ); - /** - * Loads an ASCII/Binary FBX file from URL and parses into a THREE.Group. - * THREE.Group will have an animations property of AnimationClips - * of the different animations exported with the FBX. - * @param {string} url - URL of the FBX file. - * @param {function(THREE.Group):void} onLoad - Callback for when FBX file is loaded and parsed. - * @param {function(ProgressEvent):void} onProgress - Callback fired periodically when file is being retrieved from server. - * @param {function(Event):void} onError - Callback fired when error occurs (Currently only with retrieving file, not with parsing errors). - */ - load: function (url, onLoad, onProgress, onError) { + } + + if ( getFbxVersion( FBXText ) < 7000 ) { + + throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); + + } + + FBXTree = new TextParser().parse( FBXText ); - var self = this; + } + + // console.log( FBXTree ); - var resourceDirectory = url.split(/[\\\/]/); - resourceDirectory.pop(); - resourceDirectory = resourceDirectory.join('/') + '/'; + var connections = parseConnections( FBXTree ); + var images = parseImages( FBXTree ); + var textures = parseTextures( FBXTree, new THREE.TextureLoader( this.manager ).setPath( resourceDirectory ), images, connections ); + var materials = parseMaterials( FBXTree, textures, connections ); + var skeletons = parseDeformers( FBXTree, connections ); + var geometryMap = parseGeometries( FBXTree, connections, skeletons ); + var sceneGraph = parseScene( FBXTree, connections, skeletons, geometryMap, materials ); - var loader = new THREE.FileLoader(this.manager); - loader.setResponseType('arraybuffer'); - loader.load(url, function (buffer) { + return sceneGraph; - try { + } - var scene = self.parse(url, buffer, resourceDirectory); + } ); - onLoad(scene); + // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) + // and details the connection type + function parseConnections( FBXTree ) { - } catch (error) { + var connectionMap = new Map(); - window.setTimeout(function () { + if ( 'Connections' in FBXTree ) { - if (onError) onError(error); + var rawConnections = FBXTree.Connections.properties.connections; - self.manager.itemError(url); + rawConnections.forEach( function ( rawConnection ) { - }, 0); + var fromID = rawConnection[ 0 ]; + var toID = rawConnection[ 1 ]; + var relationship = rawConnection[ 2 ]; - } + if ( ! connectionMap.has( fromID ) ) { - }, onProgress, onError); + connectionMap.set( fromID, { + parents: [], + children: [] + } ); - }, + } - /** - * Parses an ASCII/Binary FBX file and returns a THREE.Group. - * THREE.Group will have an animations property of AnimationClips - * of the different animations within the FBX file. - * @param {string} url - URL of the FBX file. - * @param {ArrayBuffer} FBXBuffer - Contents of FBX file to parse. - * @param {string} resourceDirectory - Directory to load external assets (e.g. textures ) from. - * @returns {THREE.Group} - */ - parse: function (url, FBXBuffer, resourceDirectory) { + var parentRelationship = { ID: toID, relationship: relationship }; + connectionMap.get( fromID ).parents.push( parentRelationship ); - var self = this; - var FBXTree; + if ( ! connectionMap.has( toID ) ) { - if (isFbxFormatBinary(FBXBuffer)) { + connectionMap.set( toID, { + parents: [], + children: [] + } ); - FBXTree = new BinaryParser().parse(FBXBuffer); + } - } else { + var childRelationship = { ID: fromID, relationship: relationship }; + connectionMap.get( toID ).children.push( childRelationship ); - var FBXText = convertArrayBufferToString(FBXBuffer); + } ); - if (!isFbxFormatASCII(FBXText)) { + } - self.manager.itemError(url); - throw new Error('FBXLoader: Unknown format.'); + return connectionMap; - } + } - if (getFbxVersion(FBXText) < 7000) { + // Parse FBXTree.Objects.subNodes.Video for embedded image data + // These images are connected to textures in FBXTree.Objects.subNodes.Textures + // via FBXTree.Connections. Note that images can be duplicated here, in which case only one + // may have a .Content field - we'll check for this and duplicate the data in the imageMap + function parseImages( FBXTree ) { - self.manager.itemError(url); - throw new Error('FBXLoader: FBX version not supported for file at ' + url + ', FileVersion: ' + getFbxVersion(FBXText)); + var imageMap = new Map(); - } + var names = {}; + var duplicates = []; - FBXTree = new TextParser().parse(FBXText); + if ( 'Video' in FBXTree.Objects.subNodes ) { - } + var videoNodes = FBXTree.Objects.subNodes.Video; - // console.log( FBXTree ); + for ( var nodeID in videoNodes ) { - var connections = parseConnections(FBXTree); - var images = parseImages(FBXTree); - var textures = parseTextures(FBXTree, new THREE.TextureLoader(this.manager).setPath(resourceDirectory), images, connections); - var materials = parseMaterials(FBXTree, textures, connections); - var deformers = parseDeformers(FBXTree, connections); - var geometryMap = parseGeometries(FBXTree, connections, deformers); - var sceneGraph = parseScene(FBXTree, connections, deformers, geometryMap, materials); + var videoNode = videoNodes[ nodeID ]; - return sceneGraph; + var id = parseInt( nodeID ); - } + // check whether the file name is used by another videoNode + // and if so keep a record of both ids as a duplicate pair [ id1, id2 ] + if ( videoNode.properties.fileName in names ) { - }); + duplicates.push( [ id, names[ videoNode.properties.fileName ] ] ); - /** - * Parses map of relationships between objects. - * @param {{Connections: { properties: { connections: [number, number, string][]}}}} FBXTree - * @returns {Map} - */ - function parseConnections(FBXTree) { + } - /** - * @type {Map} - */ - var connectionMap = new Map(); + names[ videoNode.properties.fileName ] = id; - if ('Connections' in FBXTree) { + // raw image data is in videoNode.properties.Content + if ( 'Content' in videoNode.properties && videoNode.properties.Content !== '' ) { - /** - * @type {[number, number, string][]} - */ - var connectionArray = FBXTree.Connections.properties.connections; - for (var connectionArrayIndex = 0, connectionArrayLength = connectionArray.length; connectionArrayIndex < connectionArrayLength; ++connectionArrayIndex) { + var image = parseImage( videoNodes[ nodeID ] ); - var connection = connectionArray[connectionArrayIndex]; + imageMap.set( id, image ); - if (!connectionMap.has(connection[0])) { + } + + } + + } + + + // check each duplicate pair - if only one is in the image map then + // create an entry for the other id containing the same image data + // Note: it seems to be possible for entries to have the same file name but different + // content, we won't overwrite these + duplicates.forEach( function ( duplicatePair ) { + + if ( imageMap.has( duplicatePair[ 0 ] ) && ! imageMap.has( duplicatePair[ 1 ] ) ) { + + var image = imageMap.get( duplicatePair[ 0 ] ); + imageMap.set( duplicatePair[ 1 ], image ); + + } else if ( imageMap.has( duplicatePair[ 1 ] ) && ! imageMap.has( duplicatePair[ 0 ] ) ) { + + var image = imageMap.get( duplicatePair[ 1 ] ); + imageMap.set( duplicatePair[ 0 ], image ); + + } - connectionMap.set(connection[0], { - parents: [], - children: [] - }); + } ); - } + return imageMap; - var parentRelationship = { ID: connection[1], relationship: connection[2] }; - connectionMap.get(connection[0]).parents.push(parentRelationship); + } - if (!connectionMap.has(connection[1])) { + // Parse embedded image data in FBXTree.Video.properties.Content + function parseImage( videoNode ) { - connectionMap.set(connection[1], { - parents: [], - children: [] - }); + var content = videoNode.properties.Content; + var fileName = videoNode.properties.RelativeFilename || videoNode.properties.Filename; + var extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); - } + var type; - var childRelationship = { ID: connection[0], relationship: connection[2] }; - connectionMap.get(connection[1]).children.push(childRelationship); + switch ( extension ) { - } + case 'bmp': - } + type = 'image/bmp'; + break; - return connectionMap; + case 'jpg': + case 'jpeg': - } + type = 'image/jpeg'; + break; - /** - * Parses map of images referenced in FBXTree. - * @param {{Objects: {subNodes: {Texture: Object.}}}} FBXTree - * @returns {Map} - */ - function parseImages(FBXTree) { + case 'png': - /** - * @type {Map} - */ - var imageMap = new Map(); + type = 'image/png'; + break; - if ('Video' in FBXTree.Objects.subNodes) { + case 'tif': - var videoNodes = FBXTree.Objects.subNodes.Video; + type = 'image/tiff'; + break; - for (var nodeID in videoNodes) { + default: - var videoNode = videoNodes[nodeID]; + console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); + return; - // raw image data is in videoNode.properties.Content - if ('Content' in videoNode.properties) { + } - var image = parseImage(videoNodes[nodeID]); - imageMap.set(parseInt(nodeID), image); + if ( typeof content === 'string' ) { // ASCII format - } + return 'data:' + type + ';base64,' + content; - } + } else { // Binary Format - } + var array = new Uint8Array( content ); + return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) ); - return imageMap; + } - } + } - /** - * @param {videoNode} videoNode - Node to get texture image information from. - * @returns {string} - image blob URL - */ - function parseImage(videoNode) { + // Parse nodes in FBXTree.Objects.subNodes.Texture + // These contain details such as UV scaling, cropping, rotation etc and are connected + // to images in FBXTree.Objects.subNodes.Video + function parseTextures( FBXTree, loader, imageMap, connections ) { - var buffer = videoNode.properties.Content; - var array = new Uint8Array(buffer); - var fileName = videoNode.properties.RelativeFilename || videoNode.properties.Filename; - var extension = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); + var textureMap = new Map(); - var type; + if ( 'Texture' in FBXTree.Objects.subNodes ) { - switch (extension) { + var textureNodes = FBXTree.Objects.subNodes.Texture; + for ( var nodeID in textureNodes ) { - case 'bmp': + var texture = parseTexture( textureNodes[ nodeID ], loader, imageMap, connections ); + textureMap.set( parseInt( nodeID ), texture ); - type = 'image/bmp'; - break; + } - case 'jpg': + } - type = 'image/jpeg'; - break; + return textureMap; - case 'png': + } - type = 'image/png'; - break; + // Parse individual node in FBXTree.Objects.subNodes.Texture + function parseTexture( textureNode, loader, imageMap, connections ) { - case 'tif': + var texture = loadTexture( textureNode, loader, imageMap, connections ); - type = 'image/tiff'; - break; + texture.ID = textureNode.id; - default: + texture.name = textureNode.attrName; - console.warn('FBXLoader: No support image type ' + extension); - return; + var wrapModeU = textureNode.properties.WrapModeU; + var wrapModeV = textureNode.properties.WrapModeV; - } + var valueU = wrapModeU !== undefined ? wrapModeU.value : 0; + var valueV = wrapModeV !== undefined ? wrapModeV.value : 0; - return window.URL.createObjectURL(new Blob([array], { type: type })); + // 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; - /** - * Parses map of textures referenced in FBXTree. - * @param {{Objects: {subNodes: {Texture: Object.}}}} FBXTree - * @param {THREE.TextureLoader} loader - * @param {Map} imageMap - * @param {Map} connections - * @returns {Map} - */ - function parseTextures(FBXTree, loader, imageMap, connections) { + if ( 'Scaling' in textureNode.properties ) { - /** - * @type {Map} - */ - var textureMap = new Map(); + var values = textureNode.properties.Scaling.value; - if ('Texture' in FBXTree.Objects.subNodes) { + texture.repeat.x = values[ 0 ]; + texture.repeat.y = values[ 1 ]; - var textureNodes = FBXTree.Objects.subNodes.Texture; - for (var nodeID in textureNodes) { + } - var texture = parseTexture(textureNodes[nodeID], loader, imageMap, connections); - textureMap.set(parseInt(nodeID), texture); + return texture; - } + } - } + // load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader + function loadTexture( textureNode, loader, imageMap, connections ) { - return textureMap; + var fileName; - } + var filePath = textureNode.properties.FileName; + var relativeFilePath = textureNode.properties.RelativeFilename; - /** - * @param {textureNode} textureNode - Node to get texture information from. - * @param {THREE.TextureLoader} loader - * @param {Map} imageMap - * @param {Map} connections - * @returns {THREE.Texture} - */ - function parseTexture(textureNode, loader, imageMap, connections) { + var children = connections.get( textureNode.id ).children; - var FBX_ID = textureNode.id; + if ( children !== undefined && children.length > 0 && imageMap.has( children[ 0 ].ID ) ) { - var name = textureNode.name; + fileName = imageMap.get( children[ 0 ].ID ); - var fileName; + } + // check that relative path is not an actually an absolute path and if so use it to load texture + else if ( relativeFilePath !== undefined && relativeFilePath[ 0 ] !== '/' && relativeFilePath.match( /^[a-zA-Z]:/ ) === null ) { - var filePath = textureNode.properties.FileName; - var relativeFilePath = textureNode.properties.RelativeFilename; + fileName = relativeFilePath; - var children = connections.get(FBX_ID).children; + } + // texture specified by absolute path + else { - if (children !== undefined && children.length > 0 && imageMap.has(children[0].ID)) { + var split = filePath.split( /[\\\/]/ ); - fileName = imageMap.get(children[0].ID); + if ( split.length > 0 ) { - } else if (relativeFilePath !== undefined && relativeFilePath[0] !== '/' && - relativeFilePath.match(/^[a-zA-Z]:/) === null) { + fileName = split[ split.length - 1 ]; - // use textureNode.properties.RelativeFilename - // if it exists and it doesn't seem an absolute path + } else { - fileName = relativeFilePath; + fileName = filePath; - } else { + } - var split = filePath.split(/[\\\/]/); + } - if (split.length > 0) { + var currentPath = loader.path; - fileName = split[split.length - 1]; + if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { - } else { + loader.setPath( undefined ); - fileName = filePath; + } - } + var texture = loader.load( fileName ); - } + loader.setPath( currentPath ); - var currentPath = loader.path; + return texture; - if (fileName.indexOf('blob:') === 0) { + } - loader.setPath(undefined); + // Parse nodes in FBXTree.Objects.subNodes.Material + function parseMaterials( FBXTree, textureMap, connections ) { - } + var materialMap = new Map(); - /** - * @type {THREE.Texture} - */ - var texture = loader.load(fileName); - texture.name = name; - texture.FBX_ID = FBX_ID; + if ( 'Material' in FBXTree.Objects.subNodes ) { - var wrapModeU = textureNode.properties.WrapModeU; - var wrapModeV = textureNode.properties.WrapModeV; + var materialNodes = FBXTree.Objects.subNodes.Material; + for ( var nodeID in materialNodes ) { - var valueU = wrapModeU !== undefined ? wrapModeU.value : 0; - var valueV = wrapModeV !== undefined ? wrapModeV.value : 0; + var material = parseMaterial( FBXTree, materialNodes[ nodeID ], textureMap, connections ); + if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); - // 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; + } - loader.setPath(currentPath); + return materialMap; - return texture; + } - } + // Parse single node in FBXTree.Objects.subNodes.Material + // Materials are connected to texture maps in FBXTree.Objects.subNodes.Textures + // FBX format currently only supports Lambert and Phong shading models + function parseMaterial( FBXTree, materialNode, textureMap, connections ) { - /** - * Parses map of Material information. - * @param {{Objects: {subNodes: {Material: Object.}}}} FBXTree - * @param {Map} textureMap - * @param {Map} connections - * @returns {Map} - */ - function parseMaterials(FBXTree, textureMap, connections) { + var ID = materialNode.id; + var name = materialNode.attrName; + var type = materialNode.properties.ShadingModel; - var materialMap = new Map(); + //Case where FBX wraps shading model in property object. + if ( typeof type === 'object' ) { - if ('Material' in FBXTree.Objects.subNodes) { + type = type.value; - var materialNodes = FBXTree.Objects.subNodes.Material; - for (var nodeID in materialNodes) { + } - var material = parseMaterial(materialNodes[nodeID], textureMap, connections); - materialMap.set(parseInt(nodeID), material); + // Ignore unused materials which don't have any connections. + if ( ! connections.has( ID ) ) return null; - } + var parameters = parseParameters( FBXTree, materialNode.properties, textureMap, ID, connections ); - } + var material; - return materialMap; + switch ( type.toLowerCase() ) { - } + 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 MeshPhongMaterial.', type ); + material = new THREE.MeshPhongMaterial( { color: 0x3300ff } ); + break; - /** - * Takes information from Material node and returns a generated THREE.Material - * @param {FBXMaterialNode} materialNode - * @param {Map} textureMap - * @param {Map} connections - * @returns {THREE.Material} - */ - function parseMaterial(materialNode, textureMap, connections) { + } - var FBX_ID = materialNode.id; - var name = materialNode.attrName; - var type = materialNode.properties.ShadingModel; + material.setValues( parameters ); + material.name = name; - //Case where FBXs wrap shading model in property object. - if (typeof type === 'object') { + return material; - type = type.value; + } - } + // 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 + function parseParameters( FBXTree, properties, textureMap, ID, connections ) { - var children = connections.get(FBX_ID).children; + var parameters = {}; - var parameters = parseParameters(materialNode.properties, textureMap, children); + if ( properties.BumpFactor ) { - var material; + parameters.bumpScale = properties.BumpFactor.value; - switch (type.toLowerCase()) { + } + if ( properties.Diffuse ) { - case 'phong': - material = new THREE.MeshPhongMaterial(); - break; - case 'lambert': - material = new THREE.MeshLambertMaterial(); - break; - default: - console.warn('No implementation given for material type ' + type + ' in FBXLoader.js. Defaulting to basic material'); - material = new THREE.MeshBasicMaterial({ color: 0x3300ff }); - break; + parameters.color = parseColor( properties.Diffuse ); - } + } + if ( properties.DisplacementFactor ) { - material.setValues(parameters); - material.name = name; + parameters.displacementScale = properties.DisplacementFactor.value; - return material; + } + if ( properties.ReflectionFactor ) { - } + parameters.reflectivity = properties.ReflectionFactor.value; - /** - * @typedef {{Diffuse: FBXVector3, Specular: FBXVector3, Shininess: FBXValue, Emissive: FBXVector3, EmissiveFactor: FBXValue, Opacity: FBXValue}} FBXMaterialProperties - */ - /** - * @typedef {{color: THREE.Color=, specular: THREE.Color=, shininess: number=, emissive: THREE.Color=, emissiveIntensity: number=, opacity: number=, transparent: boolean=, map: THREE.Texture=}} THREEMaterialParameterPack - */ - /** - * @param {FBXMaterialProperties} properties - * @param {Map} textureMap - * @param {{ID: number, relationship: string}[]} childrenRelationships - * @returns {THREEMaterialParameterPack} - */ - function parseParameters(properties, textureMap, childrenRelationships) { + } + if ( properties.Specular ) { - var parameters = {}; + parameters.specular = parseColor( properties.Specular ); - if (properties.Diffuse) { + } + if ( properties.Shininess ) { - parameters.color = parseColor(properties.Diffuse); + parameters.shininess = properties.Shininess.value; - } - if (properties.Specular) { + } + if ( properties.Emissive ) { - parameters.specular = parseColor(properties.Specular); + parameters.emissive = parseColor( properties.Emissive ); - } - if (properties.Shininess) { + } + if ( properties.EmissiveFactor ) { - parameters.shininess = properties.Shininess.value; + parameters.emissiveIntensity = parseFloat( properties.EmissiveFactor.value ); - } - if (properties.Emissive) { + } + if ( properties.Opacity ) { - parameters.emissive = parseColor(properties.Emissive); + parameters.opacity = parseFloat( properties.Opacity.value ); - } - if (properties.EmissiveFactor) { + } + if ( parameters.opacity < 1.0 ) { - parameters.emissiveIntensity = properties.EmissiveFactor.value; + parameters.transparent = true; - } - if (properties.Opacity) { + } - parameters.opacity = properties.Opacity.value; + connections.get( ID ).children.forEach( function ( child ) { - } - if (parameters.opacity < 1.0) { + var type = child.relationship; - parameters.transparent = true; + switch ( type ) { - } + case 'Bump': + parameters.bumpMap = textureMap.get( child.ID ); + break; - for (var childrenRelationshipsIndex = 0, childrenRelationshipsLength = childrenRelationships.length; childrenRelationshipsIndex < childrenRelationshipsLength; ++childrenRelationshipsIndex) { + case 'DiffuseColor': + parameters.map = getTexture( FBXTree, textureMap, child.ID, connections ); + break; - var relationship = childrenRelationships[childrenRelationshipsIndex]; + case 'DisplacementColor': + parameters.displacementMap = getTexture( FBXTree, textureMap, child.ID, connections ); + break; - var type = relationship.relationship; - switch (type) { + case 'EmissiveColor': + parameters.emissiveMap = getTexture( FBXTree, textureMap, child.ID, connections ); + break; - case "DiffuseColor": - case " \"DiffuseColor": - parameters.map = textureMap.get(relationship.ID); - break; + case 'NormalMap': + parameters.normalMap = getTexture( FBXTree, textureMap, child.ID, connections ); + break; - case "Bump": - case " \"Bump": - parameters.bumpMap = textureMap.get(relationship.ID); - break; + case 'ReflectionColor': + parameters.envMap = getTexture( FBXTree, textureMap, child.ID, connections ); + parameters.envMap.mapping = THREE.EquirectangularReflectionMapping; + break; - case "NormalMap": - case " \"NormalMap": - parameters.normalMap = textureMap.get(relationship.ID); - break; + case 'SpecularColor': + parameters.specularMap = getTexture( FBXTree, textureMap, child.ID, connections ); + break; - case " \"AmbientColor": - case " \"EmissiveColor": - case "AmbientColor": - case "EmissiveColor": - default: - console.warn('Unknown texture application of type ' + type + ', skipping texture'); - break; + case 'TransparentColor': + parameters.alphaMap = getTexture( FBXTree, textureMap, child.ID, connections ); + 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; - /** - * Generates map of Skeleton-like objects for use later when generating and binding skeletons. - * @param {{Objects: {subNodes: {Deformer: Object.}}}} FBXTree - * @param {Map} connections - * @returns {Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>} - */ - function parseDeformers(FBXTree, connections) { + } - var deformers = {}; + // get a texture from the textureMap for use by a material. + function getTexture( FBXTree, textureMap, id, connections ) { - if ('Deformer' in FBXTree.Objects.subNodes) { + // if the texture is a layered texture, just use the first layer and issue a warning + if ( 'LayeredTexture' in FBXTree.Objects.subNodes && id in FBXTree.Objects.subNodes.LayeredTexture ) { - var DeformerNodes = FBXTree.Objects.subNodes.Deformer; + console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); + id = connections.get( id ).children[ 0 ].ID; - for (var nodeID in DeformerNodes) { + } - var deformerNode = DeformerNodes[nodeID]; + return textureMap.get( id ); - if (deformerNode.attrType === 'Skin') { + } - var conns = connections.get(parseInt(nodeID)); - var skeleton = parseSkeleton(conns, DeformerNodes); - skeleton.FBX_ID = parseInt(nodeID); + // Parse nodes in FBXTree.Objects.subNodes.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. + function parseDeformers( FBXTree, connections ) { - deformers[nodeID] = skeleton; + var skeletons = {}; - } + if ( 'Deformer' in FBXTree.Objects.subNodes ) { - } + var DeformerNodes = FBXTree.Objects.subNodes.Deformer; - } + for ( var nodeID in DeformerNodes ) { - return deformers; + var deformerNode = DeformerNodes[ nodeID ]; - } + if ( deformerNode.attrType === 'Skin' ) { - /** - * Generates a "Skeleton Representation" of FBX nodes based on an FBX Skin Deformer's connections and an object containing SubDeformer nodes. - * @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} connections - * @param {Object.} DeformerNodes - * @returns {{map: Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}} - */ - function parseSkeleton(connections, DeformerNodes) { + var relationships = connections.get( parseInt( nodeID ) ); - var subDeformers = {}; - var children = connections.children; + var skeleton = parseSkeleton( relationships, DeformerNodes ); + skeleton.ID = nodeID; - for (var i = 0, l = children.length; i < l; ++i) { + 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; - var child = children[i]; + skeletons[ nodeID ] = skeleton; - var subDeformerNode = DeformerNodes[child.ID]; + } - var subDeformer = { - FBX_ID: child.ID, - index: i, - indices: [], - weights: [], - transform: parseMatrixArray(subDeformerNode.subNodes.Transform.properties.a), - transformLink: parseMatrixArray(subDeformerNode.subNodes.TransformLink.properties.a), - linkMode: subDeformerNode.properties.Mode - }; + } - if ('Indexes' in subDeformerNode.subNodes) { + } - subDeformer.indices = parseIntArray(subDeformerNode.subNodes.Indexes.properties.a); - subDeformer.weights = parseFloatArray(subDeformerNode.subNodes.Weights.properties.a); + return skeletons; - } + } - subDeformers[child.ID] = subDeformer; + // Parse single nodes in FBXTree.Objects.subNodes.Deformer + // The top level deformer nodes have type 'Skin' and subDeformer nodes have type 'Cluster' + // Each skin node represents a skeleton and each cluster node represents a bone + function parseSkeleton( connections, deformerNodes ) { - } + var rawBones = []; - return { - map: subDeformers, - bones: [] - }; + connections.children.forEach( function ( child ) { - } + var subDeformerNode = deformerNodes[ child.ID ]; - /** - * Generates Buffer geometries from geometry information in FBXTree, and generates map of THREE.BufferGeometries - * @param {{Objects: {subNodes: {Geometry: Object.} connections - * @param {Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>} deformers - * @returns {Map} - */ - function parseGeometries(FBXTree, connections, deformers) { + if ( subDeformerNode.attrType !== 'Cluster' ) return; - var geometryMap = new Map(); + var rawBone = { - if ('Geometry' in FBXTree.Objects.subNodes) { + ID: child.ID, + indices: [], + weights: [], - var geometryNodes = FBXTree.Objects.subNodes.Geometry; + // the global initial transform of the geometry node this bone is connected to + transform: new THREE.Matrix4().fromArray( subDeformerNode.subNodes.Transform.properties.a ), - for (var nodeID in geometryNodes) { + // the global initial transform of this bone + transformLink: new THREE.Matrix4().fromArray( subDeformerNode.subNodes.TransformLink.properties.a ), - var relationships = connections.get(parseInt(nodeID)); - var geo = parseGeometry(geometryNodes[nodeID], relationships, deformers); - geometryMap.set(parseInt(nodeID), geo); + }; - } + if ( 'Indexes' in subDeformerNode.subNodes ) { - } + rawBone.indices = subDeformerNode.subNodes.Indexes.properties.a; + rawBone.weights = subDeformerNode.subNodes.Weights.properties.a; - return geometryMap; + } - } + rawBones.push( rawBone ); - /** - * Generates BufferGeometry from FBXGeometryNode. - * @param {FBXGeometryNode} geometryNode - * @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships - * @param {Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}>} deformers - * @returns {THREE.BufferGeometry} - */ - function parseGeometry(geometryNode, relationships, deformers) { + } ); - switch (geometryNode.attrType) { + return { - case 'Mesh': - return parseMeshGeometry(geometryNode, relationships, deformers); - break; + rawBones: rawBones, + bones: [] - case 'NurbsCurve': - return parseNurbsGeometry(geometryNode); - break; + }; - } + } - } + // Parse nodes in FBXTree.Objects.subNodes.Geometry + function parseGeometries( FBXTree, connections, skeletons ) { - /** - * Specialty function for parsing Mesh based Geometry Nodes. - * @param {FBXGeometryNode} geometryNode - * @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships - Object representing relationships between specific geometry node and other nodes. - * @param {Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}>} deformers - Map object of deformers and subDeformers by ID. - * @returns {THREE.BufferGeometry} - */ - function parseMeshGeometry(geometryNode, relationships, deformers) { + var geometryMap = new Map(); - for (var i = 0; i < relationships.children.length; ++i) { + if ( 'Geometry' in FBXTree.Objects.subNodes ) { - var deformer = deformers[relationships.children[i].ID]; - if (deformer !== undefined) break; + var geometryNodes = FBXTree.Objects.subNodes.Geometry; - } + for ( var nodeID in geometryNodes ) { - return genGeometry(geometryNode, deformer); + var relationships = connections.get( parseInt( nodeID ) ); + var geo = parseGeometry( FBXTree, relationships, geometryNodes[ nodeID ], skeletons ); + geometryMap.set( parseInt( nodeID ), geo ); - } + } - /** - * @param {{map: Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[]}} deformer - Skeleton representation for geometry instance. - * @returns {THREE.BufferGeometry} - */ - function genGeometry(geometryNode, deformer) { + } - var geometry = new Geometry(); + return geometryMap; - var subNodes = geometryNode.subNodes; + } - // First, each index is going to be its own vertex. + // Parse single node in FBXTree.Objects.subNodes.Geometry + function parseGeometry( FBXTree, relationships, geometryNode, skeletons ) { - var vertexBuffer = parseFloatArray(subNodes.Vertices.properties.a); - var indexBuffer = parseIntArray(subNodes.PolygonVertexIndex.properties.a); + switch ( geometryNode.attrType ) { - if (subNodes.LayerElementNormal) { + case 'Mesh': + return parseMeshGeometry( FBXTree, relationships, geometryNode, skeletons ); + break; - var normalInfo = getNormals(subNodes.LayerElementNormal[0]); + case 'NurbsCurve': + return parseNurbsGeometry( geometryNode ); + break; - } + } - if (subNodes.LayerElementUV) { + } - var uvInfo = getUVs(subNodes.LayerElementUV[0]); + // Parse single node mesh geometry in FBXTree.Objects.subNodes.Geometry + function parseMeshGeometry( FBXTree, relationships, geometryNode, skeletons ) { - } + var modelNodes = relationships.parents.map( function ( parent ) { - if (subNodes.LayerElementColor) { + return FBXTree.Objects.subNodes.Model[ parent.ID ]; - var colorInfo = getColors(subNodes.LayerElementColor[0]); + } ); - } + // don't create geometry if it is not associated with any models + if ( modelNodes.length === 0 ) return; - if (subNodes.LayerElementMaterial) { + var skeleton = relationships.children.reduce( function ( skeleton, child ) { - var materialInfo = getMaterials(subNodes.LayerElementMaterial[0]); + if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; - } + return skeleton; - var faceVertexBuffer = []; - var polygonIndex = 0; + }, null ); - for (var polygonVertexIndex = 0; polygonVertexIndex < indexBuffer.length; polygonVertexIndex++) { + var preTransform = new THREE.Matrix4(); - var vertexIndex = indexBuffer[polygonVertexIndex]; + // TODO: if there is more than one model associated with the geometry, AND the models have + // different geometric transforms, then this will cause problems + // if ( modelNodes.length > 1 ) { } - var endOfFace = false; + // For now just assume one model and get the preRotations from that + var modelNode = modelNodes[ 0 ]; - if (vertexIndex < 0) { + if ( 'GeometricRotation' in modelNode.properties ) { - vertexIndex = vertexIndex ^ - 1; - indexBuffer[polygonVertexIndex] = vertexIndex; - endOfFace = true; + var array = modelNode.properties.GeometricRotation.value.map( THREE.Math.degToRad ); + array[ 3 ] = 'ZYX'; - } + preTransform.makeRotationFromEuler( new THREE.Euler().fromArray( array ) ); - var vertex = new Vertex(); - var weightIndices = []; - var weights = []; + } - vertex.position.fromArray(vertexBuffer, vertexIndex * 3); + if ( 'GeometricTranslation' in modelNode.properties ) { - if (deformer) { + preTransform.setPosition( new THREE.Vector3().fromArray( modelNode.properties.GeometricTranslation.value ) ); - var subDeformers = deformer.map; + } - for (var key in subDeformers) { + return genGeometry( FBXTree, relationships, geometryNode, skeleton, preTransform ); - var subDeformer = subDeformers[key]; - var indices = subDeformer.indices; + } - for (var j = 0; j < indices.length; j++) { + // Generate a THREE.BufferGeometry from a node in FBXTree.Objects.subNodes.Geometry + function genGeometry( FBXTree, relationships, geometryNode, skeleton, preTransform ) { - var index = indices[j]; + var subNodes = geometryNode.subNodes; - if (index === vertexIndex) { + var vertexPositions = subNodes.Vertices.properties.a; + var vertexIndices = subNodes.PolygonVertexIndex.properties.a; - weights.push(subDeformer.weights[j]); - weightIndices.push(subDeformer.index); + // create arrays to hold the final data used to build the buffergeometry + var vertexBuffer = []; + var normalBuffer = []; + var colorsBuffer = []; + var uvsBuffer = []; + var materialIndexBuffer = []; + var vertexWeightsBuffer = []; + var weightsIndicesBuffer = []; - break; + if ( subNodes.LayerElementColor ) { - } + var colorInfo = getColors( subNodes.LayerElementColor[ 0 ] ); - } + } - } + if ( subNodes.LayerElementMaterial ) { - if (weights.length > 4) { + var materialInfo = getMaterials( subNodes.LayerElementMaterial[ 0 ] ); - console.warn('FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.'); + } - var WIndex = [0, 0, 0, 0]; - var Weight = [0, 0, 0, 0]; + if ( subNodes.LayerElementNormal ) { - weights.forEach(function (weight, weightIndex) { + var normalInfo = getNormals( subNodes.LayerElementNormal[ 0 ] ); - var currentWeight = weight; - var currentIndex = weightIndices[weightIndex]; + } - Weight.forEach(function (comparedWeight, comparedWeightIndex, comparedWeightArray) { + if ( subNodes.LayerElementUV ) { - if (currentWeight > comparedWeight) { + var uvInfo = []; + var i = 0; + while ( subNodes.LayerElementUV[ i ] ) { - comparedWeightArray[comparedWeightIndex] = currentWeight; - currentWeight = comparedWeight; + uvInfo.push( getUVs( subNodes.LayerElementUV[ i ] ) ); + i ++; - var tmp = WIndex[comparedWeightIndex]; - WIndex[comparedWeightIndex] = currentIndex; - currentIndex = tmp; + } - } + } - }); + var weightTable = {}; - }); + if ( skeleton !== null ) { - weightIndices = WIndex; - weights = Weight; + skeleton.rawBones.forEach( function ( rawBone, i ) { - } + // loop over the bone's vertex indices and weights + rawBone.indices.forEach( function ( index, j ) { - for (var i = weights.length; i < 4; ++i) { + if ( weightTable[ index ] === undefined ) weightTable[ index ] = []; - weights[i] = 0; - weightIndices[i] = 0; + weightTable[ index ].push( { - } + id: i, + weight: rawBone.weights[ j ], - vertex.skinWeights.fromArray(weights); - vertex.skinIndices.fromArray(weightIndices); + } ); - } + } ); - if (normalInfo) { + } ); - vertex.normal.fromArray(getData(polygonVertexIndex, polygonIndex, vertexIndex, normalInfo)); + } - } + var polygonIndex = 0; + var faceLength = 0; + var displayedWeightsWarning = false; - if (uvInfo) { + // these will hold data for a single face + var vertexPositionIndexes = []; + var faceNormals = []; + var faceColors = []; + var faceUVs = []; + var faceWeights = []; + var faceWeightIndices = []; - vertex.uv.fromArray(getData(polygonVertexIndex, polygonIndex, vertexIndex, uvInfo)); + vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { - } + var endOfFace = false; - if (colorInfo) { + // 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 multiply by -1 and subtract 1: -3 * - 1 - 1 = 2 + if ( vertexIndex < 0 ) { - vertex.color.fromArray(getData(polygonVertexIndex, polygonIndex, vertexIndex, colorInfo)); + vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 + vertexIndices[ polygonVertexIndex ] = vertexIndex; + endOfFace = true; - } + } - faceVertexBuffer.push(vertex); + var weightIndices = []; + var weights = []; - if (endOfFace) { + vertexPositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); - var face = new Face(); - face.genTrianglesFromVertices(faceVertexBuffer); + if ( colorInfo ) { - if (materialInfo !== undefined) { + var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, colorInfo ); - var materials = getData(polygonVertexIndex, polygonIndex, vertexIndex, materialInfo); - face.materialIndex = materials[0]; + faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - } else { + } - // Seems like some models don't have materialInfo(subNodes.LayerElementMaterial). - // Set 0 in such a case. - face.materialIndex = 0; + if ( skeleton ) { - } + if ( weightTable[ vertexIndex ] !== undefined ) { - geometry.faces.push(face); - faceVertexBuffer = []; - polygonIndex++; + weightTable[ vertexIndex ].forEach( function ( wt ) { - endOfFace = false; + weights.push( wt.weight ); + weightIndices.push( wt.id ); - } + } ); - } - /** - * @type {{vertexBuffer: number[], normalBuffer: number[], uvBuffer: number[], skinIndexBuffer: number[], skinWeightBuffer: number[], materialIndexBuffer: number[]}} - */ - var bufferInfo = geometry.flattenToBuffers(); + } - var geo = new THREE.BufferGeometry(); - geo.name = geometryNode.name; - geo.addAttribute('position', new THREE.Float32BufferAttribute(bufferInfo.vertexBuffer, 3)); + if ( weights.length > 4 ) { - if (bufferInfo.normalBuffer.length > 0) { + if ( ! displayedWeightsWarning ) { - geo.addAttribute('normal', new THREE.Float32BufferAttribute(bufferInfo.normalBuffer, 3)); + console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); + displayedWeightsWarning = true; - } - if (bufferInfo.uvBuffer.length > 0) { + } - geo.addAttribute('uv', new THREE.Float32BufferAttribute(bufferInfo.uvBuffer, 2)); + var wIndex = [ 0, 0, 0, 0 ]; + var Weight = [ 0, 0, 0, 0 ]; - } - if (subNodes.LayerElementColor) { + weights.forEach( function ( weight, weightIndex ) { - geo.addAttribute('color', new THREE.Float32BufferAttribute(bufferInfo.colorBuffer, 3)); + var currentWeight = weight; + var currentIndex = weightIndices[ weightIndex ]; - } + Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { - if (deformer) { + if ( currentWeight > comparedWeight ) { - geo.addAttribute('skinIndex', new THREE.Float32BufferAttribute(bufferInfo.skinIndexBuffer, 4)); + comparedWeightArray[ comparedWeightIndex ] = currentWeight; + currentWeight = comparedWeight; - geo.addAttribute('skinWeight', new THREE.Float32BufferAttribute(bufferInfo.skinWeightBuffer, 4)); + var tmp = wIndex[ comparedWeightIndex ]; + wIndex[ comparedWeightIndex ] = currentIndex; + currentIndex = tmp; - geo.FBX_Deformer = deformer; + } - } + } ); - // Convert the material indices of each vertex into rendering groups on the geometry. + } ); - var materialIndexBuffer = bufferInfo.materialIndexBuffer; - var prevMaterialIndex = materialIndexBuffer[0]; - var startIndex = 0; + weightIndices = wIndex; + weights = Weight; - for (var i = 0; i < materialIndexBuffer.length; ++i) { + } - if (materialIndexBuffer[i] !== prevMaterialIndex) { + // if the weight array is shorter than 4 pad with 0s + while ( weights.length < 4 ) { - geo.addGroup(startIndex, i - startIndex, prevMaterialIndex); + weights.push( 0 ); + weightIndices.push( 0 ); - prevMaterialIndex = materialIndexBuffer[i]; - startIndex = i; + } - } + for ( var i = 0; i < 4; ++ i ) { - } + faceWeights.push( weights[ i ] ); + faceWeightIndices.push( weightIndices[ i ] ); - return geo; + } - } + } - /** - * Parses normal information for geometry. - * @param {FBXGeometryNode} geometryNode - * @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} - */ - function getNormals(NormalNode) { + if ( normalInfo ) { - var mappingType = NormalNode.properties.MappingInformationType; - var referenceType = NormalNode.properties.ReferenceInformationType; - var buffer = parseFloatArray(NormalNode.subNodes.Normals.properties.a); - var indexBuffer = []; - if (referenceType === 'IndexToDirect') { + var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, normalInfo ); - if ('NormalIndex' in NormalNode.subNodes) { + faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); - indexBuffer = parseIntArray(NormalNode.subNodes.NormalIndex.properties.a); + } - } else if ('NormalsIndex' in NormalNode.subNodes) { + if ( materialInfo && materialInfo.mappingType !== 'AllSame' ) { - indexBuffer = parseIntArray(NormalNode.subNodes.NormalsIndex.properties.a); + var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, materialInfo )[ 0 ]; - } + } - } + if ( uvInfo ) { - return { - dataSize: 3, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + uvInfo.forEach( function ( uv, i ) { - } + var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); - /** - * Parses UV information for geometry. - * @param {FBXGeometryNode} geometryNode - * @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} - */ - function getUVs(UVNode) { + if ( faceUVs[ i ] === undefined ) { - var mappingType = UVNode.properties.MappingInformationType; - var referenceType = UVNode.properties.ReferenceInformationType; - var buffer = parseFloatArray(UVNode.subNodes.UV.properties.a); - var indexBuffer = []; - if (referenceType === 'IndexToDirect') { + faceUVs[ i ] = []; - indexBuffer = parseIntArray(UVNode.subNodes.UVIndex.properties.a); + } - } + faceUVs[ i ].push( data[ 0 ] ); + faceUVs[ i ].push( data[ 1 ] ); - return { - dataSize: 2, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + } ); - } + } - /** - * Parses Vertex Color information for geometry. - * @param {FBXGeometryNode} geometryNode - * @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} - */ - function getColors(ColorNode) { + faceLength ++; - var mappingType = ColorNode.properties.MappingInformationType; - var referenceType = ColorNode.properties.ReferenceInformationType; - var buffer = parseFloatArray(ColorNode.subNodes.Colors.properties.a); - var indexBuffer = []; - if (referenceType === 'IndexToDirect') { + // we have reached the end of a face - it may have 4 sides though + // in which case the data is split to represent two 3 sided faces + if ( endOfFace ) { - indexBuffer = parseFloatArray(ColorNode.subNodes.ColorIndex.properties.a); + for ( var i = 2; i < faceLength; i ++ ) { - } + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ 0 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ 1 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ 2 ] ] ); - return { - dataSize: 4, - buffer: buffer, - indices: indexBuffer, - mappingType: mappingType, - referenceType: referenceType - }; + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ ( i - 1 ) * 3 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); - } + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ i * 3 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ i * 3 + 1 ] ] ); + vertexBuffer.push( vertexPositions[ vertexPositionIndexes[ i * 3 + 2 ] ] ); - /** - * Parses material application information for geometry. - * @param {FBXGeometryNode} - * @returns {{dataSize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} - */ - function getMaterials(MaterialNode) { + if ( skeleton ) { - var mappingType = MaterialNode.properties.MappingInformationType; - var referenceType = MaterialNode.properties.ReferenceInformationType; - - if (mappingType === 'NoMappingInformation') { - - return { - dataSize: 1, - buffer: [0], - indices: [0], - mappingType: 'AllSame', - referenceType: referenceType - }; + vertexWeightsBuffer.push( faceWeights[ 0 ] ); + vertexWeightsBuffer.push( faceWeights[ 1 ] ); + vertexWeightsBuffer.push( faceWeights[ 2 ] ); + vertexWeightsBuffer.push( faceWeights[ 3 ] ); - } + vertexWeightsBuffer.push( faceWeights[ ( i - 1 ) * 4 ] ); + vertexWeightsBuffer.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); + vertexWeightsBuffer.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); + vertexWeightsBuffer.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); - var materialIndexBuffer = parseIntArray(MaterialNode.subNodes.Materials.properties.a); + vertexWeightsBuffer.push( faceWeights[ i * 4 ] ); + vertexWeightsBuffer.push( faceWeights[ i * 4 + 1 ] ); + vertexWeightsBuffer.push( faceWeights[ i * 4 + 2 ] ); + vertexWeightsBuffer.push( faceWeights[ i * 4 + 3 ] ); - // 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 materialIndexBufferIndex = 0, materialIndexBufferLength = materialIndexBuffer.length; materialIndexBufferIndex < materialIndexBufferLength; ++materialIndexBufferIndex) { - - materialIndices.push(materialIndexBufferIndex); - - } - - return { - dataSize: 1, - buffer: materialIndexBuffer, - indices: materialIndices, - mappingType: mappingType, - referenceType: referenceType - }; + weightsIndicesBuffer.push( faceWeightIndices[ 0 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ 1 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ 2 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ 3 ] ); - } + weightsIndicesBuffer.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ + weightsIndicesBuffer.push( faceWeightIndices[ i * 4 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ i * 4 + 1 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ i * 4 + 2 ] ); + weightsIndicesBuffer.push( faceWeightIndices[ i * 4 + 3 ] ); - var dataArray = []; + } - var GetData = { + if ( colorInfo ) { - ByPolygonVertex: { + colorsBuffer.push( faceColors[ 0 ] ); + colorsBuffer.push( faceColors[ 1 ] ); + colorsBuffer.push( faceColors[ 2 ] ); - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ - Direct: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + colorsBuffer.push( faceColors[ ( i - 1 ) * 3 ] ); + colorsBuffer.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); + colorsBuffer.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); - var from = (polygonVertexIndex * infoObject.dataSize); - var to = (polygonVertexIndex * infoObject.dataSize) + infoObject.dataSize; + colorsBuffer.push( faceColors[ i * 3 ] ); + colorsBuffer.push( faceColors[ i * 3 + 1 ] ); + colorsBuffer.push( faceColors[ i * 3 + 2 ] ); - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + } - }, + if ( materialInfo && materialInfo.mappingType !== 'AllSame' ) { - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ - IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + materialIndexBuffer.push( materialIndex ); + materialIndexBuffer.push( materialIndex ); + materialIndexBuffer.push( materialIndex ); - var index = infoObject.indices[polygonVertexIndex]; - var from = (index * infoObject.dataSize); - var to = (index * infoObject.dataSize) + infoObject.dataSize; + } - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + if ( normalInfo ) { - } + normalBuffer.push( faceNormals[ 0 ] ); + normalBuffer.push( faceNormals[ 1 ] ); + normalBuffer.push( faceNormals[ 2 ] ); - }, + normalBuffer.push( faceNormals[ ( i - 1 ) * 3 ] ); + normalBuffer.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); + normalBuffer.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); - ByPolygon: { + normalBuffer.push( faceNormals[ i * 3 ] ); + normalBuffer.push( faceNormals[ i * 3 + 1 ] ); + normalBuffer.push( faceNormals[ i * 3 + 2 ] ); - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ - Direct: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + } - var from = polygonIndex * infoObject.dataSize; - var to = polygonIndex * infoObject.dataSize + infoObject.dataSize; + if ( uvInfo ) { - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + uvInfo.forEach( function ( uv, j ) { - }, + if ( uvsBuffer[ j ] === undefined ) uvsBuffer[ j ] = []; - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ - IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + uvsBuffer[ j ].push( faceUVs[ j ][ 0 ] ); + uvsBuffer[ j ].push( faceUVs[ j ][ 1 ] ); - var index = infoObject.indices[polygonIndex]; - var from = index * infoObject.dataSize; - var to = index * infoObject.dataSize + infoObject.dataSize; + uvsBuffer[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); + uvsBuffer[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + uvsBuffer[ j ].push( faceUVs[ j ][ i * 2 ] ); + uvsBuffer[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); - } + } ); - }, + } - ByVertice: { + } - Direct: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + polygonIndex ++; - var from = (vertexIndex * infoObject.dataSize); - var to = (vertexIndex * infoObject.dataSize) + infoObject.dataSize; + endOfFace = false; + faceLength = 0; - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + // reset arrays for the next face + vertexPositionIndexes = []; + faceNormals = []; + faceColors = []; + faceUVs = []; + faceWeights = []; + faceWeightIndices = []; - } + } - }, + } ); - AllSame: { + var geo = new THREE.BufferGeometry(); + geo.name = geometryNode.name; - /** - * Function uses the infoObject and given indices to return value array of object. - * @param {number} polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). - * @param {number} polygonIndex - Index of polygon in geometry. - * @param {number} vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). - * @param {{datasize: number, buffer: number[], indices: number[], mappingType: string, referenceType: string}} infoObject - Object containing data and how to access data. - * @returns {number[]} - */ - IndexToDirect: function (polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + var positionAttribute = new THREE.Float32BufferAttribute( vertexBuffer, 3 ); - var from = infoObject.indices[0] * infoObject.dataSize; - var to = infoObject.indices[0] * infoObject.dataSize + infoObject.dataSize; + preTransform.applyToBufferAttribute( positionAttribute ); - // return infoObject.buffer.slice( from, to ); - return slice(dataArray, infoObject.buffer, from, to); + geo.addAttribute( 'position', positionAttribute ); - } + if ( colorsBuffer.length > 0 ) { - } + geo.addAttribute( 'color', new THREE.Float32BufferAttribute( colorsBuffer, 3 ) ); - }; + } - function getData(polygonVertexIndex, polygonIndex, vertexIndex, infoObject) { + if ( skeleton ) { - return GetData[infoObject.mappingType][infoObject.referenceType](polygonVertexIndex, polygonIndex, vertexIndex, infoObject); + geo.addAttribute( 'skinIndex', new THREE.Float32BufferAttribute( weightsIndicesBuffer, 4 ) ); - } + geo.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( vertexWeightsBuffer, 4 ) ); - /** - * Specialty function for parsing NurbsCurve based Geometry Nodes. - * @param {FBXGeometryNode} geometryNode - * @param {{parents: {ID: number, relationship: string}[], children: {ID: number, relationship: string}[]}} relationships - * @returns {THREE.BufferGeometry} - */ - function parseNurbsGeometry(geometryNode) { + // used later to bind the skeleton to the model + geo.FBX_Deformer = skeleton; - if (THREE.NURBSCurve === undefined) { + } - console.error("THREE.FBXLoader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry."); - return new THREE.BufferGeometry(); + if ( normalBuffer.length > 0 ) { - } + geo.addAttribute( 'normal', new THREE.Float32BufferAttribute( normalBuffer, 3 ) ); - var order = parseInt(geometryNode.properties.Order); + } - if (isNaN(order)) { + uvsBuffer.forEach( function ( uvBuffer, i ) { - console.error("FBXLoader: Invalid Order " + geometryNode.properties.Order + " given for geometry ID: " + geometryNode.id); - return new THREE.BufferGeometry(); + // subsequent uv buffers are called 'uv1', 'uv2', ... + var name = 'uv' + ( i + 1 ).toString(); - } + // the first uv buffer is just called 'uv' + if ( i === 0 ) { - var degree = order - 1; + name = 'uv'; - var knots = parseFloatArray(geometryNode.subNodes.KnotVector.properties.a); - var controlPoints = []; - var pointsValues = parseFloatArray(geometryNode.subNodes.Points.properties.a); + } - for (var i = 0, l = pointsValues.length; i < l; i += 4) { + geo.addAttribute( name, new THREE.Float32BufferAttribute( uvsBuffer[ i ], 2 ) ); - controlPoints.push(new THREE.Vector4().fromArray(pointsValues, i)); + } ); - } + if ( materialInfo && materialInfo.mappingType !== 'AllSame' ) { - var startKnot, endKnot; + // Convert the material indices of each vertex into rendering groups on the geometry. + var prevMaterialIndex = materialIndexBuffer[ 0 ]; + var startIndex = 0; - if (geometryNode.properties.Form === 'Closed') { + materialIndexBuffer.forEach( function ( currentIndex, i ) { - controlPoints.push(controlPoints[0]); + if ( currentIndex !== prevMaterialIndex ) { - } else if (geometryNode.properties.Form === 'Periodic') { + geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); - startKnot = degree; - endKnot = knots.length - 1 - startKnot; + prevMaterialIndex = currentIndex; + startIndex = i; - for (var i = 0; i < degree; ++i) { + } - controlPoints.push(controlPoints[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; - var curve = new THREE.NURBSCurve(degree, knots, controlPoints, startKnot, endKnot); - var vertices = curve.getPoints(controlPoints.length * 7); + if ( lastIndex !== materialIndexBuffer.length ) { - var positions = new Float32Array(vertices.length * 3); + geo.addGroup( lastIndex, materialIndexBuffer.length - lastIndex, prevMaterialIndex ); - for (var i = 0, l = vertices.length; i < l; ++i) { + } - vertices[i].toArray(positions, i * 3); + } - } + // case where there are multiple materials but the whole geometry is only + // using one of them + if ( geo.groups.length === 0 ) { - var geometry = new THREE.BufferGeometry(); - geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); + geo.addGroup( 0, materialIndexBuffer.length, materialIndexBuffer[ 0 ] ); - return geometry; + } - } + } - /** - * Finally generates Scene graph and Scene graph Objects. - * @param {{Objects: {subNodes: {Model: Object.}}}} FBXTree - * @param {Map} connections - * @param {Map, array: {FBX_ID: number, indices: number[], weights: number[], transform: number[], transformLink: number[], linkMode: string}[], skeleton: THREE.Skeleton|null}>} deformers - * @param {Map} geometryMap - * @param {Map} materialMap - * @returns {THREE.Group} - */ - function parseScene(FBXTree, connections, deformers, geometryMap, materialMap) { + return geo; - var sceneGraph = new THREE.Group(); + } - var ModelNode = FBXTree.Objects.subNodes.Model; - /** - * @type {Array.} - */ - var modelArray = []; + // Parse normal from FBXTree.Objects.subNodes.Geometry.subNodes.LayerElementNormal if it exists + function getNormals( NormalNode ) { - /** - * @type {Map.} - */ - var modelMap = new Map(); + var mappingType = NormalNode.properties.MappingInformationType; + var referenceType = NormalNode.properties.ReferenceInformationType; + var buffer = NormalNode.subNodes.Normals.properties.a; + var indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - for (var nodeID in ModelNode) { + if ( 'NormalIndex' in NormalNode.subNodes ) { - var id = parseInt(nodeID); - var node = ModelNode[nodeID]; - var conns = connections.get(id); - var model = null; + indexBuffer = NormalNode.subNodes.NormalIndex.properties.a; - for (var i = 0; i < conns.parents.length; ++i) { + } else if ( 'NormalsIndex' in NormalNode.subNodes ) { - for (var FBX_ID in deformers) { + indexBuffer = NormalNode.subNodes.NormalsIndex.properties.a; - var deformer = deformers[FBX_ID]; - var subDeformers = deformer.map; - var subDeformer = subDeformers[conns.parents[i].ID]; + } - if (subDeformer) { + } - var model2 = model; - model = new THREE.Bone(); - deformer.bones[subDeformer.index] = model; + return { + dataSize: 3, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - // seems like we need this not to make non-connected bone, maybe? - // TODO: confirm - if (model2 !== null) model.add(model2); + } - } + // Parse UVs from FBXTree.Objects.subNodes.Geometry.subNodes.LayerElementUV if it exists + function getUVs( UVNode ) { - } + var mappingType = UVNode.properties.MappingInformationType; + var referenceType = UVNode.properties.ReferenceInformationType; + var buffer = UVNode.subNodes.UV.properties.a; + var indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - } + indexBuffer = UVNode.subNodes.UVIndex.properties.a; - if (!model) { + } - switch (node.attrType) { + return { + dataSize: 2, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - case "Mesh": - /** - * @type {?THREE.BufferGeometry} - */ - var geometry = null; + } - /** - * @type {THREE.MultiMaterial|THREE.Material} - */ - var material = null; + // Parse Vertex Colors from FBXTree.Objects.subNodes.Geometry.subNodes.LayerElementColor if it exists + function getColors( ColorNode ) { - /** - * @type {Array.} - */ - var materials = []; + var mappingType = ColorNode.properties.MappingInformationType; + var referenceType = ColorNode.properties.ReferenceInformationType; + var buffer = ColorNode.subNodes.Colors.properties.a; + var indexBuffer = []; + if ( referenceType === 'IndexToDirect' ) { - for (var childrenIndex = 0, childrenLength = conns.children.length; childrenIndex < childrenLength; ++childrenIndex) { + indexBuffer = ColorNode.subNodes.ColorIndex.properties.a; - var child = conns.children[childrenIndex]; + } - if (geometryMap.has(child.ID)) { + return { + dataSize: 4, + buffer: buffer, + indices: indexBuffer, + mappingType: mappingType, + referenceType: referenceType + }; - geometry = geometryMap.get(child.ID); + } - } + // Parse mapping and material data in FBXTree.Objects.subNodes.Geometry.subNodes.LayerElementMaterial if it exists + function getMaterials( MaterialNode ) { - if (materialMap.has(child.ID)) { + var mappingType = MaterialNode.properties.MappingInformationType; + var referenceType = MaterialNode.properties.ReferenceInformationType; - materials.push(materialMap.get(child.ID)); + if ( mappingType === 'NoMappingInformation' ) { - } + return { + dataSize: 1, + buffer: [ 0 ], + indices: [ 0 ], + mappingType: 'AllSame', + referenceType: referenceType + }; - } - if (materials.length > 1) { + } - material = materials; + var materialIndexBuffer = MaterialNode.subNodes.Materials.properties.a; - } else if (materials.length > 0) { + // 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 = []; - material = materials[0]; + for ( var i = 0; i < materialIndexBuffer.length; ++ i ) { - } else { + materialIndices.push( i ); - material = new THREE.MeshBasicMaterial({ color: 0x3300ff }); - materials.push(material); + } - } - if ('color' in geometry.attributes) { + return { + dataSize: 1, + buffer: materialIndexBuffer, + indices: materialIndices, + mappingType: mappingType, + referenceType: referenceType + }; - for (var materialIndex = 0, numMaterials = materials.length; materialIndex < numMaterials; ++materialIndex) { + } - materials[materialIndex].vertexColors = THREE.VertexColors; + // Functions use the infoObject and given indices to return value array of geometry. + // Parameters: + // - polygonVertexIndex - Index of vertex in draw order (which index of the index buffer refers to this vertex). + // - polygonIndex - Index of polygon in geometry. + // - vertexIndex - Index of vertex inside vertex buffer (used because some data refers to old index buffer that we don't use anymore). + // - infoObject: can be materialInfo, normalInfo, UVInfo or colorInfo + // Index type: + // - Direct: index is same as polygonVertexIndex + // - IndexToDirect: infoObject has it's own set of indices + var dataArray = []; - } + var GetData = { - } - if (geometry.FBX_Deformer) { + ByPolygonVertex: { - for (var materialsIndex = 0, materialsLength = materials.length; materialsIndex < materialsLength; ++materialsIndex) { + Direct: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - materials[materialsIndex].skinning = true; + var from = ( polygonVertexIndex * infoObject.dataSize ); + var to = ( polygonVertexIndex * infoObject.dataSize ) + infoObject.dataSize; - } - model = new THREE.SkinnedMesh(geometry, material); + return slice( dataArray, infoObject.buffer, from, to ); - } else { + }, - model = new THREE.Mesh(geometry, material); + IndexToDirect: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - } - break; + var index = infoObject.indices[ polygonVertexIndex ]; + var from = ( index * infoObject.dataSize ); + var to = ( index * infoObject.dataSize ) + infoObject.dataSize; - case "NurbsCurve": - var geometry = null; + return slice( dataArray, infoObject.buffer, from, to ); - for (var childrenIndex = 0, childrenLength = conns.children.length; childrenIndex < childrenLength; ++childrenIndex) { + } - var child = conns.children[childrenIndex]; + }, - if (geometryMap.has(child.ID)) { + ByPolygon: { - geometry = geometryMap.get(child.ID); + Direct: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - } + var from = polygonIndex * infoObject.dataSize; + var to = polygonIndex * infoObject.dataSize + infoObject.dataSize; - } + return slice( dataArray, infoObject.buffer, from, to ); - // FBX does not list materials for Nurbs lines, so we'll just put our own in here. - material = new THREE.LineBasicMaterial({ color: 0x3300ff, linewidth: 5 }); - model = new THREE.Line(geometry, material); - break; + }, - default: - model = new THREE.Object3D(); - break; + IndexToDirect: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - } + var index = infoObject.indices[ polygonIndex ]; + var from = index * infoObject.dataSize; + var to = index * infoObject.dataSize + infoObject.dataSize; - } + return slice( dataArray, infoObject.buffer, from, to ); - model.name = node.attrName.replace(/:/, '').replace(/_/, '').replace(/-/, ''); - model.FBX_ID = id; + } - modelArray.push(model); - modelMap.set(id, model); + }, - } + ByVertice: { - for (var modelArrayIndex = 0, modelArrayLength = modelArray.length; modelArrayIndex < modelArrayLength; ++modelArrayIndex) { + Direct: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - var model = modelArray[modelArrayIndex]; + var from = ( vertexIndex * infoObject.dataSize ); + var to = ( vertexIndex * infoObject.dataSize ) + infoObject.dataSize; - var node = ModelNode[model.FBX_ID]; + return slice( dataArray, infoObject.buffer, from, to ); - if ('Lcl_Translation' in node.properties) { + } - model.position.fromArray(parseFloatArray(node.properties.Lcl_Translation.value)); + }, - } + AllSame: { - if ('Lcl_Rotation' in node.properties) { + IndexToDirect: function ( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - var rotation = parseFloatArray(node.properties.Lcl_Rotation.value).map(degreeToRadian); - rotation.push('ZYX'); - model.rotation.fromArray(rotation); + var from = infoObject.indices[ 0 ] * infoObject.dataSize; + var to = infoObject.indices[ 0 ] * infoObject.dataSize + infoObject.dataSize; - } + return slice( dataArray, infoObject.buffer, from, to ); - if ('Lcl_Scaling' in node.properties) { + } - model.scale.fromArray(parseFloatArray(node.properties.Lcl_Scaling.value)); + } - } + }; - if ('PreRotation' in node.properties) { + function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { - var preRotations = new THREE.Euler().setFromVector3(parseVector3(node.properties.PreRotation).multiplyScalar(DEG2RAD), 'ZYX'); - preRotations = new THREE.Quaternion().setFromEuler(preRotations); - var currentRotation = new THREE.Quaternion().setFromEuler(model.rotation); - preRotations.multiply(currentRotation); - model.rotation.setFromQuaternion(preRotations, 'ZYX'); + return GetData[ infoObject.mappingType ][ infoObject.referenceType ]( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ); - } + } - var conns = connections.get(model.FBX_ID); - for (var parentIndex = 0; parentIndex < conns.parents.length; parentIndex++) { + // Generate a NurbGeometry from a node in FBXTree.Objects.subNodes.Geometry + function parseNurbsGeometry( geometryNode ) { - var pIndex = findIndex(modelArray, function (mod) { + if ( THREE.NURBSCurve === undefined ) { - return mod.FBX_ID === conns.parents[parentIndex].ID; + 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(); - }); - if (pIndex > - 1) { + } - modelArray[pIndex].add(model); - break; + var order = parseInt( geometryNode.properties.Order ); - } + if ( isNaN( order ) ) { - } - if (model.parent === null) { + console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geometryNode.properties.Order, geometryNode.id ); + return new THREE.BufferGeometry(); - sceneGraph.add(model); + } - } + var degree = order - 1; - } + var knots = geometryNode.subNodes.KnotVector.properties.a; + var controlPoints = []; + var pointsValues = geometryNode.subNodes.Points.properties.a; + for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) { - // Now with the bones created, we can update the skeletons and bind them to the skinned meshes. - sceneGraph.updateMatrixWorld(true); + controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) ); - // Put skeleton into bind pose. - var BindPoseNode = FBXTree.Objects.subNodes.Pose; - for (var nodeID in BindPoseNode) { + } - if (BindPoseNode[nodeID].attrType === 'BindPose') { + var startKnot, endKnot; - BindPoseNode = BindPoseNode[nodeID]; - break; + if ( geometryNode.properties.Form === 'Closed' ) { - } + controlPoints.push( controlPoints[ 0 ] ); - } - if (BindPoseNode) { + } else if ( geometryNode.properties.Form === 'Periodic' ) { - var PoseNode = BindPoseNode.subNodes.PoseNode; - var worldMatrices = new Map(); + startKnot = degree; + endKnot = knots.length - 1 - startKnot; - for (var PoseNodeIndex = 0, PoseNodeLength = PoseNode.length; PoseNodeIndex < PoseNodeLength; ++PoseNodeIndex) { + for ( var i = 0; i < degree; ++ i ) { - var node = PoseNode[PoseNodeIndex]; + controlPoints.push( controlPoints[ i ] ); - var rawMatWrd = parseMatrixArray(node.subNodes.Matrix.properties.a); + } - worldMatrices.set(parseInt(node.id), rawMatWrd); + } - } + var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); + var vertices = curve.getPoints( controlPoints.length * 7 ); - } + var positions = new Float32Array( vertices.length * 3 ); - for (var FBX_ID in deformers) { + vertices.forEach( function ( vertex, i ) { - var deformer = deformers[FBX_ID]; - var subDeformers = deformer.map; + vertex.toArray( positions, i * 3 ); - for (var key in subDeformers) { + } ); - var subDeformer = subDeformers[key]; - var subDeformerIndex = subDeformer.index; + var geometry = new THREE.BufferGeometry(); + geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); - /** - * @type {THREE.Bone} - */ - var bone = deformer.bones[subDeformerIndex]; - if (!worldMatrices.has(bone.FBX_ID)) { + return geometry; - break; + } - } - var mat = worldMatrices.get(bone.FBX_ID); - bone.matrixWorld.copy(mat); + // create the main THREE.Group() to be returned by the loader + function parseScene( FBXTree, connections, skeletons, geometryMap, materialMap ) { - } + var sceneGraph = new THREE.Group(); - // Now that skeleton is in bind pose, bind to model. - deformer.skeleton = new THREE.Skeleton(deformer.bones); + var modelMap = parseModels( FBXTree, skeletons, geometryMap, materialMap, connections ); - var conns = connections.get(deformer.FBX_ID); - var parents = conns.parents; + var modelNodes = FBXTree.Objects.subNodes.Model; - for (var parentsIndex = 0, parentsLength = parents.length; parentsIndex < parentsLength; ++parentsIndex) { + modelMap.forEach( function ( model ) { - var parent = parents[parentsIndex]; + var modelNode = modelNodes[ model.ID ]; + setLookAtProperties( FBXTree, model, modelNode, connections, sceneGraph ); - if (geometryMap.has(parent.ID)) { + var parentConnections = connections.get( model.ID ).parents; - var geoID = parent.ID; - var geoConns = connections.get(geoID); + parentConnections.forEach( function ( connection ) { - for (var i = 0; i < geoConns.parents.length; ++i) { + var parent = modelMap.get( connection.ID ); + if ( parent !== undefined ) parent.add( model ); - if (modelMap.has(geoConns.parents[i].ID)) { + } ); - var model = modelMap.get(geoConns.parents[i].ID); - //ASSERT model typeof SkinnedMesh - model.bind(deformer.skeleton, model.matrixWorld); - break; + if ( model.parent === null ) { - } + sceneGraph.add( model ); - } + } - } - } + } ); - } - //Skeleton is now bound, return objects to starting - //world positions. - sceneGraph.updateMatrixWorld(true); + bindSkeleton( FBXTree, skeletons, geometryMap, modelMap, connections, sceneGraph ); - // Silly hack with the animation parsing. We're gonna pretend the scene graph has a skeleton - // to attach animations to, since FBXs treat animations as animations for the entire scene, - // not just for individual objects. - sceneGraph.skeleton = { - bones: modelArray - }; + addAnimations( FBXTree, connections, sceneGraph, modelMap ); - var animations = parseAnimations(FBXTree, connections, sceneGraph); + createAmbientLight( FBXTree, sceneGraph ); - addAnimations(sceneGraph, animations); + return sceneGraph; - return sceneGraph; + } - } + // parse nodes in FBXTree.Objects.subNodes.Model + function parseModels( FBXTree, skeletons, geometryMap, materialMap, connections ) { - /** - * Parses animation information from FBXTree and generates an AnimationInfoObject. - * @param {{Objects: {subNodes: {AnimationCurveNode: any, AnimationCurve: any, AnimationLayer: any, AnimationStack: any}}}} FBXTree - * @param {Map} connections - */ - function parseAnimations(FBXTree, connections, sceneGraph) { + var modelMap = new Map(); + var modelNodes = FBXTree.Objects.subNodes.Model; - var rawNodes = FBXTree.Objects.subNodes.AnimationCurveNode; - var rawCurves = FBXTree.Objects.subNodes.AnimationCurve; - var rawLayers = FBXTree.Objects.subNodes.AnimationLayer; - var rawStacks = FBXTree.Objects.subNodes.AnimationStack; + for ( var nodeID in modelNodes ) { + + var id = parseInt( nodeID ); + var node = modelNodes[ nodeID ]; + var relationships = connections.get( id ); + + var model = buildSkeleton( relationships, skeletons, id, node.attrName ); + + if ( ! model ) { + + switch ( node.attrType ) { + + case 'Camera': + model = createCamera( FBXTree, relationships ); + break; + case 'Light': + model = createLight( FBXTree, relationships ); + break; + case 'Mesh': + model = createMesh( FBXTree, relationships, geometryMap, materialMap ); + break; + case 'NurbsCurve': + model = createCurve( relationships, geometryMap ); + break; + case 'LimbNode': // usually associated with a Bone, however if a Bone was not created we'll make a Group instead + case 'Null': + default: + model = new THREE.Group(); + break; - /** - * @type {{ - curves: Map, - layers: Map, - stacks: Map, - length: number, - fps: number, - frames: number - }} - */ - var returnObject = { - curves: new Map(), - layers: {}, - stacks: {}, - length: 0, - fps: 30, - frames: 0 - }; - - /** - * @type {Array.<{ - id: number; - attr: string; - internalID: number; - attrX: boolean; - attrY: boolean; - attrZ: boolean; - containerBoneID: number; - containerID: number; - }>} - */ - var animationCurveNodes = []; - for (var nodeID in rawNodes) { - - if (nodeID.match(/\d+/)) { - - var animationNode = parseAnimationNode(FBXTree, rawNodes[nodeID], connections, sceneGraph); - animationCurveNodes.push(animationNode); - - } - - } - - /** - * @type {Map.} - */ - var tmpMap = new Map(); - for (var animationCurveNodeIndex = 0; animationCurveNodeIndex < animationCurveNodes.length; ++animationCurveNodeIndex) { - if (animationCurveNodes[animationCurveNodeIndex] === null) { + } ); - continue; + } - } - tmpMap.set(animationCurveNodes[animationCurveNodeIndex].id, animationCurveNodes[animationCurveNodeIndex]); + } ); - } + return bone; + } - /** - * @type {{ - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }[]} - */ - var animationCurves = []; - for (nodeID in rawCurves) { + // create a THREE.PerspectiveCamera or THREE.OrthographicCamera + function createCamera( FBXTree, relationships ) { - if (nodeID.match(/\d+/)) { + var model; + var cameraAttribute; - var animationCurve = parseAnimationCurve(rawCurves[nodeID]); + relationships.children.forEach( function ( child ) { - // seems like this check would be necessary? - if (!connections.has(animationCurve.id)) continue; + var attr = FBXTree.Objects.subNodes.NodeAttribute[ child.ID ]; - animationCurves.push(animationCurve); + if ( attr !== undefined && attr.properties !== undefined ) { - var firstParentConn = connections.get(animationCurve.id).parents[0]; - var firstParentID = firstParentConn.ID; - var firstParentRelationship = firstParentConn.relationship; - var axis = ''; + cameraAttribute = attr.properties; - if (firstParentRelationship.match(/X/)) { + } - axis = 'x'; + } ); - } else if (firstParentRelationship.match(/Y/)) { + if ( cameraAttribute === undefined ) { - axis = 'y'; + model = new THREE.Object3D(); - } else if (firstParentRelationship.match(/Z/)) { - - axis = 'z'; - - } else { - - continue; - - } - - tmpMap.get(firstParentID).curves[axis] = animationCurve; - - } - - } - - tmpMap.forEach(function (curveNode) { - - var id = curveNode.containerBoneID; - if (!returnObject.curves.has(id)) { - - returnObject.curves.set(id, { T: null, R: null, S: null }); - - } - returnObject.curves.get(id)[curveNode.attr] = curveNode; - if (curveNode.attr === 'R') { - - var curves = curveNode.curves; - curves.x.values = curves.x.values.map(degreeToRadian); - curves.y.values = curves.y.values.map(degreeToRadian); - curves.z.values = curves.z.values.map(degreeToRadian); - - if (curveNode.preRotations !== null) { - - var preRotations = new THREE.Euler().setFromVector3(curveNode.preRotations, 'ZYX'); - preRotations = new THREE.Quaternion().setFromEuler(preRotations); - var frameRotation = new THREE.Euler(); - var frameRotationQuaternion = new THREE.Quaternion(); - for (var frame = 0; frame < curves.x.times.length; ++frame) { - - frameRotation.set(curves.x.values[frame], curves.y.values[frame], curves.z.values[frame], 'ZYX'); - frameRotationQuaternion.setFromEuler(frameRotation).premultiply(preRotations); - frameRotation.setFromQuaternion(frameRotationQuaternion, 'ZYX'); - curves.x.values[frame] = frameRotation.x; - curves.y.values[frame] = frameRotation.y; - curves.z.values[frame] = frameRotation.z; - - } - - } - - } - - }); - - for (var nodeID in rawLayers) { - - /** - * @type {{ - T: { - id: number; - attr: string; - internalID: number; - attrX: boolean; - attrY: boolean; - attrZ: boolean; - containerBoneID: number; - containerID: number; - curves: { - x: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - y: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - z: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - }, - }, - R: { - id: number; - attr: string; - internalID: number; - attrX: boolean; - attrY: boolean; - attrZ: boolean; - containerBoneID: number; - containerID: number; - curves: { - x: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - y: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - z: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - }, - }, - S: { - id: number; - attr: string; - internalID: number; - attrX: boolean; - attrY: boolean; - attrZ: boolean; - containerBoneID: number; - containerID: number; - curves: { - x: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - y: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - z: { - version: any; - id: number; - internalID: number; - times: number[]; - values: number[]; - attrFlag: number[]; - attrData: number[]; - }; - }, - } - }[]} - */ - var layer = []; - var children = connections.get(parseInt(nodeID)).children; + } else { + + var type = 0; + if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { + + type = 1; + + } - for (var childIndex = 0; childIndex < children.length; childIndex++) { + var nearClippingPlane = 1; + if ( cameraAttribute.NearPlane !== undefined ) { - // Skip lockInfluenceWeights - if (tmpMap.has(children[childIndex].ID)) { + nearClippingPlane = cameraAttribute.NearPlane.value / 1000; - var curveNode = tmpMap.get(children[childIndex].ID); - var boneID = curveNode.containerBoneID; - if (layer[boneID] === undefined) { + } + + var farClippingPlane = 1000; + if ( cameraAttribute.FarPlane !== undefined ) { + + farClippingPlane = cameraAttribute.FarPlane.value / 1000; + + } - layer[boneID] = { - T: null, - R: null, - S: null - }; - } + var width = window.innerWidth; + var height = window.innerHeight; - layer[boneID][curveNode.attr] = curveNode; + if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { - } + width = cameraAttribute.AspectWidth.value; + height = cameraAttribute.AspectHeight.value; - } + } - returnObject.layers[nodeID] = layer; + var aspect = width / height; - } + var fov = 45; + if ( cameraAttribute.FieldOfView !== undefined ) { - for (var nodeID in rawStacks) { + fov = cameraAttribute.FieldOfView.value; - var layers = []; - var children = connections.get(parseInt(nodeID)).children; - var timestamps = { max: 0, min: Number.MAX_VALUE }; + } - for (var childIndex = 0; childIndex < children.length; ++childIndex) { + switch ( type ) { - var currentLayer = returnObject.layers[children[childIndex].ID]; + case 0: // Perspective + model = new THREE.PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); + break; - if (currentLayer !== undefined) { + case 1: // Orthographic + model = new THREE.OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); + break; - layers.push(currentLayer); + default: + console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); + model = new THREE.Object3D(); + break; - for (var currentLayerIndex = 0, currentLayerLength = currentLayer.length; currentLayerIndex < currentLayerLength; ++currentLayerIndex) { + } - var layer = currentLayer[currentLayerIndex]; + } - if (layer) { + return model; - getCurveNodeMaxMinTimeStamps(layer, timestamps); + } - } + // Create a THREE.DirectionalLight, THREE.PointLight or THREE.SpotLight + function createLight( FBXTree, relationships ) { - } + var model; + var lightAttribute; - } + relationships.children.forEach( function ( child ) { - } + var attr = FBXTree.Objects.subNodes.NodeAttribute[ child.ID ]; - // Do we have an animation clip with actual length? - if (timestamps.max > timestamps.min) { + if ( attr !== undefined && attr.properties !== undefined ) { - returnObject.stacks[nodeID] = { - name: rawStacks[nodeID].attrName, - layers: layers, - length: timestamps.max - timestamps.min, - frames: (timestamps.max - timestamps.min) * 30 - }; + lightAttribute = attr.properties; - } + } - } + } ); - return returnObject; + if ( lightAttribute === undefined ) { - } + model = new THREE.Object3D(); - /** - * @param {Object} FBXTree - * @param {{id: number, attrName: string, properties: Object}} animationCurveNode - * @param {Map} connections - * @param {{skeleton: {bones: {FBX_ID: number}[]}}} sceneGraph - */ - function parseAnimationNode(FBXTree, animationCurveNode, connections, sceneGraph) { + } else { - var rawModels = FBXTree.Objects.subNodes.Model; + var type; - var returnObject = { - /** - * @type {number} - */ - id: animationCurveNode.id, + // LightType can be undefined for Point lights + if ( lightAttribute.LightType === undefined ) { - /** - * @type {string} - */ - attr: animationCurveNode.attrName, + type = 0; - /** - * @type {number} - */ - internalID: animationCurveNode.id, + } else { - /** - * @type {boolean} - */ - attrX: false, + type = lightAttribute.LightType.value; - /** - * @type {boolean} - */ - attrY: false, + } - /** - * @type {boolean} - */ - attrZ: false, + var color = 0xffffff; - /** - * @type {number} - */ - containerBoneID: - 1, + if ( lightAttribute.Color !== undefined ) { - /** - * @type {number} - */ - containerID: - 1, + color = parseColor( lightAttribute.Color ); - curves: { - x: null, - y: null, - z: null - }, + } - /** - * @type {number[]} - */ - preRotations: null - }; + var intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100; - if (returnObject.attr.match(/S|R|T/)) { + // light disabled + if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { - for (var attributeKey in animationCurveNode.properties) { + intensity = 0; - if (attributeKey.match(/X/)) { + } - returnObject.attrX = true; + var distance = 0; + if ( lightAttribute.FarAttenuationEnd !== undefined ) { - } - if (attributeKey.match(/Y/)) { + if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { - returnObject.attrY = true; + distance = 0; + + } else { + + distance = lightAttribute.FarAttenuationEnd.value / 1000; - } - if (attributeKey.match(/Z/)) { - - returnObject.attrZ = true; - - } - - } - - } else { - - return null; - - } - - var conns = connections.get(returnObject.id); - var containerIndices = conns.parents; - - for (var containerIndicesIndex = containerIndices.length - 1; containerIndicesIndex >= 0; --containerIndicesIndex) { - - var boneID = findIndex(sceneGraph.skeleton.bones, function (bone) { - - return bone.FBX_ID === containerIndices[containerIndicesIndex].ID; - - }); - if (boneID > - 1) { - - returnObject.containerBoneID = boneID; - returnObject.containerID = containerIndices[containerIndicesIndex].ID; - var model = rawModels[returnObject.containerID.toString()]; - if ('PreRotation' in model.properties) { - - returnObject.preRotations = parseVector3(model.properties.PreRotation).multiplyScalar(Math.PI / 180); - - } - break; - - } - - } - - return returnObject; - - } - - /** - * @param {{id: number, subNodes: {KeyTime: {properties: {a: string}}, KeyValueFloat: {properties: {a: string}}, KeyAttrFlags: {properties: {a: string}}, KeyAttrDataFloat: {properties: {a: string}}}}} animationCurve - */ - function parseAnimationCurve(animationCurve) { - - return { - version: null, - id: animationCurve.id, - internalID: animationCurve.id, - times: parseFloatArray(animationCurve.subNodes.KeyTime.properties.a).map(convertFBXTimeToSeconds), - values: parseFloatArray(animationCurve.subNodes.KeyValueFloat.properties.a), - - attrFlag: parseIntArray(animationCurve.subNodes.KeyAttrFlags.properties.a), - attrData: parseFloatArray(animationCurve.subNodes.KeyAttrDataFloat.properties.a) - }; - - } - - /** - * Sets the maxTimeStamp and minTimeStamp variables if it has timeStamps that are either larger or smaller - * than the max or min respectively. - * @param {{ - T: { - id: number, - attr: string, - internalID: number, - attrX: boolean, - attrY: boolean, - attrZ: boolean, - containerBoneID: number, - containerID: number, - curves: { - x: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - y: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - z: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - }, - }, - R: { - id: number, - attr: string, - internalID: number, - attrX: boolean, - attrY: boolean, - attrZ: boolean, - containerBoneID: number, - containerID: number, - curves: { - x: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - y: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - z: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - }, - }, - S: { - id: number, - attr: string, - internalID: number, - attrX: boolean, - attrY: boolean, - attrZ: boolean, - containerBoneID: number, - containerID: number, - curves: { - x: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - y: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - z: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - }, - }, - }} layer - */ - function getCurveNodeMaxMinTimeStamps(layer, timestamps) { - - if (layer.R) { - - getCurveMaxMinTimeStamp(layer.R.curves, timestamps); - - } - if (layer.S) { - - getCurveMaxMinTimeStamp(layer.S.curves, timestamps); - - } - if (layer.T) { - - getCurveMaxMinTimeStamp(layer.T.curves, timestamps); - - } - - } - - /** - * Sets the maxTimeStamp and minTimeStamp if one of the curve's time stamps - * exceeds the maximum or minimum. - * @param {{ - x: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - y: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], - }, - z: { - version: any, - id: number, - internalID: number, - times: number[], - values: number[], - attrFlag: number[], - attrData: number[], } - }} curve - */ - function getCurveMaxMinTimeStamp(curve, timestamps) { - - if (curve.x) { - - getCurveAxisMaxMinTimeStamps(curve.x, timestamps); - - } - if (curve.y) { - - getCurveAxisMaxMinTimeStamps(curve.y, timestamps); - - } - if (curve.z) { - - getCurveAxisMaxMinTimeStamps(curve.z, timestamps); - - } - - } - - /** - * Sets the maxTimeStamp and minTimeStamp if one of its timestamps exceeds the maximum or minimum. - * @param {{times: number[]}} axis - */ - function getCurveAxisMaxMinTimeStamps(axis, timestamps) { - - timestamps.max = axis.times[axis.times.length - 1] > timestamps.max ? axis.times[axis.times.length - 1] : timestamps.max; - timestamps.min = axis.times[0] < timestamps.min ? axis.times[0] : timestamps.min; - - } - - /** - * @param {{ - curves: Map; - layers: Map; - stacks: Map; - length: number; - fps: number; - frames: number; - }} animations, - * @param {{skeleton: { bones: THREE.Bone[]}}} group - */ - function addAnimations(group, animations) { - if (group.animations === undefined) { + } + + // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? + var decay = 1; + + 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 + var angle = Math.PI / 3; + + if ( lightAttribute.InnerAngle !== undefined ) { + + angle = THREE.Math.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 = THREE.Math.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; + + } + + if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { - group.animations = []; + model.castShadow = true; - } + } + + } + + return model; + + } + + function createMesh( FBXTree, relationships, geometryMap, materialMap ) { + + var model; + var geometry = null; + var material = null; + var materials = []; - var stacks = animations.stacks; + // get geometry and materials(s) from connections + relationships.children.forEach( function ( child ) { - for (var key in stacks) { + if ( geometryMap.has( child.ID ) ) { - var stack = stacks[key]; + geometry = geometryMap.get( child.ID ); - /** - * @type {{ - * name: string, - * fps: number, - * length: number, - * hierarchy: Array.<{ - * parent: number, - * name: string, - * keys: Array.<{ - * time: number, - * pos: Array., - * rot: Array., - * scl: Array. - * }> - * }> - * }} - */ - var animationData = { - name: stack.name, - fps: 30, - length: stack.length, - hierarchy: [] - }; + } - var bones = group.skeleton.bones; + if ( materialMap.has( child.ID ) ) { - for (var bonesIndex = 0, bonesLength = bones.length; bonesIndex < bonesLength; ++bonesIndex) { + materials.push( materialMap.get( child.ID ) ); - var bone = bones[bonesIndex]; + } - var name = bone.name.replace(/.*:/, ''); - var parentIndex = findIndex(bones, function (parentBone) { + } ); - return bone.parent === parentBone; + if ( materials.length > 1 ) { - }); - animationData.hierarchy.push({ parent: parentIndex, name: name, keys: [] }); + material = materials; - } + } else if ( materials.length > 0 ) { - for (var frame = 0; frame <= stack.frames; frame++) { + material = materials[ 0 ]; - for (var bonesIndex = 0, bonesLength = bones.length; bonesIndex < bonesLength; ++bonesIndex) { + } else { - var bone = bones[bonesIndex]; - var boneIndex = bonesIndex; + material = new THREE.MeshPhongMaterial( { color: 0xcccccc } ); + materials.push( material ); - var animationNode = stack.layers[0][boneIndex]; + } - for (var hierarchyIndex = 0, hierarchyLength = animationData.hierarchy.length; hierarchyIndex < hierarchyLength; ++hierarchyIndex) { + if ( 'color' in geometry.attributes ) { - var node = animationData.hierarchy[hierarchyIndex]; + materials.forEach( function ( material ) { - if (node.name === bone.name) { + material.vertexColors = THREE.VertexColors; - node.keys.push(generateKey(animations, animationNode, bone, frame)); + } ); - } + } - } + if ( geometry.FBX_Deformer ) { - } + materials.forEach( function ( material ) { - } + material.skinning = true; - group.animations.push(THREE.AnimationClip.parseAnimation(animationData, bones)); + } ); - } + model = new THREE.SkinnedMesh( geometry, material ); - } + } else { - var euler = new THREE.Euler(); - var quaternion = new THREE.Quaternion(); + model = new THREE.Mesh( geometry, material ); - /** - * @param {THREE.Bone} bone - */ - function generateKey(animations, animationNode, bone, frame) { + } - var key = { - time: frame / animations.fps, - pos: bone.position.toArray(), - rot: bone.quaternion.toArray(), - scl: bone.scale.toArray() - }; + return model; - if (animationNode === undefined) return key; + } - try { + function createCurve( relationships, geometryMap ) { - if (hasCurve(animationNode, 'T') && hasKeyOnFrame(animationNode.T, frame)) { + var geometry = relationships.children.reduce( function ( geo, child ) { - key.pos = [animationNode.T.curves.x.values[frame], animationNode.T.curves.y.values[frame], animationNode.T.curves.z.values[frame]]; + if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); - } + return geo; - if (hasCurve(animationNode, 'R') && hasKeyOnFrame(animationNode.R, frame)) { + }, null ); - var rotationX = animationNode.R.curves.x.values[frame]; - var rotationY = animationNode.R.curves.y.values[frame]; - var rotationZ = animationNode.R.curves.z.values[frame]; + // FBX does not list materials for Nurbs lines, so we'll just put our own in here. + var material = new THREE.LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } ); + return new THREE.Line( geometry, material ); - quaternion.setFromEuler(euler.set(rotationX, rotationY, rotationZ, 'ZYX')); - key.rot = quaternion.toArray(); + } - } + // Parse ambient color in FBXTree.GlobalSettings.properties - if it's not set to black (default), create an ambient light + function createAmbientLight( FBXTree, sceneGraph ) { - if (hasCurve(animationNode, 'S') && hasKeyOnFrame(animationNode.S, frame)) { + if ( 'GlobalSettings' in FBXTree && 'AmbientColor' in FBXTree.GlobalSettings.properties ) { - key.scl = [animationNode.S.curves.x.values[frame], animationNode.S.curves.y.values[frame], animationNode.S.curves.z.values[frame]]; + var ambientColor = FBXTree.GlobalSettings.properties.AmbientColor.value; + var r = ambientColor[ 0 ]; + var g = ambientColor[ 1 ]; + var b = ambientColor[ 2 ]; - } + if ( r !== 0 || g !== 0 || b !== 0 ) { - } catch (error) { + var color = new THREE.Color( r, g, b ); + sceneGraph.add( new THREE.AmbientLight( color, 1 ) ); - // Curve is not fully plotted. - console.log(bone); - console.log(error); + } - } + } - return key; + } - } + function setLookAtProperties( FBXTree, model, modelNode, connections, sceneGraph ) { - var AXES = ['x', 'y', 'z']; + if ( 'LookAtProperty' in modelNode.properties ) { - function hasCurve(animationNode, attribute) { + var children = connections.get( model.ID ).children; - if (animationNode === undefined) { + children.forEach( function ( child ) { - return false; + if ( child.relationship === 'LookAtProperty' ) { - } + var lookAtTarget = FBXTree.Objects.subNodes.Model[ child.ID ]; - var attributeNode = animationNode[attribute]; + if ( 'Lcl_Translation' in lookAtTarget.properties ) { - if (!attributeNode) { + var pos = lookAtTarget.properties.Lcl_Translation.value; - return false; + // DirectionalLight, SpotLight + if ( model.target !== undefined ) { - } + model.target.position.fromArray( pos ); + sceneGraph.add( model.target ); - return AXES.every(function (key) { + } else { // Cameras and other Object3Ds - return attributeNode.curves[key] !== null; + model.lookAt( new THREE.Vector3().fromArray( pos ) ); - }); + } - } + } - function hasKeyOnFrame(attributeNode, frame) { + } - return AXES.every(function (key) { + } ); - return isKeyExistOnFrame(attributeNode.curves[key], frame); + } - }); + } - } + // parse the model node for transform details and apply them to the model + function setModelTransforms( FBXTree, model, modelNode ) { - function isKeyExistOnFrame(curve, frame) { + // http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html + if ( 'RotationOrder' in modelNode.properties ) { - return curve.values[frame] !== undefined; + var enums = [ + 'XYZ', // default + 'XZY', + 'YZX', + 'ZXY', + 'YXZ', + 'ZYX', + 'SphericXYZ', + ]; - } + var value = parseInt( modelNode.properties.RotationOrder.value, 10 ); - /** - * An instance of a Vertex with data for drawing vertices to the screen. - * @constructor - */ - function Vertex() { + if ( value > 0 && value < 6 ) { - /** - * Position of the vertex. - * @type {THREE.Vector3} - */ - this.position = new THREE.Vector3(); + // model.rotation.order = enums[ value ]; - /** - * Normal of the vertex - * @type {THREE.Vector3} - */ - this.normal = new THREE.Vector3(); + // Note: Euler order other than XYZ is currently not supported, so just display a warning for now + console.warn( 'THREE.FBXLoader: unsupported Euler Order: %s. Currently only XYZ order is supported. Animations and rotations may be incorrect.', enums[ value ] ); - /** - * UV coordinates of the vertex. - * @type {THREE.Vector2} - */ - this.uv = new THREE.Vector2(); + } else if ( value === 6 ) { - /** - * Color of the vertex - * @type {THREE.Vector3} - */ - this.color = new THREE.Vector3(); + console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); - /** - * Indices of the bones vertex is influenced by. - * @type {THREE.Vector4} - */ - this.skinIndices = new THREE.Vector4(0, 0, 0, 0); + } - /** - * Weights that each bone influences the vertex. - * @type {THREE.Vector4} - */ - this.skinWeights = new THREE.Vector4(0, 0, 0, 0); + } - } + if ( 'Lcl_Translation' in modelNode.properties ) { - Object.assign(Vertex.prototype, { + model.position.fromArray( modelNode.properties.Lcl_Translation.value ); - copy: function (target) { + } - var returnVar = target || new Vertex(); + if ( 'Lcl_Rotation' in modelNode.properties ) { - returnVar.position.copy(this.position); - returnVar.normal.copy(this.normal); - returnVar.uv.copy(this.uv); - returnVar.skinIndices.copy(this.skinIndices); - returnVar.skinWeights.copy(this.skinWeights); + var rotation = modelNode.properties.Lcl_Rotation.value.map( THREE.Math.degToRad ); + rotation.push( 'ZYX' ); + model.rotation.fromArray( rotation ); - return returnVar; + } - }, + if ( 'Lcl_Scaling' in modelNode.properties ) { - flattenToBuffers: function (vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer) { + model.scale.fromArray( modelNode.properties.Lcl_Scaling.value ); - this.position.toArray(vertexBuffer, vertexBuffer.length); - this.normal.toArray(normalBuffer, normalBuffer.length); - this.uv.toArray(uvBuffer, uvBuffer.length); - this.color.toArray(colorBuffer, colorBuffer.length); - this.skinIndices.toArray(skinIndexBuffer, skinIndexBuffer.length); - this.skinWeights.toArray(skinWeightBuffer, skinWeightBuffer.length); + } - } + if ( 'PreRotation' in modelNode.properties ) { - }); + var array = modelNode.properties.PreRotation.value.map( THREE.Math.degToRad ); + array[ 3 ] = 'ZYX'; - /** - * @constructor - */ - function Triangle() { + var preRotations = new THREE.Euler().fromArray( array ); - /** - * @type {{position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]} - */ - this.vertices = []; + preRotations = new THREE.Quaternion().setFromEuler( preRotations ); + var currentRotation = new THREE.Quaternion().setFromEuler( model.rotation ); + preRotations.multiply( currentRotation ); + model.rotation.setFromQuaternion( preRotations, 'ZYX' ); - } + } - Object.assign(Triangle.prototype, { + } - copy: function (target) { + function bindSkeleton( FBXTree, skeletons, geometryMap, modelMap, connections, sceneGraph ) { - var returnVar = target || new Triangle(); + // Now with the bones created, we can update the skeletons and bind them to the skinned meshes. + sceneGraph.updateMatrixWorld( true ); - for (var i = 0; i < this.vertices.length; ++i) { + var worldMatrices = new Map(); - this.vertices[i].copy(returnVar.vertices[i]); + // Put skeleton into bind pose. + if ( 'Pose' in FBXTree.Objects.subNodes ) { - } + var BindPoseNode = FBXTree.Objects.subNodes.Pose; - return returnVar; + for ( var nodeID in BindPoseNode ) { - }, + if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { - flattenToBuffers: function (vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer) { + var poseNodes = BindPoseNode[ nodeID ].subNodes.PoseNode; - var vertices = this.vertices; + if ( Array.isArray( poseNodes ) ) { + + poseNodes.forEach( function ( node ) { + + var rawMatWrd = new THREE.Matrix4().fromArray( node.subNodes.Matrix.properties.a ); + worldMatrices.set( parseInt( node.properties.Node ), rawMatWrd ); + + } ); + + } else { + + var rawMatWrd = new THREE.Matrix4().fromArray( poseNodes.subNodes.Matrix.properties.a ); + worldMatrices.set( parseInt( poseNodes.properties.Node ), rawMatWrd ); + + } + + } + + } - for (var i = 0, l = vertices.length; i < l; ++i) { + } - vertices[i].flattenToBuffers(vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer); + for ( var ID in skeletons ) { - } + var skeleton = skeletons[ ID ]; - } + skeleton.bones.forEach( function ( bone, i ) { - }); + // if the bone's initial transform is set in a poseNode, copy that + if ( worldMatrices.has( bone.ID ) ) { - /** - * @constructor - */ - function Face() { + var mat = worldMatrices.get( bone.ID ); + bone.matrixWorld.copy( mat ); - /** - * @type {{vertices: {position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]}[]} - */ - this.triangles = []; - this.materialIndex = 0; + } + // otherwise use the transform from the rawBone + else { - } + bone.matrixWorld.copy( skeleton.rawBones[ i ].transformLink ); - Object.assign(Face.prototype, { + } - copy: function (target) { + } ); - var returnVar = target || new Face(); + // Now that skeleton is in bind pose, bind to model. + var parents = connections.get( parseInt( skeleton.ID ) ).parents; - for (var i = 0; i < this.triangles.length; ++i) { + parents.forEach( function ( parent ) { - this.triangles[i].copy(returnVar.triangles[i]); + if ( geometryMap.has( parent.ID ) ) { - } + var geoID = parent.ID; + var geoRelationships = connections.get( geoID ); - returnVar.materialIndex = this.materialIndex; + geoRelationships.parents.forEach( function ( geoConnParent ) { - return returnVar; + if ( modelMap.has( geoConnParent.ID ) ) { - }, + var model = modelMap.get( geoConnParent.ID ); - genTrianglesFromVertices: function (vertexArray) { + model.bind( new THREE.Skeleton( skeleton.bones ), model.matrixWorld ); - for (var i = 2; i < vertexArray.length; ++i) { + } - var triangle = new Triangle(); - triangle.vertices[0] = vertexArray[0]; - triangle.vertices[1] = vertexArray[i - 1]; - triangle.vertices[2] = vertexArray[i]; - this.triangles.push(triangle); + } ); - } + } - }, + } ); - flattenToBuffers: function (vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer, materialIndexBuffer) { + } - var triangles = this.triangles; - var materialIndex = this.materialIndex; + //Skeleton is now bound, return objects to starting world positions. + sceneGraph.updateMatrixWorld( true ); - for (var i = 0, l = triangles.length; i < l; ++i) { + } - triangles[i].flattenToBuffers(vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer); - append(materialIndexBuffer, [materialIndex, materialIndex, materialIndex]); + function parseAnimations( FBXTree, connections ) { - } + // since the actual transformation data is stored in FBXTree.Objects.subNodes.AnimationCurve, + // if this is undefined we can safely assume there are no animations + if ( FBXTree.Objects.subNodes.AnimationCurve === undefined ) return undefined; - } + var curveNodesMap = parseAnimationCurveNodes( FBXTree ); - }); + parseAnimationCurves( FBXTree, connections, curveNodesMap ); - /** - * @constructor - */ - function Geometry() { + var layersMap = parseAnimationLayers( FBXTree, connections, curveNodesMap ); + var rawClips = parseAnimStacks( FBXTree, connections, layersMap ); - /** - * @type {{triangles: {vertices: {position: THREE.Vector3, normal: THREE.Vector3, uv: THREE.Vector2, skinIndices: THREE.Vector4, skinWeights: THREE.Vector4}[]}[], materialIndex: number}[]} - */ - this.faces = []; + return rawClips; - /** - * @type {{}|THREE.Skeleton} - */ - this.skeleton = null; + } - } + // parse nodes in FBXTree.Objects.subNodes.AnimationCurveNode + // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) + // and is referenced by an AnimationLayer + function parseAnimationCurveNodes( FBXTree ) { - Object.assign(Geometry.prototype, { + var rawCurveNodes = FBXTree.Objects.subNodes.AnimationCurveNode; - /** - * @returns {{vertexBuffer: number[], normalBuffer: number[], uvBuffer: number[], skinIndexBuffer: number[], skinWeightBuffer: number[], materialIndexBuffer: number[]}} - */ - flattenToBuffers: function () { + var curveNodesMap = new Map(); - var vertexBuffer = []; - var normalBuffer = []; - var uvBuffer = []; - var colorBuffer = []; - var skinIndexBuffer = []; - var skinWeightBuffer = []; + for ( var nodeID in rawCurveNodes ) { - var materialIndexBuffer = []; + var rawCurveNode = rawCurveNodes[ nodeID ]; - var faces = this.faces; + if ( rawCurveNode.attrName.match( /S|R|T/ ) !== null ) { - for (var i = 0, l = faces.length; i < l; ++i) { + var curveNode = { - faces[i].flattenToBuffers(vertexBuffer, normalBuffer, uvBuffer, colorBuffer, skinIndexBuffer, skinWeightBuffer, materialIndexBuffer); + id: rawCurveNode.id, + attr: rawCurveNode.attrName, + curves: {}, - } + }; - return { - vertexBuffer: vertexBuffer, - normalBuffer: normalBuffer, - uvBuffer: uvBuffer, - colorBuffer: colorBuffer, - skinIndexBuffer: skinIndexBuffer, - skinWeightBuffer: skinWeightBuffer, - materialIndexBuffer: materialIndexBuffer - }; + } - } + curveNodesMap.set( curveNode.id, curveNode ); - }); + } - function TextParser() { } + return curveNodesMap; - Object.assign(TextParser.prototype, { + } - getPrevNode: function () { + // parse nodes in FBXTree.Objects.subNodes.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) + function parseAnimationCurves( FBXTree, connections, curveNodesMap ) { - return this.nodeStack[this.currentIndent - 2]; + var rawCurves = FBXTree.Objects.subNodes.AnimationCurve; - }, + for ( var nodeID in rawCurves ) { - getCurrentNode: function () { + var animationCurve = { - return this.nodeStack[this.currentIndent - 1]; + id: rawCurves[ nodeID ].id, + times: rawCurves[ nodeID ].subNodes.KeyTime.properties.a.map( convertFBXTimeToSeconds ), + values: rawCurves[ nodeID ].subNodes.KeyValueFloat.properties.a, - }, + }; - getCurrentProp: function () { + var relationships = connections.get( animationCurve.id ); - return this.currentProp; + if ( relationships !== undefined ) { - }, + var animationCurveID = relationships.parents[ 0 ].ID; + var animationCurveRelationship = relationships.parents[ 0 ].relationship; + var axis = ''; - pushStack: function (node) { + if ( animationCurveRelationship.match( /X/ ) ) { - this.nodeStack.push(node); - this.currentIndent += 1; + axis = 'x'; - }, + } else if ( animationCurveRelationship.match( /Y/ ) ) { - popStack: function () { + axis = 'y'; - this.nodeStack.pop(); - this.currentIndent -= 1; + } else if ( animationCurveRelationship.match( /Z/ ) ) { - }, + axis = 'z'; - setCurrentProp: function (val, name) { + } else { - this.currentProp = val; - this.currentPropName = name; + continue; - }, + } - // ----------parse --------------------------------------------------- - parse: function (text) { + curveNodesMap.get( animationCurveID ).curves[ axis ] = animationCurve; - this.currentIndent = 0; - this.allNodes = new FBXTree(); - this.nodeStack = []; - this.currentProp = []; - this.currentPropName = ''; + } - var split = text.split("\n"); + } - for (var line in split) { + } - var l = split[line]; + // parse nodes in FBXTree.Objects.subNodes.AnimationLayer. Each layers holds references + // to various AnimationCurveNodes and is referenced by an AnimationStack node + // note: theoretically a stack can multiple layers, however in practice there always seems to be one per stack + function parseAnimationLayers( FBXTree, connections, curveNodesMap ) { - // short cut - if (l.match(/^[\s\t]*;/)) { + var rawLayers = FBXTree.Objects.subNodes.AnimationLayer; - continue; + var layersMap = new Map(); - } // skip comment line - if (l.match(/^[\s\t]*$/)) { + for ( var nodeID in rawLayers ) { - continue; + var layerCurveNodes = []; - } // skip empty line + var connection = connections.get( parseInt( nodeID ) ); - // beginning of node - var beginningOfNodeExp = new RegExp("^\\t{" + this.currentIndent + "}(\\w+):(.*){", ''); - var match = l.match(beginningOfNodeExp); - if (match) { + if ( connection !== undefined ) { - var nodeName = match[1].trim().replace(/^"/, '').replace(/"$/, ""); - var nodeAttrs = match[2].split(','); + // all the animationCurveNodes used in the layer + var children = connection.children; - for (var i = 0, l = nodeAttrs.length; i < l; i++) { - nodeAttrs[i] = nodeAttrs[i].trim().replace(/^"/, '').replace(/"$/, ''); - } + children.forEach( function ( child, i ) { - this.parseNodeBegin(l, nodeName, nodeAttrs || null); - continue; + if ( curveNodesMap.has( child.ID ) ) { - } + var curveNode = curveNodesMap.get( child.ID ); - // node's property - var propExp = new RegExp("^\\t{" + (this.currentIndent) + "}(\\w+):[\\s\\t\\r\\n](.*)"); - var match = l.match(propExp); - if (match) { + if ( layerCurveNodes[ i ] === undefined ) { - var propName = match[1].replace(/^"/, '').replace(/"$/, "").trim(); - var propValue = match[2].replace(/^"/, '').replace(/"$/, "").trim(); + var modelID; - this.parseNodeProperty(l, propName, propValue); - continue; + connections.get( child.ID ).parents.forEach( function ( parent ) { - } + if ( parent.relationship !== undefined ) modelID = parent.ID; - // end of node - var endOfNodeExp = new RegExp("^\\t{" + (this.currentIndent - 1) + "}}"); - if (l.match(endOfNodeExp)) { + } ); - this.nodeEnd(); - continue; + var rawModel = FBXTree.Objects.subNodes.Model[ modelID.toString() ]; - } + var node = { - // for special case, - // - // Vertices: *8670 { - // a: 0.0356229953467846,13.9599733352661,-0.399196773.....(snip) - // -0.0612030513584614,13.960485458374,-0.409748703241348,-0.10..... - // 0.12490539252758,13.7450733184814,-0.454119384288788,0.09272..... - // 0.0836158767342567,13.5432004928589,-0.435397416353226,0.028..... - // - // these case the lines must contiue with previous line - if (l.match(/^[^\s\t}]/)) { + modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ), + initialPosition: [ 0, 0, 0 ], + initialRotation: [ 0, 0, 0 ], + initialScale: [ 1, 1, 1 ], - this.parseNodePropertyContinued(l); + }; - } + if ( 'Lcl_Translation' in rawModel.properties ) node.initialPosition = rawModel.properties.Lcl_Translation.value; - } + if ( 'Lcl_Rotation' in rawModel.properties ) node.initialRotation = rawModel.properties.Lcl_Rotation.value; - return this.allNodes; + if ( 'Lcl_Scaling' in rawModel.properties ) node.initialScale = rawModel.properties.Lcl_Scaling.value; - }, + // 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.properties ) node.preRotations = rawModel.properties.PreRotation.value; - parseNodeBegin: function (line, nodeName, nodeAttrs) { + layerCurveNodes[ i ] = node; - // var nodeName = match[1]; - var node = { 'name': nodeName, properties: {}, 'subNodes': {} }; - var attrs = this.parseNodeAttr(nodeAttrs); - var currentNode = this.getCurrentNode(); + } - // a top node - if (this.currentIndent === 0) { + layerCurveNodes[ i ][ curveNode.attr ] = curveNode; - this.allNodes.add(nodeName, node); + } - } else { + } ); - // a subnode + layersMap.set( parseInt( nodeID ), layerCurveNodes ); - // already exists subnode, then append it - if (nodeName in currentNode.subNodes) { + } - var tmp = currentNode.subNodes[nodeName]; + } - // console.log( "duped entry found\nkey: " + nodeName + "\nvalue: " + propValue ); - if (this.isFlattenNode(currentNode.subNodes[nodeName])) { + return layersMap; + } - if (attrs.id === '') { + // parse nodes in FBXTree.Objects.subNodes.AnimationStack. These are the top level node in the animation + // hierarchy. Each Stack node will be used to create a THREE.AnimationClip + function parseAnimStacks( FBXTree, connections, layersMap ) { - currentNode.subNodes[nodeName] = []; - currentNode.subNodes[nodeName].push(tmp); + var rawStacks = FBXTree.Objects.subNodes.AnimationStack; - } else { + // connect the stacks (clips) up to the layers + var rawClips = {}; - currentNode.subNodes[nodeName] = {}; - currentNode.subNodes[nodeName][tmp.id] = tmp; + for ( var nodeID in rawStacks ) { - } + var children = connections.get( parseInt( nodeID ) ).children; - } + if ( children.length > 1 ) { - if (attrs.id === '') { + // 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.' ); - currentNode.subNodes[nodeName].push(node); + } - } else { + var layer = layersMap.get( children[ 0 ].ID ); - currentNode.subNodes[nodeName][attrs.id] = node; + rawClips[ nodeID ] = { - } + name: rawStacks[ nodeID ].attrName, + layer: layer, - } else if (typeof attrs.id === 'number' || attrs.id.match(/^\d+$/)) { + }; - currentNode.subNodes[nodeName] = {}; - currentNode.subNodes[nodeName][attrs.id] = node; + } - } else { + return rawClips; - currentNode.subNodes[nodeName] = node; + } - } + // take raw animation data from parseAnimations and connect it up to the loaded models + function addAnimations( FBXTree, connections, sceneGraph ) { - } + sceneGraph.animations = []; - // for this ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ - // NodeAttribute: 1001463072, "NodeAttribute::", "LimbNode" { - if (nodeAttrs) { + var rawClips = parseAnimations( FBXTree, connections ); - node.id = attrs.id; - node.attrName = attrs.name; - node.attrType = attrs.type; + if ( rawClips === undefined ) return; - } + for ( var key in rawClips ) { - this.pushStack(node); + var rawClip = rawClips[ key ]; - }, + var clip = addClip( rawClip ); - parseNodeAttr: function (attrs) { + sceneGraph.animations.push( clip ); - var id = attrs[0]; + } - if (attrs[0] !== "") { + } - id = parseInt(attrs[0]); + function addClip( rawClip ) { - if (isNaN(id)) { + var tracks = []; - // PolygonVertexIndex: *16380 { - id = attrs[0]; + rawClip.layer.forEach( function ( rawTracks ) { - } + tracks = tracks.concat( generateTracks( rawTracks ) ); - } + } ); - var name = '', type = ''; + return new THREE.AnimationClip( rawClip.name, - 1, tracks ); - if (attrs.length > 1) { + } - name = attrs[1].replace(/^(\w+)::/, ''); - type = attrs[2]; + function generateTracks( rawTracks ) { - } + var tracks = []; - return { id: id, name: name, type: type }; + if ( rawTracks.T !== undefined ) { - }, + var positionTrack = generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, rawTracks.initialPosition, 'position' ); + if ( positionTrack !== undefined ) tracks.push( positionTrack ); - parseNodeProperty: function (line, propName, propValue) { + } - var currentNode = this.getCurrentNode(); - var parentName = currentNode.name; + if ( rawTracks.R !== undefined ) { - // special case parent node's is like "Properties70" - // these chilren nodes must treat with careful - if (parentName !== undefined) { + var rotationTrack = generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.initialRotation, rawTracks.preRotations ); + if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); - var propMatch = parentName.match(/Properties(\d)+/); - if (propMatch) { + } - this.parseNodeSpecialProperty(line, propName, propValue); - return; + if ( rawTracks.S !== undefined ) { - } + var scaleTrack = generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, rawTracks.initialScale, 'scale' ); + if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); - } + } - // special case Connections - if (propName == 'C') { + return tracks; - var connProps = propValue.split(',').slice(1); - var from = parseInt(connProps[0]); - var to = parseInt(connProps[1]); + } - var rest = propValue.split(',').slice(3); + function generateVectorTrack( modelName, curves, initialValue, type ) { - propName = 'connections'; - propValue = [from, to]; - append(propValue, rest); + var times = getTimesForAllAxes( curves ); + var values = getKeyframeTrackValues( times, curves, initialValue ); - if (currentNode.properties[propName] === undefined) { + return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values ); - currentNode.properties[propName] = []; + } - } + function generateRotationTrack( modelName, curves, initialValue, preRotations ) { - } + if ( curves.x !== undefined ) curves.x.values = curves.x.values.map( THREE.Math.degToRad ); + if ( curves.y !== undefined ) curves.y.values = curves.y.values.map( THREE.Math.degToRad ); + if ( curves.z !== undefined ) curves.z.values = curves.z.values.map( THREE.Math.degToRad ); - // special case Connections - if (propName == 'Node') { + var times = getTimesForAllAxes( curves ); + var values = getKeyframeTrackValues( times, curves, initialValue ); - var id = parseInt(propValue); - currentNode.properties.id = id; - currentNode.id = id; + if ( preRotations !== undefined ) { - } + preRotations = preRotations.map( THREE.Math.degToRad ); + preRotations.push( 'ZYX' ); - // already exists in properties, then append this - if (propName in currentNode.properties) { + preRotations = new THREE.Euler().fromArray( preRotations ); + preRotations = new THREE.Quaternion().setFromEuler( preRotations ); - // console.log( "duped entry found\nkey: " + propName + "\nvalue: " + propValue ); - if (Array.isArray(currentNode.properties[propName])) { + } - currentNode.properties[propName].push(propValue); + var quaternion = new THREE.Quaternion(); + var euler = new THREE.Euler(); - } else { + var quaternionValues = []; - currentNode.properties[propName] += propValue; + for ( var i = 0; i < values.length; i += 3 ) { - } + euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' ); - } else { + quaternion.setFromEuler( euler ); - // console.log( propName + ": " + propValue ); - if (Array.isArray(currentNode.properties[propName])) { + if ( preRotations !== undefined )quaternion.premultiply( preRotations ); - currentNode.properties[propName].push(propValue); + quaternion.toArray( quaternionValues, ( i / 3 ) * 4 ); - } else { + } - currentNode.properties[propName] = propValue; + return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); - } + } - } + function getKeyframeTrackValues( times, curves, initialValue ) { - this.setCurrentProp(currentNode.properties, propName); + var prevValue = initialValue; - }, + var values = []; - // TODO: - parseNodePropertyContinued: function (line) { + var xIndex = - 1; + var yIndex = - 1; + var zIndex = - 1; - this.currentProp[this.currentPropName] += line; + times.forEach( function ( time ) { - }, + 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 ); - parseNodeSpecialProperty: function (line, propName, propValue) { + // if there is an x value defined for this frame, use that + if ( xIndex !== - 1 ) { - // 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('",'); + var xValue = curves.x.values[ xIndex ]; + values.push( xValue ); + prevValue[ 0 ] = xValue; - for (var i = 0, l = props.length; i < l; i++) { - props[i] = props[i].trim().replace(/^\"/, '').replace(/\s/, '_'); - } + } else { - var innerPropName = props[0]; - var innerPropType1 = props[1]; - var innerPropType2 = props[2]; - var innerPropFlag = props[3]; - var innerPropValue = props[4]; + // otherwise use the x value from the previous frame + values.push( prevValue[ 0 ] ); - /* - if ( innerPropValue === undefined ) { - innerPropValue = props[3]; } - */ - // cast value in its type - switch (innerPropType1) { + if ( yIndex !== - 1 ) { - case "int": - innerPropValue = parseInt(innerPropValue); - break; + var yValue = curves.y.values[ yIndex ]; + values.push( yValue ); + prevValue[ 1 ] = yValue; - case "double": - innerPropValue = parseFloat(innerPropValue); - break; + } else { - case "ColorRGB": - case "Vector3D": - innerPropValue = parseFloatArray(innerPropValue); - break; + values.push( prevValue[ 1 ] ); - } + } - // CAUTION: these props must append to parent's parent - this.getPrevNode().properties[innerPropName] = { + if ( zIndex !== - 1 ) { - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue + var zValue = curves.z.values[ zIndex ]; + values.push( zValue ); + prevValue[ 2 ] = zValue; - }; + } else { - this.setCurrentProp(this.getPrevNode().properties, innerPropName); + values.push( prevValue[ 2 ] ); - }, + } - nodeEnd: function () { + } ); - this.popStack(); + return values; - }, + } - /* ---------------------------------------------------------------- */ - /* util */ - isFlattenNode: function (node) { + // For all animated objects, times are defined separately for each axis + // Here we'll combine the times into one sorted array without duplicates + function getTimesForAllAxes( curves ) { - return ('subNodes' in node && 'properties' in node) ? true : false; + 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 and remove duplicates + times = times.sort( function ( a, b ) { - // Binary format specification: - // https://code.blender.org/2013/08/fbx-binary-file-format-specification/ - // https://wiki.rogiken.org/specifications/file-format/fbx/ (more detail but Japanese) - function BinaryParser() { } + return a - b; - Object.assign(BinaryParser.prototype, { + } ).filter( function ( elem, index, array ) { - /** - * Parses binary data and builds FBXTree as much compatible as possible with the one built by TextParser. - * @param {ArrayBuffer} buffer - * @returns {THREE.FBXTree} - */ - parse: function (buffer) { + return array.indexOf( elem ) == index; - var reader = new BinaryReader(buffer); - reader.skip(23); // skip magic 23 bytes + } ); - var version = reader.getUint32(); + return times; - console.log('FBX binary version: ' + version); + } - var allNodes = new FBXTree(); + // parse an FBX file in ASCII format + function TextParser() {} - while (!this.endOfContent(reader)) { + Object.assign( TextParser.prototype, { - var node = this.parseNode(reader, version); - if (node !== null) allNodes.add(node.name, node); + getPrevNode: function () { - } + return this.nodeStack[ this.currentIndent - 2 ]; - return allNodes; + }, - }, + getCurrentNode: function () { - /** - * Checks if reader has reached the end of content. - * @param {BinaryReader} reader - * @returns {boolean} - */ - endOfContent: function (reader) { + return this.nodeStack[ this.currentIndent - 1 ]; - // footer size: 160bytes + 16-byte alignment padding - // - 16bytes: magic - // - padding til 16-byte alignment (at least 1byte?) - // (seems like some exporters embed fixed 15bytes?) - // - 4bytes: magic - // - 4bytes: version - // - 120bytes: zero - // - 16bytes: magic - if (reader.size() % 16 === 0) { + }, - return ((reader.getOffset() + 160 + 16) & ~0xf) >= reader.size(); + getCurrentProp: function () { - } else { + return this.currentProp; - return reader.getOffset() + 160 + 15 >= reader.size(); + }, - } + pushStack: function ( node ) { - }, + this.nodeStack.push( node ); + this.currentIndent += 1; - /** - * Parses Node as much compatible as possible with the one parsed by TextParser - * TODO: could be optimized more? - * @param {BinaryReader} reader - * @param {number} version - * @returns {Object} - Returns an Object as node, or null if NULL-record. - */ - parseNode: function (reader, version) { + }, - // The first three data sizes depends on version. - var endOffset = (version >= 7500) ? reader.getUint64() : reader.getUint32(); - var numProperties = (version >= 7500) ? reader.getUint64() : reader.getUint32(); - var propertyListLen = (version >= 7500) ? reader.getUint64() : reader.getUint32(); - var nameLen = reader.getUint8(); - var name = reader.getString(nameLen); + popStack: function () { - // Regards this node as NULL-record if endOffset is zero - if (endOffset === 0) return null; + this.nodeStack.pop(); + this.currentIndent -= 1; - var propertyList = []; + }, - for (var i = 0; i < numProperties; i++) { + setCurrentProp: function ( val, name ) { - propertyList.push(this.parseProperty(reader)); + this.currentProp = val; + this.currentPropName = name; - } + }, - // 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] : ''; + parse: function ( text ) { - var subNodes = {}; - var properties = {}; + this.currentIndent = 0; + this.allNodes = new FBXTree(); + this.nodeStack = []; + this.currentProp = []; + this.currentPropName = ''; - var isSingleProperty = false; + var self = this; - // 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]} - if (numProperties === 1 && reader.getOffset() === endOffset) { + var split = text.split( '\n' ); - isSingleProperty = true; + split.forEach( function ( line, i ) { - } + var matchComment = line.match( /^[\s\t]*;/ ); + var matchEmpty = line.match( /^[\s\t]*$/ ); - while (endOffset > reader.getOffset()) { + if ( matchComment || matchEmpty ) return; - var node = this.parseNode(reader, version); + var matchBeginning = line.match( '^\\t{' + self.currentIndent + '}(\\w+):(.*){', '' ); + var matchProperty = line.match( '^\\t{' + ( self.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' ); + var matchEnd = line.match( '^\\t{' + ( self.currentIndent - 1 ) + '}}' ); - if (node === null) continue; + if ( matchBeginning ) { - // special case: child node is single property - if (node.singleProperty === true) { + self.parseNodeBegin( line, matchBeginning ); - var value = node.propertyList[0]; + } else if ( matchProperty ) { - if (Array.isArray(value)) { + self.parseNodeProperty( line, matchProperty, split[ ++ i ] ); - // node represents - // Vertices: *3 { - // a: 0.01, 0.02, 0.03 - // } - // of text format here. + } else if ( matchEnd ) { - node.properties[node.name] = node.propertyList[0]; - subNodes[node.name] = node; + self.nodeEnd(); - // Later phase expects single property array is in node.properties.a as String. - // TODO: optimize - node.properties.a = value.toString(); + } else if ( line.match( /^[^\s\t}]/ ) ) { - } else { + // 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 + self.parseNodePropertyContinued( line ); - // node represents - // Version: 100 - // of text format here. + } - properties[node.name] = value; + } ); - } + return this.allNodes; - continue; + }, - } + parseNodeBegin: function ( line, property ) { - // special case: connections - if (name === 'Connections' && node.name === 'C') { + var nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); - var array = []; + var nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { - // node.propertyList would be like - // ["OO", 111264976, 144038752, "d|x"] (?, from, to, additional values) - for (var i = 1, il = node.propertyList.length; i < il; i++) { + return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); - array[i - 1] = node.propertyList[i]; + } ); - } + var node = { 'name': nodeName, properties: {}, 'subNodes': {} }; + var attrs = this.parseNodeAttr( nodeAttrs ); + var currentNode = this.getCurrentNode(); - if (properties.connections === undefined) { + // a top node + if ( this.currentIndent === 0 ) { - properties.connections = []; + this.allNodes.add( nodeName, node ); - } + } else { // a subnode - properties.connections.push(array); + // if the subnode already exists, append it + if ( nodeName in currentNode.subNodes ) { - continue; + var tmp = currentNode.subNodes[ nodeName ]; - } + if ( this.isFlattenNode( currentNode.subNodes[ nodeName ] ) ) { - // special case: child node is Properties\d+ - if (node.name.match(/^Properties\d+$/)) { + if ( attrs.id === '' ) { - // move child node's properties to this node. + currentNode.subNodes[ nodeName ] = []; + currentNode.subNodes[ nodeName ].push( tmp ); - var keys = Object.keys(node.properties); + } else { - for (var i = 0, il = keys.length; i < il; i++) { + currentNode.subNodes[ nodeName ] = {}; + currentNode.subNodes[ nodeName ][ tmp.id ] = tmp; - var key = keys[i]; - properties[key] = node.properties[key]; + } - } + } - continue; + if ( attrs.id === '' ) { - } + currentNode.subNodes[ nodeName ].push( node ); - // special case: properties - if (name.match(/^Properties\d+$/) && node.name === 'P') { + } else { - var innerPropName = node.propertyList[0]; - var innerPropType1 = node.propertyList[1]; - var innerPropType2 = node.propertyList[2]; - var innerPropFlag = node.propertyList[3]; - var innerPropValue; + currentNode.subNodes[ nodeName ][ attrs.id ] = node; - if (innerPropName.indexOf('Lcl ') === 0) innerPropName = innerPropName.replace('Lcl ', 'Lcl_'); - if (innerPropType1.indexOf('Lcl ') === 0) innerPropType1 = innerPropType1.replace('Lcl ', 'Lcl_'); + } - if (innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || - innerPropType1 === 'Vector3D' || innerPropType1.indexOf('Lcl_') === 0) { + } else if ( typeof attrs.id === 'number' || attrs.id.match( /^\d+$/ ) ) { - innerPropValue = [ - node.propertyList[4], - node.propertyList[5], - node.propertyList[6] - ]; + currentNode.subNodes[ nodeName ] = {}; + currentNode.subNodes[ nodeName ][ attrs.id ] = node; - } else { + } else { - innerPropValue = node.propertyList[4]; + currentNode.subNodes[ nodeName ] = node; - } + } - if (innerPropType1.indexOf('Lcl_') === 0) { + } - innerPropValue = innerPropValue.toString(); - } + // for this ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // NodeAttribute: 1001463072, "NodeAttribute::", "LimbNode" { + if ( nodeAttrs ) { - // this will be copied to parent. see above. - properties[innerPropName] = { + node.id = attrs.id; + node.attrName = attrs.name; + node.attrType = attrs.type; - 'type': innerPropType1, - 'type2': innerPropType2, - 'flag': innerPropFlag, - 'value': innerPropValue + } - }; + this.pushStack( node ); - continue; + }, - } + parseNodeAttr: function ( attrs ) { - // standard case - // follows TextParser's manner. - if (subNodes[node.name] === undefined) { + var id = attrs[ 0 ]; - if (typeof node.id === 'number') { + if ( attrs[ 0 ] !== '' ) { - subNodes[node.name] = {}; - subNodes[node.name][node.id] = node; + id = parseInt( attrs[ 0 ] ); - } else { + if ( isNaN( id ) ) { - subNodes[node.name] = node; + id = attrs[ 0 ]; - } + } - } else { + } - if (node.id === '') { + var name = '', type = ''; - if (!Array.isArray(subNodes[node.name])) { + if ( attrs.length > 1 ) { - subNodes[node.name] = [subNodes[node.name]]; + name = attrs[ 1 ].replace( /^(\w+)::/, '' ); + type = attrs[ 2 ]; - } + } - subNodes[node.name].push(node); + return { id: id, name: name, type: type }; - } else { + }, - if (subNodes[node.name][node.id] === undefined) { + parseNodeProperty: function ( line, property, contentLine ) { - subNodes[node.name][node.id] = node; + var propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); + var propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); - } else { + // for special case: base64 image data follows "Content: ," line + // Content: , + // "iVB..." + if ( propName === 'Content' && propValue === ',' ) { - // conflict id. irregular? + propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); - if (!Array.isArray(subNodes[node.name][node.id])) { + } - subNodes[node.name][node.id] = [subNodes[node.name][node.id]]; + var currentNode = this.getCurrentNode(); + var parentName = currentNode.name; - } + // special case where the parent node is something like "Properties70" + // these children nodes must treated carefully + if ( parentName !== undefined ) { - subNodes[node.name][node.id].push(node); + var propMatch = parentName.match( /Properties(\d)+/ ); + if ( propMatch ) { - } + this.parseNodeSpecialProperty( line, propName, propValue ); + return; - } + } - } + } - } + // Connections + if ( propName === 'C' ) { - return { + var connProps = propValue.split( ',' ).slice( 1 ); + var from = parseInt( connProps[ 0 ] ); + var to = parseInt( connProps[ 1 ] ); - singleProperty: isSingleProperty, - id: id, - attrName: attrName, - attrType: attrType, - name: name, - properties: properties, - propertyList: propertyList, // raw property list, would be used by parent - subNodes: subNodes + var rest = propValue.split( ',' ).slice( 3 ); - }; + rest = rest.map( function ( elem ) { - }, + return elem.trim().replace( /^"/, '' ); - parseProperty: function (reader) { + } ); - var type = reader.getChar(); + propName = 'connections'; + propValue = [ from, to ]; + append( propValue, rest ); - switch (type) { + if ( currentNode.properties[ propName ] === undefined ) { - case 'F': - return reader.getFloat32(); + currentNode.properties[ propName ] = []; - case 'D': - return reader.getFloat64(); + } - case 'L': - return reader.getInt64(); + } - case 'I': - return reader.getInt32(); + // Node + if ( propName === 'Node' ) { - case 'Y': - return reader.getInt16(); + var id = parseInt( propValue ); + currentNode.properties.id = id; + currentNode.id = id; - case 'C': - return reader.getBoolean(); + } - case 'f': - case 'd': - case 'l': - case 'i': - case 'b': + // already exists in properties, then append this + if ( propName in currentNode.properties ) { - var arrayLength = reader.getUint32(); - var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed - var compressedLength = reader.getUint32(); + if ( Array.isArray( currentNode.properties[ propName ] ) ) { - if (encoding === 0) { + currentNode.properties[ propName ].push( propValue ); - switch (type) { + } else { - case 'f': - return reader.getFloat32Array(arrayLength); + currentNode.properties[ propName ] += propValue; - case 'd': - return reader.getFloat64Array(arrayLength); + } - case 'l': - return reader.getInt64Array(arrayLength); + } else { - case 'i': - return reader.getInt32Array(arrayLength); + if ( Array.isArray( currentNode.properties[ propName ] ) ) { - case 'b': - return reader.getBooleanArray(arrayLength); + currentNode.properties[ propName ].push( propValue ); - } + } else { - } + currentNode.properties[ propName ] = propValue; - if (window.Zlib === undefined) { + } - throw new Error('FBXLoader: Import inflate.min.js from https://github.com/imaya/zlib.js'); + } - } + this.setCurrentProp( currentNode.properties, propName ); - var inflate = new Zlib.Inflate(new Uint8Array(reader.getArrayBuffer(compressedLength))); - var reader2 = new BinaryReader(inflate.decompress().buffer); + // convert string to array, unless it ends in ',' in which case more will be added to it + if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { - switch (type) { + currentNode.properties.a = parseNumberArray( propValue ); - case 'f': - return reader2.getFloat32Array(arrayLength); + } - case 'd': - return reader2.getFloat64Array(arrayLength); + }, - case 'l': - return reader2.getInt64Array(arrayLength); + parseNodePropertyContinued: function ( line ) { - case 'i': - return reader2.getInt32Array(arrayLength); + this.currentProp[ this.currentPropName ] += line; - case 'b': - return reader2.getBooleanArray(arrayLength); + // 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 ) !== ',' ) { - } + var currentNode = this.getCurrentNode(); + currentNode.properties.a = parseNumberArray( currentNode.properties.a ); - case 'S': - var length = reader.getUint32(); - return reader.getString(length); + } - case 'R': - var length = reader.getUint32(); - return reader.getArrayBuffer(length); + }, - default: - throw new Error('FBXLoader: Unknown property type ' + type); + parseNodeSpecialProperty: function ( line, propName, 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 ) { - } + return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); - }); + } ); + var innerPropName = props[ 0 ]; + var innerPropType1 = props[ 1 ]; + var innerPropType2 = props[ 2 ]; + var innerPropFlag = props[ 3 ]; + var innerPropValue = props[ 4 ]; - function BinaryReader(buffer, littleEndian) { + // cast value to its type + switch ( innerPropType1 ) { - this.dv = new DataView(buffer); - this.offset = 0; - this.littleEndian = (littleEndian !== undefined) ? littleEndian : true; + case 'int': + case 'enum': + case 'bool': + case 'ULongLong': + innerPropValue = parseInt( innerPropValue ); + break; - } + case 'double': + case 'Number': + case 'FieldOfView': + innerPropValue = parseFloat( innerPropValue ); + break; - Object.assign(BinaryReader.prototype, { + case 'ColorRGB': + case 'Vector3D': + case 'Lcl_Translation': + case 'Lcl_Rotation': + case 'Lcl_Scaling': + innerPropValue = parseNumberArray( innerPropValue ); + break; - getOffset: function () { + } - return this.offset; + // CAUTION: these props must append to parent's parent + this.getPrevNode().properties[ innerPropName ] = { - }, + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue - size: function () { + }; - return this.dv.buffer.byteLength; + this.setCurrentProp( this.getPrevNode().properties, innerPropName ); - }, + }, - skip: function (length) { + nodeEnd: function () { - this.offset += length; + this.popStack(); - }, + }, - // seems like true/false representation depends on exporter. - // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) - // then sees LSB. - getBoolean: function () { + isFlattenNode: function ( node ) { - return (this.getUint8() & 1) === 1; + return ( 'subNodes' in node && 'properties' in node ) ? true : false; - }, + } - getBooleanArray: function (size) { + } ); - var a = []; + // Parse an FBX file in Binary format + function BinaryParser() {} - for (var i = 0; i < size; i++) { + Object.assign( BinaryParser.prototype, { - a.push(this.getBoolean()); + parse: function ( buffer ) { - } + var reader = new BinaryReader( buffer ); + reader.skip( 23 ); // skip magic 23 bytes - return a; + var version = reader.getUint32(); - }, + console.log( 'THREE.FBXLoader: FBX binary version: ' + version ); - getInt8: function () { + var allNodes = new FBXTree(); - var value = this.dv.getInt8(this.offset); - this.offset += 1; - return value; + while ( ! this.endOfContent( reader ) ) { - }, + var node = this.parseNode( reader, version ); + if ( node !== null ) allNodes.add( node.name, node ); - getInt8Array: function (size) { + } - var a = []; + return allNodes; - for (var i = 0; i < size; i++) { + }, - a.push(this.getInt8()); + // 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 ) { - return a; + return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size(); - }, + } else { - getUint8: function () { + return reader.getOffset() + 160 + 16 >= reader.size(); - var value = this.dv.getUint8(this.offset); - this.offset += 1; - return value; + } - }, + }, - getUint8Array: function (size) { + parseNode: function ( reader, version ) { - var a = []; + // The first three data sizes depends on version. + var endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); + var numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); - for (var i = 0; i < size; i++) { + // note: do not remove this even if you get a linter warning as it moves the buffer forward + var propertyListLen = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); - a.push(this.getUint8()); + 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 a; + var propertyList = []; - }, + for ( var i = 0; i < numProperties; i ++ ) { - getInt16: function () { + propertyList.push( this.parseProperty( reader ) ); - var value = this.dv.getInt16(this.offset, this.littleEndian); - this.offset += 2; - return value; + } - }, + // 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 ] : ''; - getInt16Array: function (size) { + var subNodes = {}; + var properties = {}; - var a = []; + var isSingleProperty = false; - for (var i = 0; i < size; i++) { + // 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]} + if ( numProperties === 1 && reader.getOffset() === endOffset ) { - a.push(this.getInt16()); + isSingleProperty = true; - } + } - return a; + while ( endOffset > reader.getOffset() ) { - }, + var node = this.parseNode( reader, version ); - getUint16: function () { + if ( node === null ) continue; - var value = this.dv.getUint16(this.offset, this.littleEndian); - this.offset += 2; - return value; + // special case: child node is single property + if ( node.singleProperty === true ) { - }, + var value = node.propertyList[ 0 ]; - getUint16Array: function (size) { + if ( Array.isArray( value ) ) { - var a = []; + subNodes[ node.name ] = node; - for (var i = 0; i < size; i++) { + node.properties.a = value; - a.push(this.getUint16()); + } else { - } + properties[ node.name ] = value; - return a; + } - }, + continue; - getInt32: function () { + } - var value = this.dv.getInt32(this.offset, this.littleEndian); - this.offset += 4; - return value; + // parse connections + if ( name === 'Connections' && node.name === 'C' ) { - }, + var array = []; - getInt32Array: function (size) { + node.propertyList.forEach( function ( property, i ) { - var a = []; + array[ i - 1 ] = property; - for (var i = 0; i < size; i++) { + } ); - a.push(this.getInt32()); + if ( properties.connections === undefined ) { - } + properties.connections = []; - return a; + } - }, + properties.connections.push( array ); - getUint32: function () { + continue; - var value = this.dv.getUint32(this.offset, this.littleEndian); - this.offset += 4; - return value; + } - }, + // special case: child node is Properties\d+ + // move child node's properties to this node. + if ( node.name === 'Properties70' ) { - getUint32Array: function (size) { + var keys = Object.keys( node.properties ); - var a = []; + keys.forEach( function ( key ) { - for (var i = 0; i < size; i++) { + properties[ key ] = node.properties[ key ]; - a.push(this.getUint32()); + } ); - } + continue; - return a; + } - }, + // parse 'properties70' + if ( name === 'Properties70' && node.name === 'P' ) { - // JavaScript doesn't support 64-bit integer so attempting to calculate by ourselves. - // 1 << 32 will return 1 so using multiply operation instead here. - // There'd be 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 innerPropName = node.propertyList[ 0 ]; + var innerPropType1 = node.propertyList[ 1 ]; + var innerPropType2 = node.propertyList[ 2 ]; + var innerPropFlag = node.propertyList[ 3 ]; + var innerPropValue; - var low, high; + if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); + if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); - if (this.littleEndian) { + if ( innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { - low = this.getUint32(); - high = this.getUint32(); + innerPropValue = [ + node.propertyList[ 4 ], + node.propertyList[ 5 ], + node.propertyList[ 6 ] + ]; - } else { + } else { - high = this.getUint32(); - low = this.getUint32(); + innerPropValue = node.propertyList[ 4 ]; - } + } + + // this will be copied to parent, see above + properties[ innerPropName ] = { + + 'type': innerPropType1, + 'type2': innerPropType2, + 'flag': innerPropFlag, + 'value': innerPropValue + + }; + + continue; + + } + + if ( subNodes[ node.name ] === undefined ) { + + if ( typeof node.id === 'number' ) { + + subNodes[ node.name ] = {}; + subNodes[ node.name ][ node.id ] = node; + + } else { + + subNodes[ node.name ] = node; + + } + + } else { + + if ( node.id === '' ) { + + if ( ! Array.isArray( subNodes[ node.name ] ) ) { + + subNodes[ node.name ] = [ subNodes[ node.name ] ]; + + } + + subNodes[ node.name ].push( node ); + + } else { + + if ( subNodes[ node.name ][ node.id ] === undefined ) { - // calculate negative value - if (high & 0x80000000) { + subNodes[ node.name ][ node.id ] = node; - high = ~high & 0xFFFFFFFF; - low = ~low & 0xFFFFFFFF; + } else { - if (low === 0xFFFFFFFF) high = (high + 1) & 0xFFFFFFFF; + // conflict id. irregular? + if ( ! Array.isArray( subNodes[ node.name ][ node.id ] ) ) { - low = (low + 1) & 0xFFFFFFFF; + subNodes[ node.name ][ node.id ] = [ subNodes[ node.name ][ node.id ] ]; - return - (high * 0x100000000 + low); + } - } + subNodes[ node.name ][ node.id ].push( node ); - return high * 0x100000000 + low; + } - }, + } - getInt64Array: function (size) { + } - var a = []; + } - for (var i = 0; i < size; i++) { + return { - a.push(this.getInt64()); + singleProperty: isSingleProperty, + id: id, + attrName: attrName, + attrType: attrType, + name: name, + properties: properties, + propertyList: propertyList, // raw property list used by parent + subNodes: subNodes - } + }; - return a; + }, - }, + parseProperty: function ( reader ) { - // Note: see getInt64() comment - getUint64: function () { + var type = reader.getChar(); - var low, high; + switch ( type ) { - if (this.littleEndian) { + case 'C': + return reader.getBoolean(); - low = this.getUint32(); - high = this.getUint32(); + case 'D': + return reader.getFloat64(); - } else { + case 'F': + return reader.getFloat32(); - high = this.getUint32(); - low = this.getUint32(); + case 'I': + return reader.getInt32(); - } + case 'L': + return reader.getInt64(); - return high * 0x100000000 + low; + case 'R': + var length = reader.getUint32(); + return reader.getArrayBuffer( length ); - }, + case 'S': + var length = reader.getUint32(); + return reader.getString( length ); - getUint64Array: function (size) { + case 'Y': + return reader.getInt16(); - var a = []; + case 'b': + case 'c': + case 'd': + case 'f': + case 'i': + case 'l': - for (var i = 0; i < size; i++) { + var arrayLength = reader.getUint32(); + var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed + var compressedLength = reader.getUint32(); - a.push(this.getUint64()); + if ( encoding === 0 ) { - } + switch ( type ) { - return a; + case 'b': + case 'c': + return reader.getBooleanArray( arrayLength ); - }, + case 'd': + return reader.getFloat64Array( arrayLength ); - getFloat32: function () { + case 'f': + return reader.getFloat32Array( arrayLength ); - var value = this.dv.getFloat32(this.offset, this.littleEndian); - this.offset += 4; - return value; + case 'i': + return reader.getInt32Array( arrayLength ); - }, + case 'l': + return reader.getInt64Array( arrayLength ); - getFloat32Array: function (size) { + } - var a = []; + } - for (var i = 0; i < size; i++) { - a.push(this.getFloat32()); + var inflated = pako.inflate( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef + var reader2 = new BinaryReader( inflated.buffer ); - } + switch ( type ) { - return a; + case 'b': + case 'c': + return reader2.getBooleanArray( arrayLength ); - }, + case 'd': + return reader2.getFloat64Array( arrayLength ); - getFloat64: function () { + case 'f': + return reader2.getFloat32Array( arrayLength ); - var value = this.dv.getFloat64(this.offset, this.littleEndian); - this.offset += 8; - return value; + case 'i': + return reader2.getInt32Array( arrayLength ); - }, + case 'l': + return reader2.getInt64Array( arrayLength ); - getFloat64Array: function (size) { + } - var a = []; + default: + throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); - for (var i = 0; i < size; i++) { + } - a.push(this.getFloat64()); + } - } + } ); - return a; - }, + function BinaryReader( buffer, littleEndian ) { - getArrayBuffer: function (size) { + this.dv = new DataView( buffer ); + this.offset = 0; + this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true; - var value = this.dv.buffer.slice(this.offset, this.offset + size); - this.offset += size; - return value; + } - }, + Object.assign( BinaryReader.prototype, { - getChar: function () { + getOffset: function () { - return String.fromCharCode(this.getUint8()); + return this.offset; - }, + }, - getString: function (size) { + size: function () { - var s = ''; + return this.dv.buffer.byteLength; - while (size > 0) { + }, - var value = this.getUint8(); - size--; + skip: function ( length ) { - if (value === 0) break; + this.offset += length; - s += String.fromCharCode(value); + }, - } + // seems like true/false representation depends on exporter. + // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) + // then sees LSB. + getBoolean: function () { - this.skip(size); + return ( this.getUint8() & 1 ) === 1; - return s; + }, - } + getBooleanArray: function ( size ) { - }); + var a = []; + for ( var i = 0; i < size; i ++ ) { - function FBXTree() { } + a.push( this.getBoolean() ); - Object.assign(FBXTree.prototype, { + } - add: function (key, val) { + return a; - this[key] = val; + }, - }, + getInt8: function () { - searchConnectionParent: function (id) { + var value = this.dv.getInt8( this.offset ); + this.offset += 1; + return value; - if (this.__cache_search_connection_parent === undefined) { + }, - this.__cache_search_connection_parent = []; + getInt8Array: function ( size ) { - } + var a = []; - if (this.__cache_search_connection_parent[id] !== undefined) { + for ( var i = 0; i < size; i ++ ) { - return this.__cache_search_connection_parent[id]; + a.push( this.getInt8() ); - } else { + } - this.__cache_search_connection_parent[id] = []; + return a; - } + }, - var conns = this.Connections.properties.connections; + getUint8: function () { - var results = []; - for (var i = 0; i < conns.length; ++i) { + var value = this.dv.getUint8( this.offset ); + this.offset += 1; + return value; - if (conns[i][0] == id) { + }, - // 0 means scene root - var res = conns[i][1] === 0 ? - 1 : conns[i][1]; - results.push(res); + getUint8Array: function ( size ) { - } + var a = []; - } + for ( var i = 0; i < size; i ++ ) { - if (results.length > 0) { + a.push( this.getUint8() ); - append(this.__cache_search_connection_parent[id], results); - return results; + } - } else { + return a; - this.__cache_search_connection_parent[id] = [- 1]; - return [- 1]; + }, - } + getInt16: function () { - }, + var value = this.dv.getInt16( this.offset, this.littleEndian ); + this.offset += 2; + return value; - searchConnectionChildren: function (id) { + }, - if (this.__cache_search_connection_children === undefined) { + getInt16Array: function ( size ) { - this.__cache_search_connection_children = []; + var a = []; - } + for ( var i = 0; i < size; i ++ ) { - if (this.__cache_search_connection_children[id] !== undefined) { + a.push( this.getInt16() ); - return this.__cache_search_connection_children[id]; + } - } else { + return a; - this.__cache_search_connection_children[id] = []; + }, - } + getUint16: function () { - var conns = this.Connections.properties.connections; + var value = this.dv.getUint16( this.offset, this.littleEndian ); + this.offset += 2; + return value; - var res = []; - for (var i = 0; i < conns.length; ++i) { + }, - if (conns[i][1] == id) { + getUint16Array: function ( size ) { - // 0 means scene root - res.push(conns[i][0] === 0 ? - 1 : conns[i][0]); - // there may more than one kid, then search to the end + var a = []; - } + for ( var i = 0; i < size; i ++ ) { - } + a.push( this.getUint16() ); - if (res.length > 0) { + } - append(this.__cache_search_connection_children[id], res); - return res; + return a; - } else { + }, - this.__cache_search_connection_children[id] = []; - return []; + getInt32: function () { - } + var value = this.dv.getInt32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - }, + }, - searchConnectionType: function (id, to) { + getInt32Array: function ( size ) { - var key = id + ',' + to; // TODO: to hash - if (this.__cache_search_connection_type === undefined) { + var a = []; - this.__cache_search_connection_type = {}; + for ( var i = 0; i < size; i ++ ) { - } + a.push( this.getInt32() ); - if (this.__cache_search_connection_type[key] !== undefined) { + } - return this.__cache_search_connection_type[key]; + return a; - } else { + }, - this.__cache_search_connection_type[key] = ''; + getUint32: function () { - } + var value = this.dv.getUint32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - var conns = this.Connections.properties.connections; + }, - for (var i = 0; i < conns.length; ++i) { + getUint32Array: function ( size ) { - if (conns[i][0] == id && conns[i][1] == to) { + var a = []; - // 0 means scene root - this.__cache_search_connection_type[key] = conns[i][2]; - return conns[i][2]; + for ( var i = 0; i < size; i ++ ) { - } + a.push( this.getUint32() ); - } + } - this.__cache_search_connection_type[id] = null; - return null; + 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 () { + var low, high; - /** - * @param {ArrayBuffer} buffer - * @returns {boolean} - */ - function isFbxFormatBinary(buffer) { + if ( this.littleEndian ) { - var CORRECT = 'Kaydara FBX Binary \0'; + low = this.getUint32(); + high = this.getUint32(); - return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString(buffer, 0, CORRECT.length); + } else { - } + high = this.getUint32(); + low = this.getUint32(); - /** - * @returns {boolean} - */ - function isFbxFormatASCII(text) { + } - var CORRECT = ['K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\']; + // calculate negative value + if ( high & 0x80000000 ) { - var cursor = 0; + high = ~ high & 0xFFFFFFFF; + low = ~ low & 0xFFFFFFFF; - function read(offset) { + if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF; - var result = text[offset - 1]; - text = text.slice(cursor + offset); - cursor++; - return result; + low = ( low + 1 ) & 0xFFFFFFFF; - } + return - ( high * 0x100000000 + low ); - for (var i = 0; i < CORRECT.length; ++i) { + } - var num = read(1); - if (num == CORRECT[i]) { + return high * 0x100000000 + low; - return false; + }, - } + getInt64Array: function ( size ) { - } + var a = []; - return true; + for ( var i = 0; i < size; i ++ ) { - } + a.push( this.getInt64() ); - /** - * @returns {number} - */ - function getFbxVersion(text) { + } - var versionRegExp = /FBXVersion: (\d+)/; - var match = text.match(versionRegExp); - if (match) { + return a; - var version = parseInt(match[1]); - return version; + }, - } - throw new Error('FBXLoader: Cannot find the version number for the file given.'); + // Note: see getInt64() comment + getUint64: function () { - } + var low, high; - /** - * Converts FBX ticks into real time seconds. - * @param {number} time - FBX tick timestamp to convert. - * @returns {number} - FBX tick in real world time. - */ - function convertFBXTimeToSeconds(time) { + if ( this.littleEndian ) { - // Constant is FBX ticks per second. - return time / 46186158000; + low = this.getUint32(); + high = this.getUint32(); - } + } else { - /** - * Parses comma separated list of float numbers and returns them in an array. - * @example - * // Returns [ 5.6, 9.4, 2.5, 1.4 ] - * parseFloatArray( "5.6,9.4,2.5,1.4" ) - * @returns {number[]} - */ - function parseFloatArray(string) { + high = this.getUint32(); + low = this.getUint32(); - var array = string.split(','); + } - for (var i = 0, l = array.length; i < l; i++) { + return high * 0x100000000 + low; - array[i] = parseFloat(array[i]); + }, - } + getUint64Array: function ( size ) { - return array; + var a = []; - } + for ( var i = 0; i < size; i ++ ) { - /** - * Parses comma separated list of int numbers and returns them in an array. - * @example - * // Returns [ 5, 8, 2, 3 ] - * parseFloatArray( "5,8,2,3" ) - * @returns {number[]} - */ - function parseIntArray(string) { + a.push( this.getUint64() ); - var array = string.split(','); + } + + return a; + + }, - for (var i = 0, l = array.length; i < l; i++) { + getFloat32: function () { - array[i] = parseInt(array[i]); + var value = this.dv.getFloat32( this.offset, this.littleEndian ); + this.offset += 4; + return value; - } + }, - return array; + getFloat32Array: function ( size ) { - } + var a = []; - /** - * Parses Vector3 property from FBXTree. Property is given as .value.x, .value.y, etc. - * @param {FBXVector3} property - Property to parse as Vector3. - * @returns {THREE.Vector3} - */ - function parseVector3(property) { + for ( var i = 0; i < size; i ++ ) { - return new THREE.Vector3().fromArray(property.value); + a.push( this.getFloat32() ); + + } - } + return a; + + }, + + getFloat64: function () { + + var value = this.dv.getFloat64( this.offset, this.littleEndian ); + this.offset += 8; + return value; + + }, + + getFloat64Array: function ( size ) { + + var a = []; + + for ( var i = 0; i < size; i ++ ) { + + a.push( this.getFloat64() ); + + } + + return a; + + }, + + getArrayBuffer: function ( size ) { + + var value = this.dv.buffer.slice( this.offset, this.offset + size ); + this.offset += size; + return value; + + }, + + getChar: function () { + + return String.fromCharCode( this.getUint8() ); + + }, + + getString: function ( size ) { + + var s = THREE.LoaderUtils.decodeText( this.getUint8Array( size ) ); + + this.skip( size ); + + return s; + + } + + } ); + + // FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) + // and BinaryParser( FBX Binary format) + function FBXTree() {} + + Object.assign( FBXTree.prototype, { + + add: function ( key, val ) { + + this[ key ] = val; + + }, + + } ); + + function isFbxFormatBinary( buffer ) { + + var 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', '\\', '\\' ]; + + var cursor = 0; + + function read( offset ) { + + var 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 ] ) { + + return false; + + } - /** - * Parses Color property from FBXTree. Property is given as .value.x, .value.y, etc. - * @param {FBXVector3} property - Property to parse as Color. - * @returns {THREE.Color} - */ - function parseColor(property) { + } - return new THREE.Color().fromArray(property.value); + return true; - } + } - function parseMatrixArray(floatString) { + function getFbxVersion( text ) { - return new THREE.Matrix4().fromArray(parseFloatArray(floatString)); + var versionRegExp = /FBXVersion: (\d+)/; + var match = text.match( versionRegExp ); + if ( match ) { - } + var version = parseInt( match[ 1 ] ); + return version; - /** - * Converts ArrayBuffer to String. - * @param {ArrayBuffer} buffer - * @param {number} from - * @param {number} to - * @returns {String} - */ - function convertArrayBufferToString(buffer, from, to) { + } + throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); - if (from === undefined) from = 0; - if (to === undefined) to = buffer.byteLength; + } - var array = new Uint8Array(buffer, from, to); + // Converts FBX ticks into real time seconds. + function convertFBXTimeToSeconds( time ) { - if (window.TextDecoder !== undefined) { + return time / 46186158000; - return new TextDecoder().decode(array); + } - } - var s = ''; + // Parses comma separated list of numbers and returns them an array. + // Used internally by the TextParser + function parseNumberArray( value ) { - for (var i = 0, il = array.length; i < il; i++) { + var array = value.split( ',' ).map( function ( val ) { - s += String.fromCharCode(array[i]); + return parseFloat( val ); - } + } ); - return s; + return array; - } + } - /** - * Converts number from degrees into radians. - * @param {number} value - * @returns {number} - */ - function degreeToRadian(value) { + function parseColor( property ) { - return value * DEG2RAD; + var color = new THREE.Color(); - } + if ( property.type === 'Color' ) { - var DEG2RAD = Math.PI / 180; + return color.setScalar( property.value ); - // + } - function findIndex(array, func) { + return color.fromArray( property.value ); - for (var i = 0, l = array.length; i < l; i++) { + } - if (func(array[i])) return i; + // Converts ArrayBuffer to String. + function convertArrayBufferToString( buffer, from, to ) { - } + if ( from === undefined ) from = 0; + if ( to === undefined ) to = buffer.byteLength; - return -1; + return THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); - } + } - function append(a, b) { + function append( a, b ) { - for (var i = 0, j = a.length, l = b.length; i < l; i++ , j++) { + for ( var i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { - a[j] = b[i]; + a[ j ] = b[ i ]; - } + } - } + } - function slice(a, b, from, to) { + function slice( a, b, from, to ) { - for (var i = from, j = 0; i < to; i++ , j++) { + for ( var i = from, j = 0; i < to; i ++, j ++ ) { - a[j] = b[i]; + a[ j ] = b[ i ]; - } + } - return a; + return a; - } + } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6ae9a71..1ece185 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,8 @@ "bugs": { "url": "https://github.com/ckddbs/three-fbx-loader/issues" }, - "homepage": "https://github.com/ckddbs/three-fbx-loader#readme" -} \ No newline at end of file + "homepage": "https://github.com/ckddbs/three-fbx-loader#readme", + "dependencies": { + "pako": "^1.0.6" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..3805f8e --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +pako@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"