#region License // Easy Jiggle Bone // // Authors: Markus Ewald (Cygon), Michael Cook (Fishypants), Rodrigo Pegorari // License: Free to use. Any credit would be nice :) // Upodate: 2013-06-01 (ymd) #endregion // License //#define HAVE_DAMN_MATH_PROBLEM_SOLVED using System; using UnityEngine; namespace Framework.Support { /// Simulates a wobbly, bouncy or jiggly part of a rigid body /// /// /// This is a modified version of the jiggle bone script which can handle any /// arbitrary rotation of the bone to be jiggled. This is particularly useful if /// you use one of the modeling tools where the asset import causes the bones' /// neutral orientations to end up being weird angles. /// /// /// How to use (example of breast physics): /// /// /// Preparations: your character model should have one bone per breast and /// ideally your game would be in a stage that already lets you move around. /// In the initial pose (the one the jiggle bone script will find when /// it activates) the breast bones should be in the "rest position" to which /// they will return when the model stops moving. /// /// /// /// /// /// Select the character model in the Hierarchy window, go down the bone /// tree to locate the breast bones. Attach this jiggle bone script to each /// of them. /// /// /// /// The jiggle bone script has a property "Mass Offset" that specifies where, /// relative to the jiggle bone's base, the center of mass is located. The /// scene editor will show this point as a small green sphere with a line. /// Tweak the "Mass Offset" until the green line goes right through the center /// of mass. Actually, make it a bit longer to achieve a more stable simulation. /// /// /// /// Check the results. If during acceleration, the blue sphere gets left behind /// and twists the breasts into extreme angles, move the "Mass Offset" /// a bit further out. If it doesn't settle quickly enough, increase the /// stiffness. If it moves too slowly, decrease the mass. /// /// /// /// (Additional step required until I can solve a silly math issue). /// The bone will currently end up with a rather arbitrary roll angle. To fix /// this, click on "Play" and tweak the RestAngle into all possible directions /// until the hook on the green line points into the same direction as /// the hook on the red line. Remember the RestAngle you used, click on "Stop" /// and re-enter it (as Unity will reset any changes performed during playback). /// /// /// /// public class JiggleBone : MonoBehaviour { /// Offset the center of mass has from the bone /// /// /// If the jiggle bone is, for example, used for breast physics, the breast bones /// would typically be somewhere inside of the breasts and have arbitrary /// orientations (depending on the modeling application used) that cannot be relied /// on to estimate the bounce in response to object translation changes. /// /// /// The mass offset specifies the offset that of the center of mass has from /// the origin of the bone. /// /// public Vector3 MassOffset = Vector3.forward; /// Maximum angle the jiggle bone can bend public float AngleLimit = 45.0f; /// How heavy the jiggling mass is public float Mass = 0.90f; /// How strongly the jiggling mass follows the jiggle bone public float Stiffness = 0.15f; /// How much the mass resists movements public float Damping = 0.75f; /// Whether gravity influences the jiggle bone public bool ApplyGravity = false; /// /// If this jiggle bone might be turned 90 degrees, fixes an unexplained error /// public bool EnableCrawlingHack = false; #if !HAVE_DAMN_MATH_PROBLEM_SOLVED /// Direction at which the jiggle bone has no roll public Vector3 RestAngle = Vector3.left; #endif /// /// Stops any jiggling immediately. Use after warping to another position. /// public void Stabilize() { this.jiggleOrientation = transform.parent.rotation * this.originalOrientation; this.dynamicPosition = massRestPosition; this.dynamicVelocity = Vector3.zero; } /// Called when the script gets loaded into a game component protected virtual void Awake() { this.originalOrientation = transform.localRotation; #if HAVE_DAMN_MATH_PROBLEM_SOLVED this.up = getOrthogonalUp(Vector3.Normalize(this.MassOffset)); #endif // Because we allow for an arbitrary initial rotation of the jiggle bone, // we will need to transform all effects that are based on the center of mass // by the jiggle bone's orientation so that the rest position equals the bind // pose and impulses calculated on the center of mass can be transformed into // this coordinate frame. this.massToOriginalOrientation = Quaternion.Inverse( massRestOrientation ) * this.originalOrientation; this.originalToMassOrientation = Quaternion.Inverse( this.massToOriginalOrientation ); Stabilize(); } /// Called after Unity has finished its physics processing protected virtual void LateUpdate() { updateDynamicPosition(massRestPosition); // Orient the jiggle bone towards the dynamic point Vector3 dynamicDirection = Vector3.Normalize(this.dynamicPosition - transform.position); // HACK LookRotation uses up vector in global coordinate system // The up vector should be transformed into the local coordinate frame // so that no matter which way the model is facing, up stays up. Due to // acute brain meltdown, hardcoded standing and crawling orientations here. Vector3 bestUp; { float deltaX = massRestPosition.x - transform.position.x; float deltaY = massRestPosition.y - transform.position.y; bool isProbablyCrawling = Math.Abs(deltaY) > Math.Abs(deltaX); if(isProbablyCrawling && this.EnableCrawlingHack) { bestUp = new Vector3(-Mathf.Sign(deltaX), 0.0f, 0.0f); } else { bestUp = Vector3.up; } } this.jiggleOrientation = Quaternion.LookRotation(dynamicDirection, bestUp); this.jiggleOrientation *= this.massToOriginalOrientation; applyJiggleOrientation(); } /// Simulates the dynamic point towards which the jiggle bone is oriented /// Rest position that the dynamic point tries to reach /// /// Formula based on the original Unity jiggle bone by Michael Cook, which in turn /// uses the math from Rodrigo Pegorari's rubber simulation script. /// private void updateDynamicPosition(Vector3 target) { Vector3 force = (target - this.dynamicPosition) * this.Stiffness; if(this.ApplyGravity) { force += Physics.gravity / 1000.0f; } Vector3 acceleration = force / this.Mass; dynamicVelocity += acceleration * (1.0f - this.Damping); this.dynamicPosition += dynamicVelocity + force; } /// /// Draws some debug gizmos when the component is shown in the scene editor /// protected virtual void OnDrawGizmos() { float massOffsetLength = this.MassOffset.magnitude; // Red marker to the jiggle bone's actual orientation { Vector3 jiggleForward = this.jiggleOrientation * this.originalToMassOrientation * Vector3.forward; jiggleForward *= massOffsetLength; Gizmos.color = Color.red; Gizmos.DrawLine(transform.position, transform.position + jiggleForward * 2.0f); Gizmos.DrawSphere(transform.position + jiggleForward, massOffsetLength / 25.0f); Vector3 jiggleUp = this.jiggleOrientation * this.originalToMassOrientation * Vector3.up; jiggleUp *= massOffsetLength / 5.0f; Gizmos.DrawLine( transform.position + jiggleForward * 2.0f, (transform.position + jiggleForward * 2.0f) + jiggleUp ); } // Green marker to the rest position (the orientation the jiggle bone is chasing). { Vector3 restForward = transform.parent.rotation * massRestOrientation * Vector3.forward; restForward *= massOffsetLength; Gizmos.color = Color.green; Gizmos.DrawLine(transform.position, transform.position + restForward); Gizmos.DrawSphere(transform.position + restForward, massOffsetLength / 25.0f); Vector3 restUp = transform.parent.rotation * massRestOrientation * Vector3.up; restUp *= massOffsetLength / 5.0f; Gizmos.DrawLine( transform.position + restForward, (transform.position + restForward) + restUp ); } // Blue marker for the dynamic position Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(this.dynamicPosition, massOffsetLength / 10.0f); } /// Applies the jiggle orientation to the bone private void applyJiggleOrientation() { // The jiggle orientation is in the global coordinate frame, but the bone's // rotation is expressed relative to its parent, thus we need to transform // the jiggle orientation into the same coordinate frame. Quaternion parentOrientation = transform.parent.rotation; transform.localRotation = Quaternion.Inverse(parentOrientation) * this.jiggleOrientation; } /// /// Orientation of the mass' rest position in the coordinate frame of the parent bone /// private Quaternion massRestOrientation { get { Vector3 massDirection = Vector3.Normalize(this.MassOffset); // HACK: Up vector of bone bind position points right for some reason :-(( // I don't understand the problem sufficiently, the roll of the bone should // not matter (the massToOriginalOrientation should take care of that), but it // does. So now I'm using the left vector here... #if HAVE_DAMN_MATH_PROBLEM_SOLVED return Quaternion.LookRotation(massDirection, this.up); #else return Quaternion.LookRotation(massDirection, this.RestAngle); #endif } } /// Rest position of the center of mass in world space private Vector3 massRestPosition { get { Vector3 forward = transform.parent.rotation * this.MassOffset; return transform.position + forward; } } /// Returns the orthogonal up vector for the provided direction /// Direction of which the up vector will be found /// The up vector for the provided direction private static Vector3 getOrthogonalUp(Vector3 direction) { switch(getLongestAxis(direction)) { case 0: { // X axis is longest if(direction.x > 0.0f) { return new Vector3(direction.z, direction.x, direction.y); } else { return new Vector3(-direction.z, -direction.x, -direction.y); } } case 1: { // Y axis is longest if(direction.y > 0.0f) { return new Vector3(direction.z, direction.x, direction.y); } else { return new Vector3(-direction.z, -direction.x, -direction.y); } } case 2: { // Z axis is longest if(direction.z > 0.0f) { return new Vector3(direction.y, direction.z, direction.x); } else { return new Vector3(-direction.y, -direction.z, -direction.x); } } default: { throw new InvalidOperationException("getLongestAxis() malfunctioned"); } } } /// Returns the index of the vector's longest axis /// Direction in which the longest axis will be found /// The index of the longest axis in the vector private static int getLongestAxis(Vector3 direction) { float absX = Math.Abs(direction.x); float absY = Math.Abs(direction.y); float absZ = Math.Abs(direction.z); if(absX > absY) { if(absX > absZ) { return 0; } else { return 2; } } else { if(absY > absZ) { return 1; } else { return 2; } } } /// Transforms from the mass orientation to the jiggle bone orientation private Quaternion massToOriginalOrientation; /// Transforms from the jiggle bone orientation to the mass orientation private Quaternion originalToMassOrientation; /// Original orientation of the bone with the mass at its rest position /// /// This orientation is specified in the coordinate frame of the jiggle bone's parent /// because otherwise, the original local rotation of the jiggle bone would be lost. /// private Quaternion originalOrientation; /// Current orientation of the bone /// /// Specified in the global coordinate frame. This is so because when the character /// rotates, the jiggle bone will not turn with it (only its stiffness cause it /// to follow). The local rotation of the jiggle bone will be this orientation /// transformed into the coordinate frame of the jiggle bone's parent. /// private Quaternion jiggleOrientation; #if HAVE_DAMN_MATH_PROBLEM_SOLVED /// Direction that is upwards to the mass offset private Vector3 up; #endif /// Dynamic position towards which the simulated mass is oriented private Vector3 dynamicPosition; /// Velocity of the dynamic position private Vector3 dynamicVelocity; } } // namespace Framework.Support