Search Unity

How can I rotate character's head regardless of bone orientation?

Discussion in 'Scripting' started by mysticfall, Feb 5, 2017.

  1. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    Hi,

    I wrote a controller script which mounts the camera to the head of a character when zoomed in. It worked reasonably well until I attached the script to Ethan character from standard assets package, because for some reason, the head transform of that character was modelled in a different orientation(using -x as 'up' direction, instead of y).

    I didn't know a humanoid model could use an arbitrary axis as 'up' vector. So, my question is, how can I write an API which turns the head of an arbitrary humanoid character to certain degrees?

    If Mecanim animations are exchangeable between humanoid models, shouldn't there be some abstraction layer by which I can rotate certain body parts in certain degrees, regardless of their axis orientation?

    Thanks in advance!
     
    Last edited: May 16, 2017
  2. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    Bump... because I still haven't figured it out how to handle this problem properly :cool:
     
  3. theANMATOR2b

    theANMATOR2b

    Joined:
    Jul 12, 2014
    Posts:
    7,790
    I can't answer your question directly because - noncoder, but might be able to help.
    In 3D Max there is a reaction controller which is basically (if) something happens (do) this.
    I use this quite a bit when animating characters, in custom setups. In one simple example, in the flex modifier. (If) the arm bends at or beyond 45 degrees the bicep muscle will bulge a set amount until hitting 100% at about 165 degrees.
    I've used this controller in the past on setups like a tank tread. If the wheel rotates X amount - move the tank tread along a spline X percent - so it looks like the wheel is actually driving the treads around the sprocket.

    Other simple setups are if box rotates X amount - rotate unrelated other object on a different axis X amount.

    I don't know code - but couldn't a script be created to rotate the camera on its Y axis when the head bone rotates on its X axis a set amount?

    This is true and I think the conversion from bones to muscle space doesn't require each bone to be oriented exactly the same. The only requirement is the root bone has to be oriented with Y up.
     
  4. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    The heart of the problem is that I cannot really assume which axis of the head transform is an actual 'up' vector, because it can differ between a model from model.

    If I was developing a specific game with a particular model, it've been much easier. But it's just that I've been writing a generic first person camera controller which anyone can use for their own characters, so I cannot assume specific axis orientation of such models.

    It seems that there's no choice for me but to guess its local orientation from that of the body or even the global up vector, or let users to set a property in the editor.

    But it's quite surprising that Unity doesn't provide any convenient way to do that, since the whole point of the Mecanim, as I understand it, is to abstract away from such specific details of a model for animating it in a consistent manner.

    But thanks for the answer!
     
  5. deadopinion569

    deadopinion569

    Joined:
    Sep 17, 2016
    Posts:
    4
    I'm a noob so do correct me but rule #1 for asking question, post in as much info as you can. This includes scripts, projects, errors you see, problems you see and any other stuff. The more the better. With enough info, the pros will come in to answer your question correctly at one point.

    Let's put in some assumption, that the controller script you had works 100% on a capsule object. The "head of a character" is nothing more than xyz direction from the object reference point (usually center). Also, assumption "up" is the same unity Vector3.up which is Vector3(0, 1, 0). Hopefully your script isn't so complex that it is looking for the "head" in the script or referring to the "head" orientation.

    Now replace the capsule object with an empty object. Afterward put in "Ethan model" (just the model, nothing else) inside the empty object. Did your script worked? If it did, good. Now is the Ethan model including head in the wrong orientation? If it is just rotate the model object.

    If you are really asking for "can you rotate character's head regardless of bone orientation?" My guess is yes (if the head is separate). You just have to find the individual body part and rotate it or you export the model and change the model orientation or rotate the head. Except, the Ethan model should be mostly one model (minor body part separated). So in your case, I doubt it could.

    Hopefully, this post helped you. If not, do post in more info and some other pros might be able to answer for you.
     
  6. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    @deadopinion569 I'm afraid I wasn't clear enough with my question. First of all, in such cases like 'Ethan character' from Unity's Standard Asset, I cannot simply rotate the model object because it would leave that poor Ethan guy having a head attached 90 degrees sideways to his body :p

    And of course, it'd be trivial to solve if I can modify either the model or the script according to a specific case. But what I'm trying to do is not really creating a specific game for me, but to write a generic API (like some framework type asset) which other people can use with their own models.

    So I don't want to force each user to modify their models in order to use my API, or change the source code themselves which would be too much of a hassle.

    I didn't post my code because it's quite a mess (actually, it's the reason why I wanted to find a clean solution to this problem), and not easy to understand the context without reading the whole structure.

    But if you are interested, the relevant source code is here:
    I'm leaning toward giving up the idea about finding out proper orientation at runtime, and just put properties for up and forward vectors so users can change them if needed.
     
    Last edited: May 17, 2017
  7. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    Just changed it so that it allow users to specify the correct head orientation with properties if needed. I didn't push it to the repository, as I haven't modified the associated tests yet.

    I can't think of any better way to do it for now, but I don't really understand why it can't be easier when we have Mecanim already.

    Code (CSharp):
    1. using System;
    2. using Alensia.Core.Actor;
    3. using Alensia.Core.Common;
    4. using UnityEngine;
    5. using UnityEngine.Assertions;
    6. using Zenject;
    7.  
    8. namespace Alensia.Core.Camera
    9. {
    10.     public class HeadMountedCamera : RotatableCamera, IFirstPersonCamera, ILateTickable
    11.     {
    12.         public override float Heading
    13.         {
    14.             get { return _heading; }
    15.  
    16.             set
    17.             {
    18.                 var heading = Mathf.Clamp(
    19.                     GeometryUtils.NormalizeAspectAngle(value),
    20.                     -RotationalConstraints.Side,
    21.                     RotationalConstraints.Side);
    22.  
    23.                 _heading = heading;
    24.  
    25.                 UpdatePosition(heading, Elevation);
    26.             }
    27.         }
    28.  
    29.         public override float Elevation
    30.         {
    31.             get { return _elevation; }
    32.  
    33.             set
    34.             {
    35.                 var elevation = Mathf.Clamp(
    36.                     GeometryUtils.NormalizeAspectAngle(value),
    37.                     -RotationalConstraints.Down,
    38.                     RotationalConstraints.Up);
    39.  
    40.                 _elevation = elevation;
    41.  
    42.                 UpdatePosition(Heading, elevation);
    43.             }
    44.         }
    45.  
    46.         public float LookAhead => _settings.LookAhead;
    47.  
    48.         public Vector3 CameraOffset => _settings.CameraOffset;
    49.  
    50.         public override bool Valid => base.Valid && Head != null;
    51.  
    52.         public override RotationalConstraints RotationalConstraints => _settings.Rotation;
    53.  
    54.         public ITransformable Target { get; private set; }
    55.  
    56.         public Transform Head { get; private set; }
    57.  
    58.         public override Vector3 Pivot
    59.         {
    60.             get
    61.             {
    62.                 var humanoid = Target as IHumanoid;
    63.                 var offset = Head.TransformDirection(CameraOffset) *
    64.                              CameraOffset.magnitude;
    65.  
    66.                 return (humanoid?.Viewpoint ?? Head.position) + offset;
    67.             }
    68.         }
    69.  
    70.         public override Vector3 AxisForward => _settings.HeadAxisFoward.Of(Head);
    71.  
    72.         public override Vector3 AxisUp => _settings.HeadAxisUp.Of(Head);
    73.  
    74.         protected Vector3 FocalPoint
    75.         {
    76.             get
    77.             {
    78.                 var rotation = Target.Transform.rotation * Quaternion.Euler(-Elevation, Heading, 0);
    79.  
    80.                 return Head.position + rotation * Vector3.forward * LookAhead;
    81.             }
    82.         }
    83.  
    84.         private float _heading;
    85.  
    86.         private float _elevation;
    87.  
    88.         private Quaternion _initialRotation;
    89.  
    90.         private readonly Settings _settings;
    91.  
    92.         public HeadMountedCamera(
    93.             UnityEngine.Camera camera) : this(new Settings(), camera)
    94.         {
    95.         }
    96.  
    97.         [Inject]
    98.         public HeadMountedCamera(
    99.             Settings settings,
    100.             UnityEngine.Camera camera) : base(camera)
    101.         {
    102.             Assert.IsNotNull(settings, "settings != null");
    103.  
    104.             _settings = settings;
    105.         }
    106.  
    107.         public void Initialize(ITransformable target)
    108.         {
    109.             Assert.IsNotNull(target, "target != null");
    110.  
    111.             Target = target;
    112.  
    113.             var character = target as IHumanoid;
    114.  
    115.             if (character == null)
    116.             {
    117.                 Head = Target.Transform;
    118.             }
    119.             else
    120.             {
    121.                 Head = character.Head ?? Target.Transform;
    122.             }
    123.  
    124.             _initialRotation = Head.localRotation;
    125.         }
    126.  
    127.         protected override void OnDeactivate()
    128.         {
    129.             base.OnDeactivate();
    130.  
    131.             if (Head == null) return;
    132.  
    133.             Head.localRotation = _initialRotation;
    134.         }
    135.  
    136.         protected virtual void UpdatePosition(float heading, float elevation)
    137.         {
    138.             var rotation = Vector3.zero;
    139.  
    140.             rotation = _settings.HeadAxisUp.Set(rotation, heading);
    141.             rotation = _settings.HeadAxisFoward.Set(rotation, -elevation);
    142.  
    143.             Head.localRotation = Quaternion.Euler(rotation);
    144.  
    145.             Transform.position = Pivot;
    146.             Transform.rotation = Quaternion.LookRotation(AxisForward, AxisUp);
    147.  
    148.             if (Mathf.Abs(elevation) > 89)
    149.             {
    150.                 Transform.LookAt(FocalPoint, Transform.up);
    151.             }
    152.             else
    153.             {
    154.                 Transform.LookAt(FocalPoint);
    155.             }
    156.         }
    157.  
    158.         public virtual void LateTick()
    159.         {
    160.             if (Active) UpdatePosition(Heading, Elevation);
    161.         }
    162.  
    163.         [Serializable]
    164.         public class Settings : IEditorSettings
    165.         {
    166.             public RotationalConstraints Rotation = new RotationalConstraints
    167.             {
    168.                 Down = 65,
    169.                 Side = 85,
    170.                 Up = 60
    171.             };
    172.  
    173.             [Range(0.1f, 10f)]
    174.             public float LookAhead = 10f;
    175.  
    176.             public Vector3 CameraOffset = new Vector3(0, 0, 0.2f);
    177.  
    178.             public Axis HeadAxisUp = Axis.Y;
    179.  
    180.             public Axis HeadAxisFoward = Axis.Z;
    181.         }
    182.     }
    183. }
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. namespace Alensia.Core.Common
    5. {
    6.     public enum Axis
    7.     {
    8.         X,
    9.         Y,
    10.         Z,
    11.         InverseX,
    12.         InverseY,
    13.         InverseZ
    14.     }
    15.  
    16.     public static class AxisExtensions
    17.     {
    18.         public static Vector3 Of(this Axis axis, Transform transform)
    19.         {
    20.             switch (axis)
    21.             {
    22.                 case Axis.X:
    23.                     return transform.right;
    24.                 case Axis.Y:
    25.                     return transform.up;
    26.                 case Axis.Z:
    27.                     return transform.forward;
    28.                 case Axis.InverseX:
    29.                     return transform.right * -1;
    30.                 case Axis.InverseY:
    31.                     return transform.up * -1;
    32.                 case Axis.InverseZ:
    33.                     return transform.forward * -1;
    34.                 default:
    35.                     throw new ArgumentOutOfRangeException();
    36.             }
    37.         }
    38.  
    39.         public static Vector3 Set(this Axis axis, Vector3 vector, float value)
    40.         {
    41.             switch (axis)
    42.             {
    43.                 case Axis.X:
    44.                     vector.x = value;
    45.                     break;
    46.                 case Axis.Y:
    47.                     vector.y = value;
    48.                     break;
    49.                 case Axis.Z:
    50.                     vector.z = value;
    51.                     break;
    52.                 case Axis.InverseX:
    53.                     vector.x = -value;
    54.                     break;
    55.                 case Axis.InverseY:
    56.                     vector.y = -value;
    57.                     break;
    58.                 case Axis.InverseZ:
    59.                     vector.z = -value;
    60.                     break;
    61.                 default:
    62.                     throw new ArgumentOutOfRangeException();
    63.             }
    64.  
    65.             return vector;
    66.         }
    67.     }
    68. }
     
  8. theANMATOR2b

    theANMATOR2b

    Joined:
    Jul 12, 2014
    Posts:
    7,790
    Hey mysticfall - I seem to have stumbled mistakenly into the scripting thread by mistake. Never peaked behind the shroud of this sub-forum before. I feel a little exposed. ;)

    Couldn't the code recognize which up vector the head bone is using and then be animatable for the setup, around that axis?

    If no - I think providing the user the ability to identify the up vector of the head bone is an acceptable solution - as you mentioned.

    I'm going to holler at @TrickyHandz who is working on some very interesting content using mecanim. I believe he might be helpful here.

    Also worth checking other free characters from the asset store just to confirm the up axis is not always the same on each humanoid rigged character.
     
    mysticfall likes this.
  9. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    Welcome to the club! Though I can't claim to be a regular here myself :)

    Of course, I can easily find the up vector in the head transform's local coordinate system. But the problem is, it's entirely possible that the designer who modelled and rigged that character somehow decided to put that head into the body up side down, backwards, or sideways, in which case the 'up' vector does not really mean the direction going upwards from the character's head away from the body.

    One possible approach I can think of could be that calculating the 'real' up vector by comparing each of the local axes with relative position of other body parts. Like, for instance, we can select a position extending in the direction of each axes by fixed distance (say 30cm), then trying to find out what point lies the farthest from the chest transform, because it would probably be the 'real' up vector for the head.

    I'm still a newb with Unity, but I'm almost certain that Mecanim does such kind of calculation internally when we set up an avatar, because it would be logically impossible to animate an arbitrary humanoid character, otherwise.

    But the problem is, Mecanim - if it does such things, indeed - can calculate it with a T-posed character at design time, but my script need to do it for an animated character in runtime. I know if'd be a corner case, but what if, for example my character lies on the floor in fetal position, then wouldn't there be possibility that the point extending from the real up axis is actually closer to the chest than that extending from back of the head?

    So, I'm inclining towards the manual approach with user properties. I'm just a bit surprised to see that simply turning the head of an arbitrary character would involve so much hassle, especially when I heard people actually use procedural animations with Unity, and etc.

    I only tried characters generated by UMA and MCS, aside from the Ethan character. And if I remember it correctly (I could be wrong) only the one from MCS has matching head orientation with the global coordinates.

    Oh, and thanks much for the suggestion, and I'm eager to hear advises from an expert because I'm still quite clueless in a lot of things with Unity :)
     
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Well mecanim just animates based on the animations that are given to it. Those are created by the animator using the rig the model was set up with. If you attempted to apply the same animation to a rig oriented differently... they'd break. The animations don't really care what vector is up because the rotations it performs are all relative.

    As for why the rigs might be weird, that's just because various software will use different coordinate system. For example on our projects my artist works in 3D Studio Max, which uses right-handed z-up coordinate system, then the pipeline that he uses to Unity ends up with a y forward for the head. It's fairly arbitrary that we use a left-handed z-forward coordinate system in unity.

    What unity usually does when importing is just stick the Root object of your rig in a GameObject with an offset rotation to compensate for the fact it came from a different coordinate system. It didn't always do that either... I think it was Unity 5 they introduced it... maybe a mid version in Unity 4. Eitherway, they didn't used to do it, and we all would just do it manually. It was nice when they auto added that feature.

    From there, the animations just act within said container, and the container keeps up as unity up (unless you specifically rotate that object in code).

    I do procedural animations sometimes because I have to. For example this animation here:


    It's fairly simple, if you stand still for a period of time this little girl creepily stares into the camera, at you the player. It's supposed to spook the player out. Note, the target it stares at is arbitrary as well, I also use this same code to make it look at the player avatar as she walks around the scene.

    Anyways, because the target could be anywhere, a static animation wouldn't work. So instead I wrote this for the animation (note the interfaces and what not are just part of my framework):

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Anim;
    8. using com.spacepuppy.Scenario;
    9. using com.spacepuppy.Utils;
    10.  
    11. namespace com.mansion.Anim
    12. {
    13.  
    14.     public class DummyLookAnim : MonoBehaviour, IScriptableAnimationClip
    15.     {
    16.  
    17.         #region Fields
    18.  
    19.         [SerializeField]
    20.         [SelectableObject]
    21.         private UnityEngine.Object _target;
    22.         [SerializeField]
    23.         [Range(0f, 1f)]
    24.         private float _turnSlerpSpeed = 0.1f;
    25.         [SerializeField]
    26.         [Range(0f, 1f)]
    27.         private float _lookSlerpSpeed = 0.1f;
    28.         [SerializeField]
    29.         private float _angleClamp = 30.0f;
    30.         [SerializeField]
    31.         private bool _rotateWhenOutOfLookRange;
    32.  
    33.         private IEntity _entity;
    34.         private AnimState _state;
    35.  
    36.         #endregion
    37.  
    38.         #region CONSTRUCTOR
    39.  
    40.         private void Awake()
    41.         {
    42.             _entity = IEntity.Pool.GetFromSource<IEntity>(this);
    43.         }
    44.  
    45.         #endregion
    46.  
    47.         #region ISPAnimationClip Interface
    48.  
    49.         public float Length
    50.         {
    51.             get
    52.             {
    53.                 return float.PositiveInfinity;
    54.             }
    55.         }
    56.  
    57.         public ISPAnim CreateState(SPAnimationController controller)
    58.         {
    59.             if (_state == null)
    60.             {
    61.                 _state = new AnimState(this, controller, _target);
    62.                 return _state;
    63.             }
    64.             else
    65.             {
    66.                 if (_state.Target == _target)
    67.                 {
    68.                     if (!_state.IsPlaying)
    69.                     {
    70.                         return _state;
    71.                     }
    72.                     else
    73.                     {
    74.                         return SPAnim.Null;
    75.                     }
    76.                 }
    77.                 else
    78.                 {
    79.                     _state.Stop();
    80.                     _state = new AnimState(this, controller, _target);
    81.                     return _state;
    82.                 }
    83.             }
    84.         }
    85.  
    86.         public ISPAnim CreateState(SPAnimationController controller, Transform target)
    87.         {
    88.             if (_state == null)
    89.             {
    90.                 _state = new AnimState(this, controller, target);
    91.                 return _state;
    92.             }
    93.             else
    94.             {
    95.                 if(_state.Target == target)
    96.                 {
    97.                     if(!_state.IsPlaying)
    98.                     {
    99.                         return _state;
    100.                     }
    101.                     else
    102.                     {
    103.                         return SPAnim.Null;
    104.                     }
    105.                 }
    106.                 else
    107.                 {
    108.                     _state.Stop();
    109.                     _state = new AnimState(this, controller, target);
    110.                     return _state;
    111.                 }
    112.             }
    113.         }
    114.  
    115.         #endregion
    116.  
    117.         #region Special Types
    118.  
    119.         private class AnimState : ScriptableAnimState, IScriptableAnimationCallback
    120.         {
    121.  
    122.             private DummyLookAnim _owner;
    123.             private UnityEngine.Object _target;
    124.             private float _startTime;
    125.             private Quaternion _lastRot;
    126.             private bool _playing;
    127.  
    128.             #region CONSTRUCTOR
    129.  
    130.             public AnimState(DummyLookAnim owner, SPAnimationController controller, UnityEngine.Object target)
    131.                 : base(controller)
    132.             {
    133.                 _owner = owner;
    134.                 _target = target;
    135.             }
    136.  
    137.             #endregion
    138.  
    139.             #region ISPAnim Interface
    140.  
    141.             public UnityEngine.Object Target
    142.             {
    143.                 get { return _target; }
    144.             }
    145.  
    146.             public override float Duration
    147.             {
    148.                 get { return float.PositiveInfinity; }
    149.             }
    150.  
    151.             public override bool IsPlaying
    152.             {
    153.                 get { return _playing; }
    154.             }
    155.  
    156.             public override float Time
    157.             {
    158.                 get { return _playing ? UnityEngine.Time.time - _startTime : 0f; }
    159.                 set { }
    160.             }
    161.  
    162.             public override void CrossFade(float fadeLength, QueueMode queueMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
    163.             {
    164.                 if (_playing) return;
    165.  
    166.                 _startTime = UnityEngine.Time.time;
    167.                 _lastRot = _owner.transform.localRotation;
    168.                 _playing = true;
    169.                 this.Controller.StartScriptableAnim(this, playMode);
    170.             }
    171.  
    172.             public override void Play(QueueMode queueMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
    173.             {
    174.                 if (_playing) return;
    175.  
    176.                 _startTime = UnityEngine.Time.time;
    177.                 _lastRot = _owner.transform.localRotation;
    178.                 _playing = true;
    179.                 this.Controller.StartScriptableAnim(this, playMode);
    180.             }
    181.  
    182.             public override void Stop()
    183.             {
    184.                 _lastRot = Quaternion.identity;
    185.                 _playing = false;
    186.                 this.Controller.StopScriptableAnim(this, false);
    187.             }
    188.  
    189.             #endregion
    190.  
    191.             #region IScriptableAnimationCallback Interface
    192.  
    193.             int IScriptableAnimationCallback.Layer
    194.             {
    195.                 get
    196.                 {
    197.                     return this.Layer;
    198.                 }
    199.             }
    200.  
    201.             bool IScriptableAnimationCallback.Tick(bool layerIsObscured)
    202.             {
    203.                 if (!_playing) return false;
    204.  
    205.                 var target = GameObjectUtil.GetTransformFromSource(_target, true);
    206.                 if (target == null) return false;
    207.  
    208.                 var dir = target.position - _owner.transform.position;
    209.                 float a;
    210.  
    211.                 if (_owner._rotateWhenOutOfLookRange)
    212.                 {
    213.                     if (_owner._entity != null)
    214.                     {
    215.                         a = Vector3.Angle(_owner._entity.transform.forward.SetY(0f), dir.SetY(0f));
    216.                         if (a > _owner._angleClamp)
    217.                         {
    218.                             _owner._entity.transform.rotation = Quaternion.Slerp(_owner._entity.transform.rotation, Quaternion.LookRotation(dir.SetY(0f)), _owner._turnSlerpSpeed);
    219.                         }
    220.                     }
    221.                 }
    222.  
    223.                 //find target
    224.                 var targetRot = Quaternion.LookRotation(dir) * Quaternion.Euler(0f, -90f, -90f);
    225.  
    226.                 //constrain
    227.                 var identity = (_owner.transform.parent != null) ? _owner.transform.parent.rotation : Quaternion.identity;
    228.                 a = Quaternion.Angle(identity, targetRot);
    229.                 if (a > _owner._angleClamp)
    230.                 {
    231.                     targetRot = Quaternion.Slerp(identity, targetRot, _owner._angleClamp / a);
    232.                 }
    233.  
    234.                 //slerp locally, this has to be done because other layered animations overwrite the previous rotation state, so we save it locally
    235.                 if(_owner.transform.parent != null)
    236.                     targetRot = _owner.transform.parent.InverseTransformRotation(targetRot);
    237.                 _lastRot = Quaternion.Slerp(_lastRot, targetRot, _owner._lookSlerpSpeed);
    238.            
    239.                 //set
    240.                 _owner.transform.localRotation = _lastRot;
    241.            
    242.                 return true;
    243.             }
    244.  
    245.             void System.IDisposable.Dispose()
    246.             {
    247.                 this.Stop();
    248.             }
    249.  
    250.             #endregion
    251.  
    252.         }
    253.  
    254.         #endregion
    255.  
    256.     }
    257.  
    258. }
    259.  
    The specific code in question:

    Code (csharp):
    1.  
    2.                 //find target
    3.                 var targetRot = Quaternion.LookRotation(dir) * Quaternion.Euler(0f, -90f, -90f);
    4.  
    5.                 //constrain
    6.                 var identity = (_owner.transform.parent != null) ? _owner.transform.parent.rotation : Quaternion.identity;
    7.                 a = Quaternion.Angle(identity, targetRot);
    8.                 if (a > _owner._angleClamp)
    9.                 {
    10.                     targetRot = Quaternion.Slerp(identity, targetRot, _owner._angleClamp / a);
    11.                 }
    12.  
    13.                 //slerp locally, this has to be done because other layered animations overwrite the previous rotation state, so we save it locally
    14.                 if(_owner.transform.parent != null)
    15.                     targetRot = _owner.transform.parent.InverseTransformRotation(targetRot);
    16.                 _lastRot = Quaternion.Slerp(_lastRot, targetRot, _owner._lookSlerpSpeed);
    17.            
    18.                 //set
    19.                 _owner.transform.localRotation = _lastRot;
    20.  
    Personally I use Quaternions for everything as I find the logic cleaner.

    Now the model in question has an upside down head, exactly how you describe in your post about the 'Ethan' character.

    head_orientation.png

    Well in that code the only part of it that compensates is this bit right here:

    Code (csharp):
    1. * Quaternion.Euler(0f, -90f, -90f);
    Note, I have it hardcoded because our asset pipeline is consistent. ALL models coming in are going to have this same compensation requirement.

    But this value is easily calculated from a custom forward/up configuration like you have. That value is specifically:

    Code (csharp):
    1. Quaternion.Inverse(Quaternion.LookRotation(Vector3.up, -Vector3.right))
    Which makes sense, to get our lookat offset, we need to redact (inverse) the offset rotation that looks in the new custom direction where y-axis (up) is forward, and inverse-x-axis (-right) is up.

    The rest of the math is exactly the same as if the head was oriented in unity default y-up/z-forward system. That math being specifically:
    Code (csharp):
    1. Quaternion.LookRotation(dir)
    The rest of that junk is just clamping and slerping, compensating for the fact that I have to manually store the previous rotation since the other animations playing on the model will overwrite it, causing the slerp to not work. If I had done this:
    Code (csharp):
    1. _owner.transform.localRotation = Quaternion.Slerp(_owner.transform.localRotation, targetRot, _owner._lookSlerpSpeed);
    We'd never actually slerp, since localRotation would revert back to the regular walk animations orientation next frame.

    ...

    Anyways, I bring this up... because technically speaking, this is what it appears your code is doing (minus the radial clamping and slerping).

    Basically meaning that your code could be done with something along the lines of:

    Code (csharp):
    1.  
    2. //probably do this in the Awake method, so you're not repeatedly calculating the same thing
    3. _orientation = Quaternion.LookRotation(AxisForward, AxisUp);
    4. _offset = Quaternion.Inverse(_orientation);
    5.  
    6. //then in your UpdatePosition:
    7. Head.localRotation = Quaternion.Euler(0f, heading, -elevation) * _offset;
    8.  
    9. Transform.position = Pivot;
    10. if (Mathf.Abs(elevation) > 89)
    11.     Transform.LookAt(FocalPoint, Transform.up);
    12. else
    13.     Transform.LookAt(FocalPoint);
    14.  
    (wasn't sure why you had the 'Transform.rotation = ...LookRot...' stuff, since the LookAt would just overwrite anything that line did)

    Or at least, in theory that should work. I think I read your code correctly.

    Basically your:
    Code (csharp):
    1. Head.localRotation = Quaternion.Euler(0f, heading, -elevation) * _offset;
    Is me:
    Code (csharp):
    1. var targetRot = Quaternion.LookRotation(dir) * Quaternion.Euler(0f, -90f, -90f);
    You just set it locally, and I set it globally (well inverted globally to local, which is globally).

    ...

    Now why I bring this up over your solution is that your solution requires not only requires converting your 'Axis' enum to the appropriate axis.

    But it also requires special 'Set' methods to adjust the appropriate euler angle.

    Thing is... what if you need to 'Get'? Or 'Increment'? Or anything like that? You're going to end up with a lot of these helper methods (at the bare minimum the Get/Set methods, and constantly having to convert back and forth).

    Where as just using this offset quaternion you can do all your math as if you had correct unity orientation... and then just offset it in the end.

    Same way you would a vector.

    Because Quaternions are the rotation equivalent of a Vector. It's why we like to use them for rotation over euler angles when doing math. Euler angles just don't have the versatility.

    And sure you might be thinking... yeah, cause euler angles have gimbal lock. And I'd say... nope. I mean yes, they have gimbal lock, but that's the result of the fact that euler angles aren't vectors.

    Yes, yes, yes... I know we store them in a vector data type. But that's just because euler angles is a grouping of 3 floats, Vector3 fits that. Why create a separate data type?

    But the arithmetic, doesn't actually work out. A dot product, or subtraction, or what not of a Vector3 of euler angles doesn't create consecutive mathematical properties. Quaternions on the other hand do.

    And this is because vectors are supposed to store direction and magnitude.

    You might think that <90,0,0> - <30,0,0> results in a significant value that has direction and magnitude... <60,0,0>, yeah 60 degrees around x axis from <30,0,0> DOES give you <90,0,0>. But it doesn't result in consistent motion, especially once you start rotation around multiple axes at the same time.

    To see what I mean. The euler rotations <90,0,-40> and <90,40,0> are identical. Give it a try, put 2 gameobject in a scene and set them to each of those values... they'll be oriented identically.

    If you have 2 values that are equal, subtracting them should result in a magnitude of 0, because there's no change to get from one to the other.

    But you result in <0,40,-40>. Clearly a non-zero difference.

    Where as if you calculate the Quaternion difference:
    Code (csharp):
    1.  
    2. var a = Quaternion.Euler(90f, -40f, 0f);
    3. var b = Quaternion.Euler(90f, 0, 40f);
    4. //this is how you perform quaternion subtraction, just like how * does append, * inverse is subtract
    5. //just like our 'offset' before was the inverse of our look rotation, cause you subtract the look rotation
    6. var c = a * Quaternion.Inverse(b);
    7. Debug.Log(c); //prints <0,0,0,1>, the identity quaternion... the quaternion equivalent of 0
    8.  
    And this is why euler angles are not vectors, technically. And Quaternions are.
     
    Last edited: May 18, 2017
  11. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    @lordofduct Wow... that's a really detailed and informative explanation, thanks very much! And I think the girl looks pretty cute too :)

    So, it seems that it's true there's no out-of-the-box support for such a functionality in Unity (probably I should create a feature request later) so the approach with the user properties would be the way to go, but I should find a better way to calculate the offset rotation than my initial attempt with Axis enum.

    After I wrote that sample code, I realized that I made a mistake and used a wrong axis. But as you warned, it'd be quite messy if I were to add all the methods I possibly need to calculate them right (i.e. .Dot) to the enum, so I better find a cleaner way which just result in a Quaternion as an offset. I haven't been able to spend much time with the approach - urg... I really, really wish it was my full time job! :oops: - but I'm going to try that this weekend.

    As to Quaternion, by the way, now I can see better why I should be using it over Vector in dealing with rotation, thanks to your help. Especially, your example with 90/-40 degrees rotation made me suspect if it could be the reason why I needed to write that weird 'if (MaIthf.Abs(elevation) > 89)' check to make my tests pass.

    Regarding the consecutive setting of rotation property and invocation of LookAt method, it was my understanding that only using the latter could potentially result in deviations in roll axis, because when the code calls LookAt on the camera transform, with a certain target as an argument, the camera will rotate to point at the target, so changing its pitch and yaw, but not its roll. If I'm mistaken, please let me know.

    Again, thanks much for the answer, and I can't wait for the weekend so I can try your suggestions :)
     
    lordofduct likes this.
  12. mysticfall

    mysticfall

    Joined:
    Aug 9, 2016
    Posts:
    649
    I think I have a working solution now. The relevant code is shown blow:
    Code (CSharp):
    1. protected virtual void UpdatePosition(float heading, float elevation)
    2. {
    3.     Head.localRotation = Quaternion.identity;
    4.  
    5.     Head.Rotate(AxisUp, heading, Space.World);
    6.     Head.Rotate(AxisRight, -elevation, Space.World);
    7.  
    8.     Transform.position = Pivot;
    9.     Transform.rotation = Quaternion.LookRotation(AxisForward, AxisUp);
    10.  
    11.     Transform.LookAt(FocalPoint);
    12. }
    (I only removed the 'Set' method from Axis enum, as it's not necessary anymore.)

    I could not use @lordofduct 's example though, because I found that my case is a bit different from his own upon closer examination.

    We start from a fixed position as target of the character's gaze, in his example, and the direction from the head to the target is not affected by the former's orientation. In that case, rotating the head to the direction and compensate for the rotational offset as suggested by him would make perfect sense.

    But in my case, due to my specific design, we start from relative target angles for the head, which are indeed affected by the latter's orientation. As such, we cannot follow the same approach as before because it would lead to an incorrect result.

    If we project the relative target angles to a fixed position first, then probably we wouldn't have such a problem. But it's a bit roundabout way to solve it in my case, and the conversion would require a bit more codes, since it needs to take into account of the problem with the orientation. So I ended up with the code I included above.

    Again, @lordofduct 's solution would work perfectly fine with the case when you need to turn a character's head to a global position. And I could learn a lot about Quarternion vs Vector from his example, and was able to remove an ugly hack I had to resort to before. So thanks again! :)