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