JOML-CI / JOML

A Java math library for OpenGL rendering calculations
MIT License
724 stars 103 forks source link

Matrix4f x rotation not working properly #171

Closed Masy closed 6 years ago

Masy commented 6 years ago

When using rotationY(float) the Matrix is fine as shown in the video here: https://streamable.com/i1zxa

However, when using rotationX(float) or RotationXYZ(float, float, float) the model somehow doesn't get rotated but rather translated on the Z axis like here: https://streamable.com/hnw7r

I started in version 1.9.6 but this still happens in 1.9.11

httpdigest commented 6 years ago

If you suspect an error in any JOML method, it's better to provide a concise, reproducible unit test that shows what you expect and what the actual (possibly erroneous) outcome is. This is especially easy with those methods. For a start on how to do this, see: https://github.com/JOML-CI/JOML/blob/master/test/org/joml/test/Matrix4fTest.java#L400 The error is probably due to an incorrect composition of rotation, translation and other matrix methods.

As far as I can tell, all rotation/rotate methods work fine.

httpdigest commented 6 years ago

FYI: Since you seem to be doing cylindrical (around one axis) or spherical (around two axes) billboards, the Matrix4f methods billboardCylindrical() and billboardSpherical() are probably of interest for you. Those are likely faster because they do not require composition of translation/rotation matrix methods and no trigonometric functions. Other than that, the most efficient way to build a billboard matrix is to directly cancel out the rotational component of the (model-)view matrix, which is exactly what happens when you post-multiply the matrices produced by billboardCylindrical() or billboardSpherical() to your view matrix.

There is also a demo in the JOML-CI/joml-lwjgl3-demos repository, showing how those methods can be used: https://github.com/JOML-CI/joml-lwjgl3-demos/blob/master/src/org/joml/lwjgl/BillboardDemo.java#L302-L309

Masy commented 6 years ago

So i've tried this Unit Test:

@Test
public void testMatrixRotation() {
    Matrix4f rotationMatrix = new Matrix4f();
    float angleX = (float) Math.toRadians(-33);
    float angleY = (float) Math.toRadians(283);
    float angleZ = (float) Math.toRadians(0);

    rotationMatrix.rotationXYZ(angleX, angleY, angleZ);

    Quaternionf angles = new Quaternionf();
    Vector3f rotation = new Vector3f();

    rotationMatrix.getUnnormalizedRotation(angles).getEulerAnglesXYZ(rotation);

    assertEquals(angleX, rotation.x, 1E-6f);
    assertEquals(angleY, rotation.y, 1E-6f);
    assertEquals(angleZ, rotation.z, 1E-6f);
}

This unit test fails because of the second assert. This does not answer my question, it rather cause more, because the y rotation was the one working for me.

The result is expected:<4.939282> but was:<-1.3439027>

It seems like i'm only rotating around 2 axis because my z rotation is always 0 atm, later on the z rotation shall be used for cinematic cameras, so cylindrical or spherical rotations aren't an option.

httpdigest commented 6 years ago

Everything is correct here. The problem is that there are arbitrarily many equal rotations resulting in the same effective rotation when using Euler angles. The methods to extract Euler angles from a quaternion or a matrix will always get you the smallest possible angles to achieve that rotation. So:

float angleX = (float) Math.toRadians(-33);
float angleY = (float) Math.toRadians(283);
float angleZ = (float) Math.toRadians(0);

is effectively the same rotation as:

float angleX = (float) Math.toRadians(-33);
float angleY = (float) Math.toRadians(283 - 360); /* == -77 */
float angleZ = (float) Math.toRadians(0);

You can verify this when transforming a direction vector with the rotation matrix and the quaternion. So, the angles extracted will always be in the range [-PI...PI].

Masy commented 6 years ago

Ok, but this can't be correct: I'm using this method to create the matrix

this.transformationMatrix.identity()
    .rotationXYZ(
        (float) Math.toRadians(rotation.x),
        (float) Math.toRadians(rotation.y),
        (float) Math.toRadians(rotation.z)
    )
    .translation(position.x, position.y, position.z)
    .scale(scale);

And using the following values:

Position: (0 161 0)
Rotation: (-5,25 -198 -0)
Scale: 1.0

Somehow the output is

1 0 0 0
0 1 0 161
0 0 1 0
0 0 0 1

Which looks to me like the rotation was completely ignored.

httpdigest commented 6 years ago

Yes, that is correct, because you are overwriting the whole matrix with translation() which sets the whole matrix to a sole translation. If you want to concatenate/post-multiply a translation to an existing matrix, use translate(). Also please read the JavaDocs of those methods, which explain what every method is doing.

Masy commented 6 years ago

Ok, i still can't seem to find the issue why rotation on the x axis translates on the z axis, but i guess it's an issue on my end

Masy commented 6 years ago

Ok, i don't know why that is but the solution to the problem is to first translate, then rotate. I learned it the other way around because rotating always rotates around the origin but i guess that's not the case here

httpdigest commented 6 years ago

With OpenGL, to build a model matrix which translates, rotates and scales a model appropriately, it is always T*R*S (or equivalent T*S*R). You probably leant the DirectX/Direct3D convention, which is exactly the opposite, since Direct3D vectors are row vectors instead of column vectors in OpenGL. Thus, in OpenGL a matrix-vector multiplication is always M * v whereas in Direct3D it is always v * M. You are right that rotation (and scaling likewise) always rotates (and scales) around the origin, and this is exactly the explanation why it must be T * R * v and not R * T * v. Let's say you do T * R * v, and we assume 'v' to be in model-space, then this will first rotate the vector 'v' by the matrix 'R' (remember that matrix operations are always performed from right-to-left!) and then translate it by 'T'. When you however do R * T * v, you first translate the vector and then rotate this translated vector around the origin.

Masy commented 6 years ago

That's weird, because in my viewMatrix i first rotate, then translate aswell and that works fine. If i switch it everything goes crazy

httpdigest commented 6 years ago

Welcome to the world of applied Linear Algebra. :) On a fundamental mathematical level, it's all about transforming from one coordinate space to another coordinate space. Your camera is always said to be at the center of the view, so when you want to transform "the camera" you are actually performing the inverse* transformation to the world/scene/models. This is why camera transformation matrices are composed in the opposite way as the ones for models in the scene. You cannot really "move" the camera. I highly suggest you read up on such topics, such as:

* and the "inverse" (T * R * S)⁻¹ in matrix algebra is just: (S⁻¹ * R⁻¹ * T⁻¹). You surely have noticed that in order to move the camera to (x, y, z) you have to actually translate by (-x, -y, -z). This is why.