Skip to content
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

Implement Mul<Transform...> for Vector2, Vector3 #1082

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Yarwin
Copy link
Contributor

@Yarwin Yarwin commented Mar 16, 2025

Implements Mul<Transform...> for Vector2, Vector3.

We already support Transform * Vector so the reverse should be supported as well. Tests are translated 1:1 from Godot (the implementation haven't really changed for 12 years and I don't expect to see any changes here anytime soon).

Godot Impl

Multiplying Godot Vector x Transform results in performing XFormInv.

Initially I did it with macro, but it is not worth it.

@Yarwin Yarwin added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Mar 16, 2025
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1082

@Bromeon
Copy link
Member

Bromeon commented Mar 16, 2025

We already support Transform * Vector so the reverse should be supported as well.

Godot supports it, but is it intuitive? Mathematically, you cannot multiply a 2x1 column vector with a 2x2 matrix.
You can multiply a 1x2 row vector with a 2x2 matrix, but that gives a different result than here.

Since there is already the more explicit xform_inv for this operation, where does the need for an operator come up?

@Yarwin Yarwin marked this pull request as draft March 16, 2025 14:25
@Bromeon
Copy link
Member

Bromeon commented Mar 19, 2025

To elaborate a bit on my answer...

The risk I see is in situations like these:

let rotate: Transform3D = ...;
let rotate2: Transform3D = ...;
let rotated: Vector3 = ...;

let transform = rotate2 * rotate * rotated;

Now someone wants to change order of rotations, but the refactor happens at 3:43am:

let transform = rotate2 * rotated * rotate;

Code compiles, everything seems fine. The developer commits, doesn't test... next day another developer spends 2h debugging because some object is in the wrong place, and that code looks fine at first.

Of course it's an artificial scenario, and better variable names would help, but still, this class of issues doesn't exist with a named function. It's also much easier to recognize inverse-multiply if it doesn't look like regular matrix multiplication.


There is also more nuance here. Godot docs:

Inversely transforms (multiplies) the Vector3 by the given Transform3D transformation matrix, under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection is fine, scaling/skew is not).

So a * b isn't simply undoing b * a in all cases.


Maybe also noteworthy is that GDScript defines a lot of operators. Which is fine, with its focus on rapid prototyping and convenience rather than type-safety. For Rust, a lot of these operators would be considered weakly typed or straight unintuitive; I mean what's float ^ bool or object && int supposed to do?

Point being, just because GDScript offers an operator doesn't mean we have to expose it as an operator, too. We should definitely expose the functionality for completeness. In obscure cases, Variant::evaluate() is a decent catch-all.

In this case, we should probably add a named method, like it was the case in Godot 3 (I couldn't find why this was changed, might be interesting).

Transform3D::xform_inv(self, v: Vector3) -> Vector3

This operation also exists for some other matrix-like types, such as Projection and Basis.

Godot also uses an direct implementation and not transform.inverse() * vector.

@Yarwin
Copy link
Contributor Author

Yarwin commented Mar 20, 2025

I do agree, and I would be even more strict here – transform * vector operation makes no sense as well. We should either support both (compatibility with gdscript) or none; otherwise it is confusing.

In general, I would propose creating methods xform and xform_inv for Transform2D and Transform3D, maybe exposing basis_xform and basis_xform_inv for Transform3D (for some reason it is present only on Transform2D?) and removing deprecating Mul<VectorN> for TransformND (basis * vector is fine).

In this case, we should probably add a named method, like it was the case in Godot 3 (I couldn't find why this was changed, might be interesting).

Methods has been removed with this PR, with comment stating that they are too complex and operators should be used instead 🤔 : godotengine/godot#42780

image

image

@Bromeon
Copy link
Member

Bromeon commented Mar 20, 2025

transform * vector operation makes no sense as well.

Why does it not make sense? matrix * vector is a well-defined mathematical operation, and pretty much all libraries support matrix-vector multiplication via operator. There's also a great series of 3Blue1Brown on Youtube.

There are also very practical reasons to support chaining of it:

rotation * scale * vector
// vs.
(rotaton * scale).xform(vector)

We should either support both (compatibility with gdscript) or none; otherwise it is confusing.
[...]
maybe exposing basis_xform and basis_xform_inv for Transform3D (for some reason it is present only on Transform2D?)

Good call about basis_xform() + basis_xform_inv(), maybe we can add them to Transform3D.

I would definitely keep * for matrix * vector. Do we then need an additional xform()? No one has asked for it in the last years now, seems like people are fine with the * operator 🤔

xform_inv() is used much more rarely than *, so I think it's fine to have a named method for one but not the other...


Methods has been removed with this PR, with comment stating that they are too complex and operators should be used instead 🤔 : godotengine/godot#42780

Thanks for looking this up! (Note to self: need to expand core/variant_call.cpp to find it).

It's interesting that they found xform_inv too complex but vector * matrix more intuitive 🤔
Also, the C++ classes still have xform_inv.

@Yarwin
Copy link
Contributor Author

Yarwin commented Mar 20, 2025

Why does it not make sense?

Because it multiplies component of a transform – basis – with vector and adds origin afterwards. I.e. – it performs xform, instead of matrix multiplication, while docs state that both Transform2D and Transform3D are matrices.

Transforms are (nxn+1) matrices (n rows, n+1 columns) while vectors have n elements.
Matrices nxn+1 and nx1 (column vector) have incompatible dimensions (to perform multiplication, the number of columns in the first matrix must be equal to number of elements in the vector).

therefore

my_transform3d.basis * my_vector3 + my_transform3d.origin == my_transform3d * my_vector3;
my_transform2d.basis_xform(my_vector2) + my_transform2d.origin == my_transform2d * my_vector2;

Godot docs explicitly state that * operator converts positions between transforms.

I have no idea how common this kind of operator overload is outside Godot 🤔


RE: exposing basis_xform, basis_xform_inv on Transform3D

I start questioning if it makes sense; We don't have Basis2D, and in case of Transform3D we can just access its basis 🤔

@Bromeon
Copy link
Member

Bromeon commented Mar 20, 2025

Transforms are (nxn+1) matrices (n rows, n+1 columns) while vectors have n elements.
Matrices nxn+1 and nx1 (column vector) have incompatible dimensions (to perform multiplication, the number of columns in the first matrix must be equal to number of elements in the vector).

You're right, I glanced over this part.

The mathematical operation that includes translation would be a 4x4 matrix, multiplied by a 4x1 vector:

4x4 matrix multiplication

But since the last row are constant values, they don't need to be stored. So game developers usually use 3x4 matrices.

This representation is indeed quite common in gamedev:

@lilizoey
Copy link
Member

I think most Rust libraries dont overload Mul for 3d-affine transformation matrices and 3d vectors. From what i can tell neither glam, ndarray, nalgebra, nor ultraviolet do. glam for instance has transform_point3 and transform_vector3.

It'd be a breaking change but it may make sense to just remove them in our library too.

@Yarwin
Copy link
Contributor Author

Yarwin commented Mar 20, 2025

Alright, then it would make sense to do the following:

  • Create method xform for Transforms (which does what transform * vector does currently). Transform2D doesn't have basis (there is proposal that aims to refactor the godot's math and add Basis2D too) and implementing one in godot-rust would only increase the confusion
  • I won't implement xform_inv for Transforms, and won't implement basis_xform… for Transform3D (one shall do transform.basis… instead). The former can be done by transform.affine_inverse().xform(...) the latter exists for Transform2D solely because it lacks any Basis.
  • Remove Mul<Vector> for Transforms (that's breaking change) – impls and methods in impls can't be deprecated sadly
  • Mul<Vector3> for Basis is alright (multiplying nxn matrix by vector with n elements is 100% straightforward)

@Bromeon
Copy link
Member

Bromeon commented Mar 20, 2025

RE: exposing basis_xform, basis_xform_inv on Transform3D

I start questioning if it makes sense; We don't have Basis2D, and in case of Transform3D we can just access its basis 🤔

Good point -- that's probably the reason why it doesn't exist in Godot for 3D transforms.
Transform3D::basis is a public field in Rust, so transform.basis * vec would also work.

Maybe we should briefly mention this in Transform3D docs?


I think most Rust libraries dont overload Mul for 3d-affine transformation matrices and 3d vectors. From what i can tell neither glam, ndarray, nalgebra, nor ultraviolet do. [...]

It'd be a breaking change but it may make sense to just remove them in our library too.

Oh, interesting. I wonder if the design choice here was -- since those libraries offer many different matrix types -- to only implement Mul where it's strictly mathematical. ndarray/nalgebra aren't gamedev libraries though, so focus on math correctness makes sense.

glam for instance has transform_point3 and transform_vector3.

This one is interesting, transform_vector3 explicitly does not include the translation, while transform_point3 does. So it would be like transform.basis * vector in godot-rust (or transform.basis_xform(vector) if we had that).

I'm not sure about removing Transform3D * Vector3 as it exists in Godot (unlike the reversed op). Transform2D and Transform3D are the most common matrices, and their purpose is specifically to include translation. So the semantics of "applying the transform" are sufficiently clear, and * makes common use cases simpler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants