UE的Two Bone IK

参考:Two Bone IK

什么是Two Bone IK

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

此节点应该只能在动画蓝图里使用,这里的Effector指的是BoneChain的最尾部的Joint,这个节点的输入有:

  • Effector Location: BoneChain的最尾部的Joint的位置,对应坐标系需要自行选择
  • Joint Target Location:注意它并不是Middle Joint的目标位置,而是帮助算出BoneChain的Middle Joint所在的平面的点,对应坐标系需要自行选择
  • 输入的Component Space下的Pose
  • Alpha值,Alpha从0平滑过渡到1的过程其实就是IK算法平滑应用上去的过程

还有个重要数据,就是在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


设置Two Bone IK

主要是设置两个地方:

  • 设置IK Bone,UE会根据设置的IK Bone,沿着BoneChain往上寻找两代,找到对应的三个Joint
  • 设置调整IK Goal的方式和坐标

设置世界坐标下的IK Goal
Effector Local Space选择World Space,然后调整相应的坐标即可,这个Space只会影响输入的Effector Location,不会影响下面的Joint Target Location,我这里选择hand_rIK Bone,如下图所示:
在这里插入图片描述
也可以改成Bone Space下的Effector Local Space类型,此时就会记录相对于特定Bone坐标的Offset了,这里需要指定对应参考Bone的坐标,我这里仍然选择hand_rEffector 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,好像不让拖拽


Two Bone IK的原理

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

Joint Target Location`
那么如何求解出OutJointPos的值呢,这里有以下信息(Tips):

  1. BoneChain在IK应用前后的骨骼长度是不会变的
  2. 当三角形的三条边的长度已知时,三角形的三个角度都是可以求出来的
  3. Root Pos、OutJointPos、JointTargetPos和Effector Pos的坐标都在同一平面上,除了OutJointPos,其他坐标都是已知的

这里把图简化一下,如下图所示,注意下面的点都是三维空间的点:
在这里插入图片描述
这里可以把三角形的边连起来,如下图所示,同时作一条OutJointPos往对边的垂线,垂点为P:
在这里插入图片描述
可以得到:OutJointPos的坐标等于:RootPos的坐标加上向量RootPos->P,再加上向量P->OutJointPos,这俩向量都好算,这里有边RootPos到OutJointPos,设长度为A,向量的大小分别是cos(α) * Asin(α) * 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 solver
	AnimationCore::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 orientations
		FVector const OldDir = (JointPos - RootPos).GetSafeNormal();
		FVector const NewDir = (OutJointPos - RootPos).GetSafeNormal();
		// Find Delta Rotation take takes us from Old to New dir
		FQuat 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 orientations
		FVector const OldDir = (EndPos - JointPos).GetSafeNormal();
		FVector const NewDir = (OutEndPos - OutJointPos).GetSafeNormal();

		// Find Delta Rotation take takes us from Old to New dir
		FQuat const DeltaRotation = FQuat::FindBetweenNormals(OldDir, NewDir);
		// Rotate our Joint quaternion by this delta rotation
		InOutJointTransform.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);
}

允许骨骼伸缩的Two Bone IK

先看一下UE里相关的参数:

  • Allow Stretching: When enabled, stretching of the set two-bone chain will be allowed. You can set the limits of the stretching in the Start Stretch Ratio and the Max Stretch Scale properties.
  • Start Stretch Ratio: When the Allow Stretching property is enabled, you can set the threshold to control when the two-bone chain is able to begin stretching. This value determines when to start stretching. For example, 0.9 means once it reaches 90% of the whole length of the limb, it will start to apply a stretch to the structure.
  • Max Stretch Scale:When the Allow Stretching property is enabled, you can set the limit to control the maximus scale of the stretch allowed for the structure. This value determines what is the max stretch scale. For example, 1.5 means it will stretch until 150% of the whole length of the limb.

结合代码,设实际骨骼链长度与预期IK链长度的比例为ReachRadio,我总结了以下规则:

  1. 这个过程只允许骨骼伸长,不允许骨骼缩短
  2. Max Stretch Scale参数很好理解,代表骨骼最多被伸长的比例
  3. Start Stretch Ratio参数稍微麻烦一点,代表骨骼允许被伸长的比例上限
  4. Start Stretch Ratio可以小于1,此时若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;

然后需要设计一个算法,这个算法需要满足以下条件:

  • 算法只能伸长骨骼的长度,不能缩短
  • 当ReachRatio小于Start Stretch Ratio时,骨骼长度不做任何改变
  • 当ReachRatio大于Max Stretch Ratio时,骨骼长度乘以Max Stretch Ratio
  • 当ReachRatio在二者范围之间时,根据设计的算法进行骨骼长度的伸长。算法思路是,当ReachRatio为Start Stretch Ratio时,骨骼长度不变,当ReachRadio为Max Stretch Ratio时,骨骼长度乘以Max Stretch Ratio,其他值时比例均匀增长

整体代码如下,就是这里计算ScalingFactor的算法稍微有一点绕:

if (bAllowStretching)
{
	// StartStretchRatio和MaxStretchScale都是在IK部分设置的值, 默认为1.0和2.0
	const float ScaleRange = MaxStretchScale - StartStretchRatio;
	if (ScaleRange > KINDA_SMALL_NUMBER && MaxLimbLength > KINDA_SMALL_NUMBER)
	{
		// 计算预期骨骼链长度与实际骨骼链长度的比例
		const float ReachRatio = DesiredLength / MaxLimbLength;
		// 下面这个算法有点绕, 总之是让(1 + ScalingFactor)成为BoneChain的长度变化系数, 同时防止骨骼长度变小的情况
		// FMath::Clamp<float>((ReachRatio - StartStretchRatio) / ScaleRange, 0.f, 1.f)是算出等比例变化的量
		// 当ReachRatio为StartStretchRatio时, 返回0, 当ReachRatio为MaxStretchScale时, 返回1
		// 再乘以(MaxStretchScale - 1)  是算出等比例伸长的增量
		const float ScalingFactor = (MaxStretchScale - 1.f) * FMath::Clamp<float>((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.0
	const float ScaleRange = MaxStretchScale - StartStretchRatio;
	if (ScaleRange > KINDA_SMALL_NUMBER && MaxLimbLength > KINDA_SMALL_NUMBER)
	{
		// 计算预期骨骼链长度与实际骨骼链长度的比例
		const float ReachRatio = DesiredLength / MaxLimbLength;

		float ScalingFactor = FMath::Clamp<float>((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;
		}
	}
}

Two Bone IK里的Twist

这一块我还不太清楚,等我研究完动画系统里的Twist机制,再补充这块知识

posted @ 2023-03-17 11:09  弹吉他的小刘鸭  阅读(894)  评论(1编辑  收藏  举报  来源