Chain IK Alternative [CCDIK]

I’m trying to create a rig for a bow model and am feeling limited by the IK constraints built into the Animation Rigging package. I’ll quickly breakdown the rig and its goal. If someone could give me some pointers that would be appreciated!

I have a joint chain along the top/bottom halves of the bow like so:

I would like to animate the joint chains when the bow is drawn back.
An empty object (with diamond gizmo) is used to author the “draw position” like so:
Unity_NaPlBesVz5

To bend the bow back I create two Chain IK rigs for the top/bottom joint chains.
The IK targets (drawn via the red circle gizmos) are themselves animated using a custom distance constraint so they are pulled back with the draw position.
Unity_TYbun8CcQ2

Unfortunately it does not look good/natural :stuck_out_tongue:

I’m curious how I could go about making a better rig for this. Chain IK has proved to be very difficult to wrangle for…well…everything I’ve ever tried to use it for.

This needs to be realtime/rigged in Unity, so I don’t want to use blend shapes, or rig it in Blender.

Before using Chain IK I tried to make my own constraint that rotates each joint in the chain based on the draw distance, but it wasn’t actually IK so while it looked a bit more natural it never bent the correct amount to maintain the distance between the bow tip(s) and draw position.

Correct me if I’m wrong, but I think IK is necessary for bow-bending that maintains the bowstring’s length, which leaves two options:

  1. Two Bone IK
  2. Some Mystery IK

Two bone IK is a lot easier to wrangle, so if I can somehow smoothly fit the joint chain to a two bone ik “proxy chain” that could work. I’m sure there are ways to do this, but I am not familiar with them.
Alternatively, if there’s another IK algorithm/technique that fits the bill I am not against writing a custom constraint - I just need to know about it first.

At the end of the day, I need to rotate a chain of bones along a single axis to reach a target point at runtime, so I’m down to try anything that makes that possible.

If someone has a solution, great! but even some general info/keywords I can use to keep figuring out on my own would be super helpful. Thanks for reading :slight_smile:

I think there are two reasons why the animation appears unrealistic:

(1) Reduce the Tip Rotation Weight slightly to prevent the tip bone’s global rotation from remaining constant;

(2) The weights of all bones during the chain IK are consistent, causing the slightest change to affect the entire bow. CCDIK in FinalIK allows setting individual weights for each bone, which can make the bones farther from the center have lower weights and those on the sides have higher weights, potentially making the animation more realistic. Unfortunately, Animation Rigging does not support this feature.

1 Like

Thank you for the response. For CCDIK, I do have Final IK, but wanted to see if I could get it working with a custom constraint as a way to learn. Turns out it’s surprisingly not that hard!

This talk was pretty helpful, though it uses a modified algorithm and I went with the unmodified CCDIK algorithm since it was better for evenly spreading the joint rotations.

Unity_611oDJVAUR

Here’s the source if anyone needs it - though I’m sure it could use some more love!

public class CCDIKConstraint : RigConstraint<CCDIKConstraint.Job, CCDIKConstraint.Data, CCDIKConstraint.Binder>
{
    public struct Job : IWeightedAnimationJob
    {
        public FloatProperty jobWeight { get; set; }
        public ReadOnlyTransformHandle target;
        public NativeArray<ReadWriteTransformHandle> chain;
        public NativeArray<Vector3> bindPositions;
        public NativeArray<Quaternion> bindRotations;
        public NativeArray<float> weights;
        public NativeArray<float> steps;
        public IntProperty maxIterations;
        public FloatProperty errorDistance;
        public FloatProperty tipRotationWeight;
        public void ProcessRootMotion(AnimationStream stream){}
        public void ProcessAnimation(AnimationStream stream)
        {
            var totalWeight = jobWeight.Get(stream);
            if (totalWeight <= 0f)
                return;
            // Reset chain transforms to the local pos/rots they had when first bound.
            for (int i = 0; i < chain.Length; i++)
            {
                var handle = chain[i];
                handle.SetLocalPosition(stream, bindPositions[i]);
                handle.SetLocalRotation(stream, bindRotations[i]);
            }
            var targetPosition = target.GetPosition(stream);
            var iterationCount = 0;
            var sqrDist = 0f;
            
            do
            {
                // CCDIK: Iterate over each joint in the chain and rotate it so that the line between it and the tip is
                // aligned with the target position.
                for (int i = 0; i < chain.Length; i++)
                {
                    var weight = weights[i];
                    var tipPosition = chain[0].GetPosition(stream);
                    var jointPosition = chain[i].GetPosition(stream);
                    var jointRotation = chain[i].GetRotation(stream);
                    var correctiveRotation = CorrectiveBoneRotation(tipPosition, jointPosition, jointRotation, targetPosition);
                    correctiveRotation = Quaternion.Slerp(Quaternion.identity, correctiveRotation, totalWeight * weight);
                    chain[i].SetRotation(stream, correctiveRotation * jointRotation);
                }
                sqrDist = (chain[0].GetPosition(stream) - targetPosition).sqrMagnitude;
                iterationCount++;
            }
            while (sqrDist > errorDistance.Get(stream) && iterationCount < maxIterations.Get(stream));
            chain[0].SetRotation(stream, Quaternion.Lerp(target.GetRotation(stream), chain[0].GetRotation(stream), totalWeight * tipRotationWeight.Get(stream)));
        }
        public Quaternion CorrectiveBoneRotation(Vector3 tipPosition, Vector3 jointPosition, Quaternion jointRotation, Vector3 targetPosition)
        {
            var jointToTip = tipPosition - jointPosition;
            var jointToTarget = targetPosition - jointPosition;
            return Quaternion.FromToRotation(jointToTip, jointToTarget);
        }
    }
    [System.Serializable]
    public struct Data : IAnimationJobData
    {
        [SyncSceneToStream]
        public Transform root;
        [SyncSceneToStream]
        public Transform tip;
        [SyncSceneToStream]
        public Transform target;
        public AnimationCurve weight;
        [Range(0f, 1f), SyncSceneToStream]
        public float tipRotationWeight;
        [SyncSceneToStream]
        public int maxIterations;
        [SyncSceneToStream]
        public float errorDistance;
        public bool IsValid() => root && tip && target && maxIterations > 0;
        public void SetDefaultValues()
        {
            weight = AnimationCurve.Linear(0f, 1f, 1f, 0f);
            tipRotationWeight = 0f;
            maxIterations = 15;
            errorDistance = 0.001f;
        }
    }
    public class Binder : AnimationJobBinder<Job, Data>
    {
        public override Job Create(Animator animator, ref Data data, Component component)
        {
            var chain = ConstraintsUtils.ExtractChain(data.root, data.tip);
            Array.Reverse(chain);
            var steps = ConstraintsUtils.ExtractSteps(chain);
            var nativeChain = new NativeArray<ReadWriteTransformHandle>(chain.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            var nativeBindPositions = new NativeArray<Vector3>(chain.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            var nativeBindRotations = new NativeArray<Quaternion>(chain.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            var nativeWeights = new NativeArray<float>(chain.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            var nativeSteps = new NativeArray<float>(chain.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            for (int i = 0; i < chain.Length; i++)
            {
                var joint = chain[i];
                var t = i / (chain.Length - 1f);
                nativeChain[i] = ReadWriteTransformHandle.Bind(animator, joint);
                nativeBindPositions[i] = joint.localPosition;
                nativeBindRotations[i] = joint.localRotation;
                nativeSteps[i] = steps[i];
                nativeWeights[i] = data.weight.Evaluate(t);
            }
            var job = new Job
            {
                chain = nativeChain,
                bindPositions = nativeBindPositions,
                bindRotations = nativeBindRotations,
                weights = nativeWeights,
                steps = nativeSteps,
                target = ReadOnlyTransformHandle.Bind(animator, data.target),
                maxIterations = IntProperty.Bind(animator, component, ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(Data.maxIterations))),
                errorDistance = FloatProperty.Bind(animator, component, ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(Data.errorDistance))),
                tipRotationWeight = FloatProperty.Bind(animator, component, ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(Data.tipRotationWeight))),
            };
            return job;
        }
        public override void Destroy(Job job)
        {
            job.chain.Dispose();
            job.bindPositions.Dispose();
            job.bindRotations.Dispose();
            job.steps.Dispose();
            job.weights.Dispose();
        }
        public override void Update(Job job, ref Data data)
        {
            for (int i = 0; i < job.chain.Length; i++)
            {
                var t = i / (job.chain.Length - 1f);
                job.weights[i] = data.weight.Evaluate(t);
            }
        }
    }
}