机械臂之贝塞尔曲线的应用

前言

这篇是自制机械臂项目的运行姿态优化。想尝试做一个机械臂的可以参考这篇文章: 《如何做个机械臂》

尝试使用MarsCode来辅助编程,挺好用的,能提升效率。

缘由

目前机械臂只会固定某个速度从开始位置旋转到目标位置,看起来多少有点僵硬。故想着如何让机械臂运行起来更加自然,举个例子,可以想象一下扇扇子的动作,慢 -> 快 -> 慢。那如何解决这个问题,我第一时间想到的是贝塞尔曲线。就像css的css的缓动函数

目标

机械臂能变速旋转。

先看结果

解决过程

由于小米电机的几种控制模式都不支持变速运动。只能从网页端(上位机)层面或单片机层面来做,将原来的匀速距离旋转切分成多段的匀速运动,有点微积分的味道。写c语言有点太累,所以就从网页端来做,理论上蓝牙的传输速率比can快,所以在网页端来做完全没问题。但是,多了一步操作,系统可靠性会降低些。

尝试让MarsCode生成想要的三阶贝塞尔曲线函数:

很完美,写个渲染贝塞尔曲线图的函数,将同等间隔的一百个点渲染到canvas上:

js 复制代码
export function drawBezierCurve(p1, p2) {
  // cubic-bezier(.17,.67,.83,.67)
  p1 = [.17,.67];
  p2 = [.83,.67];

  // 创建canvas元素
  var canvas = document.createElement("canvas");
  canvas.id = "myCanvas";
  canvas.width = 500;
  canvas.height = 500;
  canvas.style.backgroundColor = "black";
  canvas.style.border = "1px solid #FFF";
  canvas.style.position = "absolute";
  canvas.style.left = "0";
  canvas.style.top = "0";

  let body = document.querySelector("body");
  body.appendChild(canvas);


  var ctx = canvas.getContext("2d");

  for(let i = 0; i <= 100; i++ ){
    let coordinate = bezierCurve(p1, p2, i / 100);
    // console.log('coordinate: ', coordinate);
    drawPoint(coordinate[0] * 500, (1 - coordinate[1]) * 500 , ctx);
  }

  function drawPoint(x, y, ctx) {
    //  绘制背景为白色的圆
    ctx.beginPath();
    ctx.arc(x, y, 2, 0, 2 * Math.PI);
    ctx.fillStyle = "white";
    ctx.fill();
    ctx.closePath();
  }
}

canvas渲染 如下图:

到此我有个疑问,为什么按时间等分计算,但渲染到图里的点却并不是等分。查询了些文档,比如这篇如何理解并应用贝塞尔曲线 (上图ai中也给了解答:在时间t时贝塞尔曲线上的点),在此我的理解是:这个函数只是计算出贝塞尔曲线的离散点,这个点代表了具体某个时间(x轴),应当要运动到路程的百分之几(y轴)。

实际验证:

为了简化问题,下面渲染个只有10个点的的贝塞尔曲线图(cubic-bezier(.17,.67,.83,.67)):

x轴是时间,y轴代表进度。那么可以明确以下观点:

① 不管前面如何运动,只要运行到最后一个点,那么就是运动到终点,即目标位置。

② 区间运动的速度等于 区间的运动距离 / 区间的运动时间,即 v = (y1 - y0) / (x1 - x0) = Δy / Δx

在此还需考虑一些实际问题,比如我设置的蓝牙传输间隔时间为15ms,can消息发送频率等。因为过快的发送蓝牙消息可能会导致消息阻塞,从而影响电机旋转的准确性。

粗略计算一下电机每秒的速度可变化的次数: 三个电机同时运行,电机每次的位置模式需要发四条指令。每秒可以发送蓝牙消息数量: 1000ms / 15ms(蓝牙间隔) = 66条指令。每秒速度的可变次数: 66 / (3 * 4) ≈ 5次。如果之后还要加电机,速度的每秒可变次数会更少。当然可以优化代码,比如将四条指令塞到一条蓝牙消息中(这个等之后做关键帧动画时再优化)。

造轮子时刻:

js 复制代码
/**
 * @description: 获取贝塞尔曲线的状态数据列表
 * @param { Array<number> } p1 e.g: [0, 0]
 * @param { Array<number> } p2 e.g: [1, 1]
 * @param { number } count 坐标点的个数
 * @returns { Array<object> } e.g: [{speed: 0.5, rotate: 0.5, duration: 0.5}]
 */
function getCubicCoords(p1, p2, count) {
  let preCoord = [0, 0];
  let cubicCoordData = [];

  for (let i = 1; i <= count; i++) {
    let t = i / count;
    let coordinate = bezierCurve(p1, p2, t);

    // drawPoint(coordinate[0] * 500, (1 - coordinate[1]) * 500, ctx);

    cubicCoordData.push({
      speed: (coordinate[1] - preCoord[1]) / (coordinate[0] - preCoord[0]),
      rotate: coordinate[1],
      duration: preCoord[0],
    });

    preCoord = coordinate;
  }

  return cubicCoordData;
}

getCubicCoords函数得到的是所有离散点所处位置时的速度,旋转角度和经历时间,但这只是百分比,具体还要乘以具体的角度和时间,并生成指令通过蓝牙发送出去:

js 复制代码
/**
 * @description 获取一个贝塞尔曲线区间的指令列表,包含 旋转角度,延迟时间,速度
 * @param { object {p1: Array<number>, p2: Array<number>} } cubicBezier
 * @param { number } rotateDeg
 * @param { number } deration
 * @param { motorId } debugger
 * @param { function } sendBleMsg
 */
// export function getCmdSeries(cubicBezier = {p1: [.9,.13], p2: [.88,.28]}, rotateDeg, duration, motorId = 21, sendBleMsg) {
export function getCmdSeries({
    cubicBezier= { p1: [0.9, 0.13], p2: [0.88, 0.28]},
    rotateDeg,
    duration,
    motorId,
    sendBleMsg,
    baseRotate = 0, // 从某个角度开始
  }) {

  let cubicCoordData = getCubicCoordsState(cubicBezier.p1, cubicBezier.p2, 3);
  let avgSpeed = rotateDeg / duration;

  cubicCoordData.forEach(item => {
    let speed = item.speed * avgSpeed;
    let time = item.duration * duration;
    let rotate = item.rotate * rotateDeg;

    setTimeout(() => {
      console.warn('---speed: ', speed, '---duration: ', time, '---rotate: ', rotate);
      sendBleMsg({ motorId: motorId, limit_spd: speed, loc_ref: baseRotate + rotate });
    }, time * 1000);

  });

}

调用下函数,测试两秒钟机械臂旋转一周的表现:

js 复制代码
  getCmdSeries({
    cubicBezier: {p1: [.17,.67], p2: [.83,.67]},
    rotateDeg: 360,
    duration: 3,
    motorId: 23,
    sendBleMsg: rotateMotor,
    baseRotate: 0, // 从某个角度开始
  });

结果如下:

待优化问题

速度切换时会有卡顿的感觉,就是一个区间的速度降到零,然后再提升到下一个区间的速度。理想的速度切换应该是平滑的。

参考文章

css缓动函数
如何理解并应用贝塞尔曲线

相关推荐
Json____7 分钟前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
迂 幵15 分钟前
vue el-table 超出隐藏移入弹窗显示
javascript·vue.js·elementui
上趣工作室20 分钟前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫20 分钟前
el-tree 父节点隐藏
前端·javascript·vue.js
fkalis21 分钟前
【海外SRC漏洞挖掘】谷歌语法发现XSS+Waf Bypass
前端·xss
zxg_神说要有光1 小时前
自由职业第二年,我忘记了为什么出发
前端·javascript·程序员
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月1 小时前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天1 小时前
【Node.js]
前端·node.js
2401_857610032 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js