RecursiveFrog 发表于 2022-1-16 07:09

怎样用unity3d实现机械臂建模来抓取物体和放下?

谢邀
人在工地,刚下板筋网
这确实是一个很好的问题,我这边大概花了2h,用3dsMax撸了个手子的模型,然后去Unity里做了下核心功能,效果如下


https://www.zhihu.com/video/1464725159672623104
<hr/>抓取放下物体不过是一个动添更新父子树+Collider碰撞检测的简单问题,没什么技术含量的

这个问题最核心的地方,是如何根据末端机械手的目标位置(要抓取的目标物体),反向求解出机械臂父链节点的姿态,并实现机械臂的动画效果
在动画模式上区别于传统的角色关键帧动画(Key-Frame Animation),这里就需要我们使用计算动画(Caculate Animation)的模式
因为机械臂要抓取的目标点可能是空间内的任意位置,无法通过关键帧动画进行预制作+实时采样输出,而是需要我们实时的根据所需的目标点来生成机械臂的动画
在运动模式上,机械臂的各个节点呈父子关系嵌套,会形成FK(Front Kinematics)正向运动效应,父节点的运动会影响子节点相对世界的运动(父--->子),但在动画解算中,我们却是要根据末端子节点的位置,来解算父链节点的姿态(子--->父),也就是所谓的IK(Inverse Kinematics)逆向运动学

求解链状体的逆向运动学是一个很复杂的问题,因为往往是有无穷多解的,很难给出一个线性的解析式
(请想象一下你拿着一条锁链的两端,锁链并没有因为你拿着两端就被完全固定,它中间的链节仍可以随意振动)
这里用到了一种比较主流的,基于迭代的求解方法,被称作CCD(Cyclic Coordinate Descent)循环坐标下降法
它的思路如下:

如果最终我们找到了某一个符合要求的姿态,那么一定满足,所有父链节点与末端节点的连线均指向目标点


每一次迭代,我们从父节点到子节点进行一次遍历,每个节点要旋转的角度,等于该节点指向末端的向量,与该节点指向目标点的夹角


每个节点旋转完成后,都保证它与末端节点的连线能够指向目标点
但每个节点旋转过后,会破坏其它节点的指向性
不过在多次迭代之后,依托于初始状态的限制,以及我们遍历节点的顺序的限制,整个链状体的姿态会逐渐收敛向一个唯一的最终结果上
<hr/>ok核心功能的实现思路大致如上所述
当然具体细节方面的话,我们得创造一套bones骨骼节点来使用CCD迭代计算结果,完成后再将arms机械臂节点通过Update插值匹配到结果上从而形成动画
bones骨骼节点是要比arms机械臂节点多一个小末端节点的,这个节点用于指示和目标点的匹配,并和其它父节计算指向向量
以及要特别注意约束问题,我们这里只是做了一个简单的平面内的CCD计算,建模时要注意把机械臂建在一个平面内,机械臂的节点轴心要调整对位,在计算旋转时,也要注意将向量/位置投影到本地的坐标平面内部进行计算
诸如此类的细节问题就有点太多了...不便展开了,这篇点赞能上去再更吧,说不定都没人看呢,明天还要上工地实习呢早点睡了(咕咕咕)

贴一下控制机械臂的C#脚本代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCDCon : MonoBehaviour//机械臂控制脚本
{


    //CCD控制

   
    Transform pbone;//骨骼承台节点

   
    int depth = 0;//节点深度- 脚本所在的物体是承台,往下depth数量的节点为机械臂

   
    int ccdDepth = 5;//CCD迭代次数

   
    float rotateTime = 3.0f;//旋转匹配时间

    float nowTime = 0;//当前时间

    List<Transform> bones;//骨骼节点

    List<Transform> arms;//臂节点

    Transform bonePoint;//骨骼末端点

    List<Quaternion> baseRotation;//初始旋量

    List<Quaternion> armRotation;//机械臂节点的初始旋量

    //状态相关

    public enum ArmState
    {
      rotate, //正在旋转
      stati //已静止
    }

    ArmState armState = ArmState.stati;

    public ArmState AState {
      get {
            return armState;
      }
    }

    private void Awake()
    {
      arms = new List<Transform>();
      bones = new List<Transform>();
      baseRotation = new List<Quaternion>();
      armRotation = new List<Quaternion>();

      Transform pre = gameObject.transform;
      Transform preb = pbone.transform;

      armRotation.Add(gameObject.transform.rotation);

      for (int i = 0; i < depth; ++i) {
            pre = pre.GetChild(0);
            preb = preb.GetChild(0);

            arms.Add(pre);
            bones.Add(preb);
            baseRotation.Add(preb.transform.localRotation);
            armRotation.Add(pre.transform.localRotation);
      }

      bonePoint = preb.GetChild(0);//骨骼末端点

    }


    Vector3 bonetopoint;//骨骼指向末端点向量
    Vector3 bonetotar;//骨骼指向目标点向量
    Vector3 vec;
    float angle;//旋转角度
    Vector3 axis;//转轴

    public void ccdgo(Vector3 p)//解算CCD尝试够到世界坐标p位置
    {

      if (armState == ArmState.rotate)
            return; //如果正在旋转应忽略

      //骨骼承台对准
      pbone.LookAt(new Vector3(p.x, pbone.transform.position.y, p.z), Vector3.up);

      //骨骼节点初始化旋转
      for (int i = 0; i < depth; ++i)
            bones.transform.localRotation = baseRotation;

      //

      //CCD解算骨骼节点
      for (int i = 0; i < ccdDepth; ++i) { //迭代次数

            for (int j = 0; j < depth; ++j) { //依次计算每根骨骼的旋量

                vec = bones.InverseTransformPoint(bonePoint.position);//转入本地坐标
                bonetopoint = new Vector3(vec.x, 0, vec.z).normalized;//Y轴强制归0,化为平面内旋转

                vec = bones.InverseTransformPoint(p);//转入本地坐标
                bonetotar = new Vector3(vec.x, 0, vec.z).normalized;//Y轴强制归0,化为平面内旋转

                angle = Vector3.Angle(bonetopoint, bonetotar);

                axis = Vector3.Cross(bonetopoint, bonetotar).normalized;//通过叉积计算转轴,注意预先确认向量顺序和正旋顺序(Unity左手系要用左手螺旋)

                bones.Rotate(axis, angle);

            }

      }

      //操作机械臂节点匹配

      //机械臂直接匹配
      //gameObject.transform.rotation = pbone.rotation;

      //for (int i = 0; i < depth; ++i)
      //    arms.rotation = bones.rotation;


      //设置状态 Update 插值匹配

      armState = ArmState.rotate;
      nowTime = 0;

      armRotation = gameObject.transform.rotation;//记录承台旋量

      for (int i = 1; i <= depth; ++i)
            armRotation = arms.localRotation;


    }


    public void ccdReset() //复位
    {
      if (armState == ArmState.rotate) return;//如果正在旋转应忽略

      //对准承台
      pbone.rotation = gameObject.transform.rotation;

      //骨骼复位
      for (int i = 0; i < depth; ++i)
            bones.localRotation = baseRotation;

      //设置状态 Update 插值匹配

      armState = ArmState.rotate;
      nowTime = 0;

      armRotation = gameObject.transform.rotation;//记录承台旋量

      for (int i = 1; i <= depth; ++i)
            armRotation = arms.localRotation;
    }

    float lerp;
    private void Update()
    {
      if (armState == ArmState.rotate) {//正在旋转匹配的
            nowTime += Time.deltaTime;
            if (nowTime >= rotateTime) { //匹配已经完成
                armState = ArmState.stati;

                gameObject.transform.rotation = pbone.rotation;

                for (int i = 0; i < depth; ++i)
                  arms.localRotation = bones.localRotation;
            }
            else {
                lerp = nowTime / rotateTime;

                gameObject.transform.rotation = Quaternion.Slerp(armRotation, pbone.rotation, lerp);

                for (int i = 1; i <= depth; ++i)
                  arms.localRotation = Quaternion.Slerp(armRotation, bones.localRotation, lerp);
            }
      }
    }



}

这里是Randy
大四即将毕业的土木狗,希望能成为优秀的傀儡师(动作TA)
2022/01/12

TheLudGamer 发表于 2022-1-16 07:12

谢邀
人在工地,刚下板筋网
这确实是一个很好的问题,我这边大概花了2h,用3dsMax撸了个手子的模型,然后去Unity里做了下核心功能,效果如下


https://www.zhihu.com/video/1464725159672623104
<hr/>抓取放下物体不过是一个动添更新父子树+Collider碰撞检测的简单问题,没什么技术含量的

这个问题最核心的地方,是如何根据末端机械手的目标位置(要抓取的目标物体),反向求解出机械臂父链节点的姿态,并实现机械臂的动画效果
在动画模式上区别于传统的角色关键帧动画(Key-Frame Animation),这里就需要我们使用计算动画(Caculate Animation)的模式
因为机械臂要抓取的目标点可能是空间内的任意位置,无法通过关键帧动画进行预制作+实时采样输出,而是需要我们实时的根据所需的目标点来生成机械臂的动画
在运动模式上,机械臂的各个节点呈父子关系嵌套,会形成FK(Front Kinematics)正向运动效应,父节点的运动会影响子节点相对世界的运动(父--->子),但在动画解算中,我们却是要根据末端子节点的位置,来解算父链节点的姿态(子--->父),也就是所谓的IK(Inverse Kinematics)逆向运动学

求解链状体的逆向运动学是一个很复杂的问题,因为往往是有无穷多解的,很难给出一个线性的解析式
(请想象一下你拿着一条锁链的两端,锁链并没有因为你拿着两端就被完全固定,它中间的链节仍可以随意振动)
这里用到了一种比较主流的,基于迭代的求解方法,被称作CCD(Cyclic Coordinate Descent)循环坐标下降法
它的思路如下:

如果最终我们找到了某一个符合要求的姿态,那么一定满足,所有父链节点与末端节点的连线均指向目标点


每一次迭代,我们从父节点到子节点进行一次遍历,每个节点要旋转的角度,等于该节点指向末端的向量,与该节点指向目标点的夹角


每个节点旋转完成后,都保证它与末端节点的连线能够指向目标点
但每个节点旋转过后,会破坏其它节点的指向性
不过在多次迭代之后,依托于初始状态的限制,以及我们遍历节点的顺序的限制,整个链状体的姿态会逐渐收敛向一个唯一的最终结果上
<hr/>ok核心功能的实现思路大致如上所述
当然具体细节方面的话,我们得创造一套bones骨骼节点来使用CCD迭代计算结果,完成后再将arms机械臂节点通过Update插值匹配到结果上从而形成动画
bones骨骼节点是要比arms机械臂节点多一个小末端节点的,这个节点用于指示和目标点的匹配,并和其它父节计算指向向量
以及要特别注意约束问题,我们这里只是做了一个简单的平面内的CCD计算,建模时要注意把机械臂建在一个平面内,机械臂的节点轴心要调整对位,在计算旋转时,也要注意将向量/位置投影到本地的坐标平面内部进行计算
诸如此类的细节问题就有点太多了...不便展开了,这篇点赞能上去再更吧,说不定都没人看呢,明天还要上工地实习呢早点睡了(咕咕咕)

贴一下控制机械臂的C#脚本代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCDCon : MonoBehaviour//机械臂控制脚本
{


    //CCD控制

   
    Transform pbone;//骨骼承台节点

   
    int depth = 0;//节点深度- 脚本所在的物体是承台,往下depth数量的节点为机械臂

   
    int ccdDepth = 5;//CCD迭代次数

   
    float rotateTime = 3.0f;//旋转匹配时间

    float nowTime = 0;//当前时间

    List<Transform> bones;//骨骼节点

    List<Transform> arms;//臂节点

    Transform bonePoint;//骨骼末端点

    List<Quaternion> baseRotation;//初始旋量

    List<Quaternion> armRotation;//机械臂节点的初始旋量

    //状态相关

    public enum ArmState
    {
      rotate, //正在旋转
      stati //已静止
    }

    ArmState armState = ArmState.stati;

    public ArmState AState {
      get {
            return armState;
      }
    }

    private void Awake()
    {
      arms = new List<Transform>();
      bones = new List<Transform>();
      baseRotation = new List<Quaternion>();
      armRotation = new List<Quaternion>();

      Transform pre = gameObject.transform;
      Transform preb = pbone.transform;

      armRotation.Add(gameObject.transform.rotation);

      for (int i = 0; i < depth; ++i) {
            pre = pre.GetChild(0);
            preb = preb.GetChild(0);

            arms.Add(pre);
            bones.Add(preb);
            baseRotation.Add(preb.transform.localRotation);
            armRotation.Add(pre.transform.localRotation);
      }

      bonePoint = preb.GetChild(0);//骨骼末端点

    }


    Vector3 bonetopoint;//骨骼指向末端点向量
    Vector3 bonetotar;//骨骼指向目标点向量
    Vector3 vec;
    float angle;//旋转角度
    Vector3 axis;//转轴

    public void ccdgo(Vector3 p)//解算CCD尝试够到世界坐标p位置
    {

      if (armState == ArmState.rotate)
            return; //如果正在旋转应忽略

      //骨骼承台对准
      pbone.LookAt(new Vector3(p.x, pbone.transform.position.y, p.z), Vector3.up);

      //骨骼节点初始化旋转
      for (int i = 0; i < depth; ++i)
            bones.transform.localRotation = baseRotation;

      //

      //CCD解算骨骼节点
      for (int i = 0; i < ccdDepth; ++i) { //迭代次数

            for (int j = 0; j < depth; ++j) { //依次计算每根骨骼的旋量

                vec = bones.InverseTransformPoint(bonePoint.position);//转入本地坐标
                bonetopoint = new Vector3(vec.x, 0, vec.z).normalized;//Y轴强制归0,化为平面内旋转

                vec = bones.InverseTransformPoint(p);//转入本地坐标
                bonetotar = new Vector3(vec.x, 0, vec.z).normalized;//Y轴强制归0,化为平面内旋转

                angle = Vector3.Angle(bonetopoint, bonetotar);

                axis = Vector3.Cross(bonetopoint, bonetotar).normalized;//通过叉积计算转轴,注意预先确认向量顺序和正旋顺序(Unity左手系要用左手螺旋)

                bones.Rotate(axis, angle);

            }

      }

      //操作机械臂节点匹配

      //机械臂直接匹配
      //gameObject.transform.rotation = pbone.rotation;

      //for (int i = 0; i < depth; ++i)
      //    arms.rotation = bones.rotation;


      //设置状态 Update 插值匹配

      armState = ArmState.rotate;
      nowTime = 0;

      armRotation = gameObject.transform.rotation;//记录承台旋量

      for (int i = 1; i <= depth; ++i)
            armRotation = arms.localRotation;


    }


    public void ccdReset() //复位
    {
      if (armState == ArmState.rotate) return;//如果正在旋转应忽略

      //对准承台
      pbone.rotation = gameObject.transform.rotation;

      //骨骼复位
      for (int i = 0; i < depth; ++i)
            bones.localRotation = baseRotation;

      //设置状态 Update 插值匹配

      armState = ArmState.rotate;
      nowTime = 0;

      armRotation = gameObject.transform.rotation;//记录承台旋量

      for (int i = 1; i <= depth; ++i)
            armRotation = arms.localRotation;
    }

    float lerp;
    private void Update()
    {
      if (armState == ArmState.rotate) {//正在旋转匹配的
            nowTime += Time.deltaTime;
            if (nowTime >= rotateTime) { //匹配已经完成
                armState = ArmState.stati;

                gameObject.transform.rotation = pbone.rotation;

                for (int i = 0; i < depth; ++i)
                  arms.localRotation = bones.localRotation;
            }
            else {
                lerp = nowTime / rotateTime;

                gameObject.transform.rotation = Quaternion.Slerp(armRotation, pbone.rotation, lerp);

                for (int i = 1; i <= depth; ++i)
                  arms.localRotation = Quaternion.Slerp(armRotation, bones.localRotation, lerp);
            }
      }
    }



}

这里是Randy
大四即将毕业的土木狗,希望能成为优秀的傀儡师(动作TA)
2022/01/12

BlaXuan 发表于 2022-1-16 07:16

为实现机械臂的末端随之前的关节转动而转动的效果,可以采用父子关系实现这种父臂带动子臂动的效果。所有物体依次成父子关系排列。在hierarchy栏里排成一大长串。也可以直接在hierarchy栏中拖动,也可以在脚本中实现父子关系。
页: [1]
查看完整版本: 怎样用unity3d实现机械臂建模来抓取物体和放下?