glam-rs icon indicating copy to clipboard operation
glam-rs copied to clipboard

RFC: Point types

Open emilk opened this issue 4 years ago • 7 comments

Point types

This issue is meant to spark a discussion about advantages and disadvantages of adding a separate type for points.

This suggestion goes somewhat against the simplicity of glam, but it is possible it can be designed in an opt-in manner.

I'll stick to 3D examples, but the same idea goes for 2D.

Summary

The size of Vec4 and Quat are the same, but since they are used very differently it is wise to give them different types (like glam does).

The same goes, I would argue, for the concepts vectors and points, which are used very differently, but currently share the same type: Vec3.

I suggest adding a new Pos3 ("Position 3D") type to represent points, and keep using Vec3 only to represent vectors.

Background

In homogeneous (a.k.a projective) coordinates we express 3-dimensional XYZ points as 4-dimensional vectors with an added projective W coordinate:

  • {x, y, z, w} is a general point, which corresponds to the coordinate [x/w, y/w, z/w]
  • {x, y, z, w=1} is a normalized point, which corresponds to the coordinate [x, y, z]
  • {x, y, z, w=0} is a vector (something with a length and a direction, but no position)

The normalized point is very useful, and since the w is always 1 we only need three values to encode. It is therefor tempting to use the same type for both vectors and normalized points (as glam does with Vec3).

However, conflating points and vectors has many downsides. For instance, when transforming a vector you must use m.transform_vector3(v), and for points you must use m.transform_point3(p). If points and vectors where different types, we could simply write m * v and m * p. This would work because we would have both impl Mul<Vec3> for Mat4 and impl Mul<Pos3> for Mat4.

Proposal

I suggest adding a new struct Pos3 { x: f32, y: f32, z: f32 } for points, with an implicit w=1.

The Pos3 would have very few members, such as distance (to another Pos3). It would NOT have length() because there is no sensible "length" of a point, as there is for vectors.

The operations possible on Pos3 would be limited to:

  • Pos3 + Vec3 -> Pos3 (translation)
  • Pos3 - Vec3 -> Pos3 (translation)
  • Pos3 - Pos3 -> Vec3 (measurement)
  • Mat4 * Pos3 -> Pos3 (transform)

Note that Pos3 + Pos3 is NOT allowed, as it makes no sense, nor is Pos3 * f32.

Quat * Pos3 is also not allowed, as again it makes no sense to rotate points (only vectors).

Basically Pos3 would be a very simple type when compared to Vec3.

Benefits

Ergonomy

It is nice to be able to write matrix * point and matrix * vector instead of m.transform_point3(p) and m.transform_vector3(v).

Type safety

I have many times seen the bug matrix.transform_vector3(point) or matrix.transform_point3(vector). With separate types, such bugs would dissappear.

Similarly things like fn draw_arrow(from: Pos3, vec: Vec3) can't accidentally be called as draw_arrow(base, tip).

Self-documenting code

Currently, something like struct Arrow(Vec3, Vec3) is ambigious (are those the base and tip of the arrow, or base and a vector?), but struct Arrow(Pos3, Pos3) (or struct Arrow(Pos3, Vec3)) is not.

Name

Point3 is popular in a lot of libraries. Pos3 has the benefit of being shorter and matching the length of Vec3. I don't feel strongly either way.

Other

I beleive Pos3 will fit in nicely with higher-level transform types (such as the isometric TransformRT).

Prior art

Point in nalgebra and Pos2 in my own emath.

Downsides

There are a few cases where some overloads may need to be added. For instance, Mat4::from_translation would probably need an overload Mat4::from_point to make it more ergonomic to create a transform that e.g. rotates around a point.

There is also a concern of a "slippery slope" towards ever-more advanced types (e.g. a separate type for unit-length vectors). As far as I can tell there is nothing in this proposal that would require more advanced types elsewhere, but I think it is a good instinct to resist complexity.

Transitioning

There are also a lot of code in glam that treats Vec3 as points, such as Vec3::distance. I would suggest that we keep all those around for those who want to ignore the existence of Pos3. We could make the transition a bit more ergonomic by adding impl From<Vec3> for Pos3 and impl From<Pos3> for Vec3.

Final thought

This may be too late to add to glam, but I thought it was worth to at least write up and have a discussion about, and see what thoughts other people have!

emilk avatar Feb 24 '21 09:02 emilk

It is something I've been wondering about, also in conjunction with adding an affine transform type. I don't think I've used a library that had a separate point type before. My feeling is it would probably introduce a bunch of places where you'd need to convert from point to vector or vice versa, i.e. it would get in the way, but at the same time it would get around some ambiguity and needing special methods for transforming points and vectors, etc. In saying that there is still ambiguity when transforming a point3 with a mat4 or rather it's cheaper if you know you're only dealing with an affine transform. Having a separate affine transform would help there.

I have been influenced a bit by Lengel's Foundations of Game Engine Development as well which has a point type and an affine transform type.

I would be interested to hear what other users think. I seem to recall @repi liking not having separate point and vector types, but maybe I'm mis-remembering.

Other prior art in Rust includes cgmath and euclid.

bitshifter avatar Feb 24 '21 09:02 bitshifter

In my pathtracer I wrap all of Glam's types in my own math library, and have separate Vector and Point types. In fact, I even have a separate Normal type as well, because surface normals also transform differently than either points or standard vectors under non-uniform scaling.

So I definitely understand the desire for differentiating these by type. I do it myself. And letting the type system help you is typically a win.

Nevertheless, my gut feeling is that Glam sits a little lower-level than that. My perspective is that Glam isn't so much a math library as it is a SIMD library targeted at performant low-dimensional linear algebra. In other words, it's not trying to encode the rules of linear algebra, it's just trying to provide efficient implementations of the operations needed to do linear algebra.

For example, I don't think my Normal type belongs in Glam, even though lots of people work with surface normals. What does belong in Glam are the functions needed to make implementing my own performant Normal type trivial.

I think the main argument for quaternions having their own type is less about mathematical distinction, and more about making the API easier to follow. If you take a look at the Quat type, it has a number of methods that would become confusing (or at least cluttering) to cram into Vec4. But vectors and points (and normals) don't really have that problem with each other.

Lastly, I feel like giving vectors and points separate types might make it a little less clear which type is "agnostic" (for lack of a better word) for use-cases that don't care about or don't want that distinction.

Anyway, that's my take on it. But I'm certainly not the source of truth for this crate! And everything I've written above is just what I personally would ideally like Glam to be. Reasonable people can absolutely disagree. And I don't think it will practically affect me either way as a user of the crate, so I won't really be put off regardless of what the decision ends up being here.

cessen avatar Feb 27 '21 12:02 cessen

I would be interested to hear what other users think. I seem to recall @repi liking not having separate point and vector types, but maybe I'm mis-remembering.

Other prior art in Rust includes cgmath and euclid.

Traditionally and coming from gamed C++ land I've been a bit against higher-level point & normal vector types and such, and I do also think glam's core and nice simplicity comes from the main focus of the lower-level vector types.

But it is indeed an interesting spectrum and as with higher level types like affine transforms, and Rust's general preference towards explicit and constrained specific types. So I think can be of interest to sketch on optional higher level point type here also, and it wouldn't be intrusive I think.

But it can be a bit hard to know where to draw the line in general, do we have a Normal type also, how about Ray or Line, Plane? We actually have some of those in our own even higher level abstraction on top of glam. But that is likely premature worrying and I think @emilk's focused suggestion around a point/position type is overall sound and interesting to explore (disclaimer: we also work together 😀)

Name Point3 is popular in a lot of libraries. Pos3 has the benefit of being shorter and matching the length of Vec3. I don't feel strongly either way.

Same opinion here, with a slight preference towards Pos3 for the same reason.

repi avatar Mar 10 '21 18:03 repi

I wanted to add a few more thoughts on this.

I'd certainly be interested in trying out a point type to see how it feels and see how much churn it causes when applying it to an existing math heavy code base and to get a better feel for using it.

A point type in conjunction with an affine type would mean affine * point and affine * vector can do the correct and optimal thing and I could potentially remove some Mat4 methods like transform_vector3 and transform_point3. There is still project_point3 which does a perspective divide, that one can't be removed unless you add a Perspective type which is a whole other can of worms.

Adding an additional type, whatever it ends up being named also raised the question should there be a Pos3A?

I decided against adding a point type when I initially wrote glam because I hadn't seen one in any of the C++ game math libraries I had used and they seemed reasonably uncommon in other math libraries I looked at. I made glam to be familiar to C++ game devs, with myself being the target audience initially. While I don't think "this is the way things have always been done" is a good justification to not try something, the down side could be that porting code or ideas from C++ or C# engines, books and articles, GLSL or HLSL may become more difficult if a point type was added. The programmer now has to divert time to thinking about which variables should be vectors and which should be points.

Now days there are a few projects I know about using glam and probably a lot that I don't. It's hard to communicate with most users to ask them about big decisions like that and I would definitely want to cast a wide net for feedback before committing to such a change.

As I said I'm definitely interested in experimenting with a point type if I find the time, but, it could be a tough sell at this, um, point.

bitshifter avatar Mar 17 '21 10:03 bitshifter

Please don't remove methods like transform_vector3 and transform_point3, even if you decide to add a Point type. They're helpful when you don't know, or have forgotten the math behind certain operations. They're useful even when the named function serves mostly as documentation or reference.

aloucks avatar Mar 17 '21 16:03 aloucks

Perhaps there's room for a crate that sits on top of glam and exposes higher-level linear algebra concepts like normals, points and planes. Glam is then more of a low-level SIMD library like @cessen says. The danger would be that this crate becomes a "mathematician's math library" like nalgebra, alienating game programmers who just want to get things done.

msvbg avatar Mar 27 '21 11:03 msvbg

The recent refactor moved the majority of the functionality into a core library which could be reused by a different high level interface. It's currently all internal, because I didn't want to supporting it as a separate crate at this stage, but it could be used for this purpose.

bitshifter avatar Mar 28 '21 21:03 bitshifter