2
\$\begingroup\$

I'm trying to make a basic FPS camera view matrix, using position, pitch/yaw, and worldUp. Currently, my camera's view matrix is just a plain rotation matrix (made with yaw/pitch/roll) and the result is the view appears to be looking down the negative Z-axis, which in my case is my floor. Most tutorials I can find recommend making a view matrix by passing in an "eye" and "target" vector. But this isn't a desirable setup for an FPS camera where I only know the position and angles (pitch/yaw/roll) of the camera -- I don't have a specific point in mind that I want the camera to be focused on!

I'm using OpenGL as a renderer, but I don't want to use GLM as I'd wish/hope to understand how to implement this without helper libs. Also, my meshes and world matrices are made so that Z is my worldUp, X as forward, and Y as right/left. That might seem odd, but old Unreal engine games used this setup and I'm just accustomed to it for mapping/modding.

As mentioned my view matrix is currently looking down at the floor, but I'm not surprised that it's wrong because I've never incorporated or multiplied my worldUp vector into the matrix, and I don't understand how to.

So that's my question: How exactly do I incorporate my worldUp vector into my rotation matrix, to turn it into a proper view matrix -- or better yet, what is the proper method for constructing a view matrix when you have exactly these variables: 1.) Camera Position (XYZ) 2.) World Up vector ("0,0,1" for me) 3.) Camera Pitch/Yaw/Roll (Really only need Pitch/Yaw!)

I've been going through loads of tutorials and I can't find what I'm looking for. This is how I've started, but all it is is a normal rotation matrix, so honestly a full methodology would be really appreciated as I've been pulling my hair out over this for a week now. Thank you for any guidance on this.

MAT4X4F mrol;
MAT4X4F mpit;
MAT4X4F myaw;
float cr, cp, cy;
float sr, sp, sy;

cr = cos(_cam_roll);    cp = cos(_cam_pitch);   cy = cos(_cam_yaw);
sr = sin(_cam_roll);    sp = sin(_cam_pitch);   sy = sin(_cam_yaw);

// Yaw (about Z-axis)
myaw.m[0] = cy;     myaw.m[1] = -sy;    myaw.m[2] = 0.0f;   myaw.m[3] = 0.0f;
myaw.m[4] = sy;     myaw.m[5] = cy;     myaw.m[6] = 0.0f;   myaw.m[7] = 0.0f;
myaw.m[8] = 0.0f;   myaw.m[9] = 0.0f;   myaw.m[10] = 1.0f;  myaw.m[11] = 0.0f;
myaw.m[12] = 0.0f;  myaw.m[13] = 0.0f;  myaw.m[14] = 0.0f;  myaw.m[15] = 1.0f;

// Pitch (about Y-axis)
mpit.m[0] = cp;     mpit.m[1] = 0.0f;   mpit.m[2] = sp;     mpit.m[3] = 0.0f;
mpit.m[4] = 0.0f;   mpit.m[5] = 1.0f;   mpit.m[6] = 0.0f;   mpit.m[7] = 0.0f;
mpit.m[8] = -sp;    mpit.m[9] = 0.0f;   mpit.m[10] = cp;    mpit.m[11] = 0.0f;
mpit.m[12] = 0.0f;  mpit.m[13] = 0.0f;  mpit.m[14] = 0.0f;  mpit.m[15] = 1.0f;

// Roll (about X-axis)
mrol.m[0] = 1.0f;   mrol.m[1] = 0.0f;   mrol.m[2] = 0.0f;   mrol.m[3] = 0.0f;
mrol.m[4] = 0.0f;   mrol.m[5] = cr;     mrol.m[6] = -sr;    mrol.m[7] = 0.0f;
mrol.m[8] = 0.0f;   mrol.m[9] = sr;     mrol.m[10] = cr;    mrol.m[11] = 0.0f;
mrol.m[12] = 0.0f;  mrol.m[13] = 0.0f;  mrol.m[14] = 0.0f;  mrol.m[15] = 1.0f;

_cam_matrix = myaw * mpit * mrol;
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Most tutorials I can find recommend making a view matrix by passing in an "eye" and "target" vector. But this isn't a desirable setup for an FPS camera where I only know the position and angles (pitch/yaw/roll) of the camera But you do know what to look at. Its quite easy to create an forward vector from yaw and pitch, and position + forward is the position to look at. \$\endgroup\$
    – tkausl
    Commented Mar 1, 2019 at 12:43
  • \$\begingroup\$ Hi and thank you for the reply- I understand what you mean but is that normally how it's done? If you'll humor me, check this brief function about 30% down the page under "FPS Camera" here: 3dgep.com/understanding-the-view-matrix/#The_View_Matrix Somehow, they aren't making a new position to look at based on Eye anywhere, they're only using Eye for the translation indices. They also don't incorporate WorldUp anywhere in that matrix, which is leading to my confusion, many functions I find are this way. Am I misunderstanding something, or is that function indeed "wrong"? \$\endgroup\$
    – ps48
    Commented Mar 1, 2019 at 22:34
  • \$\begingroup\$ My (very limited) understanding is that it's possible to directly construct the fwd, up, and right vectors, based on the euler angles and WorldUp, and then directly stick them into the viewmatrix without needing to use LookAt(eye,target) to point at a precise point. That said, I'm willing to accept that I'm wrong here as I'm still very hazy on all this. \$\endgroup\$
    – ps48
    Commented Mar 1, 2019 at 22:43

1 Answer 1

5
\$\begingroup\$

If you're transforming a vector \$\vec v\$ like this:

$$\vec v ' = M \cdot \vec v\\ \begin{bmatrix}v_x'\\v_y'\\v_z'\\1\end{bmatrix}= \begin{bmatrix}X_x & Y_x & Z_x &T_x\\ X_y & Y_y & Z_y & T_y\\ X_z & Y_z & Z_z & T_z \\ 0 & 0& 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} v_x\\v_y\\v_z\\1 \end{bmatrix}$$

Then we can usefully think of each column of the matrix as a vector:

  • \$\vec X\$ is the direction into which the x-axis basis vector (1, 0, 0, 0) gets transformed
  • \$\vec Y\$ is the direction into which the y-axis basis vector (0, 1, 0, 0) gets transformed
  • \$\vec Z\$ is the direction into which the z-axis basis vector (0, 0, 1, 0) gets transformed
  • \$\vec T\$ is the position to which the local origin (0, 0, 0, 1) gets translated

If your multiplication convention is the opposite (\$\vec v \cdot M\$ - I can't remember what order OpenGL functions assume), then take the transpose of everything above - the columns become rows instead, but the same logic applies.

So, that makes it easy to compute a matrix with any orientation and any translation we want.

If we want the local x axis "forward" to point along a given yaw & pitch angle, we can compute the column vector \$\vec X\$ with spherical coordinates:

$$\vec X = \begin{bmatrix}\cos(pitch) \cdot \cos(yaw)\\ \cos(pitch) \cdot \sin(yaw)\\ \sin(pitch) \end{bmatrix}$$

(Negate the \$\sin\$ terms if you want the rotation direction to be opposite what's shown here)

If the local y axis points "right," then that depends only on yaw, no pitch needed:

$$\vec Y = \begin{bmatrix}-\sin(yaw)\\\cos(yaw)\\0\end{bmatrix}$$

And then the local z axis is just the cross product of the two:

$$\vec Z = \begin{bmatrix}- \sin (pitch) \cdot \cos (yaw) \\ - \sin (pitch) \cdot \sin (yaw) \\ \cos (pitch) \end{bmatrix}$$

And of course, \$ \vec T \$ is just the position of your camera.

To ultimately use this to render, you'll want to invert it (to transform world coordinates into camera-local coordinates), and swizzle the axes by swapping the rows around (since for drawing we typically want depth along the negative z axis, and height along the y axis)

So your view matrix would ultimately look something like this:

$$\begin{bmatrix} -\sin(y) & \cos(y) & 0 & T_x \sin(y) - T_y \cos(y)\\ -\sin(p) \cos(y) & -\sin(p) \sin(y) & \cos(p) & \sin(p)\left(T_x \cos(y) + T_y \sin(y)\right) - T_z \cos(p)\\ -\cos(p) \cos(y) & -\cos(p) \sin(y) & -\sin(p) & cos(p)\left(T_x \cos(y) + T_y \sin(y)\right) + T_z \sin(p) \\ 0 & 0 & 0 & 1 \end{bmatrix}$$

\$\endgroup\$
4
  • \$\begingroup\$ Thank you so very, very much. This was really stressing me. Right-side-up, facing-the-right-way shot :) i.imgur.com/c3Bqili.png If you're willing to answer a follow-up question, I am curious about two things. Firstly, why was the "worldUp" vec not required to be incorporated into these calculations anywhere? And secondly, you illustrated that some rows (vectors) needed to be swapped. So, rather than placing the vectors into the matrix in order of XYZ, they needed to be placed in in the order YZX. Why is this the case? Thank you again. \$\endgroup\$
    – ps48
    Commented Mar 2, 2019 at 8:38
  • \$\begingroup\$ Here I just assumed you wanted the world up to be (0, 0, 1) and baked that into the calculations of the forward & right vectors. (Note that at pitch 0, we look perpendicular to world up, at pitch 90° we look straight up, and our yaw axis is world up). The reason for the rows needed to be swizzled at the end is mentioned in that last paragraph: the OpenGL rendering pipeline ultimately wants to get to normalized device coordinates where x runs across the screen to the right, y runs up the screen, and z points out of the screen. So I cycled the rows of the final inverted matrix then negated z. \$\endgroup\$
    – DMGregory
    Commented Mar 2, 2019 at 13:26
  • \$\begingroup\$ I couldn't have asked for more, you've answered all of my questions. The only question I have left is how to accept your answer as the accepted answer. Thanks again; I really appreciate the thoroughness here and the time you've taken to help me, have a good weekend. \$\endgroup\$
    – ps48
    Commented Mar 2, 2019 at 17:57
  • \$\begingroup\$ Oh, the checkmark. :-) \$\endgroup\$
    – ps48
    Commented Mar 2, 2019 at 19:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .