Java OpenGL Math Library (JOML)

Started by Neoptolemus, February 09, 2015, 09:55:47

Previous topic - Next topic

Neoptolemus

Hi everyone,

Since LWJGL 3 removed most of the functionality of the maths library, I thought I'd have a go at creating a Java-based equivalent to the C-based GLM library. Although a conceptual port of the GLM library already exists, I had some issues in getting it to work properly (lots of black screens). The library is fairly bare-bones at the moment, containing the most commonly-used functions, but it should be enough to get people started. I'm happy to take requests to extend it as well if you come across anything missing that you feel would be useful to others.

For reasons of flexibility, every function has an instance and static method (where it makes sense to do so). The instance method modifies the object itself (so myMatrix.transpose() will modify myMatrix directly), and the static method is more in line with what we had in LWJGL 2.9.x where you specify a source and a target matrix (so Matrix4f.transpose(myMatrix, newMatrix) will not modify myMatrix, and stores the results in newMatrix instead).

I've also avoided using local object declarations within the library, with the exception of lookAt which really needs it (at least until I have time to go through it step by step). It makes the code hard to read, but at least you know it's not going to generate hundreds or thousands of collectible objects every frame, depending on what you're doing :)

The library currently covers:

    - Float and double precision 3x3 and 4x4 matrices and 2/3 component Vectors
    - Quaternions
    - Surface mathematics (calculating the normal, tangent, binormal)
    - Camera mathematics (including versions of GLM's perspective, ortho and lookAt methods)
    - Transformation utilities (such as generating a transformation matrix from supplied location, rotation and scale)

I'll add to the library over time, but if you have any other requests then drop me a PM or reply to this thread. You can download the source and the library here:

Java OpenGL Math Library

Feel free to use the code however you want. Modify it, use it as a base, copy/paste bits, anything you like :) Hope someone finds this useful!


Commit History:

25th February 2015
  * Kai has started adding double-precision alternatives to the float-based Matrix and Vector classes
  * Added MatrixUtils for creating commonly-used matrices like the model matrix

18th February 2015
  * New SurfaceMath class. Currently allows you to calculate the normal, tangent and binormal of a surface
  * Surfaces are defined by 3 vertices and, in the case of the tangent and binormal, their corresponding UV coordinates
  * Removed the normal method from Vector3f as SurfaceMath is a better home for it.
  * New Vector2f class for 2D calculations

17th February 2015
  * Added Quaternion.lookAt to generate a rotation towards at point B from point A (Thanks to SHC for suggesting it)
  * Method allows for specification of the forward and up Vectors, or to use the defaults

16th February 2015
  * Added Quaternion class for handling rotations

11th February 2015
   * Fixed an issue with lookAt (thanks to Kai for the fix)

10th February 2015
   * Added static alias-unsafe methods (mulFast, invertFast, transposeFast) which only work if the source and target matrices are different objects
   * Added overloads for most static methods to take a FloatBuffer as the destination and write into it directly
   * Removed the call to rewind() from the store() instance method in Matrix4f
   * Re-wrote lookAt to remove local Vector3fs (only local primitive floats are used). The code is now almost totally illegible but initial testing suggests it works
   * Some other minor optimisations (such as removing calls to set() from methods that don't require it)

9th February 2015
   * Added alias safety, thanks Kai for pointing that one out

8th February 2015
   * Initial Build

Kai

That is great!
We should make it a LWJGL project, like lwjgl-math or so.
One minor thing I noticed when looking at your code is that most methods in the matix classes are not alias-safe. That is, if you use the same matrix object for more than one argument, you get wrong results.

Like in the following code:
Matrix4f mat1 = ...;
Matrix4f mat2 = ...;
/* Multiply mat1 and mat2 and store result back in mat2 */
Matrix4f.mul(mat2, mat1, mat2);


You can have a look at the best math library I came across at all times, which is javax.vecmath and https://code.google.com/p/gwt-vecmath/, which both take care of this issue.

Alias-safety is also exactly the reason why the math classes in LWJGL 3's demo/util package use the set(...) method extensively to defer setting all 16 matrix element values after all of them were finally computed.

It would be great if you take care of alias-safety in your methods. This will help preventing unintentional behaviour. :)

Neoptolemus

D'oh! We're off to a good start  ::)

Fixed that. That was an oversight on my part, thanks Kai.

spasi

It would be interesting to have a math library that provided both alias-safe and alias-unsafe methods.

It would also be useful, in the context of LWJGL, to have methods that read from or write to FloatBuffers/ByteBuffers directly, instead of having to store the final values on-heap and then using the store() method. Btw, the rewind() in store() would make it very awkward to use, it'd be better if it worked like standard LWJGL methods (store data starting at the current buffer position and do not modify the current position).

I know the above would make it painful to write and maintain such a library, some code generation might help a bit.

Neoptolemus

Thanks for the feedback Spasi. I'll look to implement some of your ideas, particularly the one of writing directly to a FloatBuffer. I assume by this you mean something like a perspective method that takes a FloatBuffer as a parameter and just writes the matrix values it calculates directly to the buffer without the need to ever create a Matrix4f object? I'll get right on it :)

Just out of interest, what would be the value in having alias-unsafe versions of a method?

I have to admit that I am an entirely self-taught programmer as my day job doesn't involve programming and I did a non-technical degree at university, so there may be some stuff that's a bit amateurish or not "best practice". Always happy to take on the criticism and improve :)

spasi

Quote from: Neoptolemus on February 09, 2015, 12:37:03Just out of interest, what would be the value in having alias-unsafe versions of a method?

An alias-safe mat4x4 multiplication for example requires tons of temporary values, which translates to register pressure at the CPU level and that means lower performance. The JVM can do an impressive job optimizing it, with clever reordering etc, last time I checked it uses about half the registers it would normally need (~8 instead of 16). But a) the unsafe version is still faster, because of lighter resource usage and b) when the target storage is off-heap (e.g. a FloatBuffer), the JVM is not able to perform any kind of reordering, which means even lower performance.

Alias-safety should of course be the default. The user can then use an unsafe version, explicitly, if they can prove that the target object will not alias. But when you're multiplying two Matrix4f and the target is a FloatBuffer, by definition there are no alias concerns and you can do the multiplication without temp storage.

Neoptolemus

Ah fair enough. I wonder if it would simply be enough to add an assertion to the static functions to ensure that the first and last parameters are not equal? After all, there is already an instance method if the person was intending to modify the original matrix, myMatrix.mul(otherMatrix), so I'm not sure why someone would want to do it via the static method unless it is purely for readability of their code.

If not then I can come up with something else, perhaps just a branch with two private functions mulAliasSafe and mulAsliasUnsafe and just have it call the appropriate method based on whether param 1 is the same as param 3?

Decisions, decisions.

spasi

The branch will be more costly than the gain from using the unsafe version and will also probably destroy any chance for inlining. That's why I mentioned that the user must call the unsafe version explicitly.

Kai

Quote from: Neoptolemus on February 09, 2015, 13:11:36
Decisions, decisions.
Hehe. That happens when you put some versatile framework out in the open sea. It gets drawn in various directions, until it arrives somewhere we don't know yet. ;)
And yes, I agree with spasi. Provide all versions of a method for various use-cases and let the user decide, as he/she always knows best - or at least likes to think that. :)

Neoptolemus

Thanks guys. I was going to try and make it "idiot proof", but for the purposes of making it as fast and efficient as possible, I'll give users the opportunity to mess things up ;)

Neoptolemus

Hi again. I've added alias-unsafe versions of the static methods (using names like mulFast, invertFast etc.) and added a Javadocs warning (in bold, no less) that the destination matrix must be different to the source matrix. I've also added extra versions of each method to take a FloatBuffer as a destination, bypassing the need to calculate a matrix first and then store it. I've also rewritten the lookAt method to remove local Vector3f objects, and instead it just uses local floats which I believe are released immediately after the method finishes.

This should hopefully result in an efficient maths library which has a very small footprint even when put to heavy use.

If you guys have any further suggestions then let me know, otherwise I'll just add to it as I come across something I think will be useful. I plan to add a SurfMath class next which contains static functions for calculating things like the tangent and bitangent/binormal of a surface (for stuff like normal mapping).

Kai

Thank you for your effort!
This is becoming really cool and very useful.

One thing though after peeking over the Matrix4f class:

I guess the method
static void mul(Matrix4f source, float scalar, Matrix4f dest)

has some copy-paste errors. :)

Some feature-requests that'd be handy for me:
- quaternions support for rotation calculations
- simple convenience methods to create rotation-(like rotating a given amount of radians about a specified vector), translation- and scaling-matrices

Neoptolemus

Fixed ;) That's what you get for staring at m00 m01 m02 m03 m10 m11 m12 etc. for too long ;)

Thanks for the suggestions, I'll add those as well as the surface maths.

Just a thought, would it be worth investigating the option of doing calculations exclusively with FloatBuffers? For example, have functions that take a FloatBuffer as a source parameter and do all of the inversion, transposition etc within the buffer. Almost bypass the need for matrices altogether except for initial loading. I could see it possibly being useful for things like translation and rotation which are done frequently every frame, rather than loading a Matrix4f object into the buffer every time.

Kai

Some overloads using simple FloatBuffers would be good, regarding that Matrix4f is more or less just an opaque data structure to the user, only used for passing it to methods declared in JOML.
I agree with you in that it would be nice if the user provided the data structure in her preferred layout (i.e. FloatBuffer) instead.
Performance-wise I would not expect it to make a big difference in under the millionth run.
After all these are just 16 floats, most probably also being SIMD-copied by the JIT, but I don't know that.
Would be interesting to see whether SIMD auto-vectorization also catches on if the math is being done on FloatBuffer.get() and FloatBuffer.put(), and if it at all worked with float fields in the first place.
Maybe someone here knows something about that.

EDIT: I have just ported my LWJGL 3 demo applications to use your math library and so far everything went very well. I just would request you to add a method in Matrix4f to transform a vector by a matrix, like so:

    public void transform(Vector4f vec) {
        transform(vec, vec);
    }

    public void transform(Vector4f vec, Vector4f vecOut) {
        vecOut.set(m00 * vec.x + m10 * vec.y + m20 * vec.z + m30 * vec.w, m01 * vec.x + m11 * vec.y + m21 * vec.z + m31
                * vec.w, m02 * vec.x + m12 * vec.y + m22 * vec.z + m32 * vec.w, m03 * vec.x + m13 * vec.y + m23 * vec.z
                + m33 * vec.w);
    }

Kai

Somehow the CamMath.lookAt was giving me very weird results. I do not know exactly where the error was with the current implementation but I have ported my own implementation in Camera class to your style of computing it. This one works fine for me. Here it is:

    /** Calculates a view matrix
     * 
     * @param position The position of the camera
     * @param lookAt The point in space to look at
     * @param up The direction of "up". In most cases it is (x=0, y=1, z=0)
     * @param dest The matrix to store the results in
     */
    public static void lookAt(Vector3f position, Vector3f lookAt, Vector3f up, Matrix4f dest) {
        // Compute direction from position to lookAt
        float dirX, dirY, dirZ;
        dirX = lookAt.x - position.x;
        dirY = lookAt.y - position.y;
        dirZ = lookAt.z - position.z;
        // Normalize direction
        float dirLength = Vector3f.distance(position, lookAt);
        dirX /= dirLength;
        dirY /= dirLength;
        dirZ /= dirLength;
        // Normalize up
        float upX, upY, upZ;
        upX = up.x;
        upY = up.y;
        upZ = up.z;
        float upLength = up.length();
        upX /= upLength;
        upY /= upLength;
        upZ /= upLength;
        // right = direction x up
        float rightX, rightY, rightZ;
        rightX = dirY * upZ - dirZ * upY;
        rightY = dirZ * upX - dirX * upZ;
        rightZ = dirX * upY - dirY * upX;
        // up = right x direction
        upX = rightY * dirZ - rightZ * dirY;
        upY = rightZ * dirX - rightX * dirZ;
        upZ = rightX * dirY - rightY * dirX;
        // Set matrix elements
        dest.m00 = rightX;
        dest.m10 = rightY;
        dest.m20 = rightZ;
        dest.m30 = -rightX * position.x - rightY * position.y - rightZ * position.z;
        dest.m01 = upX;
        dest.m11 = upY;
        dest.m21 = upZ;
        dest.m31 = -upX * position.x - upY * position.y - upZ * position.z;
        dest.m02 = -dirX;
        dest.m12 = -dirY;
        dest.m22 = -dirZ;
        dest.m32 = dirX * position.x + dirY * position.y + dirZ * position.z;
        dest.m03 = 0.0f;
        dest.m13 = 0.0f;
        dest.m23 = 0.0f;
        dest.m33 = 1.0f;
    }


Cheers,
Kai