Skip to main content

steel_math/noise_math/
interpolation.rs

1use core::simd::{Select, Simd, cmp::SimdPartialOrd};
2use std::{
3    ops,
4    simd::{SimdCast, SimdElement, num::SimdFloat},
5};
6
7/// Clamped linear interpolation.
8///
9/// Clamps the interpolation factor to [0, 1] before interpolating.
10///
11/// Java reference: `Mth.clampedLerp(double, double, double)`.
12/// Note: Vanilla's parameter order is `(factor, min, max)`, ours is `(min, max, factor)`.
13#[inline]
14#[must_use]
15pub fn clamped_lerp<T>(min: T, max: T, factor: T) -> T
16where
17    T: ops::Add<Output = T>
18        + Copy
19        + From<f32>
20        + ops::Mul<Output = T>
21        + PartialOrd
22        + ops::Sub<Output = T>,
23{
24    if factor < T::from(0.0) {
25        min
26    } else if factor > T::from(1.0) {
27        max
28    } else {
29        lerp(factor, min, max)
30    }
31}
32
33/// Clamped lerp for N lanes.
34#[inline]
35#[must_use]
36pub fn clamped_lerp_simd<const N: usize>(
37    min: Simd<f64, N>,
38    max: Simd<f64, N>,
39    factor: Simd<f64, N>,
40) -> Simd<f64, N> {
41    let zero = Simd::splat(0.0);
42    let one = Simd::splat(1.0);
43    let below = factor.simd_lt(zero);
44    let above = factor.simd_gt(one);
45
46    // lerp result for the middle case
47    let lerped = min + factor * (max - min);
48
49    // Select: below zero → min, above one → max, otherwise → lerped
50    let result = below.select(min, lerped);
51    above.select(max, result)
52}
53
54/// Clamp a value to the range [min, max].
55///
56/// Java reference: `Mth.clamp(double, double, double)`
57#[inline]
58#[must_use]
59pub fn clamp(value: f64, min: f64, max: f64) -> f64 {
60    if value < min {
61        min
62    } else if value > max {
63        max
64    } else {
65        value
66    }
67}
68
69/// Clamp a value to the range [min, max] (i32 version).
70#[inline]
71#[must_use]
72pub const fn clamp_i32(value: i32, min: i32, max: i32) -> i32 {
73    if value < min {
74        min
75    } else if value > max {
76        max
77    } else {
78        value
79    }
80}
81/// Inverse linear interpolation (find the factor t such that lerp(t, a, b) == value).
82///
83/// Java reference: `Mth.inverseLerp(double, double, double)`
84#[inline]
85#[must_use]
86pub fn inverse_lerp(value: f64, a: f64, b: f64) -> f64 {
87    (value - a) / (b - a)
88}
89
90/// Linear interpolation.
91///
92/// Formula: a + alpha * (b - a)
93///
94/// Java reference: `Mth.lerp(double, double, double)`
95#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
96#[inline(always)]
97#[must_use]
98pub fn lerp<F>(alpha: F, a: F, b: F) -> F
99where
100    F: Copy + ops::Mul<Output = F> + ops::Add<Output = F> + ops::Sub<Output = F>,
101{
102    a + alpha * (b - a)
103}
104
105/// SIMD linear interpolation.
106#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
107#[inline(always)]
108#[must_use]
109pub fn lerp_simd<F, const N: usize>(alpha: Simd<F, N>, a: Simd<F, N>, b: Simd<F, N>) -> Simd<F, N>
110where
111    F: SimdElement,
112    Simd<F, N>: ops::Mul<Output = Simd<F, N>>
113        + ops::Add<Output = Simd<F, N>>
114        + ops::Sub<Output = Simd<F, N>>,
115{
116    a + alpha * (b - a)
117}
118
119/// Bilinear interpolation.
120///
121/// Interpolates between 4 values in a 2D grid.
122///
123/// Java reference: `Mth.lerp2(double, double, double, double, double, double)`
124#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
125#[inline(always)]
126#[must_use]
127pub fn lerp2<F>(a1: F, a2: F, x00: F, x10: F, x01: F, x11: F) -> F
128where
129    F: Copy + ops::Mul<Output = F> + ops::Add<Output = F> + ops::Sub<Output = F>,
130{
131    lerp(a2, lerp(a1, x00, x10), lerp(a1, x01, x11))
132}
133
134/// SIMD bilinear interpolation.
135#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
136#[inline(always)]
137#[must_use]
138pub fn lerp2_simd<F, const N: usize>(
139    a1: Simd<F, N>,
140    a2: Simd<F, N>,
141    x00: Simd<F, N>,
142    x10: Simd<F, N>,
143    x01: Simd<F, N>,
144    x11: Simd<F, N>,
145) -> Simd<F, N>
146where
147    F: SimdElement,
148    Simd<F, N>: ops::Mul<Output = Simd<F, N>>
149        + ops::Add<Output = Simd<F, N>>
150        + ops::Sub<Output = Simd<F, N>>,
151{
152    lerp_simd(a2, lerp_simd(a1, x00, x10), lerp_simd(a1, x01, x11))
153}
154
155/// Trilinear interpolation.
156///
157/// Interpolates between 8 values in a 3D grid.
158///
159/// Java reference: `Mth.lerp3(...)`
160#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
161#[inline(always)]
162#[must_use]
163#[expect(
164    clippy::too_many_arguments,
165    reason = "matches vanilla's Mth.lerp3 signature with 8 grid corner values"
166)]
167pub fn lerp3<F>(
168    a1: F,
169    a2: F,
170    a3: F,
171    x000: F,
172    x100: F,
173    x010: F,
174    x110: F,
175    x001: F,
176    x101: F,
177    x011: F,
178    x111: F,
179) -> F
180where
181    F: Copy + ops::Mul<Output = F> + ops::Add<Output = F> + ops::Sub<Output = F>,
182{
183    lerp(
184        a3,
185        lerp2(a1, a2, x000, x100, x010, x110),
186        lerp2(a1, a2, x001, x101, x011, x111),
187    )
188}
189
190/// Trilinear interpolation for N lanes. see lerp3.
191#[inline]
192#[expect(clippy::too_many_arguments, reason = "mirrors lerp3 with SIMD vectors")]
193#[must_use]
194pub fn lerp3_simd<F, const N: usize>(
195    a1: Simd<F, N>,
196    a2: Simd<F, N>,
197    a3: Simd<F, N>,
198    x000: Simd<F, N>,
199    x100: Simd<F, N>,
200    x010: Simd<F, N>,
201    x110: Simd<F, N>,
202    x001: Simd<F, N>,
203    x101: Simd<F, N>,
204    x011: Simd<F, N>,
205    x111: Simd<F, N>,
206) -> Simd<F, N>
207where
208    F: SimdElement,
209    Simd<F, N>: ops::Mul<Output = Simd<F, N>>
210        + ops::Add<Output = Simd<F, N>>
211        + ops::Sub<Output = Simd<F, N>>,
212{
213    lerp_simd(
214        a3,
215        lerp2_simd(a1, a2, x000, x100, x010, x110),
216        lerp2_simd(a1, a2, x001, x101, x011, x111),
217    )
218}
219
220#[cfg(test)]
221mod lerp_tests {
222    use super::*;
223
224    #[test]
225    fn test_lerp() {
226        assert!((lerp(0.0_f64, 10.0, 20.0) - 10.0).abs() < 1e-10);
227        assert!((lerp(1.0_f64, 10.0, 20.0) - 20.0).abs() < 1e-10);
228        assert!((lerp(0.5_f64, 10.0, 20.0) - 15.0).abs() < 1e-10);
229    }
230}
231/// Map a value from one range to another (unclamped).
232///
233/// Unlike [`map_clamped`], the result can extrapolate outside `[to_min, to_max]`.
234///
235/// Java reference: `Mth.map(double, double, double, double, double)`
236#[inline]
237#[must_use]
238pub fn map(value: f64, from_min: f64, from_max: f64, to_min: f64, to_max: f64) -> f64 {
239    lerp(inverse_lerp(value, from_min, from_max), to_min, to_max)
240}
241
242/// Map a value from one range to another with clamped lerp.
243///
244/// Used for Y-clamped gradients in density functions.
245#[inline]
246#[must_use]
247pub fn map_clamped<T>(value: T, from_min: T, from_max: T, to_min: T, to_max: T) -> T
248where
249    T: ops::Add<Output = T>
250        + Copy
251        + ops::Div<Output = T>
252        + From<f32>
253        + ops::Mul<Output = T>
254        + PartialOrd
255        + ops::Sub<Output = T>,
256{
257    let t = (value - from_min) / (from_max - from_min);
258    clamped_lerp(to_min, to_max, t)
259}
260/// Smoothstep - quintic Hermite interpolation (NOT cubic!)
261///
262/// Formula: 6x^5 - 15x^4 + 10x^3
263///
264/// This is the standard smoothstep used in Perlin noise for smooth transitions.
265/// Java reference: `Mth.smoothstep(double)`
266#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
267#[inline(always)]
268#[must_use]
269pub fn smoothstep<F>(x: F) -> F
270where
271    F: Copy + From<f32> + ops::Mul<Output = F> + ops::Add<Output = F> + ops::Sub<Output = F>,
272{
273    x * x * x * (x * (x * F::from(6.0) - F::from(15.0)) + F::from(10.0))
274}
275
276/// Smoothstep derivative for noise with derivatives.
277///
278/// Formula: 30x^2(x-1)^2
279///
280/// Java reference: `Mth.smoothstepDerivative(double)`
281#[inline]
282#[must_use]
283pub fn smoothstep_derivative<F>(x: F) -> F
284where
285    F: Copy + From<f32> + ops::Mul<Output = F> + ops::Add<Output = F> + ops::Sub<Output = F>,
286{
287    F::from(30.0) * x * x * (x - F::from(1.0)) * (x - F::from(1.0))
288}
289
290/// Smoothstep for N lanes: 6x^5 - 15x^4 + 10x^3. Per-lane identical to [`smoothstep`].
291#[inline]
292#[must_use]
293pub fn smoothstep_simd<F, const N: usize>(x: Simd<F, N>) -> Simd<F, N>
294where
295    F: SimdElement + SimdCast,
296    Simd<F, N>: ops::Mul<Output = Simd<F, N>>
297        + ops::Sub<Output = Simd<F, N>>
298        + ops::Add<Output = Simd<F, N>>,
299{
300    x * x
301        * x
302        * (x * (x * Simd::splat(6.0).cast() - Simd::splat(15.0).cast()) + Simd::splat(10.0).cast())
303}
304
305#[cfg(test)]
306mod smoothstep_tests {
307    use super::*;
308    #[test]
309    fn test_smoothstep() {
310        // At boundaries
311        assert!((smoothstep(0.0_f64) - 0.0).abs() < 1e-10);
312        assert!((smoothstep(1.0_f64) - 1.0).abs() < 1e-10);
313        // At midpoint
314        assert!((smoothstep(0.5_f64) - 0.5).abs() < 1e-10);
315    }
316}