-
Notifications
You must be signed in to change notification settings - Fork 725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
glMatrix v4.0 - Request for feedback #453
Comments
Yes to all of these! Some comments:
|
Yes, the swizzle code is auto generated! 😆 I am definitely not patient enough to do it manually. And yes, it would just be for the vectors. I don't think it makes sense for quaternions or matrices. I am considering attributes that return vectors representing matrix columns or rows, though. |
Mostly a great improvements while keeping backwards compatibility! In realtime context, allocations are expensive, especially Float32Array objects are a bit more expensive than simple objects. So it is best to avoid it as much as possible, or provide clear API design to communicate it to the user. One are of such I found that if a getter used from object, then it is expected to be very simple operation, and not an expensive allocation. Regarding of calling static method from class method, it is worth thinking which of them will be more popular. As I would assume class method is more often used so within class static method it could call class methods but not the way around. |
I would be careful with static methods, as it broke bundlers tree-shaking up to 2020. I remember that it happened in major bundling tools: webpack, esbuild, etc... I guess they all use the same tool for optimization / minification etc... It might not be the case anymore, but it would be great to give it another try. As mentioned above, I would love to see swizzle with an |
One minor argument against moving to classes that inherit from In a little game library I'm currently working on, I can do autorotate: [1, 2, 3] With the new version as described above, I think I then have to always do: autorotate: new Vec3(1, 2, 3) Which is just ever so slightly "worse" (purely in the sense that it increases boilerplate in userland.) It would be great if the functional-style static functions like Aaaaaah API design, I love/hate it :-) |
Love all this. Swizzling support is awesome. Minor question about your performance concerns, particularly around accessors/instance methods overhead: Do you have any metrics on that? I wouldn't expect an extra function call to be noticeable |
"Vector/Matrix creation time will be slightly worse". How about creating two Vec3 objects that share the same functionality. Like a FVec3 extends Float32Array and JVec3 extends Array, then each one can somehow pull in the same methods, getters & setters. I've been using a vec3 extended Float32Array for a few years now but in the last year or so I've been moving away by using regular javascript arrays for math heavy applications that doesn't need that gl compatibility that float32arrays provide. Having both an Array and Float32Array type can give users a choice of what sort of data structure to use. Also, for your constructors, maybe you'd like to try out overloading. Here's how I initialize a vector 3 in typescript. Intellisense can then tell you the various ways to initialize it by having each constructor defined. So the following is possible
|
I am excited for this! One thing I might offer as a suggestion is the constructor may lead to an unexpected behavior when compared to shader code. const v1 = new Vec3(5); As with shader code I would expect it to create a value of (5, 5, 5) here, instead it makes a value of (0, 0, 0, 0, 0), a Float32Array of length 5. Whether this is crucial or not I can't say but it would be nice to have parity there. Your code would just require an additional check: export class Vec3 extends Float32Array {
constructor(...values) {
switch(values.length) {
case 3: super(values); break;
case 2:
case 1: typeof values[0] === 'number' ? super(3).fill(values[0]) : super(values[0], values[1] ?? 0, 3) ; break;
default: super(3); break;
}
}
} let v1 = new Vec3(); // Creates a vector with value (0, 0, 0)
let v2 = new Vec3(1, 2, 3); // Creates a vector with value (1, 2, 3)
let v3 = new Vec3(v2); // Creates a copy of v2
let v4 = new Vec3(5) //Creats a vector with value (5, 5, 5)
let arrayBuffer = new Float32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]).buffer;
let v5 = new Vec3(arrayBuffer); // Creates a vector mapped to offset 0 of arrayBuffer (0, 1, 2)
let v6 = new Vec3(arrayBuffer, 16); // Creates a vector mapped to offset 16 of arrayBuffer (4, 5, 6) This also makes sense because the length as the first argument is no longer useful as it is expected the length will always be 3. |
Lots of great comments, thanks! Replying to a few real quickly:
As I have it right now that's not really possible because the static methods are formulated such that the first operand and the output are always the same object, which isn't guaranteed in the static versions. Also, I want the static versions to continue to work on things like raw arrays, so we can't assume that we can always call through to the instance method.
That's not something I was aware of, thanks for bringing it to my attention! I'll do some more research on it, and if you have any links to related issues/docs/fixes/etc I'd appreciate it.
Yeah, I appreciate that too! And the changes described here won't break it, it just requires a bit of discipline on your part as a library author. You'd just need to follow the pattern that the library itself is going to follow: Any time glMatrix returns a new vector/matrix it'll be an instance of the class, but any time it accepts a vector or matrix it only has to be an array-like object with enough elements. In the TypeScript code I've done so far I declare types such as export type Vec3Like = [number, number, number] | Float32Array;
export class Vec3 extends Float32Array {
static add(out: Vec3Like, a: Readonly<Vec3Like>, b: Readonly<Vec3Like>): Vec3Like {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
out[2] = a[2] + b[2];
return out;
}
} This prevents the use of any of the instance methods in the function implementations, but retains maximum flexibility so it's a quirk I'm willing to deal with. It means that all of the following are still valid: let v1 = new Vec3(1, 2, 3);
let v2 = new Float32Array([4, 5, 6]);
let v3 = [7, 8, 9];
v1.add(v3);
Vec3.subtract(v2, [9, 9, 9], v1);
v1.multiply(Vec3.scale([0, 0, 0], v3, 2.5));
Not good ones yet. I don't expect it to be a lot, but it's probably something you could measure if you were, for instance, doing a tight loop of cascading matrix updates over a large scene graph. I primarily bring it up because when it was first released I pitched glMatrix as "stupidly fast" and proudly showed benchmarks of it handily trouncing competing libraries at the time. That's not the case today, and I'm not really interested in pursuing the title of "fastest possible thing" at the expense of usability any longer, though performance is still one of my largest considerations. As such I feel it's noteworthy when design decisions are made that involve a performance compromise, big or small, but won't let it be a blocking factor unless it's egregious.
I've thought about it. Still trying to work out how to do so without making the API significantly more annoying to work with, especially across multiple libraries. In the meantime, as I pointed out above, the library will still function perfectly well with raw arrays in most cases.
Ooh! I'd considered doing expansion of the scalar value here previously but dropped it because I was trying to make the constructor simpler. Having scalar inputs accidentally change the length of the underlying array is bad, though, and I'm embarrassed I overlooked it. It's worth implementing the (admittedly nice) feature just to avoid the problem. Besides, it sounds like there's multiple people who would like that feature anyway! Thanks for pointing this out. |
Hi Brandon, I'm happy to see you are getting back to this project! Typescript is the way to go, no doubt (I used to be sceptic until I was forced to get into it for work). Also esmodule-first approach is a trend I'd love to see more! As for backwards compatibility, it is really neat that you've put so much effort to please the current users but having multiple ways of using the library can add confusion. With that in mind, I'd suggest to put deprecation on fast track. Perhaps put deprecation warnings in second release and deprecate old API right after that. That way, users can rip the band-aid off in two simple upgrades.
Did you consider Swizzling is nice but I almost never use it JS. By the time I need to swizzle my vectors I'm already in GPU land but that's just me. Would you consider adding unit vectors? I find them handy all the time! As for library size, I don't think you should feel constrained by it. As long as it is tree-shakable, just have fun writing useful code :) Have you thought about adding geometric primitives, camera matrix utils and so on? |
What about Everything else looks good, especially TypeScript support. WebGL projects tend to be very large, and large projects are generally written in TypeScript for sanity reasons. |
As for Honestly there's just not any great options here that I've come across.
I'm not sure what a "unit vector" means in that context? Like a vector that's constrained to always be 1 unit in length? Could you explain a bit more? |
@toji the proposed API changes sound really helpful from usage perspective! However, I'd be really careful of assuming tree-shaking will just work, without testing against a couple of the more modern bundlers. The issue about tree-shaking class methods in Rollup appears to have closed without any resolution. Things have generally improved since the comment in rollup/rollup#349 (comment), but I (still) feel that most bundlers are far worse at tree-shaking than Google Closure Compiler today, and very finicky about it. For example — I could easily imagine a line of code like this breaking all dead code elimination on the Vec3 class: const swizzle = flip ? 'zyx' : 'xyz';
const [a, b, c] = vec[swizzle]; If that turns out to be an issue, perhaps a backup option would be to continue exporting operators as individual functions accepting arrays, but to also provide the classes (with instance methods, no static methods) for users who prefer them. |
So it turns out that doing all of the swizzle variants I was hoping for on Vec2, Vec3, and Vec4 would likely double the size of the library when minified. 😨 I know I said I wasn't too concerned about the library size but... that's significantly more than I was anticipating, and I'm not really willing to take THAT much of a size hit for one feature, especially one that will probably see reasonably light use. Gonna experiment with injecting them dynamically (which totally goes against the TypeScript ethos, but whatever.) and if that doesn't work then I may just have to drop swizzles all together. |
I prefer drop swizzles |
Fiddled around with the swizzle code last night and got to a place that I'm happier with. I changed up the autogen code so that it writes out all the necessary symbols for the swizzle operations to a single, separate file ( The primary upside to this is that if you don't care about the swizzle operators then you simple don't call that function and there's no overhead involved. If you're importing the types separately, rather than as a bundle, then you won't even need to download the swizzle table (the largest part). If you are using the bundled version the size impact has gone WAY down (from ~50Kb to ~5Kb), and tree shaking should more reliably cull those symbols out if you don't explicitly enable it. The downsides are that there's an extra step involved if you want this feature, the implementation itself may be a bit slower, depending on how your JS engine optimizes it, and TypeScript doesn't recognize the swizzle operators because they're dynamically declared. That's unfortunate, but I'm wondering if I can get around it by having a generated Latest WIP code has been pushed to the |
I'd be interested in adding a Transform object to gl-matrix. Is it something I can do on your glmatrix-next branch so it can launch with v4.0 or is it something I should wait for you to be completely done before contributing. Well, probably the first question should be does it make sense to add a transform object to gl-matrix. https://github.com/sketchpunk/oito/blob/main/packages/core/src/Transform.ts |
@sketchpunk It would be really useful if the transform object could like |
That is what I'd rather see you do. v4 is going to be a big undertaking, and if you're going through all that bother I would rather see what you come up with, being able to take risks and explore new ideas without being chained via compatibility limitations. What you're describing is different enough from traditional gl-matrix paradigm that I think it warrants this. There is absolutely no shame in making a new thing; gl-matrix will still live on and be amazing; it will just be different from the next great thing you design. You could even pick a name that isn't tied to gl. :)
Same. Maybe this could be a killer feature in a
I know that's a popular sentiment but is it really that bad typing
I don't want to go through every point in the list of ideas for v4 but getter/setter proxy performance is a concern. It's been a few years since I checked but in v8 these are costly. I dropped pixi.js for game dev because after doing some CPU profiling I discovered internally it uses getter/setters in various places like the transform components, accessing these tend to snowball, and it chews up a surprising amount of CPU when you're building non-toy sims/games that operate on thousands of objects in a tight render loop. Using gl-matrix@3 and somewhat naive webgpu, I was was able to cut the cpu usage in half compared to pixi, and this was only a few weeks ago. Certainly not a scientific measurement but it seems like there are still some perf issues there.
Thank you for producing one of the best matrix libraries. People don't appreciate how important these low-level primitive handling libraries are. They are so foundational to make jank free games/sims/animations. I'm excited you have time to spend on this! |
If you are feeling inclined to spend time on a successor to gl-matrix, I now have author privs for the |
Strength of glMatrix is its absolute interoperatbility thanks to its universal representation. I personally love this feature. Classes will make it harder. I would prefer more evolutionary changes, my personal list:
I realize that classes provide better ergnomy, however I strongly feel they will turn this into different kind of library. |
A long time ago I've tried this approach. We have real-time UIs with many points. The problem with this approach was the increased app size. Native arrays are lightweight, but |
@toji I know you covered this in the description but I wanted to leave some feedback regarding the move to Typescript. Over the years I have moved away from having any build step in my development process (bundling comes only at the publishing stage now). There are many reasons for this so I won't try to state them all here, but I have rather enjoyed the fact that gl-matrix was one of the libraries that I could just do I wanted to try experimenting with the v4.0 branch but I soon discovered this approach is no longer possible and that there really is no easy way for me use it right now since it has not been published anywhere in a transpiled form. gl-matrix is something I use a lot and it feels a bit like a step backwards. There has also been a lot of open discussion about Svelte moving away from using Typescript code (while still using JSDoc to implement Typescript based type checking). The points being made by Rich Harris and others are very pertinent when it comes to a library like gl-matrix. Anyways, I don't really expect you to change the entire code base, but I had hoped this feedback can be of some value for you. |
Hey @shannon! This has been an interesting journey for me, because while I've gotten quite comfortable working in TypeScript overall I'd never tried to build middleware with it before now. I've been playing with different ways of building, importing, and using the library in other projects (I'm intent on inflicting any pain on myself before asking the community to endure it), and trying to find the right configuration for maximum ecosystem compatibility. Being perfectly honest: It sucks. Not really the language or the tooling, but the packaging and distribution story is a mess. There's not a clean way to distribute "A TypeScript Library" that everyone can just use, because you have to match about 50 different assumptions about how their own project is set up to be compatible. Seems like there's some patterns that can be used to make it a bit better or worse, but overall it's just not practical. Which is a shame, because there's aspects of TypeScript that are really quite nice, even for a library like this. Things like being able to specify certain arguments as So I'm going to some advice that I got when I first started lamenting the packaging situation online: glMatrix 4.0 will still be written in TypeScript, but when it's packaged for distribution it'll just be JavaScript with some I'll have the build files available in at least three forms: Separate ES modules files for individual imports, a bundled ES module file with the full library in it, and a Common JS bundle as well, since I think that's still a reasonably common use case. If anyone wants to copy the TypeScript source into their own project and configure it manually they're welcome to do so, but it won't be something I try to support directly. Anyway, hope that puts some concerns at ease! There will be a build step but it won't be you that has to do it. :) And sorry that it's taking so long to get this published in an easily consumable place! I actually want to work on that really soon because I need it for my own project. Need to look into the best way to publish "beta" packages with NPM and the like. |
Thanks @toji I completely understand your point of view. I don't want to rush you either so I will experiment when it's published :-) |
@shannon 100% agree with you, but unfortunately we're in the minority these days. |
@toji good to see you returning to this great project and thanks for taking the time to share a roadmap. FWIW, the vis.gl / deck.gl ecosystem already contains an implementation of gl-matrix-based classes that seems quite similar to what you propose. One of our core component is the @math.gl/core module which contains Vector, Matrix etc classes subclassed from arrays with methods that call gl-matrix functions on We built these wrappers back in 2017 and have used them successfully in many frameworks and applications over the years. Our implementation seems fairly similar to the direction you outline, so if nothing else could be useful as a reference. Possibly the biggest difference from your proposal is that we chose to subclass from
It would certainly be nice if this developed to a point where vis.gl could replace our own PS - Unfortunately, while we have been using gl-matrix as the base for math.gl since 2017, we are just about to fork and drop the gl-matrix dependency altogether since we have not found a way to work around gl-matrix lack of ES module support: #455. We'll basically be adding copies of the gl-matrix functions we use to |
For swizzling why not return vector compatible tuples instead of creating the vectors (JS Array vs Vector f32 array buffer view), I reckon js engines have mastered js array optimisations better than array buffer views. |
Since swizzling is opt in, typescript for it should also be opt in, like |
Is there a way we could also support higher precision? As @ibgreen mentioned earlier, 32-bit precision is bit limiting for my use case as well. Hence I've always been setting |
Would it make sense to make it configurable to extend Float64Array instead of Float32Array? |
Is there a way to specify the handedness of glMatrix? i.e. change it from the right handed system to left handed system. because I want to use it for WebGPU, and according to the WebGPU spec, it uses the DirectX coordinate systems (left handed system). |
Oh no, if WebGPU uses left-handed, I blame you, @toji 😝 Left-handed matrix math should have been expunged from the world years ago... In fact it had been until some DirectX software engineer in the 90s who never took physics thought "I'll just do this my own way". |
So long thread, want to say that TS is good choice to put sources in and it will be good if backward compatible will be as much as possible, projects using this lib cannot rewrite themselves for new API... |
Is this still alive or is there a notion of an ETA for v4 stable? |
i, too, am wondering what the roadmap looks like right now |
This is still alive, I've just been swamped with a lot of other priorities and so haven't been able to spend as much time on it as I was hoping. I'm probably going to take several days over the next month or so and dedicate time to just moving this forward, especially given the excellent feedback on this thread and the obvious interest in it. Thanks for your patience, all! |
I agree that this is an important use case and thanks for reminding me to investigate it! I've been looking into this today and it's unfortunately not as trivial as it was in the prior version of the library if I want to stick to the Typescript inheritance patterns I've been using so far. Fiddled around with Mixins for a bit to try and make it work but it starts to really mess with the type checking and docs generation. I'm starting to think I might just do a really dumb, basic thing and have a build script that just copies the file and does a search and replace In any case, though, I wanted to get opinions about the mechanism for exposing the different precisions.
Is that workable for your use cases? (It would also allow for easier mixing of precisions).
Huh. I have no idea why I missed that. Wasn't intentional. I'll restore it in the next build, thanks for pointing that out! |
Yea I really had expected something like this to work: const Vec2Factory = <T extends Float32ArrayConstructor | Float64ArrayConstructor>(TypedArray: T) => {
class Vec2 extends TypedArray {
// ... snip ...
}
// Instance method alias assignments
// ... snip ...
// Static method alias assignments
// ... snip ...
return Vec2;
}
export const Vec2_F32 = Vec2Factory(Float32Array);
export const Vec2_F64 = Vec2Factory(Float64Array);
export const Vec2 = Vec2_F32;
export const vec2 = Vec2_F32; The interesting bit is that Typescript doesn't present an error with just the above snippet. It just doesn't extend it and add any of the methods (i.e. The problem I see is that the Then I was able to get the following error:
So with even more fiddling around the conctructor I got this to work but it may seem a bit odd. import { EPSILON, FloatArray } from './common.js';
import { Mat2Like } from './mat2.js';
import { Mat2dLike } from './mat2d.js';
import { Mat3Like } from './mat3.js';
import { Mat4Like } from './mat4.js';
interface FloatArrayInterface<T extends Float32Array | Float64Array = Float32Array | Float64Array> {
readonly BYTES_PER_ELEMENT: number;
readonly buffer: ArrayBufferLike;
readonly byteLength: number;
readonly byteOffset: number;
copyWithin(target: number, start: number, end?: number): this;
every(predicate: (value: number, index: number, array: T) => unknown, thisArg?: any): boolean;
fill(value: number, start?: number, end?: number): this;
filter(predicate: (value: number, index: number, array: T) => any, thisArg?: any): T;
find(predicate: (value: number, index: number, obj: T) => boolean, thisArg?: any): number | undefined;
findIndex(predicate: (value: number, index: number, obj: T) => boolean, thisArg?: any): number;
forEach(callbackfn: (value: number, index: number, array: T) => void, thisArg?: any): void;
indexOf(searchElement: number, fromIndex?: number): number;
join(separator?: string): string;
lastIndexOf(searchElement: number, fromIndex?: number): number;
readonly length: number;
map(callbackfn: (value: number, index: number, array: T) => number, thisArg?: any): T;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: T) => number): number;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: T) => number, initialValue: number): number;
reduce<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: T) => U, initialValue: U): U;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: T) => number): number;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: T) => number, initialValue: number): number;
reduceRight<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: T) => U, initialValue: U): U;
reverse(): T;
set(array: ArrayLike<number>, offset?: number): void;
slice(start?: number, end?: number): T;
some(predicate: (value: number, index: number, array: T) => unknown, thisArg?: any): boolean;
sort(compareFn?: (a: number, b: number) => number): this;
subarray(begin?: number, end?: number): T;
toLocaleString(): string;
toString(): string;
valueOf(): T;
[index: number]: number;
}
type FloatArrayConstructor = { new (...value: any[]): FloatArrayInterface, BYTES_PER_ELEMENT: number };
/**
* A 2 dimensional vector given as a {@link Vec2}, a 2-element floating point
* TypedArray, or an array of 2 numbers.
*/
export type Vec2Like = [number, number] | FloatArrayInterface;
function Vec2Factory<T extends FloatArrayConstructor>(TypedArray: T) {
/**
* 2 Dimensional Vector
*/
class Vec2 extends TypedArray {
/**
* The number of bytes in a {@link Vec2}.
*/
static readonly BYTE_LENGTH = 2 * TypedArray.BYTES_PER_ELEMENT;
/**
* Create a {@link Vec2}.
*/
constructor(...values: any[]) {
switch(values.length) {
case 2:{
const v = values[0];
if (typeof v === 'number') {
super([v, values[1]]);
} else {
super(v as ArrayBufferLike, values[1], 2);
}
break;
}
case 1: {
const v = values[0];
if (typeof v === 'number') {
super([v, v]);
} else {
super(v as ArrayBufferLike, 0, 2);
}
break;
}
default:
super(2); break;
}
}
// ... snip ...
}
// Instance method alias assignments
// ... snip ...
// Static method alias assignments
// ... snip ...
return Vec2
}
export const Vec2_F32 = Vec2Factory(Float32Array);
export const Vec2_F64 = Vec2Factory(Float64Array);
export const Vec2 = Vec2_F32;
export const vec2 = Vec2_F32; The args on the constructor are listed as any type but when you actually try to create a new Vec2 it shows the appropriate types from the super class (ArrayBufferLike), and will error if you try something else. I think you would probably just replace your *Edit: I see now this does mess up the constructor parameters because we had wanted to allow |
Alternatively, if you are ok getting rid of the // ... interface snippet from above ...
function Vec2Factory<T extends FloatArrayConstructor>(TypedArray: T) {
/**
* 2 Dimensional Vector
*/
class Vec2 extends TypedArray {
/**
* The number of bytes in a {@link Vec2}.
*/
static readonly BYTE_LENGTH = 2 * TypedArray.BYTES_PER_ELEMENT;
// ... constructor removed ....
// ... snip ...
}
// Instance method alias assignments
// ... snip ...
const factory = (...values: [Readonly<Vec2Like> | ArrayBufferLike, number?] | number[]) => {
switch(values.length) {
case 2:{
const v = values[0];
if (typeof v === 'number') {
return new Vec2([v, values[1]]);
} else {
return new Vec2(v as ArrayBufferLike, values[1], 2);
}
}
case 1: {
const v = values[0];
if (typeof v === 'number') {
return new Vec2([v, v]);
} else {
return new Vec2(v as ArrayBufferLike, 0, 2);
}
}
default:
return new Vec2(2);
}
}
factory.BYTE_LENGTH = Vec2.BYTE_LENGTH;
// Static method alias assignments
factory.sub = Vec2.subtract;
// ... snip ...
return factory;
}
export const Vec2_F32 = Vec2Factory(Float32Array);
export const Vec2_F64 = Vec2Factory(Float64Array);
export const Vec2 = Vec2_F32;
export const vec2 = Vec2_F32; import { Vec2_F64 as Vec2, vec2 } from './gl-matrix/vec2.js';
const a = Vec2(1, 2);
const b = Vec2([1, 2]);
a.add(b);
vec2.sub(a, a, b) |
I like using It also serves as a pretty clear indicator to devs about the type of work being done. That said, I'm still open to changing up the patterns here if there's a clear benefit to doing so, but in this case I'm not convinced that that jumping through template hoops is actually going to yield a better, more usable result than just doing the dumb copy thing. (I tried out that route here, we'll see how I feel about it after a few more revisions.)
I came to the same realization, so I added a Thanks for the feedback! |
Just published a new beta version to npm for all you fine people to try out! There are still things I'm investigating, but there was also enough new here that I felt it was worthwhile to push a new version and get feedback. I'm trying to both pull in feedback from this thread and address a variety of longstanding issues files against the library. Beta.2 Changelog
|
WebGPU doesn't make any assumptions of handedness that your app uses. Typically you just define whatever coordinate system you want, and then finally apply the projection matrix which also bakes in the transformation from the coordinate system used by your app to NDC, which in WebGPUs case is indeed left handed. But absolutely nothing is stopping you from writing your entire app in a right-handed coordinate system of your choice. @toji As WebGPU has z defined from 0..1 in NDC, it also opens up the possibility to reverse z order, which I assume a lot of us will be doing. I am a little torn here on how much I think gl-matrix should assist in enabling this because it is simply a matter of baking in another transform into the projection matrix produced by gl-matrix. I think the current solution is fine, and it is ok to be opinionated and not expand the API to include handedness when creating the perspective matrix, but maybe it should be documented? Also, if a lot of users struggle with this, then maybe we could add some convinience methods to Mat4 to convert to and from other coordinate systems if we can find an API that makes sense. I am sure you have considered this though, what do you think? |
Hi @toji. I'm finally able to get around to investigating the state of I'll create a fork of I've been shipping my fork of |
I'm definitely interested in taking a look, @typhonrt! I'll admit that futzing around with |
Almost done... I've taken a very thorough approach to this effort to deliver a rock solid option to review that is 100% modern continuing off what you have done so far vs outright replacement in the build process. Hopefully tomorrow / maybe Monday. Just mentioning this small delay as if what I present is approved of course merging w/ the Edit (6/9/24): I've been putting together a video overview of the proposed v4 distribution I've created. I finished all of the coding / maintenance last week. Just dropping the code will not explain the problems of "beta2" and my proposed changes. The video & code drop will be available this week. |
Alright @toji et al. I have got all of my proposed distribution changes complete along with a video overview due to the comprehensive nature of the work involved. This was a lot of work and I treated the process with full due diligence in the aim of speeding along the transition of A video overview is available here that discusses the changes in reasonable detail: I don't often make large potential PRs like this to projects without prior negotiation, so hopefully this is well received; also, the first time I've made a video on the process to describe a potential PR. I found that this is likely an easier way to kickstart a discussion. I do hope you can take the time to review things and continue the discussion. My motivation as a downstream consumer of The approaches taken show a very modern way to release a Node package using Typescript and Resources Main fork repo: New and complete package API Docs: There is a Github distribution for this release that can be used right now by assigning {
"dependencies": {
"gl-matrix": "github:typhonjs-svelte-scratch/gl-matrix-beta3-dist#glmatrix-next"
}
} I am more than willing to take the time to fully discuss this potential PR and work with you on getting |
This is really fantastic, @typhonrt! I'm blown away by the amount of effort you've put into this, thank you! I watched through your video, took a look at the generated docs, and tried to use your build in my own code and have a couple of initial bits of feedback: First, I wasn't able to get your build to work in one of my more recent project, and it seems like it's mostly due to the dist/_lib/f32/* files using non-relative imports. Specifically it got caught on It may not be possible to satisfy every possible build environment, but I think this particular issue will be easy to fix and mostly want to ensure that the library stays as environment-neutral as possible. The other issue that came up was I noticed that in the (much improved!) vector docs there's now lots of entries for every possible swizzle, which makes it difficult to browse for the more commonly used methods and attributes. I'm curious if you have any thoughts on ways that we could document the swizzles but have them separated out from the rest of the vector docs. This is tricky because I think the work that you've done to make the swizzles work better with the type system overall is amazing, but I suspect it also makes it harder to handle them in a unique way. In any case, I just don't want them harming readability of the docs. But putting some of those concerns aside, I really can't compliment you enough on the effort you've put in here! I nodded along with most of your video explanation, having run into some of the problems you pointed out before and being surprised by others simply by virtue of not having much experience with building packages for environment XYZ. I'm very happy to work with you to get over some of the remaining hurdles and roll your updates into the repo, as it feels like it will offer a more robust developer experience all around. Thank you again, and I look forward to working with you to make v4 production ready! |
I'd be glad to address the concerns you brought up @toji Docs:
I have made a 2nd commit that groups the swizzle API additions in the vector classes by the Edit (post docs / 2nd commit): One thing I did notice though is that using a default category as CDN Build:
I agree that there are challenges here and they should be addressed. The Node package in the first commit that I created does require a bundler for web distribution. I should note though that using The solution is to create all inclusive ESM CDN bundles of both variations of the library. I was going to do this, but didn't want so many moving parts in this potential PR, but can definitely address this. It certainly is possible to involve Babel if you really want to cast a wide net in the CDN bundles. Now IMHO these CDN bundles should be ESM and loaded from a standard import route pointing to the bundle or using Edit: A UMD bundle should be provided; previously I mentioned not providing one. My general idea here is to create a However, it's my general understanding that one can handle this by setting One of the big pains to my understanding with Deno and such for instance is that there is no easy way to test a Node package before it is released and consumed by Build Consideration:Now that we are discussing CDN bundles and such I'd like to bring up the possibility of switching to Rollup for the library bundling. I specifically didn't want to change the build process much that you put in place with Immediate course of action:So potential courses of action that I'd like your input on before I start any work:
Mid to longer term action items:
That is a fair amount of action items above. I'd of course like to find a happy medium where I can make a PR that will be accepted. A fair amount of the tasks above are additional hardening beyond the current |
I suppose the above is a bit TL;DR... The initial work that I posted about was the Node package overhaul itself. The remaining work of coming up with some tidy all-inclusive pre-built distributions of the library is completed. So @toji pick your poison so to speak from dist-cdn for pre-built direct in browser use cases. There are ESM 2016 / 2022 options w/ the 2016 version transpiled. There is also a fully functional ES5 / UMD version. I've fully tested them manually including testing the UMD version in RequireJS (whoa haven't used that in about ~10 years). No real need to add Playwright for testing the CDN bundles. I did have to get Tomorrow I'm finishing up some really neat additional documentation of the CDN bundles that is co-hosted with the main Node distribution API docs. There are full types for the CDN bundles and Just dropping this here for the UMD / RequireJS proof of life (the long tail is supported): |
Sorry if I'm having a bit of trouble keeping up. Been busy with other things. Also I'm not going to be available for reviews for most of the upcoming week, so I promise that lack of feedback isn't due to anything on your end. 😄 Updated docs are much easier to parse, thanks! Swizzles still take up a ton of page space, but at least their far easier to navigate around now. I do think adding @category to all the methods like you mentioned is probably the right thing to do long term, but that can easily be handled in a follow-up! I'll say up front that if UDM can be supported with minimal effort then that's great, but I'm actually not too concerned about supporting it if it proves to be a maintenance burden or imposes design decisions or dependencies we'd rather not have. I don't think it's used much in modern JS development and would rather focus on making the more common, recommended paths work well. I do view Deno and Bun support as higher priority since they're becoming more popular, but my understanding is that it should take minimal to no changes vs. node support? Fingers crossed that that's the case. 🤞 In respect to the bundling, I think it's a good idea to have the various bundles available, but I'm also not sure if I understand why the separate files can't be made compatible with both browsers and node? Specifically, I don't think I understand the benefit you mentioned with the absolute paths when it comes to using the 64 bit vs 32 bit version of the lib? From looking at the folder structure it seems like both of them could access the common files by using the same relative path, since they reside in the same relative depth in the directory tree. Finally, a couple of quick notes:
Sorry I don't have time for more in-depth feedback, but again I'm really impressed with this effort and think it should be ready to merge in as beta.3 after a couple of relatively minor details are sorted out! Thank you! |
No worries. I spent the last week finishing the "2nd half" / CDN Bundle configuration, optimized the build process, improved the API doc support, and got ESLint v9 and latest TS ESLint support all hooked up and source in line with it. Here is the latest video covering the work in the last week that provides clarity / answers questions: API docs are updated. There is placeholder information in the CDN README that I'd be glad to update after Main fork repo:
I already got the
It's all good and handled including type declaration support even for the IIFE / global use case.
Re: UMD, indeed probably not used much at all these days. I did make a point of highlighting
I think Bun will be more like Node in terms of consuming NPM packages. I did review the Deno documentation more and somewhat recently they did add the capability to load packages from NPM, but ESM CDNs remain the recommended way to work with Deno. Things should be covered for this use case and I'll look into testing everything after a
In the build optimization work which is covered in the video the Node / NPM distribution is now fully bundled via ESBuild. This provides clarity that you can't copy individual files and run them raw on your local web server. If you use
I got the latest ESLint v9 + latest typescript-eslint support setup and the source & tests fully covered.
No worries again. In many ways the work I did rewinds accrued technical debt and prepares The next maintenance task that could be tackled is getting the source code coverage in tests up to 100%. When I loop around to update TypeDoc support to But otherwise I do need wrap things up as from my perspective things are in a very sound production grade releasable state. |
Maybe we can use this approach to remove those empty alias funtions in class defination: export class Vec2 extends Float32Array {
// ...
static sub = this.subtract
static mul = this.multiply
static div = this.divide
static dist = this.distance
static sqrDist = this.squaredDistance
static mag = this.magnitude
static length = this.magnitude
static len = this.magnitude
static sqrLen = this.squaredLength
}
export interface Vec2 {
sub: typeof Vec2.prototype.subtract
mul: typeof Vec2.prototype.multiply
div: typeof Vec2.prototype.divide
dist: typeof Vec2.prototype.distance
sqrDist: typeof Vec2.prototype.squaredDistance
}
;(
[
['sub', 'subtract'],
['mul', 'multiply'],
['div', 'divide'],
['dist', 'distance'],
['sqrDist', 'squaredDistance']
] as const
).forEach(v => (Vec2.prototype[v[0]] = Vec2.prototype[v[1]] as any)) |
I also think it would be better if the return type of static method like static copy<T extends Vec2 | Vec2Like>(out: T, a: Readonly<Vec2Like>) {
out[0] = a[0]
out[1] = a[1]
return out
} const x = Vec2.copy([0, 0], [1, 2]) // x: [number, number]
const y = Vec2.copy(Vec2.create(), [1, 2]) // y: Vec2
y.normalize() |
This seems like a reasonable change for the methods that it applies to. Before I make such a change to |
@toji et al, Alright.. I didn't disappear. After a short summer OSS break I set myself out to thoroughly update my TypeDoc theme. IE the Default Modern Theme (DMT). It's a theme augmentation that takes the generated output of the default TypeDoc theme and significantly improves features, UX, and accessibility. Just like my hope / proposed TypeDoc Please see this video to get a side by side comparison between the DMT (on the left) and the default TypeDoc experience (on the right): I strongly believe that a package like You can view a live version of the API docs here: Do check out the previous post above as you can try out the proposed For those interested in producing similar API docs like I have for I just updated |
glMatrix was a project I started 12 years ago(!), because at the time there weren't too many good options for doing the type of matrix math realtime 3D apps required in JavaScript. I never thought that anyone outside of myself and a few early WebGL developers would use it, and certainly didn't anticipate it becoming as popular as it did.
Fortunately for everyone, the landscape for 3D on the web looks a lot different today than it did over a decade ago! There's a lot of great libraries that offer comprehensive tools for creating realtime 3D web apps, usually with their own very competent vector and matrix capabilities built in. Many of these offer features or syntax niceties that glMatrix hasn't been able to match due to it's history and design ethos.
The web itself has also evolved in that time. When I published the first version of glMatrix Chrome was on version 5, Firefox was at version 3, and Internet Explorer was still a thing developers cared about. Node.js and TypeScript hadn't even been released! We've made astronomical strides in terms of the capabilities of the Web platform since then. For example, none of the following existed (or at the very least were widespread) at the time glMatrix was first developed:
let
andconst
(x) => { return x; }
)someFunction(...args);
)Float32Array
wasn't around until shortly after!Over the years glMatrix has been updated in small ways to take advantage of some of these things, it hasn't strayed too far from it's original design. But these days we can do so much better, and despite the excellent competition that I strongly believe there's still a need for a solid, standalone vector and matrix math library.
I've had a bunch of ideas floating around in my head for how glMatrix could be updated to take advantage of the modern web platform, but haven't had an opportunity to work on it till recently. (Let's be honest, the library has needed some maintenence for a while now.) Now that I've had a chance to try some of it out, though, I feel pretty confident that it's a viable direction for the future of the API.
So let me walk you through my plans for a glMatrix 4.0! Feedback highly appreciated!
Backwards compatibility first and foremost
glMatrix has a lot of users, and they have a lot of carefully written algorithms using the library. It would be unrealistic to expect them to do complete re-writes of their code base just to take advantage of a nicer code pattern. So the first principle of any update is that backwards compatibility is always priority number one.
This doesn't mean that EVERYTHING is carried forward, mind you. I think it's appropriate for some functionality to be labeled as deprecated and for some lesser used, fairly awkward bit of the library to be dropped. (I'm looking at you, weird
forEach
experiment that never quite worked the way I wanted.)But the majority of the library should be able to be used with minimal or no changes to existing code, and new features should cleanly layer on top of that existing code rather than requiring developers to make a wholesale switch from the "old way" to the "new way".
Lots of ease-of-use improvements
glMatrix was designed for efficiency, but that left a lot to be desired in terms of ease-of-use. There's only so much that JavaScript allows in terms of cleaning up the syntax, but with some slightly unorthodox tricks and taking advantage of modern language features we can improve things quite a bit.
Constructors
The current syntax for creating vector and matrix objects isn't ideal (I'll be using
vec3
for examples, but everything here applied to each type in the library):We'd much rather use the familiar JavaScript
new
operator. Turns out we can without losing any backwards compatibility simply by declaring our class to extendFloat32Array
!This allows us to use a few variants of a typical constructor.
It's pretty flexible, and not that complex to implement! And of course because
Vec3
is still aFloat32Array
under the hood, you can pass it into WebGL/WebGPU (or any other API that expects Typed arrays or array-like objects) with no conversion.Static methods
For backwards compatibility we'll keep around
vec3.create()
and friends, but have them return instances of the newVec3
class instead of raw typed arrays. In order to keep everything together, they'll become static methods on theVec3
class. Same goes for every other existing method for a given type.Used as:
As a minor design aside, I felt pretty strongly that as a class the type names should begin with an uppercase, but that does break backwards compat since in the original library all the "namespaces" were lowercase. This can be resolved by having the library defined a simple alias:
Which then allows you to import whichever casing you need for your app, and even mix and match.
I would probably encourage migration to the uppercase variant over time, though.
Instance methods
Once we have a proper class backing out vectors and matrices, we can make many of the methods for those types instance methods, which operate explicitly on the
this
object.Turns out that this doesn't conflict with the static methods of the same name on the same class! And it makes the syntax for common operations much easier to type and read:
Actually there's two ways of going about this. One is that you implicitly make every operation on a vector apply to the vector itself, as shown above. The other is that you have each operation return a new instance of the vector with the result, leaving the operands unchanged. I feel pretty strongly that the former fits the ethos of glMatrix better by not creating constantly creating new objects unless it's necessary.
If you don't want to alter the values of the original object, there's still reasonably easy options that make it more explicit what you're doing and where new memory is being allocated.
And, of course you can mix and match with the older function style too, which is still handy for applying the result of two different operands to a third value or simply for migrating code piecemeal over time.
Attributes
Vectors being a real class means we can also offer a better way to access the components, because lets face it: typing
v[0]
instead ofv.x
is really annoying. Getters and setters to the rescue!Now we can choose to reference components by either index or name:
All of method implementations internally will continue to lookup components by index both because it's a bit faster and because it allows for raw arrays to be passed in as temporary vectors and matrices, which is convenient.
Swizzles!
And hey, while we're adding accessors, why not borrow one of my favorite bits of shader syntax and add swizzle operators too!
Swizzles can operate between vector sizes as well. In practice it looks like this:
(These do break the "don't allocate lots of new objects rule a bit, but as a convenience I think it's worth it.)
Operator overloading
... is what I WISH I could implement here. Got your hopes up for a second, didn't I?
But no, JavaScript still stubbornly refuses to give us access to this particular tool because some people shot themselves in the foot with C++ once I guess? Check back in another decade, maybe?
TypeScript
I'm sure this will be mildly controversial, but I'm also leaning very strongly towards implementing the next version of glMatrix in TypeScript. There's a few reasons for this, not the least of which is that I've been using it much more myself lately as part of my job, and my understanding is that it's becoming far more common across the industry. It also helps spot implementation bugs, offers better autocomplete functionality in various IDEs, and I feel like the tooling that I've seen around things like documentation generation is a bit better.
As a result, having the library implemented natively in TypeScript feels like a natural step, especially considering that it doesn't prevent use in vanilla JavaScript. We'll be building a few different variants of the distributable files regardless.
Older Browser/Runtime compatibility
While I do feel very strongly about backwards compatibility of the library, that doesn't extend to supporting outdated browsers or runtimes. As a result while I'm not planning on doing anything to explicitly break it, I'm also not going to put any effort into supporting older browsers like IE 11 or fairly old versions of Node. Exactly where the cutoff line will land I'm not sure, it'll just depend on which versions support the features that we're utilizing.
ES Modules-first approach
glMatrix has used ES Modules as it's method of tying together multiple source files for a while, and I don't intend to change that. What I am curious about is how much demand there is out there for continuing to distribute other module types, such as CommonJS or AMD modules.
One thing I am fairly reluctant to continue supporting is defining all the library symbols on the global object (like
window
), since the library itself is already reasonably large in it's current form and the above changes will only make it larger.Which brings me to a less exciting topic:
Caveats
All of the above features are great, and I'm sure that they'll be a net win for pretty much everybody, but they come with a few caveats that are worth being aware of.
File sizes will be larger
The addition of all the instance methods on top of the existing static methods, not to mention the large number of swizzle operations, would result is the library growing in size by a fair amount. I don't have numbers just yet, but I'd guess that the total size of glMatrix's distributable files growing by about 1/3 to 1/2 is in the right ballpark.
Obviously one aspect of building a well performing web app is keeping the download size down, and I don't want to adversely affect that just for the sake of adding some conveniences to the library.
I also, however, expect that most any developers that are truly size-conscious are already using a tree shaking, minifying build tool that will strip away any code that you're not actively accessing.
To that end, glMatrix's priority would be to avoid doing anything that would interfere with effective tree shaking rather than to try and reduce the base library size by only implementing a bare bones feature set.
length
is complicatedOne natural attribute that you'd want on a vector alongside the
x
,y
,z
, andw
attributes is alength
that gives the length of the vector itself, something that's already computed byvec{2|3|4}.length(v);
Unfortunately,
length
is already an attribute ofFloat32Array
, and gives (as one would expect) the number of elements in the array.We don't want to override
length
in our vector classes, since that would give nonsensical results in contexts where the object is being used as aFloat32Array
, which means that while we can retain the staticlength()
function for backwards compat we'll need an alternative for the vector instances. I landed onmagnitude
(with an shorter alias ofmag
) as the replacement term, though I personally know I'm going to goof that one up at least once, and spend more time than I should wondering why the "length" of my vector is always 3. 😮💨Vector/Matrix creation time will be slightly worse
This is a big one, as one of the most frequent criticisms leveled at glMatrix is that the creation of vectors and matrices is expensive compared to other similar libraries. This is because (for reasons that honestly escape me) creating
TypedArray
buffers or views is a slower operation in most JavaScript environments than creating a JavaScript object with the same number of elements/members.My overall response to this has been that for many apps you'll eventually want to convert the results to a
Float32Array
anyway for use with one of the graphics APIs, and that avoiding creating a lot of temporary objects is a good practice for performance anyway, regardless of the relative cost of creating those objects. Both of those principles are still true, but it doesn't change the fact that this aspect of glMatrix is simply slower than some competing libraries.The above proposals will not improve that situation, and in fact are likely to make it a bit worse. Having some extra logic in the extended classes constructor before passing through to the
super()
constructor will unavoidably add some overhead for the sake of providing a much nicer, more flexible syntax for developers.If I were starting fresh I may very well take a different approach, but as I said at the top of this post backwards compat is very important to me for a library like this, so this is an area where I'm willing to accept this as a relative weakness of the library to be weighed against what I consider to be it's many strengths. Your milage may vary.
Accessors/instance methods will have overhead
As nice as it is to be able to access the object components by name, using the getter
v.x
is likely to always be a bit slower than accessingv[0]
directly. Similarly some of the instance methods are likely to just pass through to the equivalent static method, especially in cases that would otherwise involve a significant amount of code duplication. For example:While I'm not necessarily a fan of adding functionality that's known to be less efficient than it could be, in this case I think that the aesthetic/usability benefits are worthwhile. And it's worth considering that there are plenty of times where the clarity of the piece of code will be more valuable than ensuring it uses every clock cycle to it's maximum potential. (I mean, lets be realistic: We're talking about JavaScript here. "Perfectly optimal" was never in the cards to begin with.)
I am happy knowing that in cases where the difference in overhead has a material impact on an application's performance, the conversion from accessors to indices, or to calling the static version of functions directly can be made painlessly and in isolation.
Preview
The code snippets in this post are all pretty simplistic, but I've put a fair amount of effort into validating this approach already, and currently have a WIP version of a potential glMatrix 4.0 available to look through in the
glmatrix-next
branch of this repo. It is definitely not in a generally useable state at this point, but looking at thevec2.ts
,vec3.ts
, andmat4.ts
files should give you a good idea of how things are likely to look when everything is done.Most of my efforts so far have gone into ensuring that things can work the way that I wanted, toying with file and directly structure, ensuring that the generated docs are clear and useful, and learning far more than I anticipated about TypeScript's quirks. But now that I'm satisfied that it's possible I wanted to gather some feedback from users of the library before pushing forward with the remainder of the implementation, which will probably be largely mechanical, boring, and time consuming. I'll likely take a week off work at some point to finish it up.
Thank you!
Thank you to everyone who has made use of glMatrix over the last 12 years! It's been incredible and humbling to see all the amazing work that it's been part of. And an even bigger thank you to everyone who has contributed to or helped maintain the library, even when I personally haven't had the time to do so!
The text was updated successfully, but these errors were encountered: