Hi all. So I’m working on a project where you’re able to spin a camera around a globe. The A and D keys rotate the camera around the Y axis, W and S for X, and Q and E for Z. Here is what the globe looks like at base rotation:

And here is what it looks like after holding E for a bit to rotate on the Z axis:

You’d think holding W at this point would make the camera go along the red line, but instead it follows the purple line, acting as if the camera is not rotated on the Z axis. Here’s the relevant code:

public float xAngle = 0f;
public float yAngle = 0f;
public float zAngle = 0f;
void Start()
{
Vector3 angles = transform.eulerAngles;
xAngle = angles.x;
yAngle = angles.y;
zAngle = angles.z;
}
if (Input.GetKey(KeyCode.A)) xAngle += Vector3.up.y * 60f * 0.02f;
if (Input.GetKey(KeyCode.D)) xAngle -= Vector3.up.y * 60f * 0.02f;
if (Input.GetKey(KeyCode.W)) yAngle += Vector3.up.y * 60f * 0.02f;
if (Input.GetKey(KeyCode.S)) yAngle -= Vector3.up.y * 60f * 0.02f;
if (Input.GetKey(KeyCode.Q)) zAngle += Vector3.up.y * 60f * 0.02f;
if (Input.GetKey(KeyCode.E)) zAngle -= Vector3.up.y * 60f * 0.02f;
rotation = Quaternion.Euler(yAngle, xAngle, zAngle);
negDistance = new Vector3(0f, 0f, -distance);
Vector3 position = rotation * negDistance + target.position;
transform.rotation = rotation;

Does anyone know why the X and Y rotations aren’t compensating for the Z?

Because Euler angles are terrible! In this case, it looks like a problem of rotation order. You’ll want to use Quaternion.AngleAxis for each axis manually to get the rotation to behave the way you expect it to.

rotation = Quaternion.AngleAxis(xAngle, Vector3.right);
rotation *= Quaternion.AngleAxis(yAngle, Vector3.up);
rotation *= Quaternion.AngleAxis(zAngle, Vector3.forward);
Now my rotations are all over the place.

OK so let’s look at your axes. You currently have A and D changing the X axis, which is a common mistake; the X value (with both .Euler and with the current code) rotates around the X axis, which means up and down like nodding your head. So you should have W and S affect the X axis. A and D should be affecting the Y axis, which rotates like shaking your head “no”. Q and E are correctly affecting the Z axis. So that’s the first problem.

Next, you should probably rotate them in the order of Y, then X, then Z. At least, this is the order that’s most logical to my mind. If I’m looking upwards and tilted to the right, then press D, I should rotate and then still be looking upward and tilted to the right, if that makes sense.

If it’s still not right, then you probably also need to rotate around where the axes currently are at any given moment, not the global axes. So for example, the second line (assuming you’ve moved Y to the first line) may need to be:

This still leads to some oddities. When it starts at 0,0,0 it’s fine. But say if the yAngle is at 90 degrees, modifying the xAngle will only rotate the camera around the z axis for whatever reason.

You realize that Vector3.up.y is just a complicated way of writing “1”?

It looks to me like your core problem is that you are accumulating all X rotations into a single variable and all Y rotations into another variable and so on, and then trying to recalculate your final orientation from that.

So imagine the user presses the +X button, then presses the +Y button, then presses the +X button again. You want the second +X press to take into account your current rotation, and rotate relative to that. But your script is adding up the sum of both +X presses into a single variable. No matter what you do with that variable after that point, you are treating both of those +X presses as interchangeable–but your goal is for them to be treated differently, depending on context! So this will never work.

A simple script to rotate an object relative to its own reference frame might look something like this:

float rotationSpeed; // degrees per second to rotate while button is held
void Update()
{
Quaternion currentRotation = transform.rotation;
if (some input) currentRotation *= Quaternion.AngleAxis(rotationSpeed * Time.deltaTime, Vector3.up);
if (negative input) currentRotation *= Quaternion.AngleAxis(-rotationSpeed * Time.deltaTime, Vector3.up);
if (some input) currentRotation *= Quaternion.AngleAxis(rotationSpeed * Time.deltaTime, Vector3.forward);
if (negative input) currentRotation *= Quaternion.AngleAxis(-rotationSpeed * Time.deltaTime, Vector3.forward);
if (some input) currentRotation *= Quaternion.AngleAxis(rotationSpeed * Time.deltaTime, Vector3.right);
if (negative input) currentRotation *= Quaternion.AngleAxis(-rotationSpeed * Time.deltaTime, Vector3.right);
transform.rotation = currentRotation;
}

Tilt your head up 90 degrees, then left it left 90 degrees, and your head will be tilted on its side (effectively, rotated -90 degrees on Z) as well as facing to your left. It’s just how rotations work. If you move your mouse up, left, then down, it’s functionally identical to having tilted left.

This is basically why I was trying to steer you away from, well, basically away from using the transform’s current rotation as any kind of input, because this is 100% guaranteed to happen. You can make FPS-style rotation plus tilt (the system I was describing) that won’t have this issue, with the caveat that if you’re tilted, pressing “S” will rotate downward as far as the “pre-tilt” rotation is concerned.

Unfortunately I kinda need the best of both worlds here. Never affecting the Z when using the mouse, and never using “pre-tilt” rotation. If you’re saying that’s impossible, I’ll have to rethink some things.

Yeah, I don’t think that’s mathematically possible. A lot of times in programming, the most important thing is to clearly define the actual desired behavior - implementation of that behavior may be the easier part. Sometimes forming the question is harder than answering the question.

In this case, ask questions about what exactly the desired end state is when you press various buttons in various situations. If you press W up to the north pole then press S, naturally you want it to return to the equator. If you press W up to the north pole, then press A, do you want it to go to the left-side of the equator tilted sideways (Antistone’s code)? Or do you want it to spin around the north pole (my code)? If you want something else entirely in that situation, then specifically what?

If you can answer those questions for just about every possible starting state you can think of, it’ll be easier to define the answer.

You may be used to thinking of “rotation” as being a collection of 3 variables for different axes, but that’s just an abstraction. Euler angles are not independent, and there’s more than one combination of axis rotations that will produce the same net orientation.

Let’s not even bother with combining different directions. Just rotate “upward” by 180 degrees. You’re now looking backwards–and also upside down! So even if you only rotate in the global reference frame (rather than local like you want), you can still end up with weird dependencies.

So “no Z rotation” isn’t actually a mathematically rigorous description; sometimes the same exact viewing angle can be described either with or without “Z rotation”.

There are some rotational control schemes that naturally have aesthetically-pleasing limitations, but you can’t just add a constraint like “no Z rotation” to an arbitrary system; it’s more like an emergent property.

The most common/famous system with an interesting emergent constraint is probably:

One input axis modifies your “yaw” (looking left/right) in the global reference frame

The other input axis modifies your “pitch” (looking up/down) in the global reference frame

Your pitch is clamped to prevent it from going above 90 or below -90