未验证 提交 5e55ef65 编写于 作者: M Mr.doob 提交者: GitHub

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

GLTFExporter: Support individual morph target animation.
......@@ -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
......
......@@ -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]
......
......@@ -62,6 +62,9 @@
<h2>方法</h2>
<h3>[method:AnimationClip clone]()</h3>
<p></p>
<h3>[method:this optimize]()</h3>
<p>
通过移除等效的顺序键(在变形目标序列中很常见)来优化每一个轨道
......
......@@ -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]方法返回的数组排序。
......
......@@ -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])或离散插值
......
......@@ -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;
}
};
......@@ -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 );
}
} );
......
......@@ -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;
}
} );
......
......@@ -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 );
}
} );
} );
} );
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册