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