Quellcode durchsuchen

create AnimationParser

Lewy Blue vor 7 Jahren
Ursprung
Commit
34647a14e3
1 geänderte Dateien mit 363 neuen und 351 gelöschten Zeilen
  1. 363 351
      examples/js/loaders/FBXLoader.js

+ 363 - 351
examples/js/loaders/FBXLoader.js

@@ -2174,7 +2174,7 @@ THREE.FBXLoader = ( function () {
 
 
 			sceneGraph.animations = [];
 			sceneGraph.animations = [];
 
 
-			var rawClips = this.parseAnimations();
+			var rawClips = new AnimationParser().parse( this.FBXTree, this.connections );
 
 
 			if ( rawClips === undefined ) return;
 			if ( rawClips === undefined ) return;
 
 
@@ -2190,616 +2190,628 @@ THREE.FBXLoader = ( function () {
 
 
 		},
 		},
 
 
-		parseAnimations: function () {
+		addClip: function ( rawClip, sceneGraph ) {
 
 
-			// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
-			// if this is undefined we can safely assume there are no animations
-			if ( this.FBXTree.Objects.AnimationCurve === undefined ) return undefined;
+			var tracks = [];
 
 
-			var curveNodesMap = this.parseAnimationCurveNodes();
+			var self = this;
+			rawClip.layer.forEach( function ( rawTracks ) {
 
 
-			this.parseAnimationCurves( curveNodesMap );
+				tracks = tracks.concat( self.generateTracks( rawTracks, sceneGraph ) );
 
 
-			var layersMap = this.parseAnimationLayers( curveNodesMap );
-			var rawClips = this.parseAnimStacks( layersMap );
+			} );
 
 
-			return rawClips;
+			return new THREE.AnimationClip( rawClip.name, - 1, tracks );
 
 
 		},
 		},
 
 
-		// 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 () {
+		generateTracks: function ( rawTracks, sceneGraph ) {
 
 
-			var rawCurveNodes = this.FBXTree.Objects.AnimationCurveNode;
+			var tracks = [];
 
 
-			var curveNodesMap = new Map();
+			var initialPosition = new THREE.Vector3();
+			var initialRotation = new THREE.Quaternion();
+			var initialScale = new THREE.Vector3();
 
 
-			for ( var nodeID in rawCurveNodes ) {
+			if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale );
 
 
-				var rawCurveNode = rawCurveNodes[ nodeID ];
+			initialPosition = initialPosition.toArray();
+			initialRotation = new THREE.Euler().setFromQuaternion( initialRotation ).toArray(); // todo: euler order
+			initialScale = initialScale.toArray();
 
 
-				if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) {
+			if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) {
 
 
-					var curveNode = {
+				var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' );
+				if ( positionTrack !== undefined ) tracks.push( positionTrack );
 
 
-						id: rawCurveNode.id,
-						attr: rawCurveNode.attrName,
-						curves: {},
+			}
 
 
-					};
+			if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {
 
 
-					curveNodesMap.set( curveNode.id, curveNode );
+				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 );
 
 
 			}
 			}
 
 
-			return curveNodesMap;
+			if ( rawTracks.DeformPercent !== undefined ) {
+
+				var morphTrack = this.generateMorphTrack( rawTracks, sceneGraph );
+				if ( morphTrack !== undefined ) tracks.push( morphTrack );
+
+			}
+
+			return tracks;
 
 
 		},
 		},
 
 
-		// 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 ) {
+		generateVectorTrack: function ( modelName, curves, initialValue, type ) {
 
 
-			var rawCurves = this.FBXTree.Objects.AnimationCurve;
+			var times = this.getTimesForAllAxes( curves );
+			var values = this.getKeyframeTrackValues( times, curves, initialValue );
 
 
-			// TODO: Many values are identical up to roundoff error, but won't be optimised
-			// e.g. position times: [0, 0.4, 0. 8]
-			// position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809]
-			// clearly, this should be optimised to
-			// times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809]
-			// this shows up in nearly every FBX file, and generally time array is length > 100
+			return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values );
 
 
-			for ( var nodeID in rawCurves ) {
+		},
 
 
-				var animationCurve = {
+		generateRotationTrack: function ( modelName, curves, initialValue, preRotations, postRotations ) {
 
 
-					id: rawCurves[ nodeID ].id,
-					times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
-					values: rawCurves[ nodeID ].KeyValueFloat.a,
+			if ( curves.x !== undefined ) {
 
 
-				};
+				this.interpolateRotations( curves.x );
+				curves.x.values = curves.x.values.map( THREE.Math.degToRad );
 
 
-				var relationships = this.connections.get( animationCurve.id );
+			}
+			if ( curves.y !== undefined ) {
 
 
-				if ( relationships !== undefined ) {
+				this.interpolateRotations( curves.y );
+				curves.y.values = curves.y.values.map( THREE.Math.degToRad );
 
 
-					var animationCurveID = relationships.parents[ 0 ].ID;
-					var animationCurveRelationship = relationships.parents[ 0 ].relationship;
+			}
+			if ( curves.z !== undefined ) {
 
 
-					if ( animationCurveRelationship.match( /X/ ) ) {
+				this.interpolateRotations( curves.z );
+				curves.z.values = curves.z.values.map( THREE.Math.degToRad );
 
 
-						curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;
+			}
 
 
-					} else if ( animationCurveRelationship.match( /Y/ ) ) {
+			var times = this.getTimesForAllAxes( curves );
+			var values = this.getKeyframeTrackValues( times, curves, initialValue );
 
 
-						curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;
+			if ( preRotations !== undefined ) {
 
 
-					} else if ( animationCurveRelationship.match( /Z/ ) ) {
+				preRotations = preRotations.map( THREE.Math.degToRad );
+				preRotations.push( 'ZYX' );
 
 
-						curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;
+				preRotations = new THREE.Euler().fromArray( preRotations );
+				preRotations = new THREE.Quaternion().setFromEuler( preRotations );
 
 
-					} else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) {
+			}
 
 
-						curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve;
+			if ( postRotations !== undefined ) {
 
 
-					}
+				postRotations = postRotations.map( THREE.Math.degToRad );
+				postRotations.push( 'ZYX' );
 
 
-				}
+				postRotations = new THREE.Euler().fromArray( postRotations );
+				postRotations = new THREE.Quaternion().setFromEuler( postRotations ).inverse();
 
 
 			}
 			}
 
 
-		},
+			var quaternion = new THREE.Quaternion();
+			var euler = new THREE.Euler();
 
 
-		// parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references
-		// to various AnimationCurveNodes and is referenced by an AnimationStack node
-		// note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack
-		parseAnimationLayers: function ( curveNodesMap ) {
+			var quaternionValues = [];
 
 
-			var rawLayers = this.FBXTree.Objects.AnimationLayer;
+			for ( var i = 0; i < values.length; i += 3 ) {
 
 
-			var layersMap = new Map();
+				euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' );
 
 
-			for ( var nodeID in rawLayers ) {
+				quaternion.setFromEuler( euler );
 
 
-				var layerCurveNodes = [];
+				if ( preRotations !== undefined ) quaternion.premultiply( preRotations );
+				if ( postRotations !== undefined ) quaternion.multiply( postRotations );
 
 
-				var connection = this.connections.get( parseInt( nodeID ) );
+				quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );
 
 
-				if ( connection !== undefined ) {
+			}
 
 
-				// all the animationCurveNodes used in the layer
-					var children = connection.children;
+			return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );
 
 
-					var self = this;
-					children.forEach( function ( child, i ) {
+		},
 
 
-						if ( curveNodesMap.has( child.ID ) ) {
+		generateMorphTrack: function ( rawTracks, sceneGraph ) {
 
 
-							var curveNode = curveNodesMap.get( child.ID );
+			var curves = rawTracks.DeformPercent.curves.morph;
+			var values = curves.values.map( function ( val ) {
 
 
-							// 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 ) {
+				return val / 100;
 
 
-								if ( layerCurveNodes[ i ] === undefined ) {
+			} );
 
 
-									var modelID;
+			var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ];
 
 
-									self.connections.get( child.ID ).parents.forEach( function ( parent ) {
+			return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values );
 
 
-										if ( parent.relationship !== undefined ) modelID = parent.ID;
+		},
 
 
-									} );
+		// 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 rawModel = self.FBXTree.Objects.Model[ modelID.toString() ];
+			var times = [];
 
 
-									var node = {
+			// 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 );
 
 
-										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
-										initialPosition: [ 0, 0, 0 ],
-										initialRotation: [ 0, 0, 0 ],
-										initialScale: [ 1, 1, 1 ],
-										transform: self.getModelAnimTransform( rawModel ),
+			// then sort them and remove duplicates
+			times = times.sort( function ( a, b ) {
 
 
-									};
+				return a - b;
 
 
-									// 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;
+			} ).filter( function ( elem, index, array ) {
 
 
-									layerCurveNodes[ i ] = node;
+				return array.indexOf( elem ) == index;
 
 
-								}
+			} );
 
 
-								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
+			return times;
 
 
-							} else if ( curveNode.curves.morph !== undefined ) {
+		},
 
 
-								if ( layerCurveNodes[ i ] === undefined ) {
+		getKeyframeTrackValues: function ( times, curves, initialValue ) {
 
 
-									var deformerID;
+			var prevValue = initialValue;
 
 
-									self.connections.get( child.ID ).parents.forEach( function ( parent ) {
+			var values = [];
 
 
-										if ( parent.relationship !== undefined ) deformerID = parent.ID;
+			var xIndex = - 1;
+			var yIndex = - 1;
+			var zIndex = - 1;
 
 
-									} );
+			times.forEach( function ( time ) {
 
 
-									var morpherID = self.connections.get( deformerID ).parents[ 0 ].ID;
-									var geoID = self.connections.get( morpherID ).parents[ 0 ].ID;
+				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 );
 
 
-									// assuming geometry is not used in more than one model
-									var modelID = self.connections.get( geoID ).parents[ 0 ].ID;
+				// if there is an x value defined for this frame, use that
+				if ( xIndex !== - 1 ) {
 
 
-									var rawModel = self.FBXTree.Objects.Model[ modelID ];
+					var xValue = curves.x.values[ xIndex ];
+					values.push( xValue );
+					prevValue[ 0 ] = xValue;
 
 
-									var node = {
+				} else {
 
 
-										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
-										morphName: self.FBXTree.Objects.Deformer[ deformerID ].attrName,
+					// otherwise use the x value from the previous frame
+					values.push( prevValue[ 0 ] );
 
 
-									};
+				}
 
 
-									layerCurveNodes[ i ] = node;
+				if ( yIndex !== - 1 ) {
 
 
-								}
+					var yValue = curves.y.values[ yIndex ];
+					values.push( yValue );
+					prevValue[ 1 ] = yValue;
 
 
-								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
+				} else {
 
 
-							}
+					values.push( prevValue[ 1 ] );
 
 
-						}
+				}
 
 
-					} );
+				if ( zIndex !== - 1 ) {
 
 
-					layersMap.set( parseInt( nodeID ), layerCurveNodes );
+					var zValue = curves.z.values[ zIndex ];
+					values.push( zValue );
+					prevValue[ 2 ] = zValue;
+
+				} else {
+
+					values.push( prevValue[ 2 ] );
 
 
 				}
 				}
 
 
-			}
+			} );
 
 
-			return layersMap;
+			return values;
 
 
 		},
 		},
 
 
-		getModelAnimTransform: function ( modelNode ) {
+		// 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 transformData = {};
+			for ( var i = 1; i < curve.values.length; i ++ ) {
 
 
-			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value );
+				var initialValue = curve.values[ i - 1 ];
+				var valuesSpan = curve.values[ i ] - initialValue;
 
 
-			if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value;
-			if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value;
+				var absoluteSpan = Math.abs( valuesSpan );
 
 
-			if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value;
-			if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value;
+				if ( absoluteSpan >= 180 ) {
 
 
-			if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value;
+					var numSubIntervals = absoluteSpan / 180;
 
 
-			if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value;
+					var step = valuesSpan / numSubIntervals;
+					var nextValue = initialValue + step;
 
 
-			return generateTransform( transformData );
+					var initialTime = curve.times[ i - 1 ];
+					var timeSpan = curve.times[ i ] - initialTime;
+					var interval = timeSpan / numSubIntervals;
+					var nextTime = initialTime + interval;
 
 
-		},
+					var interpolatedTimes = [];
+					var interpolatedValues = [];
 
 
-		// 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 ) {
+					while ( nextTime < curve.times[ i ] ) {
 
 
-			var rawStacks = this.FBXTree.Objects.AnimationStack;
+						interpolatedTimes.push( nextTime );
+						nextTime += interval;
 
 
-			// connect the stacks (clips) up to the layers
-			var rawClips = {};
+						interpolatedValues.push( nextValue );
+						nextValue += step;
 
 
-			for ( var nodeID in rawStacks ) {
+					}
 
 
-				var children = this.connections.get( parseInt( nodeID ) ).children;
+					curve.times = inject( curve.times, i, interpolatedTimes );
+					curve.values = inject( curve.values, i, interpolatedValues );
 
 
-				if ( children.length > 1 ) {
+				}
 
 
-				// 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 layer = layersMap.get( children[ 0 ].ID );
+		// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
+		createAmbientLight: function ( sceneGraph ) {
 
 
-				rawClips[ nodeID ] = {
+			if ( 'GlobalSettings' in this.FBXTree && 'AmbientColor' in this.FBXTree.GlobalSettings ) {
 
 
-					name: rawStacks[ nodeID ].attrName,
-					layer: layer,
+				var ambientColor = this.FBXTree.GlobalSettings.AmbientColor.value;
+				var r = ambientColor[ 0 ];
+				var g = ambientColor[ 1 ];
+				var b = ambientColor[ 2 ];
 
 
-				};
+				if ( r !== 0 || g !== 0 || b !== 0 ) {
 
 
-			}
+					var color = new THREE.Color( r, g, b );
+					sceneGraph.add( new THREE.AmbientLight( color, 1 ) );
 
 
-			return rawClips;
+				}
 
 
-		},
+			}
 
 
-		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 animation data from FBXTree
+	function AnimationParser() {}
 
 
-			}
+	AnimationParser.prototype = {
 
 
-			if ( rawTracks.DeformPercent !== undefined ) {
+		constructor: AnimationParser,
 
 
-				var morphTrack = this.generateMorphTrack( rawTracks, sceneGraph );
-				if ( morphTrack !== undefined ) tracks.push( morphTrack );
+		parse: function ( FBXTree, connections ) {
 
 
-			}
+			this.FBXTree = FBXTree;
+			this.connections = connections;
 
 
-			return tracks;
+			// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
+			// if this is undefined we can safely assume there are no animations
+			if ( this.FBXTree.Objects.AnimationCurve === undefined ) return undefined;
 
 
-		},
+			var curveNodesMap = this.parseAnimationCurveNodes();
 
 
-		generateVectorTrack: function ( modelName, curves, initialValue, type ) {
+			this.parseAnimationCurves( curveNodesMap );
 
 
-			var times = this.getTimesForAllAxes( curves );
-			var values = this.getKeyframeTrackValues( times, curves, initialValue );
+			var layersMap = this.parseAnimationLayers( curveNodesMap );
+			var rawClips = this.parseAnimStacks( layersMap );
 
 
-			return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values );
+			return rawClips;
 
 
 		},
 		},
 
 
-		generateRotationTrack: function ( modelName, curves, initialValue, preRotations, postRotations ) {
-
-			if ( curves.x !== undefined ) {
+		// 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 () {
 
 
-				this.interpolateRotations( curves.x );
-				curves.x.values = curves.x.values.map( THREE.Math.degToRad );
+			var rawCurveNodes = this.FBXTree.Objects.AnimationCurveNode;
 
 
-			}
-			if ( curves.y !== undefined ) {
+			var curveNodesMap = new Map();
 
 
-				this.interpolateRotations( curves.y );
-				curves.y.values = curves.y.values.map( THREE.Math.degToRad );
+			for ( var nodeID in rawCurveNodes ) {
 
 
-			}
-			if ( curves.z !== undefined ) {
+				var rawCurveNode = rawCurveNodes[ nodeID ];
 
 
-				this.interpolateRotations( curves.z );
-				curves.z.values = curves.z.values.map( THREE.Math.degToRad );
+				if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) {
 
 
-			}
+					var curveNode = {
 
 
-			var times = this.getTimesForAllAxes( curves );
-			var values = this.getKeyframeTrackValues( times, curves, initialValue );
+						id: rawCurveNode.id,
+						attr: rawCurveNode.attrName,
+						curves: {},
 
 
-			if ( preRotations !== undefined ) {
+					};
 
 
-				preRotations = preRotations.map( THREE.Math.degToRad );
-				preRotations.push( 'ZYX' );
+					curveNodesMap.set( curveNode.id, curveNode );
 
 
-				preRotations = new THREE.Euler().fromArray( preRotations );
-				preRotations = new THREE.Quaternion().setFromEuler( preRotations );
+				}
 
 
 			}
 			}
 
 
-			if ( postRotations !== undefined ) {
-
-				postRotations = postRotations.map( THREE.Math.degToRad );
-				postRotations.push( 'ZYX' );
-
-				postRotations = new THREE.Euler().fromArray( postRotations );
-				postRotations = new THREE.Quaternion().setFromEuler( postRotations ).inverse();
+			return curveNodesMap;
 
 
-			}
+		},
 
 
-			var quaternion = new THREE.Quaternion();
-			var euler = new THREE.Euler();
+		// 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 quaternionValues = [];
+			var rawCurves = this.FBXTree.Objects.AnimationCurve;
 
 
-			for ( var i = 0; i < values.length; i += 3 ) {
+			// 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
 
 
-				euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' );
+			for ( var nodeID in rawCurves ) {
 
 
-				quaternion.setFromEuler( euler );
+				var animationCurve = {
 
 
-				if ( preRotations !== undefined ) quaternion.premultiply( preRotations );
-				if ( postRotations !== undefined ) quaternion.multiply( postRotations );
+					id: rawCurves[ nodeID ].id,
+					times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
+					values: rawCurves[ nodeID ].KeyValueFloat.a,
 
 
-				quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );
+				};
 
 
-			}
+				var relationships = this.connections.get( animationCurve.id );
 
 
-			return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );
+				if ( relationships !== undefined ) {
 
 
-		},
+					var animationCurveID = relationships.parents[ 0 ].ID;
+					var animationCurveRelationship = relationships.parents[ 0 ].relationship;
 
 
-		generateMorphTrack: function ( rawTracks, sceneGraph ) {
+					if ( animationCurveRelationship.match( /X/ ) ) {
 
 
-			var curves = rawTracks.DeformPercent.curves.morph;
-			var values = curves.values.map( function ( val ) {
+						curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;
 
 
-				return val / 100;
+					} else if ( animationCurveRelationship.match( /Y/ ) ) {
 
 
-			} );
+						curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;
 
 
-			var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ];
+					} else if ( animationCurveRelationship.match( /Z/ ) ) {
 
 
-			return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values );
+						curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;
 
 
-		},
+					} else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) {
 
 
-		// 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 ) {
+						curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve;
 
 
-			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 ) {
+			}
 
 
-				return a - b;
+		},
 
 
-			} ).filter( function ( elem, index, array ) {
+		// 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 ) {
 
 
-				return array.indexOf( elem ) == index;
+			var rawLayers = this.FBXTree.Objects.AnimationLayer;
 
 
-			} );
+			var layersMap = new Map();
 
 
-			return times;
+			for ( var nodeID in rawLayers ) {
 
 
-		},
+				var layerCurveNodes = [];
 
 
-		getKeyframeTrackValues: function ( times, curves, initialValue ) {
+				var connection = this.connections.get( parseInt( nodeID ) );
 
 
-			var prevValue = initialValue;
+				if ( connection !== undefined ) {
 
 
-			var values = [];
+					// all the animationCurveNodes used in the layer
+					var children = connection.children;
 
 
-			var xIndex = - 1;
-			var yIndex = - 1;
-			var zIndex = - 1;
+					var self = this;
+					children.forEach( function ( child, i ) {
 
 
-			times.forEach( function ( time ) {
+						if ( curveNodesMap.has( child.ID ) ) {
 
 
-				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 );
+							var curveNode = curveNodesMap.get( child.ID );
 
 
-				// if there is an x value defined for this frame, use that
-				if ( xIndex !== - 1 ) {
+							// 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 xValue = curves.x.values[ xIndex ];
-					values.push( xValue );
-					prevValue[ 0 ] = xValue;
+								if ( layerCurveNodes[ i ] === undefined ) {
 
 
-				} else {
+									var modelID;
 
 
-					// otherwise use the x value from the previous frame
-					values.push( prevValue[ 0 ] );
+									self.connections.get( child.ID ).parents.forEach( function ( parent ) {
 
 
-				}
+										if ( parent.relationship !== undefined ) modelID = parent.ID;
 
 
-				if ( yIndex !== - 1 ) {
+									} );
 
 
-					var yValue = curves.y.values[ yIndex ];
-					values.push( yValue );
-					prevValue[ 1 ] = yValue;
+									var rawModel = self.FBXTree.Objects.Model[ modelID.toString() ];
 
 
-				} else {
+									var node = {
 
 
-					values.push( prevValue[ 1 ] );
+										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
+										initialPosition: [ 0, 0, 0 ],
+										initialRotation: [ 0, 0, 0 ],
+										initialScale: [ 1, 1, 1 ],
+										transform: self.getModelAnimTransform( rawModel ),
 
 
-				}
+									};
 
 
-				if ( zIndex !== - 1 ) {
+									// 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;
 
 
-					var zValue = curves.z.values[ zIndex ];
-					values.push( zValue );
-					prevValue[ 2 ] = zValue;
+									layerCurveNodes[ i ] = node;
 
 
-				} else {
+								}
 
 
-					values.push( prevValue[ 2 ] );
+								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
 
 
-				}
+							} else if ( curveNode.curves.morph !== undefined ) {
 
 
-			} );
+								if ( layerCurveNodes[ i ] === undefined ) {
 
 
-			return values;
+									var deformerID;
 
 
-		},
+									self.connections.get( child.ID ).parents.forEach( function ( parent ) {
 
 
-		// Rotations are defined as Euler angles which can have values  of any size
-		// These will be converted to quaternions which don't support values greater than
-		// PI, so we'll interpolate large rotations
-		interpolateRotations: function ( curve ) {
+										if ( parent.relationship !== undefined ) deformerID = parent.ID;
 
 
-			for ( var i = 1; i < curve.values.length; i ++ ) {
+									} );
 
 
-				var initialValue = curve.values[ i - 1 ];
-				var valuesSpan = curve.values[ i ] - initialValue;
+									var morpherID = self.connections.get( deformerID ).parents[ 0 ].ID;
+									var geoID = self.connections.get( morpherID ).parents[ 0 ].ID;
 
 
-				var absoluteSpan = Math.abs( valuesSpan );
+									// assuming geometry is not used in more than one model
+									var modelID = self.connections.get( geoID ).parents[ 0 ].ID;
 
 
-				if ( absoluteSpan >= 180 ) {
+									var rawModel = self.FBXTree.Objects.Model[ modelID ];
 
 
-					var numSubIntervals = absoluteSpan / 180;
+									var node = {
 
 
-					var step = valuesSpan / numSubIntervals;
-					var nextValue = initialValue + step;
+										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
+										morphName: self.FBXTree.Objects.Deformer[ deformerID ].attrName,
 
 
-					var initialTime = curve.times[ i - 1 ];
-					var timeSpan = curve.times[ i ] - initialTime;
-					var interval = timeSpan / numSubIntervals;
-					var nextTime = initialTime + interval;
+									};
 
 
-					var interpolatedTimes = [];
-					var interpolatedValues = [];
+									layerCurveNodes[ i ] = node;
 
 
-					while ( nextTime < curve.times[ i ] ) {
+								}
 
 
-						interpolatedTimes.push( nextTime );
-						nextTime += interval;
+								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
 
 
-						interpolatedValues.push( nextValue );
-						nextValue += step;
+							}
 
 
-					}
+						}
 
 
-					curve.times = inject( curve.times, i, interpolatedTimes );
-					curve.values = inject( curve.values, i, interpolatedValues );
+					} );
+
+					layersMap.set( parseInt( nodeID ), layerCurveNodes );
 
 
 				}
 				}
 
 
 			}
 			}
 
 
-		},
+			return layersMap;
 
 
-		// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
-		createAmbientLight: function ( sceneGraph ) {
+		},
 
 
-			if ( 'GlobalSettings' in this.FBXTree && 'AmbientColor' in this.FBXTree.GlobalSettings ) {
+		getModelAnimTransform: function ( modelNode ) {
 
 
-				var ambientColor = this.FBXTree.GlobalSettings.AmbientColor.value;
-				var r = ambientColor[ 0 ];
-				var g = ambientColor[ 1 ];
-				var b = ambientColor[ 2 ];
+			var transformData = {};
 
 
-				if ( r !== 0 || g !== 0 || b !== 0 ) {
+			if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value );
 
 
-					var color = new THREE.Color( r, g, b );
-					sceneGraph.add( new THREE.AmbientLight( color, 1 ) );
+			if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value;
+			if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value;
 
 
-				}
+			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;
 
 
-		setupMorphMaterials: function ( sceneGraph ) {
+			return generateTransform( transformData );
 
 
-			sceneGraph.traverse( function ( child ) {
+		},
 
 
-				if ( child.isMesh ) {
+		// 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 ) {
 
 
-					if ( child.geometry.morphAttributes.position || child.geometry.morphAttributes.normal ) {
+			var rawStacks = this.FBXTree.Objects.AnimationStack;
 
 
-						var uuid = child.uuid;
-						var matUuid = child.material.uuid;
+			// connect the stacks (clips) up to the layers
+			var rawClips = {};
 
 
-						// if a geometry has morph targets, it cannot share the material with other geometries
-						var sharedMat = false;
+			for ( var nodeID in rawStacks ) {
 
 
-						sceneGraph.traverse( function ( child ) {
+				var children = this.connections.get( parseInt( nodeID ) ).children;
 
 
-							if ( child.isMesh ) {
+				if ( children.length > 1 ) {
 
 
-								if ( child.material.uuid === matUuid && child.uuid !== uuid ) sharedMat = true;
+					// 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 layer = layersMap.get( children[ 0 ].ID );
 
 
-						if ( sharedMat === true ) child.material = child.material.clone();
+				rawClips[ nodeID ] = {
 
 
-						child.material.morphTargets = true;
+					name: rawStacks[ nodeID ].attrName,
+					layer: layer,
 
 
-					}
+				};
 
 
-				}
+			}
 
 
-			} );
+			return rawClips;
 
 
 		},
 		},