diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 3e152531ed29dc..a834398d8cd945 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -601,7 +601,8 @@ class FBXTreeParser { } - // the transparency handling is implemented based on Blender/Unity's approach: https://github.com/sobotka/blender-addons/blob/7d80f2f97161fc8e353a657b179b9aa1f8e5280b/io_scene_fbx/import_fbx.py#L1444-L1459 + // the transparency handling is implemented based on Blender's approach: + // https://github.com/blender/blender/blob/main/scripts/addons_core/io_scene_fbx/import_fbx.py parameters.opacity = 1 - ( materialNode.TransparencyFactor ? parseFloat( materialNode.TransparencyFactor.value ) : 0 ); @@ -611,7 +612,10 @@ class FBXTreeParser { if ( parameters.opacity === null ) { - parameters.opacity = 1 - ( materialNode.TransparentColor ? parseFloat( materialNode.TransparentColor.value[ 0 ] ) : 0 ); + // Default to opaque. Some exporters (e.g. 3ds Max) define TransparentColor + // as white (1,1,1) without intending transparency, which makes the Unity-style + // fallback of `1 - TransparentColor.r` produce incorrect zero opacity. + parameters.opacity = 1; } @@ -2779,7 +2783,13 @@ class AnimationParser { node.transform = child.matrix; - if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder; + if ( child.userData.transformData ) { + + node.eulerOrder = child.userData.transformData.eulerOrder; + + if ( child.userData.transformData.rotation ) node.initialRotation = child.userData.transformData.rotation; + + } } @@ -2919,7 +2929,7 @@ class AnimationParser { if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { - const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder ); + const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder, rawTracks.initialRotation ); if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); } @@ -2951,17 +2961,33 @@ class AnimationParser { } - generateRotationTrack( modelName, curves, preRotation, postRotation, eulerOrder ) { + generateRotationTrack( modelName, curves, preRotation, postRotation, eulerOrder, initialRotation ) { let times; let values; - if ( curves.x !== undefined && curves.y !== undefined && curves.z !== undefined ) { + if ( curves.x !== undefined || curves.y !== undefined || curves.z !== undefined ) { + + // Get merged, sorted, unique times from all available curves + const mergedTimes = this.getTimesForAllAxes( curves ); + + if ( mergedTimes.length > 0 ) { - const result = this.interpolateRotations( curves.x, curves.y, curves.z, eulerOrder ); + const initialRot = initialRotation || [ 0, 0, 0 ]; - times = result[ 0 ]; - values = result[ 1 ]; + // Synchronize all curves to the merged time array. + // Missing axes are filled with constant values from the initial rotation (Lcl Rotation). + // Existing curves at different times are linearly interpolated. + const syncX = this.synchronizeCurve( curves.x, mergedTimes, initialRot[ 0 ] ); + const syncY = this.synchronizeCurve( curves.y, mergedTimes, initialRot[ 1 ] ); + const syncZ = this.synchronizeCurve( curves.z, mergedTimes, initialRot[ 2 ] ); + + const result = this.interpolateRotations( syncX, syncY, syncZ, eulerOrder ); + + times = result[ 0 ]; + values = result[ 1 ]; + + } } @@ -2993,7 +3019,7 @@ class AnimationParser { const quaternionValues = []; - if ( ! values || ! times ) return new QuaternionKeyframeTrack( modelName + '.quaternion', [ 0 ], [ 0 ] ); + if ( ! values || ! times ) return undefined; for ( let i = 0; i < values.length; i += 3 ) { @@ -3146,6 +3172,62 @@ class AnimationParser { } + // Synchronize a curve to a target time array using linear interpolation. + // If the curve is undefined (axis not animated), returns constant values from initialValue. + synchronizeCurve( curve, targetTimes, initialValue ) { + + if ( curve === undefined ) { + + return { times: targetTimes, values: targetTimes.map( () => initialValue ) }; + + } + + // If the curve already has the same number of keyframes as the target, assume times match + if ( curve.times.length === targetTimes.length ) return curve; + + // Linearly interpolate curve values at each target time + const values = []; + + for ( let i = 0; i < targetTimes.length; i ++ ) { + + values.push( this.sampleCurveValue( curve, targetTimes[ i ], initialValue ) ); + + } + + return { times: targetTimes, values: values }; + + } + + // Sample a single value from a curve at a given time using linear interpolation + sampleCurveValue( curve, time, initialValue ) { + + const times = curve.times; + const values = curve.values; + + // Before first keyframe + if ( time <= times[ 0 ] ) return values[ 0 ]; + + // After last keyframe + if ( time >= times[ times.length - 1 ] ) return values[ values.length - 1 ]; + + // Find surrounding keyframes and linearly interpolate + for ( let i = 0; i < times.length - 1; i ++ ) { + + if ( time >= times[ i ] && time <= times[ i + 1 ] ) { + + if ( times[ i ] === time ) return values[ i ]; + + const alpha = ( time - times[ i ] ) / ( times[ i + 1 ] - times[ i ] ); + return values[ i ] * ( 1 - alpha ) + values[ i + 1 ] * alpha; + + } + + } + + return initialValue; + + } + // Rotations are defined as Euler angles which can have values of any size // These will be converted to quaternions which don't support values greater than // PI, so we'll interpolate large rotations @@ -3215,7 +3297,7 @@ class AnimationParser { const Q2 = new Quaternion().setFromEuler( E2 ); // Check unroll - if ( Q1.dot( Q2 ) ) { + if ( Q1.dot( Q2 ) < 0 ) { Q2.set( - Q2.x, - Q2.y, - Q2.z, - Q2.w ); diff --git a/examples/models/fbx/Head_69.fbx b/examples/models/fbx/Head_69.fbx new file mode 100755 index 00000000000000..b10d2fafd4f027 Binary files /dev/null and b/examples/models/fbx/Head_69.fbx differ diff --git a/examples/models/fbx/RotationTest.fbx b/examples/models/fbx/RotationTest.fbx new file mode 100644 index 00000000000000..abe3cf04b3a2ae Binary files /dev/null and b/examples/models/fbx/RotationTest.fbx differ diff --git a/examples/models/fbx/archer/ArcherRi01.FBX b/examples/models/fbx/archer/ArcherRi01.fbx similarity index 100% rename from examples/models/fbx/archer/ArcherRi01.FBX rename to examples/models/fbx/archer/ArcherRi01.fbx diff --git a/examples/models/fbx/exampleWindow.fbx b/examples/models/fbx/exampleWindow.fbx new file mode 100644 index 00000000000000..dbaa72f653a3b7 Binary files /dev/null and b/examples/models/fbx/exampleWindow.fbx differ diff --git a/examples/webgl_loader_fbx.html b/examples/webgl_loader_fbx.html index 4d4a3b18e77cfa..44e0de4898324e 100644 --- a/examples/webgl_loader_fbx.html +++ b/examples/webgl_loader_fbx.html @@ -54,12 +54,16 @@ 'warrior/Warrior', 'stanford-bunny', 'mixamo', + 'RotationTest', + 'exampleWindow', + 'Head_69', ]; const scales = new Map(); scales.set( 'warrior/Warrior', 100 ); scales.set( 'archer/ArcherRi01', 100 ); scales.set( 'stanford-bunny', 0.001 ); + scales.set( 'Head_69', 100 ); init();