Np! They’re great, but not as straightforward as vectors. I had to solve a few nasty hand alignment issues of my own with Mecanim and IK stuff, but I don’t remember a lot of the solution. It seems like your issue is more straightforward with simply adding the calculated delta rotation between two points. Here’s the whole code I wrote on finding rotations for IK if you feel adventurous.
// (c) Copyright Cleverous 2015. All rights reserved.
using System.Collections;
using UnityEngine;
namespace Deftly
{
public class IkProxy : MonoBehaviour
{
// Subject Information
public Transform SubjectTr;
public Subject Subject;
protected Animator Animator;
protected bool UseLeftIk;
protected bool UseRightIk;
protected int RightHandLayer;
protected int LeftHandLayer;
protected float CharScaleMultiplier;
// Weapon Information
protected Weapon _weapon;
protected Transform _weaponTransform;
protected WeaponType _weaponType;
protected MountPivot _weaponPivot;
protected Transform _weaponOriginalNonDomGoal;
protected Hand DominantHand = Hand.Right;
protected bool _cantProcessIk;
protected bool _doNewRecoil;
protected Vector3 _localRecoil;
protected float _recoilTimer;
protected float _recoilRng1;
protected float _recoilRng2;
protected bool _transitioning;
protected GameObject _nonDomWorldTarget;
protected virtual void CreateNonDomWorldTarget()
{
_nonDomWorldTarget = new GameObject
{
hideFlags = HideFlags.HideInHierarchy,
name = "_NonDomHandTarget"
};
}
protected virtual void Awake()
{
Animator = GetComponent<Animator>();
}
protected virtual void OnEnable()
{
StartCoroutine(Recoil());
CreateNonDomWorldTarget();
}
protected virtual void OnDestroy()
{
Destroy(_nonDomWorldTarget); // cleanup
}
protected virtual void Start()
{
CharScaleMultiplier = Subject.Stats.CharacterScale;
Subject.OnReload += DoReload;
Subject.OnFire += DoRecoil;
}
protected virtual void DoRecoil(Subject x)
{
_recoilTimer = 0;
_doNewRecoil = true;
}
protected virtual IEnumerator Recoil()
{
while (true)
{
if (_doNewRecoil)
{
if (_recoilTimer < _weapon.Stats.TimeToCooldown)
{
_recoilTimer += Time.deltaTime;
_localRecoil = _weapon.Stats.Recoil * _weapon.Stats.RecoilCurve.Evaluate(_recoilTimer / _weapon.Stats.TimeToCooldown);
}
else
{
_recoilRng1 = Random.Range(-_weapon.Stats.RecoilChaos, 1 * _weapon.Stats.RecoilChaos);
_recoilRng2 = Random.Range(-_weapon.Stats.RecoilChaos, 1 * _weapon.Stats.RecoilChaos);
_doNewRecoil = false;
}
}
yield return null;
}
}
public virtual void UseLeftHandIk(bool status) { UseLeftIk = status; }
public virtual void UseRightHandIk(bool status) { UseRightIk = status; }
public virtual void DisableHandIk()
{
UseLeftIk = false;
UseRightIk = false;
}
public virtual void EnableHandIk()
{
UseLeftIk = true;
UseRightIk = true;
}
public virtual void ResetHandIk()
{
UseLeftIk = Subject.Stats.UseLeftHandIk;
UseRightIk = Subject.Stats.UseRightHandIk;
}
public virtual void SetupWeapon(GameObject weapon)
{
// TODO blend IK into position
// Cache relevant stuff
_weaponTransform = weapon.transform;
_weapon = _weaponTransform.GetComponent<Weapon>();
_weaponType = _weapon.Stats.WeaponType;
_weaponPivot = _weapon.Stats.MountPivot;
_weaponOriginalNonDomGoal = _weapon.Stats.NonDominantHandGoal;
if (_weaponOriginalNonDomGoal && _nonDomWorldTarget == null) { CreateNonDomWorldTarget(); }
// Set Weapon Parent
_weaponTransform.SetParent(GetRightHandBone());
// Set Weapon IN HAND Position and IN HAND Rotation
_weaponTransform.localPosition = Vector3.zero + SubjectTr.TransformVector(Subject.WeaponPositionInHandCorrection);
_weaponTransform.rotation = FindWeaponOrientation();
// Turn on/off ik per the Subject
ResetHandIk();
// Get Layer Information
LeftHandLayer = Subject.LeftHandIkLayer;
RightHandLayer = Subject.RightHandIkLayer;
// Tell the Animator its Type ID
if (Subject.ControlStats.AnimatorWeaponType != "") Animator.SetInteger(Subject.ControlStats.AnimatorWeaponType, _weapon.Stats.TypeId);
// Fire the event to do the swap animation.
DoSwitchedWeapon();
}
public virtual void DoReload() { StartCoroutine(ReloadTransition()); }
public virtual void DoSwitchedWeapon() { StartCoroutine(WeaponTransition()); }
public virtual IEnumerator ReloadTransition()
{
if (Subject.LogDebug) Debug.Log("Reloading.");
_transitioning = true;
if (Subject.ControlStats.AnimatorReload != "") Animator.SetBool(Subject.ControlStats.AnimatorReload, true);
yield return new WaitForSeconds(_weapon.Stats.ReloadTime);
if (Subject.ControlStats.AnimatorReload != "") Animator.SetBool(Subject.ControlStats.AnimatorReload, false);
_transitioning = false;
if (Subject.LogDebug) Debug.Log("Done Reloading.");
}
public virtual IEnumerator WeaponTransition()
{
if (Subject.LogDebug) Debug.Log("Swapping.");
_transitioning = true;
if (Subject.ControlStats.AnimatorSwap != "") Animator.SetBool(Subject.ControlStats.AnimatorSwap, true);
yield return new WaitForSeconds(_weapon.Stats.SwapTime);
if (Subject.ControlStats.AnimatorSwap != "") Animator.SetBool(Subject.ControlStats.AnimatorSwap, false);
_transitioning = false;
if (Subject.LogDebug) Debug.Log("Done Swapping.");
}
protected virtual void Update()
{
_cantProcessIk = !_weapon || Subject.IsDead || _transitioning || _weaponType == WeaponType.Melee;
if (_cantProcessIk) return;
if (UseLeftIk && _weaponOriginalNonDomGoal != null)
{
_nonDomWorldTarget.transform.position = _weaponOriginalNonDomGoal.position;
_nonDomWorldTarget.transform.rotation = _weaponOriginalNonDomGoal.rotation;
}
}
protected virtual void OnAnimatorIK(int layerIndex)
{
if (_cantProcessIk) return;
UseLeftIk = Subject.Stats.UseLeftHandIk;
UseRightIk = Subject.Stats.UseRightHandIk;
// _weaponTransform.rotation = FindWeaponOrientation();
if (layerIndex == RightHandLayer && UseRightIk) ApplyRightIk();
if (layerIndex == LeftHandLayer && UseLeftIk && _weaponOriginalNonDomGoal != null) ApplyLeftIk();
}
protected virtual void ApplyRightIk()
{
SetIkPositionWeight(AvatarIKGoal.RightHand, 1);
SetIkPosition(AvatarIKGoal.RightHand, FindDominantHandPosition());
if (_weapon.Stats.UseElbowHintR)
{
SetIkHintWeight(AvatarIKHint.RightElbow, 1);
SetIkHintPosition(AvatarIKHint.RightElbow, FindDominantElbowHintPosition());
}
}
protected virtual void ApplyLeftIk()
{
SetIkPositionWeight(AvatarIKGoal.LeftHand, 1);
SetIkRotationWeight(AvatarIKGoal.LeftHand, 1);
SetIkPosition(AvatarIKGoal.LeftHand, FindNonDominantHandPosition());
SetIkRotation(AvatarIKGoal.LeftHand, FindNonDominantHandRotation());
if (_weapon.Stats.UseElbowHintL)
{
SetIkHintWeight(AvatarIKHint.LeftElbow, 1);
SetIkHintPosition(AvatarIKHint.LeftElbow, FindNonDominantElbowHintPosition());
}
}
protected virtual void LateUpdate()
{
if (_cantProcessIk) return;
// Fix the broken rotation
if (UseRightIk) GetRightHandBone().rotation = FindDominantHandRotation();
// cache the correct position non-dom goal data
if (_nonDomWorldTarget && _weaponOriginalNonDomGoal)
{
_nonDomWorldTarget.transform.position = _weaponOriginalNonDomGoal.position;
_nonDomWorldTarget.transform.rotation = _weaponOriginalNonDomGoal.rotation;
}
// show any debugs
if (Subject.ShowGunDebug)
{
Debug.DrawRay(GetRightHandBone().position, Subject.InvertHandForward ? -GetRightHandBone().right*2 : GetRightHandBone().right, Color.red);
Debug.DrawRay(GetRightHandBone().position, SubjectTr.forward * 0.5f, Color.green);
}
}
protected virtual Quaternion FindWeaponOrientation()
{
Transform bone = GetRightHandBone();
return Quaternion.LookRotation(
bone.TransformDirection(Vector3.Cross(Subject.ThumbDirection, Subject.PalmDirection)),
bone.TransformDirection(DominantHand == Hand.Right ? Subject.ThumbDirection : -Subject.ThumbDirection));
/*
return Quaternion.LookRotation(
DominantHand == Hand.Right && !Subject.InvertHandForward ? bone.right : -bone.right,
bone.TransformDirection(DominantHand == Hand.Right ? Subject.ThumbDirection : -Subject.ThumbDirection));
*/
}
protected virtual Vector3 FindDominantHandPosition()
{
Vector3 a = _weaponPivot == MountPivot.LowerSpine ? FindSpinePosition() : FindShoulderPosition();
Vector3 b = SubjectTr.TransformVector(_weapon.Stats.PositionOffset + Subject.DominantHandPosCorrection + new Vector3(0, _localRecoil.y, _localRecoil.z) * CharScaleMultiplier);
return a + b;
}
protected virtual Quaternion FindDominantHandRotation()
{
return Subject.DominantHandRotCorrection == Vector3.zero
? Quaternion.identity
: Quaternion.LookRotation(SubjectTr.forward) * Quaternion.Euler(Subject.DominantHandRotCorrection) * Quaternion.Euler(new Vector3(_localRecoil.x * _recoilRng1, _localRecoil.x, -_localRecoil.x * _recoilRng2));
}
protected virtual Vector3 FindDominantElbowHintPosition()
{
Vector3 pos = Animator.GetBoneTransform(HumanBodyBones.RightUpperArm).position + SubjectTr.TransformVector(_weapon.Stats.DominantElbowOffset * CharScaleMultiplier);
#if UNITY_EDITOR
Debug.DrawRay(pos, Vector3.up * 0.1f, Color.green);
Debug.DrawRay(pos, Vector3.left * 0.1f, Color.green);
Debug.DrawRay(pos, Vector3.right * 0.1f, Color.green);
Debug.DrawRay(pos, Vector3.down * 0.1f, Color.green);
#endif
return pos;
}
protected virtual Vector3 FindNonDominantHandPosition()
{
// The original goal will always be wrong until UT fixes the SetIKRotation() bug.
// The Internal Animation pass is done and Mecanim gets the rotation wrong every time on the right hand. (which breaks the left hand)
// I correct the rotation manually in LateUpdate() and cache the position for the LH Goal while its correct.
// I use that goal here. So everything is 1 frame behind for the left hand.
return _nonDomWorldTarget.transform.position + SubjectTr.InverseTransformVector(Subject.NonDominantHandPosCorrection * CharScaleMultiplier);
/*
Transform goal = CurrentWeapon.Stats.NonDominantHandGoal;
if (goal)
{
//if (_nonDomCached)
return goal.position; // + SubjectTr.TransformVector(Subject.NonDominantHandPosCorrection);
}
return CurrentWeaponTr.position + CurrentWeapon.Stats.NonDominantHandPos;
*/
}
protected virtual Quaternion FindNonDominantHandRotation()
{
return _nonDomWorldTarget.transform.rotation * Quaternion.Euler(Subject.NonDominantHandRotCorrection);
/*
Transform goal = CurrentWeapon.Stats.NonDominantHandGoal;
if (goal)
{
//if (_nonDomCached)
return Quaternion.LookRotation(goal.forward, goal.up) * Quaternion.Euler(Subject.NonDominantHandRotCorrection);
}
Quaternion a = CurrentWeaponTr.rotation;
Quaternion b = Quaternion.Euler(CurrentWeapon.Stats.NonDominantHandRot);
return a * b;
*/
}
protected virtual Vector3 FindNonDominantElbowHintPosition()
{
Vector3 pos = Animator.GetBoneTransform(HumanBodyBones.LeftUpperArm).position + SubjectTr.TransformVector(_weapon.Stats.NonDominantElbowOffset * CharScaleMultiplier);
#if UNITY_EDITOR
Debug.DrawRay(pos, Vector3.up * 0.1f, Color.magenta);
Debug.DrawRay(pos, Vector3.left * 0.1f, Color.magenta);
Debug.DrawRay(pos, Vector3.right * 0.1f, Color.magenta);
Debug.DrawRay(pos, Vector3.down * 0.1f, Color.magenta);
#endif
return pos;
}
protected virtual Vector3 FindSpinePosition() { return Animator.GetBoneTransform(HumanBodyBones.Spine).position; }
protected virtual Vector3 FindShoulderPosition() { return Animator.GetBoneTransform(HumanBodyBones.RightUpperArm).position; }
protected virtual Transform GetRightHandBone() { return Animator.GetBoneTransform(HumanBodyBones.RightHand); }
protected virtual Transform GetLeftHandBone() { return Animator.GetBoneTransform(HumanBodyBones.LeftHand); }
protected static Quaternion GetDelta(Quaternion targetRotation, Quaternion currentRotation)
{
return targetRotation*Quaternion.Inverse(currentRotation);
}
// You can call these, but Mecanim requires it to come from OnAnimatorIK()
// They'll be overidden anyway, so I need some sort of queue system for handling overrides that is analyzed during OnAnimatorIK()
// The issue with Mecanim's rotation errors is holding back proper implementation of this as well.
public virtual void SetIkPositionWeight(AvatarIKGoal armature, float weight) { Animator.SetIKPositionWeight(armature, weight); }
public virtual void SetIkRotationWeight(AvatarIKGoal armature, float weight) { Animator.SetIKRotationWeight(armature, weight); }
public virtual void SetIkPosition(AvatarIKGoal armature, Vector3 position) { Animator.SetIKPosition(armature, position); }
public virtual void SetIkRotation(AvatarIKGoal armature, Quaternion rotation) { Animator.SetIKRotation(armature, rotation); }
public virtual void SetIkHintPosition(AvatarIKHint hint, Vector3 position) { Animator.SetIKHintPosition(hint, position);}
public virtual void SetIkHintWeight(AvatarIKHint hint, float weight) { Animator.SetIKHintPositionWeight(hint, weight);}
}
}