1use core::simd::{Select, Simd, cmp::SimdPartialOrd};
2use std::{
3 ops,
4 simd::{SimdCast, SimdElement, num::SimdFloat},
5};
6
7#[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#[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 let lerped = min + factor * (max - min);
40
41 let result = below.select(min, lerped);
43 above.select(max, result)
44}
45
46#[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#[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#[inline]
77#[must_use]
78pub fn inverse_lerp(value: f64, a: f64, b: f64) -> f64 {
79 (value - a) / (b - a)
80}
81
82#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 assert!((smoothstep(0.0) - 0.0).abs() < 1e-10);
280 assert!((smoothstep(1.0) - 1.0).abs() < 1e-10);
281 assert!((smoothstep(0.5) - 0.5).abs() < 1e-10);
283 }
284}