Our team is developing a new WebGL enigne for this jam. In the spirit of camaraderie, we hope that you can benefit from our work! Today, I'd like to share some work on the vector math library interface, specifically about the way we harmonize the needs of math routines with javascript's object system. You can see the result of this research in our math module.
State of the Art
Other math libraries like glMatrix implement vector and matrix operations as bare monomorphized functions that take references to input and output locations, like mat4.multiply(out, a, b). That pattern is efficient (JS engines prefer functions with consistent types, and allocation is a bad idea in fast math code), but it leads to routines that look like they're programmed in assembly language. We would rather, when possible, write code that looks like equations.
Operations
Let's spend a moment considering the various ways to do math in native JS. Ideally, the methods on our vector types would be as convenient. We have three patterns, two that operate on existing memory and one that has to create a new object.
javascript
c = a + b; // Assignment
a += b; // Updating assignment
console.log(a + b); // An expression (creates a new Number)
We can mirror these three on our Vec class. Suppose that Vec is a class that contains its coordinates. Then,
javascript
let a=new Vec(), b=new Vec(), c=new Vec();
c.eqAdd(a,b); // Overwrites the content of c with the sum of a and b
a.addEq(b); // The equivalent of +=, adding b to a.
console.log(a.add(b)); // Allocates a new vector and sets it to the sum of a and b
Each of these methods has its own use-case. Assignment and updating assignment are useful in high-performance code, and the allocating expression is useful for writing natural, expression-like equations in situations where performance is not as important.
Returning this
To aid expression chaining, we want all methods to return the object they're writing to. That makes it possible to build expressions that look like the following:
javascript
// Several ways to sum three vectors
d.eqAdd(d.eqAdd(a,b),c));
d.addEq(a).addEq(b).addEq(c);
a.add(b).add(c);
Code generation
To avoid having to write every method three times, I wrote a wrapper to scan classes for implementations of eq___, and generate variants for all of them that were found. We do this using the fact that you can call toString on functions in order to get their implementations. A little parsing (copied from Angular's implenetation) extracts the function signature, and from there we build source strings, eval them, and assign them to the class. In this way, automatic implementations for addEq and add can come from a manually written eqAdd. The codegen can be triggered like this,
javascript
const Vec2 = generateVariantMethods(
class Vec2 extends AbstractVecN {
...
});
Class method decorators have been proposed, but until they're added, this is the best way I can think of to modify class members dynamically.
Constructors
In addition to the relationships between operators discussed above, there is also a relationship between assignments and constructors. For example, if we can construct a vector in polar coordinates with Vec2.Polar(r,theta), we will also want to be able to assign a polar coordinate to a vector with a.eqPolar(r,theta). For this reason, the codegen will also produce a static Foo method, that can be used as a constructor, from every eqFoo assignment method it finds. This gives us the primary constructor, Vec2.From(x,y), from the assignment function a.eqFrom(x,y). We get a lot of not-so-useful constructors from this (like Vec2.Add), but that doesn't seem to be a problem.
Codegen Example
For a class method eqFoo(self,other), the following functions are generated:
javascript
fooEq(other) {
return this.eqFoo(this,other); // Updating assignment
}
foo(other) {
// the Default static method is expected to allocate a default-value object
return (this.constructor.Default()).eqFoo(this,other);
}
static Foo(self,other) { // Like `foo`, but available on the class like a constructor.
return (this.constructor.Default()).eqFoo(self,other);
}
Conclusion
With these codegen features, our math library will be easier to use than most others, without sacrificing crucial performance. I wrote objects for all WebGL types: vectors from one to four dimensions, matrices, and even integer-valued vectors. They're all based on typed arrays and highly performant. If you'd like to use it, pick it up here!.
Previous Use our Code posts:
1. Better errors for shader compilation
2. WebGL Type Info
3. Matrix pretty printing