Skip to content

Common Pitfalls

Kai Burjack edited this page Dec 30, 2018 · 9 revisions

Below you will find very common problems and solutions when working the first time with JOML, especially when migrating from other libraries which worked differently.

Angles are always in radians

Any parameter in any method representing an angle will always be in radians. This includes the parameters in methods like Matrix4f.rotate(angle, ...) and Matrix4f.perspective(fovY, ...).

So, the following will not produce the desired result:

Matrix4f m = new Matrix4f().perspective(60.0f, aspect, near, far);

Use Math.toRadians() instead:

Matrix4f m = new Matrix4f().perspective((float) Math.toRadians(60.0f), aspect, near, far);

Methods without a dest parameter modify this and return this

The following will not produce the desired result:

if (shouldMoveRight)
  position = position.add(forward.cross(up).mul(speed));
if (shouldMoveLeft)
  position = position.sub(forward.cross(up).mul(speed));
if (shouldMoveUp)
  position = position.add(up.mul(speed));
if (shouldMoveDown)
  position = position.sub(up.mul(speed));

The problem with the above code is that the forward and up vectors are being modified, since the cross and mul method calls modify this and this in that context is forward and up, respectively.

Instead use the overloads with explicit dest parameters like in following code:

Vector3f tmp = new Vector3f();
if (shouldMoveRight)
  position.add(forward.cross(up, tmp).mul(speed));
if (shouldMoveLeft)
  position.sub(forward.cross(up, tmp).mul(speed));
if (shouldMoveUp)
  position.add(up.mul(speed, tmp));
if (shouldMoveDown)
  position.sub(up.mul(speed, tmp));

This prevents forward and up from being modified. Here, the result will be written to a temporary tmp vector which will also be the returned object of the cross and mul calls. Also, the reassignment of position in every line was superfluous, since the add and sub calls operate on position and return position (remember that method overloads that do not take a dest parameter will always modify and return this).

The above was demonstrated with vectors. The same applies to matrices as well:

Matrix4f proj = new Matrix4f().perspective(...);
Matrix4f viewProj = proj.lookAt(...);

The above code is very likely not what was intended. The code creates a new Matrix4f instance and applies a perspective projection transformation to it. The result is then stored in the proj variable. So far so good. However, afterwards calling lookAt(...) on that matrix object will not create a new Matrix4f instance with the lookAt transformation applied to it, but instead modifies the previous matrix stored in proj. At the end, both variables proj and viewProj reference the same object.

Matrix element subscripts are column then row

In the index subscripts of the matrix elements, that is, in the indices/numbers of m00 throught m33 (in a 4x4 matrix) when read from left to right, the first (leftmost) number means the column and the second (rightmost) number means the row of the matrix element. This is different to the notation used in most math books, but aligns better with the column-major memory layout of OpenGL, where a matrix is an array of columns (so the column index comes first), instead of an array of rows.

When using the convention of matrix-vector multiplication M * v then v is a column vector and the first column of M is the column vector (m00, m01, m02, m03)T. Consequently, all vectors in JOML can be interpreted as column-vectors.

This is different to some other vector/matrix libraries, which treat the first element index as the row and the second as the column (while also using column-vectors and with it the M * v matrix-vector multiplication).

Matrix memory layout is column-major

When it comes to serializing matrix elements into a linear data structure such as an array or a NIO buffer, some matrix/vector math libraries use row-major memory layout and some other libraries use column-major layout. JOML uses the latter. This is also the memory layout expected by OpenGL.

That means, the following code will not do what you intended it to do:

glMatrixUniform4fv(matrixLocation, true, matrix.get(arrayOrBuffer));

Instead, use false as the transpose parameter argument:

glMatrixUniform4fv(matrixLocation, false, matrix.get(arrayOrBuffer));

Methods taking a NIO Buffer do not modify the Buffer position

All methods in JOML taking a NIO Buffer, such as Matrix4f.get(ByteBuffer) or Matrix4f.get(FloatBuffer) do not modify the buffer's position, limit or mark. That means the following code will not work as intended:

ByteBuffer buffer = ...;
matrix.get(buffer);
buffer.flip();
glMatrixUniform4fv(matrixLocation, false, buffer);

This is to comply with the behaviour of all methods in the LWJGL API, which also never alter the buffer's position. Furthermore, because the call to flip() or rewind() on the Buffer can be omitted, this allows for a more concise syntax when filling a buffer with a matrix and using that Buffer in a further OpenGL call:

glMatrixUniform4fv(matrixLocation, false, matrix.get(buffer));

Use direct NIO Buffers

The vector and matrix classes support writing to and reading their values from a java.nio.ByteBuffer (and typed views thereof). JOML makes the assumption that the only reason why a client would want to transfer JOML objects from/to a NIO Buffer is in order to communicate with lower-level APIs and native code, such as OpenGL when using an OpenGL wrapper/binding library like JOGL or LWJGL.

Because of this, by default JOML only supports writing to and reading from direct NIO Buffers, as opposed to Buffers which are wrappers of Java primitive arrays.

Whenever it is necessary to transfer JOML object to/from a non-direct NIO Buffer, the JVM argument -Djoml.nounsafe must be used. This disables all efficient memory operations that are optimized for direct NIO Buffers and uses a much slower method of copying the data.

Therefore, in order to achieve maximum performance, only direct NIO Buffers should be used.