- Home /
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;
}
}
Answer by robertbu · May 23, 2013 at 03:26 AM
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.
The only place outside of the code that I've posted that reads eulerAngles is my SetupCamera() function:
void SetupCamera()
{
Vector3 angles = thisTransform.eulerAngles;
...
}
Within the code that I've posted, the if (in$$anonymous$$otion) block sets IdealYaw using eulerAngles when the player is in motion:
IdealYaw = targetTransform.eulerAngles.y;
Ideal$$anonymous$$ch = targetTransform.eulerAngles.z + $$anonymous$$$$anonymous$$ch;
So I modified the if (in$$anonymous$$otion) block to look like this:
// Follow the player when the player is in motion
if (in$$anonymous$$otion)
{
// Set Ideal$$anonymous$$ch and IdealYaw relatie to the target, always facing the direction of travel.
Vector3 cameraAngles = new Vector3(Vector3.forward);
cameraAngles = target.eulerAngles;
// TODO: Fix euler angle rotation, it currently resets to 0 when 360 degrees of roytation are exceeded.
IdealYaw = cameraAngles.y;
Ideal$$anonymous$$ch = cameraAngles.z + $$anonymous$$$$anonymous$$ch;
Debug.Log("Ideal Yaw Value: " + IdealYaw);
}
The problem still persists. I'm not sure whether Vector3 cameraAngles should be somewhere else or not, and I'm not exactly sure how to maintain it either.
The idea is to never read from eulerAngles anywhere. Typically when all the rotation information is contained within a class you'd have a class instance Vector3...something like 'myRotation'. Any changes you make to your rotation, you would make to this myRotation ins$$anonymous$$d. And then this variable is assigned to eulerAngles. So the myRotation variable would always be correct.
As I look at your code, your case is more complicated since you are using the rotation of another game object to drive the rotation of this game object. But the my assertion still holds. If you are reading a eulerAngle, the potential for pain is high, and it's not just the 360/0 boundary. Run this piece of code:
function Start() {
transform.eulerAngles = Vector3(180,0,0);
Debug.Log(transform.eulerAngles);
}
You'll see that the angle you get back is (0,180,180). There are multiple euler angle representations for any given 'physical' roation, and there is no guarantee you'll get back what you think you should.
So you have a couple of different paths you can go with this code. First, you could implement the Vector3 solution for the character, and then read from that rather than targetTransform. The second way is to rethink how you are setting these angles. It is often possible for example to manipulate a position used for a LookRotation() rather than mess with eulerAngles.
I've added the following as a class member:
Vector3 targetRotationAngles
And I've modified the if(in$$anonymous$$otion) block to look like this:
// Follow the player when the player is in motion
if (in$$anonymous$$otion)
{
// Set Ideal$$anonymous$$ch 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 = targetRotationAngles.y + targetTransform.eulerAngles.y;
Ideal$$anonymous$$ch = targetRotationAngles.z + $$anonymous$$$$anonymous$$ch;
Debug.Log("Ideal Yaw Value: " + IdealYaw);
}
The above code did not solve the problem (it still wraps at -360 or 360 degrees), and it also slowed down the framerate a little. Did I handle Vector3 targetRotation angles improperly, or is it a better option to try something with LookRotation() ins$$anonymous$$d?
I've given you question some thought. As long as you are accessing targetTransform.eulerAngles, you are going to have this problem. In theory you might be able to code around it, but the solution would likely be ugly and complicated. Given that you are basing your rotation on another game object, it may be difficult to use a Vector3 as I've indicated. You follow-up code shows that I've not explained the issue and solution very well.
If it were me, I'd move to a different model of how to match the rotations. One using Vectors and look directions rather than trying to work with the rotation. I can help with that, but only if I have a firm understanding of the goal of the rotational relationship between the two objects. Right now I don't. Can you describe or provide a picture of what you are trying to achieve?
Ok, here's the issue:
The game features a camera that can be swiveled around the player character when the character is stopped. The swiveling code works just fine and is taken form FatalFrog's sample code. The camera is also supposed to follow the character as a chase camera (without adjustment capabilities) when the character is in motion.
I will now post the entire C# class file to the OP, since it doesn't fit within the character limit.