Skip to content

Instantly share code, notes, and snippets.

@Farfarer
Created October 15, 2012 09:22
Show Gist options
  • Save Farfarer/3891630 to your computer and use it in GitHub Desktop.
Save Farfarer/3891630 to your computer and use it in GitHub Desktop.
Solve simple IK for character legs on differing ground height for Unity3D.
function LateUpdate () {
if ( collision.grounded ) {
IKOn = true;
}
else {
IKOn = false;
}
ik.solveLegIK ();
}
// Define bones.
private var rootBone : Transform;
private var pelvis : Transform;
private var leftThigh : Transform;
private var leftCalf : Transform;
private var leftFoot : Transform;
private var rightThigh : Transform;
private var rightCalf : Transform;
private var rightFoot : Transform;
private var leftThighLength : float;
private var rightThighLength : float;
private var leftCalfLength : float;
private var rightCalfLength : float;
private var leftLegLength : float;
private var rightLegLength : float;
// Define offsets.
private var animationOffset : float = 0.01; // Difference between feet height in the animation that determines a dominant foot (i.e. foot that's taking the load whilst walking).
private var ankleHeightL : float = 0.0;
private var ankleHeightR : float = 0.0;
private var rootOffset : float = 0.0;
private var targetHeight : float = 0.0;
private var prevTargetHeight : float = 0.0;
// Foot states.
private var leftDown : boolean = false;
private var rightDown : boolean = false;
private var leftCollide : boolean = false;
private var rightCollide : boolean = false;
// Components.
private var characterMotor : characterMotor;
function Awake () {
rootBone = transform.Find ( "Bip001" );
pelvis = rootBone.Find ( "Bip001 Pelvis" );
leftThigh = pelvis.Find ( "Bip001 L Thigh" );
leftCalf = leftThigh.Find ( "Bip001 L Calf" );
leftFoot = leftCalf.Find ( "Bip001 L Foot" );
rightThigh = pelvis.Find ( "Bip001 R Thigh" );
rightCalf = rightThigh.Find ( "Bip001 R Calf" );
rightFoot = rightCalf.Find ( "Bip001 R Foot" );
leftThighLength = Vector3.Distance ( leftThigh.position, leftCalf.position );
leftCalfLength = Vector3.Distance ( leftCalf.position, leftFoot.position );
leftLegLength = leftThighLength + leftCalfLength;
rightThighLength = Vector3.Distance ( rightThigh.position, rightCalf.position );
rightCalfLength = Vector3.Distance ( rightCalf.position, rightFoot.position );
rightLegLength = rightThighLength + rightCalfLength;
prevTargetHeight = rootBone.position.y;
characterMotor = transform.GetComponent ( "characterMotor" ) as characterMotor;
}
public function solveLegIK () {
if ( characterMotor.IKOn ) {
rootOffset = rootBone.position.y - transform.position.y;
ankleHeightL = leftFoot.position.y - transform.position.y;
ankleHeightR = rightFoot.position.y - transform.position.y;
// Default feet targets.
var leftGround = leftFoot.position;
var rightGround = rightFoot.position;
var originalLeftGround = leftFoot.position;
var originalRightGround = rightFoot.position;
//Debug.DrawLine(leftGround, rightGround, Color.white);
// Are we walking in the animation?
// i.e. Is there a height difference between the feet in the animation. If so, which foot - if either - should be planted on the ground?
var animationDelta = leftGround.y - rightGround.y;
if ( animationDelta < -animationOffset ) {
leftDown = true;
rightDown = false;
} else if ( animationDelta > animationOffset ) {
leftDown = false;
rightDown = true;
} else {
leftDown = true;
rightDown = true;
}
// Raycast check to see if we're through collision and set the target points accordingly.
// If we're not, then we keep the positions as defined in the animation.
var hit : RaycastHit;
var rayDistance : float = 0.0;
// If right is dominant foot, check to see if the left foot is colliding as well.
if ( ( !leftDown ) && ( rightDown ) ) {
rayDistance = leftCalfLength + ankleHeightL;
} else {
rayDistance = leftCalfLength + leftLegLength;
}
if ( rayDistance > 0 ) {
//Debug.DrawRay(leftFoot.position + Vector3(0,leftCalfLength,0), -Vector3.up * rayDistance, Color.magenta);
if ( Physics.Raycast ( leftFoot.position + Vector3 ( 0, leftCalfLength, 0 ), -Vector3.up, hit, rayDistance ) ) {
if ( hit.point.y + ankleHeightL < leftFoot.position.y ) {
//Debug.DrawRay(leftFoot.position + Vector3(0,leftCalfLength,0), -Vector3.up * hit.distance, Color.blue);
leftGround = hit.point + Vector3 ( 0, ankleHeightL, 0 );
}
}
}
// If left is dominant foot, check to see if the right foot is colliding as well.
if ( ( leftDown ) && ( !rightDown ) ) {
rayDistance = rightCalfLength + ankleHeightR;
} else {
rayDistance = rightCalfLength + rightLegLength;
}
if ( rayDistance > 0 ) {
//Debug.DrawRay(rightFoot.position + Vector3(0,leftCalfLength,0), -Vector3.up * rayDistance, Color.magenta);
if ( Physics.Raycast ( rightFoot.position + Vector3 ( 0, rightCalfLength, 0 ), -Vector3.up, hit, rayDistance ) ) {
if ( hit.point.y + ankleHeightR < rightFoot.position.y ) {
//Debug.DrawRay(rightFoot.position + Vector3(0,rightCalfLength,0), -Vector3.up * hit.distance, Color.blue);
rightGround = hit.point + Vector3( 0, ankleHeightR, 0 );
}
}
}
// Work out which legs are grounded, or if the distance to floor is such that both should be.
// Run IK on those legs.
if ( ( leftDown ) && ( !rightDown ) ) { // Left foot is down.
targetHeight = leftGround.y - ankleHeightL + rootOffset;
}
else if ( ( !leftDown ) && ( rightDown ) ) { // Right foot is down.
targetHeight = rightGround.y - ankleHeightR + rootOffset;
}
else {
// Both feet are more or less equal in the animation - both should be considered as being on the ground.
// So move down to lowest foot and do the IK from there.
if ( leftGround.y < rightGround.y ) {
targetHeight = leftGround.y - ankleHeightL + rootOffset;
}
else {
targetHeight = rightGround.y - ankleHeightR + rootOffset;
}
}
// Move the root bone - if we move the transform, it'll fall through the world.
// Quickly lerp it's movement for a smoother transition - looks very snappy otherwise.
// Currently 1/8 second.
targetHeight = Mathf.Lerp ( prevTargetHeight, targetHeight, Time.deltaTime * 8 );
var heightDiff = targetHeight - rootBone.position.y;
rootBone.position.y = targetHeight;
prevTargetHeight = rootBone.position.y;
// Fix position for unmoved foot, as we've moved the root bone down.
// Otherwise the foot tries to go to it's original point in worldspace, not it's new one.
if ( ( !leftDown ) && ( rightDown ) && ( leftGround == originalLeftGround ) ) {
leftGround.y += heightDiff;
}
if ( ( leftDown ) && ( !rightDown ) && ( rightGround == originalRightGround ) ) {
rightGround.y += heightDiff;
}
// Solve IK.
// Expects 3DS Max FBX bones that are rotated 270 on X.
IKSolve ( leftThigh, leftCalf, leftFoot, leftGround );
IKSolve ( rightThigh, rightCalf, rightFoot, rightGround );
}
else {
prevTargetHeight = rootBone.position.y;
}
}
function IKSolve ( start : Transform, mid : Transform, end : Transform, target : Vector3 ) {
var startEndAxis = Vector3.Normalize ( end.position - start.position );
var startEndDist = Vector3.Distance ( start.position, end.position );
var startTargetAxis = Vector3.Normalize ( target - start.position );
var startTargetDist = Vector3.Distance ( start.position, target );
var startMidDist = Vector3.Distance ( start.position, mid.position );
var midEndDist = Vector3.Distance ( mid.position, end.position );
var midAngle = Mathf.Acos ( ( startMidDist * startMidDist + midEndDist * midEndDist - startEndDist * startEndDist ) / ( 2 * startMidDist * midEndDist ) ) * Mathf.Rad2Deg;
if ( startTargetDist > ( startMidDist + midEndDist ) ) {
// If the target's too far away, aim to reach as far as the leg can go.
// Minus a tiny amount to account for floating point inaccuracy. It'll start throwing NaNs if you don't have this.
startTargetDist = startMidDist + midEndDist - 0.01;
}
var newMidAngle = Mathf.Acos ( ( startMidDist * startMidDist + midEndDist * midEndDist - startTargetDist * startTargetDist ) / ( 2 * startMidDist * midEndDist ) ) * Mathf.Rad2Deg;
var rotateAngle = ( newMidAngle - midAngle ) * 0.5;
// Check it's not using rubbish data, otherwise it'll throw errors.
if ( !float.IsNaN ( rotateAngle ) ) {
start.RotateAround ( start.position, start.forward, rotateAngle );
mid.RotateAround ( mid.position, mid.forward, -1 * rotateAngle * 2 );
}
}
@StephenBrooks187
Copy link

Is there any way to see how this is supposed to be set up on a character? I cannot seem to get it working properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment