using System; using UnityEngine; using Framework.Services; using Framework.Support; namespace Framework.Actors { /// Allows a character's head to be pointed in different directions /// /// /// This component takes care of posing the bones of a character's head. It can /// do this with one bone (meaning the head will rotate as if on a swivel), /// two bones (distributing the rotation between the neck and head bones) and /// three bones (like two bones, but also bending the chest when the player /// attempts to look straight up or down). /// /// /// Care has been taken to make the head end up in the exact orientation /// requested at all times, allowing a mouse look camera to be parented to /// the real head. /// /// /// The head poser also supports VR, allowing the eyes to be put in the exact /// position the actual player's eyes are. For this to work, all three bones /// (chest, neck and head) need to be present. /// /// public class HeadPoser : ScriptComponent { /// Fired before the head and neck bones are posed public event Action BeforeHeadPosing; /// Fired after the head and neck bones have been posed public event Action AfterHeadPosing; /// Bone that transforms the chest. Optional. public Transform Chest; /// Bone that transforms the neck public Transform Neck; /// Bone that transforms the head public Transform Head; /// Offset of the eyes from the head bone. /// /// Required to use the method. /// public Vector3 EyeOffset; /// How much the neck follows the rotations of the head public float NeckRotationRatio = 0.333f; /// How much the character's chest will bend forward and backward /// /// For most cases it's enough to have the character's head and neck rotate, /// but for cases where a character may want to look straight down or straight up, /// it will be more realistic if the last bit of this movement is helped by /// moving the chest accordingly. /// public AnimationCurve ChestBendByPitch = CreateDefaultChestBendCurve(); /// Sets up a default animation curve for the chest bend ratio /// A new default animation curve public static AnimationCurve CreateDefaultChestBendCurve() { var curve = new AnimationCurve( new Keyframe(-90.0f, -40.0f), // * Once the head is fully pitched back, new Keyframe(-45.0f, 0.0f), // any further rotation happens through the spine new Keyframe(+70.0f, 0.0f), // * When the chin hits the chest, further new Keyframe(+90.0f, 17.5f), // rotation will again happen through the spine new Keyframe(+120.0f, 45.0f) // ); //curve.SetKeyTangentMode(3, TangentMode.Smooth); return curve; } /// /// Returns the quaternion for the coordinate frame in which the head is moved /// /// The quaternion defining the rotational coordinate frame for the head /// /// When the character bends over or such, the head rotation still happens on /// the top of the spine (i.e. is rotated by 90 degrees then). /// public Quaternion GetBaseOrientation() { if(this.Chest == null) { return this.Neck.parent.rotation; } else { return this.Chest.parent.rotation; } } /// Returns the relative orientation of the head in its orientation space /// The relative orientation of the head public Quaternion GetLocalHeadOrientation() { Quaternion relativeOrientation = GetBaseOrientation(); return Quaternion.Inverse(relativeOrientation) * this.Head.rotation; } /// Poses the head and its supporting bones to match an orientation /// Orientation the head will assume public void Orient(Quaternion orientation) { OnBeforeHeadPosing(); // Chest rotation if(this.Chest != null) { // Assumption: the neutral chest orientation from the model's bind pose // has the head pointing forward. Also, any rotation we apply to the chest // would directly result in the same rotation happening on the head. // Figure out the absolute target orientation in the chest's coordinate space. Quaternion chestToTargetOrientation = Quaternion.Inverse(this.Chest.parent.rotation) * orientation; // Apply the neck's bind pose to the target orientation (so if looking forward // has the neck bone pointing upward,, that's how it will end up here). chestToTargetOrientation *= this.neutralChestOrientation; // Rotation needed to completely face the target with chest rotation alone Quaternion chestDeltaOrientation = Quaternion.Inverse(this.neutralChestOrientation) * chestToTargetOrientation; // Now extract the pitch as a vanilla Euler angle. If it's rotated more than // 180 degrees, we assume it's bent back, not more than a half turn forward. Vector3 eulerAngles = chestDeltaOrientation.eulerAngles; if(eulerAngles.x >= 180.0f) { eulerAngles.x -= 360.0f; } // Translate the pitch into the desired chest rotation using the curve // set up for this head poser. This will be a small fraction of the angle // because mouse of the head movement is done by the head and neck bones. float chestRotation = this.ChestBendByPitch.Evaluate(eulerAngles.x); Quaternion chestBendOrientation = Quaternion.Euler(chestRotation, 0.0f, 0.0f); chestBendOrientation *= this.neutralChestOrientation; // Done, apply the chest rotation (updating the child bones so the following // code will calculate rotations based on the remaining direction delta!) this.Chest.localRotation = chestBendOrientation; } // Neck rotation { // Figure out the absolute target orientation in the neck's coordinate space. Quaternion neckToTargetOrientation = Quaternion.Inverse(this.Neck.parent.rotation) * orientation; // Apply the neck's bind pose to the target orientation (so if looking forward // has the neck bone pointing upward,, that's how it will end up here). neckToTargetOrientation *= this.neutralNeckOrientation; // Now scale down the amount of neck rotation to the ratio by which the neck should // follow the head Quaternion scaledNeckOrientation = Quaternion.SlerpUnclamped( this.neutralNeckOrientation, neckToTargetOrientation, this.NeckRotationRatio ); // What we got now can be directly assigned to the neck bone this.Neck.localRotation = scaledNeckOrientation; } // Head rotation { // Figure out the absolute target orientation in the head's coordinate space. Quaternion headToTargetOrientation = Quaternion.Inverse(this.Head.parent.rotation) * orientation; // Apply the head's bind pose to the target orientation (so if looking forward // has the head bone pointing upward,, that's how it will end up here). headToTargetOrientation *= this.neutralHeadOrientation; // What we got now can be directly assigned to the head bone this.Head.localRotation = headToTargetOrientation; } OnAfterHeadPosing(); } /* /// Poses the head and its supporting so it is looking at a target /// Target position the head will look at public void LookAt(Vector3 position) { // This should pose the head in a best-effort attempt to make the eyes // point at the target. throw new NotImplementedException("Not implemented yet"); } */ /// Poses the head to exactly match an eye position and orientation /// Position the eyes will be at /// Orientation the head will assume /// /// This is mainly useful for VR applications where the player can freely turn /// the character's head as well as shift in different directions. This method will /// force the head into the desired position, even if the neck needs to stretch /// or twist into unrealistic orientations. /// public void Pose(Vector3 eyePosition, Quaternion orientation) { OnBeforeHeadPosing(); // Determine the position the head bone needs to be at in order for // the eyes to be placed at the requested location. Vector3 worldEyeOffset = orientation * this.EyeOffset; Vector3 headPosition = eyePosition - worldEyeOffset; // If we have no chest bone, the only choice is to stretch or compress // the neck. That might look silly, but so long as if (this.Chest == null) { // Just make the neck point towards the position the head bone needs to be at Quaternion neckOrientation = this.neutralNeckOrientation * this.Neck.parent.rotation; this.Neck.transform.LookAt(headPosition, neckOrientation * Vector3.up); } else { // Reset the scale so we don't confuse the inverse kinematics this.Neck.localScale = Vector3.one; this.Head.localScale = Vector3.one; // Use the chest and neck as an IK chain to get the head into // the required position InverseKinematicsHelper.SolveTwoBoneIK( this.Chest, this.Neck, this.Head, headPosition, this.Neck.right, 1.0f ); } // The head target may be beyond the range the bones can bridge. // In this case, we stretch the neck :) float targetDistance = Vector3.Distance(this.Neck.position, headPosition); float factor = targetDistance / this.neckLength; this.Neck.localScale = new Vector3(factor, factor, factor); this.Head.localScale = new Vector3(1.0f / factor, 1.0f / factor, 1.0f / factor); // Now for the simplest part of the posing: put the head in the same // orientation as the HMD. this.Head.rotation = orientation; OnAfterHeadPosing(); } /// Called once before the component is updated for the first time protected override void Awake() { base.Awake(); saveBindPose(); } /// Fires the event protected virtual void OnBeforeHeadPosing() { if(BeforeHeadPosing != null) { BeforeHeadPosing(); } } /// Fires the event protected virtual void OnAfterHeadPosing() { if(AfterHeadPosing != null) { AfterHeadPosing(); } } /// Saves the orientation of the head and neck bones in their bind pose private void saveBindPose() { if(this.Neck == null) { Debug.LogError( "HeadPoser does not have a 'Neck' bone assigned. " + "Head movement will not work." ); return; } if(this.Head == null) { Debug.LogError( "HeadPoser does not have a 'Head' bone assigned. " + "Head movement will not work." ); return; } // Memorize the neutral bone orientations Transform neckTransform = this.Neck.transform; this.neutralNeckOrientation = neckTransform.localRotation; Transform headTransform = this.Head.transform; this.neutralHeadOrientation = headTransform.localRotation; this.neckLength = Vector3.Distance( neckTransform.position, headTransform.position ); // The chest bone is optional. If present, memorize it, too. if(this.Chest != null) { Transform chestTransform = this.Chest.transform; this.neutralChestOrientation = chestTransform.localRotation; } } #if false /// /// Attempt to scale the neck along its length axis without touching the other axes /// /// Factor by which the neck length should be extended private void fancyNonUniformNeckScaling(float factor) { float halfFactor = (factor - 1.0f) / 2.0f + 1.0f; // From experience, different modeling tools leave rig bones in pretty arbitraty // positions (via intermediate bones). Thus we look for the direction that is // pointing forward via the dot product. We scale the other angles by a bit, too, // since non-uniform scales tend to distort the head if the neck bones have // influence on the head. If it only needs to look okay for small adjustments... Vector3 headDirection = Vector3.Normalize(this.Head.position - this.Neck.position); float forwardDot = Math.Abs(Vector3.Dot(this.Neck.forward, headDirection)); float rightDot = Math.Abs(Vector3.Dot(this.Neck.forward, headDirection)); float upDot = Math.Abs(Vector3.Dot(this.Neck.forward, headDirection)); if (forwardDot > rightDot) { if (forwardDot > upDot) { this.Neck.localScale = new Vector3(halfFactor, halfFactor, factor); } else { this.Neck.localScale = new Vector3(halfFactor, factor, halfFactor); } } else { if (rightDot > upDot) { this.Neck.localScale = new Vector3(factor, halfFactor, halfFactor); } else { this.Neck.localScale = new Vector3(halfFactor, factor, halfFactor); } } Vector3 headScale = this.Neck.localScale; headScale.x = 1.0f / headScale.x; headScale.y = 1.0f / headScale.y; headScale.z = 1.0f / headScale.z; this.Head.localScale = headScale; } #endif /// Neutral orientaiton for the chest bone private Quaternion neutralChestOrientation; /// Neutral orientation for the neck bone private Quaternion neutralNeckOrientation; /// Distance from the base of the neck to the head bone private float neckLength; /// Neutral orientation for the head bone private Quaternion neutralHeadOrientation; } } // namespace Framework.Actors