Workaround for Quaternion.eulerAngles 360 Degree Limit?

Hi guys,

I have a camera follow script that uses swipes to rotate the camera when the player is stationary, and to turn the character when the player is in motion (similar to the control scheme in Lily) while consistently following the character from behind. It uses a boolean value to check whether the player is in motion or not, and also has two members for its ideal pitch and yaw. Here are snippets of the relevant code:

...

	Transform targetTransform;

    /// Camera transform
    Transform thisTransform;

...

    float yaw = 0;
    float pitch = 0;

...

    float idealYaw = 0;
    float idealPitch = 0;

...
	
	bool inMotion = false;

    public float Yaw
    {
        get { return yaw; }
    }

    public float IdealYaw
    {
        get { return idealYaw; }
        set { idealYaw = value; }
    }

    public float Pitch
    {
        get { return pitch; }
    }

    public float IdealPitch
    {
        get { return idealPitch; }
        set { idealPitch = clampPitchAngle ? ClampAngle( value, minPitch, maxPitch ) : value; }
    }
...

void Apply()
    {
		...
		
		// Follow the player when the player is in motion
		if (inMotion)
		{
			...
		}
		
		// Actually start moving the camera
		
        distance = Mathf.Lerp( distance, targetZoom, Time.deltaTime * smoothZoomSpeed );
        yaw = Mathf.Lerp( yaw, IdealYaw, Time.deltaTime * smoothOrbitSpeed );
        pitch = Mathf.LerpAngle( pitch, IdealPitch, Time.deltaTime * smoothOrbitSpeed );
		
        transform.rotation = Quaternion.Euler( pitch, yaw, 0 );
        transform.position = ( targetTransform.position + panOffset ) - distance * thisTransform.forward;
    }
	
...

    void LateUpdate()
    {
        Apply();
    }

...

Because Quaternion.eulerAngles resets each member angle to 0 whenever it exceeds 360 degrees in either direction, the camera suddenly jerks around the character and starts the rotation process again when turning for too long a period of time (blame the gimbal lock problem). Is there any way to allow the camera to rotate more than 360 degrees in a given direction, such as by taking a value from Quaternion transformation and somehow converting it into degrees? There’s obviously some stuff missing if I change the if (inMotion) block of Apply() to read like this:

if (inMotion)
		{
			// Set IdealPitch and IdealYaw relatie to the target, always facing the direction of travel.
			
			// TODO: Fix euler angle rotation, it currently resets to 0 when 360 degrees of roytation are exceeded.
			IdealYaw = targetTransform.rotation.y;
			IdealPitch = targetTransform.eulerAngles.z + minPitch;
			
			Debug.Log("Ideal Yaw Value: " + IdealYaw);
		}

The above code sets the IdealYaw to rotation.y of targetTransform, and it results in IdealYaw being set to values between -1 and 1. Therefore, some sort of work must be done in the middle to change those values into degrees, in a manner that bypasses the gimbal lock problem. Any suggestions?

MachCUBED

EDIT: Here is the entire class file (code adapted from FingerGestures TBToolbox by FatalFrog):

using UnityEngine;
    using System.Collections.Generic;
    
    public class CameraFollowAndOrbit : TouchControlScript {

        /// Initial camera distance to target
        public float initialDistance = 5.0f;

        /// Minimum distance between camera and target
        public float minDistance = 1.0f;

        /// Maximum distance between camera and target
        public float maxDistance = 20.0f;

        /// Affects horizontal rotation speed
        public float yawSensitivity = 80.0f;
    
        /// Affects vertical rotation speed
        public float pitchSensitivity = 80.0f;
    
        /// Keep pitch angle value between minPitch and maxPitch?
        public bool clampPitchAngle = true;
        public float minPitch = 5;
        public float maxPitch = 80;

        /// Allow the user to affect the orbit distance using the pinch zoom gesture
        public bool allowPinchZoom = true;
    
        /// Affects pinch zoom speed
        public float pinchZoomSensitivity = 2.0f;

        /// Use smooth camera motions?
        public float smoothZoomSpeed = 3.0f;
        public float smoothOrbitSpeed = 4.0f;
    	
        /// Damp time for turning the player character when moving
    	public float directionDampTime = 0.25f;

        /// Handles swipe turning sensitivity
    	public float turningFactor = 5.0f;
    	
        /// The animator of the player object...
    	protected Animator animator;
    	
        /// The object to orbit around
        public GameObject target;
    	Transform targetTransform;
    	
        /// Camera transform
        Transform thisTransform;
    
        float distance = 10.0f;
        float yaw = 0;
        float pitch = 0;
    
        float idealDistance = 0;
        float idealYaw = 0;
        float idealPitch = 0;
    
        Vector3 idealPanOffset = Vector3.zero;
        Vector3 panOffset = Vector3.zero;
    	Vector3 targetRotationAngles;
    	
    	bool inMotion = false;
    
        public float Distance
        {
            get { return distance; }
        }
    
        public float IdealDistance
        {
            get { return idealDistance; }
            set { idealDistance = Mathf.Clamp( value, minDistance, maxDistance ); }
        }
    
        public float Yaw
        {
            get { return yaw; }
        }
    
        public float IdealYaw
        {
            get { return idealYaw; }
            set { idealYaw = value; }
        }
    
        public float Pitch
        {
            get { return pitch; }
        }
    
        public float IdealPitch
        {
            get { return idealPitch; }
            set { idealPitch = clampPitchAngle ? ClampAngle( value, minPitch, maxPitch ) : value; }
        }
    
        public Vector3 IdealPanOffset
        {
            get { return idealPanOffset; }
            set { idealPanOffset = value; }
        }
    
        public Vector3 PanOffset
        {
            get { return panOffset; }
        }
    
        void InstallGestureRecognizers()
        {
            List<GestureRecognizer> recogniers = new List<GestureRecognizer>( GetComponents<GestureRecognizer>() );
            DragRecognizer drag = recogniers.Find( r => r.EventMessageName == "OnDrag" ) as DragRecognizer;
            PinchRecognizer pinch = recogniers.Find( r => r.EventMessageName == "OnPinch" ) as PinchRecognizer;
    
            if( !drag )
            {
                drag = gameObject.AddComponent<DragRecognizer>();
                drag.RequiredFingerCount = 1;
                drag.IsExclusive = true;
                drag.MaxSimultaneousGestures = 1;
                drag.SendMessageToSelection = GestureRecognizer.SelectionType.None;
            }
    
            if( !pinch )
                pinch = gameObject.AddComponent<PinchRecognizer>();
        }
    	
    	void SetupTargetAndTransforms()
    	{
    		thisTransform = transform;
    		
    		if (!target)
    			target = GameObject.FindGameObjectWithTag("Player");
    		
    		targetTransform = target.transform;
    		
    		targetRotationAngles = targetTransform.eulerAngles;
    		
    		animator = target.GetComponent<Animator>();
    	}
    	
    	void SetupCamera()
    	{
    		Vector3 angles = thisTransform.eulerAngles;
    
            distance = IdealDistance = initialDistance;
            yaw = IdealYaw = angles.y;
            pitch = IdealPitch = angles.x;
    
            // Make the rigid body not change rotation
            if( rigidbody )
                rigidbody.freezeRotation = true;
    	}
    	
        void Start()
    	{
    		SetupTargetAndTransforms();
    		
            InstallGestureRecognizers();
    
            SetupCamera();
    		
    		SetupTouchMasks();
    
            Apply();
        }
    
        #region Gesture Event Messages
    
        float nextDragTime;
    
        void OnDrag( DragGesture gesture )
        {
            // wait for drag cooldown timer to wear off
            //  used to avoid dragging right after a pinch or pan, when lifting off one finger but the other one is still on screen
            if( Time.time < nextDragTime )
                return;
    
            if( target )
            {	
    			// Turn the player when the player is in motion.
    			if (inMotion)
    			{
    				float h = ( gesture.TotalMove.x * yawSensitivity * turningFactor / screenWidth ) * gesture.ElapsedTime;
    				
    				// Prevent the swipe input value from exceeding the input limits.
    				if (h > 1)
    					h = 1;
    				if (h < -1)
    					h = -1;
    				
    				// Set the direction based on the swipe input value
    				animator.SetFloat("Direction", h, directionDampTime, Time.deltaTime);
    			}
    			else
    			{
    				// No camera yaw or pitch adjustment when the player is in motion
    				IdealYaw += gesture.DeltaMove.x * yawSensitivity * 0.02f;
    				IdealPitch -= gesture.DeltaMove.y * pitchSensitivity * 0.02f;
    			}
    			
    			// Reset player turning at end of gesture
    			if( gesture.Phase == ContinuousGesturePhase.Ended)
    			{
          			animator.SetFloat("Direction", 0.0f, 0.0f, Time.deltaTime);
    			}
    			
    			float directionDragValue = animator.GetFloat("Direction");
    			Debug.Log("Direction Float:" + directionDragValue);
            }
        }
    
        void OnPinch( PinchGesture gesture )
        {
            if( allowPinchZoom )
            {
                IdealDistance -= gesture.Delta * pinchZoomSensitivity;
                nextDragTime = Time.time + 0.25f;
            }
        }
    
        #endregion
    
        void Apply()
        {
    		// Handle camera obstacle raycasting first
    		
    		RaycastHit hit = new RaycastHit();
    		
    		// Only collide with non-Player (8) layers
    		int layerMask = ~((1 << 8) | (1 << 2));
    		
    		// New variable for handling zoom capabilities
    		float targetZoom;
    		
    		// Cast a line from the target transform to the camera and find out if we hit anything in-between
    		if ( Physics.Linecast( targetTransform.position, thisTransform.position, out hit, layerMask ) ) 
    		{
    			// We hit something, so translate this to a zoom value
    			Vector3 position = hit.point + thisTransform.TransformDirection( Vector3.forward );
    			Vector3 difference = thisTransform.position - position;
    			targetZoom = difference.magnitude;
    		}
    		else
    			// We didn't hit anything, so the camera should use the zoom set externally
    			targetZoom = IdealDistance;
    		
    		// Follow the player when the player is in motion
    		if (inMotion)
    		{
    			// Set IdealPitch and IdealYaw relatie to the target, always facing the direction of travel.			
    			// TODO: Fix euler angle rotation, it currently resets to 0 when 360 degrees of roytation are exceeded.
    			IdealYaw = targetTransform.eulerAngles.y;
    			IdealPitch = targetTransform.eulerAngles.z + minPitch;
    			
    			Debug.Log("Ideal Yaw Value: " + IdealYaw);
    		}
    		
    		// Actually start moving the camera
    		
            distance = Mathf.Lerp( distance, targetZoom, Time.deltaTime * smoothZoomSpeed );
            yaw = Mathf.Lerp( yaw, IdealYaw, Time.deltaTime * smoothOrbitSpeed );
            pitch = Mathf.LerpAngle( pitch, IdealPitch, Time.deltaTime * smoothOrbitSpeed );
    		
            thisTransform.rotation = Quaternion.Euler( pitch, yaw, 0 );
            thisTransform.position = ( targetTransform.position + panOffset ) - distance * thisTransform.forward;
        }
    	
    	void Update()
    	{
    		if (animator.GetFloat("Speed") != 0.0f)
    		{
    			inMotion = true;
    		}
    		else
    		{
    			inMotion = false;
    		}
    	}
    
        void LateUpdate()
        {
            Apply();
        }
    
        static float ClampAngle( float angle, float min, float max )
        {
            if( angle < -360 )
                angle += 360;
    
            if( angle > 360 )
                angle -= 360;
    
            return Mathf.Clamp( angle, min, max );
        }
    
        // recenter the camera
        public void ResetPanning()
        {
            IdealPanOffset = Vector3.zero;
        }
    }

I’m assuming you are getting into this situation because somewhere in the code you are reading from Transform.eulerAngles. The fix is to not read from eulerAngles. Treat eulerAngles as write only, and maintain your own Vector3. Transform.eulerAngles can be set to angles beyond 360, or negative angles. By using your own Vector3, you will not have the wrapping problem.