本篇文章主要讲解如何在Unity中使用Joint组件完成一些刚体物理之间的连接效果,并且讲解一个简单案例。

什么是Joint

官方文档介绍
Joint可以连接一个刚体与 另一个刚体 或世界空间某点,Joint可以通过施加力的方式来限制运动,joint中文翻译可以叫 约束。
在力学观点下,运动分为6个自由度,沿着xyz轴的位移,绕xyz轴的旋转。
通过将某些轴的位移和旋转给一些限制条件来达到限制刚体运动的目的。

Unity的Joint实际就是调用了
NVIDIA PhysX的PxConstraint

有哪些Joint

PhysX提供了 Fixed Joint,Character Joint、Hinge Joint、Spring Joint等等。
这些种种的Joint其实都是基于通用的 Configurable Joint调配参数封装出来的,比如Fixed Joint就是将6自由度运动全部配置为lock,两只刚体加上了这个约束就会绑定在一起,没有任何相对运动。

通过点击这个按钮我们可以看到一些用来调节joint参数的Gizmos。

Joint计算原理

在Unity中 Joint是作为Component存在,他必须依附一个Go,这个被依附的Go需要有rigidbody,Joint会影响这个刚体,
通常我们指定另一个刚体到ConnectedBody上表示另一个被Joint影响的刚体,如果想要这个joint attached 刚体被固定在空间点上,那么conectedBody可以不填。

自由度配置

Joint拥有3个位移自由度和3个旋转自由度。相应的,在Configurable Joint中对应如下几个参数:

  • 位移有 - XMotion、YMotion、ZMotion
  • 旋转有 - AngularXMotion,AngularYMotion,AngularZMotion
    在这6个自由度上,分配有以下三种配置:
  • ConfigurableJointMotion.Free
  • ConfigurableJointMotion.Locked
  • ConfigurableJointMotion.Limited
    其中Free意味着约束不存在。比如XMotion Free,代表了两个刚体在X轴向可以相对随意移动。
    Locked则表示刚性约束,比如XMotion Locked代表了两个刚体在X轴向的相对位置被完全固定不可改变。(但在实际物理模拟中是存在被强行拆散可能性的,例如刚体穿插等等,会导致Solver失败)
    Limited则意味着有限范围的偏差。当超出有效范围时,约束将会开始生效,企图使两个刚体回到正常偏差范围内。

Axis与Anchor

Joint默认会以这个attach的GO的Transform坐标系作为Joint的六自由度坐标系,如果我们想要一个另外的坐标系,需要填Axis和SecondAxis,他会作为这个body transform的child而存在。

然后是使用Joint是最容易出错的点——Anchor
这里我们画一幅图来说明

cubeA 身上放了一个Joint组件,连接着cubeB,cubeA的anchor放在蓝圈位置,joint的坐标系的xy是红绿线。
anchor实际上有两个,一个连着A 叫Anchor,另一个连着B,叫ConnectedAnchor,一般我们都会勾选AutoConfigureConnectedAnchor让两个anchor在一起。
从anchor到A的连线在joint游戏开始时 anchor到重心的距离会被记录,可以看做一个不可变形的刚杆(如果anchor在某个cubeA的中心那么属于cubeA到anchor就没有钢杆了),
游戏运行时anchor和conectedanchor会发生相对运动, 以joint坐标系为准,这里我们假定没有自己配置Axis,那么joint坐标系就是A的坐标系,
如果此时我们配置了Joint的XYZ的Motion为lock,其他angular xyz motion为free,并且把A的刚体配置成Kinematic(让他不受重力下落影响,固定在空中),
那么就会得到一根钢杆一头在蓝圈 一头在cube的重心的自由旋转运动,拽着A去甩B其实有点像双节棍。

如果我们配置了angular xyz为lock,xyz的motion中某个轴 比如Y轴为free,那么便可以得到沿着y轴的滑动一样的效果。

通过对于不同维度的控制我们可以得到很多不一样的效果。

Limit与Spring

整个计算流程包括三步
1、计算两物体轴向距离
2、将轴向距离投影到joint空间下
3、调节轴向距离满足joint参数

如果我们想让两个anchor的某些轴向的位移在一个范围里运动,那便可以将这几个轴的motion调整为limit,这种两个anchor的距离(注意是距离,算的是两点作差的值)会被限制在limit内,
如果把A固定,B自由下落那么B到了某个点位就会啪的一下卡住,好像撞到了地面一样,实际上是被Joint拉住了。
如果我们想他在这个边缘位置不要这么生硬的停下而是有弹性、柔和一点的运动,那么可以配置spring值。

需要注意的是xyz都是共享一个limit的,只存在anchor之间 按线运动、按圆圈运动、在球体范围内运动三种情况。
默认spring是0,表示没有任何弹簧就是一个很刚性的约束(看起来非常硬)。
damper标志了弹簧震动的阻尼系数,阻尼会根据刚体运动速度产生一个反向的作用力,以降低震动的频率。

angular limit和其spring的配置原理也是一样的,只不过是计算轴向距离变成计算两个rotation的差异,另外angularlimit关于主轴的计算可定制性更强,可以存在最小负角和最大正角,其他两个轴则定义相对限制多。
按两个方向分别进行定义是由现实意义的,因为人的很多关节都是这样的。比如你的膝关节、肘关节都只能往一个角度弯曲,而不能反向弯曲。这也决定了当我们使用Joint来模拟物理骨骼时,必须正确的分配Joint主轴朝向。
至于为什么不每个轴都分配最大和最小角度,可能是有数学限制吧,这里博主也不知道。

Drive与Target

上述所讲的anchor与anchor之间的限制主要还是一种受到外力导致运动后 应该怎么限制运动,
其实Joint还提供了一种内力驱动来让Joint对两个刚体产生力,很多的结构比如挖掘机旋转关节,车轮的悬挂系统都会输出一些内部的力对外做功。
要配置这些力我们就要用到Drive和Target。
首先一般用drive的轴的自由度配置都会配成free,然后通过配置target position、target rotation来对两anchor连接的刚体施加力。
主要分为几个步骤
1、计算两个刚体的坐标差异
2、将差异投影到joint坐标系
3、计算差异和配置的target的距离
4、为了让距离满足0,会对两刚体求解施加力

PositionRelative = PositionOfConnected - PositionOfBinded
ErrorOfPosition = TargetPosition - PositionRelative 
F = Spring * ErrorOfPosition - Damp * Velocity
  • PositionOfBinded为Joint所绑定的刚体Anchor位置
  • PositionOfConnected为另一个连接的刚体的Anchor位置
  • PositionRelative为当前两者位置之差,而TargetPosition则为预期之差
  • ErrorOfPosition为预期和当前的差项,Joint将根据这个差项产生一个弹性力作用于两个刚体,使得PositionRelative在迭代中逼近TargetPosition

关于Rotation
差异和target 计算的部分包括:

ErrorOfRotation = TargetRotation - (RotationRelative - RotationRelativeInited)
  • RotationOfBinded为Joint所绑定刚体的旋转量
  • RotationOfConnected为连接刚体的旋转量
  • RotationRelative = RotationOfConnected - RotationOfBinded 为两者之差,也可以理解为Connected相对于Binded的旋转量。
  • RotationRelativeInited为初始情况下的RotationRelative

所有的Drive,都由以下结构构成:

  • PositionSpring - 表示驱动力的弹性系数
  • PositionDamper - 表示驱动力的阻尼系数
  • MaximumForce - 表示最大驱动力值。即意味着F不随着Error无限增大
    利用以上的定义,将会使用以下公式产生弹性力:
    \(F_{drive}=Spring*E - Damp * dE/dt\)
    E代表了当前状态与目标状态之差。

可以用代码运行时去修改target position、target rotation来获得一些有趣的物理效果。

Joint应用案例

讲解一个基于Joint的吊车的实现

车轮

旋转

车轮我们使用了capsule collider,连接在box拼接的车身的刚体上
然后让capsule collider关于轴心方向的轴的旋转是free的,其他旋转都lock,接着我们希望车轮有一点轻微的悬挂系统,如果此时我们把joint组件加在了车轮上,使用车轮坐标系,我们会发现joint没法找到一个稳定的上方向,这导致我们决定将joint加在车身,让joint坐标系跟车身保持一致,维持一个相对稳定的Joint坐标系。

红色的圆圈表示这个joint绑定的东西在joint坐标系的x轴可以自由的旋转。

这里的anchor配置相当重要,anchor位置为车轮的中心在车身坐标系下的相对位置,这里我通过编辑器写了一点计算代码配置anchor位置。

然后我们通过控制车轮的x轴角速度便可以实现一个摩擦力驱动的车轮了。

悬挂系统

我们xyz的linear motion都是lock住的,这样遇到一些凸起的路面我们轮子就和车身的相对距离永远不变,这样不符合真实的感觉,真实的感觉应该是轮子会往上有一点抬升,而车身不会有y轴方向的运动。

这里我们的y轴和车身绑定已经相当稳定,我们可以把y轴配置成free,然后把yDrive调大,target position是000,其实也就是让这个车轮回归初始位置的意思,这样当车轮支撑车身时候车身会有重力影响产生一定的下沉同时有被举起的感觉,悬空时车轮也会往下掉一点,遇到障碍物是压过了障碍物的轮子会比其他轮子往上抬升一点。

总体运动起来 车身的震动幅度相比没有悬挂系统更小一点。

吊臂

旋转

吊车的大臂可以以转台为中心来做旋转,我们以y为上方向 配置angular y motion为free,然后在运行时根据输入来配置target rotation,可以实现大臂绕着一个点旋转,通过把drive和damp同时拉大可以获得一个有些重量感的吊臂。

抬升

抬升的做法有一点trick,对于一般的吊车这里通常是液压的,由下面的浅红色小臂通过液压给力然后把红色的大臂推出去。大臂配置一个绕轴旋转。
但是笔者经过尝试,发现这种通过给力的方式在车子运动起来的同时大臂会摇摇晃晃,因为是通过力来作用的,就很容易出现弹簧一样的感觉,并且也不利于控制具体的抬升角度。

于是这里的做法是大臂尾部两个joint得到一个绕轴旋转,绕轴旋转的joint通过配置drive 和 target rotation来让吊臂产生某个角度的抬升。

接着我们制作了一个小臂的一头连着大臂一头连着转台,连着大臂的那端可以沿着小臂轴向有位移,形成了一个动轨滑块结构。

然后让这个小臂对大臂施加一个拉力,这个力往下,而上一个joint给的力往上,以此可以减少大臂的摇晃感。

吊钩

伸长

最后是吊钩,吊钩其实就是让这个钩子可以沿着一条轴伸长,始终日常垂直往下,这里我显示给钩子摆了一个垂直往下的姿势,y轴向上,然后joint组件添加在钩子上,anchor一头绑在吊臂上,一头绑在吊钩center,
允许y轴可以free运动,用ydrive来分离两个anchor,驱动吊钩往下运动,这样就得到了一个绕着初始点旋转的一根弹性杆,用来做伸长后的吊钩看起来还行。

除了这些外,给一些angular的drive并且target rotation配置成000,可以让吊钩在空中甩来甩去时更快的回到垂直向下的初始旋转位置。

吸附

最后吊钩要把东西吊起来,可以做一个范围检测,运行时创建一个lock的joint 把钩子和被钩的东西绑在一起,然后再收起吊绳,我们就能吊起东西了!

总结

对绳子不太满意,因为有点弹性杆的感觉,实际上这种拉的紧绷的钢性绳子相当难模拟,拉起时候的感觉也会有弹来弹去的味道,暂时没有想到很好的解决办法

引用

https://docs.unity3d.com/cn/current/Manual/class-ConfigurableJoint.html
https://zhuanlan.zhihu.com/p/380394542

flyingziming
2023.3.20

标签: none

添加新评论