From 34647a14e30ae58bd1fef9ff266f0050517c55e1 Mon Sep 17 00:00:00 2001 From: Lewy Blue Date: Mon, 30 Jul 2018 21:00:43 +0100 Subject: [PATCH] create AnimationParser --- examples/js/loaders/FBXLoader.js | 714 ++++++++++++++++--------------- 1 file changed, 363 insertions(+), 351 deletions(-) diff --git a/examples/js/loaders/FBXLoader.js b/examples/js/loaders/FBXLoader.js index 471aa867be..ae93a78d72 100644 --- a/examples/js/loaders/FBXLoader.js +++ b/examples/js/loaders/FBXLoader.js @@ -2174,7 +2174,7 @@ THREE.FBXLoader = ( function () { sceneGraph.animations = []; - var rawClips = this.parseAnimations(); + var rawClips = new AnimationParser().parse( this.FBXTree, this.connections ); 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; }, -- GitLab