|
@@ -763,7 +763,6 @@ THREE.FBXLoader = ( function () {
|
|
|
|
|
|
},
|
|
},
|
|
|
|
|
|
-
|
|
|
|
// create the main THREE.Group() to be returned by the loader
|
|
// create the main THREE.Group() to be returned by the loader
|
|
parseScene: function ( deformers, geometryMap, materialMap ) {
|
|
parseScene: function ( deformers, geometryMap, materialMap ) {
|
|
|
|
|
|
@@ -798,20 +797,23 @@ THREE.FBXLoader = ( function () {
|
|
} );
|
|
} );
|
|
|
|
|
|
this.bindSkeleton( deformers.skeletons, geometryMap, modelMap );
|
|
this.bindSkeleton( deformers.skeletons, geometryMap, modelMap );
|
|
- this.addAnimations( sceneGraph );
|
|
|
|
|
|
|
|
this.createAmbientLight( sceneGraph );
|
|
this.createAmbientLight( sceneGraph );
|
|
|
|
|
|
this.setupMorphMaterials( 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 all the models where already combined in a single group, just return that
|
|
if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) {
|
|
if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) {
|
|
|
|
|
|
- sceneGraph.children[ 0 ].animations = sceneGraph.animations;
|
|
|
|
|
|
+ sceneGraph.children[ 0 ].animations = animations;
|
|
return sceneGraph.children[ 0 ];
|
|
return sceneGraph.children[ 0 ];
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ sceneGraph.animations = animations;
|
|
|
|
+
|
|
return sceneGraph;
|
|
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;
|
|
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 );
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
|
|
},
|
|
},
|
|
|
|
|