From 50c5ed4876250f785be54b8f6124e7663afa38dc Mon Sep 17 00:00:00 2001 From: lawnjelly Date: Tue, 9 May 2023 13:49:26 +0100 Subject: [PATCH] Make acos and asin safe A common bug with using acos and asin is that input outside -1 to 1 range will result in Nan output. This can occur due to floating point error in the input. The standard solution is to provide safe_acos function with clamped input. For Godot it may make more sense to make the standard functions safe. --- core/math/basis.cpp | 4 ++-- core/math/math_funcs.h | 10 ++++++---- core/math/quaternion.cpp | 3 ++- doc/classes/@GlobalScope.xml | 4 ++-- tests/core/math/test_math_funcs.h | 8 ++++---- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/core/math/basis.cpp b/core/math/basis.cpp index 95a4187062f..bfd902c7e28 100644 --- a/core/math/basis.cpp +++ b/core/math/basis.cpp @@ -807,8 +807,8 @@ void Basis::get_axis_angle(Vector3 &r_axis, real_t &r_angle) const { z = (rows[1][0] - rows[0][1]) / s; r_axis = Vector3(x, y, z); - // CLAMP to avoid NaN if the value passed to acos is not in [0,1]. - r_angle = Math::acos(CLAMP((rows[0][0] + rows[1][1] + rows[2][2] - 1) / 2, (real_t)0.0, (real_t)1.0)); + // acos does clamping. + r_angle = Math::acos((rows[0][0] + rows[1][1] + rows[2][2] - 1) / 2); } void Basis::set_quaternion(const Quaternion &p_quaternion) { diff --git a/core/math/math_funcs.h b/core/math/math_funcs.h index 078320d6203..f96d3a909fb 100644 --- a/core/math/math_funcs.h +++ b/core/math/math_funcs.h @@ -74,11 +74,13 @@ public: static _ALWAYS_INLINE_ double tanh(double p_x) { return ::tanh(p_x); } static _ALWAYS_INLINE_ float tanh(float p_x) { return ::tanhf(p_x); } - static _ALWAYS_INLINE_ double asin(double p_x) { return ::asin(p_x); } - static _ALWAYS_INLINE_ float asin(float p_x) { return ::asinf(p_x); } + // Always does clamping so always safe to use. + static _ALWAYS_INLINE_ double asin(double p_x) { return p_x < -1 ? (-Math_PI / 2) : (p_x > 1 ? (Math_PI / 2) : ::asin(p_x)); } + static _ALWAYS_INLINE_ float asin(float p_x) { return p_x < -1 ? (-Math_PI / 2) : (p_x > 1 ? (Math_PI / 2) : ::asinf(p_x)); } - static _ALWAYS_INLINE_ double acos(double p_x) { return ::acos(p_x); } - static _ALWAYS_INLINE_ float acos(float p_x) { return ::acosf(p_x); } + // Always does clamping so always safe to use. + static _ALWAYS_INLINE_ double acos(double p_x) { return p_x < -1 ? Math_PI : (p_x > 1 ? 0 : ::acos(p_x)); } + static _ALWAYS_INLINE_ float acos(float p_x) { return p_x < -1 ? Math_PI : (p_x > 1 ? 0 : ::acosf(p_x)); } static _ALWAYS_INLINE_ double atan(double p_x) { return ::atan(p_x); } static _ALWAYS_INLINE_ float atan(float p_x) { return ::atanf(p_x); } diff --git a/core/math/quaternion.cpp b/core/math/quaternion.cpp index 34e212a5b65..e4ad17c8ef8 100644 --- a/core/math/quaternion.cpp +++ b/core/math/quaternion.cpp @@ -35,7 +35,8 @@ real_t Quaternion::angle_to(const Quaternion &p_to) const { real_t d = dot(p_to); - return Math::acos(CLAMP(d * d * 2 - 1, -1, 1)); + // acos does clamping. + return Math::acos(d * d * 2 - 1); } Vector3 Quaternion::get_euler(EulerOrder p_order) const { diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index a0b623936a6..86f9e5ebb3a 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -65,7 +65,7 @@ - Returns the arc cosine of [param x] in radians. Use to get the angle of cosine [param x]. [param x] must be between [code]-1.0[/code] and [code]1.0[/code] (inclusive), otherwise, [method acos] will return [constant @GDScript.NAN]. + Returns the arc cosine of [param x] in radians. Use to get the angle of cosine [param x]. [param x] will be clamped between [code]-1.0[/code] and [code]1.0[/code] (inclusive), in order to prevent [method acos] from returning [constant @GDScript.NAN]. [codeblock] # c is 0.523599 or 30 degrees if converted with rad_to_deg(c) var c = acos(0.866025) @@ -76,7 +76,7 @@ - Returns the arc sine of [param x] in radians. Use to get the angle of sine [param x]. [param x] must be between [code]-1.0[/code] and [code]1.0[/code] (inclusive), otherwise, [method asin] will return [constant @GDScript.NAN]. + Returns the arc sine of [param x] in radians. Use to get the angle of sine [param x]. [param x] will be clamped between [code]-1.0[/code] and [code]1.0[/code] (inclusive), in order to prevent [method asin] from returning [constant @GDScript.NAN]. [codeblock] # s is 0.523599 or 30 degrees if converted with rad_to_deg(s) var s = asin(0.5) diff --git a/tests/core/math/test_math_funcs.h b/tests/core/math/test_math_funcs.h index 3e587390f8e..da96ace63f4 100644 --- a/tests/core/math/test_math_funcs.h +++ b/tests/core/math/test_math_funcs.h @@ -157,15 +157,15 @@ TEST_CASE_TEMPLATE("[Math] asin/acos/atan", T, float, double) { CHECK(Math::asin((T)0.1) == doctest::Approx((T)0.1001674212)); CHECK(Math::asin((T)0.5) == doctest::Approx((T)0.5235987756)); CHECK(Math::asin((T)1.0) == doctest::Approx((T)1.5707963268)); - CHECK(Math::is_nan(Math::asin((T)1.5))); - CHECK(Math::is_nan(Math::asin((T)450.0))); + CHECK(Math::asin((T)2.0) == doctest::Approx((T)1.5707963268)); + CHECK(Math::asin((T)-2.0) == doctest::Approx((T)-1.5707963268)); CHECK(Math::acos((T)-0.1) == doctest::Approx((T)1.670963748)); CHECK(Math::acos((T)0.1) == doctest::Approx((T)1.4706289056)); CHECK(Math::acos((T)0.5) == doctest::Approx((T)1.0471975512)); CHECK(Math::acos((T)1.0) == doctest::Approx((T)0.0)); - CHECK(Math::is_nan(Math::acos((T)1.5))); - CHECK(Math::is_nan(Math::acos((T)450.0))); + CHECK(Math::acos((T)2.0) == doctest::Approx((T)0.0)); + CHECK(Math::acos((T)-2.0) == doctest::Approx((T)Math_PI)); CHECK(Math::atan((T)-0.1) == doctest::Approx((T)-0.0996686525)); CHECK(Math::atan((T)0.1) == doctest::Approx((T)0.0996686525));