Procedural Animation Demo
Procedural animation written in C# using IK
A small scene created in Unity (URP 2022) and Blender.
IKMovement.cs
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class IKMovement : MonoBehaviour
{
//Chain length of bones
public int ChainLength = 2;
//Target the chain should bent to
public Transform Target;
public Transform Pole;
//Solver iterations per update
[Header("Solver Parameters")]
public int Iterations = 10;
//Distance when the solver stops
public float Delta = 0.001f;
//Strength of going back to the start position.
[Range(0, 1)]
public float SnapBackStrength = 1f;
protected float[] BonesLength; //Target to Origin
protected float CompleteLength;
protected Transform[] Bones;
protected Vector3[] Positions;
protected Vector3[] StartDirectionSucc;
protected Quaternion[] StartRotationBone;
protected Quaternion StartRotationTarget;
protected Transform Root;
// Start is called before the first frame update
void Awake()
{
Init();
}
void Init()
{
//initial array
Bones = new Transform[ChainLength + 1];
Positions = new Vector3[ChainLength + 1];
BonesLength = new float[ChainLength];
StartDirectionSucc = new Vector3[ChainLength + 1];
StartRotationBone = new Quaternion[ChainLength + 1];
//find root
Root = transform;
for (var i = 0; i <= ChainLength; i++)
{
if (Root == null)
throw new UnityException("The chain value is longer than the ancestor chain!");
Root = Root.parent;
}
//init target
if (Target == null)
{
Target = new GameObject(gameObject.name + " Target").transform;
SetPositionRootSpace(Target, GetPositionRootSpace(transform));
}
StartRotationTarget = GetRotationRootSpace(Target);
//init data
var current = transform;
CompleteLength = 0;
for (var i = Bones.Length - 1; i >= 0; i--)
{
Bones[i] = current;
StartRotationBone[i] = GetRotationRootSpace(current);
if (i == Bones.Length - 1)
{
//leaf
StartDirectionSucc[i] = GetPositionRootSpace(Target) - GetPositionRootSpace(current);
}
else
{
//mid bone
StartDirectionSucc[i] = GetPositionRootSpace(Bones[i + 1]) - GetPositionRootSpace(current);
BonesLength[i] = StartDirectionSucc[i].magnitude;
CompleteLength += BonesLength[i];
}
current = current.parent;
}
}
// Update is called once per frame
void LateUpdate()
{
ResolveIK();
}
private void ResolveIK()
{
if (Target == null)
return;
if (BonesLength.Length != ChainLength)
Init();
//Fabric
// root
// (bone0) (bonelen 0) (bone1) (bonelen 1) (bone2)...
// x--------------------x--------------------x---...
//get position
for (int i = 0; i < Bones.Length; i++)
Positions[i] = GetPositionRootSpace(Bones[i]);
var targetPosition = GetPositionRootSpace(Target);
var targetRotation = GetRotationRootSpace(Target);
//1st is possible to reach?
if ((targetPosition - GetPositionRootSpace(Bones[0])).sqrMagnitude >= CompleteLength * CompleteLength)
{
//just strech it
var direction = (targetPosition - Positions[0]).normalized;
//set everything after root
for (int i = 1; i < Positions.Length; i++)
Positions[i] = Positions[i - 1] + direction * BonesLength[i - 1];
}
else
{
for (int i = 0; i < Positions.Length - 1; i++)
Positions[i + 1] = Vector3.Lerp(Positions[i + 1], Positions[i] + StartDirectionSucc[i], SnapBackStrength);
for (int iteration = 0; iteration < Iterations; iteration++)
{
//https://www.youtube.com/watch?v=UNoX65PRehA
//back
for (int i = Positions.Length - 1; i > 0; i--)
{
if (i == Positions.Length - 1)
Positions[i] = targetPosition; //set it to target
else
Positions[i] = Positions[i + 1] + (Positions[i] - Positions[i + 1]).normalized * BonesLength[i]; //set in line on distance
}
//forward
for (int i = 1; i < Positions.Length; i++)
Positions[i] = Positions[i - 1] + (Positions[i] - Positions[i - 1]).normalized * BonesLength[i - 1];
//close enough?
if ((Positions[Positions.Length - 1] - targetPosition).sqrMagnitude < Delta * Delta)
break;
}
}
//move towards pole
if (Pole != null)
{
var polePosition = GetPositionRootSpace(Pole);
for (int i = 1; i < Positions.Length - 1; i++)
{
var plane = new Plane(Positions[i + 1] - Positions[i - 1], Positions[i - 1]);
var projectedPole = plane.ClosestPointOnPlane(polePosition);
var projectedBone = plane.ClosestPointOnPlane(Positions[i]);
var angle = Vector3.SignedAngle(projectedBone - Positions[i - 1], projectedPole - Positions[i - 1], plane.normal);
Positions[i] = Quaternion.AngleAxis(angle, plane.normal) * (Positions[i] - Positions[i - 1]) + Positions[i - 1];
}
}
//set position & rotation
for (int i = 0; i < Positions.Length; i++)
{
if (i == Positions.Length - 1)
SetRotationRootSpace(Bones[i], Quaternion.Inverse(targetRotation) * StartRotationTarget * Quaternion.Inverse(StartRotationBone[i]));
else
SetRotationRootSpace(Bones[i], Quaternion.FromToRotation(StartDirectionSucc[i], Positions[i + 1] - Positions[i]) * Quaternion.Inverse(StartRotationBone[i]));
SetPositionRootSpace(Bones[i], Positions[i]);
}
}
private Vector3 GetPositionRootSpace(Transform current)
{
if (Root == null)
return current.position;
else
return Quaternion.Inverse(Root.rotation) * (current.position - Root.position);
}
private void SetPositionRootSpace(Transform current, Vector3 position)
{
if (Root == null)
current.position = position;
else
current.position = Root.rotation * position + Root.position;
}
private Quaternion GetRotationRootSpace(Transform current)
{
//inverse(after) * before => rot: before -> after
if (Root == null)
return current.rotation;
else
return Quaternion.Inverse(current.rotation) * Root.rotation;
}
private void SetRotationRootSpace(Transform current, Quaternion rotation)
{
if (Root == null)
current.rotation = rotation;
else
current.rotation = Root.rotation * rotation;
}
void OnDrawGizmos()
{
#if UNITY_EDITOR
var current = this.transform;
for (int i = 0; i < ChainLength && current != null && current.parent != null; i++)
{
var scale = Vector3.Distance(current.position, current.parent.position) * 0.1f;
Handles.matrix = Matrix4x4.TRS(current.position, Quaternion.FromToRotation(Vector3.up, current.parent.position - current.position), new Vector3(scale, Vector3.Distance(current.parent.position, current.position), scale));
Handles.color = Color.green;
Handles.DrawWireCube(Vector3.up * 0.5f, Vector3.one);
current = current.parent;
}
#endif
}
}
LegMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LegMovement : MonoBehaviour
{
//raycast point above legs that shoots ray at ground/wall
[SerializeField]
private Transform raycastPoint;
[SerializeField]
private Transform target;
[SerializeField]
private Transform stepPoint; //used if wall colliding with raycast
[SerializeField]
private GameObject player;
[SerializeField]
private LayerMask mask;
[SerializeField]
private float offset;
[SerializeField]
private float moveDist;
[SerializeField]
private float speed = 10f;
[SerializeField]
private LegMovement otherLeg;
[SerializeField]
private int moveVal;
private Vector3 restPos;
public Vector3 newPos;
public Vector3 stepNormal;
public bool Grounded;
private static int currentMoveVal = 1; //1 = FL & BR, 2 = FR & BL
private bool hasMoved; //check if leg has moved
private bool moving;
private bool movingDown;
private float moveDir
{
get
{
return SpiderController.instance.move_y;
}
}
private bool moveBackwards = false;
public GameObject movingObj;
// Start is called before the first frame update
void Start()
{
restPos = target.position;
//stepPoint.position = new Vector3(restPos.x + offset, restPos.y, restPos.z);
}
// Update is called once per frame
void Update()
{
/*
if (moveDir > 0 && !moveBackwards)
{
moveBackwards = true;
offset = -0.6f;
stepPoint.localPosition = new Vector3(stepPoint.localPosition.x + offset, stepPoint.localPosition.y, stepPoint.localPosition.z);
}
else if (moveDir < 0 && moveBackwards)
{
moveBackwards = false;
offset = 0.6f;
stepPoint.localPosition = new Vector3(stepPoint.localPosition.x + offset, stepPoint.localPosition.y, stepPoint.localPosition.z);
}
*/
newPos = calcPoint(stepPoint.position);
if (Vector3.Distance(restPos, newPos) > moveDist || moving && Grounded) //if distance between current leg pos is too far OR leg is ready to move and is on ground, move leg
{
Step(newPos);
}
updateIK();
}
public Vector3 calcPoint(Vector3 pos) //calc where leg is going to move to
{
Vector3 dir = pos - raycastPoint.position;
RaycastHit hit;
if (Physics.SphereCast(raycastPoint.position, .5f, dir, out hit, 5f, mask)) //Physics.SphereCast is like raycast but with sphere (anything that collides w/ Sphere) SphereCast(Vector3 origin, float radius, Vector3 dir, out RaycastHit hit, float maxDistance, LayerMask mask);
{
stepNormal = hit.normal;
pos = hit.point; //point raycast hits (floor or wall)
Grounded = true;
if (hit.transform.gameObject.CompareTag("Moving"))
{
movingObj = hit.transform.gameObject;
}
else
{
movingObj = null;
}
}
else
{
stepNormal = Vector3.zero;
pos = restPos;
Grounded = false;
}
return pos;
}
public void Step(Vector3 pos)
{
if (currentMoveVal == moveVal)
{
Grounded = false;
hasMoved = false;
moving = true;
target.position = Vector3.MoveTowards(target.position, pos + Vector3.up, speed * Time.deltaTime);
restPos = Vector3.MoveTowards(target.position, pos + Vector3.up, speed * Time.deltaTime); //MIGHT BE BUG, SHOULD JUST SET TO target.position AFTER
if (target.position == pos + Vector3.up) //if leg has reached point to start moving down
{
movingDown = true;
}
if (movingDown)
{
target.position = Vector3.MoveTowards(target.position, pos, speed * Time.deltaTime);
restPos = Vector3.MoveTowards(target.position, pos, speed * Time.deltaTime);
}
if (target.position == pos)
{
Grounded = true;
hasMoved = true;
moving = false;
movingDown = false;
if (currentMoveVal == moveVal && otherLeg.hasMoved == true)
{
currentMoveVal = currentMoveVal * -1 + 3; //if leg == 1, set val to 2
}
}
}
}
public void updateIK()
{
target.position = restPos;
}
}
SpiderController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpiderController : MonoBehaviour
{
private Rigidbody rb;
[SerializeField]
private float speed;
[SerializeField]
private float runSpeed;
[SerializeField]
private float angSpeed;
[SerializeField]
private float run_angSpeed;
[SerializeField]
private int[] refLeg;
[SerializeField]
private int[] refLegOpp;
[SerializeField]
private LegMovement[] legs;
[SerializeField]
private AnimationCurve sensCurve;
[SerializeField]
private float targetSurfDist = -1f;
private bool Grounded;
private float initSpeed;
private float initangSpeed;
private float move_x;
[HideInInspector]
public float move_y;
// Start is called before the first frame update
void Start()
{
rb = gameObject.GetComponent<Rigidbody>();
initSpeed = speed;
initangSpeed = angSpeed;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
Application.targetFrameRate = 60;
QualitySettings.vSyncCount = 1;
}
//Make accessible from other Scripts
public static SpiderController instance;
private void Awake()
{
instance = this;
}
// Update is called once per frame
void Update()
{
move_x = Input.GetAxis("Horizontal");
move_y = Input.GetAxis("Vertical");
if (move_y != 0)
{
Vector3 movement = transform.right * move_y * speed * Time.deltaTime;
rb.velocity = movement;
}
transform.Rotate(0, move_x * angSpeed * Time.deltaTime, 0); //rotate
calcOrient();
if (Input.GetButtonDown("Fire3"))
{
speed = runSpeed;
angSpeed = run_angSpeed;
}
if (Input.GetButtonUp("Fire3"))
{
speed = initSpeed;
angSpeed = initangSpeed;
}
}
public void calcOrient()
{
Vector3 up = Vector3.zero;
Vector3 point, a, b, c;
float avgSurfDist = 0;
Grounded = false;
for (var i = 0; i < legs.Length; i++) //get pos leg wants to step on
{
Debug.Log("Draw ray");
point = legs[i].newPos;
a = ((legs[refLeg[i]].newPos) - point).normalized;
b = ((legs[refLegOpp[i]].newPos) - point).normalized;
avgSurfDist += transform.InverseTransformPoint(point).y;
c = Vector3.Cross(a, b); //get distance between legs vertically and horizontally =||=
Debug.DrawRay(point, a, Color.red, 0);
Debug.DrawRay(point, b, Color.red, 0);
Debug.DrawRay(point, c, Color.red, 0);
up += c * sensCurve.Evaluate(c.magnitude) + (legs[i].stepNormal == Vector3.zero ? transform.forward : legs[i].stepNormal); //if legs[i].stepNormal == Vector3.zero; return gameObject.transform.forward, else, return legs[i].stepNormal
Grounded |= legs[i].Grounded; //if legs[i].Grounded == true, Grounded = true, else null
}
up /= legs.Length;
avgSurfDist /= legs.Length;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.ProjectOnPlane(transform.forward, up), up), 20f * Time.deltaTime);
if (Grounded)
{
transform.Translate(0, -(-avgSurfDist + targetSurfDist), 0, Space.Self);
}
/*
else
{
gameObject.transform.Translate(0, -10 * Time.deltaTime, 0, Space.Self);
}
*/
}
public void maintainVelocity()
{
transform.parent = null;
for (int i = 0; i < legs.Length; i++)
{
if (legs[i].movingObj == null)
{
continue;
}
else
{
transform.parent = legs[i].movingObj.transform;
}
}
}
}