Skip to content

Commit 29ee962

Browse files
authored
FBXLoader: Fix rotation animations. (#33323)
1 parent 965f22f commit 29ee962

3 files changed

Lines changed: 88 additions & 9 deletions

File tree

examples/jsm/loaders/FBXLoader.js

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,7 +2779,13 @@ class AnimationParser {
27792779

27802780
node.transform = child.matrix;
27812781

2782-
if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder;
2782+
if ( child.userData.transformData ) {
2783+
2784+
node.eulerOrder = child.userData.transformData.eulerOrder;
2785+
2786+
if ( child.userData.transformData.rotation ) node.initialRotation = child.userData.transformData.rotation;
2787+
2788+
}
27832789

27842790
}
27852791

@@ -2919,7 +2925,7 @@ class AnimationParser {
29192925

29202926
if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {
29212927

2922-
const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder );
2928+
const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder, rawTracks.initialRotation );
29232929
if ( rotationTrack !== undefined ) tracks.push( rotationTrack );
29242930

29252931
}
@@ -2951,17 +2957,33 @@ class AnimationParser {
29512957

29522958
}
29532959

2954-
generateRotationTrack( modelName, curves, preRotation, postRotation, eulerOrder ) {
2960+
generateRotationTrack( modelName, curves, preRotation, postRotation, eulerOrder, initialRotation ) {
29552961

29562962
let times;
29572963
let values;
29582964

2959-
if ( curves.x !== undefined && curves.y !== undefined && curves.z !== undefined ) {
2965+
if ( curves.x !== undefined || curves.y !== undefined || curves.z !== undefined ) {
2966+
2967+
// Get merged, sorted, unique times from all available curves
2968+
const mergedTimes = this.getTimesForAllAxes( curves );
2969+
2970+
if ( mergedTimes.length > 0 ) {
29602971

2961-
const result = this.interpolateRotations( curves.x, curves.y, curves.z, eulerOrder );
2972+
const initialRot = initialRotation || [ 0, 0, 0 ];
29622973

2963-
times = result[ 0 ];
2964-
values = result[ 1 ];
2974+
// Synchronize all curves to the merged time array.
2975+
// Missing axes are filled with constant values from the initial rotation (Lcl Rotation).
2976+
// Existing curves at different times are linearly interpolated.
2977+
const syncX = this.synchronizeCurve( curves.x, mergedTimes, initialRot[ 0 ] );
2978+
const syncY = this.synchronizeCurve( curves.y, mergedTimes, initialRot[ 1 ] );
2979+
const syncZ = this.synchronizeCurve( curves.z, mergedTimes, initialRot[ 2 ] );
2980+
2981+
const result = this.interpolateRotations( syncX, syncY, syncZ, eulerOrder );
2982+
2983+
times = result[ 0 ];
2984+
values = result[ 1 ];
2985+
2986+
}
29652987

29662988
}
29672989

@@ -2993,7 +3015,7 @@ class AnimationParser {
29933015

29943016
const quaternionValues = [];
29953017

2996-
if ( ! values || ! times ) return new QuaternionKeyframeTrack( modelName + '.quaternion', [ 0 ], [ 0 ] );
3018+
if ( ! values || ! times ) return undefined;
29973019

29983020
for ( let i = 0; i < values.length; i += 3 ) {
29993021

@@ -3146,6 +3168,62 @@ class AnimationParser {
31463168

31473169
}
31483170

3171+
// Synchronize a curve to a target time array using linear interpolation.
3172+
// If the curve is undefined (axis not animated), returns constant values from initialValue.
3173+
synchronizeCurve( curve, targetTimes, initialValue ) {
3174+
3175+
if ( curve === undefined ) {
3176+
3177+
return { times: targetTimes, values: targetTimes.map( () => initialValue ) };
3178+
3179+
}
3180+
3181+
// If the curve already has the same number of keyframes as the target, assume times match
3182+
if ( curve.times.length === targetTimes.length ) return curve;
3183+
3184+
// Linearly interpolate curve values at each target time
3185+
const values = [];
3186+
3187+
for ( let i = 0; i < targetTimes.length; i ++ ) {
3188+
3189+
values.push( this.sampleCurveValue( curve, targetTimes[ i ], initialValue ) );
3190+
3191+
}
3192+
3193+
return { times: targetTimes, values: values };
3194+
3195+
}
3196+
3197+
// Sample a single value from a curve at a given time using linear interpolation
3198+
sampleCurveValue( curve, time, initialValue ) {
3199+
3200+
const times = curve.times;
3201+
const values = curve.values;
3202+
3203+
// Before first keyframe
3204+
if ( time <= times[ 0 ] ) return values[ 0 ];
3205+
3206+
// After last keyframe
3207+
if ( time >= times[ times.length - 1 ] ) return values[ values.length - 1 ];
3208+
3209+
// Find surrounding keyframes and linearly interpolate
3210+
for ( let i = 0; i < times.length - 1; i ++ ) {
3211+
3212+
if ( time >= times[ i ] && time <= times[ i + 1 ] ) {
3213+
3214+
if ( times[ i ] === time ) return values[ i ];
3215+
3216+
const alpha = ( time - times[ i ] ) / ( times[ i + 1 ] - times[ i ] );
3217+
return values[ i ] * ( 1 - alpha ) + values[ i + 1 ] * alpha;
3218+
3219+
}
3220+
3221+
}
3222+
3223+
return initialValue;
3224+
3225+
}
3226+
31493227
// Rotations are defined as Euler angles which can have values of any size
31503228
// These will be converted to quaternions which don't support values greater than
31513229
// PI, so we'll interpolate large rotations
@@ -3215,7 +3293,7 @@ class AnimationParser {
32153293
const Q2 = new Quaternion().setFromEuler( E2 );
32163294

32173295
// Check unroll
3218-
if ( Q1.dot( Q2 ) ) {
3296+
if ( Q1.dot( Q2 ) < 0 ) {
32193297

32203298
Q2.set( - Q2.x, - Q2.y, - Q2.z, - Q2.w );
32213299

18.8 KB
Binary file not shown.

examples/webgl_loader_fbx.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
'warrior/Warrior',
5555
'stanford-bunny',
5656
'mixamo',
57+
'RotationTest'
5758
];
5859

5960
const scales = new Map();

0 commit comments

Comments
 (0)