View Matrix

Started by taquionm, October 20, 2015, 18:44:37

Previous topic - Next topic

taquionm

Hi all,

I'm trying to develop some games with LWJGL 3 and using also JOML as a Math library. Now I'm trying to create a Camera so I need to create a view matrix. The code fragment that creates that matrix is this:

        Matrix4f matrix = new Matrix4f();
        Vector3f cameraPos = camera.getPosition();
        Vector3f rotation = camera.getRotation();
        matrix.translate(-cameraPos.x, -cameraPos.y, -cameraPos.z).
                rotateX((float)Math.toRadians(rotation.x)).
                rotateY((float)Math.toRadians(rotation.y)).
                rotateZ((float)Math.toRadians(rotation.z));


Where
cameraPos
and
rotation
are vectors that model the camera position and the pitch, yaw and roll angles.

The matrix Works well while moving the camera, but when I rotate it along the x axis it seems to be rotating over an origin which is not the camera position. This causes a displacement which can be quite confusing.

I've read about the method
lookAt
but it seems to be used for theird person cameras (which is not waht I want) and also it is not clear to me how to calculate the target coordinates.

Can you provide any clues about what is happening?

Kai

Technically, there is no such thing as a first-person or third-person or any other "camera" in OpenGL. You just transform vertices, that's it. However, Matrix4f.lookAt and all other matrix functions can be used to build the transformations necessary to create the illusion of a camera in a scene.
But your concrete problem is the same as in this forum post: http://forum.lwjgl.org/index.php?topic=5962.msg31971#msg31971
So just do the translation after the rotations.
Explanation: You always want to rotate the scene around your virtual camera position. So, before rotating the vertices of the scene, you must make sure that the rotation origin is your camera position. So, as you did, create a translation of the negative camera position. However, the vertices must be translated before being rotated, so you must apply the translation to your matrix as the last step after applying the rotations. This might not be intuitive, but trust me it all makes sense and is identical to how the OpenGL 1.1 matrix stack operates.

taquionm

Thank you very much for your answer. Applying the rotation firs indeed solves the problem, but then, when moving through the scene (with WSAD controls for translations and mouse for cmarea rotation), the movements are nor applied to the direction where the camera is facing.

If there are no rotations, WSAD controls work fine, but when camera is rotated, let's say, it's rotated 180 degress, when moving forward what you see is that is moving backwards. I guess that I cannot just modify the camera position without taking into consideration rotation but I haven't found a explanation about how to do this.

Thanks again for your detailed response.

Kai

Yes, technically it is correct that your camera is moving "backwards," which is in fact 'world-forward', when it is rotated 180 degrees, since your position is still in world-coordinates.
Or in other words: When you press 'W' you are likely just increasing the 'z' coordinate of your position, which results in the camera moving to that exact new location in world-space.
So your camera would always be positioned at the right spot when you move with WSAD.

But your "W" is not with respect to where the camera is looking at, but where it should be positioned in the world.
What you need is to figure out where "forward" (i.e. -Z in local camera coordiantes) is in world coordinates.
To do this, you need to compute the "forward" vector of the camera.

In order to do this, after creating your view matrix with the rotations and translations and all that, we need to transform an orientation/direction by the inverse rotational transformation of that matrix. So we need the rotational part of the whole view transformation first, which can be done by computing the "normal matrix" using `Matrix4f.normal(Matrix3f)`.
You probably know that the normal matrix is used in OpenGL to transform the normals of a mesh.
This is exactly what we need to do here, too, but instead of transforming the normals of some mesh, we transform another world-space direction, which is our "forward."

Then we need to invert this normal matrix, since we do not want to get the transformation that brings world-Z to local camera-Z, but the other way around: We want to obtain the world-Z based on the local camera-Z (which is (0, 0, -1))!
So just `Matrix3f.invert()` the computed normal matrix.
At last, we transform our camera-local forward, which is always (0, 0, -1) using `Matrix3f.transform(Vector3f)` to obtain 'world forward'.

Then, when you have the forward vector you just increment/decrement your whole `position` vector by that `forward` vector when moving forwards/backwards.

Altogether it looks like so:

Store some "forward" vector and normal matrix as field in your class:
Vector3f forward = new Vector3f();
Matrix3f normal = new Matrix3f();


Then during each frame compute view matrix and the 'forward' vector:
view.identity()
    .rotateX((float)Math.toRadians(rotation.x))
    .rotateY((float)Math.toRadians(rotation.y))
    .rotateZ((float)Math.toRadians(rotation.z))
    .translate(-position.x, -position.y, -position.z)
    .normal(normal) // <- returns the 'normal' matrix
    .invert()
    .transform(forward.set(0, 0, -1)) // <- what is forward?
    .mul(0.2f); // <- some scaling factor determining how fast we move

The above uses some compact form of chaining all the necessary methods together, which only works because each of those JOML methods return the object being modified, which just suits us well in this case. :)
(consult the JavaDocs of JOML if you are not sure whether a method returns 'this' or some parameter. but generally, it is always the object being modified.)

Then in your keyboard callback, do this:
if (key == GLFW.GLFW_KEY_W)
    position.add(forward);
if (key == GLFW.GLFW_KEY_S)
    position.sub(forward);


I'll leave the quest to find the 'up' and 'right/left' vector for the other movements to the reader. :)

taquionm

Many thanks for your explanation. I will try your approach.

Kai

I forgot to tell one thing, though. Generally, computing the normal matrix is not necessary when the matrix has no non-uniform scaling or shearing applied, which is always the case with simple camera view transformations.
In this case the needed rotation is already the upper left 3x3 submatrix of the matrix. This is also mentioned in the "Matrix4f.normal()" method's JavaDocs.
We now only need to invert the obtained 3x3 matrix. Since we already assume the matrix to be orthonormal, we don't need the costly "invert()" method but can use the easy "transpose()" method.

So do this:
// Create view matrix
view.identity()
    .rotateX((float)Math.toRadians(rotation.x))
    .rotateY((float)Math.toRadians(rotation.y))
    .rotateZ((float)Math.toRadians(rotation.z))
    .translate(-position.x, -position.y, -position.z);
// Set 'normal' to the upper left 3x3 submatrix of 'view'
// by using Matrix3f.set(Matrix4f)
normal.set(view)
      .transpose() // <- invert (in our case: transpose) to have view-to-world transformation
      .transform(forward.set(0, 0, -1)); // compute world-space forward

Kai

There is even another cool thing to save a few multiplications and additions when obtaining the 'forward' vector:
We want to transform (0, 0, -1) from camera-local space into world-space. To do this we used the inverse rotational transformation of our view matrix.
Now, observe that (0, 0, -1) just happens to resemble the third base vector of our orthonormal matrix. (it's just its negation.)
So in order to obtain (0, 0, -1) in world-space, we actually don't have to perform any vector-matrix multiplication at all, but can just use the third column of our inverted/transposed rotation/normal matrix.
This is because each column in a matrix represents the image of the respective base vector in that dimension. The first column represents the vector that is the result of mapping (1, 0, 0). The second column is the result of mapping (0, 1, 0). And the third column is the result of mapping (0, 0, 1) with this matrix.
So, in effect, we can optimize our matrix calculations from the previous posting to this:
// Create view matrix
view.identity()
    .rotateX((float)Math.toRadians(rotation.x))
    .rotateY((float)Math.toRadians(rotation.y))
    .rotateZ((float)Math.toRadians(rotation.z))
    .translate(-position.x, -position.y, -position.z);
// Set 'normal' to the upper left 3x3 submatrix of 'view'
// by using Matrix3f.set(Matrix4f)
normal.set(view)
      .transpose() // <- invert (in our case: transpose) to have view-to-world transformation
      .getColumn(2, forward) // the third column is just our world-space "backward"
      .negate(); // make 'backward' to 'forward'

This saves us one matrix-vector multiplication within "transform".

When we want the ultimate solution, we can even get rid of 'transpose()' when we remember that the third column was just the third row before transposing.
So the final solution is this:
// Create view matrix
view.identity()
    .rotateX((float)Math.toRadians(rotation.x))
    .rotateY((float)Math.toRadians(rotation.y))
    .rotateZ((float)Math.toRadians(rotation.z))
    .translate(-position.x, -position.y, -position.z);
// Set 'normal' to the upper left 3x3 submatrix of 'view'
// by using Matrix3f.set(Matrix4f)
normal.set(view)
      .getRow(2, forward) // the third column is just our world-space "backward"
      .negate(); // make 'backward' to 'forward'


And when even building the normal matrix by just copying the upper left 3x3 submatrix of 'view' is too much, we can just directly access the first three columns of the third row of 'view':
// Create view matrix
view.identity()
    .rotateX((float)Math.toRadians(rotation.x))
    .rotateY((float)Math.toRadians(rotation.y))
    .rotateZ((float)Math.toRadians(rotation.z))
    .translate(-position.x, -position.y, -position.z);
// Read the first three columns of the third row of 'view'
forward.set(view.m02, view.m12, view.m22).negate();