-
-
Notifications
You must be signed in to change notification settings - Fork 21.6k
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
Vector3 normalized() does not return normalized vector for very small nonzero vectors #74852
Comments
I think |
The Vector3 The Vector3 |
No. The values printed by the given reproduction code are sometimes zero, but not usually. Some examples:
These were produced by the given reproduction code, and their length is very different from both one and zero. |
I think this is a floating point precision issue, normalizing a very small (or large) 3d vector involves sqrt() and more division, which can produce pretty inaccurate numbers with very large or small values. Might work better with 64-bit double precision build, some of these issues are just fundamental with 32-bit floats and have to be taken into consideration when doing calculations like this. There might be some algorithms to produce more solid results, but they will probably be slower too. |
When you are not on a double-precision build of Godot multiplying with 0.00000000000000001 is too much for normal float, remove 3-4 zeros and it works again. The editor print displays zero but it is not stored as zero and the internal operations that involve further multiplications and sqrt freak out and flow over. |
This is not the issue. It is just an easy way to replicate the issue in a small amount of code. Getting very small nonzero vectors like this is common when doing complicated vector math, as I said in the opening post. I ran into this issue trying to help someone troubleshoot custom physics code that was breaking because of the cross product operation between two normalized vectors producing an extremely small but nonzero vector. |
Floating point inaccuracies are just something you need to be prepared for and handle when they occur, things will start to break down when you get too large or small values. In this case it might be possible to alter the |
This is partly why a lot of
If you might get near zero length input, you really need to do it manually anyway, because you could get zero length output.
(edited my answer a little because it can be done)
The only issue is that GDScript makes warnings when you don't use a return value I think? Using from gdscript would then become:
This could be added if there was popular support, might have to have a separate function name for backward compatibility / preventing warnings. Something like Or alternatively revamp my old PR #56567 :
But test against an EPSILON instead of zero. This would mean as well as returning the length (which is useful), you could test against a returned length of zero for failure. |
Thinking about this, we should probably add to the documentation that |
I should note, in situations where a non-zero but really small vector is normalized, the result does lead to something closer to being normalized a lot of the time. This means calling .normalized().normalized() on a vector you want normalized can result in this error occuring less than calling it once. Basically, this can be fixed in the normalized() method itself without having to complain about how its the fault of floating point error or that we should just deal with it or that there is no solution. It should be expected behaviour that normalized() returns a vector that passes a isNormalized() check when fed a non-zero vector. Hell, it doesnt even need to have an "exact" length of 1, the point is it just needs to be normalized enough to pass normalization checks like the one that occurs inside of the rotate(axis, angle) method, and the fact that normalize() can return a non-zero vector that isnt normalized should be viewed as a bug that should be fixed and not some feature we throw tape around and ignore and leave to fester |
@lawnjelly Wouldn't it be enough to add an epsilon to the length check? something like:
|
That is probably a good idea for the existing function.. however the problem remains that effectively when using this, unless you can guarantee non small lengths, you need to check for the error condition. This means you have to call e.g. Imo the whole |
I wonder if alternatively something like this would make sense: real_t lengthsq = length_squared();
if (lengthsq < CMP_EPSILON2) {
if (x == 0 && y == 0 && z == 0) {
// x = y = z = 0; // If we'd want to avoid +/- zeros.
return; // Exact zero.
}
// Near-zero which could fail normalizing. Let's scale it by some power of two.
// Is there some specific value which could guarantee the normalization down below won't fail?
const real_t multiplier = 1024.0 * 1024.0;
x *= multiplier;
y *= multiplier;
z *= multiplier;
lengthsq = length_squared();
}
real_t length = Math::sqrt(lengthsq);
x /= length;
y /= length;
z /= length; I mean if the input is such near-zero value then it's indeed likely to be not too meaningful because of the potentially already cumulated floating points errors, and scaling it just to not fail might enlarge the already in there error. So I'm not convinced doing something like this makes sense at all, just sharing a thought. 🙃 |
So there are essentially two problems caused by small values:
Normalization twice or pre-multiplying sound like viable ideas for dealing with (1), but don't deal with (2). And with (2) some use cases might be more sensitive to breakdown in direction resolution than others. 🤔 That suggests something like:
This makes things a bit slower, but performance sensitive loops using normalization should probably be hand optimized (e.g. SIMD / reciprocal sqrt etc). The question is whether we treat the threshold for breakdown in direction as different from the breakdown in unit length, and whether they are significantly different enough to warrant this. I'll have a look in the testbed I wrote yesterday. 👍 UPDATE: One thing we may have to be aware of is subnormals: And making sure the epsilons work ok even when subnormals are flushed to zero, and I think making sure it works well in For instance with Most implementations of What I might do is to write it as SIMD instrinsics and make sure the epsilons work there, because that's the most approximate version that we would encounter I guess. |
Ok incredibly inefficient but this is just for ensuring 32 bit calculations, with flushing denormals to zero, I think this is right:
I'm not convinced the FLUSH_ZERO and DENORMAL_ZERO are working though, as through my debugger the smallest values stored go down to But this does suggest we could get away with 2 epsilons as described above, one threshold for a multiplier to ensure unit length, and a second for complete failure, but probably above zero to ensure consistent results on different platforms. I would suggest maybe staying with As an aside, I suspect that in the recalculation of square length after applying a multiplier in @kleonc 's version, can we simplify it to:
However, given we are dealing with precision issues it may not be wise. 🤔 |
Since nobody has mentioned it, a high-level work-around would be to check if the vector is nearly zero before getting the normalized vector.
Another option is we could add this into Also, I want to mention that you generally should not expect values like 0.00000000000000001 to work at all. If it does, great, but don't expect it to be reliable. If you often need super precise values like this, I recommend compiling Godot with double precision support enabled. |
Not a suggestion, but I wanted to apologize for getting hot headed with my previous comment earlier this thread. I failed to maintain a non-adversarial stance and my own emotions overtook me. Thank you for considering my suggestions/comments in spite of myself at the time. I really like lawnjelly's current idea, the one in the issue just mentioned, and it would be interesting to see that after it gets some testing. This problem is one that had effected me relatively recently and I'm happy to see it be addressed with such speed and consideration as it has been. Thank you all for spending time looking and thinking about this problem as you have been. Again, sorry for my behaviour and thank you for your time |
I am generally more on the side that this should not be fixed, and that whathever you are doing if you have an issue with this is the actual problem. An epsilon check in the normalize will probably not fix your problem anyway so, at most, could be a debug build feature that errors as if it was zero. |
Godot version
4.0.stable
System information
Windows 10
Issue description
See title. Normalized vectors are required for a lot of operations, like axis rotation, so normalized() should always return a real normalized vector.
Extremely small nonzero vectors can be produced by normal, innocuous game code when doing complicated vector math involving cross products etc.
Steps to reproduce
Create a new Node3D scene with the following script, then run the scene and wait for the errors to start coming in:
Values this small CAN AND WILL pop up in real game code without doing things this silly (see "Issue description"). This is PURELY to reproduce the issue in a very small amount of code.
Minimal reproduction project
N/A
The text was updated successfully, but these errors were encountered: