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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement some of the Transform2D API in rust #791

Merged
merged 1 commit into from Oct 30, 2021

Conversation

setzer22
Copy link
Contributor

@setzer22 setzer22 commented Oct 5, 2021

As we discussed, here's an initial implementation of the Transform2D. The implementation is currently untested, so I'm marking it as a work-in-progress. I'm just posting this early so we can discuss if you thing this is the right approach 馃憤

The code closely follows the exposed GDScript API and even copies its documentation strings. The Implementation is adapted from Godot's C++ source code, delegating to glam (via Vector2) when necessary.

I decided against adding a conversion between glam::Affine2 and Transform2D because it was not immediately clear to me if the mapping would be 1:1 (godot's Transform2D does not necessarily need to be an affine transformation, although it's assumed in certain places).

Instead, I decided to just follow the C++ implementation, which was simple enough, to ensure results would be predictable when porting code from GDScript. We are still using glam underneath when delegating all of Vector3's operations, which happens in many places.

@setzer22 setzer22 marked this pull request as draft October 5, 2021 18:24
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot!

Style-wise, you could use Self to refer to the current type, and use #[inline] for methods. I'm not a huge fan of the latter, but we have currently configured Clippy to check it. Might need to do a benchmark whether it has an impact on speed/code size.

One thing I'm not very sure about, is the getters/setters for translation (=origin), rotation and scale. Their presence kind of implies that the transform is affine, but with field public, this invariant cannot be upheld. What do we return if someone creates a non-affine transform? Even something like shear (which is affine) cannot be represented.

Also, I would like to go with the existing convention for getters that have no get_() prefix.

I haven't checked the algorithms themselves yet, this is where tests might come in handy.

Comment on lines 35 to 78
pub fn from_rotation_position(rotation: f32, position: Vector2) -> Transform2D {
todo!()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit a strange method, even the way it exists in Godot. Several things are unclear:

  • Is position actually the translation?
  • Is there a scale component (length of position)
  • How does the resulting transform look, if I had to describe it in terms of the identity + rotated(), translated() and scaled()?

I'm also OK to remove this method for these reasons; we already use sometimes more idiomatic and expressive APIs rather than providing a 1:1 GDScript port.

For example, in interpolate_with() below you use this method, but still have to provide a scale a posteriori; it would have been clearer to use translated() & Co.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is some value in keeping a method like this, as it seems to be the most widely used constructor used in GDScript. It is a bit unnatural to think of rotations in terms of their axis vectors when, in 2d, rotations are typically represented with a single angle. So having a more user-friendly constructor feels like a good idea.

What we can do, is extending the API by also accepting a scale vector, and have the implementation be expressed in terms of .translated(), .rotated() and .scaled() to avoid code duplication. I'm including a proposal for this on my next commit 馃憤

gdnative-core/src/core_types/transform2d.rs Outdated Show resolved Hide resolved
gdnative-core/src/core_types/transform2d.rs Outdated Show resolved Hide resolved
Comment on lines 80 to 88
/// Returns the transform's origin (translation).
pub fn get_origin(&self) -> Vector2 {
self.origin
}

/// Sets the transform's origin (translation).
pub fn set_origin(&mut self, origin: Vector2) {
self.origin = origin;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these 2 methods are not useful, since origin is already public.

What could be an option is to name them set/get_translation() instead, that would be symmetric with the methods concerning rotation and scale. Would need to be made obvious in the doc that for affine transforms, it's the same as accessing origin directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this method is not very useful, I just added it to be symmetrical with the rotation and scale properties. But I think it makes sense to remove it. This way users won't get confused with two different things that are essentially the same.

Would need to be made obvious in the doc that for affine transforms, it's the same as accessing origin directly.

Actually, if we keept this implementation, it's not just for affine transforms, the method is always accessing the origin field directly 馃槄

gdnative-core/src/core_types/transform2d.rs Show resolved Hide resolved
gdnative-core/src/core_types/transform2d.rs Outdated Show resolved Hide resolved
@Bromeon Bromeon added c: core Component: core (mod core_types, object, log, init, ...) feature Adds functionality to the library labels Oct 5, 2021
Copy link
Contributor

@jacobsky jacobsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR! It looks good to me and I agree with all points on @Bromeon 's feedback.

One thing that I did notice is that the clippy lints requesting #[inline] fail, so please make sure to check with clippy that all of the necessary inlining has been done :)

setzer22 added a commit to setzer22/godot-rust that referenced this pull request Oct 9, 2021
@setzer22
Copy link
Contributor Author

setzer22 commented Oct 9, 2021

Thanks @Bromeon and @jacobsky for the reviews! I think I've addressed all of the comments (please let me know if you think there's anything else missing).

I will have a look now at some tests against the godot API to make sure the current implementation is working as expected.

Copy link
Contributor

@jacobsky jacobsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the good work! I think it's looking good. There were a couple minor things that I noticed related to the comment blocks.

@@ -1,10 +1,216 @@
use super::Vector2;

/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent
/// transformations such as translation, rotation, or scaling. It consists of a three
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight nitpick. "It consists of three Vector2 values" without the 'a'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! In my defense I'll say the typo was copied straight from the Godot docs 馃榿

@@ -1,10 +1,216 @@
use super::Vector2;

/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent
/// transformations such as translation, rotation, or scaling. It consists of a three
/// Vector2 values: x, y, and the origin.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Transform2D {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe my understanding is a little off here, but I thought that the x and y components were there to represent the scale and rotation along each whereas the origin represents the translation.

If I'm mistaken please let me know.

Copy link
Contributor Author

@setzer22 setzer22 Oct 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the x and y components are the basis vectors of the transformation. So they can represent both a rotation and a translation of the coordinate space defined by the transform.

I don't think this docstring is stating otherwise, but perhaps the wording could be a bit better. I'll change it so it calls the vectors "x axis" and "y axis" instead.

We had to keep the same x and y names to be consistent with Godot's naming conveniton, but I prefer writing x_axis and y_axis to make the distinction clear where possible (such as in function arguments).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it's not the doc string that I was commenting on, it was the x, y and origin that were following the doc string. Apologies for the confusion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular the following

/// The x basis vector of the transform. Objects will move along this vector when
/// moving on the X axis in the coordinate space of this transform
pub x: Vector2,
/// The y basis vector of the transform. Objects will move along this vector when
/// moving on the Y axis in the coordinate space of this transform
pub y: Vector2,
/// The origin of the transform. The coordinate space defined by this transform
/// starts at this point.
pub origin: Vector2,

It feels a smidge like x and y translate the object when they should be morphing it, unless my understanding is wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that x and y are terrible names, and to be honest, I don't think Godot having them is reason enough to keep them. It's not like there's automated tooling to convert GDScript -> Rust. It's only important that the manual mapping is more or less clear.

This might be a candidate for #773. Maybe basis_x and basis_y would be better, but I'm open to suggestions. The 3D class Transform has the fields basis and origin.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?

setzer22 added a commit to setzer22/godot-rust that referenced this pull request Oct 9, 2021
@setzer22
Copy link
Contributor Author

setzer22 commented Oct 9, 2021

Just uploaded a first draft for the tests. Since I'm new at this and there's quite a bit more work to test the remaining functions, I'd like to wait for some early feedback before continuing. If you thing the current strategy's fine, I can go ahead and add the remaining methods 馃憤

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Added some comments.


let new_transform_godot = unsafe {
let mut tr = Transform2D::IDENTITY;
let tr_p = &mut tr as *mut _ as *mut sys::godot_transform2d;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be encapsulated in a sys() or to_sys() method on the transform. Same for from_sys() below. You could have a look at how Color does it.

Also, could you only wrap the unsafe statements in unsafe {}, even if it means having 2? If things explode, it's easier to locate the potential places :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed a slight inconsistency with the meaning of sys(), from_sys() and to_sys(). For instance, in Vector2, sys() performs a cast from &Vector2 to *const godot_vector2. This is the version I implemented. But in Color, it transmutes to a reference instead: from &Color to &godot_color. Which convention should we keep for Transform2d?

Moreover, In this case calling the constructor requires passing in a *mut godot_transform2d. Using sys() is not useful here since we need a mutable pointer. Should I add a sys_mut() to encapsulate this logic instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the docs and the dark corners of the Nomicon:

transmute is incredibly unsafe. There are a vast number of ways to cause undefined behavior with this function. transmute should be the absolute last resort.

Get out of our way type system! We're going to reinterpret these bits or die trying! Even though this book is all about doing things that are unsafe, I really can't emphasize that you should deeply think about finding Another Way than the operations covered in this section. This is really, truly, the most horribly unsafe thing you can do in Rust. The guardrails here are dental floss. [...] The ways to cause Undefined Behavior with this are mind boggling.

We might want to go with pointer casts, while dangerous they don't entirely suspend the type system.

Yes, sys_mut() is OK.

gdnative-core/src/core_types/transform2d.rs Show resolved Hide resolved
gdnative-core/src/core_types/transform2d.rs Outdated Show resolved Hide resolved
@Bromeon
Copy link
Member

Bromeon commented Oct 10, 2021

Let's run some intermediate tests...
bors try

bors bot added a commit that referenced this pull request Oct 10, 2021
@bors
Copy link
Contributor

bors bot commented Oct 10, 2021

try

Build failed:

@Bromeon
Copy link
Member

Bromeon commented Oct 14, 2021

The current MSRV (minimum supported Rust version) 1.48 does not have clamp stabilized yet, so we get this error:

error[E0658]: use of unstable library feature 'clamp'
   --> gdnative-core/src/core_types/transform2d.rs:190:19
    |
190 |         let dot = f32::clamp(v1.dot(v2), -1.0, 1.0);
    |                   ^^^^^^^^^^
    |
    = note: see issue #44095 <https://github.com/rust-lang/rust/issues/44095> for more information

If we want to keep that compatibility, we'd probably have to write our own clamp for now...

Let's see if there are other issues:
bors try

bors bot added a commit that referenced this pull request Oct 14, 2021
@bors
Copy link
Contributor

bors bot commented Oct 14, 2021

try

Build failed:

@setzer22
Copy link
Contributor Author

The current MSRV (minimum supported Rust version) 1.48 does not have clamp stabilized yet, so we get this error:
If we want to keep that compatibility, we'd probably have to write our own clamp for now...

Oops, sometimes I forget I'm on nightly. Thanks for catching that! f32::clamp is a very handy method! 馃檪

I will add an implementation for the time being, with a comment that says it can be removed once f32::clamp is stabilized.

@setzer22
Copy link
Contributor Author

I pushed a new version with one more test case, which uncovered several bugs in the implementation. I will keep adding more test for the remaining functions and let you know once it's ready.

@Bromeon I left a few replies on your review's comments. Please have a look whenever you have some time!

gdnative-core/src/core_types/transform2d.rs Show resolved Hide resolved
Comment on lines 68 to 78
#[inline]
pub fn from_rotation_translation_scale(
rotation: f32,
translation: Vector2,
scale: Vector2,
) -> Transform2D {
Self::IDENTITY
.translated(translation)
.rotated(rotation)
.scaled(scale)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be clearer if the method name and parameter order matched the order of transform application, no?

Also, doc is no longer matching the parameters.

Comment on lines 253 to 257
#[doc(hidden)]
#[inline]
pub fn to_sys(self) -> sys::godot_transform2d {
unsafe { std::mem::transmute(self) }
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like this method it used; I would remove it. We can always add it if necessary.

@Bromeon
Copy link
Member

Bromeon commented Oct 14, 2021

bors try

bors bot added a commit that referenced this pull request Oct 14, 2021
@bors
Copy link
Contributor

bors bot commented Oct 14, 2021

try

Timed out.

@Bromeon
Copy link
Member

Bromeon commented Oct 14, 2021

bors try

bors bot added a commit that referenced this pull request Oct 14, 2021
@bors
Copy link
Contributor

bors bot commented Oct 14, 2021

try

Build succeeded:

@setzer22 setzer22 marked this pull request as ready for review October 20, 2021 16:20
@setzer22
Copy link
Contributor Author

I've just added a few more tests and I believe this is ready for a final review 馃槃

bors bot added a commit that referenced this pull request Oct 25, 2021
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good, just small remarks/questions.
Maybe @jacobsky could have another look?

After that, once you combine the commits to one, we can merge it in my opinion. Let's get it out there, addendums can still be done later (like the mentioned field names).

@@ -1,10 +1,216 @@
use super::Vector2;

/// A 2脳3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent
/// transformations such as translation, rotation, or scaling. It consists of a three
/// Vector2 values: x, y, and the origin.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Transform2D {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?

let mut inverted = *self;

let det = self.basis_determinant();
// assert!(det != 0.0, "The determinant cannot be zero");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still have this assertion, or maybe a debug_assert!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this? Maybe field renaming should be part of a separate PR, e.g. one for #773?

If you want, I could rename this to axis_x, axis_y and origin. I didn't change it to avoid introducing a breaking change, but I agree it's better naming. Just let me know 馃憤

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still have this assertion, or maybe a debug_assert!?

I was meaning to ask, then forgot 馃槄. The assert was "ported" from Godot's cpp codebase, but then I commented it because I wasn't sure if a panic was acceptable in this kind of function.

A debug_assert! looks like a good tradeoff to me, I'm pushing it with my next commit 馃憤

/// Returns the transform's scale.
#[inline]
pub fn scale(&self) -> Vector2 {
let det_sign = f32::signum(self.basis_determinant());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason why you use the notation f32::signum(val) instead of val.signum()?
signum() is not an associated method.

Copy link
Contributor Author

@setzer22 setzer22 Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason, I'm used to treating these as plain functions (like sin and cos) as it looks more natural and closer to how it's written in math.

But it's best to keep whatever the current convention is 馃憤 I will change ti to val.signum()

@bors
Copy link
Contributor

bors bot commented Oct 25, 2021

try

Build succeeded:

setzer22 added a commit to setzer22/godot-rust that referenced this pull request Oct 25, 2021
@setzer22
Copy link
Contributor Author

After that, once you combine the commits to one

@Bromeon I'm unsure how to do this. There's usually a way to squash the commits when merging on your end and that's typically how I do this.

Do you need me to do anything on my fork? 馃槄

Added some of the Transform2D methods from the Godot API to its Rust
counterpart. The implementation uses rust, but closely follows the
C++ codebase.

Tests have been added to validate that calling the native transform2d
API is equivalent to using the Rust methods.
@setzer22
Copy link
Contributor Author

Alright, I took care of the squashing on my end. Let me know if that works for you 馃槃

@setzer22 setzer22 changed the title [WIP] Implement some of the Transform2D API in rust Implement some of the Transform2D API in rust Oct 26, 2021
@jacobsky
Copy link
Contributor

@setzer22 Thank you for the hard work! I think everything looks good to go!

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 29, 2021

馃敀 Permission denied

Existing reviewers: click here to make jacobsky a reviewer

@jacobsky
Copy link
Contributor

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 29, 2021

馃敀 Permission denied

Existing reviewers: click here to make jacobsky a reviewer

@jacobsky jacobsky self-requested a review October 29, 2021 22:50
@jacobsky
Copy link
Contributor

Odd, I'm listed as a reviewer on this PR, but bors doesn't seem to trust me 馃ぃ
image

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 29, 2021

馃敀 Permission denied

Existing reviewers: click here to make jacobsky a reviewer

@jacobsky
Copy link
Contributor

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 29, 2021

馃敀 Permission denied

Existing reviewers: click here to make jacobsky a reviewer

@jacobsky
Copy link
Contributor

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 30, 2021

Build succeeded:

@bors bors bot merged commit c24925a into godot-rust:master Oct 30, 2021
Bogay pushed a commit to Bogay/godot-rust that referenced this pull request Nov 17, 2021
Added some of the Transform2D methods from the Godot API to its Rust
counterpart. The implementation uses rust, but closely follows the
C++ codebase.

Tests have been added to validate that calling the native transform2d
API is equivalent to using the Rust methods.
bors bot added a commit that referenced this pull request Dec 1, 2021
821: Implement some methods of the Transform API r=Bromeon a=setzer22

Here are some more Transform methods. I followed the same strategy as for #791, where the implementation was done by translating the C++ code to Rust.

I can also add some tests similar to the ones from the previous PR if you'd like 馃憤 Although the code for Transform is far simpler, because it delegates most of the implementation to `Vector3` and `Basis`.

Co-authored-by: Setzer22 <jsanchezfsms@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Component: core (mod core_types, object, log, init, ...) feature Adds functionality to the library
Projects
None yet
3 participants