Skip to content

Commit 5e55ef6

Browse files
authored
Merge pull request #15011 from donmccurdy/feat-gltfexporter-multimorphanimations
GLTFExporter: Support individual morph target animation.
2 parents dc59d01 + 4b93acc commit 5e55ef6

File tree

9 files changed

+431
-14
lines changed

9 files changed

+431
-14
lines changed

docs/api/en/animation/AnimationClip.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ <h3>[property:String uuid]</h3>
6666
<h2>Methods</h2>
6767

6868

69+
<h3>[method:AnimationClip clone]()</h3>
70+
<p>
71+
Returns a copy of this clip.
72+
</p>
73+
6974
<h3>[method:this optimize]()</h3>
7075
<p>
7176
Optimizes each track by removing equivalent sequential keys (which are common in morph target

docs/api/en/animation/KeyframeTrack.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ <h3>[property:Constant ValueBufferType ]</h3>
160160
<h2>Methods</h2>
161161

162162

163+
<h3>[method:KeyframeTrack clone]()</h3>
164+
<p>
165+
Returns a copy of this track.
166+
</p>
167+
163168
<h3>[method:null createInterpolant]()</h3>
164169
<p>
165170
Creates a [page:LinearInterpolant LinearInterpolant], [page:CubicInterpolant CubicInterpolant]

docs/api/zh/animation/AnimationClip.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ <h3>[property:String uuid]</h3>
6262
<h2>方法</h2>
6363

6464

65+
<h3>[method:AnimationClip clone]()</h3>
66+
<p></p>
67+
6568
<h3>[method:this optimize]()</h3>
6669
<p>
6770
通过移除等效的顺序键(在变形目标序列中很常见)来优化每一个轨道

docs/api/zh/animation/AnimationUtils.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ <h3>[method:Array getKeyframeOrder]( times )</h3>
3838
返回一个数组,时间和值可以根据此数组排序。
3939
</p>
4040

41+
<h3>[method:Number insertKeyframe]( [param:KeyframeTrack track], [param:Number time] )</h3>
42+
<p></p>
43+
4144
<h3>[method:Boolean isTypedArray]( object )</h3>
4245
<p>
4346
如果该对象是类型化数组,返回*true*
4447

4548
</p>
4649

50+
<h3>[method:AnimationClip mergeMorphTargetTracks]( [param:AnimationClip clip], [param:Object3D root] )</h3>
51+
<p></p>
52+
4753
<h3>[method:Array sortedArray]( values, stride, order )</h3>
4854
<p>
4955
将[page:AnimationUtils.getKeyframeOrder getKeyframeOrder]方法返回的数组排序。

docs/api/zh/animation/KeyframeTrack.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ <h3>[property:Constant ValueBufferType ]</h3>
137137
<h2>方法</h2>
138138

139139

140+
<h3>[method:KeyframeTrack clone]()</h3>
141+
<p></p>
142+
140143
<h3>[method:null createInterpolant]()</h3>
141144
<p>
142145
根据传入构造器中的插值类型参数,创建线性插值([page:LinearInterpolant LinearInterpolant]),立方插值([page:CubicInterpolant CubicInterpolant])或离散插值

examples/js/exporters/GLTFExporter.js

Lines changed: 200 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,9 +1227,9 @@ THREE.GLTFExporter.prototype = {
12271227

12281228
var baseAttribute = geometry.attributes[ attributeName ];
12291229

1230-
if ( cachedData.attributes.has( baseAttribute ) ) {
1230+
if ( cachedData.attributes.has( attribute ) ) {
12311231

1232-
target[ gltfAttributeName ] = cachedData.attributes.get( baseAttribute );
1232+
target[ gltfAttributeName ] = cachedData.attributes.get( attribute );
12331233
continue;
12341234

12351235
}
@@ -1443,12 +1443,15 @@ THREE.GLTFExporter.prototype = {
14431443

14441444
}
14451445

1446+
clip = THREE.GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
1447+
1448+
var tracks = clip.tracks;
14461449
var channels = [];
14471450
var samplers = [];
14481451

1449-
for ( var i = 0; i < clip.tracks.length; ++ i ) {
1452+
for ( var i = 0; i < tracks.length; ++ i ) {
14501453

1451-
var track = clip.tracks[ i ];
1454+
var track = tracks[ i ];
14521455
var trackBinding = THREE.PropertyBinding.parseTrackName( track.name );
14531456
var trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName );
14541457
var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];
@@ -1479,16 +1482,6 @@ THREE.GLTFExporter.prototype = {
14791482

14801483
if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
14811484

1482-
if ( trackNode.morphTargetInfluences.length !== 1 &&
1483-
trackBinding.propertyIndex !== undefined ) {
1484-
1485-
console.warn( 'THREE.GLTFExporter: Skipping animation track "%s". ' +
1486-
'Morph target keyframe tracks must target all available morph targets ' +
1487-
'for the given mesh.', track.name );
1488-
continue;
1489-
1490-
}
1491-
14921485
outputItemSize /= trackNode.morphTargetInfluences.length;
14931486

14941487
}
@@ -2006,3 +1999,196 @@ THREE.GLTFExporter.prototype = {
20061999
}
20072000

20082001
};
2002+
2003+
THREE.GLTFExporter.Utils = {
2004+
2005+
insertKeyframe: function ( track, time ) {
2006+
2007+
var tolerance = 0.001; // 1ms
2008+
var valueSize = track.getValueSize();
2009+
2010+
var times = new track.TimeBufferType( track.times.length + 1 );
2011+
var values = new track.ValueBufferType( track.values.length + valueSize );
2012+
var interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
2013+
2014+
var index;
2015+
2016+
if ( track.times.length === 0 ) {
2017+
2018+
times[ 0 ] = time;
2019+
2020+
for ( var i = 0; i < valueSize; i ++ ) {
2021+
2022+
values[ i ] = 0;
2023+
2024+
}
2025+
2026+
index = 0;
2027+
2028+
} else if ( time < track.times[ 0 ] ) {
2029+
2030+
if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
2031+
2032+
times[ 0 ] = time;
2033+
times.set( track.times, 1 );
2034+
2035+
values.set( interpolant.evaluate( time ), 0 );
2036+
values.set( track.values, valueSize );
2037+
2038+
index = 0;
2039+
2040+
} else if ( time > track.times[ track.times.length - 1 ] ) {
2041+
2042+
if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
2043+
2044+
return track.times.length - 1;
2045+
2046+
}
2047+
2048+
times[ times.length - 1 ] = time;
2049+
times.set( track.times, 0 );
2050+
2051+
values.set( track.values, 0 );
2052+
values.set( interpolant.evaluate( time ), track.values.length );
2053+
2054+
index = times.length - 1;
2055+
2056+
} else {
2057+
2058+
for ( var i = 0; i < track.times.length; i ++ ) {
2059+
2060+
if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
2061+
2062+
if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
2063+
2064+
times.set( track.times.slice( 0, i + 1 ), 0 );
2065+
times[ i + 1 ] = time;
2066+
times.set( track.times.slice( i + 1 ), i + 2 );
2067+
2068+
values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
2069+
values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
2070+
values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
2071+
2072+
index = i + 1;
2073+
2074+
break;
2075+
2076+
}
2077+
2078+
}
2079+
2080+
}
2081+
2082+
track.times = times;
2083+
track.values = values;
2084+
2085+
return index;
2086+
2087+
},
2088+
2089+
mergeMorphTargetTracks: function ( clip, root ) {
2090+
2091+
var tracks = [];
2092+
var mergedTracks = {};
2093+
var sourceTracks = clip.tracks;
2094+
2095+
for ( var i = 0; i < sourceTracks.length; ++ i ) {
2096+
2097+
var sourceTrack = sourceTracks[ i ];
2098+
var sourceTrackBinding = THREE.PropertyBinding.parseTrackName( sourceTrack.name );
2099+
var sourceTrackNode = THREE.PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
2100+
2101+
if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
2102+
2103+
// Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
2104+
tracks.push( sourceTrack );
2105+
continue;
2106+
2107+
}
2108+
2109+
if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete
2110+
&& sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
2111+
2112+
if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
2113+
2114+
// This should never happen, because glTF morph target animations
2115+
// affect all targets already.
2116+
throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
2117+
2118+
}
2119+
2120+
console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
2121+
2122+
sourceTrack = sourceTrack.clone();
2123+
sourceTrack.setInterpolation( InterpolateLinear );
2124+
2125+
}
2126+
2127+
var targetCount = sourceTrackNode.morphTargetInfluences.length;
2128+
var targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
2129+
2130+
if ( targetIndex === undefined ) {
2131+
2132+
throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
2133+
2134+
}
2135+
2136+
var mergedTrack;
2137+
2138+
// If this is the first time we've seen this object, create a new
2139+
// track to store merged keyframe data for each morph target.
2140+
if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
2141+
2142+
mergedTrack = sourceTrack.clone();
2143+
2144+
var values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
2145+
2146+
for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
2147+
2148+
values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
2149+
2150+
}
2151+
2152+
mergedTrack.name = '.morphTargetInfluences';
2153+
mergedTrack.values = values;
2154+
2155+
mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
2156+
tracks.push( mergedTrack );
2157+
2158+
continue;
2159+
2160+
}
2161+
2162+
var mergedKeyframeIndex = 0;
2163+
var sourceKeyframeIndex = 0;
2164+
var sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
2165+
2166+
mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
2167+
2168+
// For every existing keyframe of the merged track, write a (possibly
2169+
// interpolated) value from the source track.
2170+
for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
2171+
2172+
mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
2173+
2174+
}
2175+
2176+
// For every existing keyframe of the source track, write a (possibly
2177+
// new) keyframe to the merged track. Values from the previous loop may
2178+
// be written again, but keyframes are de-duplicated.
2179+
for ( var j = 0; j < sourceTrack.times.length; j ++ ) {
2180+
2181+
var keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
2182+
mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
2183+
2184+
}
2185+
2186+
}
2187+
2188+
clip.tracks = tracks;
2189+
2190+
return clip;
2191+
2192+
}
2193+
2194+
};

src/animation/AnimationClip.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,21 @@ Object.assign( AnimationClip.prototype, {
445445

446446
return this;
447447

448+
},
449+
450+
451+
clone: function () {
452+
453+
var tracks = [];
454+
455+
for ( var i = 0; i < this.tracks.length; i ++ ) {
456+
457+
tracks.push( this.tracks[ i ].clone() );
458+
459+
}
460+
461+
return new AnimationClip( this.name, this.duration, tracks );
462+
448463
}
449464

450465
} );

src/animation/KeyframeTrack.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,21 @@ Object.assign( KeyframeTrack.prototype, {
447447

448448
return this;
449449

450+
},
451+
452+
clone: function () {
453+
454+
var times = AnimationUtils.arraySlice( this.times, 0 );
455+
var values = AnimationUtils.arraySlice( this.values, 0 );
456+
457+
var TypedKeyframeTrack = this.constructor;
458+
var track = new TypedKeyframeTrack( this.name, times, values );
459+
460+
// Interpolant argument to constructor is not saved, so copy the factory method directly.
461+
track.createInterpolant = this.createInterpolant;
462+
463+
return track;
464+
450465
}
451466

452467
} );

0 commit comments

Comments
 (0)