Skip to main content

renderling/atlas/
atlas_image.rs

1//! Images and texture formats.
2//!
3//! Used to represent textures before they are sent to the GPU.
4use glam::UVec2;
5use image::EncodableLayout;
6use snafu::prelude::*;
7
8fn cwd() -> Option<String> {
9    #[cfg(target_arch = "wasm32")]
10    {
11        Some("localhost".to_string())
12    }
13    #[cfg(not(target_arch = "wasm32"))]
14    {
15        let cwd = std::env::current_dir().ok()?;
16        Some(format!("{}", cwd.display()))
17    }
18}
19
20#[derive(Debug, Snafu)]
21pub enum AtlasImageError {
22    #[snafu(display("Cannot load image '{}' from cwd '{:?}': {source}", path.display(), cwd()))]
23    CannotLoad {
24        source: std::io::Error,
25        path: std::path::PathBuf,
26    },
27
28    #[snafu(display("Image error: {source}\nCurrent dir: {:?}", cwd()))]
29    Image { source: image::error::ImageError },
30}
31
32#[derive(Clone, Copy, Debug)]
33pub enum AtlasImageFormat {
34    R8,
35    R8G8,
36    R8G8B8,
37    R8G8B8A8,
38    R16,
39    R16G16,
40    R16G16B16,
41    R16G16B16A16,
42    R16G16B16A16FLOAT,
43    R32FLOAT,
44    R32G32B32FLOAT,
45    R32G32B32A32FLOAT,
46    D32FLOAT,
47}
48
49impl From<AtlasImageFormat> for wgpu::TextureFormat {
50    fn from(value: AtlasImageFormat) -> Self {
51        match value {
52            AtlasImageFormat::R8 => wgpu::TextureFormat::R8Unorm,
53            AtlasImageFormat::R8G8 => wgpu::TextureFormat::Rg8Unorm,
54            AtlasImageFormat::R8G8B8 => wgpu::TextureFormat::Rgba8Unorm, /* No direct 3-channel */
55            // format, using
56            // 4-channel
57            AtlasImageFormat::R8G8B8A8 => wgpu::TextureFormat::Rgba8Unorm,
58            AtlasImageFormat::R16 => wgpu::TextureFormat::R16Unorm,
59            AtlasImageFormat::R16G16 => wgpu::TextureFormat::Rg16Unorm,
60            AtlasImageFormat::R16G16B16 => wgpu::TextureFormat::Rgba16Unorm, /* No direct 3-channel format, using 4-channel */
61            AtlasImageFormat::R16G16B16A16 => wgpu::TextureFormat::Rgba16Unorm,
62            AtlasImageFormat::R16G16B16A16FLOAT => wgpu::TextureFormat::Rgba16Float,
63            AtlasImageFormat::R32FLOAT => wgpu::TextureFormat::R32Float,
64            AtlasImageFormat::R32G32B32FLOAT => wgpu::TextureFormat::Rgba32Float, /* No direct 3-channel format, using 4-channel */
65            AtlasImageFormat::R32G32B32A32FLOAT => wgpu::TextureFormat::Rgba32Float,
66            AtlasImageFormat::D32FLOAT => wgpu::TextureFormat::Depth32Float,
67        }
68    }
69}
70
71impl AtlasImageFormat {
72    pub fn from_wgpu_texture_format(value: wgpu::TextureFormat) -> Option<Self> {
73        match value {
74            wgpu::TextureFormat::R8Uint => Some(AtlasImageFormat::R8),
75            wgpu::TextureFormat::R16Uint => Some(AtlasImageFormat::R16),
76            wgpu::TextureFormat::R32Float => Some(AtlasImageFormat::R32FLOAT),
77            wgpu::TextureFormat::Rg8Uint => Some(AtlasImageFormat::R8G8),
78            wgpu::TextureFormat::Rg16Uint => Some(AtlasImageFormat::R16G16),
79            wgpu::TextureFormat::Rgba16Float => Some(AtlasImageFormat::R16G16B16A16FLOAT),
80            wgpu::TextureFormat::Depth32Float => Some(AtlasImageFormat::D32FLOAT),
81            _ => None,
82        }
83    }
84
85    pub fn zero_pixel(&self) -> &[u8] {
86        match self {
87            AtlasImageFormat::R8 => &[0],
88            AtlasImageFormat::R8G8 => &[0, 0],
89            AtlasImageFormat::R8G8B8 => &[0, 0, 0],
90            AtlasImageFormat::R8G8B8A8 => &[0, 0, 0, 0],
91            AtlasImageFormat::R16 => &[0, 0],
92            AtlasImageFormat::R16G16 => &[0, 0, 0, 0],
93            AtlasImageFormat::R16G16B16 => &[0, 0, 0, 0, 0, 0],
94            AtlasImageFormat::R16G16B16A16 => &[0, 0, 0, 0, 0, 0, 0, 0],
95            AtlasImageFormat::R16G16B16A16FLOAT => &[0, 0, 0, 0, 0, 0, 0, 0],
96            AtlasImageFormat::R32FLOAT | AtlasImageFormat::D32FLOAT => &[0, 0, 0, 0],
97            AtlasImageFormat::R32G32B32FLOAT => &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
98            AtlasImageFormat::R32G32B32A32FLOAT => {
99                &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
100            }
101        }
102    }
103}
104
105/// Image data in transit from CPU to GPU.
106#[derive(Clone, Debug)]
107pub struct AtlasImage {
108    pub pixels: Vec<u8>,
109    pub size: UVec2,
110    pub format: AtlasImageFormat,
111    // Whether or not to convert from sRGB color space into linear color space.
112    pub apply_linear_transfer: bool,
113}
114
115#[cfg(feature = "gltf")]
116impl From<gltf::image::Data> for AtlasImage {
117    fn from(value: gltf::image::Data) -> Self {
118        let pixels = value.pixels;
119        let size = UVec2::new(value.width, value.height);
120        let format = match value.format {
121            gltf::image::Format::R8 => AtlasImageFormat::R8,
122            gltf::image::Format::R8G8 => AtlasImageFormat::R8G8,
123            gltf::image::Format::R8G8B8 => AtlasImageFormat::R8G8B8,
124            gltf::image::Format::R8G8B8A8 => AtlasImageFormat::R8G8B8A8,
125            gltf::image::Format::R16 => AtlasImageFormat::R16,
126            gltf::image::Format::R16G16 => AtlasImageFormat::R16G16,
127            gltf::image::Format::R16G16B16 => AtlasImageFormat::R16G16B16,
128            gltf::image::Format::R16G16B16A16 => AtlasImageFormat::R16G16B16A16,
129            gltf::image::Format::R32G32B32FLOAT => AtlasImageFormat::R32G32B32FLOAT,
130            gltf::image::Format::R32G32B32A32FLOAT => AtlasImageFormat::R32G32B32A32FLOAT,
131        };
132
133        AtlasImage {
134            size,
135            pixels,
136            format,
137            // Determining this gets deferred until material construction
138            apply_linear_transfer: false,
139        }
140    }
141}
142
143impl From<image::DynamicImage> for AtlasImage {
144    fn from(value: image::DynamicImage) -> Self {
145        let width = value.width();
146        let height = value.height();
147
148        use AtlasImageFormat::*;
149        let (pixels, format) = match value {
150            image::DynamicImage::ImageLuma8(img) => (img.into_vec(), R8),
151            i @ image::DynamicImage::ImageLumaA8(_) => (i.into_rgba8().into_vec(), R8G8B8A8),
152            image::DynamicImage::ImageRgb8(img) => (img.into_vec(), R8G8B8),
153            image::DynamicImage::ImageRgba8(img) => (img.into_vec(), R8G8B8A8),
154            image::DynamicImage::ImageLuma16(img) => (img.as_bytes().to_vec(), R16),
155            i @ image::DynamicImage::ImageLumaA16(_) => {
156                (i.into_rgba16().as_bytes().to_vec(), R16G16B16A16)
157            }
158            i @ image::DynamicImage::ImageRgb16(_) => (i.as_bytes().to_vec(), R16G16B16),
159            i @ image::DynamicImage::ImageRgba16(_) => (i.as_bytes().to_vec(), R16G16B16A16),
160            i @ image::DynamicImage::ImageRgb32F(_) => (i.as_bytes().to_vec(), R32G32B32FLOAT),
161            i @ image::DynamicImage::ImageRgba32F(_) => (i.as_bytes().to_vec(), R32G32B32A32FLOAT),
162            _ => todo!(),
163        };
164        AtlasImage {
165            pixels,
166            format,
167            // Most of the time when people are using `image` to load images, those images
168            // have color data that was authored in sRGB space.
169            apply_linear_transfer: true,
170            size: UVec2::new(width, height),
171        }
172    }
173}
174
175impl TryFrom<std::path::PathBuf> for AtlasImage {
176    type Error = AtlasImageError;
177
178    fn try_from(value: std::path::PathBuf) -> Result<Self, Self::Error> {
179        let img = image::open(value).context(ImageSnafu)?;
180        Ok(img.into())
181    }
182}
183
184impl AtlasImage {
185    pub fn from_hdr_path(p: impl AsRef<std::path::Path>) -> Result<Self, AtlasImageError> {
186        let bytes = std::fs::read(p.as_ref()).with_context(|_| CannotLoadSnafu {
187            path: std::path::PathBuf::from(p.as_ref()),
188        })?;
189        Self::from_hdr_bytes(&bytes)
190    }
191
192    pub fn from_hdr_bytes(bytes: &[u8]) -> Result<Self, AtlasImageError> {
193        // Decode HDR data.
194        let decoder = image::codecs::hdr::HdrDecoder::new(bytes).context(ImageSnafu)?;
195        let width = decoder.metadata().width;
196        let height = decoder.metadata().height;
197        let img = image::DynamicImage::from_decoder(decoder).unwrap();
198        let pixels = img.into_rgb32f();
199
200        // Add alpha data.
201        let mut pixel_data: Vec<f32> = Vec::new();
202        for pixel in pixels.pixels() {
203            pixel_data.push(pixel[0]);
204            pixel_data.push(pixel[1]);
205            pixel_data.push(pixel[2]);
206            pixel_data.push(1.0);
207        }
208        let mut pixels = vec![];
209        pixels.extend_from_slice(bytemuck::cast_slice(pixel_data.as_slice()));
210
211        Ok(Self {
212            pixels,
213            size: UVec2::new(width, height),
214            format: AtlasImageFormat::R32G32B32A32FLOAT,
215            apply_linear_transfer: false,
216        })
217    }
218
219    pub fn from_path(p: impl AsRef<std::path::Path>) -> Result<Self, AtlasImageError> {
220        Self::try_from(p.as_ref().to_path_buf())
221    }
222
223    pub fn into_rgba8(self) -> Option<image::RgbaImage> {
224        let pixels = convert_pixels(
225            self.pixels,
226            self.format,
227            wgpu::TextureFormat::Rgba8Unorm,
228            self.apply_linear_transfer,
229        );
230        image::RgbaImage::from_vec(self.size.x, self.size.y, pixels)
231    }
232
233    /// Returns a new [`AtlasImage`] with zeroed data.
234    pub fn new(size: UVec2, format: AtlasImageFormat) -> Self {
235        Self {
236            pixels: std::iter::repeat_n(format.zero_pixel(), (size.x * size.y) as usize)
237                .flatten()
238                .copied()
239                .collect(),
240            size,
241            format,
242            apply_linear_transfer: false,
243        }
244    }
245}
246
247fn apply_linear_xfer(bytes: &mut [u8], format: AtlasImageFormat) {
248    use crate::color::*;
249    match format {
250        AtlasImageFormat::R8
251        | AtlasImageFormat::R8G8
252        | AtlasImageFormat::R8G8B8
253        | AtlasImageFormat::R8G8B8A8 => {
254            bytes.iter_mut().for_each(linear_xfer_u8);
255        }
256        AtlasImageFormat::R16
257        | AtlasImageFormat::R16G16
258        | AtlasImageFormat::R16G16B16
259        | AtlasImageFormat::R16G16B16A16 => {
260            let bytes: &mut [u16] = bytemuck::cast_slice_mut(bytes);
261            bytes.iter_mut().for_each(linear_xfer_u16);
262        }
263        AtlasImageFormat::R16G16B16A16FLOAT => {
264            let bytes: &mut [u16] = bytemuck::cast_slice_mut(bytes);
265            bytes.iter_mut().for_each(linear_xfer_f16);
266        }
267        AtlasImageFormat::R32G32B32FLOAT
268        | AtlasImageFormat::R32G32B32A32FLOAT
269        | AtlasImageFormat::D32FLOAT
270        | AtlasImageFormat::R32FLOAT => {
271            let bytes: &mut [f32] = bytemuck::cast_slice_mut(bytes);
272            bytes.iter_mut().for_each(linear_xfer_f32);
273        }
274    }
275}
276
277/// Interpret/convert the `AtlasImage` pixel data into `wgpu::TextureFormat`
278/// pixels, if possible.
279///
280/// This applies the linear transfer function if `apply_linear_transfer` is
281/// `true`.
282pub fn convert_pixels(
283    bytes: impl IntoIterator<Item = u8>,
284    from_format: AtlasImageFormat,
285    to_format: wgpu::TextureFormat,
286    apply_linear_transfer: bool,
287) -> Vec<u8> {
288    use crate::color::*;
289    let mut bytes = bytes.into_iter().collect::<Vec<_>>();
290    log::trace!("converting image of format {from_format:?}");
291    // Convert using linear transfer, if needed
292    if apply_linear_transfer {
293        log::trace!("  converting to linear color space (from sRGB)");
294        apply_linear_xfer(&mut bytes, from_format);
295    }
296
297    // Hamfisted conversion to `to_format`
298    match (from_format, to_format) {
299        (AtlasImageFormat::R8, wgpu::TextureFormat::Rgba8Unorm) => {
300            bytes.into_iter().flat_map(|r| [r, 0, 0, 255]).collect()
301        }
302        (AtlasImageFormat::R8G8, wgpu::TextureFormat::Rgba8Unorm) => bytes
303            .chunks_exact(2)
304            .flat_map(|p| {
305                if let [r, g] = p {
306                    [*r, *g, 0, 255]
307                } else {
308                    unreachable!()
309                }
310            })
311            .collect(),
312        (AtlasImageFormat::R8G8B8, wgpu::TextureFormat::Rgba8Unorm) => bytes
313            .chunks_exact(3)
314            .flat_map(|p| {
315                if let [r, g, b] = p {
316                    [*r, *g, *b, 255]
317                } else {
318                    unreachable!()
319                }
320            })
321            .collect(),
322        (AtlasImageFormat::R8G8B8A8, wgpu::TextureFormat::Rgba8Unorm) => bytes,
323        (AtlasImageFormat::R16, wgpu::TextureFormat::Rgba8Unorm) => {
324            bytemuck::cast_slice::<u8, u16>(&bytes)
325                .iter()
326                .flat_map(|r| [u16_to_u8(*r), 0, 0, 255])
327                .collect()
328        }
329        (AtlasImageFormat::R16G16, wgpu::TextureFormat::Rgba8Unorm) => {
330            bytemuck::cast_slice::<u8, u16>(&bytes)
331                .chunks_exact(2)
332                .flat_map(|p| {
333                    if let [r, g] = p {
334                        [u16_to_u8(*r), u16_to_u8(*g), 0, 255]
335                    } else {
336                        unreachable!()
337                    }
338                })
339                .collect()
340        }
341        (AtlasImageFormat::R16G16B16, wgpu::TextureFormat::Rgba8Unorm) => {
342            bytemuck::cast_slice::<u8, u16>(&bytes)
343                .chunks_exact(3)
344                .flat_map(|p| {
345                    if let [r, g, b] = p {
346                        [u16_to_u8(*r), u16_to_u8(*g), u16_to_u8(*b), 255]
347                    } else {
348                        unreachable!()
349                    }
350                })
351                .collect()
352        }
353
354        (AtlasImageFormat::R16G16B16A16, wgpu::TextureFormat::Rgba8Unorm) => {
355            bytemuck::cast_slice::<u8, u16>(&bytes)
356                .iter()
357                .copied()
358                .map(u16_to_u8)
359                .collect()
360        }
361        (AtlasImageFormat::R16G16B16A16FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {
362            bytemuck::cast_slice::<u8, u16>(&bytes)
363                .iter()
364                .map(|bits| half::f16::from_bits(*bits).to_f32())
365                .collect::<Vec<_>>()
366                .chunks_exact(4)
367                .flat_map(|p| {
368                    if let [r, g, b, a] = p {
369                        [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), f32_to_u8(*a)]
370                    } else {
371                        unreachable!()
372                    }
373                })
374                .collect()
375        }
376        (AtlasImageFormat::R32G32B32FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {
377            bytemuck::cast_slice::<u8, f32>(&bytes)
378                .chunks_exact(3)
379                .flat_map(|p| {
380                    if let [r, g, b] = p {
381                        [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), 255]
382                    } else {
383                        unreachable!()
384                    }
385                })
386                .collect()
387        }
388        (AtlasImageFormat::R32G32B32A32FLOAT, wgpu::TextureFormat::Rgba8Unorm)
389        | (AtlasImageFormat::R32FLOAT, wgpu::TextureFormat::Rgba8Unorm)
390        | (AtlasImageFormat::D32FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {
391            bytemuck::cast_slice::<u8, f32>(&bytes)
392                .iter()
393                .copied()
394                .map(f32_to_u8)
395                .collect()
396        }
397        (AtlasImageFormat::R32FLOAT, wgpu::TextureFormat::R32Float) => bytes,
398        // TODO: add more atlas format conversions
399        (from, to) => panic!("cannot convert from atlas format {from:?} to {to:?}"),
400    }
401}