Skip to main content

opendefocus_shared/
cpu_image.rs

1use glam::{UVec2, Vec2, Vec4};
2
3use image::{GenericImageView, ImageBuffer, Pixel, imageops::interpolate_bilinear};
4
5use crate::math::mix_vec;
6
7#[derive(PartialEq)]
8pub enum Interpolation {
9    Nearest,
10    Linear,
11}
12pub struct Sampler {
13    interpolation: Interpolation,
14}
15
16impl Sampler {
17    pub fn new(interpolation: Interpolation) -> Self {
18        Self { interpolation }
19    }
20}
21
22/// Converts a pixel to a Vec4, handling pixels with fewer than 4 channels.
23///
24/// This function converts any pixel type with f32 subpixels to a Vec4.
25/// For pixels with fewer than 4 channels, it repeats the last available channel
26/// to fill the remaining components. This is useful for converting RGB pixels
27/// to RGBA by duplicating the last channel as alpha.
28///
29/// # Performance
30/// - Accesses pixel channels only once
31/// - Pre-calculates the channel clamping value
32/// - Uses array indexing for better compiler optimization
33///
34/// # Safety
35/// Handles edge cases where CHANNEL_COUNT might be 0 or very small
36fn pixel_to_vec4<P: Pixel<Subpixel = f32>>(pixel: &P) -> Vec4 {
37    let pixel_channels = pixel.channels();
38
39    let max_channel = (P::CHANNEL_COUNT as usize).saturating_sub(1);
40
41    Vec4::new(
42        pixel_channels[0.min(max_channel)],
43        pixel_channels[1.min(max_channel)],
44        pixel_channels[2.min(max_channel)],
45        pixel_channels[3.min(max_channel)],
46    )
47}
48
49pub struct CPUImage<P>
50where
51    P: Pixel<Subpixel = f32>,
52{
53    images: Vec<ImageBuffer<P, Vec<f32>>>,
54}
55
56impl<P> CPUImage<P>
57where
58    P: Pixel<Subpixel = f32>,
59{
60    pub fn new(data: &[ImageBuffer<P, Vec<f32>>]) -> Self {
61        Self {
62            images: data.to_vec(),
63        }
64    }
65    fn bilinear(&self, coordinates: Vec2, mip: usize) -> Vec4 {
66        let image = &self.images[mip.clamp(0, self.images.len() - 1)];
67        let (width, height) = image.dimensions();
68        let resolution = UVec2::new(width, height);
69        if coordinates.x.ceil() as u32 >= resolution.x
70            || coordinates.y.ceil() as u32 >= resolution.y
71        {
72            return Vec4::default();
73        }
74
75        match interpolate_bilinear(image, coordinates.x, coordinates.y) {
76            Some(pixel) => pixel_to_vec4(&pixel),
77            None => Vec4::default(),
78        }
79    }
80
81    pub fn load_single_mip(&self, coordinates: Vec2, sampler: &Sampler, mip: usize) -> Vec4 {
82        let image = &self.images[mip.clamp(0, self.images.len() - 1)];
83        let (width, height) = image.dimensions();
84        let resolution = Vec2::new(width as f32, height as f32);
85        let converted_coordinates = (coordinates * resolution).clamp(Vec2::ZERO, resolution - 1.0);
86        if sampler.interpolation == Interpolation::Nearest {
87            let pixel = unsafe {
88                image.unsafe_get_pixel(
89                    converted_coordinates.x as u32,
90                    converted_coordinates.y as u32,
91                )
92            };
93            pixel_to_vec4(&pixel)
94        } else {
95            self.bilinear(converted_coordinates, mip)
96        }
97    }
98
99    pub fn load_texture(&self, coordinates: Vec2, sampler: &Sampler, mip: f32) -> Vec4 {
100        let multiple_images = mip.round() as usize == mip as usize;
101        if !multiple_images {
102            return self.load_single_mip(coordinates, sampler, mip as usize);
103        }
104
105        let bottom = mip.floor();
106        let top = mip.ceil();
107        let fract = mip - bottom;
108
109        let bottom_pixel = self.load_single_mip(coordinates, sampler, bottom as usize);
110        let top_pixel = self.load_single_mip(coordinates, sampler, top as usize);
111
112        mix_vec(bottom_pixel, top_pixel, fract)
113    }
114}