Browse Source

move all animation parsing to animation parser

Lewy Blue 7 years ago
parent
commit
9a970e016f
1 changed files with 884 additions and 879 deletions
  1. 884 879
      examples/js/loaders/FBXLoader.js

+ 884 - 879
examples/js/loaders/FBXLoader.js

@@ -763,7 +763,6 @@ THREE.FBXLoader = ( function () {
 
 		},
 
-
 		// create the main THREE.Group() to be returned by the loader
 		parseScene: function ( deformers, geometryMap, materialMap ) {
 
@@ -798,20 +797,23 @@ THREE.FBXLoader = ( function () {
 			} );
 
 			this.bindSkeleton( deformers.skeletons, geometryMap, modelMap );
-			this.addAnimations( sceneGraph );
 
 			this.createAmbientLight( sceneGraph );
 
 			this.setupMorphMaterials( sceneGraph );
 
+			var animations = new AnimationParser().parse( sceneGraph );
+
 			// if all the models where already combined in a single group, just return that
 			if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) {
 
-				sceneGraph.children[ 0 ].animations = sceneGraph.animations;
+				sceneGraph.children[ 0 ].animations = animations;
 				return sceneGraph.children[ 0 ];
 
 			}
 
+			sceneGraph.animations = animations;
+
 			return sceneGraph;
 
 		},
@@ -1344,1476 +1346,1479 @@ THREE.FBXLoader = ( function () {
 
 		},
 
-		// take raw animation clips and turn them into three.js animation clips
-		addAnimations: function ( sceneGraph ) {
-
-			sceneGraph.animations = [];
-
-			var rawClips = new AnimationParser( connections ).parse();
+		// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
+		createAmbientLight: function ( sceneGraph ) {
 
-			if ( rawClips === undefined ) return;
+			if ( 'GlobalSettings' in FBXTree && 'AmbientColor' in FBXTree.GlobalSettings ) {
 
-			for ( var key in rawClips ) {
+				var ambientColor = FBXTree.GlobalSettings.AmbientColor.value;
+				var r = ambientColor[ 0 ];
+				var g = ambientColor[ 1 ];
+				var b = ambientColor[ 2 ];
 
-				var rawClip = rawClips[ key ];
+				if ( r !== 0 || g !== 0 || b !== 0 ) {
 
-				var clip = this.addClip( rawClip, sceneGraph );
+					var color = new THREE.Color( r, g, b );
+					sceneGraph.add( new THREE.AmbientLight( color, 1 ) );
 
-				sceneGraph.animations.push( clip );
+				}
 
 			}
 
 		},
 
-		addClip: function ( rawClip, sceneGraph ) {
-
-			var tracks = [];
+		setupMorphMaterials: function ( sceneGraph ) {
 
-			var self = this;
-			rawClip.layer.forEach( function ( rawTracks ) {
+			sceneGraph.traverse( function ( child ) {
 
-				tracks = tracks.concat( self.generateTracks( rawTracks, sceneGraph ) );
+				if ( child.isMesh ) {
 
-			} );
+					if ( child.geometry.morphAttributes.position || child.geometry.morphAttributes.normal ) {
 
-			return new THREE.AnimationClip( rawClip.name, - 1, tracks );
+						var uuid = child.uuid;
+						var matUuid = child.material.uuid;
 
-		},
+						// if a geometry has morph targets, it cannot share the material with other geometries
+						var sharedMat = false;
 
-		generateTracks: function ( rawTracks, sceneGraph ) {
+						sceneGraph.traverse( function ( child ) {
 
-			var tracks = [];
+							if ( child.isMesh ) {
 
-			var initialPosition = new THREE.Vector3();
-			var initialRotation = new THREE.Quaternion();
-			var initialScale = new THREE.Vector3();
+								if ( child.material.uuid === matUuid && child.uuid !== uuid ) sharedMat = true;
 
-			if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale );
+							}
 
-			initialPosition = initialPosition.toArray();
-			initialRotation = new THREE.Euler().setFromQuaternion( initialRotation ).toArray(); // todo: euler order
-			initialScale = initialScale.toArray();
+						} );
 
-			if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) {
+						if ( sharedMat === true ) child.material = child.material.clone();
 
-				var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' );
-				if ( positionTrack !== undefined ) tracks.push( positionTrack );
+						child.material.morphTargets = true;
 
-			}
+					}
 
-			if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {
+				}
 
-				var rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotations, rawTracks.postRotations );
-				if ( rotationTrack !== undefined ) tracks.push( rotationTrack );
+			} );
 
-			}
+		},
 
-			if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) {
+	};
 
-				var scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' );
-				if ( scaleTrack !== undefined ) tracks.push( scaleTrack );
+	// parse Geometry data from FBXTree and return map of BufferGeometries
+	function GeometryParser() {}
 
-			}
+	GeometryParser.prototype = {
 
-			if ( rawTracks.DeformPercent !== undefined ) {
+		constructor: GeometryParser,
 
-				var morphTrack = this.generateMorphTrack( rawTracks, sceneGraph );
-				if ( morphTrack !== undefined ) tracks.push( morphTrack );
+		// Parse nodes in FBXTree.Objects.Geometry
+		parse: function ( deformers ) {
 
-			}
+			var geometryMap = new Map();
 
-			return tracks;
+			if ( 'Geometry' in FBXTree.Objects ) {
 
-		},
+				var geoNodes = FBXTree.Objects.Geometry;
 
-		generateVectorTrack: function ( modelName, curves, initialValue, type ) {
+				for ( var nodeID in geoNodes ) {
 
-			var times = this.getTimesForAllAxes( curves );
-			var values = this.getKeyframeTrackValues( times, curves, initialValue );
+					var relationships = connections.get( parseInt( nodeID ) );
+					var geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers );
 
-			return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values );
+					geometryMap.set( parseInt( nodeID ), geo );
 
-		},
+				}
 
-		generateRotationTrack: function ( modelName, curves, initialValue, preRotations, postRotations ) {
+			}
 
-			if ( curves.x !== undefined ) {
+			return geometryMap;
 
-				this.interpolateRotations( curves.x );
-				curves.x.values = curves.x.values.map( THREE.Math.degToRad );
+		},
 
-			}
-			if ( curves.y !== undefined ) {
+		// Parse single node in FBXTree.Objects.Geometry
+		parseGeometry: function ( relationships, geoNode, deformers ) {
 
-				this.interpolateRotations( curves.y );
-				curves.y.values = curves.y.values.map( THREE.Math.degToRad );
+			switch ( geoNode.attrType ) {
 
-			}
-			if ( curves.z !== undefined ) {
+				case 'Mesh':
+					return this.parseMeshGeometry( relationships, geoNode, deformers );
+					break;
 
-				this.interpolateRotations( curves.z );
-				curves.z.values = curves.z.values.map( THREE.Math.degToRad );
+				case 'NurbsCurve':
+					return this.parseNurbsGeometry( geoNode );
+					break;
 
 			}
 
-			var times = this.getTimesForAllAxes( curves );
-			var values = this.getKeyframeTrackValues( times, curves, initialValue );
-
-			if ( preRotations !== undefined ) {
+		},
 
-				preRotations = preRotations.map( THREE.Math.degToRad );
-				preRotations.push( 'ZYX' );
+		// Parse single node mesh geometry in FBXTree.Objects.Geometry
+		parseMeshGeometry: function ( relationships, geoNode, deformers ) {
 
-				preRotations = new THREE.Euler().fromArray( preRotations );
-				preRotations = new THREE.Quaternion().setFromEuler( preRotations );
+			var skeletons = deformers.skeletons;
+			var morphTargets = deformers.morphTargets;
 
-			}
+			var modelNodes = relationships.parents.map( function ( parent ) {
 
-			if ( postRotations !== undefined ) {
+				return FBXTree.Objects.Model[ parent.ID ];
 
-				postRotations = postRotations.map( THREE.Math.degToRad );
-				postRotations.push( 'ZYX' );
+			} );
 
-				postRotations = new THREE.Euler().fromArray( postRotations );
-				postRotations = new THREE.Quaternion().setFromEuler( postRotations ).inverse();
+			// don't create geometry if it is not associated with any models
+			if ( modelNodes.length === 0 ) return;
 
-			}
+			var skeleton = relationships.children.reduce( function ( skeleton, child ) {
 
-			var quaternion = new THREE.Quaternion();
-			var euler = new THREE.Euler();
+				if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ];
 
-			var quaternionValues = [];
+				return skeleton;
 
-			for ( var i = 0; i < values.length; i += 3 ) {
+			}, null );
 
-				euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' );
+			var morphTarget = relationships.children.reduce( function ( morphTarget, child ) {
 
-				quaternion.setFromEuler( euler );
+				if ( morphTargets[ child.ID ] !== undefined ) morphTarget = morphTargets[ child.ID ];
 
-				if ( preRotations !== undefined ) quaternion.premultiply( preRotations );
-				if ( postRotations !== undefined ) quaternion.multiply( postRotations );
+				return morphTarget;
 
-				quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );
+			}, null );
 
-			}
+			// 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 ) { }
 
-			return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );
+			// For now just assume one model and get the preRotations from that
+			var modelNode = modelNodes[ 0 ];
 
-		},
+			var transformData = {};
 
-		generateMorphTrack: function ( rawTracks, sceneGraph ) {
+			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = modelNode.RotationOrder.value;
+			if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value;
+			if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value;
+			if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value;
 
-			var curves = rawTracks.DeformPercent.curves.morph;
-			var values = curves.values.map( function ( val ) {
+			var transform = generateTransform( transformData );
 
-				return val / 100;
+			return this.genGeometry( geoNode, skeleton, morphTarget, transform );
 
-			} );
+		},
 
-			var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ];
+		// Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry
+		genGeometry: function ( geoNode, skeleton, morphTarget, preTransform ) {
 
-			return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values );
+			var geo = new THREE.BufferGeometry();
+			if ( geoNode.attrName ) geo.name = geoNode.attrName;
 
-		},
+			var geoInfo = this.parseGeoNode( geoNode, skeleton );
+			var buffers = this.genBuffers( geoInfo );
 
-		// For all animated objects, times are defined separately for each axis
-		// Here we'll combine the times into one sorted array without duplicates
-		getTimesForAllAxes: function ( curves ) {
+			var positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 );
 
-			var times = [];
+			preTransform.applyToBufferAttribute( positionAttribute );
 
-			// 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 );
+			geo.addAttribute( 'position', positionAttribute );
 
-			// then sort them and remove duplicates
-			times = times.sort( function ( a, b ) {
+			if ( buffers.colors.length > 0 ) {
 
-				return a - b;
+				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) );
 
-			} ).filter( function ( elem, index, array ) {
+			}
 
-				return array.indexOf( elem ) == index;
+			if ( skeleton ) {
 
-			} );
+				geo.addAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) );
 
-			return times;
+				geo.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) );
 
-		},
+				// used later to bind the skeleton to the model
+				geo.FBX_Deformer = skeleton;
 
-		getKeyframeTrackValues: function ( times, curves, initialValue ) {
+			}
 
-			var prevValue = initialValue;
+			if ( buffers.normal.length > 0 ) {
 
-			var values = [];
+				var normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 );
 
-			var xIndex = - 1;
-			var yIndex = - 1;
-			var zIndex = - 1;
+				var normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform );
+				normalMatrix.applyToBufferAttribute( normalAttribute );
 
-			times.forEach( function ( time ) {
+				geo.addAttribute( 'normal', normalAttribute );
 
-				if ( curves.x ) xIndex = curves.x.times.indexOf( time );
-				if ( curves.y ) yIndex = curves.y.times.indexOf( time );
-				if ( curves.z ) zIndex = curves.z.times.indexOf( time );
+			}
 
-				// if there is an x value defined for this frame, use that
-				if ( xIndex !== - 1 ) {
+			buffers.uvs.forEach( function ( uvBuffer, i ) {
 
-					var xValue = curves.x.values[ xIndex ];
-					values.push( xValue );
-					prevValue[ 0 ] = xValue;
+				// subsequent uv buffers are called 'uv1', 'uv2', ...
+				var name = 'uv' + ( i + 1 ).toString();
 
-				} else {
+				// the first uv buffer is just called 'uv'
+				if ( i === 0 ) {
 
-					// otherwise use the x value from the previous frame
-					values.push( prevValue[ 0 ] );
+					name = 'uv';
 
 				}
 
-				if ( yIndex !== - 1 ) {
+				geo.addAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) );
 
-					var yValue = curves.y.values[ yIndex ];
-					values.push( yValue );
-					prevValue[ 1 ] = yValue;
+			} );
 
-				} else {
+			if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
 
-					values.push( prevValue[ 1 ] );
+				// Convert the material indices of each vertex into rendering groups on the geometry.
+				var prevMaterialIndex = buffers.materialIndex[ 0 ];
+				var startIndex = 0;
 
-				}
+				buffers.materialIndex.forEach( function ( currentIndex, i ) {
 
-				if ( zIndex !== - 1 ) {
+					if ( currentIndex !== prevMaterialIndex ) {
 
-					var zValue = curves.z.values[ zIndex ];
-					values.push( zValue );
-					prevValue[ 2 ] = zValue;
+						geo.addGroup( startIndex, i - startIndex, prevMaterialIndex );
 
-				} else {
+						prevMaterialIndex = currentIndex;
+						startIndex = i;
 
-					values.push( prevValue[ 2 ] );
+					}
 
-				}
+				} );
 
-			} );
+				// the loop above doesn't add the last group, do that here.
+				if ( geo.groups.length > 0 ) {
 
-			return values;
+					var lastGroup = geo.groups[ geo.groups.length - 1 ];
+					var lastIndex = lastGroup.start + lastGroup.count;
 
-		},
+					if ( lastIndex !== buffers.materialIndex.length ) {
 
-		// Rotations are defined as Euler angles which can have values  of any size
-		// These will be converted to quaternions which don't support values greater than
-		// PI, so we'll interpolate large rotations
-		interpolateRotations: function ( curve ) {
+						geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex );
 
-			for ( var i = 1; i < curve.values.length; i ++ ) {
+					}
 
-				var initialValue = curve.values[ i - 1 ];
-				var valuesSpan = curve.values[ i ] - initialValue;
+				}
 
-				var absoluteSpan = Math.abs( valuesSpan );
+				// case where there are multiple materials but the whole geometry is only
+				// using one of them
+				if ( geo.groups.length === 0 ) {
 
-				if ( absoluteSpan >= 180 ) {
+					geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] );
 
-					var numSubIntervals = absoluteSpan / 180;
+				}
 
-					var step = valuesSpan / numSubIntervals;
-					var nextValue = initialValue + step;
+			}
 
-					var initialTime = curve.times[ i - 1 ];
-					var timeSpan = curve.times[ i ] - initialTime;
-					var interval = timeSpan / numSubIntervals;
-					var nextTime = initialTime + interval;
+			this.addMorphTargets( geo, geoNode, morphTarget, preTransform );
 
-					var interpolatedTimes = [];
-					var interpolatedValues = [];
+			return geo;
 
-					while ( nextTime < curve.times[ i ] ) {
+		},
 
-						interpolatedTimes.push( nextTime );
-						nextTime += interval;
+		parseGeoNode: function ( geoNode, skeleton ) {
 
-						interpolatedValues.push( nextValue );
-						nextValue += step;
+			var geoInfo = {};
 
-					}
+			geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : [];
+			geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : [];
 
-					curve.times = inject( curve.times, i, interpolatedTimes );
-					curve.values = inject( curve.values, i, interpolatedValues );
+			if ( geoNode.LayerElementColor ) {
 
-				}
+				geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] );
 
 			}
 
-		},
+			if ( geoNode.LayerElementMaterial ) {
 
-		// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
-		createAmbientLight: function ( sceneGraph ) {
+				geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] );
 
-			if ( 'GlobalSettings' in FBXTree && 'AmbientColor' in FBXTree.GlobalSettings ) {
+			}
 
-				var ambientColor = FBXTree.GlobalSettings.AmbientColor.value;
-				var r = ambientColor[ 0 ];
-				var g = ambientColor[ 1 ];
-				var b = ambientColor[ 2 ];
+			if ( geoNode.LayerElementNormal ) {
 
-				if ( r !== 0 || g !== 0 || b !== 0 ) {
+				geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] );
 
-					var color = new THREE.Color( r, g, b );
-					sceneGraph.add( new THREE.AmbientLight( color, 1 ) );
+			}
 
-				}
+			if ( geoNode.LayerElementUV ) {
 
-			}
+				geoInfo.uv = [];
 
-		},
+				var i = 0;
+				while ( geoNode.LayerElementUV[ i ] ) {
 
-		setupMorphMaterials: function ( sceneGraph ) {
+					geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) );
+					i ++;
 
-			sceneGraph.traverse( function ( child ) {
+				}
 
-				if ( child.isMesh ) {
+			}
 
-					if ( child.geometry.morphAttributes.position || child.geometry.morphAttributes.normal ) {
+			geoInfo.weightTable = {};
 
-						var uuid = child.uuid;
-						var matUuid = child.material.uuid;
+			if ( skeleton !== null ) {
 
-						// if a geometry has morph targets, it cannot share the material with other geometries
-						var sharedMat = false;
+				geoInfo.skeleton = skeleton;
 
-						sceneGraph.traverse( function ( child ) {
+				skeleton.rawBones.forEach( function ( rawBone, i ) {
 
-							if ( child.isMesh ) {
+					// loop over the bone's vertex indices and weights
+					rawBone.indices.forEach( function ( index, j ) {
 
-								if ( child.material.uuid === matUuid && child.uuid !== uuid ) sharedMat = true;
+						if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = [];
 
-							}
+						geoInfo.weightTable[ index ].push( {
 
-						} );
+							id: i,
+							weight: rawBone.weights[ j ],
 
-						if ( sharedMat === true ) child.material = child.material.clone();
+						} );
 
-						child.material.morphTargets = true;
+					} );
 
-					}
+				} );
 
-				}
+			}
 
-			} );
+			return geoInfo;
 
 		},
 
-	};
+		genBuffers: function ( geoInfo ) {
 
-	// parse Geometry data from FBXTree and return map of BufferGeometries and nurbs curves
-	function GeometryParser() {}
+			var buffers = {
+				vertex: [],
+				normal: [],
+				colors: [],
+				uvs: [],
+				materialIndex: [],
+				vertexWeights: [],
+				weightsIndices: [],
+			};
 
-	GeometryParser.prototype = {
+			var polygonIndex = 0;
+			var faceLength = 0;
+			var displayedWeightsWarning = false;
 
-		constructor: GeometryParser,
+			// these will hold data for a single face
+			var facePositionIndexes = [];
+			var faceNormals = [];
+			var faceColors = [];
+			var faceUVs = [];
+			var faceWeights = [];
+			var faceWeightIndices = [];
 
-		// Parse nodes in FBXTree.Objects.Geometry
-		parse: function ( deformers ) {
+			var self = this;
+			geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) {
 
-			var geometryMap = new Map();
+				var endOfFace = false;
 
-			if ( 'Geometry' in FBXTree.Objects ) {
+				// Face index and vertex index arrays are combined in a single array
+				// A cube with quad faces looks like this:
+				// PolygonVertexIndex: *24 {
+				//  a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5
+				//  }
+				// Negative numbers mark the end of a face - first face here is 0, 1, 3, -3
+				// to find index of last vertex bit shift the index: ^ - 1
+				if ( vertexIndex < 0 ) {
 
-				var geoNodes = FBXTree.Objects.Geometry;
+					vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1
+					endOfFace = true;
 
-				for ( var nodeID in geoNodes ) {
+				}
 
-					var relationships = connections.get( parseInt( nodeID ) );
-					var geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers );
+				var weightIndices = [];
+				var weights = [];
 
-					geometryMap.set( parseInt( nodeID ), geo );
+				facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 );
 
-				}
+				if ( geoInfo.color ) {
 
-			}
+					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color );
 
-			return geometryMap;
+					faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] );
 
-		},
+				}
 
-		// Parse single node in FBXTree.Objects.Geometry
-		parseGeometry: function ( relationships, geoNode, deformers ) {
+				if ( geoInfo.skeleton ) {
 
-			switch ( geoNode.attrType ) {
+					if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) {
 
-				case 'Mesh':
-					return this.parseMeshGeometry( relationships, geoNode, deformers );
-					break;
+						geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) {
 
-				case 'NurbsCurve':
-					return this.parseNurbsGeometry( geoNode );
-					break;
+							weights.push( wt.weight );
+							weightIndices.push( wt.id );
 
-			}
+						} );
 
-		},
 
-		// Parse single node mesh geometry in FBXTree.Objects.Geometry
-		parseMeshGeometry: function ( relationships, geoNode, deformers ) {
+					}
 
-			var skeletons = deformers.skeletons;
-			var morphTargets = deformers.morphTargets;
+					if ( weights.length > 4 ) {
 
-			var modelNodes = relationships.parents.map( function ( parent ) {
+						if ( ! displayedWeightsWarning ) {
 
-				return FBXTree.Objects.Model[ parent.ID ];
+							console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' );
+							displayedWeightsWarning = true;
 
-			} );
+						}
 
-			// don't create geometry if it is not associated with any models
-			if ( modelNodes.length === 0 ) return;
+						var wIndex = [ 0, 0, 0, 0 ];
+						var Weight = [ 0, 0, 0, 0 ];
 
-			var skeleton = relationships.children.reduce( function ( skeleton, child ) {
+						weights.forEach( function ( weight, weightIndex ) {
 
-				if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ];
+							var currentWeight = weight;
+							var currentIndex = weightIndices[ weightIndex ];
 
-				return skeleton;
+							Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) {
 
-			}, null );
+								if ( currentWeight > comparedWeight ) {
 
-			var morphTarget = relationships.children.reduce( function ( morphTarget, child ) {
+									comparedWeightArray[ comparedWeightIndex ] = currentWeight;
+									currentWeight = comparedWeight;
 
-				if ( morphTargets[ child.ID ] !== undefined ) morphTarget = morphTargets[ child.ID ];
+									var tmp = wIndex[ comparedWeightIndex ];
+									wIndex[ comparedWeightIndex ] = currentIndex;
+									currentIndex = tmp;
 
-				return morphTarget;
+								}
 
-			}, null );
+							} );
 
-			// 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 ) { }
+						} );
 
-			// For now just assume one model and get the preRotations from that
-			var modelNode = modelNodes[ 0 ];
+						weightIndices = wIndex;
+						weights = Weight;
 
-			var transformData = {};
+					}
 
-			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = modelNode.RotationOrder.value;
-			if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value;
-			if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value;
-			if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value;
+					// if the weight array is shorter than 4 pad with 0s
+					while ( weights.length < 4 ) {
 
-			var transform = generateTransform( transformData );
+						weights.push( 0 );
+						weightIndices.push( 0 );
 
-			return this.genGeometry( geoNode, skeleton, morphTarget, transform );
+					}
 
-		},
+					for ( var i = 0; i < 4; ++ i ) {
 
-		// Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry
-		genGeometry: function ( geoNode, skeleton, morphTarget, preTransform ) {
+						faceWeights.push( weights[ i ] );
+						faceWeightIndices.push( weightIndices[ i ] );
 
-			var geo = new THREE.BufferGeometry();
-			if ( geoNode.attrName ) geo.name = geoNode.attrName;
+					}
 
-			var geoInfo = this.parseGeoNode( geoNode, skeleton );
-			var buffers = this.genBuffers( geoInfo );
+				}
 
-			var positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 );
+				if ( geoInfo.normal ) {
 
-			preTransform.applyToBufferAttribute( positionAttribute );
+					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal );
 
-			geo.addAttribute( 'position', positionAttribute );
+					faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] );
 
-			if ( buffers.colors.length > 0 ) {
+				}
 
-				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) );
+				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
 
-			}
+					var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ];
 
-			if ( skeleton ) {
+				}
 
-				geo.addAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) );
+				if ( geoInfo.uv ) {
 
-				geo.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) );
+					geoInfo.uv.forEach( function ( uv, i ) {
 
-				// used later to bind the skeleton to the model
-				geo.FBX_Deformer = skeleton;
+						var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv );
 
-			}
+						if ( faceUVs[ i ] === undefined ) {
 
-			if ( buffers.normal.length > 0 ) {
+							faceUVs[ i ] = [];
 
-				var normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 );
+						}
 
-				var normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform );
-				normalMatrix.applyToBufferAttribute( normalAttribute );
+						faceUVs[ i ].push( data[ 0 ] );
+						faceUVs[ i ].push( data[ 1 ] );
 
-				geo.addAttribute( 'normal', normalAttribute );
+					} );
 
-			}
+				}
 
-			buffers.uvs.forEach( function ( uvBuffer, i ) {
+				faceLength ++;
 
-				// subsequent uv buffers are called 'uv1', 'uv2', ...
-				var name = 'uv' + ( i + 1 ).toString();
+				if ( endOfFace ) {
 
-				// the first uv buffer is just called 'uv'
-				if ( i === 0 ) {
+					self.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength );
 
-					name = 'uv';
+					polygonIndex ++;
+					faceLength = 0;
 
-				}
+					// reset arrays for the next face
+					facePositionIndexes = [];
+					faceNormals = [];
+					faceColors = [];
+					faceUVs = [];
+					faceWeights = [];
+					faceWeightIndices = [];
 
-				geo.addAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) );
+				}
 
 			} );
 
-			if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
-
-				// Convert the material indices of each vertex into rendering groups on the geometry.
-				var prevMaterialIndex = buffers.materialIndex[ 0 ];
-				var startIndex = 0;
-
-				buffers.materialIndex.forEach( function ( currentIndex, i ) {
+			return buffers;
 
-					if ( currentIndex !== prevMaterialIndex ) {
+		},
 
-						geo.addGroup( startIndex, i - startIndex, prevMaterialIndex );
+		// Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris
+		genFace: function ( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) {
 
-						prevMaterialIndex = currentIndex;
-						startIndex = i;
+			for ( var i = 2; i < faceLength; i ++ ) {
 
-					}
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] );
 
-				} );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] );
 
-				// the loop above doesn't add the last group, do that here.
-				if ( geo.groups.length > 0 ) {
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] );
+				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] );
 
-					var lastGroup = geo.groups[ geo.groups.length - 1 ];
-					var lastIndex = lastGroup.start + lastGroup.count;
+				if ( geoInfo.skeleton ) {
 
-					if ( lastIndex !== buffers.materialIndex.length ) {
+					buffers.vertexWeights.push( faceWeights[ 0 ] );
+					buffers.vertexWeights.push( faceWeights[ 1 ] );
+					buffers.vertexWeights.push( faceWeights[ 2 ] );
+					buffers.vertexWeights.push( faceWeights[ 3 ] );
 
-						geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex );
+					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] );
+					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] );
+					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] );
+					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] );
 
-					}
+					buffers.vertexWeights.push( faceWeights[ i * 4 ] );
+					buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] );
+					buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] );
+					buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] );
 
-				}
+					buffers.weightsIndices.push( faceWeightIndices[ 0 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ 1 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ 2 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ 3 ] );
 
-				// case where there are multiple materials but the whole geometry is only
-				// using one of them
-				if ( geo.groups.length === 0 ) {
+					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] );
 
-					geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] );
+					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] );
 
 				}
 
-			}
+				if ( geoInfo.color ) {
 
-			this.addMorphTargets( geo, geoNode, morphTarget, preTransform );
+					buffers.colors.push( faceColors[ 0 ] );
+					buffers.colors.push( faceColors[ 1 ] );
+					buffers.colors.push( faceColors[ 2 ] );
 
-			return geo;
+					buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] );
+					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] );
+					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] );
 
-		},
+					buffers.colors.push( faceColors[ i * 3 ] );
+					buffers.colors.push( faceColors[ i * 3 + 1 ] );
+					buffers.colors.push( faceColors[ i * 3 + 2 ] );
 
-		parseGeoNode: function ( geoNode, skeleton ) {
+				}
 
-			var geoInfo = {};
+				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
 
-			geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : [];
-			geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : [];
+					buffers.materialIndex.push( materialIndex );
+					buffers.materialIndex.push( materialIndex );
+					buffers.materialIndex.push( materialIndex );
 
-			if ( geoNode.LayerElementColor ) {
+				}
 
-				geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] );
+				if ( geoInfo.normal ) {
 
-			}
+					buffers.normal.push( faceNormals[ 0 ] );
+					buffers.normal.push( faceNormals[ 1 ] );
+					buffers.normal.push( faceNormals[ 2 ] );
 
-			if ( geoNode.LayerElementMaterial ) {
+					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] );
+					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] );
+					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] );
 
-				geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] );
+					buffers.normal.push( faceNormals[ i * 3 ] );
+					buffers.normal.push( faceNormals[ i * 3 + 1 ] );
+					buffers.normal.push( faceNormals[ i * 3 + 2 ] );
 
-			}
+				}
 
-			if ( geoNode.LayerElementNormal ) {
+				if ( geoInfo.uv ) {
 
-				geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] );
+					geoInfo.uv.forEach( function ( uv, j ) {
 
-			}
+						if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = [];
 
-			if ( geoNode.LayerElementUV ) {
+						buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] );
+						buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] );
 
-				geoInfo.uv = [];
+						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] );
+						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] );
 
-				var i = 0;
-				while ( geoNode.LayerElementUV[ i ] ) {
+						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] );
+						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] );
 
-					geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) );
-					i ++;
+					} );
 
 				}
 
 			}
 
-			geoInfo.weightTable = {};
+		},
 
-			if ( skeleton !== null ) {
+		addMorphTargets: function ( parentGeo, parentGeoNode, morphTarget, preTransform ) {
 
-				geoInfo.skeleton = skeleton;
+			if ( morphTarget === null ) return;
 
-				skeleton.rawBones.forEach( function ( rawBone, i ) {
+			parentGeo.morphAttributes.position = [];
+			parentGeo.morphAttributes.normal = [];
 
-					// loop over the bone's vertex indices and weights
-					rawBone.indices.forEach( function ( index, j ) {
+			var self = this;
+			morphTarget.rawTargets.forEach( function ( rawTarget ) {
 
-						if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = [];
+				var morphGeoNode = FBXTree.Objects.Geometry[ rawTarget.geoID ];
 
-						geoInfo.weightTable[ index ].push( {
+				if ( morphGeoNode !== undefined ) {
 
-							id: i,
-							weight: rawBone.weights[ j ],
+					self.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform );
 
-						} );
+				}
 
-					} );
+			} );
 
-				} );
+		},
 
-			}
+		// a morph geometry node is similar to a standard  node, and the node is also contained
+		// in FBXTree.Objects.Geometry, however it can only have attributes for position, normal
+		// and a special attribute Index defining which vertices of the original geometry are affected
+		// Normal and position attributes only have data for the vertices that are affected by the morph
+		genMorphGeometry: function ( parentGeo, parentGeoNode, morphGeoNode, preTransform ) {
 
-			return geoInfo;
+			var morphGeo = new THREE.BufferGeometry();
+			if ( morphGeoNode.attrName ) morphGeo.name = morphGeoNode.attrName;
 
-		},
+			var vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : [];
 
-		genBuffers: function ( geoInfo ) {
+			// make a copy of the parent's vertex positions
+			var vertexPositions = ( parentGeoNode.Vertices !== undefined ) ? parentGeoNode.Vertices.a.slice() : [];
 
-			var buffers = {
-				vertex: [],
-				normal: [],
-				colors: [],
-				uvs: [],
-				materialIndex: [],
-				vertexWeights: [],
-				weightsIndices: [],
-			};
+			var morphPositions = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : [];
+			var indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : [];
 
-			var polygonIndex = 0;
-			var faceLength = 0;
-			var displayedWeightsWarning = false;
+			for ( var i = 0; i < indices.length; i ++ ) {
 
-			// these will hold data for a single face
-			var facePositionIndexes = [];
-			var faceNormals = [];
-			var faceColors = [];
-			var faceUVs = [];
-			var faceWeights = [];
-			var faceWeightIndices = [];
+				var morphIndex = indices[ i ] * 3;
 
-			var self = this;
-			geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) {
+				// FBX format uses blend shapes rather than morph targets. This can be converted
+				// by additively combining the blend shape positions with the original geometry's positions
+				vertexPositions[ morphIndex ] += morphPositions[ i * 3 ];
+				vertexPositions[ morphIndex + 1 ] += morphPositions[ i * 3 + 1 ];
+				vertexPositions[ morphIndex + 2 ] += morphPositions[ i * 3 + 2 ];
 
-				var endOfFace = false;
+			}
 
-				// Face index and vertex index arrays are combined in a single array
-				// A cube with quad faces looks like this:
-				// PolygonVertexIndex: *24 {
-				//  a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5
-				//  }
-				// Negative numbers mark the end of a face - first face here is 0, 1, 3, -3
-				// to find index of last vertex bit shift the index: ^ - 1
-				if ( vertexIndex < 0 ) {
+			// TODO: add morph normal support
+			var morphGeoInfo = {
+				vertexIndices: vertexIndices,
+				vertexPositions: vertexPositions,
+			};
 
-					vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1
-					endOfFace = true;
+			var morphBuffers = this.genBuffers( morphGeoInfo );
 
-				}
+			var positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 );
+			positionAttribute.name = morphGeoNode.attrName;
 
-				var weightIndices = [];
-				var weights = [];
+			preTransform.applyToBufferAttribute( positionAttribute );
 
-				facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 );
+			parentGeo.morphAttributes.position.push( positionAttribute );
 
-				if ( geoInfo.color ) {
+		},
 
-					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color );
+		// Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists
+		parseNormals: function ( NormalNode ) {
 
-					faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] );
+			var mappingType = NormalNode.MappingInformationType;
+			var referenceType = NormalNode.ReferenceInformationType;
+			var buffer = NormalNode.Normals.a;
+			var indexBuffer = [];
+			if ( referenceType === 'IndexToDirect' ) {
 
-				}
+				if ( 'NormalIndex' in NormalNode ) {
 
-				if ( geoInfo.skeleton ) {
+					indexBuffer = NormalNode.NormalIndex.a;
 
-					if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) {
+				} else if ( 'NormalsIndex' in NormalNode ) {
 
-						geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) {
+					indexBuffer = NormalNode.NormalsIndex.a;
 
-							weights.push( wt.weight );
-							weightIndices.push( wt.id );
+				}
 
-						} );
+			}
 
+			return {
+				dataSize: 3,
+				buffer: buffer,
+				indices: indexBuffer,
+				mappingType: mappingType,
+				referenceType: referenceType
+			};
 
-					}
+		},
 
-					if ( weights.length > 4 ) {
+		// Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists
+		parseUVs: function ( UVNode ) {
 
-						if ( ! displayedWeightsWarning ) {
+			var mappingType = UVNode.MappingInformationType;
+			var referenceType = UVNode.ReferenceInformationType;
+			var buffer = UVNode.UV.a;
+			var indexBuffer = [];
+			if ( referenceType === 'IndexToDirect' ) {
 
-							console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' );
-							displayedWeightsWarning = true;
+				indexBuffer = UVNode.UVIndex.a;
 
-						}
+			}
 
-						var wIndex = [ 0, 0, 0, 0 ];
-						var Weight = [ 0, 0, 0, 0 ];
+			return {
+				dataSize: 2,
+				buffer: buffer,
+				indices: indexBuffer,
+				mappingType: mappingType,
+				referenceType: referenceType
+			};
 
-						weights.forEach( function ( weight, weightIndex ) {
+		},
 
-							var currentWeight = weight;
-							var currentIndex = weightIndices[ weightIndex ];
+		// Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists
+		parseVertexColors: function ( ColorNode ) {
 
-							Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) {
+			var mappingType = ColorNode.MappingInformationType;
+			var referenceType = ColorNode.ReferenceInformationType;
+			var buffer = ColorNode.Colors.a;
+			var indexBuffer = [];
+			if ( referenceType === 'IndexToDirect' ) {
 
-								if ( currentWeight > comparedWeight ) {
+				indexBuffer = ColorNode.ColorIndex.a;
 
-									comparedWeightArray[ comparedWeightIndex ] = currentWeight;
-									currentWeight = comparedWeight;
+			}
 
-									var tmp = wIndex[ comparedWeightIndex ];
-									wIndex[ comparedWeightIndex ] = currentIndex;
-									currentIndex = tmp;
+			return {
+				dataSize: 4,
+				buffer: buffer,
+				indices: indexBuffer,
+				mappingType: mappingType,
+				referenceType: referenceType
+			};
 
-								}
+		},
 
-							} );
+		// Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists
+		parseMaterialIndices: function ( MaterialNode ) {
 
-						} );
+			var mappingType = MaterialNode.MappingInformationType;
+			var referenceType = MaterialNode.ReferenceInformationType;
 
-						weightIndices = wIndex;
-						weights = Weight;
+			if ( mappingType === 'NoMappingInformation' ) {
 
-					}
+				return {
+					dataSize: 1,
+					buffer: [ 0 ],
+					indices: [ 0 ],
+					mappingType: 'AllSame',
+					referenceType: referenceType
+				};
 
-					// if the weight array is shorter than 4 pad with 0s
-					while ( weights.length < 4 ) {
+			}
 
-						weights.push( 0 );
-						weightIndices.push( 0 );
+			var materialIndexBuffer = MaterialNode.Materials.a;
 
-					}
+			// Since materials are stored as indices, there's a bit of a mismatch between FBX and what
+			// we expect.So we create an intermediate buffer that points to the index in the buffer,
+			// for conforming with the other functions we've written for other data.
+			var materialIndices = [];
 
-					for ( var i = 0; i < 4; ++ i ) {
+			for ( var i = 0; i < materialIndexBuffer.length; ++ i ) {
 
-						faceWeights.push( weights[ i ] );
-						faceWeightIndices.push( weightIndices[ i ] );
+				materialIndices.push( i );
 
-					}
+			}
 
-				}
+			return {
+				dataSize: 1,
+				buffer: materialIndexBuffer,
+				indices: materialIndices,
+				mappingType: mappingType,
+				referenceType: referenceType
+			};
 
-				if ( geoInfo.normal ) {
+		},
 
-					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal );
+		// Generate a NurbGeometry from a node in FBXTree.Objects.Geometry
+		parseNurbsGeometry: function ( geoNode ) {
 
-					faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] );
+			if ( THREE.NURBSCurve === undefined ) {
 
-				}
+				console.error( 'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' );
+				return new THREE.BufferGeometry();
 
-				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
+			}
 
-					var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ];
+			var order = parseInt( geoNode.Order );
 
-				}
+			if ( isNaN( order ) ) {
 
-				if ( geoInfo.uv ) {
+				console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id );
+				return new THREE.BufferGeometry();
 
-					geoInfo.uv.forEach( function ( uv, i ) {
+			}
 
-						var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv );
+			var degree = order - 1;
 
-						if ( faceUVs[ i ] === undefined ) {
+			var knots = geoNode.KnotVector.a;
+			var controlPoints = [];
+			var pointsValues = geoNode.Points.a;
 
-							faceUVs[ i ] = [];
+			for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) {
 
-						}
+				controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) );
 
-						faceUVs[ i ].push( data[ 0 ] );
-						faceUVs[ i ].push( data[ 1 ] );
+			}
 
-					} );
+			var startKnot, endKnot;
 
-				}
+			if ( geoNode.Form === 'Closed' ) {
 
-				faceLength ++;
+				controlPoints.push( controlPoints[ 0 ] );
 
-				if ( endOfFace ) {
+			} else if ( geoNode.Form === 'Periodic' ) {
 
-					self.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength );
+				startKnot = degree;
+				endKnot = knots.length - 1 - startKnot;
 
-					polygonIndex ++;
-					faceLength = 0;
+				for ( var i = 0; i < degree; ++ i ) {
 
-					// reset arrays for the next face
-					facePositionIndexes = [];
-					faceNormals = [];
-					faceColors = [];
-					faceUVs = [];
-					faceWeights = [];
-					faceWeightIndices = [];
+					controlPoints.push( controlPoints[ i ] );
 
 				}
 
-			} );
-
-			return buffers;
-
-		},
-
-		// Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris
-		genFace: function ( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) {
-
-			for ( var i = 2; i < faceLength; i ++ ) {
+			}
 
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] );
+			var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot );
+			var vertices = curve.getPoints( controlPoints.length * 7 );
 
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] );
+			var positions = new Float32Array( vertices.length * 3 );
 
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] );
-				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] );
+			vertices.forEach( function ( vertex, i ) {
 
-				if ( geoInfo.skeleton ) {
+				vertex.toArray( positions, i * 3 );
 
-					buffers.vertexWeights.push( faceWeights[ 0 ] );
-					buffers.vertexWeights.push( faceWeights[ 1 ] );
-					buffers.vertexWeights.push( faceWeights[ 2 ] );
-					buffers.vertexWeights.push( faceWeights[ 3 ] );
+			} );
 
-					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] );
-					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] );
-					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] );
-					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] );
+			var geometry = new THREE.BufferGeometry();
+			geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
 
-					buffers.vertexWeights.push( faceWeights[ i * 4 ] );
-					buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] );
-					buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] );
-					buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] );
+			return geometry;
 
-					buffers.weightsIndices.push( faceWeightIndices[ 0 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ 1 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ 2 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ 3 ] );
+		},
 
-					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] );
+	};
 
-					buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] );
-					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] );
+	// parse animation data from FBXTree
+	function AnimationParser() {}
 
-				}
+	AnimationParser.prototype = {
 
-				if ( geoInfo.color ) {
+		constructor: AnimationParser,
 
-					buffers.colors.push( faceColors[ 0 ] );
-					buffers.colors.push( faceColors[ 1 ] );
-					buffers.colors.push( faceColors[ 2 ] );
+		// take raw animation clips and turn them into three.js animation clips
+		parse: function ( sceneGraph ) {
 
-					buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] );
-					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] );
-					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] );
+			var animationClips = [];
 
-					buffers.colors.push( faceColors[ i * 3 ] );
-					buffers.colors.push( faceColors[ i * 3 + 1 ] );
-					buffers.colors.push( faceColors[ i * 3 + 2 ] );
 
-				}
+			var rawClips = this.parseClips();
 
-				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
+			if ( rawClips === undefined ) return;
 
-					buffers.materialIndex.push( materialIndex );
-					buffers.materialIndex.push( materialIndex );
-					buffers.materialIndex.push( materialIndex );
+			for ( var key in rawClips ) {
 
-				}
+				var rawClip = rawClips[ key ];
 
-				if ( geoInfo.normal ) {
+				var clip = this.addClip( rawClip, sceneGraph );
 
-					buffers.normal.push( faceNormals[ 0 ] );
-					buffers.normal.push( faceNormals[ 1 ] );
-					buffers.normal.push( faceNormals[ 2 ] );
+				animationClips.push( clip );
 
-					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] );
-					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] );
-					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] );
+			}
 
-					buffers.normal.push( faceNormals[ i * 3 ] );
-					buffers.normal.push( faceNormals[ i * 3 + 1 ] );
-					buffers.normal.push( faceNormals[ i * 3 + 2 ] );
+			return animationClips;
 
-				}
+		},
 
-				if ( geoInfo.uv ) {
+		parseClips: function () {
 
-					geoInfo.uv.forEach( function ( uv, j ) {
+			// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
+			// if this is undefined we can safely assume there are no animations
+			if ( FBXTree.Objects.AnimationCurve === undefined ) return undefined;
 
-						if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = [];
+			var curveNodesMap = this.parseAnimationCurveNodes();
 
-						buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] );
-						buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] );
+			this.parseAnimationCurves( curveNodesMap );
 
-						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] );
-						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] );
+			var layersMap = this.parseAnimationLayers( curveNodesMap );
+			var rawClips = this.parseAnimStacks( layersMap );
 
-						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] );
-						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] );
+			return rawClips;
 
-					} );
+		},
 
-				}
+		// parse nodes in FBXTree.Objects.AnimationCurveNode
+		// each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation )
+		// and is referenced by an AnimationLayer
+		parseAnimationCurveNodes: function () {
 
-			}
+			var rawCurveNodes = FBXTree.Objects.AnimationCurveNode;
 
-		},
+			var curveNodesMap = new Map();
 
-		addMorphTargets: function ( parentGeo, parentGeoNode, morphTarget, preTransform ) {
+			for ( var nodeID in rawCurveNodes ) {
 
-			if ( morphTarget === null ) return;
+				var rawCurveNode = rawCurveNodes[ nodeID ];
 
-			parentGeo.morphAttributes.position = [];
-			parentGeo.morphAttributes.normal = [];
+				if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) {
 
-			var self = this;
-			morphTarget.rawTargets.forEach( function ( rawTarget ) {
+					var curveNode = {
 
-				var morphGeoNode = FBXTree.Objects.Geometry[ rawTarget.geoID ];
+						id: rawCurveNode.id,
+						attr: rawCurveNode.attrName,
+						curves: {},
 
-				if ( morphGeoNode !== undefined ) {
+					};
 
-					self.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform );
+					curveNodesMap.set( curveNode.id, curveNode );
 
 				}
 
-			} );
-
-		},
+			}
 
-		// a morph geometry node is similar to a standard  node, and the node is also contained
-		// in FBXTree.Objects.Geometry, however it can only have attributes for position, normal
-		// and a special attribute Index defining which vertices of the original geometry are affected
-		// Normal and position attributes only have data for the vertices that are affected by the morph
-		genMorphGeometry: function ( parentGeo, parentGeoNode, morphGeoNode, preTransform ) {
+			return curveNodesMap;
 
-			var morphGeo = new THREE.BufferGeometry();
-			if ( morphGeoNode.attrName ) morphGeo.name = morphGeoNode.attrName;
+		},
 
-			var vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : [];
+		// parse nodes in FBXTree.Objects.AnimationCurve and connect them up to
+		// previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated
+		// axis ( e.g. times and values of x rotation)
+		parseAnimationCurves: function ( curveNodesMap ) {
 
-			// make a copy of the parent's vertex positions
-			var vertexPositions = ( parentGeoNode.Vertices !== undefined ) ? parentGeoNode.Vertices.a.slice() : [];
+			var rawCurves = FBXTree.Objects.AnimationCurve;
 
-			var morphPositions = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : [];
-			var indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : [];
+			// TODO: Many values are identical up to roundoff error, but won't be optimised
+			// e.g. position times: [0, 0.4, 0. 8]
+			// position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809]
+			// clearly, this should be optimised to
+			// times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809]
+			// this shows up in nearly every FBX file, and generally time array is length > 100
 
-			for ( var i = 0; i < indices.length; i ++ ) {
+			for ( var nodeID in rawCurves ) {
 
-				var morphIndex = indices[ i ] * 3;
+				var animationCurve = {
 
-				// FBX format uses blend shapes rather than morph targets. This can be converted
-				// by additively combining the blend shape positions with the original geometry's positions
-				vertexPositions[ morphIndex ] += morphPositions[ i * 3 ];
-				vertexPositions[ morphIndex + 1 ] += morphPositions[ i * 3 + 1 ];
-				vertexPositions[ morphIndex + 2 ] += morphPositions[ i * 3 + 2 ];
+					id: rawCurves[ nodeID ].id,
+					times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
+					values: rawCurves[ nodeID ].KeyValueFloat.a,
 
-			}
+				};
 
-			// TODO: add morph normal support
-			var morphGeoInfo = {
-				vertexIndices: vertexIndices,
-				vertexPositions: vertexPositions,
-			};
+				var relationships = connections.get( animationCurve.id );
 
-			var morphBuffers = this.genBuffers( morphGeoInfo );
+				if ( relationships !== undefined ) {
 
-			var positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 );
-			positionAttribute.name = morphGeoNode.attrName;
+					var animationCurveID = relationships.parents[ 0 ].ID;
+					var animationCurveRelationship = relationships.parents[ 0 ].relationship;
 
-			preTransform.applyToBufferAttribute( positionAttribute );
+					if ( animationCurveRelationship.match( /X/ ) ) {
 
-			parentGeo.morphAttributes.position.push( positionAttribute );
+						curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;
 
-		},
+					} else if ( animationCurveRelationship.match( /Y/ ) ) {
 
-		// Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists
-		parseNormals: function ( NormalNode ) {
+						curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;
 
-			var mappingType = NormalNode.MappingInformationType;
-			var referenceType = NormalNode.ReferenceInformationType;
-			var buffer = NormalNode.Normals.a;
-			var indexBuffer = [];
-			if ( referenceType === 'IndexToDirect' ) {
+					} else if ( animationCurveRelationship.match( /Z/ ) ) {
 
-				if ( 'NormalIndex' in NormalNode ) {
+						curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;
 
-					indexBuffer = NormalNode.NormalIndex.a;
+					} else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) {
 
-				} else if ( 'NormalsIndex' in NormalNode ) {
+						curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve;
 
-					indexBuffer = NormalNode.NormalsIndex.a;
+					}
 
 				}
 
 			}
 
-			return {
-				dataSize: 3,
-				buffer: buffer,
-				indices: indexBuffer,
-				mappingType: mappingType,
-				referenceType: referenceType
-			};
-
 		},
 
-		// Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists
-		parseUVs: function ( UVNode ) {
-
-			var mappingType = UVNode.MappingInformationType;
-			var referenceType = UVNode.ReferenceInformationType;
-			var buffer = UVNode.UV.a;
-			var indexBuffer = [];
-			if ( referenceType === 'IndexToDirect' ) {
+		// parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references
+		// to various AnimationCurveNodes and is referenced by an AnimationStack node
+		// note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack
+		parseAnimationLayers: function ( curveNodesMap ) {
 
-				indexBuffer = UVNode.UVIndex.a;
+			var rawLayers = FBXTree.Objects.AnimationLayer;
 
-			}
+			var layersMap = new Map();
 
-			return {
-				dataSize: 2,
-				buffer: buffer,
-				indices: indexBuffer,
-				mappingType: mappingType,
-				referenceType: referenceType
-			};
+			for ( var nodeID in rawLayers ) {
 
-		},
+				var layerCurveNodes = [];
 
-		// Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists
-		parseVertexColors: function ( ColorNode ) {
+				var connection = connections.get( parseInt( nodeID ) );
 
-			var mappingType = ColorNode.MappingInformationType;
-			var referenceType = ColorNode.ReferenceInformationType;
-			var buffer = ColorNode.Colors.a;
-			var indexBuffer = [];
-			if ( referenceType === 'IndexToDirect' ) {
+				if ( connection !== undefined ) {
 
-				indexBuffer = ColorNode.ColorIndex.a;
+					// all the animationCurveNodes used in the layer
+					var children = connection.children;
 
-			}
+					var self = this;
+					children.forEach( function ( child, i ) {
 
-			return {
-				dataSize: 4,
-				buffer: buffer,
-				indices: indexBuffer,
-				mappingType: mappingType,
-				referenceType: referenceType
-			};
+						if ( curveNodesMap.has( child.ID ) ) {
 
-		},
+							var curveNode = curveNodesMap.get( child.ID );
 
-		// Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists
-		parseMaterialIndices: function ( MaterialNode ) {
+							// check that the curves are defined for at least one axis, otherwise ignore the curveNode
+							if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) {
 
-			var mappingType = MaterialNode.MappingInformationType;
-			var referenceType = MaterialNode.ReferenceInformationType;
+								if ( layerCurveNodes[ i ] === undefined ) {
 
-			if ( mappingType === 'NoMappingInformation' ) {
+									var modelID;
 
-				return {
-					dataSize: 1,
-					buffer: [ 0 ],
-					indices: [ 0 ],
-					mappingType: 'AllSame',
-					referenceType: referenceType
-				};
+									connections.get( child.ID ).parents.forEach( function ( parent ) {
 
-			}
+										if ( parent.relationship !== undefined ) modelID = parent.ID;
 
-			var materialIndexBuffer = MaterialNode.Materials.a;
+									} );
 
-			// Since materials are stored as indices, there's a bit of a mismatch between FBX and what
-			// we expect.So we create an intermediate buffer that points to the index in the buffer,
-			// for conforming with the other functions we've written for other data.
-			var materialIndices = [];
+									var rawModel = FBXTree.Objects.Model[ modelID.toString() ];
 
-			for ( var i = 0; i < materialIndexBuffer.length; ++ i ) {
+									var node = {
 
-				materialIndices.push( i );
+										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
+										initialPosition: [ 0, 0, 0 ],
+										initialRotation: [ 0, 0, 0 ],
+										initialScale: [ 1, 1, 1 ],
+										transform: self.getModelAnimTransform( rawModel ),
 
-			}
+									};
 
-			return {
-				dataSize: 1,
-				buffer: materialIndexBuffer,
-				indices: materialIndices,
-				mappingType: mappingType,
-				referenceType: referenceType
-			};
+									// if the animated model is pre rotated, we'll have to apply the pre rotations to every
+									// animation value as well
+									if ( 'PreRotation' in rawModel ) node.preRotations = rawModel.PreRotation.value;
+									if ( 'PostRotation' in rawModel ) node.postRotations = rawModel.PostRotation.value;
 
-		},
+									layerCurveNodes[ i ] = node;
 
-		// Generate a NurbGeometry from a node in FBXTree.Objects.Geometry
-		parseNurbsGeometry: function ( geoNode ) {
+								}
 
-			if ( THREE.NURBSCurve === undefined ) {
+								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
 
-				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();
+							} else if ( curveNode.curves.morph !== undefined ) {
 
-			}
+								if ( layerCurveNodes[ i ] === undefined ) {
 
-			var order = parseInt( geoNode.Order );
+									var deformerID;
 
-			if ( isNaN( order ) ) {
+									connections.get( child.ID ).parents.forEach( function ( parent ) {
 
-				console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id );
-				return new THREE.BufferGeometry();
+										if ( parent.relationship !== undefined ) deformerID = parent.ID;
 
-			}
+									} );
 
-			var degree = order - 1;
+									var morpherID = connections.get( deformerID ).parents[ 0 ].ID;
+									var geoID = connections.get( morpherID ).parents[ 0 ].ID;
 
-			var knots = geoNode.KnotVector.a;
-			var controlPoints = [];
-			var pointsValues = geoNode.Points.a;
+									// assuming geometry is not used in more than one model
+									var modelID = connections.get( geoID ).parents[ 0 ].ID;
 
-			for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) {
+									var rawModel = FBXTree.Objects.Model[ modelID ];
 
-				controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) );
+									var node = {
 
-			}
+										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
+										morphName: FBXTree.Objects.Deformer[ deformerID ].attrName,
 
-			var startKnot, endKnot;
+									};
 
-			if ( geoNode.Form === 'Closed' ) {
+									layerCurveNodes[ i ] = node;
 
-				controlPoints.push( controlPoints[ 0 ] );
+								}
 
-			} else if ( geoNode.Form === 'Periodic' ) {
+								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
 
-				startKnot = degree;
-				endKnot = knots.length - 1 - startKnot;
+							}
 
-				for ( var i = 0; i < degree; ++ i ) {
+						}
 
-					controlPoints.push( controlPoints[ i ] );
+					} );
+
+					layersMap.set( parseInt( nodeID ), layerCurveNodes );
 
 				}
 
 			}
 
-			var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot );
-			var vertices = curve.getPoints( controlPoints.length * 7 );
+			return layersMap;
 
-			var positions = new Float32Array( vertices.length * 3 );
+		},
 
-			vertices.forEach( function ( vertex, i ) {
+		getModelAnimTransform: function ( modelNode ) {
 
-				vertex.toArray( positions, i * 3 );
+			var transformData = {};
 
-			} );
+			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value );
 
-			var geometry = new THREE.BufferGeometry();
-			geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
+			if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value;
+			if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value;
 
-			return geometry;
+			if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value;
+			if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value;
+
+			if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value;
+
+			if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value;
+
+			return generateTransform( transformData );
 
 		},
 
-	};
+		// parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation
+		// hierarchy. Each Stack node will be used to create a THREE.AnimationClip
+		parseAnimStacks: function ( layersMap ) {
 
-	// parse animation data from FBXTree
-	function AnimationParser() {}
+			var rawStacks = FBXTree.Objects.AnimationStack;
 
-	AnimationParser.prototype = {
+			// connect the stacks (clips) up to the layers
+			var rawClips = {};
 
-		constructor: AnimationParser,
+			for ( var nodeID in rawStacks ) {
 
-		parse: function () {
+				var children = connections.get( parseInt( nodeID ) ).children;
 
-			// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
-			// if this is undefined we can safely assume there are no animations
-			if ( FBXTree.Objects.AnimationCurve === undefined ) return undefined;
+				if ( children.length > 1 ) {
 
-			var curveNodesMap = this.parseAnimationCurveNodes();
+					// 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.' );
 
-			this.parseAnimationCurves( curveNodesMap );
+				}
 
-			var layersMap = this.parseAnimationLayers( curveNodesMap );
-			var rawClips = this.parseAnimStacks( layersMap );
+				var layer = layersMap.get( children[ 0 ].ID );
+
+				rawClips[ nodeID ] = {
+
+					name: rawStacks[ nodeID ].attrName,
+					layer: layer,
+
+				};
+
+			}
 
 			return rawClips;
 
 		},
 
-		// parse nodes in FBXTree.Objects.AnimationCurveNode
-		// each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation )
-		// and is referenced by an AnimationLayer
-		parseAnimationCurveNodes: function () {
+		addClip: function ( rawClip, sceneGraph ) {
 
-			var rawCurveNodes = FBXTree.Objects.AnimationCurveNode;
+			var tracks = [];
 
-			var curveNodesMap = new Map();
+			var self = this;
+			rawClip.layer.forEach( function ( rawTracks ) {
 
-			for ( var nodeID in rawCurveNodes ) {
+				tracks = tracks.concat( self.generateTracks( rawTracks, sceneGraph ) );
 
-				var rawCurveNode = rawCurveNodes[ nodeID ];
+			} );
 
-				if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) {
+			return new THREE.AnimationClip( rawClip.name, - 1, tracks );
 
-					var curveNode = {
+		},
 
-						id: rawCurveNode.id,
-						attr: rawCurveNode.attrName,
-						curves: {},
+		generateTracks: function ( rawTracks, sceneGraph ) {
 
-					};
+			var tracks = [];
 
-					curveNodesMap.set( curveNode.id, curveNode );
+			var initialPosition = new THREE.Vector3();
+			var initialRotation = new THREE.Quaternion();
+			var initialScale = new THREE.Vector3();
 
-				}
+			if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale );
+
+			initialPosition = initialPosition.toArray();
+			initialRotation = new THREE.Euler().setFromQuaternion( initialRotation ).toArray(); // todo: euler order
+			initialScale = initialScale.toArray();
+
+			if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) {
+
+				var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' );
+				if ( positionTrack !== undefined ) tracks.push( positionTrack );
 
 			}
 
-			return curveNodesMap;
+			if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {
 
-		},
+				var rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotations, rawTracks.postRotations );
+				if ( rotationTrack !== undefined ) tracks.push( rotationTrack );
 
-		// parse nodes in FBXTree.Objects.AnimationCurve and connect them up to
-		// previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated
-		// axis ( e.g. times and values of x rotation)
-		parseAnimationCurves: function ( curveNodesMap ) {
+			}
 
-			var rawCurves = FBXTree.Objects.AnimationCurve;
+			if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) {
 
-			// TODO: Many values are identical up to roundoff error, but won't be optimised
-			// e.g. position times: [0, 0.4, 0. 8]
-			// position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809]
-			// clearly, this should be optimised to
-			// times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809]
-			// this shows up in nearly every FBX file, and generally time array is length > 100
+				var scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' );
+				if ( scaleTrack !== undefined ) tracks.push( scaleTrack );
 
-			for ( var nodeID in rawCurves ) {
+			}
 
-				var animationCurve = {
+			if ( rawTracks.DeformPercent !== undefined ) {
 
-					id: rawCurves[ nodeID ].id,
-					times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
-					values: rawCurves[ nodeID ].KeyValueFloat.a,
+				var morphTrack = this.generateMorphTrack( rawTracks, sceneGraph );
+				if ( morphTrack !== undefined ) tracks.push( morphTrack );
 
-				};
+			}
 
-				var relationships = connections.get( animationCurve.id );
+			return tracks;
 
-				if ( relationships !== undefined ) {
+		},
 
-					var animationCurveID = relationships.parents[ 0 ].ID;
-					var animationCurveRelationship = relationships.parents[ 0 ].relationship;
+		generateVectorTrack: function ( modelName, curves, initialValue, type ) {
 
-					if ( animationCurveRelationship.match( /X/ ) ) {
+			var times = this.getTimesForAllAxes( curves );
+			var values = this.getKeyframeTrackValues( times, curves, initialValue );
 
-						curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;
+			return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values );
 
-					} else if ( animationCurveRelationship.match( /Y/ ) ) {
+		},
 
-						curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;
+		generateRotationTrack: function ( modelName, curves, initialValue, preRotations, postRotations ) {
 
-					} else if ( animationCurveRelationship.match( /Z/ ) ) {
+			if ( curves.x !== undefined ) {
 
-						curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;
+				this.interpolateRotations( curves.x );
+				curves.x.values = curves.x.values.map( THREE.Math.degToRad );
 
-					} else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) {
+			}
+			if ( curves.y !== undefined ) {
 
-						curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve;
+				this.interpolateRotations( curves.y );
+				curves.y.values = curves.y.values.map( THREE.Math.degToRad );
 
-					}
+			}
+			if ( curves.z !== undefined ) {
 
-				}
+				this.interpolateRotations( curves.z );
+				curves.z.values = curves.z.values.map( THREE.Math.degToRad );
 
 			}
 
-		},
+			var times = this.getTimesForAllAxes( curves );
+			var values = this.getKeyframeTrackValues( times, curves, initialValue );
 
-		// parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references
-		// to various AnimationCurveNodes and is referenced by an AnimationStack node
-		// note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack
-		parseAnimationLayers: function ( curveNodesMap ) {
+			if ( preRotations !== undefined ) {
 
-			var rawLayers = FBXTree.Objects.AnimationLayer;
+				preRotations = preRotations.map( THREE.Math.degToRad );
+				preRotations.push( 'ZYX' );
 
-			var layersMap = new Map();
+				preRotations = new THREE.Euler().fromArray( preRotations );
+				preRotations = new THREE.Quaternion().setFromEuler( preRotations );
 
-			for ( var nodeID in rawLayers ) {
+			}
 
-				var layerCurveNodes = [];
+			if ( postRotations !== undefined ) {
 
-				var connection = connections.get( parseInt( nodeID ) );
+				postRotations = postRotations.map( THREE.Math.degToRad );
+				postRotations.push( 'ZYX' );
 
-				if ( connection !== undefined ) {
+				postRotations = new THREE.Euler().fromArray( postRotations );
+				postRotations = new THREE.Quaternion().setFromEuler( postRotations ).inverse();
 
-					// all the animationCurveNodes used in the layer
-					var children = connection.children;
+			}
 
-					var self = this;
-					children.forEach( function ( child, i ) {
+			var quaternion = new THREE.Quaternion();
+			var euler = new THREE.Euler();
 
-						if ( curveNodesMap.has( child.ID ) ) {
+			var quaternionValues = [];
 
-							var curveNode = curveNodesMap.get( child.ID );
+			for ( var i = 0; i < values.length; i += 3 ) {
 
-							// check that the curves are defined for at least one axis, otherwise ignore the curveNode
-							if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) {
+				euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' );
 
-								if ( layerCurveNodes[ i ] === undefined ) {
+				quaternion.setFromEuler( euler );
 
-									var modelID;
+				if ( preRotations !== undefined ) quaternion.premultiply( preRotations );
+				if ( postRotations !== undefined ) quaternion.multiply( postRotations );
 
-									connections.get( child.ID ).parents.forEach( function ( parent ) {
+				quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );
 
-										if ( parent.relationship !== undefined ) modelID = parent.ID;
+			}
 
-									} );
+			return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );
 
-									var rawModel = FBXTree.Objects.Model[ modelID.toString() ];
+		},
 
-									var node = {
+		generateMorphTrack: function ( rawTracks, sceneGraph ) {
 
-										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
-										initialPosition: [ 0, 0, 0 ],
-										initialRotation: [ 0, 0, 0 ],
-										initialScale: [ 1, 1, 1 ],
-										transform: self.getModelAnimTransform( rawModel ),
+			var curves = rawTracks.DeformPercent.curves.morph;
+			var values = curves.values.map( function ( val ) {
 
-									};
+				return val / 100;
 
-									// if the animated model is pre rotated, we'll have to apply the pre rotations to every
-									// animation value as well
-									if ( 'PreRotation' in rawModel ) node.preRotations = rawModel.PreRotation.value;
-									if ( 'PostRotation' in rawModel ) node.postRotations = rawModel.PostRotation.value;
+			} );
 
-									layerCurveNodes[ i ] = node;
+			var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ];
 
-								}
+			return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values );
 
-								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
+		},
 
-							} else if ( curveNode.curves.morph !== undefined ) {
+		// For all animated objects, times are defined separately for each axis
+		// Here we'll combine the times into one sorted array without duplicates
+		getTimesForAllAxes: function ( curves ) {
 
-								if ( layerCurveNodes[ i ] === undefined ) {
+			var times = [];
 
-									var deformerID;
+			// 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 );
 
-									connections.get( child.ID ).parents.forEach( function ( parent ) {
+			// then sort them and remove duplicates
+			times = times.sort( function ( a, b ) {
 
-										if ( parent.relationship !== undefined ) deformerID = parent.ID;
+				return a - b;
 
-									} );
+			} ).filter( function ( elem, index, array ) {
 
-									var morpherID = connections.get( deformerID ).parents[ 0 ].ID;
-									var geoID = connections.get( morpherID ).parents[ 0 ].ID;
+				return array.indexOf( elem ) == index;
 
-									// assuming geometry is not used in more than one model
-									var modelID = connections.get( geoID ).parents[ 0 ].ID;
+			} );
 
-									var rawModel = FBXTree.Objects.Model[ modelID ];
+			return times;
 
-									var node = {
+		},
 
-										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
-										morphName: FBXTree.Objects.Deformer[ deformerID ].attrName,
+		getKeyframeTrackValues: function ( times, curves, initialValue ) {
 
-									};
+			var prevValue = initialValue;
 
-									layerCurveNodes[ i ] = node;
+			var values = [];
 
-								}
+			var xIndex = - 1;
+			var yIndex = - 1;
+			var zIndex = - 1;
 
-								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
+			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 );
 
-						}
+				// if there is an x value defined for this frame, use that
+				if ( xIndex !== - 1 ) {
 
-					} );
+					var xValue = curves.x.values[ xIndex ];
+					values.push( xValue );
+					prevValue[ 0 ] = xValue;
 
-					layersMap.set( parseInt( nodeID ), layerCurveNodes );
+				} else {
+
+					// otherwise use the x value from the previous frame
+					values.push( prevValue[ 0 ] );
 
 				}
 
-			}
+				if ( yIndex !== - 1 ) {
 
-			return layersMap;
+					var yValue = curves.y.values[ yIndex ];
+					values.push( yValue );
+					prevValue[ 1 ] = yValue;
 
-		},
+				} else {
 
-		getModelAnimTransform: function ( modelNode ) {
+					values.push( prevValue[ 1 ] );
 
-			var transformData = {};
+				}
 
-			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value );
+				if ( zIndex !== - 1 ) {
 
-			if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value;
-			if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value;
+					var zValue = curves.z.values[ zIndex ];
+					values.push( zValue );
+					prevValue[ 2 ] = zValue;
 
-			if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value;
-			if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value;
+				} else {
 
-			if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value;
+					values.push( prevValue[ 2 ] );
 
-			if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value;
+				}
 
-			return generateTransform( transformData );
+			} );
+
+			return values;
 
 		},
 
-		// parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation
-		// hierarchy. Each Stack node will be used to create a THREE.AnimationClip
-		parseAnimStacks: function ( layersMap ) {
+		// Rotations are defined as Euler angles which can have values  of any size
+		// These will be converted to quaternions which don't support values greater than
+		// PI, so we'll interpolate large rotations
+		interpolateRotations: function ( curve ) {
 
-			var rawStacks = FBXTree.Objects.AnimationStack;
+			for ( var i = 1; i < curve.values.length; i ++ ) {
 
-			// connect the stacks (clips) up to the layers
-			var rawClips = {};
+				var initialValue = curve.values[ i - 1 ];
+				var valuesSpan = curve.values[ i ] - initialValue;
 
-			for ( var nodeID in rawStacks ) {
+				var absoluteSpan = Math.abs( valuesSpan );
 
-				var children = connections.get( parseInt( nodeID ) ).children;
+				if ( absoluteSpan >= 180 ) {
 
-				if ( children.length > 1 ) {
+					var numSubIntervals = absoluteSpan / 180;
 
-					// 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.' );
+					var step = valuesSpan / numSubIntervals;
+					var nextValue = initialValue + step;
 
-				}
+					var initialTime = curve.times[ i - 1 ];
+					var timeSpan = curve.times[ i ] - initialTime;
+					var interval = timeSpan / numSubIntervals;
+					var nextTime = initialTime + interval;
 
-				var layer = layersMap.get( children[ 0 ].ID );
+					var interpolatedTimes = [];
+					var interpolatedValues = [];
 
-				rawClips[ nodeID ] = {
+					while ( nextTime < curve.times[ i ] ) {
 
-					name: rawStacks[ nodeID ].attrName,
-					layer: layer,
+						interpolatedTimes.push( nextTime );
+						nextTime += interval;
 
-				};
+						interpolatedValues.push( nextValue );
+						nextValue += step;
 
-			}
+					}
 
-			return rawClips;
+					curve.times = inject( curve.times, i, interpolatedTimes );
+					curve.values = inject( curve.values, i, interpolatedValues );
+
+				}
+
+			}
 
 		},