逆运动学(2): 2D逆运动学实践

Number of views 31

介绍

在本系列的前一部分中,我们讨论了具有两个自由度的机械臂的逆运动学问题,就像下图中那样。

image1741941668829.png在这种情况下,机械臂的长度 c 和 a 通常是已知的。如果我们要到达的目标点是 C,那么我们就可以形成一个所有边长都已知的三角形。

image1741941812061.png然后,我们推导出了控制机械臂关节旋转的角度 A 和 B 的方程:

image1741942117468.png乍一看,这些方程可能显得相当复杂;然而,从几何角度来看,它们的含义在观察上述图示时应该是相当直观的。

创建机械臂

实现这一解决方案的第一步是创建一个机械臂。Unity本身并不直接提供“关节”的概念,但是可以利用引擎提供的父子对象系统来创建一个组件层次结构,这个结构会像一个机械臂一样工作。

思路是为每个关节使用一个GameObject,这样旋转其变换(transform)将导致连接在其上的手臂部分也一起旋转。将第二个关节作为第一个关节的子对象,会使它们像第一张图中展示的那样一起旋转。

最终形成的关系层次结构为:

  • Root
    • Joint A
      • Bone A
      • Joint B
        • Bone B
        • Hand

然后,我们可以给根对象添加一个名为 SimpleIK 的脚本,该脚本将负责旋转各个关节以到达目标位置。

using System.Collections;
using UnityEngine;

namespace IK
{
    public class SimpleIK : MonoBehaviour
    {
        [Header("Joints")]
        public Transform Joint0;
        public Transform Joint1;
        public Transform Hand;

        [Header("Target")]
        public Transform Target;

        ...
    }
}

在本教程的前一部分中推导出的方程需要知道前两段骨骼的长度(分别称为 c 和 a)。由于这些骨骼的长度不会改变,可以在 Start 函数中计算它们。然而,这要求机械臂在游戏开始时处于一个预制的配置状态。

private length0;
private length1;

void Start ()
{
    length0 = Vector2.Distance(Joint0.position, Joint1.position);
    length1 = Vector2.Distance(Joint1.position, Hand.position  );
}

旋转关节

在展示代码的最终版本之前,让我们先从一个简化的版本开始。如果我们直接将方程(1)和(2)转换为代码,我们会得到类似以下的内容:

void Update ()
{
    // Distance from Joint0 to Target
    float length2 = Vector2.Distance(Joint0.position, Target.position);

    // Inner angle alpha
    float cosAngle0 = ((length2 * length2) + (length0 * length0) - (length1 * length1)) / (2 * length2 * length0);
    float angle0 = Mathf.Acos(cosAngle0) * Mathf.Rad2Deg;

    // Inner angle beta
    float cosAngle1 = ((length1 * length1) + (length0 * length0) - (length2 * length2)) / (2 * length1 * length0);
    float angle1 = Mathf.Acos(cosAngle1) * Mathf.Rad2Deg;

    // Angle from Joint0 and Target
    Vector2 diff = Target.position - Joint0.position;
    float atan = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;

    // So they work in Unity reference frame
    float jointAngle0 = atan - angle0;    // Angle A
    float jointAngle1 = 180f - angle1;    // Angle B

    ...
}

数学函数arccos和arctan在Unity中分别被称为 Mathf.Acos 和 Mathf.Atan2。此外,最终的角度通过 Mathf.Rad2Deg 转换为度数,因为 Transform 组件接受的是度数而不是弧度。

针对无法到达的目标位置进行调整

虽然上述代码在许多情况下可以正常工作,但在目标位置没法到达的情况下它就会失效。如果目标位置超出了机械臂的可触及范围,当前实现没有考虑到这种情况,会导致我们不期望的行为。

一种常见的解决方案是将机械臂完全伸展成直线状态并朝向目标位置。这种状态与我们试图模拟的期望行为是一致的。

下面的代码通过检查根节点到目标的距离是否大于机械臂的总长度来检测目标是否超出可到达的范围:

void Update ()
{
    float jointAngle0;
    float jointAngle1;

    float length2 = Vector2.Distance(Joint0.position, Target.position);

    // Angle from Joint0 and Target
    Vector2 diff = Target.position - Joint0.position;
    float atan = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;

    // Is the target reachable?
    // If not, we stretch as far as possible
    if (length0 + length1 < length2)
    {
        jointAngle0 = atan;
        jointAngle1 = 0f;
    }
    else
    {
        float cosAngle0 = ((length2 * length2) + (length0 * length0) - (length1 * length1)) / (2 * length2 * length0);
        float angle0 = Mathf.Acos(cosAngle0) * Mathf.Rad2Deg;

        float cosAngle1 = ((length1 * length1) + (length0 * length0) - (length2 * length2)) / (2 * length1 * length0);
        float angle1 = Mathf.Acos(cosAngle1) * Mathf.Rad2Deg;

        // So they work in Unity reference frame
        jointAngle0 = atan - angle0;
        jointAngle1 = 180f - angle1;
    }

    ...
}

关节旋转

现在剩下的是如何让关节旋转。这可以通过访问关节的 Transform 组件的 localEulerAngles 属性来完成。不过在Unity中没法直接更改 z 轴的角度,因此需要复制该向量,修改后来替换原来的值。

Vector3 Euler0 = Joint0.transform.localEulerAngles;
Euler0.z = jointAngle0;
Joint0.transform.localEulerAngles = Euler0;

Vector3 Euler1 = Joint1.transform.localEulerAngles;
Euler1.z = jointAngle1;
Joint1.transform.localEulerAngles = Euler1;

结论

这篇文章为2D机械臂逆运动学的课程画上了句号。

你可以在这里继续阅读本在线课程的其余部分:

0 Answers