You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 13:05:56