1. 从上一阶段的遗留问题说起:为什么碰撞检测和姿态解算必须放在一起做
如果你跟着这个系列的第一篇一路做下来,大概率已经能让机械臂在场景里动起来了——关节能转、末端能跟着目标点走、看起来像那么回事。但真正上手抓东西的时候,问题就全冒出来了:夹爪明明对准了方块,结果穿模而过;或者末端到了位置,姿态却是歪的,夹爪以一个诡异的角度怼上去,方块直接被弹飞。这些现象背后其实是两个独立但必须协同的子系统在打架:碰撞检测负责“别穿过去”,姿态解算负责“以正确的角度贴上去”。任何一个没调好,抓取都会失败。
这篇内容就是来解决这个阶段的典型问题的。我会把 Unity 里机械臂从“能动能走”推进到“能稳稳抓住一个物体”的完整链路拆开讲,包括碰撞体的选型与配置、逆向运动学(IK)求解姿态时的坑、四元数插值的正确用法、以及抓取瞬间的物理参数调优。适合已经做过基础关节驱动、想让机械臂真正干活的开发者,也适合做数字孪生、工业仿真、教育演示这类项目、需要机械臂和场景物体产生真实交互的朋友。整篇内容基于 Unity 2022 LTS 和通用渲染管线验证,其他版本思路一致,参数需要微调。
我个人的习惯是:机械臂控制这件事,先保证碰撞不出错,再谈姿态精不精准。因为碰撞穿模是“硬伤”,一旦发生,后面所有逻辑都白搭;而姿态偏差是“软伤”,可以通过迭代和补偿慢慢收敛。所以下面的顺序也是先碰撞、后姿态、最后合起来做抓取。
2. 碰撞检测:机械臂抓取的第一道门槛
2.1 为什么机械臂的碰撞体不能直接用 Mesh Collider
新手最容易犯的错,就是给机械臂每个连杆直接挂Mesh Collider,觉得“模型长什么样,碰撞体就长什么样,最准”。实测下来这是灾难。机械臂的连杆模型往往有倒角、螺纹、线缆槽这些细节,Mesh Collider会把这些全部纳入碰撞计算,三角面数量轻松上万。六个关节串起来,每帧的碰撞检测开销直接让帧率掉到个位数。更麻烦的是,Mesh Collider默认不是凸体(non-convex),两个非凸碰撞体之间的检测在 PhysX 里是受限的,动态物体之间的碰撞经常出现“该碰不碰”的诡异现象。
正确的做法是用基础几何体拼出碰撞代理。每个连杆用一到两个Box Collider或Capsule Collider近似,关节处用一个Sphere Collider覆盖旋转区域。这样碰撞体面数从万级降到几十,检测效率提升两个数量级,而且凸体之间的碰撞结果稳定可预测。
具体配置上,我一般这样处理:
- 连杆主体:
Box Collider,尺寸贴合连杆外形,Center对齐连杆质心 - 关节连接处:
Sphere Collider,半径略大于关节外壳,保证旋转时不会露出缝隙 - 夹爪指尖:
Box Collider,这是唯一需要精确的地方,因为要和被抓物体接触
注意:碰撞代理不需要和视觉模型完全重合,允许有 5% 到 10% 的偏差。宁可碰撞体略小一点,也不要过大,否则机械臂会在还没碰到物体时就触发碰撞,导致 IK 求解失败。
2.2 碰撞层(Layer)与碰撞矩阵的规划
机械臂场景里通常有三类物体:机械臂自身、被抓目标、环境障碍。如果全部放在 Default 层,机械臂的连杆之间会互相碰撞,IK 求解时关节还没到位就被自己的碰撞体挡住,解算器直接报无解。所以必须做层分离。
我的分层方案是这样的:
| 层名 | 用途 | 与其他层的碰撞关系 |
|---|---|---|
| RobotArm | 机械臂所有连杆和关节 | 与 Target、Obstacle 碰撞,与自身不碰撞 |
| Target | 被抓取的物体 | 与 RobotArm、Obstacle 碰撞 |
| Obstacle | 桌面、围栏等环境 | 与所有层碰撞 |
| Gripper | 夹爪指尖单独一层 | 仅与 Target 碰撞 |
在Edit > Project Settings > Physics > Layer Collision Matrix里把 RobotArm 与自身的勾选去掉,这一步能解决 80% 的“IK 解算无解”问题。Gripper 单独分层是为了后续做抓取判定时,能精确检测“指尖是否接触到目标”,而不被连杆的碰撞干扰。
2.3 用 SphereCast 做抓取前的接近检测
碰撞体配好之后,机械臂能“不穿模”了,但还需要知道“什么时候该闭合夹爪”。最直接的做法是在指尖前方做射线检测,但单条Raycast太细,目标稍微偏一点就漏检。我推荐用Physics.SphereCast,它相当于用一个球体去扫掠,容错率高得多。
public class GripperSensor : MonoBehaviour { public float castRadius = 0.02f; public float castDistance = 0.08f; public LayerMask targetLayer; public bool DetectTarget(out RaycastHit hit) { Vector3 origin = transform.position; Vector3 direction = transform.forward; return Physics.SphereCast(origin, castRadius, direction, out hit, castDistance, targetLayer); } }castRadius取指尖宽度的一半左右,castDistance取夹爪行程的 1.5 倍。这样当目标进入夹爪可闭合范围时,SphereCast就能稳定命中。实测下来,相比单射线,SphereCast的漏检率从 30% 降到 5% 以内。
实操心得:
SphereCast的起点不要放在指尖表面,要稍微往内缩 1 到 2 厘米。因为如果起点就在表面,球体初始位置可能已经和目标重叠,PhysX 会返回距离为 0 的命中,导致判定时机过早。
3. 姿态解算:让夹爪以正确角度贴上去
3.1 逆向运动学求解姿态的基本原理
机械臂的末端要到达某个位置并保持某个朝向,这个过程叫逆向运动学(IK)。Unity 自带的AnimatorIK 主要面向人形角色,对机械臂这种串联结构支持有限,所以大多数项目会用两种方案:一是自己写解析 IK(Analytical IK),二是用Unity.Mathematics配合迭代求解(如 CCD、FABRIK)。
解析 IK 的优点是快、精确,缺点是只适用于特定构型(比如六轴串联臂)。以常见的六轴机械臂为例,前三个关节决定末端位置,后三个关节决定末端姿态,这叫位置与姿态解耦。如果机械臂满足 Pieper 准则(三个相邻关节轴交于一点),就能用几何法直接算出所有关节角。
我一般先用解析法求位置解,再用四元数求姿态解。位置解的核心是余弦定理:
// 已知连杆长度 L1、L2,目标距离 d // 求肘关节角度 float cosTheta = (L1 * L1 + L2 * L2 - d * d) / (2 * L1 * L2); cosTheta = Mathf.Clamp(cosTheta, -1f, 1f); float elbowAngle = Mathf.Acos(cosTheta) * Mathf.Rad2Deg;这里Mathf.Clamp是必须的,因为浮点误差可能让cosTheta略微超出 [-1, 1],Acos会返回 NaN,整个 IK 直接崩掉。这个坑我踩过不止一次,后来养成习惯,所有反三角函数输入都先 Clamp。
3.2 四元数姿态解算:为什么不用欧拉角
姿态解算里最容易出问题的就是旋转表示。很多人图省事用欧拉角,transform.eulerAngles = new Vector3(x, y, z),结果遇到万向节死锁(Gimbal Lock),某个角度附近机械臂会突然翻转。机械臂抓取时末端姿态变化范围大,欧拉角几乎必然出问题。
四元数没有万向节死锁,插值也平滑,是姿态解算的正确选择。核心操作有两个:一是从目标朝向构造四元数,二是用Quaternion.Slerp做平滑过渡。
// 根据目标法线和接近方向构造末端姿态 Quaternion targetRotation = Quaternion.LookRotation(approachDir, targetNormal); // 平滑插值,避免姿态突变 currentRotation = Quaternion.Slerp(currentRotation, targetRotation, Time.deltaTime * rotationSpeed);Quaternion.LookRotation的第一个参数是“前方”,第二个是“上方”。抓取时,approachDir是夹爪接近方向(通常是目标表面法线的反方向),targetNormal是目标表面的法线。这样构造出来的姿态,夹爪会垂直于目标表面贴上去,抓取成功率最高。
注意:
Quaternion.Slerp的第三个参数是插值系数,不是速度。如果直接写Time.deltaTime * rotationSpeed,当rotationSpeed较大时,系数可能超过 1,Slerp会 Clamp 到 1,表现为瞬间到位。正确做法是用1 - Mathf.Exp(-rotationSpeed * Time.deltaTime)做指数平滑,这样无论帧率如何变化,收敛速度都一致。
3.3 姿态解算中的坐标系转换陷阱
机械臂的 IK 求解涉及多个坐标系:世界坐标系、基座坐标系、各连杆局部坐标系、工具坐标系。姿态解算最容易出错的地方就是坐标系搞混。比如目标物体的法线是世界坐标系的,但 IK 求解需要在基座坐标系里算,中间少了一步转换,结果就是夹爪朝向完全不对。
我的做法是:所有 IK 计算统一在基座坐标系里做。先把目标的位置和姿态从世界坐标系转到基座坐标系,求解完关节角,再把关节角应用到各连杆的局部旋转上。转换用Transform.InverseTransformPoint和Transform.InverseTransformDirection。
Vector3 localTargetPos = baseTransform.InverseTransformPoint(target.position); Vector3 localTargetNormal = baseTransform.InverseTransformDirection(target.up);这样整个求解过程只在一个坐标系里,逻辑清晰,调试也方便。如果发现姿态不对,只需要检查转换那一步,不用满世界找问题。
4. 抓取实现:把碰撞检测和姿态解算串起来
4.1 完整的抓取状态机设计
机械臂抓取不是一步到位的,而是一个多阶段过程。我一般用一个状态机来管理,状态划分如下:
| 状态 | 行为 | 退出条件 |
|---|---|---|
| Idle | 待机,等待抓取指令 | 收到目标物体 |
| Approaching | IK 驱动末端接近目标 | 距离目标小于阈值 |
| Aligning | 姿态解算,调整夹爪朝向 | 姿态误差小于 5 度 |
| Descending | 沿接近方向直线下降 | SphereCast 命中目标 |
| Grasping | 闭合夹爪,锁定物体 | 夹爪闭合到位 |
| Lifting | 抬起物体 | 到达指定高度 |
| Done | 抓取完成 | 收到释放指令 |
每个状态只做一件事,状态之间的转换条件明确。这样调试时哪个环节出问题一目了然。比如夹爪总是抓空,就看Descending到Grasping的转换,大概率是SphereCast的检测距离或层设置有问题。
4.2 抓取瞬间的物理参数调优
夹爪闭合的瞬间,物理引擎会计算接触力。如果参数不对,要么物体被弹飞,要么夹爪穿模。关键参数有三个:
- 夹爪的
Rigidbody.mass:不能太大,否则接触时动量过大,物体被撞飞。我一般设成目标物体质量的 2 到 3 倍。 - 接触材质的
Dynamic Friction:设成 0.6 到 0.8,保证夹住后不打滑。 Fixed Timestep:默认 0.02 秒,抓取瞬间可能不够精细,可以临时调到 0.01 秒。
另外,夹爪闭合不要用Transform直接移动,要用Rigidbody.MovePosition或者在关节上加Configurable Joint并设置targetPosition。直接改Transform会绕过物理引擎,接触力计算不出来,物体要么穿过去,要么被瞬移的碰撞体弹开。
// 用 Configurable Joint 驱动夹爪闭合 JointDrive drive = new JointDrive { positionSpring = 500f, positionDamper = 50f, maximumForce = 100f }; joint.xDrive = drive; joint.targetPosition = new Vector3(closeDistance, 0, 0);positionSpring和positionDamper的比例大概是 10:1,maximumForce要足够大,否则夹不紧。实测下来,maximumForce至少要是物体重力的 5 倍,才能保证抓取过程中物体不滑落。
4.3 抓取后的物体锁定与释放
物体被夹住后,如果继续用物理碰撞来维持抓取,会有微小的抖动和滑移。更稳的做法是抓取成功后,把物体的Rigidbody设为isKinematic = true,并把它Parent到夹爪上。这样物体完全跟随夹爪运动,没有任何物理抖动。
释放时反过来:先解除Parent,把物体放回场景根节点,恢复isKinematic = false,再给一个微小的初速度(如果需要抛出)。注意释放前要确认夹爪已经张开到足够宽度,否则物体可能和夹爪碰撞体穿插,恢复物理后直接被弹飞。
实操心得:
Parent切换的瞬间,物体的世界坐标会跳变。正确做法是先记录物体的世界位置和旋转,切换Parent后再设回去。用transform.SetParent(parent, true)可以自动保持世界坐标不变,这个true参数很多人不知道,但非常关键。
5. 常见问题与排查技巧实录
5.1 IK 解算无解或抖动
这是最高频的问题。排查顺序如下:
- 检查碰撞层矩阵,确认机械臂自身层没有勾选碰撞
- 检查目标点是否超出机械臂工作空间,超出时 IK 无解是正常的
- 检查关节角度限制,
Configurable Joint的angularXLimit等参数是否设得太紧 - 检查
Mathf.Acos的输入是否 Clamp,浮点误差会导致 NaN
如果 IK 解算结果抖动,大概率是目标点在两个解之间跳变(肘部上翻和下翻)。解决办法是加一个解选择偏好,比如始终选择肘部朝上的解,或者在两个解之间做平滑过渡。
5.2 夹爪穿模或抓空
穿模通常是碰撞体配置问题。检查夹爪指尖的Box Collider是否覆盖了实际接触区域,SphereCast的检测层是否包含目标层。抓空则多半是检测时机问题,SphereCast距离太短,夹爪还没到位就闭合了。把castDistance加大到夹爪行程的 2 倍试试。
5.3 物体被抓取后抖动或滑落
抖动一般是物理抓取(靠碰撞维持)导致的,改用Parent锁定即可。滑落则是摩擦力不够,检查接触材质的Dynamic Friction和Static Friction,同时确认夹爪的maximumForce是否足够。如果物体较重,还需要考虑在夹爪内侧加Physic Material,把摩擦力调到 0.9 以上。
5.4 姿态解算结果与预期不符
先确认坐标系转换是否正确。在Aligning状态里打印出目标姿态的四元数和当前末端姿态的四元数,对比Quaternion.Angle的差值。如果差值一直不收敛,检查Slerp的插值系数是否被 Clamp 到 1 了。如果差值收敛但方向不对,检查LookRotation的两个参数是否搞反了。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| IK 无解 | 碰撞层未分离 / 超出工作空间 | 检查 Layer Matrix,打印目标距离 |
| 姿态抖动 | 解跳变 / Slerp 系数过大 | 加解选择偏好,改用指数平滑 |
| 夹爪穿模 | 碰撞体过小 / 检测层错误 | 可视化碰撞体,检查 LayerMask |
| 物体滑落 | 摩擦力不足 / 夹持力不够 | 调高摩擦系数和 maximumForce |
| 抓取后抖动 | 物理抓取不稳定 | 改用 Parent 锁定 |
6. 一些让抓取更稳的进阶技巧
6.1 用预测位置补偿 IK 延迟
IK 求解和物理更新不在同一帧,快速移动时末端会滞后。可以在求解前对目标位置做一阶预测:predictedPos = currentPos + velocity * deltaTime。这样末端会提前往目标移动方向偏一点,实际到达时刚好对上。实测能减少 30% 到 40% 的到位误差。
6.2 夹爪接触力的自适应调整
不同物体质量不同,固定的夹持力要么夹不紧轻物体,要么压坏重物体。可以根据物体的Rigidbody.mass动态调整maximumForce:maxForce = mass * gravity * safetyFactor,safetyFactor取 3 到 5。这样轻物体用小力,重物体用大力,抓取更自然。
6.3 抓取失败的重试机制
再稳的系统也有失败的时候。加一个重试逻辑:如果Grasping状态超时(比如 0.5 秒还没检测到夹爪闭合到位),就退回Approaching状态重新来一次。重试前把目标位置稍微偏移一点,避免在同一个位置反复失败。重试次数设 2 到 3 次,超过就报错,避免死循环。
这套东西我在几个数字孪生项目里都用过,从六轴工业臂到小型桌面臂都跑得通。核心就一句话:碰撞检测保证不穿模,姿态解算保证贴得正,物理参数保证抓得牢。三件事分开调,调好再串起来,比一上来就整体调效率高得多。如果卡在某个环节,先把其他部分注释掉,单独测那一个模块,定位问题的速度会快很多。