瀏覽代碼

Merge pull request #15011 from donmccurdy/feat-gltfexporter-multimorphanimations

GLTFExporter: Support individual morph target animation.
Mr.doob 6 年之前
父節點
當前提交
5e55ef650b

+ 5 - 0
docs/api/en/animation/AnimationClip.html

@@ -66,6 +66,11 @@
 		<h2>Methods</h2>
 
 
+		<h3>[method:AnimationClip clone]()</h3>
+		<p>
+			Returns a copy of this clip.
+		</p>
+
 		<h3>[method:this optimize]()</h3>
 		<p>
 			Optimizes each track by removing equivalent sequential keys (which are common in morph target

+ 5 - 0
docs/api/en/animation/KeyframeTrack.html

@@ -160,6 +160,11 @@
 		<h2>Methods</h2>
 
 
+		<h3>[method:KeyframeTrack clone]()</h3>
+		<p>
+			Returns a copy of this track.
+		</p>
+
 		<h3>[method:null createInterpolant]()</h3>
 		<p>
 			Creates a [page:LinearInterpolant LinearInterpolant], [page:CubicInterpolant CubicInterpolant]

+ 3 - 0
docs/api/zh/animation/AnimationClip.html

@@ -62,6 +62,9 @@
 		<h2>方法</h2>
 
 
+		<h3>[method:AnimationClip clone]()</h3>
+		<p></p>
+
 		<h3>[method:this optimize]()</h3>
 		<p>
             通过移除等效的顺序键(在变形目标序列中很常见)来优化每一个轨道

+ 6 - 0
docs/api/zh/animation/AnimationUtils.html

@@ -38,12 +38,18 @@
             返回一个数组,时间和值可以根据此数组排序。
 		</p>
 
+		<h3>[method:Number insertKeyframe]( [param:KeyframeTrack track], [param:Number time] )</h3>
+		<p></p>
+
 		<h3>[method:Boolean isTypedArray]( object )</h3>
 		<p>
             如果该对象是类型化数组,返回*true*
 
 		</p>
 
+		<h3>[method:AnimationClip mergeMorphTargetTracks]( [param:AnimationClip clip], [param:Object3D root] )</h3>
+		<p></p>
+
 		<h3>[method:Array sortedArray]( values, stride, order )</h3>
 		<p>
             将[page:AnimationUtils.getKeyframeOrder getKeyframeOrder]方法返回的数组排序。

+ 3 - 0
docs/api/zh/animation/KeyframeTrack.html

@@ -137,6 +137,9 @@
 		<h2>方法</h2>
 
 
+		<h3>[method:KeyframeTrack clone]()</h3>
+		<p></p>
+
 		<h3>[method:null createInterpolant]()</h3>
 		<p>
 			根据传入构造器中的插值类型参数,创建线性插值([page:LinearInterpolant LinearInterpolant]),立方插值([page:CubicInterpolant CubicInterpolant])或离散插值

+ 200 - 14
examples/js/exporters/GLTFExporter.js

@@ -1227,9 +1227,9 @@ THREE.GLTFExporter.prototype = {
 
 						var baseAttribute = geometry.attributes[ attributeName ];
 
-						if ( cachedData.attributes.has( baseAttribute ) ) {
+						if ( cachedData.attributes.has( attribute ) ) {
 
-							target[ gltfAttributeName ] = cachedData.attributes.get( baseAttribute );
+							target[ gltfAttributeName ] = cachedData.attributes.get( attribute );
 							continue;
 
 						}
@@ -1443,12 +1443,15 @@ THREE.GLTFExporter.prototype = {
 
 			}
 
+			clip = THREE.GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
+
+			var tracks = clip.tracks;
 			var channels = [];
 			var samplers = [];
 
-			for ( var i = 0; i < clip.tracks.length; ++ i ) {
+			for ( var i = 0; i < tracks.length; ++ i ) {
 
-				var track = clip.tracks[ i ];
+				var track = tracks[ i ];
 				var trackBinding = THREE.PropertyBinding.parseTrackName( track.name );
 				var trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName );
 				var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];
@@ -1479,16 +1482,6 @@ THREE.GLTFExporter.prototype = {
 
 				if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
 
-					if ( trackNode.morphTargetInfluences.length !== 1 &&
-						trackBinding.propertyIndex !== undefined ) {
-
-						console.warn( 'THREE.GLTFExporter: Skipping animation track "%s". ' +
-							'Morph target keyframe tracks must target all available morph targets ' +
-							'for the given mesh.', track.name );
-						continue;
-
-					}
-
 					outputItemSize /= trackNode.morphTargetInfluences.length;
 
 				}
@@ -2006,3 +1999,196 @@ THREE.GLTFExporter.prototype = {
 	}
 
 };
+
+THREE.GLTFExporter.Utils = {
+
+	insertKeyframe: function ( track, time ) {
+
+		var tolerance = 0.001; // 1ms
+		var valueSize = track.getValueSize();
+
+		var times = new track.TimeBufferType( track.times.length + 1 );
+		var values = new track.ValueBufferType( track.values.length + valueSize );
+		var interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
+
+		var index;
+
+		if ( track.times.length === 0 ) {
+
+			times[ 0 ] = time;
+
+			for ( var i = 0; i < valueSize; i ++ ) {
+
+				values[ i ] = 0;
+
+			}
+
+			index = 0;
+
+		} else if ( time < track.times[ 0 ] ) {
+
+			if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
+
+			times[ 0 ] = time;
+			times.set( track.times, 1 );
+
+			values.set( interpolant.evaluate( time ), 0 );
+			values.set( track.values, valueSize );
+
+			index = 0;
+
+		} else if ( time > track.times[ track.times.length - 1 ] ) {
+
+			if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
+
+				return track.times.length - 1;
+
+			}
+
+			times[ times.length - 1 ] = time;
+			times.set( track.times, 0 );
+
+			values.set( track.values, 0 );
+			values.set( interpolant.evaluate( time ), track.values.length );
+
+			index = times.length - 1;
+
+		} else {
+
+			for ( var i = 0; i < track.times.length; i ++ ) {
+
+				if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
+
+				if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
+
+					times.set( track.times.slice( 0, i + 1 ), 0 );
+					times[ i + 1 ] = time;
+					times.set( track.times.slice( i + 1 ), i + 2 );
+
+					values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
+					values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
+					values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
+
+					index = i + 1;
+
+					break;
+
+				}
+
+			}
+
+		}
+
+		track.times = times;
+		track.values = values;
+
+		return index;
+
+	},
+
+	mergeMorphTargetTracks: function ( clip, root ) {
+
+		var tracks = [];
+		var mergedTracks = {};
+		var sourceTracks = clip.tracks;
+
+		for ( var i = 0; i < sourceTracks.length; ++ i ) {
+
+			var sourceTrack = sourceTracks[ i ];
+			var sourceTrackBinding = THREE.PropertyBinding.parseTrackName( sourceTrack.name );
+			var sourceTrackNode = THREE.PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
+
+			if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
+
+				// Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
+				tracks.push( sourceTrack );
+				continue;
+
+			}
+
+			if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete
+				&& sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
+
+				if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
+
+					// This should never happen, because glTF morph target animations
+					// affect all targets already.
+					throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
+
+				}
+
+				console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
+
+				sourceTrack = sourceTrack.clone();
+				sourceTrack.setInterpolation( InterpolateLinear );
+
+			}
+
+			var targetCount = sourceTrackNode.morphTargetInfluences.length;
+			var targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
+
+			if ( targetIndex === undefined ) {
+
+				throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
+
+			}
+
+			var mergedTrack;
+
+			// If this is the first time we've seen this object, create a new
+			// track to store merged keyframe data for each morph target.
+			if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
+
+				mergedTrack = sourceTrack.clone();
+
+				var values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
+
+				for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
+
+					values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
+
+				}
+
+				mergedTrack.name = '.morphTargetInfluences';
+				mergedTrack.values = values;
+
+				mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
+				tracks.push( mergedTrack );
+
+				continue;
+
+			}
+
+			var mergedKeyframeIndex = 0;
+			var sourceKeyframeIndex = 0;
+			var sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
+
+			mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
+
+			// For every existing keyframe of the merged track, write a (possibly
+			// interpolated) value from the source track.
+			for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
+
+				mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
+
+			}
+
+			// For every existing keyframe of the source track, write a (possibly
+			// new) keyframe to the merged track. Values from the previous loop may
+			// be written again, but keyframes are de-duplicated.
+			for ( var j = 0; j < sourceTrack.times.length; j ++ ) {
+
+				var keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
+				mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
+
+			}
+
+		}
+
+		clip.tracks = tracks;
+
+		return clip;
+
+	}
+
+};

+ 15 - 0
src/animation/AnimationClip.js

@@ -445,6 +445,21 @@ Object.assign( AnimationClip.prototype, {
 
 		return this;
 
+	},
+
+
+	clone: function () {
+
+		var tracks = [];
+
+		for ( var i = 0; i < this.tracks.length; i ++ ) {
+
+			tracks.push( this.tracks[ i ].clone() );
+
+		}
+
+		return new AnimationClip( this.name, this.duration, tracks );
+
 	}
 
 } );

+ 15 - 0
src/animation/KeyframeTrack.js

@@ -447,6 +447,21 @@ Object.assign( KeyframeTrack.prototype, {
 
 		return this;
 
+	},
+
+	clone: function () {
+
+		var times = AnimationUtils.arraySlice( this.times, 0 );
+		var values = AnimationUtils.arraySlice( this.values, 0 );
+
+		var TypedKeyframeTrack = this.constructor;
+		var track = new TypedKeyframeTrack( this.name, times, values );
+
+		// Interpolant argument to constructor is not saved, so copy the factory method directly.
+		track.createInterpolant = this.createInterpolant;
+
+		return track;
+
 	}
 
 } );

+ 179 - 0
test/unit/example/exporters/GLTFExporter.tests.js

@@ -5,6 +5,19 @@
 
 import * as GLTFExporter from '../../../../examples/js/exporters/GLTFExporter';
 
+import { AnimationClip } from '../../../../src/animation/AnimationClip';
+import { BufferAttribute } from '../../../../src/core/BufferAttribute';
+import { BufferGeometry } from '../../../../src/core/BufferGeometry';
+import { Mesh } from '../../../../src/objects/Mesh';
+import { Object3D } from '../../../../src/core/Object3D';
+import { NumberKeyframeTrack } from '../../../../src/animation/tracks/NumberKeyframeTrack';
+import { VectorKeyframeTrack } from '../../../../src/animation/tracks/VectorKeyframeTrack';
+import {
+  InterpolateLinear,
+  InterpolateSmooth,
+  InterpolateDiscrete
+} from '../../../../src/constants.js';
+
 export default QUnit.module( 'Exporters', () => {
 
   QUnit.module( 'GLTFExporter', () => {
@@ -156,6 +169,172 @@ export default QUnit.module( 'Exporters', () => {
 
     } );
 
+    QUnit.test( 'parse - individual morph targets', ( assert ) => {
+
+      var done = assert.async();
+
+      // Creates a geometry with four (4) morph targets, three (3) of which are
+      // animated by an animation clip. Because glTF requires all morph targets
+      // to be animated in unison, the exporter should write an empty track for
+      // the fourth target.
+
+      var geometry = new THREE.BufferGeometry();
+      var position = new THREE.BufferAttribute( new Float32Array( [ 0, 0, 0, 0, 0, 1, 1, 0, 1 ] ), 3 );
+      geometry.addAttribute( 'position',  position );
+      geometry.morphAttributes.position = [ position, position, position, position ];
+
+      var mesh = new THREE.Mesh( geometry );
+      mesh.morphTargetDictionary.a = 0;
+      mesh.morphTargetDictionary.b = 1;
+      mesh.morphTargetDictionary.c = 2;
+      mesh.morphTargetDictionary.d = 3;
+
+      var timesA =  [ 0, 1, 2 ];
+      var timesB =  [       2, 3, 4 ];
+      var timesC =  [             4, 5, 6 ];
+      var valuesA = [ 0, 1, 0 ];
+      var valuesB = [       0, 1, 0 ];
+      var valuesC = [             0, 1, 0 ];
+      var trackA = new THREE.VectorKeyframeTrack( '.morphTargetInfluences[a]', timesA, valuesA, THREE.InterpolateLinear );
+      var trackB = new THREE.VectorKeyframeTrack( '.morphTargetInfluences[b]', timesB, valuesB, THREE.InterpolateLinear );
+      var trackC = new THREE.VectorKeyframeTrack( '.morphTargetInfluences[c]', timesC, valuesC, THREE.InterpolateLinear );
+
+      var clip = new THREE.AnimationClip( 'clip1', undefined, [ trackA, trackB, trackC ] );
+
+      var exporter = new THREE.GLTFExporter();
+
+      exporter.parse( mesh, function ( gltf ) {
+
+        assert.equal( 1, gltf.animations.length, 'one animation' );
+        assert.equal( 1, gltf.animations[ 0 ].channels.length, 'one channel' );
+        assert.equal( 1, gltf.animations[ 0 ].samplers.length, 'one sampler' );
+
+        var channel = gltf.animations[ 0 ].channels[ 0 ];
+        var sampler = gltf.animations[ 0 ].samplers[ 0 ];
+
+        assert.smartEqual( channel, { sampler: 0, target: { node: 0, path: 'weights' } } );
+        assert.equal( sampler.interpolation, 'LINEAR' );
+
+        var input = gltf.accessors[ sampler.input ];
+        var output = gltf.accessors[ sampler.output ];
+
+        assert.equal( input.count, 7 );
+        assert.equal( input.type, 'SCALAR' );
+        assert.smartEqual( input.min, [ 0 ] );
+        assert.smartEqual( input.max, [ 6 ] );
+
+        assert.equal( output.count, 28 ); // 4 targets * 7 frames
+        assert.equal( output.type, 'SCALAR' );
+        assert.smartEqual( output.min, [ 0 ] );
+        assert.smartEqual( output.max, [ 1 ] );
+
+        done();
+
+      }, { animations: [ clip ] } );
+
+    } );
+
+    QUnit.test( 'utils - insertKeyframe', ( assert ) => {
+
+      var track;
+      var index;
+
+      function createTrack () {
+        return new VectorKeyframeTrack(
+          'foo.bar',
+          [ 5,    10,   15,   20,   25,   30 ],
+          [ 0, 5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0 ],
+          InterpolateLinear
+        );
+      }
+
+      track = createTrack();
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 0 );
+      assert.equal( index, 0, 'prepend - index' );
+      assert.smartEqual( Array.from( track.times ), [ 0, 5, 10, 15, 20, 25, 30 ], 'prepend - time' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 0, 5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0 ], 'prepend - value' );
+
+      track = createTrack();
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 7.5 );
+      assert.equal( index, 1, 'insert - index (linear)' );
+      assert.smartEqual( Array.from( track.times ), [ 5, 7.5, 10, 15, 20, 25, 30 ], 'insert - time (linear)' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 0.5, 4.5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0 ], 'insert - value (linear)' );
+
+      track = createTrack();
+      track.setInterpolation( InterpolateDiscrete );
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 16 );
+      assert.equal( index, 3, 'insert - index (linear)' );
+      assert.smartEqual( Array.from( track.times ), [ 5, 10, 15, 16, 20, 25, 30 ], 'insert - time (discrete)' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 1, 4, 2, 3, 2, 3, 3, 2, 4, 1, 5, 0 ], 'insert - value (discrete)' );
+
+      track = createTrack();
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 100 );
+      assert.equal( index, 6, 'append - index' );
+      assert.smartEqual( Array.from( track.times ), [ 5, 10, 15, 20, 25, 30, 100 ], 'append time' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0, 5, 0 ], 'append value' );
+
+      track = createTrack();
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 15 );
+      assert.equal( index, 2, 'existing - index' );
+      assert.smartEqual( Array.from( track.times ), [ 5, 10, 15, 20, 25, 30 ], 'existing - time' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0 ], 'existing - value' );
+
+      track = createTrack();
+      index = THREE.GLTFExporter.Utils.insertKeyframe( track, 20.000005 );
+      assert.equal( index, 3, 'tolerance - index' );
+      assert.smartEqual( Array.from( track.times ), [ 5, 10, 15, 20, 25, 30 ], 'tolerance - time' );
+      assert.smartEqual( Array.from( track.values ), [ 0, 5, 1, 4, 2, 3, 3, 2, 4, 1, 5, 0 ], 'tolerance - value' );
+
+    } );
+
+    QUnit.test( 'utils - mergeMorphTargetTracks', ( assert ) => {
+
+      var trackA = new NumberKeyframeTrack(
+        'foo.morphTargetInfluences[a]',
+        [ 5, 10, 15, 20, 25, 30 ],
+        [ 0, 0.2, 0.4, 0.6, 0.8, 1.0 ],
+        InterpolateLinear
+      );
+
+      var trackB = new NumberKeyframeTrack(
+        'foo.morphTargetInfluences[b]',
+        [ 10, 50 ],
+        [ 0.25, 0.75 ],
+        InterpolateLinear
+      );
+
+      var geometry = new BufferGeometry();
+      var position = new BufferAttribute( new Float32Array( [ 0, 0, 0, 0, 0, 1, 1, 0, 1 ] ), 3 );
+      geometry.addAttribute( 'position',  position );
+      geometry.morphAttributes.position = [ position, position ];
+
+      var mesh = new Mesh( geometry );
+      mesh.name = 'foo';
+      mesh.morphTargetDictionary.a = 0;
+      mesh.morphTargetDictionary.b = 1;
+
+      var root = new Object3D();
+      root.add( mesh );
+
+      var clip = new AnimationClip( 'waltz', undefined, [ trackA, trackB ] );
+      clip = THREE.GLTFExporter.Utils.mergeMorphTargetTracks( clip, root );
+
+      assert.equal( clip.tracks.length, 1, 'tracks are merged' );
+
+      var track = clip.tracks[ 0 ];
+
+      assert.smartEqual( Array.from( track.times ), [ 5, 10, 15, 20, 25, 30, 50 ], 'all keyframes are present' );
+
+      var expectedValues = [ 0, 0.25, 0.2, 0.25, 0.4, 0.3125, 0.6, 0.375, 0.8, 0.4375, 1.0, 0.5, 1.0, 0.75 ];
+
+      for ( var i = 0; i < track.values.length; i ++ ) {
+
+        assert.numEqual( track.values[ i ], expectedValues[ i ], 'all values are merged or interpolated - ' + i );
+
+      }
+
+    } );
+
   } );
 
 } );