"Why does pitch affect the x component?"
Short answer: because you could be looking left or right.
Now, you may say if yaw is zero (or ±π), then front.x = cos(pitch) * cos(yaw)
is cos(pitch) * 1
, so pitch is affecting the x component even with no yaw at all!
This is a bit of an artifact of the yaw convention they've chosen to use. OpenGL examples I've found seem to base this off of a version of Euler/Tait-Byran angles, where yaw is measured as the angle between forward and the positive x-axis:
This threw me for a loop at first too - I'm used to 0 yaw meaning we have applied zero rotation about the vertical axis (Unity uses this convention), but these articles take 0 yaw to mean "facing along the positive x axis."
Once we adapt to this slightly-turned way of looking at things, the formulas make sense again. When yaw is zero or ±π, our camera is looking along the x-axis, and so naturally pitching up or down should foreshorten the x component of the forward vector.
To see where these particular formulas come from, we can work it through as a pair of transformation matrices.
Starting with a matrix that, when pitch
is zero gives the identity matrix, and as we increase pitch the y+ and z+ axes spin through the unit circle:
(Here I'm using a right-handed coordinate system where positive pitch tips "upward," and assuming vectors will be multiplied in from the left side)
┌ 1 0 0 ┐
PitchMatrix = | 0 cos(pitch) sin(pitch) |
└ 0 -sin(pitch) cos(pitch) ┘
We can see, again according to your intuition, the x axis (first row) is unaffected by `pitch alone.
Then the yaw matrix is similar, just shuffled and with cos exchanged with -sin to satisfy the rule that "0 yaw = local z- rotates to face x+"
┌ -sin(yaw) 0 cos(yaw) ┐
YawMatrix = | 0 1 0 |
└ -cos(yaw) 0 -sin(yaw) ┘
Now, to get the combined result of both these matrices on a vector, we can multiply the vector by each one in turn:
TransformedVector = LocalVector * PitchMatrix * YawMatrix
(Note that contrary to Bálint's remark, I'm actually applying the YawMatrix
to the vector after the PitchMatrix
has been applied. This ensures we pitch about the local x, and yaw about the global y axis, which is typically how we want to use this. For example, a common Euler/Tait-Bryan angle multiplication order is z first, then x, and y last)
Subbing this in with localVector = (0, 0, -1)...
┌ 1 0 0 ┐┌ -sin(yaw) 0 cos(yaw)┐
TransformedForward = [ 0 0 -1 ]| 0 cos(pitch) sin(pitch) || 0 1 0 |
└ 0 -sin(pitch) cos(pitch) ┘└ -cos(yaw) 0 -sin(yaw)┘
Starting from the right, the first multiplication effectively selects the bottom row:
┌ -sin(yaw) 0 cos(yaw) ┐
= [ 0 sin(pitch) -cos(pitch) ]| 0 1 0 |
└ -cos(yaw) 0 -sin(yaw) ┘
Again we can see this satisfies our intuition - the x coordinate was untouched and is still zero. Following through with the yaw rotation...
= [ cos(pitch) * cos(yaw) sin(pitch) cos(pitch) * sin(yaw) ]
Now we have the formulas we started with:
front.x = cos(pitch)*cos(yaw);
front.y = sin(pitch);
front.z = cos(pitch)*sin(yaw);
You don't strictly have to use these rotation conventions. There's a million ways to form a coordinate system, and in all honesty the mess above is not my favourite way - so just use the conventions that make the most sense to you.