参考:Two Bone IK
其实就是最简单的IK问题了,有三个Joint A、B和C组成的BoneChain A->B->C,在A的位置不变的情况下,通过改变A和B的旋转,把C挪到目标的位置点,UE里提供了这么个动画节点,如下图所示:

此节点应该只能在动画蓝图里使用,这里的Effector指的是BoneChain的最尾部的Joint,这个节点的输入有:
还有个重要数据,就是在Two Bone IK的Details栏里指定需要Effector对应的Bone,UE会自动找到其Parent和Parent的Parent骨骼,如下图所示:

IK的本质就是在Bone Chain里,改变除了Effector以外其他Joint的Rotation数据,而实际编辑时,改变的是除了Start Joint以外其他Joint的Transition数据,如下图所示:

所谓的IK,就是根据这些Joint的坐标,反算出Joint的Rotation数据
关于Joint Target Location
注意它并不是Middle Joint的目标值,可以试想一下,在Two Bone IK过程中,不考虑特殊情况时,三个Joint里,起点和终点的坐标是固定的,此时只要求出Middle Bone的坐标即可。但其实此时的Middle Joint的坐标的解是有无穷多个的,所以此时,额外规定了一个Joint Target Location的坐标,它用于表示,Middle Joint的坐标会在IK Effector、RootPos和Joint Target Location三个点组成的平面上
可以看看这个视频Knee Pole Vectors,里面拖拽的就是Joint Target Location
主要是设置两个地方:
设置世界坐标下的IK Goal
Effector Local Space选择World Space,然后调整相应的坐标即可,这个Space只会影响输入的Effector Location,不会影响下面的Joint Target Location,我这里选择hand_r为IK Bone,如下图所示:

也可以改成Bone Space下的Effector Local Space类型,此时就会记录相对于特定Bone坐标的Offset了,这里需要指定对应参考Bone的坐标,我这里仍然选择hand_r为Effector Target,此时的Gizmos会直接绘制在要改变的关节上,如下图所示:

Joint Target Location的设置
Two Bone IK Chain里不止能调整End Bone,也可以调整Middle Bone的坐标,这里的Joint Target Location也有自己设置的类型,不过需要注意的是,根据这篇文章,Joint Target Location是不支持World Space和Component Space模式的,这两种模式下,设置该值会不起作用,如下图所示:

左上角的图还有个小的gizmos,应该绘制的是Joint Target Location,好像不让拖拽
可以先来列举一下问题,我有三个Joint的坐标,它们是已知的,分别为RootPos、JointPos和EndPos,如下图红线所示,假设我要移动JointPos的位置,让它变到IOutJointPos点,此时我三个Joint的坐标变为RootPos、OutJointPos和EffectorPos,图中还有一个指定的JointTargetPos点,它与这三个点在同一个平面上:

那么如何求解出OutJointPos的值呢,这里有以下信息(Tips):
这里把图简化一下,如下图所示,注意下面的点都是三维空间的点:

这里可以把三角形的边连起来,如下图所示,同时作一条OutJointPos往对边的垂线,垂点为P:

可以得到:OutJointPos的坐标等于:RootPos的坐标加上向量RootPos->P,再加上向量P->OutJointPos,这俩向量都好算,这里有边RootPos到OutJointPos,设长度为A,向量的大小分别是cos(α) * A和sin(α) * A,向量的方向也好算,一个是RootPos到EffectorPos的单位向量,另外一个是算出垂直于RoootPos-EffectorPos线段, 且指向JointTargetPos方向的垂线方向即可,计算代码如下:
// 计算DesiredDir向量, 它代表RootPos指向EffectorOis的方向
FVector DesiredDir = (DesiredPos - RootPos).GetSafeNormal;// 计算JointTargetDelta向量, 它代表RootPos指向JointTargetPos的向量
// Get joint target (used for defining plane that joint should be in).
FVector JointTargetDelta = JointTarget - RootPos;// 这里的DesiredDir为单位向量, JointTargetDelta不是单位向量, | 符号代表点乘
// ((JointTargetDelta | DesiredDir) * DesiredDir)算出的是RootPos->DesiredPos在RootPos->JointTarget上的投影向量
// 算出垂直于RoootPos-EffectorPos线段, 且指向JointTargetPos方向的垂线方向, 即JointBendDir
JointBendDir = JointTargetDelta - ((JointTargetDelta | DesiredDir) * DesiredDir);
// 算出垂线方向
JointBendDir.Normalize();
相关代码核心基本都放在AnimationCore::SolveTwoBoneIK这个静态函数里了,执行地方是在FAnimNode_TwoBoneIK::EvaluateSkeletalControl_AnyThread函数里,会在动画节点的Evaluate阶段,在Input动画Evaluate之后,调用此函数执行类似于Pose后处理的操作。
核心函数会最后算出新的OutJointPos的坐标值,不过IK过程本质改变的是Joint的旋转数据,应该后面会用类似Quaterion.FromToRotation函数算出BoneChain的Parent和MiddleJoint的DeltaRotation吧
看了下代码,确实是这样:
void SolveTwoBoneIK(FTransform& InOutRootTransform, FTransform& InOutJointTransform, FTransform& InOutEndTransform, const FVector& JointTarget, const FVector& Effector, float UpperLimbLength, float LowerLimbLength, bool bAllowStretching, float StartStretchRatio, float MaxStretchScale)
{FVector OutJointPos, OutEndPos;FVector RootPos = InOutRootTransform.GetLocation();FVector JointPos = InOutJointTransform.GetLocation();FVector EndPos = InOutEndTransform.GetLocation();// IK solverAnimationCore::SolveTwoBoneIK(RootPos, JointPos, EndPos, JointTarget, Effector, OutJointPos, OutEndPos, UpperLimbLength, LowerLimbLength, bAllowStretching, StartStretchRatio, MaxStretchScale);// IK解算完后, 改变joint的rotation, 由于可能有骨骼伸缩, 这里还要改变骨骼长度// Update transform for upper bone.{// Get difference in direction for old and new joint orientationsFVector const OldDir = (JointPos - RootPos).GetSafeNormal();FVector const NewDir = (OutJointPos - RootPos).GetSafeNormal();// Find Delta Rotation take takes us from Old to New dirFQuat const DeltaRotation = FQuat::FindBetweenNormals(OldDir, NewDir);// Rotate our Joint quaternion by this delta rotation// 注意DeltaRotation是左乘InOutRootTransform.SetRotation(DeltaRotation * InOutRootTransform.GetRotation());// And put joint where it should be.InOutRootTransform.SetTranslation(RootPos);}// update transform for middle bone{// Get difference in direction for old and new joint orientationsFVector const OldDir = (EndPos - JointPos).GetSafeNormal();FVector const NewDir = (OutEndPos - OutJointPos).GetSafeNormal();// Find Delta Rotation take takes us from Old to New dirFQuat const DeltaRotation = FQuat::FindBetweenNormals(OldDir, NewDir);// Rotate our Joint quaternion by this delta rotationInOutJointTransform.SetRotation(DeltaRotation * InOutJointTransform.GetRotation());// And put joint where it should be.InOutJointTransform.SetTranslation(OutJointPos);}// Update transform for end bone.// currently not doing anything to rotation// keeping input rotation// Set correct location for end bone.InOutEndTransform.SetTranslation(OutEndPos);
}
先看一下UE里相关的参数:
结合代码,设实际骨骼链长度与预期IK链长度的比例为ReachRadio,我总结了以下规则:
ReachRadio在(Start Stretch Ratio, 1)范围内,骨骼也会被伸长,具体伸长的比例后面代码里会有具体到代码,先是计算骨骼比例ReachRadio,如下所示:
// 计算预期骨骼链长度与实际骨骼链长度的比例
const float ReachRatio = DesiredLength / MaxLimbLength;
然后计算[Start Stretch Ratio, Max Stretch Ratio]这个区间范围的值:
// StartStretchRatio和MaxStretchScale都是在IK部分设置的值, 默认为1.0和2.0
const float ScaleRange = MaxStretchScale - StartStretchRatio;
然后需要设计一个算法,这个算法需要满足以下条件:
整体代码如下,就是这里计算ScalingFactor的算法稍微有一点绕:
if (bAllowStretching)
{// StartStretchRatio和MaxStretchScale都是在IK部分设置的值, 默认为1.0和2.0const float ScaleRange = MaxStretchScale - StartStretchRatio;if (ScaleRange > KINDA_SMALL_NUMBER && MaxLimbLength > KINDA_SMALL_NUMBER){// 计算预期骨骼链长度与实际骨骼链长度的比例const float ReachRatio = DesiredLength / MaxLimbLength;// 下面这个算法有点绕, 总之是让(1 + ScalingFactor)成为BoneChain的长度变化系数, 同时防止骨骼长度变小的情况// FMath::Clamp((ReachRatio - StartStretchRatio) / ScaleRange, 0.f, 1.f)是算出等比例变化的量// 当ReachRatio为StartStretchRatio时, 返回0, 当ReachRatio为MaxStretchScale时, 返回1// 再乘以(MaxStretchScale - 1) 是算出等比例伸长的增量const float ScalingFactor = (MaxStretchScale - 1.f) * FMath::Clamp((ReachRatio - StartStretchRatio) / ScaleRange, 0.f, 1.f);// 当MaxStretchScale小于1时, 这里的ScalingFactor < 0, 此时的UpperLimbLength等信息不会改变if (ScalingFactor > KINDA_SMALL_NUMBER){// ScalingFactor大于0, 所以这里在AllowStretching时, 骨骼只可能变长, 不可能变短LowerLimbLength *= (1.f + ScalingFactor);UpperLimbLength *= (1.f + ScalingFactor);MaxLimbLength *= (1.f + ScalingFactor);}}
}
我自己写了个算法,感觉更容易理解一些,其实计算过程是一样的:
if (bAllowStretching)
{// StartStretchRatio和MaxStretchScale都是在IK部分设置的值, 默认为1.0和2.0const float ScaleRange = MaxStretchScale - StartStretchRatio;if (ScaleRange > KINDA_SMALL_NUMBER && MaxLimbLength > KINDA_SMALL_NUMBER){// 计算预期骨骼链长度与实际骨骼链长度的比例const float ReachRatio = DesiredLength / MaxLimbLength;float ScalingFactor = FMath::Clamp((ReachRatio - StartStretchRatio) / ScaleRange, 0.f, 1.f);// 把ScalingFactor从[0,1]区间映射到[1, MaxStretchScale]区间ScalingFactor = Mathf.Map(1, MaxStretchScale, ScalingFactor);// 返回1 + (MaxStretchScale - 1) * ScalingFactor // 当MaxStretchScale小于1时, 这里的ScalingFactor < 0, 此时的UpperLimbLength等信息不会改变if (ScalingFactor > KINDA_SMALL_NUMBER){LowerLimbLength *= ScalingFactor;UpperLimbLength *= ScalingFactor;MaxLimbLength *= ScalingFactor;}}
}
这一块我还不太清楚,等我研究完动画系统里的Twist机制,再补充这块知识