如何用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
相关产品推荐
相关产品推荐

