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

如何用Three.js与Tween.js实现Collada模型目标关节位姿补间

问题:Three.js + Tween.js 驱动YuMi机器人模型无法到达目标关节位置

我尝试复刻Three.js官方的「加载Collada运动学并使用」示例,将其应用到自己的YuMi机器人模型上。编写了YuMiMotion类并传入目标关节位姿调用,但模型无法正确移动到目标位置。以下是我的代码实现:

核心类代码

import { Object3D, MathUtils } from "three";
import { ColladaLoader } from "three/addons/loaders/ColladaLoader.js";
import yumi_path from "../assets/dae/yumi.dae";
import { Tween, Easing, update } from "@tweenjs/tween.js";

export class YuMiMotion {
  constructor(scene, joint_vlaues) {
    this.scene = scene;
    this.tgt_jnt_vals = joint_vlaues;

    this.loader = new ColladaLoader();
    this.yumi_model = new Object3D();
    this.kinematics;
    this.kinematicsTween;
    this.tweenParameters = {};
    this.loadYuMi();
    this.startMotion();
  }

  startMotion = () => {
    if (this.kinematics == undefined) {
      console.log("Kinematics Still not loaded!");
      this.anim_frame_id1 = requestAnimationFrame(this.startMotion);
    }
    else {
      console.log(this.kinematics);
      this.setupTween();
      cancelAnimationFrame(this.anim_frame_id1);
      this.animate();
     }    
  }

  animate = () => { 
    update();
    this.anim_frame_id2 = requestAnimationFrame(this.animate);
  }
  
  loadYuMi = async() => {
    const onLoad = (result, yumi) => {
      const model = result.scene.children[0];
      yumi.add(model.clone(true));
      yumi.traverse(function (child) {
        if (child.isMesh) {
          child.material.flatShading = true;
        }
      });
      this.kinematics = result.kinematics;
    };

    await this.loader.load(yumi_path, (collada) =>
      onLoad(collada, this.yumi_model, this.kinematics)
    ),
      undefined,
      function (error) {
        console.log("An error happened", error);
      };

    this.yumi_model.position.set(0.0, 0.0, -6.0);
    this.yumi_model.rotation.set(-Math.PI / 2, 0.0, -Math.PI / 2);
    this.yumi_model.scale.set(20, 20, 20);

    this.scene.add(this.yumi_model);
  }

  setupTween =() =>{

    const duration = MathUtils.randInt( 1000, 5000 );
    const target = {};

    for (const prop in this.tgt_jnt_vals) {
          const joint = this.kinematics.joints[ prop ];
          const old = this.tweenParameters[ prop ];
          const position = old ? old : joint.zeroPosition;
          this.tweenParameters[ prop ] = position;
      target[prop] = this.tgt_jnt_vals[prop];
    }

    this.kinematicsTween = new Tween( this.tweenParameters ).to( target, duration ).easing( Easing.Quadratic.Out );
    this.kinematicsTween.onUpdate( ( object )=> {
      for ( const prop in this.kinematics.joints ) {
        if ( prop in this.kinematics.joints) {
          if ( ! this.kinematics.joints[ prop ].static ) {
            this.kinematics.setJointValue( prop, object[ prop ] );
          }
        }
      }
    });
    this.kinematicsTween.start();
    setTimeout( this.setupTween, duration );
  }
}

调用代码

let target_position = {
  gripper_l_joint: 0.01,
  gripper_l_joint_m: 0.01,
  gripper_r_joint: 0.01,
  gripper_r_joint_m: 0.01,
  yumi_joint_1_l: 10.0,
  yumi_joint_1_r:  10.0,
  yumi_joint_2_l:  20.0,
  yumi_joint_2_r:  20.0,
  yumi_joint_3_l:  30.0,
  yumi_joint_3_r:  30.0,
  yumi_joint_4_l:  40.0,
  yumi_joint_4_r:  40.0,
  yumi_joint_5_l:  50.0,
  yumi_joint_5_r:  50.0,
  yumi_joint_6_l:  60.0,
  yumi_joint_6_r:  60.0,
  yumi_joint_7_l:  70.0,
  yumi_joint_7_r:  70.0
};
new YuMiMotion(this.scene, target_position);

解决方案

针对代码中的问题,主要有以下几个修复点:

1. 角度单位不匹配:度数转弧度

Three.js关节运动使用弧度作为单位,但你传入的目标值是度数(如10.0、20.0),这会导致关节移动幅度远小于预期。修复方式:
在setupTween的循环中自动转换单位:

// 替换setupTween中的目标值赋值代码
target[prop] = MathUtils.degToRad(this.tgt_jnt_vals[prop]);

2. 异步加载逻辑错误

ColladaLoader.load是异步方法,但你用await的方式不正确——load不返回Promise,await无法等待加载完成。应该在onLoad回调中触发运动启动:

// 修改loadYuMi方法
loadYuMi = () => {
  const onLoad = (result) => {
    const model = result.scene.children[0];
    this.yumi_model.add(model.clone(true));
    this.yumi_model.traverse(child => {
      if (child.isMesh) child.material.flatShading = true;
    });
    this.kinematics = result.kinematics;
    
    // 加载完成后启动运动
    this.startMotion();
  };

  this.loader.load(
    yumi_path,
    onLoad,
    undefined,
    error => console.log("加载错误:", error)
  );

  this.yumi_model.position.set(0.0, 0.0, -6.0);
  this.yumi_model.rotation.set(-Math.PI / 2, 0.0, -Math.PI / 2);
  this.yumi_model.scale.set(20, 20, 20);
  this.scene.add(this.yumi_model);
}

// 构造函数中移除startMotion调用
constructor(scene, joint_vlaues) {
  this.scene = scene;
  this.tgt_jnt_vals = joint_vlaues;
  this.loader = new ColladaLoader();
  this.yumi_model = new Object3D();
  this.kinematics;
  this.kinematicsTween;
  this.tweenParameters = {};
  this.loadYuMi();
}

3. Tween循环逻辑修复

当前setTimeout会在每次tween结束后重置起始值为joint.zeroPosition,导致关节反复跳回初始位置。应保存当前关节值作为下一次tween的起始:

setupTween = () => {
  const duration = MathUtils.randInt(1000, 5000);
  const target = {};

  for (const prop in this.tgt_jnt_vals) {
    const joint = this.kinematics.joints[prop];
    // 用当前tween参数值作为起始,无值则用初始位姿
    const currentValue = this.tweenParameters[prop] ?? joint.zeroPosition;
    this.tweenParameters[prop] = currentValue;
    // 目标值转弧度
    target[prop] = MathUtils.degToRad(this.tgt_jnt_vals[prop]);
  }

  this.kinematicsTween = new Tween(this.tweenParameters)
    .to(target, duration)
    .easing(Easing.Quadratic.Out)
    .onUpdate(object => {
      for (const prop in object) {
        if (this.kinematics.joints[prop] && !this.kinematics.joints[prop].static) {
          this.kinematics.setJointValue(prop, object[prop]);
        }
      }
    })
    // 用tween完成事件替代setTimeout,避免时间误差
    .onComplete(() => this.setupTween())
    .start();
}

4. 确保模型矩阵同步更新

在动画循环中添加矩阵更新,保证关节运动实时反映到模型上:

animate = () => {
  update();
  // 更新模型世界矩阵
  this.yumi_model.updateMatrixWorld(true);
  this.anim_frame_id2 = requestAnimationFrame(this.animate);
}

将以上修复点应用到代码中,即可让YuMi机器人正确移动到目标关节位置。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 01:01:18