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 normalize_and_get_length() to Vector2 and Vector3 #56567

Closed
wants to merge 1 commit into from

Conversation

lawnjelly
Copy link
Member

@lawnjelly lawnjelly commented Jan 6, 2022

Length is calculated as part of the normalize routine. This is often desired, and also can be used to flag the condition where the Vector was too small to normalize (by returning zero length).

Helps address #74852

Notes

Most engines I've worked on have returned the length from normalize, as this can be a useful value in a lot of cases, and should cost nothing from c++ if not used (as it should be optimized out).

  • In a PR meeting it was decided this would be better as a separate function, rather than modifying the existing normalize().
  • Returning zero if less then EPSILON allows users to deal with error cases where the input Vector length is small.
  • Due to the ability to handle error conditions (over the existing normalized() bound function) the function is bound, in order to be available from script etc.
  • I noticed that Vector3::normalize() sets all values to zero if length is zero (curiously Vector2 does not do this). I'm presuming this is to get a standard value for later comparisons because of the different bit representations near zero. This didn't seem appropriate to do in this PR with an epsilon, but can change if desired.

This can be done longhand which is what I normally end up doing, e.g.:

real_t length = myvec.length();
if (length >= EPSILON)
{
myvec /= length;
}
// do something with length

but the form enabled by the PR is more concise and less error prone (especially epsilons can be standardized in the normalize function):

real_t length = myvec.normalize_and_get_length();
if (length == 0)
{
// Failed ...
}
// do something with length

or simply

if (myvec.normalize_and_get_length() == 0)
{
// Failed
}

(This is one of those rare occasions we can do an exact floating point comparison)

@lawnjelly lawnjelly requested a review from a team as a code owner January 6, 2022 17:58
Copy link
Member

@aaronfranke aaronfranke left a comment

Choose a reason for hiding this comment

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

Note: This would only be for internal use. normalized is what's exposed, and it returns a Vector2.

Comment on lines 255 to 262
real_t l = x * x + y * y;
if (l != 0) {
l = Math::sqrt(l);
x /= l;
y /= l;
return l;
}
return 0.0;
Copy link
Member

Choose a reason for hiding this comment

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

Could be simplified:

Suggested change
real_t l = x * x + y * y;
if (l != 0) {
l = Math::sqrt(l);
x /= l;
y /= l;
return l;
}
return 0.0;
real_t l = x * x + y * y;
if (l != 0) {
l = Math::sqrt(l);
x /= l;
y /= l;
}
return l;

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup sure. 👍
I was intrigued to find in godbolt that the assembly is slightly different.

Even adjusting for the literal issue I mentioned in #56396 (and is probably addressed in a separate PR if we get some agreement on it), I wonder if it is trying to account for signed zero. We'd have to time them to see which was "better", I don't expect there's a lot in it! 😁

Copy link
Member

@aaronfranke aaronfranke Jan 6, 2022

Choose a reason for hiding this comment

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

-0.0 would be the only case that is different, so that has to be it. I also checked NaN but that should enter the if and then explode things there so it wouldn't be affected by this.

@lawnjelly
Copy link
Member Author

PR Meeting: Add normalize_and_get_length() separate function if this has enough use, rather than change normalize().

@lawnjelly lawnjelly marked this pull request as draft February 3, 2022 09:07
@YuriSizov YuriSizov modified the milestones: 4.0, 4.x Feb 12, 2023
@lawnjelly lawnjelly changed the title Return length when calling normalize() Add normalize_and_get_length() to Vector2 and Vector3 Mar 13, 2023
@lawnjelly lawnjelly marked this pull request as ready for review March 13, 2023 09:46
@lawnjelly lawnjelly requested a review from a team as a code owner March 13, 2023 09:46
@@ -241,6 +242,17 @@ _FORCE_INLINE_ bool Vector2::operator!=(const Vector2 &p_vec2) const {
return x != p_vec2.x || y != p_vec2.y;
}

real_t Vector2::normalize_and_get_length() {
real_t lengthsq = length_squared();
if (lengthsq >= (real_t)CMP_EPSILON) {
Copy link
Member

Choose a reason for hiding this comment

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

Should it be CMP_EPSILON or CMP_EPSILON2

Copy link
Member Author

@lawnjelly lawnjelly Mar 13, 2023

Choose a reason for hiding this comment

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

Ah yes, I'm expecting some discussion on the epsilon, and will need a bit of testing. I saw CMP_EPSILON2 but wanted to make sure it is ok for this use with 32 bit real_t. But you are correct we are using squares here.

It has to be a value that is large enough that when sqrted afterwards, it still provides accuracy when used as the divisor. So I think a bit of testing will be good to check this in practice rather than going straight for the smaller CMP_EPSILON2 (without knowing how it was tested).

Copy link
Member

Choose a reason for hiding this comment

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

I'm fully agnostic and the only difference with 64 bit would be the lower limit on magnitude

Copy link
Member Author

@lawnjelly lawnjelly Mar 13, 2023

Choose a reason for hiding this comment

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

32 / 64 bit epsilon differences can be quite difficult.

Last time we discussed it (that I remember), reduz was in favour of keeping the 32 bit epsilons when compiling as 64 bit. This loses out on some potential accuracy in 64 bit, but if you use different epsilons you open up a massive surface of difficult to find bugs. Epsilon bugs are some of the worst imo. 😁

Anyway I'm doing some empirical testing on the epsilon as we speak. 👍
Ogre looks like it uses 1e-8f, which is larger than CMP_EPSILON2 (which is 1e-10f).

Copy link
Member

Choose a reason for hiding this comment

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

Epsilon bugs are some of the worst imo.

Agreed

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok after (I hope) some testing, I'm satisfied that CMP_EPSILON2 should be okay at 32 bit, so I've pushed a version with this epsilon.

I tested in gdscript then in a c++ testbed. Of course there's always the chance I messed up, it's pretty easy to mess up, with math precision. 😁

@lawnjelly lawnjelly force-pushed the normalize_length branch 2 times, most recently from 917b24e to dec5096 Compare March 14, 2023 12:47
<return type="float" />
<description>
Scales the vector to unit length, and returns the original length.
If the original vector was too small to normalize, the length returned is [code]zero[/code].
Copy link
Member

@aaronfranke aaronfranke Mar 14, 2023

Choose a reason for hiding this comment

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

This would be the only method exposed to GDScript that mutates the struct. That's a bit odd. Is this method necessary to expose, or is it intended to be an internal helper for the C++ code?

Copy link
Member

@AThousandShips AThousandShips Mar 14, 2023

Choose a reason for hiding this comment

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

There are some issues with it because of access issues with GDScript, primitive types are non-mutable when returned, including as properties, GDScript passes them by copy not reference, unless that has changed, as in mutating functions cannot affect them

See #70995

Copy link
Member Author

Choose a reason for hiding this comment

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

If you can think of an easy way to return two values efficiently from bound functions, I'm happy to modify the PR.

If we don't want it to mutate the struct, we need to return the new Vector and the length (or the new Vector and success or failure).

Originally this was non-bound, but it is now also a potential candidate solution to the problem in #74852, and whichever solution we go with needs to be available from script.

Another non-mutating alternative would be a normalized() function that returns either the normalized vector or failure (maybe a Vector or NULL? can that work?).

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps then for binding returning a variant that is either the normalized value or NULL is the way to go try_normalized() or something like that?

I can modify this particular PR to have this function unbound as before, but will be good to get some discussion going on how to solve the gdscript problem.

Copy link
Member

@AThousandShips AThousandShips Mar 14, 2023

Choose a reason for hiding this comment

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

It could return an array or dictionary, somewhat cumbersome and inefficient but it gets it done, also I like try_normalized

Length is calculated as part of the normalize routine. This is often desired, and also can be used to flag the condition where the Vector was too small to normalize (by returning zero length).
@lawnjelly
Copy link
Member Author

Closing in favour of #74912 , as it appears that modifying in place isn't good for bound functions.

@lawnjelly lawnjelly closed this Mar 14, 2023
@YuriSizov YuriSizov removed this from the 4.x milestone Dec 2, 2023
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.

6 participants