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 (https://github.com/JOML-CI/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
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. :)
D'oh! We're off to a good start ::)
Fixed that. That was an oversight on my part, thanks Kai.
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.
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 :)
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.
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.
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.
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. :)
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 ;)
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).
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
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.
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);
}
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
Just tested your code and I'm getting some curious results. I did a straight-up copy and paste and I've noticed that the camera behaves slightly oddly.
If I set the camera's position to be, for example, (0, 2, 2) so it's two units away from my plane and 2 units above it, it looks like this:
(https://dl.dropboxusercontent.com/u/88069272/1.png)
However, if I then change the camera's position to something like (0, 2, 1), it now looks like this:
(https://dl.dropboxusercontent.com/u/88069272/2.png)
It looks as though the camera's Y position has also changed slightly as it looks like it's higher above the plane than in the first image.
If I change the camera position again to (0, 2, 4) so it's further from the plane, it looks like this:
(https://dl.dropboxusercontent.com/u/88069272/3.png)
Now it looks like the Y position is only slightly above the centre of the plane, whereas in the second image it looked like it was quite high up. I didn't get this same result previously. Is this me missing something, perhaps some other transformations I'm missing which would "correct" the final result?
EDIT: Or perhaps I'm just tired and falling for some optical illusion? :P
Hm... I would be very glad if you could provide the code you are using for that demo.
I am quite certain that the code I provided in the last post computes the correct orthonormal basis for a "lookat" matrix. :)
Your previous computation was missing out on recomputing the "up" vector, which is essential, since the initial vectors will most likely never give an orthonormal basis by themselves, as "up" is likely just always set to (0,1,0) and the direction in which the camera is looking is not orthogonal to up.
Basically, the "lookat" matrix is just an orthonormal basis (right,up,direction) and you need two cross-products to compute that from the initial arguments; one for "right" and one for recomputing "up".
EDIT: I just tested it with the ray tracing demos (as those are very susceptible to all kinds of matrix errors because they compute all sorts of rays and stuff) with various camera positions and directions and the results do seem plausible.
Quote from: Kai on February 11, 2015, 07:38:50Performance-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.
The problem is off-heap memory access, as I said above, the JVM is not able to reorder FloatBuffer reads and writes. Because of that, direct math on NIO buffers can be significantly slower (30% or more in many cases).
Quote from: Kai on February 11, 2015, 07:38:50Would 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.
The JVM does not do auto-vectorization and never will, because of semantics differences. This could only be done using explicit operations provided by a library backed by intrinsics, like in Javascript (https://01.org/node/1495).
Kai,
I'm sure it's more likely to be an error on my part given that you've done some pretty thorough testing, it just seemed odd that my first code (despite being wrong) actually produced good results. Might be a massive coincidence or just that I've not had a chance yet to subject it to any significant tests and therefore the mistake went unnoticed. Let me try implementing a proper freecam mode and get back to you. In the meantime, with your blessing, I'll submit a new build with your version of lookAt, and the transform method too (and I'll add a thanks to you of course!).
Sure thing. Don't need to mention me there... it's basically exactly what GLU is also doing in gluLookAt. :)
I do not have the copyright on that, because you cannot really compute a lookat matrix anyhow else. ;)
EDIT: Have edited the lookAt method code in my post a bit: We can make use of Vector3f.distance there, instead of expanding euclidean manually.
Quick update: I've added a Quaternion class for handling rotations. It supports the following features:
- Define Quaternion from axis + angle (degrees and radians)
- Define Quaternion from euler angles (degrees and radians)
- Multiplication, division, addition and subtraction
- Generate rotation matrix from Quaternion (store in Matrix4f or floatbuffer)
- Conjugation, normalization, dot product, inversion, length etc.
- Spherical linear interpolation (slerp)
As always, would appreciate some feedback as I've not worked with Quaternions before, so if there are any oversights on my part then let me know and I'll address them :) I've done some initial testing and the results seem to work, but since I had to research this stuff using various websites, there is a chance there is some inconsistency in results.
Just for information, you can also compute a look-at quaternion, as mentioned in this Awesome StackOverflow answer (http://stackoverflow.com/a/17654730/1026805).
public static Quaternion LookAt(Vector3 sourcePoint, Vector3 destPoint)
{
Vector3 forwardVector = Vector3.Normalize(destPoint - sourcePoint);
float dot = Vector3.Dot(Vector3.forward, forwardVector);
if (Math.Abs(dot - (-1.0f)) < 0.000001f)
{
return new Quaternion(Vector3.up.x, Vector3.up.y, Vector3.up.z, 3.1415926535897932f);
}
if (Math.Abs(dot - (1.0f)) < 0.000001f)
{
return Quaternion.identity;
}
float rotAngle = (float)Math.Acos(dot);
Vector3 rotAxis = Vector3.Cross(Vector3.forward, forwardVector);
rotAxis = Vector3.Normalize(rotAxis);
return CreateFromAxisAngle(rotAxis, rotAngle);
}
The above code is C# directly ripped off from that answer. I'd like to see this in the library too, will be helpful to someone.
Thanks for the suggestion SHC. I've added the function as both a static function (with supplied destination) and as an instance function (modifies the quaternion it's called upon). Initial tests seem to show it works (I can make a mesh always turn to face the camera), but let me know if anything isn't working.
Oh that function is sweet, as it allows for smooth tweening between two camera orientations. Will test it soon.
One suggestion, though:
We should parameterize this function a bit more.
Currently, this function depends on some implicit parameters, namely "what is forward" (through some static field in Vector3) and what is "up" (also through some static field).
I would suggest adding these as explicit parameters.
I've added overloads for both the static and instance method definitions to include custom up and forward vectors. If you choose not to specify them then the method will use the default Vectors of (0,0,1) for forward and (0,1,0) for up.
This library is looking great, and moving along so fast! Any plans to get this into maven central?
Quote from: BrickFarmer on February 17, 2015, 14:55:15
This library is looking great, and moving along so fast! Any plans to get this into maven central?
Maven-support would be awesome.
Would then also be good to have it into Travis-CI (like LWJGL3 does it) with an automatic publishing to Maven Central.
This would require the following steps:
- setup github user for Travis (like lwjgl3 does it with the github user LWJGL-CI)
- fork the git project for that user
- build a Maven POM for the project with Maven Central deployment information
- setup a Travis-CI Java/Maven project on behalf of the created Github user
If no-one minds, I could take care of creating the POM, the github user (I'd use a new googlemail-address for that and with PM to you, Neoptolemus, for the password of mail account and github user), the forking of the project and a Travis-CI project.
Although I have no experience with Travis. Maybe spasi can help there, too?
That sounds like a great idea! I'm happy to let someone else who's used Maven/Travis before fork it and get it on there. Just let me know what I have to do to keep it in sync with what's on Github.
Quote from: Neoptolemus on February 17, 2015, 15:45:44
Just let me know what I have to do to keep it in sync with what's on Github.
It should work without manually intervention. My plan is to get Travis configured so that you can just happily push changes to your own repository and Travis will do a git fetch and merge on its own fork and everything should be sync'ed then. :)
Sounds awesome! Thanks for setting that up :) As always, keep throwing suggestions my way for new features.
My current plans are:
* A Vector2f class for those wanting to do 2D games
* A class for doing surface mathematics like surface normal, binormal, bitangent etc
* Probably some other stuff when I have time to think about it
Quote from: Kai on February 17, 2015, 15:31:57
Would then also be good to have it into Travis-CI (like LWJGL3 does it) with an automatic publishing to Maven Central.
This would require the following steps:
- setup github user for Travis (like lwjgl3 does it with the github user LWJGL-CI)
Wait, does that mean that CI builds of LWJGL3 are regularly pushed to Maven Central?
I am discovering https://travis-ci.org/LWJGL-CI/lwjgl3 due to your post, what is its role?
Regards,
Olivier.
No. As far as I can tell, Travis is just a build server like Jenkins/Hudson, so basically some advanced shell environment with preconfigured GUI. But it also provides you with a toolchain for compiling various languages, like GCC for native languages and a JDK for us Java folks.
It does not per se deploy to Central. One has to configure that in the pom and trigger the right maven shell commands from the build job description of Travis.
However that is my partly uneducted impression of it. Might be wrong.
Hi all, new update:
* New SurfaceMath class. Currently allows you to calculate the normal, tangent and binormal of a surface (as defined by 3 vertices and, in the case of the tangent and binormal, the UV coordinates). I've removed the normal method from Vector3f as SurfaceMath is a better home for it.
* New Vector2f class for 2D calculations
As always, suggestions are always welcome!
Thanks for your work!
Hi!, have you though on implementing both Mutable and Immutable implementations? (Link (https://github.com/Ghrum/Ghrum/tree/master/LibMultimedia/src/main/java/com/ghrum/multimedia/math))
Keep up the good work!, looks great.
Hi Wolftein1, I've not considered implementing both mutable and immutable vectors, but I will take a look at your link and if it will be useful to a few people then I'll have a go :)
Quick update. I've changed the link on the first post to point to the new repository location, which is at:
https://github.com/JOML-CI/Java-OpenGL-Math-Library
This is the new working repository, so if you're using the library then make sure you visit this to ensure you have the latest version ;)
I'd probably hide the whole Quaternion code in the matrices. I don't know what you had in mind with that but I did just create a quaternion object, get the matrix for my rotation angle and axis and then multiply it with my other transformations anyway. Is there some point to keeping the quaternions separate from the matrices? Seems unnecesarily complicated compared to the old lwjgl utilities or glm.
Quaternions have several advantages over matrices. Firstly you can still combine rotations by multiplying quaternions. They are also good for interpolating rotations in animations. Multiplying two quaternions is more computationally simple too. Then of course they are more memory efficient. 1 quaternion (4 values) compared to a rotation matrix of 9 values. Or 1 quaternion and one 3d vector (for translation) compared to 16 values.
The only real detriment is the computational time of having to generate the matrix at the end of it.