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.
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);
}
}
}
}