Repository: bitshifter/glam-rs (rust)

Issue:
lerp()'s implementation has a loss of precision when inputs span multiple orders of magnitude
The general implementation of `lerp` in glam for floating point types takes the form as follows:

```rust
fn lerp(a: Self, b: Self, t: f32) -> Self {
  a + (b - a) * t
}
```

However, this presents floating point precision issues if `a` and `b` have very different exponent values due to the `(b - a)` in the computation. You can test this via `lerp(-16.0e30, 16.0, 1.0)` returning 0.0 instead of the correct 16.0. It may be more accurate to use the following form:

```rust
fn lerp(a: Self, b: Self, t: f32) -> Self {
  a * (1.0 - t) + b * t
}
```

This may not be a significant regression in performance as floating point multiplication is generally much faster than addition. 

Pull request description:
Switch to precise lerp.
Reviving #278, fixes #275.

Patch:
diff --git a/codegen/templates/vec.rs.tera b/codegen/templates/vec.rs.tera
index e8408ff..b1c1ed6 100644
--- a/codegen/templates/vec.rs.tera
+++ b/codegen/templates/vec.rs.tera
@@ -1867,7 +1867,7 @@ impl {{ self_t }} {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: {{ scalar_t }}) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/coresimd/vec3a.rs b/src/f32/coresimd/vec3a.rs
index 8d280e0..cc715f9 100644
--- a/src/f32/coresimd/vec3a.rs
+++ b/src/f32/coresimd/vec3a.rs
@@ -726,7 +726,7 @@ impl Vec3A {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/coresimd/vec4.rs b/src/f32/coresimd/vec4.rs
index 47d0bc6..7876731 100644
--- a/src/f32/coresimd/vec4.rs
+++ b/src/f32/coresimd/vec4.rs
@@ -713,7 +713,7 @@ impl Vec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/neon/vec3a.rs b/src/f32/neon/vec3a.rs
index 5b80cb3..a6e7f9e 100644
--- a/src/f32/neon/vec3a.rs
+++ b/src/f32/neon/vec3a.rs
@@ -770,7 +770,7 @@ impl Vec3A {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/neon/vec4.rs b/src/f32/neon/vec4.rs
index 1c81ea1..296a3eb 100644
--- a/src/f32/neon/vec4.rs
+++ b/src/f32/neon/vec4.rs
@@ -747,7 +747,7 @@ impl Vec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/scalar/vec3a.rs b/src/f32/scalar/vec3a.rs
index 50a788a..abc3af0 100644
--- a/src/f32/scalar/vec3a.rs
+++ b/src/f32/scalar/vec3a.rs
@@ -769,7 +769,7 @@ impl Vec3A {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/scalar/vec4.rs b/src/f32/scalar/vec4.rs
index 1afdabd..68e881c 100644
--- a/src/f32/scalar/vec4.rs
+++ b/src/f32/scalar/vec4.rs
@@ -827,7 +827,7 @@ impl Vec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/sse2/vec3a.rs b/src/f32/sse2/vec3a.rs
index 0f92f52..62f22bc 100644
--- a/src/f32/sse2/vec3a.rs
+++ b/src/f32/sse2/vec3a.rs
@@ -769,7 +769,7 @@ impl Vec3A {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/sse2/vec4.rs b/src/f32/sse2/vec4.rs
index c773f68..afb30e5 100644
--- a/src/f32/sse2/vec4.rs
+++ b/src/f32/sse2/vec4.rs
@@ -755,7 +755,7 @@ impl Vec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/vec2.rs b/src/f32/vec2.rs
index 241774a..fa2367c 100644
--- a/src/f32/vec2.rs
+++ b/src/f32/vec2.rs
@@ -692,7 +692,7 @@ impl Vec2 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/vec3.rs b/src/f32/vec3.rs
index b5a5a4f..2e8df47 100644
--- a/src/f32/vec3.rs
+++ b/src/f32/vec3.rs
@@ -759,7 +759,7 @@ impl Vec3 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/wasm32/vec3a.rs b/src/f32/wasm32/vec3a.rs
index 8d682dd..1c5393a 100644
--- a/src/f32/wasm32/vec3a.rs
+++ b/src/f32/wasm32/vec3a.rs
@@ -737,7 +737,7 @@ impl Vec3A {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f32/wasm32/vec4.rs b/src/f32/wasm32/vec4.rs
index d552616..6037a08 100644
--- a/src/f32/wasm32/vec4.rs
+++ b/src/f32/wasm32/vec4.rs
@@ -730,7 +730,7 @@ impl Vec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f32) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f64/dvec2.rs b/src/f64/dvec2.rs
index 61aa26d..25e83fa 100644
--- a/src/f64/dvec2.rs
+++ b/src/f64/dvec2.rs
@@ -692,7 +692,7 @@ impl DVec2 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f64) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f64/dvec3.rs b/src/f64/dvec3.rs
index 5e4b483..85c69bc 100644
--- a/src/f64/dvec3.rs
+++ b/src/f64/dvec3.rs
@@ -759,7 +759,7 @@ impl DVec3 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f64) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.
diff --git a/src/f64/dvec4.rs b/src/f64/dvec4.rs
index 314a48e..3444ba3 100644
--- a/src/f64/dvec4.rs
+++ b/src/f64/dvec4.rs
@@ -816,7 +816,7 @@ impl DVec4 {
     #[inline]
     #[must_use]
     pub fn lerp(self, rhs: Self, s: f64) -> Self {
-        self + ((rhs - self) * s)
+        self * (1.0 - s) + rhs * s
     }
 
     /// Moves towards `rhs` based on the value `d`.


Test patch:
diff --git a/tests/vec2.rs b/tests/vec2.rs
index f16b86f..197d338 100644
--- a/tests/vec2.rs
+++ b/tests/vec2.rs
@@ -843,6 +843,13 @@ macro_rules! impl_vec2_float_tests {
             assert_approx_eq!($vec2::ZERO, v0.lerp(v1, 0.5));
         });
 
+        glam_test!(test_lerp_big_difference, {
+            let v0 = $vec2::new(-1e30, -1e30);
+            let v1 = $vec2::new(16.0, 16.0);
+            assert_approx_eq!(v0, v0.lerp(v1, 0.0));
+            assert_approx_eq!(v1, v0.lerp(v1, 1.0));
+        });
+
         glam_test!(test_move_towards, {
             let v0 = $vec2::new(-1.0, -1.0);
             let v1 = $vec2::new(1.0, 1.0);
diff --git a/tests/vec3.rs b/tests/vec3.rs
index 62ce25a..4957d5a 100644
--- a/tests/vec3.rs
+++ b/tests/vec3.rs
@@ -987,6 +987,13 @@ macro_rules! impl_vec3_float_tests {
             assert_approx_eq!($vec3::ZERO, v0.lerp(v1, 0.5));
         });
 
+        glam_test!(test_lerp_big_difference, {
+            let v0 = $vec3::new(-1e30, -1e30, -1e30);
+            let v1 = $vec3::new(16.0, 16.0, 16.0);
+            assert_approx_eq!(v0, v0.lerp(v1, 0.0));
+            assert_approx_eq!(v1, v0.lerp(v1, 1.0));
+        });
+
         glam_test!(test_move_towards, {
             let v0 = $vec3::new(-1.0, -1.0, -1.0);
             let v1 = $vec3::new(1.0, 1.0, 1.0);
diff --git a/tests/vec4.rs b/tests/vec4.rs
index 4d36d95..21626a4 100644
--- a/tests/vec4.rs
+++ b/tests/vec4.rs
@@ -1124,6 +1124,13 @@ macro_rules! impl_vec4_float_tests {
             assert_approx_eq!($vec4::ZERO, v0.lerp(v1, 0.5));
         });
 
+        glam_test!(test_lerp_big_difference, {
+            let v0 = $vec4::new(-1e30, -1e30, -1e30, -1e30);
+            let v1 = $vec4::new(16.0, 16.0, 16.0, 16.0);
+            assert_approx_eq!(v0, v0.lerp(v1, 0.0));
+            assert_approx_eq!(v1, v0.lerp(v1, 1.0));
+        });
+
         glam_test!(test_move_towards, {
             let v0 = $vec4::new(-1.0, -1.0, -1.0, -1.0);
             let v1 = $vec4::new(1.0, 1.0, 1.0, 1.0);