using UnityEngine; using System.Collections; namespace RootMotion.FinalIK { /// /// Grounding for LimbIK, CCD and/or FABRIK solvers. /// [HelpURL("http://www.root-motion.com/finalikdox/html/page9.html")] [AddComponentMenu("Scripts/RootMotion.FinalIK/Grounder/Grounder Quadruped")] public class GrounderQuadruped: Grounder { // Open the User Manual URL [ContextMenu("User Manual")] protected override void OpenUserManual() { Application.OpenURL("http://www.root-motion.com/finalikdox/html/page9.html"); } // Open the Script Reference URL [ContextMenu("Scrpt Reference")] protected override void OpenScriptReference() { Application.OpenURL("http://www.root-motion.com/finalikdox/html/class_root_motion_1_1_final_i_k_1_1_grounder_quadruped.html"); } #region Main Interface /// /// The %Grounding solver for the forelegs. /// [Tooltip("The Grounding solver for the forelegs.")] public Grounding forelegSolver = new Grounding(); /// /// The weight of rotating the character root to the ground angle (range: 0 - 1). /// [Tooltip("The weight of rotating the character root to the ground angle (range: 0 - 1).")] [Range(0f, 1f)] public float rootRotationWeight = 0.5f; /// /// The maximum angle of rotating the quadruped downwards (going downhill, range: -90 - 0). /// [Tooltip("The maximum angle of rotating the quadruped downwards (going downhill, range: -90 - 0).")] [Range(-90f, 0f)] public float minRootRotation = -25f; /// /// The maximum angle of rotating the quadruped upwards (going uphill, range: 0 - 90). /// [Tooltip("The maximum angle of rotating the quadruped upwards (going uphill, range: 0 - 90).")] [Range(0f, 90f)] public float maxRootRotation = 45f; /// /// The speed of interpolating the character root rotation (range: 0 - inf). /// [Tooltip("The speed of interpolating the character root rotation (range: 0 - inf).")] public float rootRotationSpeed = 5f; /// /// The maximum IK offset for the legs (range: 0 - inf). /// [Tooltip("The maximum IK offset for the legs (range: 0 - inf).")] public float maxLegOffset = 0.5f; /// /// The maximum IK offset for the forelegs (range: 0 - inf). /// [Tooltip("The maximum IK offset for the forelegs (range: 0 - inf).")] public float maxForeLegOffset = 0.5f; /// /// The weight of maintaining the head's rotation as it was before solving the Grounding (range: 0 - 1). /// [Tooltip("The weight of maintaining the head's rotation as it was before solving the Grounding (range: 0 - 1).")] [Range(0f, 1f)] public float maintainHeadRotationWeight = 0.5f; /// /// The root Transform of the character, with the rigidbody and the collider. /// [Tooltip("The root Transform of the character, with the rigidbody and the collider.")] public Transform characterRoot; /// /// The pelvis transform. Common ancestor of both legs and the spine. /// [Tooltip("The pelvis transform. Common ancestor of both legs and the spine.")] public Transform pelvis; /// /// The last bone in the spine that is the common parent for both forelegs. /// [Tooltip("The last bone in the spine that is the common parent for both forelegs.")] public Transform lastSpineBone; /// /// The head (optional, if you intend to maintain its rotation). /// [Tooltip("The head (optional, if you intend to maintain its rotation).")] public Transform head; /// /// %IK componets of the hindlegs. Can be any type of IK components. /// public IK[] legs; /// /// %IK components for the forelegs. Can be any type of IK components. /// public IK[] forelegs; /// /// When using GrounderQuadruped on a spherical object, update this vector to always point towards the center of that object. /// [HideInInspector] public Vector3 gravity = Vector3.down; #endregion Main Interface public override void ResetPosition() { for (int i = 0; i < legs.Length; i++) { legs[i].GetIKSolver().IKPosition = feet[i].transform.position; if (legs[i] is LimbIK) { var leg = legs[i] as LimbIK; leg.solver.IKRotation = solver.legs[i].transform.rotation; } } solver.Reset(); forelegSolver.Reset(); } // Contains all the required information about a foot public struct Foot { public IKSolver solver; public Transform transform; public Quaternion rotation; public Grounding.Leg leg; // The custom constructor public Foot (IKSolver solver, Transform transform) { this.solver = solver; this.transform = transform; this.leg = null; rotation = transform.rotation; } } private Foot[] feet = new Foot[0]; private Vector3 animatedPelvisLocalPosition; private Quaternion animatedPelvisLocalRotation; private Quaternion animatedHeadLocalRotation; private Vector3 solvedPelvisLocalPosition; private Quaternion solvedPelvisLocalRotation; private Quaternion solvedHeadLocalRotation; private int solvedFeet; private bool solved; private float angle; private Transform forefeetRoot; private Quaternion headRotation; private float lastWeight; private Rigidbody characterRootRigidbody; // Can we initiate the Grounding? private bool IsReadyToInitiate() { if (pelvis == null) return false; if (lastSpineBone == null) return false; if (legs.Length == 0) return false; if (forelegs.Length == 0) return false; if (characterRoot == null) return false; if (!IsReadyToInitiateLegs(legs)) return false; if (!IsReadyToInitiateLegs(forelegs)) return false; return true; } // Are the leg IK components valid for initiation? private bool IsReadyToInitiateLegs(IK[] ikComponents) { foreach (IK leg in ikComponents) { if (leg == null) return false; if (leg is FullBodyBipedIK) { LogWarning("GrounderIK does not support FullBodyBipedIK, use CCDIK, FABRIK, LimbIK or TrigonometricIK instead. If you want to use FullBodyBipedIK, use the GrounderFBBIK component."); return false; } if (leg is FABRIKRoot) { LogWarning("GrounderIK does not support FABRIKRoot, use CCDIK, FABRIK, LimbIK or TrigonometricIK instead."); return false; } if (leg is AimIK) { LogWarning("GrounderIK does not support AimIK, use CCDIK, FABRIK, LimbIK or TrigonometricIK instead."); return false; } } return true; } // Weigh out the IK solvers properly when the component is disabled void OnDisable() { if (!initiated) return; for (int i = 0; i < feet.Length; i++) { if (feet[i].solver != null) feet[i].solver.IKPositionWeight = 0f; } } // Initiate once we have all the required components void Update() { weight = Mathf.Clamp(weight, 0f, 1f); if (weight <= 0f) return; solved = false; if (initiated) return; if (!IsReadyToInitiate()) return; Initiate(); } // Initiate this Grounder private void Initiate() { // Building the feet feet = new Foot[legs.Length + forelegs.Length]; // Gathering the last bones of the IK solvers as feet Transform[] footBones = InitiateFeet(legs, ref feet, 0); Transform[] forefootBones = InitiateFeet(forelegs, ref feet, legs.Length); // Store the default localPosition and localRotation of the pelvis animatedPelvisLocalPosition = pelvis.localPosition; animatedPelvisLocalRotation = pelvis.localRotation; if (head != null) animatedHeadLocalRotation = head.localRotation; forefeetRoot = new GameObject().transform; forefeetRoot.parent = transform; forefeetRoot.name = "Forefeet Root"; // Initiate the Grounding solver.Initiate(transform, footBones); forelegSolver.Initiate(forefeetRoot, forefootBones); for (int i = 0; i < footBones.Length; i++) feet[i].leg = solver.legs[i]; for (int i = 0; i < forefootBones.Length; i++) feet[i + legs.Length].leg = forelegSolver.legs[i]; characterRootRigidbody = characterRoot.GetComponent(); initiated = true; } // Initiate the feet private Transform[] InitiateFeet(IK[] ikComponents, ref Foot[] f, int indexOffset) { Transform[] bones = new Transform[ikComponents.Length]; for (int i = 0; i < ikComponents.Length; i++) { IKSolver.Point[] points = ikComponents[i].GetIKSolver().GetPoints(); f[i + indexOffset] = new Foot(ikComponents[i].GetIKSolver(), points[points.Length - 1].transform); bones[i] = f[i + indexOffset].transform; // Add to the update delegates of each ik solver f[i + indexOffset].solver.OnPreUpdate += OnSolverUpdate; f[i + indexOffset].solver.OnPostUpdate += OnPostSolverUpdate; } return bones; } void LateUpdate () { if (weight <= 0f) return; // Clamping values rootRotationWeight = Mathf.Clamp(rootRotationWeight, 0f, 1f); minRootRotation = Mathf.Clamp(minRootRotation, -90f, maxRootRotation); maxRootRotation = Mathf.Clamp(maxRootRotation, minRootRotation, 90f); rootRotationSpeed = Mathf.Clamp(rootRotationSpeed, 0f, rootRotationSpeed); maxLegOffset = Mathf.Clamp(maxLegOffset, 0f, maxLegOffset); maxForeLegOffset = Mathf.Clamp(maxForeLegOffset, 0f, maxForeLegOffset); maintainHeadRotationWeight = Mathf.Clamp(maintainHeadRotationWeight, 0f, 1f); // Rotate the character root RootRotation(); } // Rotate the character along with the terrain private void RootRotation() { if (rootRotationWeight <= 0f) return; if (rootRotationSpeed <= 0f) return; solver.rotateSolver = true; forelegSolver.rotateSolver = true; // Get the horizontal rotation of the character Vector3 tangent = characterRoot.forward; Vector3 normal = -gravity; Vector3.OrthoNormalize(ref normal, ref tangent); Quaternion horizontalRotation = Quaternion.LookRotation(tangent, -gravity); // Get the direction from root hit to forelegs root hit in the space of the horizontal character rotation Vector3 hitDirection = forelegSolver.rootHit.point - solver.rootHit.point; Vector3 hitDirectionLocal = Quaternion.Inverse(horizontalRotation) * hitDirection; // Get the angle between the horizontal and hit directions float angleTarget = Mathf.Atan2(hitDirectionLocal.y, hitDirectionLocal.z) * Mathf.Rad2Deg; angleTarget = Mathf.Clamp(angleTarget * rootRotationWeight, minRootRotation, maxRootRotation); // Interpolate the angle angle = Mathf.Lerp(angle, angleTarget, Time.deltaTime * rootRotationSpeed); if (characterRootRigidbody == null) { characterRoot.rotation = Quaternion.Slerp(characterRoot.rotation, Quaternion.AngleAxis(-angle, characterRoot.right) * horizontalRotation, weight); } else { characterRootRigidbody.MoveRotation(Quaternion.Slerp(characterRoot.rotation, Quaternion.AngleAxis(-angle, characterRoot.right) * horizontalRotation, weight)); } } // Called before updating the first IK solver private void OnSolverUpdate() { if (!enabled) return; if (weight <= 0f) { if (lastWeight <= 0f) return; // Weigh out the limb solvers properly OnDisable(); } lastWeight = weight; // If another IK has already solved in this frame, do nothing if (solved) return; if (OnPreGrounder != null) OnPreGrounder(); // If the bone transforms have not changed since last solved state, consider them unanimated if (pelvis.localPosition != solvedPelvisLocalPosition) animatedPelvisLocalPosition = pelvis.localPosition; else pelvis.localPosition = animatedPelvisLocalPosition; if (pelvis.localRotation != solvedPelvisLocalRotation) animatedPelvisLocalRotation = pelvis.localRotation; else pelvis.localRotation = animatedPelvisLocalRotation; if (head != null) { if (head.localRotation != solvedHeadLocalRotation) animatedHeadLocalRotation = head.localRotation; else head.localRotation = animatedHeadLocalRotation; } for (int i = 0; i < feet.Length; i++) feet[i].rotation = feet[i].transform.rotation; // Store the head rotation so it could be maintained later if (head != null) headRotation = head.rotation; // Position the forefeet root to the center of forefeet UpdateForefeetRoot(); // Update the Grounding solver.Update(); forelegSolver.Update(); // Move the pelvis pelvis.position += solver.pelvis.IKOffset * weight; // Rotate the pelvis Vector3 spineDirection = lastSpineBone.position - pelvis.position; Vector3 newSpinePosition = lastSpineBone.position + forelegSolver.root.up * Mathf.Clamp(forelegSolver.pelvis.heightOffset, Mathf.NegativeInfinity, 0f) - solver.root.up * solver.pelvis.heightOffset; Vector3 newDirection = newSpinePosition - pelvis.position; Quaternion f = Quaternion.FromToRotation(spineDirection, newDirection); pelvis.rotation = Quaternion.Slerp(Quaternion.identity, f, weight) * pelvis.rotation; // Update the IKPositions and IKPositonWeights of the legs for (int i = 0; i < feet.Length; i++) SetFootIK(feet[i], (i < 2? maxLegOffset: maxForeLegOffset)); solved = true; solvedFeet = 0; if (OnPostGrounder != null) OnPostGrounder(); } // Position the forefeet root to the center of forefeet private void UpdateForefeetRoot() { // Get the centroid Vector3 foreFeetCenter = Vector3.zero; for (int i = 0; i < forelegSolver.legs.Length; i++) { foreFeetCenter += forelegSolver.legs[i].transform.position; } foreFeetCenter /= (float)forelegs.Length; Vector3 dir = foreFeetCenter - transform.position; // Ortho-normalize to this Transform's rotation Vector3 normal = transform.up; Vector3 tangent = dir; Vector3.OrthoNormalize(ref normal, ref tangent); // Positioning the forefeet root forefeetRoot.position = transform.position + tangent.normalized * dir.magnitude; } // Set the IK position and weight for a limb private void SetFootIK(Foot foot, float maxOffset) { Vector3 direction = foot.leg.IKPosition - foot.transform.position; foot.solver.IKPosition = foot.transform.position + Vector3.ClampMagnitude(direction, maxOffset); foot.solver.IKPositionWeight = weight; } // Rotating the feet after IK has finished private void OnPostSolverUpdate() { if (weight <= 0f) return; if (!enabled) return; // Only do this after the last IK solver has finished solvedFeet ++; if (solvedFeet < feet.Length) return; for (int i = 0; i < feet.Length; i++) { feet[i].transform.rotation = Quaternion.Slerp(Quaternion.identity, feet[i].leg.rotationOffset, weight) * feet[i].rotation; } if (head != null) head.rotation = Quaternion.Lerp(head.rotation, headRotation, maintainHeadRotationWeight * weight); // Store the solved transform's of the bones so we know if they are not animated solvedPelvisLocalPosition = pelvis.localPosition; solvedPelvisLocalRotation = pelvis.localRotation; if (head != null) solvedHeadLocalRotation = head.localRotation; if (OnPostIK != null) OnPostIK(); } // Cleaning up the delegates void OnDestroy() { if (initiated) { DestroyLegs(legs); DestroyLegs(forelegs); } } // Cleaning up the delegates private void DestroyLegs(IK[] ikComponents) { foreach (IK leg in ikComponents) { if (leg != null) { leg.GetIKSolver().OnPreUpdate -= OnSolverUpdate; leg.GetIKSolver().OnPostUpdate -= OnPostSolverUpdate; } } } } }