Whats the best way to keep an objects rotation set to face between 4 points?

So I’m trying to implement a climbing system and I want the players body to rotate based on where their hands and feet are so they can move semi naturally over uneven walls.


(Blue cubes are hands/feet, green is the center of them, purple is a raycast from the players origin point)

What would be the best way to rotate the player based on these 4 points? I’ve tried a few approaches but mostly I’ve only managed to get the character to spin like a top…

void calculateNormals(CharacterController cc)
    {
        normal2 = Vector3.zero;

        RaycastHit hit = new RaycastHit();

        if (Physics.Raycast(new Ray(rightHand.transform.position, cc.transform.forward), out hit,5))
            normal2 += normRHand = hit.point;
        if (Physics.Raycast(new Ray(leftHand.transform.position, cc.transform.forward), out hit, 5))
            normal2 += normLHand = hit.point;
        if (Physics.Raycast(new Ray(rightFoot.transform.position, cc.transform.forward), out hit,5))
            normal2 += normRFoot = hit.point;
        if (Physics.Raycast(new Ray(leftFoot.transform.position, cc.transform.forward), out hit, 5))
            normal2 += normLFoot = hit.point;

        normal2 /= 4;

        cc.transform.LookAt(normal2);
}

Here’s my latest attempt.


Not quite the outcome I was looking for…

You can set them as weighted targets by the character’s inverse kinematics system. Make sure the character rig is set up as humanoid rather than generic. From there going into it’s animation controller and make sure the Base Layer has IK Pass ticked (the little settings button next to Base Layer). From there you can modify the weights of the characters arms, legs, and head based from their targets (these are external Transform components in the world, like your 4 blue cubes). Here’s an example of me doing it:

using UnityEngine;

    /// <summary>
    /// Attach this class to a humanoid character with an Animator to make the arms, legs, and head to follow individual Transform targets.
    /// Make sure the referenced AnimatorController in the Animator Component allows IK to edit weights. You can turn on and off via the <see cref="UpdateIK(bool)"/> method.
    /// You can assign this character to a new VR target via the <see cref="SwitchWearables"/> class, by it calling
    /// <see cref="SetIKTarget(Transform, Transform, Transform, Transform, Transform, Transform)"/> on this class.
    /// </summary>
    public class VRIK : BaseFramework
    {
        [SerializeField] private VRIK_Hand leftHand;
        [SerializeField] private VRIK_Hand rightHand;
        [SerializeField] private Animator animator;
        [SerializeField] private Mesh mesh;
        [SerializeField] private Transform leftHandTarget;
        [SerializeField] private Transform rightHandTarget;
        [SerializeField] private Transform rightFootTarget;
        [SerializeField] private Transform leftFootTarget;
        [SerializeField] private Transform headTarget;
        [SerializeField] private Transform hipTarget;
        [SerializeField] private Vector3 hipOffsetAngle = new Vector3(0f, 90f, 0f);
        [SerializeField] private float lookWeight = 1f;
        [SerializeField] private float bodyWeight = 0.25f;
        [SerializeField] private float headWeight = 0.9f;
        [SerializeField] private float eyesWeight = 1f;
        [SerializeField] private float clampWeight = 1f;

        /// <summary>
        /// In metres.
        /// </summary>
        [SerializeField, Tooltip("In meters.")] private float characterHeight = 1.61544f;
        [SerializeField] private float deltaHeadHeight = 1.61544f;

        [SerializeField] private float lowerLeg;
        [SerializeField] private float upperLeg;
        [SerializeField] private bool updateIKTargets = true;

        private bool setCharacterSize = false;
      
        public bool IsIKActive
        {
            get
            {
                return updateIKTargets;
            }
        }
      
        public bool LeftHandGripping
        {
            get
            {
                return leftHand.IsGripping;
            }
        }

        public bool RightHandGripping
        {
            get
            {
                return rightHand.IsGripping;
            }
        }

        public void UpdateIK(bool shouldUpdate)
        {
            this.updateIKTargets = shouldUpdate;
        }

        public void SetIKTarget(Transform leftHandTarget, Transform leftFootTarget, Transform rightHandTarget, Transform rightFootTarget, Transform headTarget, Transform hipTarget)
        {
            if(leftHandTarget != null && leftFootTarget != null && rightHandTarget != null && rightFootTarget != null && headTarget != null && hipTarget != null)
            {
                this.leftHandTarget = leftHandTarget;
                this.leftFootTarget = leftFootTarget;
                this.rightHandTarget = rightHandTarget;
                this.rightFootTarget = rightFootTarget;
                this.headTarget = headTarget;
                this.hipTarget = hipTarget;

                // Can manually set this class as ready
                Ready = true;
            }
        }

        protected override bool Initialise()
        {
            // Debug only
            //PrintMuscleIndex();
          
            leftHand.Setup(this, animator);
            rightHand.Setup(this, animator);

            return leftHandTarget != null && leftFootTarget != null && rightHandTarget != null && rightFootTarget != null ? base.Initialise() : false;
        }

        // Updates every frame, just before IK is sent to the Animator as a manipulator to it's current animation playing.
        private void OnAnimatorIK()
        {
            if (Ready && updateIKTargets)
            {
                // Only need to set the characters size at the beginning of IK checks
                if (!setCharacterSize)
                {
                    SetCharacterSize(characterHeight);
                }

                // Set right hand to follow target this frame
                SetLimbIK(AvatarIKGoal.LeftHand, 1, leftHandTarget);

                // Set left hand to follow target this frame
                SetLimbIK(AvatarIKGoal.RightHand, 1, rightHandTarget);

                // Set right foot to follow target this frame
                SetLimbIK(AvatarIKGoal.RightFoot, 1, rightFootTarget);

                // Set left foot to follow target this frame
                SetLimbIK(AvatarIKGoal.LeftFoot, 1, leftFootTarget);

                // Get the head to follow the view of the headset
                SetHeadIK();
            }
        }

        private void Update()
        {
            if (Ready && updateIKTargets)
            {
                // Sets the overall position of the character as the player walks around their play area without teleporting.
                SetCharacterPosition();
            }
        }

        private void SetLimbIK(AvatarIKGoal limb, float weight, Transform target)
        {
            animator.SetIKPositionWeight(limb, weight);
            animator.SetIKRotationWeight(limb, weight);
            animator.SetIKPosition(limb, target.position);
            animator.SetIKRotation(limb, target.rotation);
        }

        /// <summary>
        /// Sets the head to face the direction of the player. Not currently working.
        /// </summary>
        private void SetHeadIK()
        {
            animator.SetLookAtWeight(lookWeight, bodyWeight, headWeight, eyesWeight, clampWeight);
            animator.SetLookAtPosition(headTarget.TransformDirection(Vector3.forward));
        }

        private void SetCharacterPosition()
        {
            // Get raw transform data.
            Vector3 standingDirection = transform.parent.InverseTransformDirection(hipTarget.eulerAngles);
            Vector3 standingPoint = transform.parent.InverseTransformPoint(hipTarget.position);
            Vector3 headPoint = transform.parent.InverseTransformPoint(headTarget.position);

            deltaHeadHeight = headPoint.y;

            // Remove transform elements not relevant.
            standingPoint.y = deltaHeadHeight - characterHeight;
            standingDirection.x = 0f;
            standingDirection.z = 0f;

            // Set character transform.
            transform.localPosition = standingPoint;
            transform.localRotation = Quaternion.Euler(standingDirection + hipOffsetAngle);

            SetFeetPosition();
        }
      
        /// <summary>
        /// Sets each foot position. Calculated as an offset from center along the X axis plane.
        /// </summary>
        private void SetFeetPosition()
        {
            // Set their base height and position
            Vector3 leftRootPos = transform.TransformPoint(new Vector3(-0.15f, 0f, 0f));
            Vector3 rightRootPos = transform.TransformPoint(new Vector3(0.15f, 0f, 0f));

            // Set to left.
            leftFootTarget.position = leftRootPos;
            leftFootTarget.rotation = Quaternion.Euler(transform.eulerAngles);

            // Set to right.
            rightFootTarget.position = rightRootPos;
            rightFootTarget.rotation = Quaternion.Euler(transform.eulerAngles);
        }

        /// <summary>
        /// Changes this characters overall scale so model fits intended height.
        /// </summary>
        /// <param name="heightInMeters">The intended height in meters you want the character to be.</param>
        public void SetCharacterSize(float heightInMeters)
        {
            if(characterHeight != heightInMeters)
            {
                characterHeight = heightInMeters;
            }
          
            transform.localScale = Vector3.one * (heightInMeters / mesh.bounds.size.y);
            setCharacterSize = true;
        }

        /// <summary>
        /// Muscle name and index lookup (See Debug.Log). Call this somewhere at the beginning to get a reference for all the difference muscles in this humanoid.
        /// </summary>
        public void PrintMuscleIndex()
        {
            string[] muscleName = HumanTrait.MuscleName;
            int i = 0;
            while (i < HumanTrait.MuscleCount)
            {
                Debug.LogFormat("{0}: {1} min: {2}, max: {3}", i, muscleName[i], HumanTrait.GetMuscleDefaultMin(i), HumanTrait.GetMuscleDefaultMax(i));
                i++;
            }
        }

And here’s the Unity manual on it: Unity - Manual: Inverse Kinematics

Getting the hands and feet into position isn’t the issue, it’s making the character rotate based on the hands and feet.

I’ve made a bit of progress using
cc.transform.rotation = Quaternion.FromToRotation(-Vector3.forward, hit.normal);
which works pretty good except when climbing on a wall with a normal of (0, 0, 1) which causes the character to flip upside down.