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

Add move_toward_smooth helper #92236

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

ubitux
Copy link

@ubitux ubitux commented May 22, 2024

  • I didn't test if the documentation in the XML files were rendered correctly because I couldn't find a way to preview it locally; I'd be happy to check it if someone can provide the information Edit: looked it up from within the GUI instead of looking for a local HTML export
  • I'm not sure if I need to document something in the Changelog apparently this is done separately
  • With regards to the API, it might make sense to use a time constant (equal to 1/rate) instead of rate, but I feel like it can be more confusing when there is already delta in the parameters which also a "time unit"

@AThousandShips
Copy link
Member

Please open a proposal to track the support and details of this feature

@ubitux
Copy link
Author

ubitux commented May 22, 2024

Please open a proposal to track the support and details of this feature

Done: godotengine/godot-proposals#9803

@ubitux ubitux force-pushed the move-toward-smooth branch 2 times, most recently from 1daf722 to a129ecb Compare May 22, 2024 09:50
core/math/math_funcs.h Outdated Show resolved Hide resolved
doc/classes/@GlobalScope.xml Outdated Show resolved Hide resolved
doc/classes/Vector3.xml Outdated Show resolved Hide resolved
doc/classes/Vector2.xml Outdated Show resolved Hide resolved
Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

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

Looks good generally, save for the documentation details and the addition of a dedicated method to the engine, especially to handle the single/double precision behavior

core/math/vector2.cpp Outdated Show resolved Hide resolved
core/math/vector3.cpp Outdated Show resolved Hide resolved
@ubitux ubitux force-pushed the move-toward-smooth branch from a129ecb to 41fc1d8 Compare May 22, 2024 10:16
@ubitux ubitux requested a review from AThousandShips May 22, 2024 10:16
@ubitux
Copy link
Author

ubitux commented May 22, 2024

Looks good generally, save for the documentation details and the addition of a dedicated method to the engine, especially to handle the single/double precision behavior

Sorry, what do you mean by "save for"; is it a typo for "[it is] safe for"?

@AThousandShips
Copy link
Member

I mean "except for", an English phrase

Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

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

Otherwise this looks good I believe, will need general approval and decision on the necessity, but looks good code wise

Would be appreciated if you also added tests for expm1 but that's not necessary and can be added later

Edit: We can also consider binding expm1 to scripting down the line as well, but out of scope for this IMO

@ubitux ubitux force-pushed the move-toward-smooth branch 2 times, most recently from 1f25712 to fefec26 Compare May 22, 2024 10:35
@ubitux
Copy link
Author

ubitux commented May 22, 2024

Would be appreciated if you also added tests for expm1 but that's not necessary and can be added later

Sure, added

@AThousandShips
Copy link
Member

AThousandShips commented May 22, 2024

Also, forgot, we might want to add this to the C# glue as well, what do you think @godotengine/dotnet? Can be done in a follow-up unless they think it's relevant to add it there too

But no need to add that right now until we get a full go-ahead from the core team on the need for this

@Calinou
Copy link
Member

Calinou commented May 22, 2024

This will be handy in many demo projects as I keep forgetting how to do smooth lerping in a FPS-independent manner.

@ubitux
Copy link
Author

ubitux commented May 23, 2024

Also, forgot, we might want to add this to the C# glue as well, what do you think @godotengine/dotnet? Can be done in a follow-up unless they think it's relevant to add it there too

I don't mind doing the change but testing it is going to be challenging for me


On an unrelated note, I've been thinking more about the API. Instead of move_toward_smooth(from, to, delta, rate), we could leave the rate handling to the user who would call it like this: move_toward_smooth(from, to, delta * RATE)

Benefits:

  • consistent with move_toward() where the user is expected to scale the delta value themselves: it makes move_toward_smooth() a drop-in replacement of move_toward()
  • allow the user to do delta / TIME_CONSTANT or delta * RATE depending on their taste

Drawbacks:

  • loss of information: the documentation is going to be much more vague about how to use the function
  • it may be more meaningful to instead change move_toward into move_toward(from, to, delta, rate)

Thoughts?

@huwpascoe
Copy link
Contributor

This is definitely a good to have core feature.

Would it make sense to add SLERP versions?

move_toward(from, to, delta, rate)

please not that, worst of all options

Opinion: Delta at the end feels more natural as the other 3 args are user defined whereas delta is unknown: move_toward_smooth(from, to, rate, delta)

@ubitux
Copy link
Author

ubitux commented May 24, 2024

Would it make sense to add SLERP versions?

I'm not familiar enough with the involved maths to provide an educated answer

Opinion: Delta at the end feels more natural as the other 3 args are user defined whereas delta is unknown: move_toward_smooth(from, to, rate, delta)

Ok so we have 3 choices for move_toward_smooth:

  1. move_toward_smooth(from, to, delta, rate) (current proposition, same first 3 arguments as move_toward)
  2. move_toward_smooth(from, to, rate, delta) (@huwpascoe proposition for a more organized arguments order)
  3. move_toward_smooth(from, to, delta) (where delta is multiplied by the user at will for consistency with move_toward)

Is it possible to have Godot core team take on the API?

@ubitux ubitux force-pushed the move-toward-smooth branch from fefec26 to 8852f91 Compare May 27, 2024 01:05
@ubitux
Copy link
Author

ubitux commented May 27, 2024

Since there was no answer I made my pick and pushed a new version.

Changes from last time:

  • Split out the expm1 changes into a dedicated commit since it's a bit off-topic and helps reviewing the main commit
  • Decided on move_toward_smooth(from, to, delta); users are expected to multiply delta themselves to bend the curve; I can revert back or change the prototype to whatever decision is made
  • Added C# support, but this is done blindly (compilation included) because I was unable to test it

@huwpascoe
Copy link
Contributor

Case in point - where does this end?

how many slerp/lerp functions exist?

Copy link
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

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

Tested locally, this doesn't appear to be independent of FPS (or physics tick rate for things in _physics_process()).

I've changed the Voxel demo to make use of move_toward_smooth() for FPS-independent smoothing. The project below uses 120 physics ticks per second; if you change it to 30 in the project settings, notice how player movement acceleration becomes much slower (while it should remain just as fast). Try creating a flat world in the project so you can test this more easily.

The player movement code is in player/player.gd. You can check the original demo here: https://github.com/godotengine/godot-demo-projects/tree/master/3d/voxel

Testing project: voxel_move_toward_smooth.zip

@lawnjelly
Copy link
Member

I've changed the Voxel demo to make use of move_toward_smooth() for FPS-independent smoothing. The project below uses 120 physics ticks per second; if you change it to 30 in the project settings, notice how player movement acceleration becomes much slower (while it should remain just as fast). Try creating a flat world in the project so you can test this more easily.

I haven't checked the test project in detail, but just to point out, this code is purely math, it doesn't touch physics, and should therefore not be tested with a project that uses physics, because that would be conflating two different things. The only relevant factor is physics tick rate.

A suitable test would therefore be something like moving a box across the screen at different tick rates.

This is basic to the scientific method - remove any extraneous variables.
https://www.scribbr.co.uk/research-methods/extraneous-variable/

@ubitux
Copy link
Author

ubitux commented Jun 9, 2024

I've changed the Voxel demo to make use of move_toward_smooth() for FPS-independent smoothing. The project below uses 120 physics ticks per second; if you change it to 30 in the project settings, notice how player movement acceleration becomes much slower (while it should remain just as fast). Try creating a flat world in the project so you can test this more easily.

The code is:

	velocity.x = move_toward_smooth(velocity.x, 0.0, MOVEMENT_FRICTION_GROUND if is_on_floor() else MOVEMENT_FRICTION_AIR)
	velocity.z = move_toward_smooth(velocity.z, 0.0, MOVEMENT_FRICTION_GROUND if is_on_floor() else MOVEMENT_FRICTION_AIR)

You need to use delta otherwise it cannot work

@Calinou
Copy link
Member

Calinou commented Jun 9, 2024

You need to use delta otherwise it cannot work

Right, I had assumed it was automatic for some reason.

@danijmn
Copy link

danijmn commented Oct 3, 2024

Great work, this is both simple to use and immensely useful! This implementation is far more elegant than Unity's Mathf.SmoothDamp.

Just a couple of notes:

  • It's probably better to document the function's behavior when negative delta values are passed (quickly moves towards infinity).
  • You may also want to document the fact that, given the nature of the exponential function, it may take much longer to reach the target value than the user might expect. In particular, I feel like it should be warned that this can cause strange behavior if some piece of code is setup to only run before/after reaching the target value (especially code that was previously using the regular move_toward) - though callers can work around this by testing is_equal_approx(from, to).

@ubitux ubitux force-pushed the move-toward-smooth branch from c486dd0 to 8cff7a0 Compare October 5, 2024 15:11
@ubitux
Copy link
Author

ubitux commented Oct 5, 2024

Great work, this is both simple to use and immensely useful! This implementation is far more elegant than Unity's Mathf.SmoothDamp.

Just a couple of notes:

* It's probably better to document the function's behavior when negative delta values are passed (quickly moves towards infinity).

Shouldn't this remain an undefined or unspecified behavior? Delta is not really supposed to be something else than the delta parameters of the process and physics_process functions.

* You may also want to document the fact that, given the nature of the exponential function, it may take much longer to reach the target value than the user might expect. In particular, I feel like it should be warned that this can cause strange behavior if some piece of code is setup to only run before/after reaching the target value (especially code that was previously using the regular `move_toward`) - though callers can work around this by testing `is_equal_approx(from, to)`.

I added "Note that due to the nature of the exponential function, the return value will quickly get close to the target [param to] but may never reach it. It is recommended to rely on [method is_equal_approx] to test whether the target is reached."

BTW, I did have to make 7 variants of that same documentation (1 gds function, 2 gds vector methods, 2 c# functions, 2 c# vector methods); this is kind of a maintenance burden, I don't know if extending the documentation extensively makes a lot of sense in that situation.

@danijmn
Copy link

danijmn commented Oct 5, 2024

Shouldn't this remain an undefined or unspecified behavior? Delta is not really supposed to be something else than the delta parameters of the process and physics_process functions.

Agreed, and personally I can't think of any good use for a negative value. Though the documentation for the original move_toward mentions that possibility:

Use a negative delta value to move away.

The difference should be immediately obvious though (using the "smooth" version will move away much faster), so I don't think it's really necessary to document this aspect. The other point was much more important, thanks for adding the note.

And yeah, I agree that documenting 7 variants of what is essentially a single function is kind of a burden, but there's no way around that as far as I'm aware of. Speaking of this, once these changes are merged, the example in the tutorial about interpolation (section "Smoothing motion") should eventually be changed,
from
$Sprite2D.position = $Sprite2D.position.lerp(mouse_pos, delta * FOLLOW_SPEED)
to
$Sprite2D.position = $Sprite2D.position.move_toward_smooth(mouse_pos, delta * FOLLOW_SPEED)

@ubitux ubitux force-pushed the move-toward-smooth branch from 8cff7a0 to 8f8fb4f Compare October 7, 2024 10:58
@Repiteo Repiteo modified the milestones: 4.x, 4.4 Oct 16, 2024
@clayjohn clayjohn modified the milestones: 4.4, 4.x Oct 16, 2024
@Mickeon Mickeon requested review from Calinou and removed request for Calinou December 3, 2024 19:15
@Mickeon
Copy link
Contributor

Mickeon commented Dec 3, 2024

Shouldn't this remain an undefined or unspecified behavior? Delta is not really supposed to be something else than the delta parameters of the process and physics_process functions.

You can't really tell the user what to do with this function, I suppose.
Note that move_toward works just fine backwards. But regardless of what it is decided to do with negative value, you should document what happens with them.

And yeah, I agree that documenting 7 variants of what is essentially a single function is kind of a burden, but there's no way around that as far as I'm aware of.

Yyyyyeah. We can't easily maintain all of them in quality, but at least all of them should mention accurate information.

@ubitux
Copy link
Author

ubitux commented Dec 4, 2024

Shouldn't this remain an undefined or unspecified behavior? Delta is not really supposed to be something else than the delta parameters of the process and physics_process functions.

You can't really tell the user what to do with this function, I suppose. Note that move_toward works just fine backwards. But regardless of what it is decided to do with negative value, you should document what happens with them.

In that case, is it OK to document it as undefined behavior? Right now the negative delta doesn't seem useful or meaningful and we may find a more relevant behavior in the future. Documenting a behavior with a negative value implies that it's a legit case (here, as far as I know, it's not).

Also, this PR is opened since 7 months for something fairly small: what really is blocking here?

@Mickeon
Copy link
Contributor

Mickeon commented Dec 4, 2024

In that case, is it OK to document it as undefined behavior?

Yeah.

Also, this PR is opened since 7 months for something fairly small: what really is blocking here?

Core review approval. It's new, exposed API and regardless of size it's likely worth evaluating whether this function is worth having in core.

@Mickeon Mickeon requested a review from akien-mga December 4, 2024 12:14
@passivestar
Copy link
Contributor

Also, this PR is opened since 7 months for something fairly small: what really is blocking here?

not implemented for all the things that lerp is implemented for (vec4, quaternion, basis, color and float angle). since this is meant to be a framerate-independent version of lerp it should either cover everything that lerp covers or only do the basic float version. otherwise the API is arbitrary and inconsistent imo

@danijmn
Copy link

danijmn commented Dec 4, 2024

Also, this PR is opened since 7 months for something fairly small: what really is blocking here?

not implemented for all the things that lerp is implemented for (vec4, quaternion, basis, color and float angle). since this is meant to be a framerate-independent version of lerp it should either cover everything that lerp covers or only do the basic float version. otherwise the API is arbitrary and inconsistent imo

It's not really meant to be a replacement for lerp. IMO lerp should remain a generic interpolation method with a floating-point "weight" parameter in the [0.0, 1.0] range, and it's up to the user to decide where this "weight" comes from. As far as I can see, in Godot, this has always been the intended use.

This function, move_toward_smooth, is meant to be an alternative to move_toward, and the latter is currently only implemented for floats, Vector2 and Vector3 in Godot.

In Unity, this also seems to be the general philosophy. MoveTowards is only implemented for floats, Vector2, Vector3, Vector4 and angles in degrees (to make sure they wrap correctly around 360). SmoothDamp (which is the move_toward_smooth equivalent) is implemented for the same types, except Vector4. Lerp is implemented for a myriad of other types, including quaternions, colors, etc. And that's probably the way it should be, because the interpolation method varies with the type of data to be interpolated, but the 'weight' parameter does not - it's always just a float in the [0.0, 1.0] range, conforming to the user's expectations. That parameter can be supplied by move_toward_smooth if so desired. Vector2 and Vector3 variants can be implemented for convenience (already done in this PR), specially because the original method already implements them.

@ubitux
Copy link
Author

ubitux commented Dec 4, 2024

@danijmn Thank you, I indeed aligned move_toward_smooth on move_toward, so I implemented it only where the latter was.

Vector2 and Vector3 variants can be implemented for convenience, specially because the original method already implements them.

I don't know if I misunderstood something here, but I implemented it for vec2 and vec3, in C# as well

@danijmn
Copy link

danijmn commented Dec 4, 2024

I don't know if I misunderstood something here, but I implemented it for vec2 and vec3, in C# as well

Sorry, bad choice of words! I amended the comment.

@passivestar
Copy link
Contributor

passivestar commented Dec 4, 2024

In Unity, this also seems to be the general philosophy. MoveTowards is only implemented for floats, Vector2, Vector3, Vector4 and angles in degrees (to make sure they wrap correctly around 360)

If we go purely by the existing API methods shouldn't this PR also include rotate_toward_smooth since there's rotate_toward in godot?

On a sidenote to me as a user it doesn't really make sense that there's rotate_toward for 2d rotations but not for 3d rotations, I wouldn't necessarily presume that it's some kind of a design philosophy, it might as well be an oversight. Is it possible that unity and godot are missing methods that would make sense to have to better meet user expectations? Just wanted to bring this to the discussion to make sure we're being intentional and not just blindly copying what has been

@danijmn
Copy link

danijmn commented Dec 13, 2024

If we go purely by the existing API methods shouldn't this PR also include rotate_toward_smooth since there's rotate_toward in godot?

On a sidenote to me as a user it doesn't really make sense that there's rotate_toward for 2d rotations but not for 3d rotations, I wouldn't necessarily presume that it's some kind of a design philosophy, it might as well be an oversight. Is it possible that unity and godot are missing methods that would make sense to have to better meet user expectations? Just wanted to bring this to the discussion to make sure we're being intentional and not just blindly copying what has been

You're right, if we go down the route of matching the existing API, we should also cover rotate_toward.
The implementation is trivial:

static _ALWAYS_INLINE_ double rotate_toward_smooth(double p_from, double p_to, double p_delta) {
	return Math::lerp_angle(p_from, p_to, -Math::expm1(-p_delta));
}
static _ALWAYS_INLINE_ float rotate_toward_smooth(float p_from, float p_to, float p_delta) {
	return Math::lerp_angle(p_from, p_to, -Math::expm1(-p_delta));
}

Some tests:

TEST_CASE_TEMPLATE("[Math] rotate_toward_smooth", T, float, double) {
	// Low delta -> takes a step toward "to", taking into account the shortest angle difference
	CHECK(Math::rotate_toward_smooth((T)0.0, (T)0.75 * (T)Math_PI, (T)0.1) == doctest::Approx((T)0.07137 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)0.75 * (T)Math_PI, (T)0.3 * (T)Math_PI, (T)0.5) == doctest::Approx((T)0.57294 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)0.25 * (T)Math_PI, (T)Math_PI, (T)0.3) == doctest::Approx((T)0.44439 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)-1.25 * (T)Math_PI, (T)1.0) == doctest::Approx((T)-0.97409 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)0.75 * (T)Math_PI, (T)4.1) == doctest::Approx((T)-1.23757 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)Math_PI, (T)3.3) == doctest::Approx((T)-0.98156 * (T)Math_PI));

	// High delta -> immediately converges on "to" without overshooting
	CHECK(Math::rotate_toward_smooth((T)0.0, (T)0.75 * (T)Math_PI, (T)1000.0) == doctest::Approx((T)0.75 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)0.75 * (T)Math_PI, (T)0.3 * (T)Math_PI, (T)1000.0) == doctest::Approx((T)0.3 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)0.25 * (T)Math_PI, (T)Math_PI, (T)1000.0) == doctest::Approx((T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)-1.25 * (T)Math_PI, (T)1000.0) == doctest::Approx((T)-1.25 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)0.75 * (T)Math_PI, (T)1000.0) == doctest::Approx((T)-1.25 * (T)Math_PI));
	CHECK(Math::rotate_toward_smooth((T)-0.5 * (T)Math_PI, (T)Math_PI, (T)1000.0) == doctest::Approx((T)-Math_PI));
}

Adding variants for all other kinds of lerp functions might bring some convenience to the user, but bear in mind that this way of smoothing transitions is not necessarily the best one. It doesn't support setting up different types of easing/transitions like Tween does, so it's not very versatile. It is much more convenient though, as it doesn't require saving any extra state.

IMO adding variants for every type covered by lerp is overkill. Besides, we already have a lot smoothing functions, such as cubic_interpolate_*, ease, smoothstep, Tween.interpolate_value, etc. I like the suggestion of a single exp_smooth_delta function which can be plugged into lerp, or even just exposing expm1, provided that adequate examples are given. But I would still keep move_toward_smooth at the very least. Many users are likely to stumble upon that when looking for the regular move_toward, and in many cases it will be quite an improvement.

@ettiSurreal
Copy link
Contributor

ettiSurreal commented Jan 2, 2025

If we go purely by the existing API methods shouldn't this PR also include rotate_toward_smooth since there's rotate_toward in godot?

Quick clarification because I am partially at fault.
This method is more analogous to lerp than move_toward, so you're probably thinking of lerp_angle.
When I implemented rotate_toward it was initially named move_toward_angle (since it's just move_toward but it behaves like lerp_angle), but after some discussion it was renamed to what it is now. Still to me the "toward" methods are methods that move by an increment, and are meant for linear motion.
I already noted that on the proposal before reading the discussion here, but mentioning that I really don't think these methods should be called move_toward_smooth, since it evidently can cause confusion with similarly named but ultimately unrelated methods, so I'd suggest naming it exp_decay (lerp), exp_decay_angle (lerp_angle) and spherical_exp_decay (slerp) as per Freya's talk on the topic.

On a sidenote to me as a user it doesn't really make sense that there's rotate_toward for 2d rotations but not for 3d rotations

Working on that! These are all analogous for slerp that instead rotate by an increment (radian) instead of a percentage. There are currently a few issues though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide a smooth move_toward