bevy_image/
image.rs

1#[cfg(feature = "basis-universal")]
2use super::basis::*;
3#[cfg(feature = "dds")]
4use super::dds::*;
5#[cfg(feature = "ktx2")]
6use super::ktx2::*;
7#[cfg(not(feature = "bevy_reflect"))]
8use bevy_reflect::TypePath;
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11
12use bevy_asset::{Asset, RenderAssetUsages};
13use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza};
14use bevy_math::{AspectRatio, UVec2, UVec3, Vec2};
15use core::hash::Hash;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use tracing::warn;
19use wgpu_types::{
20    AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor,
21    SamplerDescriptor, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
22    TextureViewDescriptor,
23};
24
25pub trait BevyDefault {
26    fn bevy_default() -> Self;
27}
28
29impl BevyDefault for TextureFormat {
30    fn bevy_default() -> Self {
31        TextureFormat::Rgba8UnormSrgb
32    }
33}
34
35pub const TEXTURE_ASSET_INDEX: u64 = 0;
36pub const SAMPLER_ASSET_INDEX: u64 = 1;
37
38#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
39pub enum ImageFormat {
40    #[cfg(feature = "basis-universal")]
41    Basis,
42    #[cfg(feature = "bmp")]
43    Bmp,
44    #[cfg(feature = "dds")]
45    Dds,
46    #[cfg(feature = "ff")]
47    Farbfeld,
48    #[cfg(feature = "gif")]
49    Gif,
50    #[cfg(feature = "exr")]
51    OpenExr,
52    #[cfg(feature = "hdr")]
53    Hdr,
54    #[cfg(feature = "ico")]
55    Ico,
56    #[cfg(feature = "jpeg")]
57    Jpeg,
58    #[cfg(feature = "ktx2")]
59    Ktx2,
60    #[cfg(feature = "png")]
61    Png,
62    #[cfg(feature = "pnm")]
63    Pnm,
64    #[cfg(feature = "qoi")]
65    Qoi,
66    #[cfg(feature = "tga")]
67    Tga,
68    #[cfg(feature = "tiff")]
69    Tiff,
70    #[cfg(feature = "webp")]
71    WebP,
72}
73
74macro_rules! feature_gate {
75    ($feature: tt, $value: ident) => {{
76        #[cfg(not(feature = $feature))]
77        {
78            tracing::warn!("feature \"{}\" is not enabled", $feature);
79            return None;
80        }
81        #[cfg(feature = $feature)]
82        ImageFormat::$value
83    }};
84}
85
86impl ImageFormat {
87    /// Gets the file extensions for a given format.
88    pub const fn to_file_extensions(&self) -> &'static [&'static str] {
89        match self {
90            #[cfg(feature = "basis-universal")]
91            ImageFormat::Basis => &["basis"],
92            #[cfg(feature = "bmp")]
93            ImageFormat::Bmp => &["bmp"],
94            #[cfg(feature = "dds")]
95            ImageFormat::Dds => &["dds"],
96            #[cfg(feature = "ff")]
97            ImageFormat::Farbfeld => &["ff", "farbfeld"],
98            #[cfg(feature = "gif")]
99            ImageFormat::Gif => &["gif"],
100            #[cfg(feature = "exr")]
101            ImageFormat::OpenExr => &["exr"],
102            #[cfg(feature = "hdr")]
103            ImageFormat::Hdr => &["hdr"],
104            #[cfg(feature = "ico")]
105            ImageFormat::Ico => &["ico"],
106            #[cfg(feature = "jpeg")]
107            ImageFormat::Jpeg => &["jpg", "jpeg"],
108            #[cfg(feature = "ktx2")]
109            ImageFormat::Ktx2 => &["ktx2"],
110            #[cfg(feature = "pnm")]
111            ImageFormat::Pnm => &["pam", "pbm", "pgm", "ppm"],
112            #[cfg(feature = "png")]
113            ImageFormat::Png => &["png"],
114            #[cfg(feature = "qoi")]
115            ImageFormat::Qoi => &["qoi"],
116            #[cfg(feature = "tga")]
117            ImageFormat::Tga => &["tga"],
118            #[cfg(feature = "tiff")]
119            ImageFormat::Tiff => &["tif", "tiff"],
120            #[cfg(feature = "webp")]
121            ImageFormat::WebP => &["webp"],
122            // FIXME: https://github.com/rust-lang/rust/issues/129031
123            #[expect(
124                clippy::allow_attributes,
125                reason = "`unreachable_patterns` may not always lint"
126            )]
127            #[allow(
128                unreachable_patterns,
129                reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
130            )]
131            _ => &[],
132        }
133    }
134
135    /// Gets the MIME types for a given format.
136    ///
137    /// If a format doesn't have any dedicated MIME types, this list will be empty.
138    pub const fn to_mime_types(&self) -> &'static [&'static str] {
139        match self {
140            #[cfg(feature = "basis-universal")]
141            ImageFormat::Basis => &["image/basis", "image/x-basis"],
142            #[cfg(feature = "bmp")]
143            ImageFormat::Bmp => &["image/bmp", "image/x-bmp"],
144            #[cfg(feature = "dds")]
145            ImageFormat::Dds => &["image/vnd-ms.dds"],
146            #[cfg(feature = "hdr")]
147            ImageFormat::Hdr => &["image/vnd.radiance"],
148            #[cfg(feature = "gif")]
149            ImageFormat::Gif => &["image/gif"],
150            #[cfg(feature = "ff")]
151            ImageFormat::Farbfeld => &[],
152            #[cfg(feature = "ico")]
153            ImageFormat::Ico => &["image/x-icon"],
154            #[cfg(feature = "jpeg")]
155            ImageFormat::Jpeg => &["image/jpeg"],
156            #[cfg(feature = "ktx2")]
157            ImageFormat::Ktx2 => &["image/ktx2"],
158            #[cfg(feature = "png")]
159            ImageFormat::Png => &["image/png"],
160            #[cfg(feature = "qoi")]
161            ImageFormat::Qoi => &["image/qoi", "image/x-qoi"],
162            #[cfg(feature = "exr")]
163            ImageFormat::OpenExr => &["image/x-exr"],
164            #[cfg(feature = "pnm")]
165            ImageFormat::Pnm => &[
166                "image/x-portable-bitmap",
167                "image/x-portable-graymap",
168                "image/x-portable-pixmap",
169                "image/x-portable-anymap",
170            ],
171            #[cfg(feature = "tga")]
172            ImageFormat::Tga => &["image/x-targa", "image/x-tga"],
173            #[cfg(feature = "tiff")]
174            ImageFormat::Tiff => &["image/tiff"],
175            #[cfg(feature = "webp")]
176            ImageFormat::WebP => &["image/webp"],
177            // FIXME: https://github.com/rust-lang/rust/issues/129031
178            #[expect(
179                clippy::allow_attributes,
180                reason = "`unreachable_patterns` may not always lint"
181            )]
182            #[allow(
183                unreachable_patterns,
184                reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
185            )]
186            _ => &[],
187        }
188    }
189
190    pub fn from_mime_type(mime_type: &str) -> Option<Self> {
191        #[expect(
192            clippy::allow_attributes,
193            reason = "`unreachable_code` may not always lint"
194        )]
195        #[allow(
196            unreachable_code,
197            reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
198        )]
199        Some(match mime_type.to_ascii_lowercase().as_str() {
200            // note: farbfeld does not have a MIME type
201            "image/basis" | "image/x-basis" => feature_gate!("basis-universal", Basis),
202            "image/bmp" | "image/x-bmp" => feature_gate!("bmp", Bmp),
203            "image/vnd-ms.dds" => feature_gate!("dds", Dds),
204            "image/vnd.radiance" => feature_gate!("hdr", Hdr),
205            "image/gif" => feature_gate!("gif", Gif),
206            "image/x-icon" => feature_gate!("ico", Ico),
207            "image/jpeg" => feature_gate!("jpeg", Jpeg),
208            "image/ktx2" => feature_gate!("ktx2", Ktx2),
209            "image/png" => feature_gate!("png", Png),
210            "image/qoi" | "image/x-qoi" => feature_gate!("qoi", Qoi),
211            "image/x-exr" => feature_gate!("exr", OpenExr),
212            "image/x-portable-bitmap"
213            | "image/x-portable-graymap"
214            | "image/x-portable-pixmap"
215            | "image/x-portable-anymap" => feature_gate!("pnm", Pnm),
216            "image/x-targa" | "image/x-tga" => feature_gate!("tga", Tga),
217            "image/tiff" => feature_gate!("tiff", Tiff),
218            "image/webp" => feature_gate!("webp", WebP),
219            _ => return None,
220        })
221    }
222
223    pub fn from_extension(extension: &str) -> Option<Self> {
224        #[expect(
225            clippy::allow_attributes,
226            reason = "`unreachable_code` may not always lint"
227        )]
228        #[allow(
229            unreachable_code,
230            reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
231        )]
232        Some(match extension.to_ascii_lowercase().as_str() {
233            "basis" => feature_gate!("basis-universal", Basis),
234            "bmp" => feature_gate!("bmp", Bmp),
235            "dds" => feature_gate!("dds", Dds),
236            "ff" | "farbfeld" => feature_gate!("ff", Farbfeld),
237            "gif" => feature_gate!("gif", Gif),
238            "exr" => feature_gate!("exr", OpenExr),
239            "hdr" => feature_gate!("hdr", Hdr),
240            "ico" => feature_gate!("ico", Ico),
241            "jpg" | "jpeg" => feature_gate!("jpeg", Jpeg),
242            "ktx2" => feature_gate!("ktx2", Ktx2),
243            "pam" | "pbm" | "pgm" | "ppm" => feature_gate!("pnm", Pnm),
244            "png" => feature_gate!("png", Png),
245            "qoi" => feature_gate!("qoi", Qoi),
246            "tga" => feature_gate!("tga", Tga),
247            "tif" | "tiff" => feature_gate!("tiff", Tiff),
248            "webp" => feature_gate!("webp", WebP),
249            _ => return None,
250        })
251    }
252
253    pub fn as_image_crate_format(&self) -> Option<image::ImageFormat> {
254        #[expect(
255            clippy::allow_attributes,
256            reason = "`unreachable_code` may not always lint"
257        )]
258        #[allow(
259            unreachable_code,
260            reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
261        )]
262        Some(match self {
263            #[cfg(feature = "bmp")]
264            ImageFormat::Bmp => image::ImageFormat::Bmp,
265            #[cfg(feature = "dds")]
266            ImageFormat::Dds => image::ImageFormat::Dds,
267            #[cfg(feature = "ff")]
268            ImageFormat::Farbfeld => image::ImageFormat::Farbfeld,
269            #[cfg(feature = "gif")]
270            ImageFormat::Gif => image::ImageFormat::Gif,
271            #[cfg(feature = "exr")]
272            ImageFormat::OpenExr => image::ImageFormat::OpenExr,
273            #[cfg(feature = "hdr")]
274            ImageFormat::Hdr => image::ImageFormat::Hdr,
275            #[cfg(feature = "ico")]
276            ImageFormat::Ico => image::ImageFormat::Ico,
277            #[cfg(feature = "jpeg")]
278            ImageFormat::Jpeg => image::ImageFormat::Jpeg,
279            #[cfg(feature = "png")]
280            ImageFormat::Png => image::ImageFormat::Png,
281            #[cfg(feature = "pnm")]
282            ImageFormat::Pnm => image::ImageFormat::Pnm,
283            #[cfg(feature = "qoi")]
284            ImageFormat::Qoi => image::ImageFormat::Qoi,
285            #[cfg(feature = "tga")]
286            ImageFormat::Tga => image::ImageFormat::Tga,
287            #[cfg(feature = "tiff")]
288            ImageFormat::Tiff => image::ImageFormat::Tiff,
289            #[cfg(feature = "webp")]
290            ImageFormat::WebP => image::ImageFormat::WebP,
291            #[cfg(feature = "basis-universal")]
292            ImageFormat::Basis => return None,
293            #[cfg(feature = "ktx2")]
294            ImageFormat::Ktx2 => return None,
295            // FIXME: https://github.com/rust-lang/rust/issues/129031
296            #[expect(
297                clippy::allow_attributes,
298                reason = "`unreachable_patterns` may not always lint"
299            )]
300            #[allow(
301                unreachable_patterns,
302                reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
303            )]
304            _ => return None,
305        })
306    }
307
308    pub fn from_image_crate_format(format: image::ImageFormat) -> Option<ImageFormat> {
309        #[expect(
310            clippy::allow_attributes,
311            reason = "`unreachable_code` may not always lint"
312        )]
313        #[allow(
314            unreachable_code,
315            reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
316        )]
317        Some(match format {
318            image::ImageFormat::Bmp => feature_gate!("bmp", Bmp),
319            image::ImageFormat::Dds => feature_gate!("dds", Dds),
320            image::ImageFormat::Farbfeld => feature_gate!("ff", Farbfeld),
321            image::ImageFormat::Gif => feature_gate!("gif", Gif),
322            image::ImageFormat::OpenExr => feature_gate!("exr", OpenExr),
323            image::ImageFormat::Hdr => feature_gate!("hdr", Hdr),
324            image::ImageFormat::Ico => feature_gate!("ico", Ico),
325            image::ImageFormat::Jpeg => feature_gate!("jpeg", Jpeg),
326            image::ImageFormat::Png => feature_gate!("png", Png),
327            image::ImageFormat::Pnm => feature_gate!("pnm", Pnm),
328            image::ImageFormat::Qoi => feature_gate!("qoi", Qoi),
329            image::ImageFormat::Tga => feature_gate!("tga", Tga),
330            image::ImageFormat::Tiff => feature_gate!("tiff", Tiff),
331            image::ImageFormat::WebP => feature_gate!("webp", WebP),
332            _ => return None,
333        })
334    }
335}
336
337#[derive(Asset, Debug, Clone)]
338#[cfg_attr(
339    feature = "bevy_reflect",
340    derive(Reflect),
341    reflect(opaque, Default, Debug, Clone)
342)]
343#[cfg_attr(not(feature = "bevy_reflect"), derive(TypePath))]
344pub struct Image {
345    /// Raw pixel data.
346    /// If the image is being used as a storage texture which doesn't need to be initialized by the
347    /// CPU, then this should be `None`
348    /// Otherwise, it should always be `Some`
349    pub data: Option<Vec<u8>>,
350    // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors
351    pub texture_descriptor: TextureDescriptor<Option<&'static str>, &'static [TextureFormat]>,
352    /// The [`ImageSampler`] to use during rendering.
353    pub sampler: ImageSampler,
354    pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
355    pub asset_usage: RenderAssetUsages,
356}
357
358/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
359/// [`ImageSampler::Default`], will read the sampler from the `ImagePlugin` at setup.
360/// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`].
361#[derive(Debug, Default, Clone, Serialize, Deserialize)]
362pub enum ImageSampler {
363    /// Default image sampler, derived from the `ImagePlugin` setup.
364    #[default]
365    Default,
366    /// Custom sampler for this image which will override global default.
367    Descriptor(ImageSamplerDescriptor),
368}
369
370impl ImageSampler {
371    /// Returns an image sampler with [`ImageFilterMode::Linear`] min and mag filters
372    #[inline]
373    pub fn linear() -> ImageSampler {
374        ImageSampler::Descriptor(ImageSamplerDescriptor::linear())
375    }
376
377    /// Returns an image sampler with [`ImageFilterMode::Nearest`] min and mag filters
378    #[inline]
379    pub fn nearest() -> ImageSampler {
380        ImageSampler::Descriptor(ImageSamplerDescriptor::nearest())
381    }
382
383    /// Initialize the descriptor if it is not already initialized.
384    ///
385    /// Descriptor is typically initialized by Bevy when the image is loaded,
386    /// so this is convenient shortcut for updating the descriptor.
387    pub fn get_or_init_descriptor(&mut self) -> &mut ImageSamplerDescriptor {
388        match self {
389            ImageSampler::Default => {
390                *self = ImageSampler::Descriptor(ImageSamplerDescriptor::default());
391                match self {
392                    ImageSampler::Descriptor(descriptor) => descriptor,
393                    _ => unreachable!(),
394                }
395            }
396            ImageSampler::Descriptor(descriptor) => descriptor,
397        }
398    }
399}
400
401/// How edges should be handled in texture addressing.
402///
403/// See [`ImageSamplerDescriptor`] for information how to configure this.
404///
405/// This type mirrors [`AddressMode`].
406#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
407pub enum ImageAddressMode {
408    /// Clamp the value to the edge of the texture.
409    ///
410    /// -0.25 -> 0.0
411    /// 1.25  -> 1.0
412    #[default]
413    ClampToEdge,
414    /// Repeat the texture in a tiling fashion.
415    ///
416    /// -0.25 -> 0.75
417    /// 1.25 -> 0.25
418    Repeat,
419    /// Repeat the texture, mirroring it every repeat.
420    ///
421    /// -0.25 -> 0.25
422    /// 1.25 -> 0.75
423    MirrorRepeat,
424    /// Clamp the value to the border of the texture
425    /// Requires the wgpu feature [`Features::ADDRESS_MODE_CLAMP_TO_BORDER`].
426    ///
427    /// -0.25 -> border
428    /// 1.25 -> border
429    ClampToBorder,
430}
431
432/// Texel mixing mode when sampling between texels.
433///
434/// This type mirrors [`FilterMode`].
435#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
436pub enum ImageFilterMode {
437    /// Nearest neighbor sampling.
438    ///
439    /// This creates a pixelated effect when used as a mag filter.
440    #[default]
441    Nearest,
442    /// Linear Interpolation.
443    ///
444    /// This makes textures smooth but blurry when used as a mag filter.
445    Linear,
446}
447
448/// Comparison function used for depth and stencil operations.
449///
450/// This type mirrors [`CompareFunction`].
451#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
452pub enum ImageCompareFunction {
453    /// Function never passes
454    Never,
455    /// Function passes if new value less than existing value
456    Less,
457    /// Function passes if new value is equal to existing value. When using
458    /// this compare function, make sure to mark your Vertex Shader's `@builtin(position)`
459    /// output as `@invariant` to prevent artifacting.
460    Equal,
461    /// Function passes if new value is less than or equal to existing value
462    LessEqual,
463    /// Function passes if new value is greater than existing value
464    Greater,
465    /// Function passes if new value is not equal to existing value. When using
466    /// this compare function, make sure to mark your Vertex Shader's `@builtin(position)`
467    /// output as `@invariant` to prevent artifacting.
468    NotEqual,
469    /// Function passes if new value is greater than or equal to existing value
470    GreaterEqual,
471    /// Function always passes
472    Always,
473}
474
475/// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`].
476///
477/// This type mirrors [`SamplerBorderColor`].
478#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
479pub enum ImageSamplerBorderColor {
480    /// RGBA color `[0, 0, 0, 0]`.
481    TransparentBlack,
482    /// RGBA color `[0, 0, 0, 1]`.
483    OpaqueBlack,
484    /// RGBA color `[1, 1, 1, 1]`.
485    OpaqueWhite,
486    /// On the Metal wgpu backend, this is equivalent to [`Self::TransparentBlack`] for
487    /// textures that have an alpha component, and equivalent to [`Self::OpaqueBlack`]
488    /// for textures that do not have an alpha component. On other backends,
489    /// this is equivalent to [`Self::TransparentBlack`]. Requires
490    /// [`Features::ADDRESS_MODE_CLAMP_TO_ZERO`]. Not supported on the web.
491    Zero,
492}
493
494/// Indicates to an `ImageLoader` how an [`Image`] should be sampled.
495///
496/// As this type is part of the `ImageLoaderSettings`,
497/// it will be serialized to an image asset `.meta` file which might require a migration in case of
498/// a breaking change.
499///
500/// This types mirrors [`SamplerDescriptor`], but that might change in future versions.
501#[derive(Clone, Debug, Serialize, Deserialize)]
502pub struct ImageSamplerDescriptor {
503    pub label: Option<String>,
504    /// How to deal with out of bounds accesses in the u (i.e. x) direction.
505    pub address_mode_u: ImageAddressMode,
506    /// How to deal with out of bounds accesses in the v (i.e. y) direction.
507    pub address_mode_v: ImageAddressMode,
508    /// How to deal with out of bounds accesses in the w (i.e. z) direction.
509    pub address_mode_w: ImageAddressMode,
510    /// How to filter the texture when it needs to be magnified (made larger).
511    pub mag_filter: ImageFilterMode,
512    /// How to filter the texture when it needs to be minified (made smaller).
513    pub min_filter: ImageFilterMode,
514    /// How to filter between mip map levels
515    pub mipmap_filter: ImageFilterMode,
516    /// Minimum level of detail (i.e. mip level) to use.
517    pub lod_min_clamp: f32,
518    /// Maximum level of detail (i.e. mip level) to use.
519    pub lod_max_clamp: f32,
520    /// If this is enabled, this is a comparison sampler using the given comparison function.
521    pub compare: Option<ImageCompareFunction>,
522    /// Must be at least 1. If this is not 1, all filter modes must be linear.
523    pub anisotropy_clamp: u16,
524    /// Border color to use when `address_mode` is [`ImageAddressMode::ClampToBorder`].
525    pub border_color: Option<ImageSamplerBorderColor>,
526}
527
528impl Default for ImageSamplerDescriptor {
529    fn default() -> Self {
530        Self {
531            address_mode_u: Default::default(),
532            address_mode_v: Default::default(),
533            address_mode_w: Default::default(),
534            mag_filter: Default::default(),
535            min_filter: Default::default(),
536            mipmap_filter: Default::default(),
537            lod_min_clamp: 0.0,
538            lod_max_clamp: 32.0,
539            compare: None,
540            anisotropy_clamp: 1,
541            border_color: None,
542            label: None,
543        }
544    }
545}
546
547impl ImageSamplerDescriptor {
548    /// Returns a sampler descriptor with [`Linear`](ImageFilterMode::Linear) min and mag filters
549    #[inline]
550    pub fn linear() -> ImageSamplerDescriptor {
551        ImageSamplerDescriptor {
552            mag_filter: ImageFilterMode::Linear,
553            min_filter: ImageFilterMode::Linear,
554            mipmap_filter: ImageFilterMode::Linear,
555            ..Default::default()
556        }
557    }
558
559    /// Returns a sampler descriptor with [`Nearest`](ImageFilterMode::Nearest) min and mag filters
560    #[inline]
561    pub fn nearest() -> ImageSamplerDescriptor {
562        ImageSamplerDescriptor {
563            mag_filter: ImageFilterMode::Nearest,
564            min_filter: ImageFilterMode::Nearest,
565            mipmap_filter: ImageFilterMode::Nearest,
566            ..Default::default()
567        }
568    }
569
570    pub fn as_wgpu(&self) -> SamplerDescriptor<Option<&str>> {
571        SamplerDescriptor {
572            label: self.label.as_deref(),
573            address_mode_u: self.address_mode_u.into(),
574            address_mode_v: self.address_mode_v.into(),
575            address_mode_w: self.address_mode_w.into(),
576            mag_filter: self.mag_filter.into(),
577            min_filter: self.min_filter.into(),
578            mipmap_filter: self.mipmap_filter.into(),
579            lod_min_clamp: self.lod_min_clamp,
580            lod_max_clamp: self.lod_max_clamp,
581            compare: self.compare.map(Into::into),
582            anisotropy_clamp: self.anisotropy_clamp,
583            border_color: self.border_color.map(Into::into),
584        }
585    }
586}
587
588impl From<ImageAddressMode> for AddressMode {
589    fn from(value: ImageAddressMode) -> Self {
590        match value {
591            ImageAddressMode::ClampToEdge => AddressMode::ClampToEdge,
592            ImageAddressMode::Repeat => AddressMode::Repeat,
593            ImageAddressMode::MirrorRepeat => AddressMode::MirrorRepeat,
594            ImageAddressMode::ClampToBorder => AddressMode::ClampToBorder,
595        }
596    }
597}
598
599impl From<ImageFilterMode> for FilterMode {
600    fn from(value: ImageFilterMode) -> Self {
601        match value {
602            ImageFilterMode::Nearest => FilterMode::Nearest,
603            ImageFilterMode::Linear => FilterMode::Linear,
604        }
605    }
606}
607
608impl From<ImageCompareFunction> for CompareFunction {
609    fn from(value: ImageCompareFunction) -> Self {
610        match value {
611            ImageCompareFunction::Never => CompareFunction::Never,
612            ImageCompareFunction::Less => CompareFunction::Less,
613            ImageCompareFunction::Equal => CompareFunction::Equal,
614            ImageCompareFunction::LessEqual => CompareFunction::LessEqual,
615            ImageCompareFunction::Greater => CompareFunction::Greater,
616            ImageCompareFunction::NotEqual => CompareFunction::NotEqual,
617            ImageCompareFunction::GreaterEqual => CompareFunction::GreaterEqual,
618            ImageCompareFunction::Always => CompareFunction::Always,
619        }
620    }
621}
622
623impl From<ImageSamplerBorderColor> for SamplerBorderColor {
624    fn from(value: ImageSamplerBorderColor) -> Self {
625        match value {
626            ImageSamplerBorderColor::TransparentBlack => SamplerBorderColor::TransparentBlack,
627            ImageSamplerBorderColor::OpaqueBlack => SamplerBorderColor::OpaqueBlack,
628            ImageSamplerBorderColor::OpaqueWhite => SamplerBorderColor::OpaqueWhite,
629            ImageSamplerBorderColor::Zero => SamplerBorderColor::Zero,
630        }
631    }
632}
633
634impl From<AddressMode> for ImageAddressMode {
635    fn from(value: AddressMode) -> Self {
636        match value {
637            AddressMode::ClampToEdge => ImageAddressMode::ClampToEdge,
638            AddressMode::Repeat => ImageAddressMode::Repeat,
639            AddressMode::MirrorRepeat => ImageAddressMode::MirrorRepeat,
640            AddressMode::ClampToBorder => ImageAddressMode::ClampToBorder,
641        }
642    }
643}
644
645impl From<FilterMode> for ImageFilterMode {
646    fn from(value: FilterMode) -> Self {
647        match value {
648            FilterMode::Nearest => ImageFilterMode::Nearest,
649            FilterMode::Linear => ImageFilterMode::Linear,
650        }
651    }
652}
653
654impl From<CompareFunction> for ImageCompareFunction {
655    fn from(value: CompareFunction) -> Self {
656        match value {
657            CompareFunction::Never => ImageCompareFunction::Never,
658            CompareFunction::Less => ImageCompareFunction::Less,
659            CompareFunction::Equal => ImageCompareFunction::Equal,
660            CompareFunction::LessEqual => ImageCompareFunction::LessEqual,
661            CompareFunction::Greater => ImageCompareFunction::Greater,
662            CompareFunction::NotEqual => ImageCompareFunction::NotEqual,
663            CompareFunction::GreaterEqual => ImageCompareFunction::GreaterEqual,
664            CompareFunction::Always => ImageCompareFunction::Always,
665        }
666    }
667}
668
669impl From<SamplerBorderColor> for ImageSamplerBorderColor {
670    fn from(value: SamplerBorderColor) -> Self {
671        match value {
672            SamplerBorderColor::TransparentBlack => ImageSamplerBorderColor::TransparentBlack,
673            SamplerBorderColor::OpaqueBlack => ImageSamplerBorderColor::OpaqueBlack,
674            SamplerBorderColor::OpaqueWhite => ImageSamplerBorderColor::OpaqueWhite,
675            SamplerBorderColor::Zero => ImageSamplerBorderColor::Zero,
676        }
677    }
678}
679
680impl From<SamplerDescriptor<Option<&str>>> for ImageSamplerDescriptor {
681    fn from(value: SamplerDescriptor<Option<&str>>) -> Self {
682        ImageSamplerDescriptor {
683            label: value.label.map(ToString::to_string),
684            address_mode_u: value.address_mode_u.into(),
685            address_mode_v: value.address_mode_v.into(),
686            address_mode_w: value.address_mode_w.into(),
687            mag_filter: value.mag_filter.into(),
688            min_filter: value.min_filter.into(),
689            mipmap_filter: value.mipmap_filter.into(),
690            lod_min_clamp: value.lod_min_clamp,
691            lod_max_clamp: value.lod_max_clamp,
692            compare: value.compare.map(Into::into),
693            anisotropy_clamp: value.anisotropy_clamp,
694            border_color: value.border_color.map(Into::into),
695        }
696    }
697}
698
699impl Default for Image {
700    /// default is a 1x1x1 all '1.0' texture
701    fn default() -> Self {
702        let mut image = Image::default_uninit();
703        image.data = Some(vec![255; image.texture_descriptor.format.pixel_size()]);
704        image
705    }
706}
707
708impl Image {
709    /// Creates a new image from raw binary data and the corresponding metadata.
710    ///
711    /// # Panics
712    /// Panics if the length of the `data`, volume of the `size` and the size of the `format`
713    /// do not match.
714    pub fn new(
715        size: Extent3d,
716        dimension: TextureDimension,
717        data: Vec<u8>,
718        format: TextureFormat,
719        asset_usage: RenderAssetUsages,
720    ) -> Self {
721        debug_assert_eq!(
722            size.volume() * format.pixel_size(),
723            data.len(),
724            "Pixel data, size and format have to match",
725        );
726        let mut image = Image::new_uninit(size, dimension, format, asset_usage);
727        image.data = Some(data);
728        image
729    }
730
731    /// Exactly the same as [`Image::new`], but doesn't initialize the image
732    pub fn new_uninit(
733        size: Extent3d,
734        dimension: TextureDimension,
735        format: TextureFormat,
736        asset_usage: RenderAssetUsages,
737    ) -> Self {
738        Image {
739            data: None,
740            texture_descriptor: TextureDescriptor {
741                size,
742                format,
743                dimension,
744                label: None,
745                mip_level_count: 1,
746                sample_count: 1,
747                usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
748                view_formats: &[],
749            },
750            sampler: ImageSampler::Default,
751            texture_view_descriptor: None,
752            asset_usage,
753        }
754    }
755
756    /// A transparent white 1x1x1 image.
757    ///
758    /// Contrast to [`Image::default`], which is opaque.
759    pub fn transparent() -> Image {
760        // We rely on the default texture format being RGBA8UnormSrgb
761        // when constructing a transparent color from bytes.
762        // If this changes, this function will need to be updated.
763        let format = TextureFormat::bevy_default();
764        debug_assert!(format.pixel_size() == 4);
765        let data = vec![255, 255, 255, 0];
766        Image::new(
767            Extent3d {
768                width: 1,
769                height: 1,
770                depth_or_array_layers: 1,
771            },
772            TextureDimension::D2,
773            data,
774            format,
775            RenderAssetUsages::default(),
776        )
777    }
778    /// Creates a new uninitialized 1x1x1 image
779    pub fn default_uninit() -> Image {
780        Image::new_uninit(
781            Extent3d {
782                width: 1,
783                height: 1,
784                depth_or_array_layers: 1,
785            },
786            TextureDimension::D2,
787            TextureFormat::bevy_default(),
788            RenderAssetUsages::default(),
789        )
790    }
791
792    /// Creates a new image from raw binary data and the corresponding metadata, by filling
793    /// the image data with the `pixel` data repeated multiple times.
794    ///
795    /// # Panics
796    /// Panics if the size of the `format` is not a multiple of the length of the `pixel` data.
797    pub fn new_fill(
798        size: Extent3d,
799        dimension: TextureDimension,
800        pixel: &[u8],
801        format: TextureFormat,
802        asset_usage: RenderAssetUsages,
803    ) -> Self {
804        let byte_len = format.pixel_size() * size.volume();
805        debug_assert_eq!(
806            pixel.len() % format.pixel_size(),
807            0,
808            "Must not have incomplete pixel data (pixel size is {}B).",
809            format.pixel_size(),
810        );
811        debug_assert!(
812            pixel.len() <= byte_len,
813            "Fill data must fit within pixel buffer (expected {}B).",
814            byte_len,
815        );
816        let data = pixel.iter().copied().cycle().take(byte_len).collect();
817        Image::new(size, dimension, data, format, asset_usage)
818    }
819
820    /// Returns the width of a 2D image.
821    #[inline]
822    pub fn width(&self) -> u32 {
823        self.texture_descriptor.size.width
824    }
825
826    /// Returns the height of a 2D image.
827    #[inline]
828    pub fn height(&self) -> u32 {
829        self.texture_descriptor.size.height
830    }
831
832    /// Returns the aspect ratio (width / height) of a 2D image.
833    #[inline]
834    pub fn aspect_ratio(&self) -> AspectRatio {
835        AspectRatio::try_from_pixels(self.width(), self.height()).expect(
836            "Failed to calculate aspect ratio: Image dimensions must be positive, non-zero values",
837        )
838    }
839
840    /// Returns the size of a 2D image as f32.
841    #[inline]
842    pub fn size_f32(&self) -> Vec2 {
843        Vec2::new(self.width() as f32, self.height() as f32)
844    }
845
846    /// Returns the size of a 2D image.
847    #[inline]
848    pub fn size(&self) -> UVec2 {
849        UVec2::new(self.width(), self.height())
850    }
851
852    /// Resizes the image to the new size, by removing information or appending 0 to the `data`.
853    /// Does not properly resize the contents of the image, but only its internal `data` buffer.
854    pub fn resize(&mut self, size: Extent3d) {
855        self.texture_descriptor.size = size;
856        if let Some(ref mut data) = self.data {
857            data.resize(
858                size.volume() * self.texture_descriptor.format.pixel_size(),
859                0,
860            );
861        } else {
862            warn!("Resized an uninitialized image. Directly modify image.texture_descriptor.size instead");
863        }
864    }
865
866    /// Changes the `size`, asserting that the total number of data elements (pixels) remains the
867    /// same.
868    ///
869    /// # Panics
870    /// Panics if the `new_size` does not have the same volume as to old one.
871    pub fn reinterpret_size(&mut self, new_size: Extent3d) {
872        assert_eq!(
873            new_size.volume(),
874            self.texture_descriptor.size.volume(),
875            "Incompatible sizes: old = {:?} new = {:?}",
876            self.texture_descriptor.size,
877            new_size
878        );
879
880        self.texture_descriptor.size = new_size;
881    }
882
883    /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
884    /// it as a 2D array texture, where each of the stacked images becomes one layer of the
885    /// array. This is primarily for use with the `texture2DArray` shader uniform type.
886    ///
887    /// # Panics
888    /// Panics if the texture is not 2D, has more than one layers or is not evenly dividable into
889    /// the `layers`.
890    pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) {
891        // Must be a stacked image, and the height must be divisible by layers.
892        assert_eq!(self.texture_descriptor.dimension, TextureDimension::D2);
893        assert_eq!(self.texture_descriptor.size.depth_or_array_layers, 1);
894        assert_eq!(self.height() % layers, 0);
895
896        self.reinterpret_size(Extent3d {
897            width: self.width(),
898            height: self.height() / layers,
899            depth_or_array_layers: layers,
900        });
901    }
902
903    /// Convert a texture from a format to another. Only a few formats are
904    /// supported as input and output:
905    /// - `TextureFormat::R8Unorm`
906    /// - `TextureFormat::Rg8Unorm`
907    /// - `TextureFormat::Rgba8UnormSrgb`
908    ///
909    /// To get [`Image`] as a [`image::DynamicImage`] see:
910    /// [`Image::try_into_dynamic`].
911    pub fn convert(&self, new_format: TextureFormat) -> Option<Self> {
912        self.clone()
913            .try_into_dynamic()
914            .ok()
915            .and_then(|img| match new_format {
916                TextureFormat::R8Unorm => {
917                    Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false))
918                }
919                TextureFormat::Rg8Unorm => Some((
920                    image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()),
921                    false,
922                )),
923                TextureFormat::Rgba8UnormSrgb => {
924                    Some((image::DynamicImage::ImageRgba8(img.into_rgba8()), true))
925                }
926                _ => None,
927            })
928            .map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb, self.asset_usage))
929    }
930
931    /// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image`
932    /// crate
933    pub fn from_buffer(
934        #[cfg(all(debug_assertions, feature = "dds"))] name: String,
935        buffer: &[u8],
936        image_type: ImageType,
937        #[cfg_attr(
938            not(any(feature = "basis-universal", feature = "dds", feature = "ktx2")),
939            expect(unused_variables, reason = "only used with certain features")
940        )]
941        supported_compressed_formats: CompressedImageFormats,
942        is_srgb: bool,
943        image_sampler: ImageSampler,
944        asset_usage: RenderAssetUsages,
945    ) -> Result<Image, TextureError> {
946        let format = image_type.to_image_format()?;
947
948        // Load the image in the expected format.
949        // Some formats like PNG allow for R or RG textures too, so the texture
950        // format needs to be determined. For RGB textures an alpha channel
951        // needs to be added, so the image data needs to be converted in those
952        // cases.
953
954        let mut image = match format {
955            #[cfg(feature = "basis-universal")]
956            ImageFormat::Basis => {
957                basis_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?
958            }
959            #[cfg(feature = "dds")]
960            ImageFormat::Dds => dds_buffer_to_image(
961                #[cfg(debug_assertions)]
962                name,
963                buffer,
964                supported_compressed_formats,
965                is_srgb,
966            )?,
967            #[cfg(feature = "ktx2")]
968            ImageFormat::Ktx2 => {
969                ktx2_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?
970            }
971            #[expect(
972                clippy::allow_attributes,
973                reason = "`unreachable_patterns` may not always lint"
974            )]
975            #[allow(
976                unreachable_patterns,
977                reason = "The wildcard pattern may be unreachable if only the specially-handled formats are enabled; however, the wildcard pattern is needed for any formats not specially handled"
978            )]
979            _ => {
980                let image_crate_format = format
981                    .as_image_crate_format()
982                    .ok_or_else(|| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?;
983                let mut reader = image::ImageReader::new(std::io::Cursor::new(buffer));
984                reader.set_format(image_crate_format);
985                reader.no_limits();
986                let dyn_img = reader.decode()?;
987                Self::from_dynamic(dyn_img, is_srgb, asset_usage)
988            }
989        };
990        image.sampler = image_sampler;
991        Ok(image)
992    }
993
994    /// Whether the texture format is compressed or uncompressed
995    pub fn is_compressed(&self) -> bool {
996        let format_description = self.texture_descriptor.format;
997        format_description
998            .required_features()
999            .contains(Features::TEXTURE_COMPRESSION_ASTC)
1000            || format_description
1001                .required_features()
1002                .contains(Features::TEXTURE_COMPRESSION_BC)
1003            || format_description
1004                .required_features()
1005                .contains(Features::TEXTURE_COMPRESSION_ETC2)
1006    }
1007
1008    /// Compute the byte offset where the data of a specific pixel is stored
1009    ///
1010    /// Returns None if the provided coordinates are out of bounds.
1011    ///
1012    /// For 2D textures, Z is the layer number. For 1D textures, Y and Z are ignored.
1013    #[inline(always)]
1014    pub fn pixel_data_offset(&self, coords: UVec3) -> Option<usize> {
1015        let width = self.texture_descriptor.size.width;
1016        let height = self.texture_descriptor.size.height;
1017        let depth = self.texture_descriptor.size.depth_or_array_layers;
1018
1019        let pixel_size = self.texture_descriptor.format.pixel_size();
1020        let pixel_offset = match self.texture_descriptor.dimension {
1021            TextureDimension::D3 | TextureDimension::D2 => {
1022                if coords.x >= width || coords.y >= height || coords.z >= depth {
1023                    return None;
1024                }
1025                coords.z * height * width + coords.y * width + coords.x
1026            }
1027            TextureDimension::D1 => {
1028                if coords.x >= width {
1029                    return None;
1030                }
1031                coords.x
1032            }
1033        };
1034
1035        Some(pixel_offset as usize * pixel_size)
1036    }
1037
1038    /// Get a reference to the data bytes where a specific pixel's value is stored
1039    #[inline(always)]
1040    pub fn pixel_bytes(&self, coords: UVec3) -> Option<&[u8]> {
1041        let len = self.texture_descriptor.format.pixel_size();
1042        let data = self.data.as_ref()?;
1043        self.pixel_data_offset(coords)
1044            .map(|start| &data[start..(start + len)])
1045    }
1046
1047    /// Get a mutable reference to the data bytes where a specific pixel's value is stored
1048    #[inline(always)]
1049    pub fn pixel_bytes_mut(&mut self, coords: UVec3) -> Option<&mut [u8]> {
1050        let len = self.texture_descriptor.format.pixel_size();
1051        let offset = self.pixel_data_offset(coords);
1052        let data = self.data.as_mut()?;
1053        offset.map(|start| &mut data[start..(start + len)])
1054    }
1055
1056    /// Read the color of a specific pixel (1D texture).
1057    ///
1058    /// See [`get_color_at`](Self::get_color_at) for more details.
1059    #[inline(always)]
1060    pub fn get_color_at_1d(&self, x: u32) -> Result<Color, TextureAccessError> {
1061        if self.texture_descriptor.dimension != TextureDimension::D1 {
1062            return Err(TextureAccessError::WrongDimension);
1063        }
1064        self.get_color_at_internal(UVec3::new(x, 0, 0))
1065    }
1066
1067    /// Read the color of a specific pixel (2D texture).
1068    ///
1069    /// This function will find the raw byte data of a specific pixel and
1070    /// decode it into a user-friendly [`Color`] struct for you.
1071    ///
1072    /// Supports many of the common [`TextureFormat`]s:
1073    ///  - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1074    ///  - 16-bit and 32-bit unsigned integer
1075    ///  - 16-bit and 32-bit float
1076    ///
1077    /// Be careful: as the data is converted to [`Color`] (which uses `f32` internally),
1078    /// there may be issues with precision when using non-f32 [`TextureFormat`]s.
1079    /// If you read a value you previously wrote using `set_color_at`, it will not match.
1080    /// If you are working with a 32-bit integer [`TextureFormat`], the value will be
1081    /// inaccurate (as `f32` does not have enough bits to represent it exactly).
1082    ///
1083    /// Single channel (R) formats are assumed to represent grayscale, so the value
1084    /// will be copied to all three RGB channels in the resulting [`Color`].
1085    ///
1086    /// Other [`TextureFormat`]s are unsupported, such as:
1087    ///  - block-compressed formats
1088    ///  - non-byte-aligned formats like 10-bit
1089    ///  - signed integer formats
1090    #[inline(always)]
1091    pub fn get_color_at(&self, x: u32, y: u32) -> Result<Color, TextureAccessError> {
1092        if self.texture_descriptor.dimension != TextureDimension::D2 {
1093            return Err(TextureAccessError::WrongDimension);
1094        }
1095        self.get_color_at_internal(UVec3::new(x, y, 0))
1096    }
1097
1098    /// Read the color of a specific pixel (2D texture with layers or 3D texture).
1099    ///
1100    /// See [`get_color_at`](Self::get_color_at) for more details.
1101    #[inline(always)]
1102    pub fn get_color_at_3d(&self, x: u32, y: u32, z: u32) -> Result<Color, TextureAccessError> {
1103        match (
1104            self.texture_descriptor.dimension,
1105            self.texture_descriptor.size.depth_or_array_layers,
1106        ) {
1107            (TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1108                self.get_color_at_internal(UVec3::new(x, y, z))
1109            }
1110            _ => Err(TextureAccessError::WrongDimension),
1111        }
1112    }
1113
1114    /// Change the color of a specific pixel (1D texture).
1115    ///
1116    /// See [`set_color_at`](Self::set_color_at) for more details.
1117    #[inline(always)]
1118    pub fn set_color_at_1d(&mut self, x: u32, color: Color) -> Result<(), TextureAccessError> {
1119        if self.texture_descriptor.dimension != TextureDimension::D1 {
1120            return Err(TextureAccessError::WrongDimension);
1121        }
1122        self.set_color_at_internal(UVec3::new(x, 0, 0), color)
1123    }
1124
1125    /// Change the color of a specific pixel (2D texture).
1126    ///
1127    /// This function will find the raw byte data of a specific pixel and
1128    /// change it according to a [`Color`] you provide. The [`Color`] struct
1129    /// will be encoded into the [`Image`]'s [`TextureFormat`].
1130    ///
1131    /// Supports many of the common [`TextureFormat`]s:
1132    ///  - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1133    ///  - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`)
1134    ///  - 16-bit and 32-bit float
1135    ///
1136    /// Be careful: writing to non-f32 [`TextureFormat`]s is lossy! The data has to be converted,
1137    /// so if you read it back using `get_color_at`, the `Color` you get will not equal the value
1138    /// you used when writing it using this function.
1139    ///
1140    /// For R and RG formats, only the respective values from the linear RGB [`Color`] will be used.
1141    ///
1142    /// Other [`TextureFormat`]s are unsupported, such as:
1143    ///  - block-compressed formats
1144    ///  - non-byte-aligned formats like 10-bit
1145    ///  - signed integer formats
1146    #[inline(always)]
1147    pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> {
1148        if self.texture_descriptor.dimension != TextureDimension::D2 {
1149            return Err(TextureAccessError::WrongDimension);
1150        }
1151        self.set_color_at_internal(UVec3::new(x, y, 0), color)
1152    }
1153
1154    /// Change the color of a specific pixel (2D texture with layers or 3D texture).
1155    ///
1156    /// See [`set_color_at`](Self::set_color_at) for more details.
1157    #[inline(always)]
1158    pub fn set_color_at_3d(
1159        &mut self,
1160        x: u32,
1161        y: u32,
1162        z: u32,
1163        color: Color,
1164    ) -> Result<(), TextureAccessError> {
1165        match (
1166            self.texture_descriptor.dimension,
1167            self.texture_descriptor.size.depth_or_array_layers,
1168        ) {
1169            (TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1170                self.set_color_at_internal(UVec3::new(x, y, z), color)
1171            }
1172            _ => Err(TextureAccessError::WrongDimension),
1173        }
1174    }
1175
1176    #[inline(always)]
1177    fn get_color_at_internal(&self, coords: UVec3) -> Result<Color, TextureAccessError> {
1178        let Some(bytes) = self.pixel_bytes(coords) else {
1179            return Err(TextureAccessError::OutOfBounds {
1180                x: coords.x,
1181                y: coords.y,
1182                z: coords.z,
1183            });
1184        };
1185
1186        // NOTE: GPUs are always Little Endian.
1187        // Make sure to respect that when we create color values from bytes.
1188        match self.texture_descriptor.format {
1189            TextureFormat::Rgba8UnormSrgb => Ok(Color::srgba(
1190                bytes[0] as f32 / u8::MAX as f32,
1191                bytes[1] as f32 / u8::MAX as f32,
1192                bytes[2] as f32 / u8::MAX as f32,
1193                bytes[3] as f32 / u8::MAX as f32,
1194            )),
1195            TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => Ok(Color::linear_rgba(
1196                bytes[0] as f32 / u8::MAX as f32,
1197                bytes[1] as f32 / u8::MAX as f32,
1198                bytes[2] as f32 / u8::MAX as f32,
1199                bytes[3] as f32 / u8::MAX as f32,
1200            )),
1201            TextureFormat::Bgra8UnormSrgb => Ok(Color::srgba(
1202                bytes[2] as f32 / u8::MAX as f32,
1203                bytes[1] as f32 / u8::MAX as f32,
1204                bytes[0] as f32 / u8::MAX as f32,
1205                bytes[3] as f32 / u8::MAX as f32,
1206            )),
1207            TextureFormat::Bgra8Unorm => Ok(Color::linear_rgba(
1208                bytes[2] as f32 / u8::MAX as f32,
1209                bytes[1] as f32 / u8::MAX as f32,
1210                bytes[0] as f32 / u8::MAX as f32,
1211                bytes[3] as f32 / u8::MAX as f32,
1212            )),
1213            TextureFormat::Rgba32Float => Ok(Color::linear_rgba(
1214                f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1215                f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1216                f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1217                f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1218            )),
1219            TextureFormat::Rgba16Float => Ok(Color::linear_rgba(
1220                half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32(),
1221                half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32(),
1222                half::f16::from_le_bytes([bytes[4], bytes[5]]).to_f32(),
1223                half::f16::from_le_bytes([bytes[6], bytes[7]]).to_f32(),
1224            )),
1225            TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1226                let (r, g, b, a) = (
1227                    u16::from_le_bytes([bytes[0], bytes[1]]),
1228                    u16::from_le_bytes([bytes[2], bytes[3]]),
1229                    u16::from_le_bytes([bytes[4], bytes[5]]),
1230                    u16::from_le_bytes([bytes[6], bytes[7]]),
1231                );
1232                Ok(Color::linear_rgba(
1233                    // going via f64 to avoid rounding errors with large numbers and division
1234                    (r as f64 / u16::MAX as f64) as f32,
1235                    (g as f64 / u16::MAX as f64) as f32,
1236                    (b as f64 / u16::MAX as f64) as f32,
1237                    (a as f64 / u16::MAX as f64) as f32,
1238                ))
1239            }
1240            TextureFormat::Rgba32Uint => {
1241                let (r, g, b, a) = (
1242                    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1243                    u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1244                    u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1245                    u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1246                );
1247                Ok(Color::linear_rgba(
1248                    // going via f64 to avoid rounding errors with large numbers and division
1249                    (r as f64 / u32::MAX as f64) as f32,
1250                    (g as f64 / u32::MAX as f64) as f32,
1251                    (b as f64 / u32::MAX as f64) as f32,
1252                    (a as f64 / u32::MAX as f64) as f32,
1253                ))
1254            }
1255            // assume R-only texture format means grayscale (linear)
1256            // copy value to all of RGB in Color
1257            TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1258                let x = bytes[0] as f32 / u8::MAX as f32;
1259                Ok(Color::linear_rgb(x, x, x))
1260            }
1261            TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1262                let x = u16::from_le_bytes([bytes[0], bytes[1]]);
1263                // going via f64 to avoid rounding errors with large numbers and division
1264                let x = (x as f64 / u16::MAX as f64) as f32;
1265                Ok(Color::linear_rgb(x, x, x))
1266            }
1267            TextureFormat::R32Uint => {
1268                let x = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1269                // going via f64 to avoid rounding errors with large numbers and division
1270                let x = (x as f64 / u32::MAX as f64) as f32;
1271                Ok(Color::linear_rgb(x, x, x))
1272            }
1273            TextureFormat::R16Float => {
1274                let x = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1275                Ok(Color::linear_rgb(x, x, x))
1276            }
1277            TextureFormat::R32Float => {
1278                let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1279                Ok(Color::linear_rgb(x, x, x))
1280            }
1281            TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1282                let r = bytes[0] as f32 / u8::MAX as f32;
1283                let g = bytes[1] as f32 / u8::MAX as f32;
1284                Ok(Color::linear_rgb(r, g, 0.0))
1285            }
1286            TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1287                let r = u16::from_le_bytes([bytes[0], bytes[1]]);
1288                let g = u16::from_le_bytes([bytes[2], bytes[3]]);
1289                // going via f64 to avoid rounding errors with large numbers and division
1290                let r = (r as f64 / u16::MAX as f64) as f32;
1291                let g = (g as f64 / u16::MAX as f64) as f32;
1292                Ok(Color::linear_rgb(r, g, 0.0))
1293            }
1294            TextureFormat::Rg32Uint => {
1295                let r = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1296                let g = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1297                // going via f64 to avoid rounding errors with large numbers and division
1298                let r = (r as f64 / u32::MAX as f64) as f32;
1299                let g = (g as f64 / u32::MAX as f64) as f32;
1300                Ok(Color::linear_rgb(r, g, 0.0))
1301            }
1302            TextureFormat::Rg16Float => {
1303                let r = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1304                let g = half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32();
1305                Ok(Color::linear_rgb(r, g, 0.0))
1306            }
1307            TextureFormat::Rg32Float => {
1308                let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1309                let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1310                Ok(Color::linear_rgb(r, g, 0.0))
1311            }
1312            _ => Err(TextureAccessError::UnsupportedTextureFormat(
1313                self.texture_descriptor.format,
1314            )),
1315        }
1316    }
1317
1318    #[inline(always)]
1319    fn set_color_at_internal(
1320        &mut self,
1321        coords: UVec3,
1322        color: Color,
1323    ) -> Result<(), TextureAccessError> {
1324        let format = self.texture_descriptor.format;
1325
1326        let Some(bytes) = self.pixel_bytes_mut(coords) else {
1327            return Err(TextureAccessError::OutOfBounds {
1328                x: coords.x,
1329                y: coords.y,
1330                z: coords.z,
1331            });
1332        };
1333
1334        // NOTE: GPUs are always Little Endian.
1335        // Make sure to respect that when we convert color values to bytes.
1336        match format {
1337            TextureFormat::Rgba8UnormSrgb => {
1338                let [r, g, b, a] = Srgba::from(color).to_f32_array();
1339                bytes[0] = (r * u8::MAX as f32) as u8;
1340                bytes[1] = (g * u8::MAX as f32) as u8;
1341                bytes[2] = (b * u8::MAX as f32) as u8;
1342                bytes[3] = (a * u8::MAX as f32) as u8;
1343            }
1344            TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => {
1345                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1346                bytes[0] = (r * u8::MAX as f32) as u8;
1347                bytes[1] = (g * u8::MAX as f32) as u8;
1348                bytes[2] = (b * u8::MAX as f32) as u8;
1349                bytes[3] = (a * u8::MAX as f32) as u8;
1350            }
1351            TextureFormat::Bgra8UnormSrgb => {
1352                let [r, g, b, a] = Srgba::from(color).to_f32_array();
1353                bytes[0] = (b * u8::MAX as f32) as u8;
1354                bytes[1] = (g * u8::MAX as f32) as u8;
1355                bytes[2] = (r * u8::MAX as f32) as u8;
1356                bytes[3] = (a * u8::MAX as f32) as u8;
1357            }
1358            TextureFormat::Bgra8Unorm => {
1359                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1360                bytes[0] = (b * u8::MAX as f32) as u8;
1361                bytes[1] = (g * u8::MAX as f32) as u8;
1362                bytes[2] = (r * u8::MAX as f32) as u8;
1363                bytes[3] = (a * u8::MAX as f32) as u8;
1364            }
1365            TextureFormat::Rgba16Float => {
1366                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1367                bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1368                bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1369                bytes[4..6].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(b)));
1370                bytes[6..8].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(a)));
1371            }
1372            TextureFormat::Rgba32Float => {
1373                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1374                bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1375                bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1376                bytes[8..12].copy_from_slice(&f32::to_le_bytes(b));
1377                bytes[12..16].copy_from_slice(&f32::to_le_bytes(a));
1378            }
1379            TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1380                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1381                let [r, g, b, a] = [
1382                    (r * u16::MAX as f32) as u16,
1383                    (g * u16::MAX as f32) as u16,
1384                    (b * u16::MAX as f32) as u16,
1385                    (a * u16::MAX as f32) as u16,
1386                ];
1387                bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1388                bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1389                bytes[4..6].copy_from_slice(&u16::to_le_bytes(b));
1390                bytes[6..8].copy_from_slice(&u16::to_le_bytes(a));
1391            }
1392            TextureFormat::Rgba32Uint => {
1393                let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1394                let [r, g, b, a] = [
1395                    (r * u32::MAX as f32) as u32,
1396                    (g * u32::MAX as f32) as u32,
1397                    (b * u32::MAX as f32) as u32,
1398                    (a * u32::MAX as f32) as u32,
1399                ];
1400                bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1401                bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1402                bytes[8..12].copy_from_slice(&u32::to_le_bytes(b));
1403                bytes[12..16].copy_from_slice(&u32::to_le_bytes(a));
1404            }
1405            TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1406                // Convert to grayscale with minimal loss if color is already gray
1407                let linear = LinearRgba::from(color);
1408                let luminance = Xyza::from(linear).y;
1409                let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1410                bytes[0] = (r * u8::MAX as f32) as u8;
1411            }
1412            TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1413                // Convert to grayscale with minimal loss if color is already gray
1414                let linear = LinearRgba::from(color);
1415                let luminance = Xyza::from(linear).y;
1416                let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1417                let r = (r * u16::MAX as f32) as u16;
1418                bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1419            }
1420            TextureFormat::R32Uint => {
1421                // Convert to grayscale with minimal loss if color is already gray
1422                let linear = LinearRgba::from(color);
1423                let luminance = Xyza::from(linear).y;
1424                let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1425                // go via f64 to avoid imprecision
1426                let r = (r as f64 * u32::MAX as f64) as u32;
1427                bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1428            }
1429            TextureFormat::R16Float => {
1430                // Convert to grayscale with minimal loss if color is already gray
1431                let linear = LinearRgba::from(color);
1432                let luminance = Xyza::from(linear).y;
1433                let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1434                let x = half::f16::from_f32(r);
1435                bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(x));
1436            }
1437            TextureFormat::R32Float => {
1438                // Convert to grayscale with minimal loss if color is already gray
1439                let linear = LinearRgba::from(color);
1440                let luminance = Xyza::from(linear).y;
1441                let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1442                bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1443            }
1444            TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1445                let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1446                bytes[0] = (r * u8::MAX as f32) as u8;
1447                bytes[1] = (g * u8::MAX as f32) as u8;
1448            }
1449            TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1450                let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1451                let r = (r * u16::MAX as f32) as u16;
1452                let g = (g * u16::MAX as f32) as u16;
1453                bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1454                bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1455            }
1456            TextureFormat::Rg32Uint => {
1457                let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1458                // go via f64 to avoid imprecision
1459                let r = (r as f64 * u32::MAX as f64) as u32;
1460                let g = (g as f64 * u32::MAX as f64) as u32;
1461                bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1462                bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1463            }
1464            TextureFormat::Rg16Float => {
1465                let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1466                bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1467                bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1468            }
1469            TextureFormat::Rg32Float => {
1470                let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1471                bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1472                bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1473            }
1474            _ => {
1475                return Err(TextureAccessError::UnsupportedTextureFormat(
1476                    self.texture_descriptor.format,
1477                ));
1478            }
1479        }
1480        Ok(())
1481    }
1482}
1483
1484#[derive(Clone, Copy, Debug)]
1485pub enum DataFormat {
1486    Rgb,
1487    Rgba,
1488    Rrr,
1489    Rrrg,
1490    Rg,
1491}
1492
1493/// Texture data need to be transcoded from this format for use with `wgpu`.
1494#[derive(Clone, Copy, Debug)]
1495pub enum TranscodeFormat {
1496    Etc1s,
1497    Uastc(DataFormat),
1498    // Has to be transcoded to R8Unorm for use with `wgpu`.
1499    R8UnormSrgb,
1500    // Has to be transcoded to R8G8Unorm for use with `wgpu`.
1501    Rg8UnormSrgb,
1502    // Has to be transcoded to Rgba8 for use with `wgpu`.
1503    Rgb8,
1504}
1505
1506/// An error that occurs when accessing specific pixels in a texture.
1507#[derive(Error, Debug)]
1508pub enum TextureAccessError {
1509    #[error("out of bounds (x: {x}, y: {y}, z: {z})")]
1510    OutOfBounds { x: u32, y: u32, z: u32 },
1511    #[error("unsupported texture format: {0:?}")]
1512    UnsupportedTextureFormat(TextureFormat),
1513    #[error("attempt to access texture with different dimension")]
1514    WrongDimension,
1515}
1516
1517/// An error that occurs when loading a texture.
1518#[derive(Error, Debug)]
1519pub enum TextureError {
1520    /// Image MIME type is invalid.
1521    #[error("invalid image mime type: {0}")]
1522    InvalidImageMimeType(String),
1523    /// Image extension is invalid.
1524    #[error("invalid image extension: {0}")]
1525    InvalidImageExtension(String),
1526    /// Failed to load an image.
1527    #[error("failed to load an image: {0}")]
1528    ImageError(#[from] image::ImageError),
1529    /// Texture format isn't supported.
1530    #[error("unsupported texture format: {0}")]
1531    UnsupportedTextureFormat(String),
1532    /// Supercompression isn't supported.
1533    #[error("supercompression not supported: {0}")]
1534    SuperCompressionNotSupported(String),
1535    /// Failed to decompress an image.
1536    #[error("failed to decompress an image: {0}")]
1537    SuperDecompressionError(String),
1538    /// Invalid data.
1539    #[error("invalid data: {0}")]
1540    InvalidData(String),
1541    /// Transcode error.
1542    #[error("transcode error: {0}")]
1543    TranscodeError(String),
1544    /// Format requires transcoding.
1545    #[error("format requires transcoding: {0:?}")]
1546    FormatRequiresTranscodingError(TranscodeFormat),
1547    /// Only cubemaps with six faces are supported.
1548    #[error("only cubemaps with six faces are supported")]
1549    IncompleteCubemap,
1550}
1551
1552/// The type of a raw image buffer.
1553#[derive(Debug)]
1554pub enum ImageType<'a> {
1555    /// The mime type of an image, for example `"image/png"`.
1556    MimeType(&'a str),
1557    /// The extension of an image file, for example `"png"`.
1558    Extension(&'a str),
1559    /// The direct format of the image
1560    Format(ImageFormat),
1561}
1562
1563impl<'a> ImageType<'a> {
1564    pub fn to_image_format(&self) -> Result<ImageFormat, TextureError> {
1565        match self {
1566            ImageType::MimeType(mime_type) => ImageFormat::from_mime_type(mime_type)
1567                .ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())),
1568            ImageType::Extension(extension) => ImageFormat::from_extension(extension)
1569                .ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())),
1570            ImageType::Format(format) => Ok(*format),
1571        }
1572    }
1573}
1574
1575/// Used to calculate the volume of an item.
1576pub trait Volume {
1577    fn volume(&self) -> usize;
1578}
1579
1580impl Volume for Extent3d {
1581    /// Calculates the volume of the [`Extent3d`].
1582    fn volume(&self) -> usize {
1583        (self.width * self.height * self.depth_or_array_layers) as usize
1584    }
1585}
1586
1587/// Extends the wgpu [`TextureFormat`] with information about the pixel.
1588pub trait TextureFormatPixelInfo {
1589    /// Returns the size of a pixel in bytes of the format.
1590    fn pixel_size(&self) -> usize;
1591}
1592
1593impl TextureFormatPixelInfo for TextureFormat {
1594    fn pixel_size(&self) -> usize {
1595        let info = self;
1596        match info.block_dimensions() {
1597            (1, 1) => info.block_copy_size(None).unwrap() as usize,
1598            _ => panic!("Using pixel_size for compressed textures is invalid"),
1599        }
1600    }
1601}
1602
1603bitflags::bitflags! {
1604    #[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
1605    #[repr(transparent)]
1606    pub struct CompressedImageFormats: u32 {
1607        const NONE     = 0;
1608        const ASTC_LDR = 1 << 0;
1609        const BC       = 1 << 1;
1610        const ETC2     = 1 << 2;
1611    }
1612}
1613
1614impl CompressedImageFormats {
1615    pub fn from_features(features: Features) -> Self {
1616        let mut supported_compressed_formats = Self::default();
1617        if features.contains(Features::TEXTURE_COMPRESSION_ASTC) {
1618            supported_compressed_formats |= Self::ASTC_LDR;
1619        }
1620        if features.contains(Features::TEXTURE_COMPRESSION_BC) {
1621            supported_compressed_formats |= Self::BC;
1622        }
1623        if features.contains(Features::TEXTURE_COMPRESSION_ETC2) {
1624            supported_compressed_formats |= Self::ETC2;
1625        }
1626        supported_compressed_formats
1627    }
1628
1629    pub fn supports(&self, format: TextureFormat) -> bool {
1630        match format {
1631            TextureFormat::Bc1RgbaUnorm
1632            | TextureFormat::Bc1RgbaUnormSrgb
1633            | TextureFormat::Bc2RgbaUnorm
1634            | TextureFormat::Bc2RgbaUnormSrgb
1635            | TextureFormat::Bc3RgbaUnorm
1636            | TextureFormat::Bc3RgbaUnormSrgb
1637            | TextureFormat::Bc4RUnorm
1638            | TextureFormat::Bc4RSnorm
1639            | TextureFormat::Bc5RgUnorm
1640            | TextureFormat::Bc5RgSnorm
1641            | TextureFormat::Bc6hRgbUfloat
1642            | TextureFormat::Bc6hRgbFloat
1643            | TextureFormat::Bc7RgbaUnorm
1644            | TextureFormat::Bc7RgbaUnormSrgb => self.contains(CompressedImageFormats::BC),
1645            TextureFormat::Etc2Rgb8Unorm
1646            | TextureFormat::Etc2Rgb8UnormSrgb
1647            | TextureFormat::Etc2Rgb8A1Unorm
1648            | TextureFormat::Etc2Rgb8A1UnormSrgb
1649            | TextureFormat::Etc2Rgba8Unorm
1650            | TextureFormat::Etc2Rgba8UnormSrgb
1651            | TextureFormat::EacR11Unorm
1652            | TextureFormat::EacR11Snorm
1653            | TextureFormat::EacRg11Unorm
1654            | TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2),
1655            TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR),
1656            _ => true,
1657        }
1658    }
1659}
1660
1661#[cfg(test)]
1662mod test {
1663    use super::*;
1664
1665    #[test]
1666    fn image_size() {
1667        let size = Extent3d {
1668            width: 200,
1669            height: 100,
1670            depth_or_array_layers: 1,
1671        };
1672        let image = Image::new_fill(
1673            size,
1674            TextureDimension::D2,
1675            &[0, 0, 0, 255],
1676            TextureFormat::Rgba8Unorm,
1677            RenderAssetUsages::MAIN_WORLD,
1678        );
1679        assert_eq!(
1680            Vec2::new(size.width as f32, size.height as f32),
1681            image.size_f32()
1682        );
1683    }
1684
1685    #[test]
1686    fn image_default_size() {
1687        let image = Image::default();
1688        assert_eq!(UVec2::ONE, image.size());
1689        assert_eq!(Vec2::ONE, image.size_f32());
1690    }
1691
1692    #[test]
1693    fn on_edge_pixel_is_invalid() {
1694        let image = Image::new_fill(
1695            Extent3d {
1696                width: 5,
1697                height: 10,
1698                depth_or_array_layers: 1,
1699            },
1700            TextureDimension::D2,
1701            &[0, 0, 0, 255],
1702            TextureFormat::Rgba8Unorm,
1703            RenderAssetUsages::MAIN_WORLD,
1704        );
1705        assert!(matches!(image.get_color_at(4, 9), Ok(Color::BLACK)));
1706        assert!(matches!(
1707            image.get_color_at(0, 10),
1708            Err(TextureAccessError::OutOfBounds { x: 0, y: 10, z: 0 })
1709        ));
1710        assert!(matches!(
1711            image.get_color_at(5, 10),
1712            Err(TextureAccessError::OutOfBounds { x: 5, y: 10, z: 0 })
1713        ));
1714    }
1715
1716    #[test]
1717    fn get_set_pixel_2d_with_layers() {
1718        let mut image = Image::new_fill(
1719            Extent3d {
1720                width: 5,
1721                height: 10,
1722                depth_or_array_layers: 3,
1723            },
1724            TextureDimension::D2,
1725            &[0, 0, 0, 255],
1726            TextureFormat::Rgba8Unorm,
1727            RenderAssetUsages::MAIN_WORLD,
1728        );
1729        image.set_color_at_3d(0, 0, 0, Color::WHITE).unwrap();
1730        assert!(matches!(image.get_color_at_3d(0, 0, 0), Ok(Color::WHITE)));
1731        image.set_color_at_3d(2, 3, 1, Color::WHITE).unwrap();
1732        assert!(matches!(image.get_color_at_3d(2, 3, 1), Ok(Color::WHITE)));
1733        image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
1734        assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
1735    }
1736}