diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 6a25cc3ab29cf1..98181ddc8eb653 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -824,8 +824,6 @@ class FBXTreeParser { indices: [], weights: [], transformLink: new Matrix4().fromArray( boneNode.TransformLink.a ), - // transform: new Matrix4().fromArray( boneNode.Transform.a ), - // linkMode: boneNode.Mode, }; @@ -918,8 +916,6 @@ class FBXTreeParser { } ); - this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); - this.addGlobalSceneSettings(); sceneGraph.traverse( function ( node ) { @@ -942,6 +938,64 @@ class FBXTreeParser { } ); + // Like Blender's FBX importer, use the BindPose section to set the + // rest pose for bones that are not part of a skin cluster. The BindPose + // provides a more authoritative rest pose than the Lcl properties which + // may represent an animation frame rather than the true rest state. + // Bones WITH clusters will get their bind pose from TransformLink + // (set via bindSkeleton below), which takes priority. + const bindPoseMatrices = this.parsePoseNodes(); + const clusterBoneIDs = new Set(); + + for ( const ID in deformers.skeletons ) { + + deformers.skeletons[ ID ].rawBones.forEach( function ( _, i ) { + + const bone = deformers.skeletons[ ID ].bones[ i ]; + if ( bone ) clusterBoneIDs.add( bone.ID ); + + } ); + + } + + const tempMatrix = new Matrix4(); + + sceneGraph.traverse( function ( node ) { + + if ( node.isBone && node.ID !== undefined && ! clusterBoneIDs.has( node.ID ) ) { + + const bindPose = bindPoseMatrices[ node.ID ]; + + if ( bindPose !== undefined ) { + + if ( node.parent ) { + + tempMatrix.copy( node.parent.matrixWorld ).invert(); + tempMatrix.multiply( bindPose ); + + } else { + + tempMatrix.copy( bindPose ); + + } + + tempMatrix.decompose( node.position, node.quaternion, node.scale ); + node.updateMatrix(); + node.matrixWorld.copy( bindPose ); + + } + + } + + } ); + + // Bind skeletons after transforms are applied so that bind matrices + // are computed from the final scene state. This ensures the rest pose + // is correct even when the FBX file's Cluster TransformLink matrices + // differ from the reconstructed bone transforms (common in files + // without a BindPose section). + this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); + const animations = new AnimationParser().parse(); // if all the models where already combined in a single group, just return that @@ -1460,12 +1514,30 @@ class FBXTreeParser { bindSkeleton( skeletons, geometryMap, modelMap ) { - const bindMatrices = this.parsePoseNodes(); - for ( const ID in skeletons ) { const skeleton = skeletons[ ID ]; + // Compute bone inverses from TransformLink rather than from the + // bones' current matrixWorld. The TransformLink matrices represent + // each bone's global transform at the time the skin weights were + // painted, which may differ from the scene-reconstructed transforms. + const boneInverses = []; + + for ( let i = 0, l = skeleton.bones.length; i < l; i ++ ) { + + const inverse = new Matrix4(); + + if ( skeleton.bones[ i ] && skeleton.rawBones[ i ] ) { + + inverse.copy( skeleton.rawBones[ i ].transformLink ).invert(); + + } + + boneInverses.push( inverse ); + + } + const parents = connections.get( parseInt( skeleton.ID ) ).parents; parents.forEach( function ( parent ) { @@ -1481,7 +1553,16 @@ class FBXTreeParser { const model = modelMap.get( geoConnParent.ID ); - model.bind( new Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); + // Use the mesh's current matrixWorld as bind matrix. + // The BindPose section is intentionally not used here + // since it may contain scale/rotation from the model + // hierarchy that is inconsistent with the TransformLink- + // based bone inverses. Always provide a bind matrix to + // prevent bind() from calling calculateInverses() which + // would overwrite the bone inverses computed above. + model.updateMatrixWorld( true ); + + model.bind( new Skeleton( skeleton.bones, boneInverses ), model.matrixWorld ); } @@ -1495,6 +1576,7 @@ class FBXTreeParser { } + // Parse BindPose nodes and return a map of node ID to bind matrix. parsePoseNodes() { const bindMatrices = {}; diff --git a/examples/models/fbx/archer/ArcherRi01.FBX b/examples/models/fbx/archer/ArcherRi01.FBX new file mode 100644 index 00000000000000..eea0764387aeed Binary files /dev/null and b/examples/models/fbx/archer/ArcherRi01.FBX differ diff --git a/examples/models/fbx/archer/ArcherRi01.png b/examples/models/fbx/archer/ArcherRi01.png new file mode 100644 index 00000000000000..7db076164925d5 Binary files /dev/null and b/examples/models/fbx/archer/ArcherRi01.png differ diff --git a/examples/models/fbx/archer/ArcherRi01_W.PNG b/examples/models/fbx/archer/ArcherRi01_W.PNG new file mode 100644 index 00000000000000..425fd9ed348fbe Binary files /dev/null and b/examples/models/fbx/archer/ArcherRi01_W.PNG differ diff --git a/examples/models/fbx/warrior/100820_kl_npc_d_512.png b/examples/models/fbx/warrior/100820_kl_npc_d_512.png new file mode 100644 index 00000000000000..f00f67084113f8 Binary files /dev/null and b/examples/models/fbx/warrior/100820_kl_npc_d_512.png differ diff --git a/examples/models/fbx/warrior/Warrior.fbx b/examples/models/fbx/warrior/Warrior.fbx new file mode 100644 index 00000000000000..4319d23a0f94f9 Binary files /dev/null and b/examples/models/fbx/warrior/Warrior.fbx differ diff --git a/examples/webgl_loader_fbx.html b/examples/webgl_loader_fbx.html index 21926e497a388c..4d4a3b18e77cfa 100644 --- a/examples/webgl_loader_fbx.html +++ b/examples/webgl_loader_fbx.html @@ -50,8 +50,16 @@ 'monkey', 'monkey_embedded_texture', 'vCube', + 'archer/ArcherRi01', + 'warrior/Warrior', + 'stanford-bunny', + 'mixamo', ]; + const scales = new Map(); + scales.set( 'warrior/Warrior', 100 ); + scales.set( 'archer/ArcherRi01', 100 ); + scales.set( 'stanford-bunny', 0.001 ); init(); @@ -162,6 +170,9 @@ object = group; + const scale = scales.get( asset ); + object.scale.setScalar( scale || 1 ); + if ( object.animations && object.animations.length ) { mixer = new THREE.AnimationMixer( object );