如何在React-Three-Fiber中保持Three.js模型连续动画间的姿态
解决React-Three-Fiber中3D Avatar动画切换重置姿态的问题
错误原因分析
你之前写的group.current.skeleton会报错,是因为group.current是顶层Group容器,它本身没有skeleton属性——骨架(skeleton)是属于skinnedMesh的,所以找不到才抛出Cannot read properties of null (reading 'skeleton')错误。
另外,切换动画时模型回到休息态的核心原因是:你调用了currentAction.reset(),这个方法会强制把动画的姿态重置回初始状态,直接覆盖了上一段动画的结束姿态。
解决方案步骤
1. 给SkinnedMesh添加引用
首先在组件里给skinnedMesh加一个ref,方便后续获取骨架:
const skinnedMeshRef = useRef();
然后在返回的JSX里给skinnedMesh绑定这个ref:
<skinnedMesh ref={skinnedMeshRef} // 其他原有属性保持不变 />
2. 修改动画切换逻辑,去掉reset()
把动画切换的useEffect逻辑改成下面这样:
- 不再调用
reset(),避免重置姿态 - 先让上一个动作淡出,再让新动作从当前姿态淡入播放
- 确保新动作继承当前的骨骼姿态
修改后的useEffect代码:
useEffect(() => { const targetAction = actions[names[animationIndex]]; let prevAction = null; // 遍历所有动作,找到正在播放的那一个 Object.values(actions).forEach(action => { if (action.isRunning()) prevAction = action; }); if (prevAction && prevAction !== targetAction) { // 上一个动作淡出,新动作从当前姿态开始淡入 prevAction.fadeOut(0.5); targetAction.setLoop(THREE.LoopOnce, 1); targetAction.clampWhenFinished = true; targetAction.play().fadeIn(0.5); } else if (!prevAction) { // 第一次播放动画,正常启动 targetAction.setLoop(THREE.LoopOnce, 1); targetAction.clampWhenFinished = true; targetAction.play().fadeIn(0.5); } // 组件卸载时淡出当前动作 return () => { if (targetAction) targetAction.fadeOut(0.5); }; }, [animationIndex, actions, names]);
3. 正确保存结束姿态(可选)
如果需要在组件卸载时把当前姿态设为模型默认姿态,可以通过skinnedMesh的骨架来实现,修改清理函数:
return () => { if (targetAction) targetAction.fadeOut(0.5); // 等待淡出完成后,保存当前姿态为骨架默认姿态 setTimeout(() => { if (skinnedMeshRef.current) { skinnedMeshRef.current.skeleton.pose(); // 也可以手动保存每个骨骼的矩阵,下次加载时直接应用 // skinnedMeshRef.current.skeleton.bones.forEach(bone => { // bone.matrix.copy(bone.matrixWorld); // bone.matrix.decompose(bone.position, bone.quaternion, bone.scale); // }); } }, 500); // 和淡出时间保持一致 };
修改后的完整组件代码
import React, { useEffect, useRef } from "react"; import { useGLTF, useAnimations } from "@react-three/drei"; import { useCharacterAnimations } from "../contexts/CharacterAnimations"; import * as THREE from "three"; const Man = (props) => { const group = useRef(); const skinnedMeshRef = useRef(); // 新增:skinnedMesh的引用 const { nodes, materials, animations } = useGLTF("./models/man.glb"); const { setAnimations, animationIndex } = useCharacterAnimations(); const { actions, names } = useAnimations(animations, group); useEffect(() => { setAnimations(names); }, [names]); useEffect(() => { const targetAction = actions[names[animationIndex]]; let prevAction = null; Object.values(actions).forEach(action => { if (action.isRunning()) prevAction = action; }); if (prevAction && prevAction !== targetAction) { prevAction.fadeOut(0.5); targetAction.setLoop(THREE.LoopOnce, 1); targetAction.clampWhenFinished = true; targetAction.play().fadeIn(0.5); } else if (!prevAction) { targetAction.setLoop(THREE.LoopOnce, 1); targetAction.clampWhenFinished = true; targetAction.play().fadeIn(0.5); } return () => { if (targetAction) targetAction.fadeOut(0.5); // 可选:保存当前姿态为默认姿态 setTimeout(() => { if (skinnedMeshRef.current) { skinnedMeshRef.current.skeleton.pose(); } }, 500); }; }, [animationIndex, actions, names]); return ( <group ref={group} {...props} dispose={null}> <group name="Scene"> <group name="Armature001" rotation={[1.829, 0, 0]}> <primitive object={nodes.root} /> <skinnedMesh ref={skinnedMeshRef} // 绑定ref name="rp_manuel_animated_001_dancing_geo" geometry={nodes.rp_manuel_animated_001_dancing_geo.geometry} material={materials["rp_manuel_animated_001_mat.005"]} skeleton={nodes.rp_manuel_animated_001_dancing_geo.skeleton} castShadow /> </group> </group> </group> ); }; export default Man; useGLTF.preload("./models/man.glb");
内容的提问来源于stack exchange,提问作者CuriousBeaver
相关产品推荐
相关产品推荐

