Search Unity

Showcase IK Foot Placement Setup

Discussion in 'Animation Rigging' started by toomasio, May 24, 2022.

  1. toomasio

    toomasio

    Joined:
    Nov 19, 2013
    Posts:
    199
    Had a tough time figuring out the execution order of how this stuff works. So I am dumping what I figured out here in case I need to check this again and so anyone else can also benefit from this thread.

    I believe all constraints work AFTER animations transforms systems, but BEFORE LateUpdate in Monobehaviour scripts. So in order to override IK positions, you will need a parent constraint to the bone you are referencing BEFORE you raycast based on that bone position.

    This is a setup for ONE FOOT. You can take what you learn here and either add to the script or duplicate this process for the opposite foot. In this case I am using the left foot.

    Setup (One Foot only):
    IKfootref.png
    IKfootref2.png
    IKfootraycast.png
    IKfootIK.png

    Script example:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Animations.Rigging;
    5.  
    6. public class AnimationRiggingFootPlanter : MonoBehaviour
    7. {
    8.     [SerializeField] private MultiParentConstraint footRefConstraint;
    9.     [SerializeField] private TwoBoneIKConstraint footIK;
    10.     [SerializeField] private Transform IKTarget;
    11.     [SerializeField] private float rayYOffset = 1;
    12.     [SerializeField] private float rayDistance = 0.1f;
    13.     [SerializeField] private float plantedYOffset = 0.1f;
    14.     [SerializeField] private LayerMask mask;
    15.  
    16.     private Vector3 rayOrigin;
    17.  
    18.     private void LateUpdate()
    19.     {
    20.         footIK.weight = 0;
    21.         footRefConstraint.weight = 1;
    22.         transform.position = footRefConstraint.transform.position;
    23.         rayOrigin = transform.position + Vector3.up * rayYOffset;
    24.         var footPos = footRefConstraint.transform.position;
    25.  
    26.         if (Physics.Raycast(rayOrigin, Vector3.down, out var hit, rayDistance,mask))
    27.         {
    28.             var hitPosY = hit.point.y + plantedYOffset;
    29.             if (footPos.y < hitPosY)
    30.             {
    31.                 footIK.weight = 1;
    32.                 var pos = hit.point;
    33.                 pos.y += plantedYOffset;
    34.                 IKTarget.position = pos;
    35.                 var tarRot = Quaternion.FromToRotation(Vector3.up, hit.normal) * footRefConstraint.transform.rotation;
    36.                 IKTarget.rotation = tarRot;
    37.             }
    38.         }
    39.         Debug.DrawRay(rayOrigin, Vector3.down * rayDistance, Color.red);
    40.     }
    41. }
    Result:
    IKfoot.png

    Hope this helps some people! If there is anything I messed up on please let me know!
     
    Last edited: May 24, 2022
    BruceKristelijn likes this.
  2. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    615
    I thought this was neat, but I wanted to extend the concept for hands, as well as making a few additional improvements while I'm at it.

    Left foot.png

    IKPlanter Left Foot.png

    Relaxation of the ray values:
    ray step = Step Height (how far the foot may move up)
    ray dip = Dip Height (how far the foot may move down)
    solver direction = relative ray direction

    "The solver direction points upwards?" This is because the left foot up transform is the direction in which we want to cast: the solver direction is relative. This has the neat, totally unintended, benefit to work in different gravity scenarios as well.

    Left foot transform.png
    Up = Down

    Helpers.png

    You will also need to set the target offset and create a constraint, but don't worry: the context menu has got you covered.

    Planted.png

    The new script looks a little something like this:
    Code (CSharp):
    1. #nullable enable
    2. using UnityEngine;
    3. using UnityEngine.Animations.Rigging;
    4.  
    5. [RequireComponent(typeof(TwoBoneIKConstraint))]
    6. public class IKPlanter : MonoBehaviour
    7. {
    8.     private TwoBoneIKConstraint? _twoBoneIKConstraint;
    9.     public TwoBoneIKConstraint twoBoneIKConstraint => _twoBoneIKConstraint != null ? _twoBoneIKConstraint : (_twoBoneIKConstraint = GetComponent<TwoBoneIKConstraint>());
    10.  
    11.     public Transform tip => twoBoneIKConstraint.data.tip;
    12.     public Transform target => twoBoneIKConstraint.data.target;
    13.  
    14.     [field: SerializeField] public Transform constraint { get; set; }
    15.     [field: SerializeField] public Vector3 solverDirection { get; set; } = Vector3.forward;
    16.     [field: SerializeField] public float rayStep { get; set; } = 0.5f;
    17.     [field: SerializeField] public float rayDip { get; set; } = 0.1f;
    18.     [field: SerializeField] public LayerMask layerMask { get; set; } = Physics.AllLayers;
    19.     [field: SerializeField] public float targetOffset { get; set; }
    20.  
    21.     private void LateUpdate()
    22.     {
    23.         var direction = (Direction)(constraint.rotation * solverDirection);
    24.  
    25.         var rayDistance = rayStep + rayDip;
    26.         if (Physics.Raycast(constraint.position - direction * rayStep, direction * rayDistance, out var hit, rayDistance, layerMask, QueryTriggerInteraction.Ignore))
    27.         {
    28.             twoBoneIKConstraint.weight = 1f;
    29.             target.position = hit.point - direction * targetOffset;
    30.             target.rotation = Quaternion.FromToRotation(-direction, hit.normal) * constraint.rotation;
    31.         }
    32.         else
    33.         {
    34.             twoBoneIKConstraint.weight = 0f;
    35.         }
    36.     }
    37.  
    38.     [ContextMenu(nameof(UseLeftFeetBottomOffset))] public void UseLeftFeetBottomOffset() => targetOffset = GetComponentInParent<Animator>(true).leftFeetBottomHeight;
    39.     [ContextMenu(nameof(UseRightFeetBottomOffset))] public void UseRightFeetBottomOffset() => targetOffset = GetComponentInParent<Animator>(true).rightFeetBottomHeight;
    40.     [ContextMenu(nameof(NormalizeSolverDirection))] public void NormalizeSolverDirection() => solverDirection = solverDirection.normalized;
    41.     [ContextMenu(nameof(CreateMultiParentConstraint))] public void CreateMultiParentConstraint()
    42.     {
    43.         if (constraint == null)
    44.         {
    45.             constraint = new GameObject($"{tip.name} Constraint").transform;
    46.             constraint.SetParent(transform.parent);
    47.             constraint.SetSiblingIndex(transform.GetSiblingIndex());
    48.         }
    49.  
    50.         if (!constraint.TryGetComponent(out MultiParentConstraint multiParentConstraint))
    51.         {
    52.             multiParentConstraint = constraint.gameObject.AddComponent<MultiParentConstraint>();
    53.         }
    54.  
    55.         multiParentConstraint.data.constrainedObject = multiParentConstraint.transform;
    56.         multiParentConstraint.data.sourceObjects = new WeightedTransformArray()
    57.             {
    58.                 new WeightedTransform(tip, 1f)
    59.             };
    60.         multiParentConstraint.data.constrainedPositionXAxis
    61.             = multiParentConstraint.data.constrainedPositionYAxis
    62.             = multiParentConstraint.data.constrainedPositionZAxis
    63.             = multiParentConstraint.data.constrainedRotationXAxis
    64.             = multiParentConstraint.data.constrainedRotationYAxis
    65.             = multiParentConstraint.data.constrainedRotationZAxis
    66.             = true;
    67.     }
    68.  
    69.     private void OnDrawGizmosSelected()
    70.     {
    71.         var direction = (Direction)(tip.transform.rotation * solverDirection);
    72.  
    73.         Gizmos.color = Color.green;
    74.         Gizmos.DrawRay(tip.position, direction * -rayStep);
    75.         Gizmos.color = Color.red;
    76.         Gizmos.DrawRay(tip.position, direction * rayDip);
    77.         Gizmos.color = Color.blue;
    78.         Gizmos.DrawRay(tip.position, direction * targetOffset);
    79.     }
    80. }
    81.  
     
    toomasio likes this.
  3. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    615
    We can use the same script for hands, with just a teeny tiny change in our setup:

    Left Hand.png

    Left Hand Inspector.png

    Nothing special here yet, but instead of creating our constraint automatically like last time, we're gonna do it manually.

    Left Hand Constraint.png

    We're going to constrain its position to "Ball", and its rotation to how the hand would be rotated under normal circumstances. We DO apply an offset to place the hands in a more natural "holding"-position.

    Et voilà!
    Hands-.png

    The fingers obviously need some work but the hands snap to the ball in a holding manner, all dynamic, no tricks. Once I animate the fingers more realistically, I can easily fine-tune the holding position by changing e.g. the position offset on the constraint, or tweaking the IKPlanter values ever so slightly, daily and nightly (10 points if you got that reference). Or perhaps I'll give the fingers IKPlanters as well, who knows what objects our character will hold right, from boxes to balls to demon babies, heck who cares about shooting 10 extra raycasts per frame if it looks cool go wild explore ta-ta!
     
    toomasio likes this.
  4. toomasio

    toomasio

    Joined:
    Nov 19, 2013
    Posts:
    199
    If you try this lemme know how it works out. Thanks for your detailed approach!
     
  5. HitsuSan

    HitsuSan

    Joined:
    Sep 28, 2014
    Posts:
    158
    Is this really working for you? I've tried it but it seems like it's flickering a lot and i can't really figure out why.
     
    Last edited: Mar 2, 2023
  6. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    615
    We’re working on a new game where something was going wrong with the feet, so… no there’s definitely a bug in there. Unfortunately we’ve pushed it back right now since visuals have less priority over prototyping.

    However the flickering I thought was fixed. Are you setting the solver direction correctly? It’s a really annoying value actually that I’d like to change as well, but it wants a normalized direction of which angle to respect when raycasting from the feet. So when your feet’s forward is looking down, then the solver direction should be (0, 0, 1) - not great UX honestly.
     
  7. HitsuSan

    HitsuSan

    Joined:
    Sep 28, 2014
    Posts:
    158
    Sorry i didn't realize you had a totally different script going on from the original post and i'm testing it right now but it seems like the flicker still persist so i'm trying to figure out what's going on. The bone direction unfortunately changes from rig to rig system so trying to figure out the direction like that is quite normal.

    What is (Direction) in your script? Looks like a simple vector3 casting but you never know... ^^
     
    Last edited: Mar 3, 2023
  8. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    615
    Oh whoops that is some of our internal api so I can’t show you, but it should be pretty easy to make one yourself: it’s a quaternion that you can also use as a normalized Vector3 direction, which just makes it a little easier to do math-stuff.

    In this script a Vector3 should suffice though, maybe only line 30 needs a little tweaking, I don’t remember and I can’t look into the code till after the weekend unfortunately but it should work probably.

    Imma be real: not my finest professional hour this :oops:
     
  9. HitsuSan

    HitsuSan

    Joined:
    Sep 28, 2014
    Posts:
    158
    Haha, not at all, we figured that out pretty quickly and you helped a lot so thanks :)
     
  10. cmann

    cmann

    Joined:
    Aug 1, 2015
    Posts:
    31
    This doesn't actually solve the problem though...
    Sure you get the correct position to do the ray cast in LateUpdate, but the IK constraint is only updated on the next frame so you're still one frame behind.