#[derive(Clone, Debug, PartialEq)]
pub struct Lut3d {
pub edge: u32,
pub lo: [f32; 3],
pub hi: [f32; 3],
pub data: Vec<f32>,
}
impl Lut3d {
pub const DISPLAY_EDGE: u32 = 65;
pub const DISPLAY_LO: [f32; 3] = [-0.25, -0.25, -0.25];
pub const DISPLAY_HI: [f32; 3] = [4.0, 4.0, 4.0];
pub fn build<F: FnMut([f32; 3]) -> [f32; 3]>(
edge: u32,
lo: [f32; 3],
hi: [f32; 3],
mut f: F,
) -> Self {
let edge = edge.max(2);
let n = edge as usize;
let mut data = Vec::with_capacity(n * n * n * 3);
let step = |i: usize, axis: usize| -> f32 {
let t = i as f32 / (edge - 1) as f32;
lo[axis] + (hi[axis] - lo[axis]) * t
};
for b in 0..n {
for g in 0..n {
for r in 0..n {
let out = f([step(r, 0), step(g, 1), step(b, 2)]);
data.extend_from_slice(&out);
}
}
}
Self { edge, lo, hi, data }
}
pub fn identity(edge: u32) -> Self {
Self::build(edge, [0.0; 3], [1.0; 3], |c| c)
}
fn index(&self, r: u32, g: u32, b: u32) -> usize {
(((b * self.edge + g) * self.edge + r) * 3) as usize
}
pub fn sample(&self, r: u32, g: u32, b: u32) -> [f32; 3] {
let i = self.index(r, g, b);
[self.data[i], self.data[i + 1], self.data[i + 2]]
}
pub fn eval(&self, rgb: [f32; 3]) -> [f32; 3] {
let last = self.edge - 1;
let mut p = [0.0f32; 3];
for a in 0..3 {
let span = self.hi[a] - self.lo[a];
let t = if span.abs() > f32::EPSILON {
(rgb[a] - self.lo[a]) / span
} else {
0.0
}
.clamp(0.0, 1.0);
p[a] = t * last as f32;
}
let i0 = [
p[0].floor() as u32,
p[1].floor() as u32,
p[2].floor() as u32,
];
let f = [
p[0] - i0[0] as f32,
p[1] - i0[1] as f32,
p[2] - i0[2] as f32,
];
let i1 = [
(i0[0] + 1).min(last),
(i0[1] + 1).min(last),
(i0[2] + 1).min(last),
];
std::array::from_fn(|a| {
let c00 = lerp(
self.sample(i0[0], i0[1], i0[2])[a],
self.sample(i1[0], i0[1], i0[2])[a],
f[0],
);
let c10 = lerp(
self.sample(i0[0], i1[1], i0[2])[a],
self.sample(i1[0], i1[1], i0[2])[a],
f[0],
);
let c01 = lerp(
self.sample(i0[0], i0[1], i1[2])[a],
self.sample(i1[0], i0[1], i1[2])[a],
f[0],
);
let c11 = lerp(
self.sample(i0[0], i1[1], i1[2])[a],
self.sample(i1[0], i1[1], i1[2])[a],
f[0],
);
let c0 = lerp(c00, c10, f[1]);
let c1 = lerp(c01, c11, f[1]);
lerp(c0, c1, f[2])
})
}
pub fn domain(&self) -> ([f32; 3], [f32; 3]) {
(self.lo, self.hi)
}
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_lut_interpolates_linearly() {
let lut = Lut3d::identity(3);
assert_eq!(lut.edge, 3);
let c = lut.eval([0.25, 0.5, 0.75]);
assert!((c[0] - 0.25).abs() < 1e-6);
assert!((c[1] - 0.5).abs() < 1e-6);
assert!((c[2] - 0.75).abs() < 1e-6);
}
#[test]
fn build_covers_the_domain_corners() {
let lut = Lut3d::build(2, [-1.0; 3], [2.0; 3], |c| [c[0] * 2.0, c[1], c[2]]);
assert_eq!(lut.sample(0, 0, 0), [-2.0, -1.0, -1.0]);
assert_eq!(lut.sample(1, 1, 1), [4.0, 2.0, 2.0]);
assert_eq!(lut.eval([-100.0, -100.0, -100.0]), [-2.0, -1.0, -1.0]);
assert_eq!(lut.eval([100.0, 100.0, 100.0]), [4.0, 2.0, 2.0]);
}
#[test]
fn trilinear_matches_manual_blend() {
let lut = Lut3d::build(5, [0.0; 3], [1.0; 3], |c| {
[c[0] * 0.5 + c[1] * 0.25 + c[2] * 0.25, c[1], c[2]]
});
for probe in [[0.1, 0.2, 0.3], [0.9, 0.4, 0.7], [0.0, 1.0, 0.5]] {
let out = lut.eval(probe);
let expect = [
probe[0] * 0.5 + probe[1] * 0.25 + probe[2] * 0.25,
probe[1],
probe[2],
];
for a in 0..3 {
assert!(
(out[a] - expect[a]).abs() < 1e-5,
"axis {a}: {} vs {}",
out[a],
expect[a]
);
}
}
}
}