use crate::gfx::bitmap::Bitmap;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Wrap {
Repeat,
Clamp,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum Filter {
#[default]
Bilinear,
Nearest,
}
impl Wrap {
#[inline]
fn apply(self, i: i32, n: i32) -> i32 {
match self {
Wrap::Repeat => i.rem_euclid(n),
Wrap::Clamp => i.clamp(0, n - 1),
}
}
}
pub struct TextureSampler<'a> {
texels: &'a Bitmap,
wrap_u: Wrap,
wrap_v: Wrap,
filter: Filter,
}
#[inline]
pub fn srgb8_to_linear(v: u8) -> f32 {
let f = v as f32 / 255.0;
f * f
}
impl<'a> TextureSampler<'a> {
pub fn new(texels: &'a Bitmap, wrap_u: Wrap, wrap_v: Wrap) -> Option<TextureSampler<'a>> {
if texels.is_empty() {
None
} else {
Some(TextureSampler {
texels,
wrap_u,
wrap_v,
filter: Filter::Bilinear,
})
}
}
pub fn with_filter(mut self, filter: Filter) -> TextureSampler<'a> {
self.filter = filter;
self
}
pub fn sample(&self, u: f32, v: f32) -> [f32; 3] {
match self.filter {
Filter::Bilinear => self.sample_bilinear(u, v),
Filter::Nearest => self.sample_nearest(u, v),
}
}
fn sample_nearest(&self, u: f32, v: f32) -> [f32; 3] {
let (w, h) = (self.texels.width() as i32, self.texels.height() as i32);
let x = self.wrap_u.apply((u * w as f32).floor() as i32, w);
let y = self.wrap_v.apply((v * h as f32).floor() as i32, h);
let p = self.texels.row(y as u32)[x as usize];
[
srgb8_to_linear(p.r),
srgb8_to_linear(p.g),
srgb8_to_linear(p.b),
]
}
fn sample_bilinear(&self, u: f32, v: f32) -> [f32; 3] {
let (w, h) = (self.texels.width() as i32, self.texels.height() as i32);
let x = u * w as f32 - 0.5;
let y = v * h as f32 - 0.5;
let (x0f, y0f) = (x.floor(), y.floor());
let (fx, fy) = (x - x0f, y - y0f);
let (x0, y0) = (x0f as i32, y0f as i32);
let tx = |i: i32| self.wrap_u.apply(i, w);
let ty = |i: i32| self.wrap_v.apply(i, h);
let p00 = self.texels.row(ty(y0) as u32)[tx(x0) as usize];
let p10 = self.texels.row(ty(y0) as u32)[tx(x0 + 1) as usize];
let p01 = self.texels.row(ty(y0 + 1) as u32)[tx(x0) as usize];
let p11 = self.texels.row(ty(y0 + 1) as u32)[tx(x0 + 1) as usize];
let mut out = [0.0f32; 3];
let ch = |p: crate::base::Rgba, k: usize| match k {
0 => srgb8_to_linear(p.r),
1 => srgb8_to_linear(p.g),
_ => srgb8_to_linear(p.b),
};
for (k, o) in out.iter_mut().enumerate() {
let top = ch(p00, k) * (1.0 - fx) + ch(p10, k) * fx;
let bot = ch(p01, k) * (1.0 - fx) + ch(p11, k) * fx;
*o = top * (1.0 - fy) + bot * fy;
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::Rgba;
fn checker() -> Bitmap {
Bitmap::from_fn(2, 2, |x, y| {
if (x + y) % 2 == 0 {
Rgba::WHITE
} else {
Rgba::BLACK
}
})
}
#[test]
fn texel_centers_sample_exactly() {
let t = checker();
let s = TextureSampler::new(&t, Wrap::Repeat, Wrap::Repeat).unwrap();
assert_eq!(s.sample(0.25, 0.25), [1.0, 1.0, 1.0]);
assert_eq!(s.sample(0.75, 0.25), [0.0, 0.0, 0.0]);
assert_eq!(s.sample(0.75, 0.75), [1.0, 1.0, 1.0]);
}
#[test]
fn midpoints_blend_bilinearly() {
let t = checker();
let s = TextureSampler::new(&t, Wrap::Repeat, Wrap::Repeat).unwrap();
let m = s.sample(0.5, 0.25);
assert!((m[0] - 0.5).abs() < 1e-6, "{m:?}");
}
#[test]
fn wrap_modes() {
let t = checker();
let rep = TextureSampler::new(&t, Wrap::Repeat, Wrap::Repeat).unwrap();
assert_eq!(rep.sample(1.25, 0.25), rep.sample(0.25, 0.25));
assert_eq!(rep.sample(-0.75, 0.25), rep.sample(0.25, 0.25));
let cl = TextureSampler::new(&t, Wrap::Clamp, Wrap::Clamp).unwrap();
assert_eq!(cl.sample(9.0, 0.25), cl.sample(0.75, 0.25));
assert_eq!(cl.sample(-9.0, -9.0), cl.sample(0.25, 0.25));
}
#[test]
fn srgb_gamma_pair_round_trips() {
for v in [0u8, 1, 7, 128, 200, 255] {
let lin = srgb8_to_linear(v);
let back = (lin.sqrt() * 255.0 + 0.5) as u8;
assert!((back as i32 - v as i32).abs() <= 1, "{v} -> {back}");
}
}
#[test]
fn empty_texture_refused() {
let t = Bitmap::default();
assert!(TextureSampler::new(&t, Wrap::Repeat, Wrap::Repeat).is_none());
}
#[test]
fn nearest_filter_has_hard_edges() {
let t = checker();
let s = TextureSampler::new(&t, Wrap::Repeat, Wrap::Repeat)
.unwrap()
.with_filter(Filter::Nearest);
assert_eq!(s.sample(0.49, 0.25), [1.0, 1.0, 1.0]);
assert_eq!(s.sample(0.51, 0.25), [0.0, 0.0, 0.0]);
assert_eq!(s.sample(1.49, 0.25), s.sample(0.49, 0.25));
}
}