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

React+Three.js动画切换卡顿及THREE.PropertyBinding警告求助

React + Three.js 3D动画切换卡顿与警告问题解决

问题描述

使用React和Three.js开发3D网站时,切换动画会卡顿数秒,同时出现警告:THREE.PropertyBinding: No target node found for track: Armature.quaternion。尝试用fadeIn/fadeOut实现平滑过渡未达预期效果。

解决方案

1. 修复Armature.quaternion警告

原因:FBX动画文件中的骨骼根节点名称为Armature,但GLTF模型的根骨骼节点是Hips,导致动画轨道找不到目标节点。
解决方法:重映射动画轨道的节点路径,将Armature替换为模型中的根节点Hips:

// 封装重映射函数
function remapAnimationTracks(animation, oldRootPath, newRootPath) {
  animation.tracks.forEach(track => {
    track.name = track.name.replace(oldRootPath, newRootPath);
  });
}

// 在加载FBX动画后调用
const { animations: typingAnimation } = useFBX(typing);
remapAnimationTracks(typingAnimation[0], "Armature", "Hips");

const { animations: standingAnimation } = useFBX(standing);
remapAnimationTracks(standingAnimation[0], "Armature", "Hips");

const { animations: fallingAnimation } = useFBX(falling);
remapAnimationTracks(fallingAnimation[0], "Armature", "Hips");

2. 实现平滑动画过渡并解决卡顿

原因:原代码中直接reset动画并在清理函数中立即停止,导致过渡中断,引发卡顿。
解决方法:保存当前激活的动画引用,实现新旧动画的交叉淡入淡出:

// 添加ref保存当前激活的动画
const currentActionRef = useRef(null);

useEffect(() => {
  const newAction = actions[animation];
  if (!newAction) return;

  // 旧动画淡出,新动画淡入
  if (currentActionRef.current) {
    currentActionRef.current.fadeOut(0.5);
  }
  newAction.reset().fadeIn(0.5).play();
  currentActionRef.current = newAction;

  return () => {
    newAction.fadeOut(0.5);
    // 延迟停止动画,确保过渡完成
    setTimeout(() => newAction.stop(), 500);
  };
}, [animation, actions]);

3. 性能优化减少卡顿

  • 确保动画与模型的骨骼绑定完全匹配,避免运行时额外计算
  • 使用Preload组件预加载模型和动画资源,避免切换时加载延迟
  • 关闭未使用的morphTarget(如果模型不需要),减少渲染开销

修改后的完整代码

import { useAnimations, useFBX, useGLTF, Preload } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useControls } from "leva";
import React, { useEffect, useRef } from "react";
import * as THREE from "three";
import avatar from '../assets/3D/avatar.glb'
import typing from '../animations/Typing.fbx';
import standing from '../animations/Standing.fbx';
import falling from '../animations/Falling.fbx';

// 重映射动画轨道节点路径
function remapAnimationTracks(animation, oldRootPath, newRootPath) {
  animation.tracks.forEach(track => {
    track.name = track.name.replace(oldRootPath, newRootPath);
  });
}

export function Avatar(props) {
  const { animation } = props;
  const { headFollow, cursorFollow, wireframe } = useControls({
    headFollow: false,
    cursorFollow: false,
    wireframe: false,
  });
  const avatarRef = useRef();
  const currentActionRef = useRef(null);
  const { nodes, materials } = useGLTF(avatar);

  const { animations: typingAnimation } = useFBX(typing);
  remapAnimationTracks(typingAnimation[0], "Armature", "Hips");
  typingAnimation[0].name = "Typing";

  const { animations: standingAnimation } = useFBX(standing);
  remapAnimationTracks(standingAnimation[0], "Armature", "Hips");
  standingAnimation[0].name = "Standing";

  const { animations: fallingAnimation } = useFBX(falling);
  remapAnimationTracks(fallingAnimation[0], "Armature", "Hips");
  fallingAnimation[0].name = "Falling";

  const { actions } = useAnimations(
    [typingAnimation[0], standingAnimation[0], fallingAnimation[0]],
    avatarRef
  );

  useFrame((state) => {
    if (headFollow) {
      avatarRef.current.getObjectByName("Head")?.lookAt(state.camera.position);
    }
    if (cursorFollow) {
      const target = new THREE.Vector3(state.mouse.x, state.mouse.y, 1);
      avatarRef.current.getObjectByName("Spine2")?.lookAt(target);
    }
  });

  useEffect(() => {
    const newAction = actions[animation];
    if (!newAction) return;

    if (currentActionRef.current) {
      currentActionRef.current.fadeOut(0.5);
    }
    newAction.reset().fadeIn(0.5).play();
    currentActionRef.current = newAction;

    return () => {
      newAction.fadeOut(0.5);
      setTimeout(() => newAction.stop(), 500);
    };
  }, [animation, actions]);

  useEffect(() => {
    Object.values(materials).forEach((material) => {
      material.wireframe = wireframe;
    });
  }, [wireframe]);

  return (
    <>
      <Preload />
      <group {...props} ref={avatarRef} dispose={null}>
        <group rotation-x={-Math.PI / 2}>
          <primitive object={nodes.Hips} />
          <skinnedMesh
            name="EyeLeft"
            geometry={nodes.EyeLeft.geometry}
            material={materials.Wolf3D_Eye}
            skeleton={nodes.EyeLeft.skeleton}
            morphTargetDictionary={nodes.EyeLeft.morphTargetDictionary}
            morphTargetInfluences={nodes.EyeLeft.morphTargetInfluences}
          />
          <skinnedMesh
            name="EyeRight"
            geometry={nodes.EyeRight.geometry}
            material={materials.Wolf3D_Eye}
            skeleton={nodes.EyeRight.skeleton}
            morphTargetDictionary={nodes.EyeRight.morphTargetDictionary}
            morphTargetInfluences={nodes.EyeRight.morphTargetInfluences}
          />
          <skinnedMesh
            name="Wolf3D_Head"
            geometry={nodes.Wolf3D_Head.geometry}
            material={materials.Wolf3D_Skin}
            skeleton={nodes.Wolf3D_Head.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Head.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Head.morphTargetInfluences}
          />
          <skinnedMesh
            name="Wolf3D_Teeth"
            geometry={nodes.Wolf3D_Teeth.geometry}
            material={materials.Wolf3D_Teeth}
            skeleton={nodes.Wolf3D_Teeth.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Teeth.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Teeth.morphTargetInfluences}
          />
          <skinnedMesh
            geometry={nodes.Wolf3D_Hair.geometry}
            material={materials.Wolf3D_Hair}
            skeleton={nodes.Wolf3D_Hair.skeleton}
          />
          <skinnedMesh
            geometry={nodes.Wolf3D_Glasses.geometry}
            material={materials.Wolf3D_Glasses}
            skeleton={nodes.Wolf3D_Glasses.skeleton}
          />
          <skinnedMesh
            name="Wolf3D_Outfit_Top"
            geometry={nodes.Wolf3D_Outfit_Top.geometry}
            material={materials.Wolf3D_Outfit_Top}
            skeleton={nodes.Wolf3D_Outfit_Top.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Outfit_Top.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Outfit_Top.morphTargetInfluences}
          />
          <skinnedMesh
            name="Wolf3D_Outfit_Bottom"
            geometry={nodes.Wolf3D_Outfit_Bottom.geometry}
            material={materials.Wolf3D_Outfit_Bottom}
            skeleton={nodes.Wolf3D_Outfit_Bottom.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Outfit_Bottom.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Outfit_Bottom.morphTargetInfluences}
          />
          <skinnedMesh
            name="Wolf3D_Outfit_Footwear"
            geometry={nodes.Wolf3D_Outfit_Footwear.geometry}
            material={materials.Wolf3D_Outfit_Footwear}
            skeleton={nodes.Wolf3D_Outfit_Footwear.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Outfit_Footwear.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Outfit_Footwear.morphTargetInfluences}
          />
          <skinnedMesh
            name="Wolf3D_Body"
            geometry={nodes.Wolf3D_Body.geometry}
            material={materials.Wolf3D_Body}
            skeleton={nodes.Wolf3D_Body.skeleton}
            morphTargetDictionary={nodes.Wolf3D_Body.morphTargetDictionary}
            morphTargetInfluences={nodes.Wolf3D_Body.morphTargetInfluences}
          />
        </group>
      </group>
    </>
  );
}

内容的提问来源于stack exchange,提问作者Anubhav Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 10:15:01