-
Notifications
You must be signed in to change notification settings - Fork 103
Design
This article explains the design decisions behind JOML.
When working with vectors, we would really like to do this:
v = (1, 2, 3);
w = (2, 3, 4);
z = v + w * 2;
However, this is not valid Java syntax for three reasons:
- the use of implicit types
- the use of the illegal parentheses as vector constructor
- the use of the non-overloaded + and * operators that only work on primitives
Because Java does not have operator overloading, JOML needs to find a way to make vector arithmetic as painless as possible in Java. The natural way to do this is to think about operators as functions with an infix notation. If we then give the symbols valid Java method names, we can make operators work like this:
v = (1, 2, 3);
w = (2, 3, 4);
z = v.add(w.mul(2));
This solves the last of the above problems but keeps the first two.
We must now find a way to construct vectors in Java. And what would be more reasonable than using a Java constructor. :) With constructors we have to name our vector types. JOML chooses Vector3f to mean a vector comprising three single-precision floating point values:
Vector3f v = new Vector3f(1, 2, 3);
Vector3f w = new Vector3f(2, 3, 4);
Vector3f z = v.add(w.mul(2));
The above is now valid Java, and in fact it would work this way in JOML. But in Java there is no such thing as "value types." This means that your vectors are objects in Java memory that have identity. Therefore, we must now define the semantics of each vector operation, specifying exactly what arguments that operation takes and what exactly the return value is, in terms of Java object identities.
The general contract for simple operators in JOML is the following:
- the operator is implemented at least as instance method of the class of the left operand
- in the case of a binary operator the method takes a single argument which is the right operand
- if nothing else is mentioned then the result of the operation is written back to the object on which the method is invoked (i.e. the left operand)
- in the case of a unary operator the method does not take any arguments and the operation is applied to the object on which the method is invoked
- the return value is the object on which the method is invoked
- it is guaranteed for every operator that no operation produces wrong results due to the left and right operands being the identical/same Java objects
An example of a binary operator implemented as instance method is add:
Vector3f v1 = new Vector3f(3, 2, 1);
Vector3f v2 = new Vector3f(1, 2, 3);
v1.add(v2);
The result is the component-wise sum of v1 and v2 which is then written back into v1. In this case, v1 would now represent the vector (4, 4, 4).
An example of a unary operator:
Vector3f v1 = new Vector3f(3, 2, 1);
v1.negate();
The variable v1 will now represent the vector (-3, -2, -1).
Optionally, the operator is implemented as a static method. If this is the case, then:
- the method is declared in the class of the left operand
- in the case of a binary operator the method takes three arguments. First the left operand, then the second operand and lastly the destination object into which to store the result
- in the case of a unary operator the method takes a single argument, which is the single operand
- the method does not return a value (it is void)
- it is guaranteed that no operation produces wrong results due to the left and right operands being the identical/same Java objects
An example of this is:
Vector3f v1 = new Vector3f(3, 2, 1);
Vector3f v2 = new Vector3f(1, 2, 3);
Vector3f.add(v1, v2, v1);