Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GLTFExporter: Support individual morph target animation. #15011

Merged
merged 10 commits into from
Jan 25, 2019
5 changes: 5 additions & 0 deletions docs/api/en/animation/AnimationClip.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ <h3>[property:String uuid]</h3>
<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
Expand Down
5 changes: 5 additions & 0 deletions docs/api/en/animation/KeyframeTrack.html
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ <h3>[property:Constant ValueBufferType ]</h3>
<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]
Expand Down
3 changes: 3 additions & 0 deletions docs/api/zh/animation/AnimationClip.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ <h3>[property:String uuid]</h3>
<h2>方法</h2>


<h3>[method:AnimationClip clone]()</h3>
<p></p>

<h3>[method:this optimize]()</h3>
<p>
通过移除等效的顺序键(在变形目标序列中很常见)来优化每一个轨道
Expand Down
6 changes: 6 additions & 0 deletions docs/api/zh/animation/AnimationUtils.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ <h3>[method:Array getKeyframeOrder]( times )</h3>
返回一个数组,时间和值可以根据此数组排序。
</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]方法返回的数组排序。
Expand Down
3 changes: 3 additions & 0 deletions docs/api/zh/animation/KeyframeTrack.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ <h3>[property:Constant ValueBufferType ]</h3>
<h2>方法</h2>


<h3>[method:KeyframeTrack clone]()</h3>
<p></p>

<h3>[method:null createInterpolant]()</h3>
<p>
根据传入构造器中的插值类型参数,创建线性插值([page:LinearInterpolant LinearInterpolant]),立方插值([page:CubicInterpolant CubicInterpolant])或离散插值
Expand Down
214 changes: 200 additions & 14 deletions examples/js/exporters/GLTFExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1192,9 +1192,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;

}
Expand Down Expand Up @@ -1408,12 +1408,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 ];
Expand Down Expand Up @@ -1444,16 +1447,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;

}
Expand Down Expand Up @@ -1907,3 +1900,196 @@ THREE.GLTFExporter.prototype = {
}

};

THREE.GLTFExporter.Utils = {

insertKeyframe: function ( track, time ) {

var tolerance = 0.001; // 1ms
fernandojsg marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 15 additions & 0 deletions src/animation/AnimationClip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

}

} );
Expand Down
15 changes: 15 additions & 0 deletions src/animation/KeyframeTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}

} );
Expand Down
Loading