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