// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "TwoBoneIK.h" namespace AnimationCore { void SolveTwoBoneIK(FTransform& InOutRootTransform, FTransform& InOutJointTransform, FTransform& InOutEndTransform, const FVector& JointTarget, const FVector& Effector, bool bAllowStretching, float StartStretchRatio, float MaxStretchScale) { float LowerLimbLength = (InOutEndTransform.GetLocation() - InOutJointTransform.GetLocation()).Size(); float UpperLimbLength = (InOutJointTransform.GetLocation() - InOutRootTransform.GetLocation()).Size(); SolveTwoBoneIK(InOutRootTransform, InOutJointTransform, InOutEndTransform, JointTarget, Effector, UpperLimbLength, LowerLimbLength, bAllowStretching, StartStretchRatio, MaxStretchScale); } 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); // 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 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); } void SolveTwoBoneIK(const FVector& RootPos, const FVector& JointPos, const FVector& EndPos, const FVector& JointTarget, const FVector& Effector, FVector& OutJointPos, FVector& OutEndPos, bool bAllowStretching, float StartStretchRatio, float MaxStretchScale) { float LowerLimbLength = (EndPos - JointPos).Size(); float UpperLimbLength = (JointPos - RootPos).Size(); SolveTwoBoneIK(RootPos, JointPos, EndPos, JointTarget, Effector, OutJointPos, OutEndPos, UpperLimbLength, LowerLimbLength, bAllowStretching, StartStretchRatio, MaxStretchScale); } void SolveTwoBoneIK(const FVector& RootPos, const FVector& JointPos, const FVector& EndPos, const FVector& JointTarget, const FVector& Effector, FVector& OutJointPos, FVector& OutEndPos, float UpperLimbLength, float LowerLimbLength, bool bAllowStretching, float StartStretchRatio, float MaxStretchScale) { // This is our reach goal. FVector DesiredPos = Effector; FVector DesiredDelta = DesiredPos - RootPos; float DesiredLength = DesiredDelta.Size(); // Find lengths of upper and lower limb in the ref skeleton. // Use actual sizes instead of ref skeleton, so we take into account translation and scaling from other bone controllers. float MaxLimbLength = LowerLimbLength + UpperLimbLength; // Check to handle case where DesiredPos is the same as RootPos. FVector DesiredDir; if (DesiredLength < (float)KINDA_SMALL_NUMBER) { DesiredLength = (float)KINDA_SMALL_NUMBER; DesiredDir = FVector(1, 0, 0); } else { DesiredDir = DesiredDelta.GetSafeNormal(); } // Get joint target (used for defining plane that joint should be in). FVector JointTargetDelta = JointTarget - RootPos; const float JointTargetLengthSqr = JointTargetDelta.SizeSquared(); // Same check as above, to cover case when JointTarget position is the same as RootPos. FVector JointPlaneNormal, JointBendDir; if (JointTargetLengthSqr < FMath::Square((float)KINDA_SMALL_NUMBER)) { JointBendDir = FVector(0, 1, 0); JointPlaneNormal = FVector(0, 0, 1); } else { JointPlaneNormal = DesiredDir ^ JointTargetDelta; // If we are trying to point the limb in the same direction that we are supposed to displace the joint in, // we have to just pick 2 random vector perp to DesiredDir and each other. if (JointPlaneNormal.SizeSquared() < FMath::Square((float)KINDA_SMALL_NUMBER)) { DesiredDir.FindBestAxisVectors(JointPlaneNormal, JointBendDir); } else { JointPlaneNormal.Normalize(); // Find the final member of the reference frame by removing any component of JointTargetDelta along DesiredDir. // This should never leave a zero vector, because we've checked DesiredDir and JointTargetDelta are not parallel. JointBendDir = JointTargetDelta - ((JointTargetDelta | DesiredDir) * DesiredDir); JointBendDir.Normalize(); } } //UE_LOG(LogAnimationCore, Log, TEXT("UpperLimb : %0.2f, LowerLimb : %0.2f, MaxLimb : %0.2f"), UpperLimbLength, LowerLimbLength, MaxLimbLength); if (bAllowStretching) { const float ScaleRange = MaxStretchScale - StartStretchRatio; if (ScaleRange > KINDA_SMALL_NUMBER && MaxLimbLength > KINDA_SMALL_NUMBER) { const float ReachRatio = DesiredLength / MaxLimbLength; const float ScalingFactor = (MaxStretchScale - 1.f) * FMath::Clamp((ReachRatio - StartStretchRatio) / ScaleRange, 0.f, 1.f); if (ScalingFactor > KINDA_SMALL_NUMBER) { LowerLimbLength *= (1.f + ScalingFactor); UpperLimbLength *= (1.f + ScalingFactor); MaxLimbLength *= (1.f + ScalingFactor); } } } OutEndPos = DesiredPos; OutJointPos = JointPos; // If we are trying to reach a goal beyond the length of the limb, clamp it to something solvable and extend limb fully. if (DesiredLength >= MaxLimbLength) { OutEndPos = RootPos + (MaxLimbLength * DesiredDir); OutJointPos = RootPos + (UpperLimbLength * DesiredDir); } else { // So we have a triangle we know the side lengths of. We can work out the angle between DesiredDir and the direction of the upper limb // using the sin rule: const float TwoAB = 2.f * UpperLimbLength * DesiredLength; const float CosAngle = (TwoAB != 0.f) ? ((UpperLimbLength*UpperLimbLength) + (DesiredLength*DesiredLength) - (LowerLimbLength*LowerLimbLength)) / TwoAB : 0.f; // If CosAngle is less than 0, the upper arm actually points the opposite way to DesiredDir, so we handle that. const bool bReverseUpperBone = (CosAngle < 0.f); // Angle between upper limb and DesiredDir // ACos clamps internally so we dont need to worry about out-of-range values here. const float Angle = FMath::Acos(CosAngle); // Now we calculate the distance of the joint from the root -> effector line. // This forms a right-angle triangle, with the upper limb as the hypotenuse. const float JointLineDist = UpperLimbLength * FMath::Sin(Angle); // And the final side of that triangle - distance along DesiredDir of perpendicular. // ProjJointDistSqr can't be neg, because JointLineDist must be <= UpperLimbLength because appSin(Angle) is <= 1. const float ProjJointDistSqr = (UpperLimbLength*UpperLimbLength) - (JointLineDist*JointLineDist); // although this shouldn't be ever negative, sometimes Xbox release produces -0.f, causing ProjJointDist to be NaN // so now I branch it. float ProjJointDist = (ProjJointDistSqr > 0.f) ? FMath::Sqrt(ProjJointDistSqr) : 0.f; if (bReverseUpperBone) { ProjJointDist *= -1.f; } // So now we can work out where to put the joint! OutJointPos = RootPos + (ProjJointDist * DesiredDir) + (JointLineDist * JointBendDir); } } }