1use crate::ImageLoader;
2
3#[cfg(feature = "basis-universal")]
4use super::basis::*;
5#[cfg(feature = "dds")]
6use super::dds::*;
7#[cfg(feature = "ktx2")]
8use super::ktx2::*;
9use bevy_app::{App, Plugin};
10#[cfg(not(feature = "bevy_reflect"))]
11use bevy_reflect::TypePath;
12#[cfg(feature = "bevy_reflect")]
13use bevy_reflect::{std_traits::ReflectDefault, Reflect};
14
15use bevy_asset::{uuid_handle, Asset, AssetApp, Assets, Handle, RenderAssetUsages};
16use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza};
17use bevy_ecs::resource::Resource;
18use bevy_math::{AspectRatio, UVec2, UVec3, Vec2};
19use core::hash::Hash;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use wgpu_types::{
23 AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor,
24 SamplerDescriptor, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat,
25 TextureUsages, TextureViewDescriptor,
26};
27
28pub trait BevyDefault {
31 fn bevy_default() -> Self;
33}
34
35impl BevyDefault for TextureFormat {
36 fn bevy_default() -> Self {
37 TextureFormat::Rgba8UnormSrgb
38 }
39}
40
41pub const TRANSPARENT_IMAGE_HANDLE: Handle<Image> =
47 uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46");
48
49pub struct ImagePlugin {
51 pub default_sampler: ImageSamplerDescriptor,
53}
54
55impl Default for ImagePlugin {
56 fn default() -> Self {
57 ImagePlugin::default_linear()
58 }
59}
60
61impl ImagePlugin {
62 pub fn default_linear() -> ImagePlugin {
64 ImagePlugin {
65 default_sampler: ImageSamplerDescriptor::linear(),
66 }
67 }
68
69 pub fn default_nearest() -> ImagePlugin {
71 ImagePlugin {
72 default_sampler: ImageSamplerDescriptor::nearest(),
73 }
74 }
75}
76
77impl Plugin for ImagePlugin {
78 fn build(&self, app: &mut App) {
79 #[cfg(feature = "exr")]
80 app.init_asset_loader::<crate::ExrTextureLoader>();
81
82 #[cfg(feature = "hdr")]
83 app.init_asset_loader::<crate::HdrTextureLoader>();
84
85 app.init_asset::<Image>();
86 #[cfg(feature = "bevy_reflect")]
87 app.register_asset_reflect::<Image>();
88
89 let mut image_assets = app.world_mut().resource_mut::<Assets<Image>>();
90
91 image_assets
92 .insert(&Handle::default(), Image::default())
93 .unwrap();
94 image_assets
95 .insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent())
96 .unwrap();
97
98 #[cfg(feature = "compressed_image_saver")]
99 if let Some(processor) = app
100 .world()
101 .get_resource::<bevy_asset::processor::AssetProcessor>()
102 {
103 processor.register_processor::<bevy_asset::processor::LoadTransformAndSave<
104 ImageLoader,
105 bevy_asset::transformer::IdentityAssetTransformer<Image>,
106 crate::CompressedImageSaver,
107 >>(crate::CompressedImageSaver.into());
108 processor.set_default_processor::<bevy_asset::processor::LoadTransformAndSave<
109 ImageLoader,
110 bevy_asset::transformer::IdentityAssetTransformer<Image>,
111 crate::CompressedImageSaver,
112 >>("png");
113 }
114
115 app.preregister_asset_loader::<ImageLoader>(ImageLoader::SUPPORTED_FILE_EXTENSIONS);
116 }
117}
118
119pub const TEXTURE_ASSET_INDEX: u64 = 0;
120pub const SAMPLER_ASSET_INDEX: u64 = 1;
121
122#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
123pub enum ImageFormat {
124 #[cfg(feature = "basis-universal")]
125 Basis,
126 #[cfg(feature = "bmp")]
127 Bmp,
128 #[cfg(feature = "dds")]
129 Dds,
130 #[cfg(feature = "ff")]
131 Farbfeld,
132 #[cfg(feature = "gif")]
133 Gif,
134 #[cfg(feature = "exr")]
135 OpenExr,
136 #[cfg(feature = "hdr")]
137 Hdr,
138 #[cfg(feature = "ico")]
139 Ico,
140 #[cfg(feature = "jpeg")]
141 Jpeg,
142 #[cfg(feature = "ktx2")]
143 Ktx2,
144 #[cfg(feature = "png")]
145 Png,
146 #[cfg(feature = "pnm")]
147 Pnm,
148 #[cfg(feature = "qoi")]
149 Qoi,
150 #[cfg(feature = "tga")]
151 Tga,
152 #[cfg(feature = "tiff")]
153 Tiff,
154 #[cfg(feature = "webp")]
155 WebP,
156}
157
158macro_rules! feature_gate {
159 ($feature: tt, $value: ident) => {{
160 #[cfg(not(feature = $feature))]
161 {
162 tracing::warn!("feature \"{}\" is not enabled", $feature);
163 return None;
164 }
165 #[cfg(feature = $feature)]
166 ImageFormat::$value
167 }};
168}
169
170impl ImageFormat {
171 pub const fn to_file_extensions(&self) -> &'static [&'static str] {
173 match self {
174 #[cfg(feature = "basis-universal")]
175 ImageFormat::Basis => &["basis"],
176 #[cfg(feature = "bmp")]
177 ImageFormat::Bmp => &["bmp"],
178 #[cfg(feature = "dds")]
179 ImageFormat::Dds => &["dds"],
180 #[cfg(feature = "ff")]
181 ImageFormat::Farbfeld => &["ff", "farbfeld"],
182 #[cfg(feature = "gif")]
183 ImageFormat::Gif => &["gif"],
184 #[cfg(feature = "exr")]
185 ImageFormat::OpenExr => &["exr"],
186 #[cfg(feature = "hdr")]
187 ImageFormat::Hdr => &["hdr"],
188 #[cfg(feature = "ico")]
189 ImageFormat::Ico => &["ico"],
190 #[cfg(feature = "jpeg")]
191 ImageFormat::Jpeg => &["jpg", "jpeg"],
192 #[cfg(feature = "ktx2")]
193 ImageFormat::Ktx2 => &["ktx2"],
194 #[cfg(feature = "pnm")]
195 ImageFormat::Pnm => &["pam", "pbm", "pgm", "ppm"],
196 #[cfg(feature = "png")]
197 ImageFormat::Png => &["png"],
198 #[cfg(feature = "qoi")]
199 ImageFormat::Qoi => &["qoi"],
200 #[cfg(feature = "tga")]
201 ImageFormat::Tga => &["tga"],
202 #[cfg(feature = "tiff")]
203 ImageFormat::Tiff => &["tif", "tiff"],
204 #[cfg(feature = "webp")]
205 ImageFormat::WebP => &["webp"],
206 #[expect(
208 clippy::allow_attributes,
209 reason = "`unreachable_patterns` may not always lint"
210 )]
211 #[allow(
212 unreachable_patterns,
213 reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
214 )]
215 _ => &[],
216 }
217 }
218
219 pub const fn to_mime_types(&self) -> &'static [&'static str] {
223 match self {
224 #[cfg(feature = "basis-universal")]
225 ImageFormat::Basis => &["image/basis", "image/x-basis"],
226 #[cfg(feature = "bmp")]
227 ImageFormat::Bmp => &["image/bmp", "image/x-bmp"],
228 #[cfg(feature = "dds")]
229 ImageFormat::Dds => &["image/vnd-ms.dds"],
230 #[cfg(feature = "hdr")]
231 ImageFormat::Hdr => &["image/vnd.radiance"],
232 #[cfg(feature = "gif")]
233 ImageFormat::Gif => &["image/gif"],
234 #[cfg(feature = "ff")]
235 ImageFormat::Farbfeld => &[],
236 #[cfg(feature = "ico")]
237 ImageFormat::Ico => &["image/x-icon"],
238 #[cfg(feature = "jpeg")]
239 ImageFormat::Jpeg => &["image/jpeg"],
240 #[cfg(feature = "ktx2")]
241 ImageFormat::Ktx2 => &["image/ktx2"],
242 #[cfg(feature = "png")]
243 ImageFormat::Png => &["image/png"],
244 #[cfg(feature = "qoi")]
245 ImageFormat::Qoi => &["image/qoi", "image/x-qoi"],
246 #[cfg(feature = "exr")]
247 ImageFormat::OpenExr => &["image/x-exr"],
248 #[cfg(feature = "pnm")]
249 ImageFormat::Pnm => &[
250 "image/x-portable-bitmap",
251 "image/x-portable-graymap",
252 "image/x-portable-pixmap",
253 "image/x-portable-anymap",
254 ],
255 #[cfg(feature = "tga")]
256 ImageFormat::Tga => &["image/x-targa", "image/x-tga"],
257 #[cfg(feature = "tiff")]
258 ImageFormat::Tiff => &["image/tiff"],
259 #[cfg(feature = "webp")]
260 ImageFormat::WebP => &["image/webp"],
261 #[expect(
263 clippy::allow_attributes,
264 reason = "`unreachable_patterns` may not always lint"
265 )]
266 #[allow(
267 unreachable_patterns,
268 reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
269 )]
270 _ => &[],
271 }
272 }
273
274 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
275 #[expect(
276 clippy::allow_attributes,
277 reason = "`unreachable_code` may not always lint"
278 )]
279 #[allow(
280 unreachable_code,
281 reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
282 )]
283 Some(match mime_type.to_ascii_lowercase().as_str() {
284 "image/basis" | "image/x-basis" => feature_gate!("basis-universal", Basis),
286 "image/bmp" | "image/x-bmp" => feature_gate!("bmp", Bmp),
287 "image/vnd-ms.dds" => feature_gate!("dds", Dds),
288 "image/vnd.radiance" => feature_gate!("hdr", Hdr),
289 "image/gif" => feature_gate!("gif", Gif),
290 "image/x-icon" => feature_gate!("ico", Ico),
291 "image/jpeg" => feature_gate!("jpeg", Jpeg),
292 "image/ktx2" => feature_gate!("ktx2", Ktx2),
293 "image/png" => feature_gate!("png", Png),
294 "image/qoi" | "image/x-qoi" => feature_gate!("qoi", Qoi),
295 "image/x-exr" => feature_gate!("exr", OpenExr),
296 "image/x-portable-bitmap"
297 | "image/x-portable-graymap"
298 | "image/x-portable-pixmap"
299 | "image/x-portable-anymap" => feature_gate!("pnm", Pnm),
300 "image/x-targa" | "image/x-tga" => feature_gate!("tga", Tga),
301 "image/tiff" => feature_gate!("tiff", Tiff),
302 "image/webp" => feature_gate!("webp", WebP),
303 _ => return None,
304 })
305 }
306
307 pub fn from_extension(extension: &str) -> Option<Self> {
308 #[expect(
309 clippy::allow_attributes,
310 reason = "`unreachable_code` may not always lint"
311 )]
312 #[allow(
313 unreachable_code,
314 reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
315 )]
316 Some(match extension.to_ascii_lowercase().as_str() {
317 "basis" => feature_gate!("basis-universal", Basis),
318 "bmp" => feature_gate!("bmp", Bmp),
319 "dds" => feature_gate!("dds", Dds),
320 "ff" | "farbfeld" => feature_gate!("ff", Farbfeld),
321 "gif" => feature_gate!("gif", Gif),
322 "exr" => feature_gate!("exr", OpenExr),
323 "hdr" => feature_gate!("hdr", Hdr),
324 "ico" => feature_gate!("ico", Ico),
325 "jpg" | "jpeg" => feature_gate!("jpeg", Jpeg),
326 "ktx2" => feature_gate!("ktx2", Ktx2),
327 "pam" | "pbm" | "pgm" | "ppm" => feature_gate!("pnm", Pnm),
328 "png" => feature_gate!("png", Png),
329 "qoi" => feature_gate!("qoi", Qoi),
330 "tga" => feature_gate!("tga", Tga),
331 "tif" | "tiff" => feature_gate!("tiff", Tiff),
332 "webp" => feature_gate!("webp", WebP),
333 _ => return None,
334 })
335 }
336
337 pub fn as_image_crate_format(&self) -> Option<image::ImageFormat> {
338 #[expect(
339 clippy::allow_attributes,
340 reason = "`unreachable_code` may not always lint"
341 )]
342 #[allow(
343 unreachable_code,
344 reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
345 )]
346 Some(match self {
347 #[cfg(feature = "bmp")]
348 ImageFormat::Bmp => image::ImageFormat::Bmp,
349 #[cfg(feature = "dds")]
350 ImageFormat::Dds => image::ImageFormat::Dds,
351 #[cfg(feature = "ff")]
352 ImageFormat::Farbfeld => image::ImageFormat::Farbfeld,
353 #[cfg(feature = "gif")]
354 ImageFormat::Gif => image::ImageFormat::Gif,
355 #[cfg(feature = "exr")]
356 ImageFormat::OpenExr => image::ImageFormat::OpenExr,
357 #[cfg(feature = "hdr")]
358 ImageFormat::Hdr => image::ImageFormat::Hdr,
359 #[cfg(feature = "ico")]
360 ImageFormat::Ico => image::ImageFormat::Ico,
361 #[cfg(feature = "jpeg")]
362 ImageFormat::Jpeg => image::ImageFormat::Jpeg,
363 #[cfg(feature = "png")]
364 ImageFormat::Png => image::ImageFormat::Png,
365 #[cfg(feature = "pnm")]
366 ImageFormat::Pnm => image::ImageFormat::Pnm,
367 #[cfg(feature = "qoi")]
368 ImageFormat::Qoi => image::ImageFormat::Qoi,
369 #[cfg(feature = "tga")]
370 ImageFormat::Tga => image::ImageFormat::Tga,
371 #[cfg(feature = "tiff")]
372 ImageFormat::Tiff => image::ImageFormat::Tiff,
373 #[cfg(feature = "webp")]
374 ImageFormat::WebP => image::ImageFormat::WebP,
375 #[cfg(feature = "basis-universal")]
376 ImageFormat::Basis => return None,
377 #[cfg(feature = "ktx2")]
378 ImageFormat::Ktx2 => return None,
379 #[expect(
381 clippy::allow_attributes,
382 reason = "`unreachable_patterns` may not always lint"
383 )]
384 #[allow(
385 unreachable_patterns,
386 reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable"
387 )]
388 _ => return None,
389 })
390 }
391
392 pub fn from_image_crate_format(format: image::ImageFormat) -> Option<ImageFormat> {
393 #[expect(
394 clippy::allow_attributes,
395 reason = "`unreachable_code` may not always lint"
396 )]
397 #[allow(
398 unreachable_code,
399 reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed."
400 )]
401 Some(match format {
402 image::ImageFormat::Bmp => feature_gate!("bmp", Bmp),
403 image::ImageFormat::Dds => feature_gate!("dds", Dds),
404 image::ImageFormat::Farbfeld => feature_gate!("ff", Farbfeld),
405 image::ImageFormat::Gif => feature_gate!("gif", Gif),
406 image::ImageFormat::OpenExr => feature_gate!("exr", OpenExr),
407 image::ImageFormat::Hdr => feature_gate!("hdr", Hdr),
408 image::ImageFormat::Ico => feature_gate!("ico", Ico),
409 image::ImageFormat::Jpeg => feature_gate!("jpeg", Jpeg),
410 image::ImageFormat::Png => feature_gate!("png", Png),
411 image::ImageFormat::Pnm => feature_gate!("pnm", Pnm),
412 image::ImageFormat::Qoi => feature_gate!("qoi", Qoi),
413 image::ImageFormat::Tga => feature_gate!("tga", Tga),
414 image::ImageFormat::Tiff => feature_gate!("tiff", Tiff),
415 image::ImageFormat::WebP => feature_gate!("webp", WebP),
416 _ => return None,
417 })
418 }
419}
420
421pub trait ToExtents {
422 fn to_extents(self) -> Extent3d;
423}
424impl ToExtents for UVec2 {
425 fn to_extents(self) -> Extent3d {
426 Extent3d {
427 width: self.x,
428 height: self.y,
429 depth_or_array_layers: 1,
430 }
431 }
432}
433impl ToExtents for UVec3 {
434 fn to_extents(self) -> Extent3d {
435 Extent3d {
436 width: self.x,
437 height: self.y,
438 depth_or_array_layers: self.z,
439 }
440 }
441}
442
443#[derive(Asset, Debug, Clone, PartialEq)]
450#[cfg_attr(
451 feature = "bevy_reflect",
452 derive(Reflect),
453 reflect(opaque, Default, Debug, Clone)
454)]
455#[cfg_attr(not(feature = "bevy_reflect"), derive(TypePath))]
456pub struct Image {
457 pub data: Option<Vec<u8>>,
462 pub data_order: TextureDataOrder,
466 pub texture_descriptor: TextureDescriptor<Option<&'static str>, &'static [TextureFormat]>,
475 pub sampler: ImageSampler,
477 pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
485 pub asset_usage: RenderAssetUsages,
486 pub copy_on_resize: bool,
488}
489
490#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
494pub enum ImageSampler {
495 #[default]
497 Default,
498 Descriptor(ImageSamplerDescriptor),
500}
501
502impl ImageSampler {
503 #[inline]
505 pub fn linear() -> ImageSampler {
506 ImageSampler::Descriptor(ImageSamplerDescriptor::linear())
507 }
508
509 #[inline]
511 pub fn nearest() -> ImageSampler {
512 ImageSampler::Descriptor(ImageSamplerDescriptor::nearest())
513 }
514
515 pub fn get_or_init_descriptor(&mut self) -> &mut ImageSamplerDescriptor {
520 match self {
521 ImageSampler::Default => {
522 *self = ImageSampler::Descriptor(ImageSamplerDescriptor::default());
523 match self {
524 ImageSampler::Descriptor(descriptor) => descriptor,
525 _ => unreachable!(),
526 }
527 }
528 ImageSampler::Descriptor(descriptor) => descriptor,
529 }
530 }
531}
532
533#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
539pub enum ImageAddressMode {
540 #[default]
545 ClampToEdge,
546 Repeat,
551 MirrorRepeat,
556 ClampToBorder,
562}
563
564#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
568pub enum ImageFilterMode {
569 #[default]
573 Nearest,
574 Linear,
578}
579
580#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
584pub enum ImageCompareFunction {
585 Never,
587 Less,
589 Equal,
593 LessEqual,
595 Greater,
597 NotEqual,
601 GreaterEqual,
603 Always,
605}
606
607#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
611pub enum ImageSamplerBorderColor {
612 TransparentBlack,
614 OpaqueBlack,
616 OpaqueWhite,
618 Zero,
624}
625
626#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
634pub struct ImageSamplerDescriptor {
635 pub label: Option<String>,
636 pub address_mode_u: ImageAddressMode,
638 pub address_mode_v: ImageAddressMode,
640 pub address_mode_w: ImageAddressMode,
642 pub mag_filter: ImageFilterMode,
644 pub min_filter: ImageFilterMode,
646 pub mipmap_filter: ImageFilterMode,
648 pub lod_min_clamp: f32,
650 pub lod_max_clamp: f32,
652 pub compare: Option<ImageCompareFunction>,
654 pub anisotropy_clamp: u16,
656 pub border_color: Option<ImageSamplerBorderColor>,
658}
659
660impl Default for ImageSamplerDescriptor {
661 fn default() -> Self {
662 Self {
663 address_mode_u: Default::default(),
664 address_mode_v: Default::default(),
665 address_mode_w: Default::default(),
666 mag_filter: Default::default(),
667 min_filter: Default::default(),
668 mipmap_filter: Default::default(),
669 lod_min_clamp: 0.0,
670 lod_max_clamp: 32.0,
671 compare: None,
672 anisotropy_clamp: 1,
673 border_color: None,
674 label: None,
675 }
676 }
677}
678
679impl ImageSamplerDescriptor {
680 #[inline]
682 pub fn linear() -> ImageSamplerDescriptor {
683 ImageSamplerDescriptor {
684 mag_filter: ImageFilterMode::Linear,
685 min_filter: ImageFilterMode::Linear,
686 mipmap_filter: ImageFilterMode::Linear,
687 ..Default::default()
688 }
689 }
690
691 #[inline]
693 pub fn nearest() -> ImageSamplerDescriptor {
694 ImageSamplerDescriptor {
695 mag_filter: ImageFilterMode::Nearest,
696 min_filter: ImageFilterMode::Nearest,
697 mipmap_filter: ImageFilterMode::Nearest,
698 ..Default::default()
699 }
700 }
701
702 pub fn as_wgpu(&self) -> SamplerDescriptor<Option<&str>> {
703 SamplerDescriptor {
704 label: self.label.as_deref(),
705 address_mode_u: self.address_mode_u.into(),
706 address_mode_v: self.address_mode_v.into(),
707 address_mode_w: self.address_mode_w.into(),
708 mag_filter: self.mag_filter.into(),
709 min_filter: self.min_filter.into(),
710 mipmap_filter: self.mipmap_filter.into(),
711 lod_min_clamp: self.lod_min_clamp,
712 lod_max_clamp: self.lod_max_clamp,
713 compare: self.compare.map(Into::into),
714 anisotropy_clamp: self.anisotropy_clamp,
715 border_color: self.border_color.map(Into::into),
716 }
717 }
718}
719
720impl From<ImageAddressMode> for AddressMode {
721 fn from(value: ImageAddressMode) -> Self {
722 match value {
723 ImageAddressMode::ClampToEdge => AddressMode::ClampToEdge,
724 ImageAddressMode::Repeat => AddressMode::Repeat,
725 ImageAddressMode::MirrorRepeat => AddressMode::MirrorRepeat,
726 ImageAddressMode::ClampToBorder => AddressMode::ClampToBorder,
727 }
728 }
729}
730
731impl From<ImageFilterMode> for FilterMode {
732 fn from(value: ImageFilterMode) -> Self {
733 match value {
734 ImageFilterMode::Nearest => FilterMode::Nearest,
735 ImageFilterMode::Linear => FilterMode::Linear,
736 }
737 }
738}
739
740impl From<ImageCompareFunction> for CompareFunction {
741 fn from(value: ImageCompareFunction) -> Self {
742 match value {
743 ImageCompareFunction::Never => CompareFunction::Never,
744 ImageCompareFunction::Less => CompareFunction::Less,
745 ImageCompareFunction::Equal => CompareFunction::Equal,
746 ImageCompareFunction::LessEqual => CompareFunction::LessEqual,
747 ImageCompareFunction::Greater => CompareFunction::Greater,
748 ImageCompareFunction::NotEqual => CompareFunction::NotEqual,
749 ImageCompareFunction::GreaterEqual => CompareFunction::GreaterEqual,
750 ImageCompareFunction::Always => CompareFunction::Always,
751 }
752 }
753}
754
755impl From<ImageSamplerBorderColor> for SamplerBorderColor {
756 fn from(value: ImageSamplerBorderColor) -> Self {
757 match value {
758 ImageSamplerBorderColor::TransparentBlack => SamplerBorderColor::TransparentBlack,
759 ImageSamplerBorderColor::OpaqueBlack => SamplerBorderColor::OpaqueBlack,
760 ImageSamplerBorderColor::OpaqueWhite => SamplerBorderColor::OpaqueWhite,
761 ImageSamplerBorderColor::Zero => SamplerBorderColor::Zero,
762 }
763 }
764}
765
766impl From<AddressMode> for ImageAddressMode {
767 fn from(value: AddressMode) -> Self {
768 match value {
769 AddressMode::ClampToEdge => ImageAddressMode::ClampToEdge,
770 AddressMode::Repeat => ImageAddressMode::Repeat,
771 AddressMode::MirrorRepeat => ImageAddressMode::MirrorRepeat,
772 AddressMode::ClampToBorder => ImageAddressMode::ClampToBorder,
773 }
774 }
775}
776
777impl From<FilterMode> for ImageFilterMode {
778 fn from(value: FilterMode) -> Self {
779 match value {
780 FilterMode::Nearest => ImageFilterMode::Nearest,
781 FilterMode::Linear => ImageFilterMode::Linear,
782 }
783 }
784}
785
786impl From<CompareFunction> for ImageCompareFunction {
787 fn from(value: CompareFunction) -> Self {
788 match value {
789 CompareFunction::Never => ImageCompareFunction::Never,
790 CompareFunction::Less => ImageCompareFunction::Less,
791 CompareFunction::Equal => ImageCompareFunction::Equal,
792 CompareFunction::LessEqual => ImageCompareFunction::LessEqual,
793 CompareFunction::Greater => ImageCompareFunction::Greater,
794 CompareFunction::NotEqual => ImageCompareFunction::NotEqual,
795 CompareFunction::GreaterEqual => ImageCompareFunction::GreaterEqual,
796 CompareFunction::Always => ImageCompareFunction::Always,
797 }
798 }
799}
800
801impl From<SamplerBorderColor> for ImageSamplerBorderColor {
802 fn from(value: SamplerBorderColor) -> Self {
803 match value {
804 SamplerBorderColor::TransparentBlack => ImageSamplerBorderColor::TransparentBlack,
805 SamplerBorderColor::OpaqueBlack => ImageSamplerBorderColor::OpaqueBlack,
806 SamplerBorderColor::OpaqueWhite => ImageSamplerBorderColor::OpaqueWhite,
807 SamplerBorderColor::Zero => ImageSamplerBorderColor::Zero,
808 }
809 }
810}
811
812impl From<SamplerDescriptor<Option<&str>>> for ImageSamplerDescriptor {
813 fn from(value: SamplerDescriptor<Option<&str>>) -> Self {
814 ImageSamplerDescriptor {
815 label: value.label.map(ToString::to_string),
816 address_mode_u: value.address_mode_u.into(),
817 address_mode_v: value.address_mode_v.into(),
818 address_mode_w: value.address_mode_w.into(),
819 mag_filter: value.mag_filter.into(),
820 min_filter: value.min_filter.into(),
821 mipmap_filter: value.mipmap_filter.into(),
822 lod_min_clamp: value.lod_min_clamp,
823 lod_max_clamp: value.lod_max_clamp,
824 compare: value.compare.map(Into::into),
825 anisotropy_clamp: value.anisotropy_clamp,
826 border_color: value.border_color.map(Into::into),
827 }
828 }
829}
830
831impl Default for Image {
832 fn default() -> Self {
834 let mut image = Image::default_uninit();
835 image.data = Some(vec![
836 255;
837 image
838 .texture_descriptor
839 .format
840 .pixel_size()
841 .unwrap_or(0)
842 ]);
843 image
844 }
845}
846
847impl Image {
848 pub fn new(
854 size: Extent3d,
855 dimension: TextureDimension,
856 data: Vec<u8>,
857 format: TextureFormat,
858 asset_usage: RenderAssetUsages,
859 ) -> Self {
860 if let Ok(pixel_size) = format.pixel_size() {
861 debug_assert_eq!(
862 size.volume() * pixel_size,
863 data.len(),
864 "Pixel data, size and format have to match",
865 );
866 }
867 let mut image = Image::new_uninit(size, dimension, format, asset_usage);
868 image.data = Some(data);
869 image
870 }
871
872 pub fn new_uninit(
874 size: Extent3d,
875 dimension: TextureDimension,
876 format: TextureFormat,
877 asset_usage: RenderAssetUsages,
878 ) -> Self {
879 Image {
880 data: None,
881 data_order: TextureDataOrder::default(),
882 texture_descriptor: TextureDescriptor {
883 size,
884 format,
885 dimension,
886 label: None,
887 mip_level_count: 1,
888 sample_count: 1,
889 usage: TextureUsages::TEXTURE_BINDING
890 | TextureUsages::COPY_DST
891 | TextureUsages::COPY_SRC,
892 view_formats: &[],
893 },
894 sampler: ImageSampler::Default,
895 texture_view_descriptor: None,
896 asset_usage,
897 copy_on_resize: false,
898 }
899 }
900
901 pub fn transparent() -> Image {
905 let format = TextureFormat::bevy_default();
909 if let Ok(pixel_size) = format.pixel_size() {
910 debug_assert!(pixel_size == 4);
911 }
912 let data = vec![255, 255, 255, 0];
913 Image::new(
914 Extent3d::default(),
915 TextureDimension::D2,
916 data,
917 format,
918 RenderAssetUsages::default(),
919 )
920 }
921 pub fn default_uninit() -> Image {
923 Image::new_uninit(
924 Extent3d::default(),
925 TextureDimension::D2,
926 TextureFormat::bevy_default(),
927 RenderAssetUsages::default(),
928 )
929 }
930
931 pub fn new_fill(
937 size: Extent3d,
938 dimension: TextureDimension,
939 pixel: &[u8],
940 format: TextureFormat,
941 asset_usage: RenderAssetUsages,
942 ) -> Self {
943 let mut image = Image::new_uninit(size, dimension, format, asset_usage);
944 if let Ok(pixel_size) = image.texture_descriptor.format.pixel_size()
945 && pixel_size > 0
946 {
947 let byte_len = pixel_size * size.volume();
948 debug_assert_eq!(
949 pixel.len() % pixel_size,
950 0,
951 "Must not have incomplete pixel data (pixel size is {}B).",
952 pixel_size,
953 );
954 debug_assert!(
955 pixel.len() <= byte_len,
956 "Fill data must fit within pixel buffer (expected {byte_len}B).",
957 );
958 let data = pixel.iter().copied().cycle().take(byte_len).collect();
959 image.data = Some(data);
960 }
961 image
962 }
963
964 pub fn new_target_texture(width: u32, height: u32, format: TextureFormat) -> Self {
984 let size = Extent3d {
985 width,
986 height,
987 ..Default::default()
988 };
989 let usage = TextureUsages::TEXTURE_BINDING
991 | TextureUsages::COPY_DST
992 | TextureUsages::RENDER_ATTACHMENT;
993 let data = vec![
995 0;
996 format.pixel_size().expect(
997 "Failed to create Image: can't get pixel size for this TextureFormat"
998 ) * size.volume()
999 ];
1000
1001 Image {
1002 data: Some(data),
1003 data_order: TextureDataOrder::default(),
1004 texture_descriptor: TextureDescriptor {
1005 size,
1006 format,
1007 dimension: TextureDimension::D2,
1008 label: None,
1009 mip_level_count: 1,
1010 sample_count: 1,
1011 usage,
1012 view_formats: &[],
1013 },
1014 sampler: ImageSampler::Default,
1015 texture_view_descriptor: None,
1016 asset_usage: RenderAssetUsages::default(),
1017 copy_on_resize: true,
1018 }
1019 }
1020
1021 #[inline]
1023 pub fn width(&self) -> u32 {
1024 self.texture_descriptor.size.width
1025 }
1026
1027 #[inline]
1029 pub fn height(&self) -> u32 {
1030 self.texture_descriptor.size.height
1031 }
1032
1033 #[inline]
1035 pub fn aspect_ratio(&self) -> AspectRatio {
1036 AspectRatio::try_from_pixels(self.width(), self.height()).expect(
1037 "Failed to calculate aspect ratio: Image dimensions must be positive, non-zero values",
1038 )
1039 }
1040
1041 #[inline]
1043 pub fn size_f32(&self) -> Vec2 {
1044 Vec2::new(self.width() as f32, self.height() as f32)
1045 }
1046
1047 #[inline]
1049 pub fn size(&self) -> UVec2 {
1050 UVec2::new(self.width(), self.height())
1051 }
1052
1053 pub fn resize(&mut self, size: Extent3d) {
1058 self.texture_descriptor.size = size;
1059 if let Some(ref mut data) = self.data
1060 && let Ok(pixel_size) = self.texture_descriptor.format.pixel_size()
1061 {
1062 data.resize(size.volume() * pixel_size, 0);
1063 }
1064 }
1065
1066 pub fn reinterpret_size(&mut self, new_size: Extent3d) {
1072 assert_eq!(
1073 new_size.volume(),
1074 self.texture_descriptor.size.volume(),
1075 "Incompatible sizes: old = {:?} new = {:?}",
1076 self.texture_descriptor.size,
1077 new_size
1078 );
1079
1080 self.texture_descriptor.size = new_size;
1081 }
1082
1083 pub fn resize_in_place(&mut self, new_size: Extent3d) {
1088 if let Ok(pixel_size) = self.texture_descriptor.format.pixel_size() {
1089 let old_size = self.texture_descriptor.size;
1090 let byte_len = pixel_size * new_size.volume();
1091 self.texture_descriptor.size = new_size;
1092
1093 let Some(ref mut data) = self.data else {
1094 self.copy_on_resize = true;
1095 return;
1096 };
1097
1098 let mut new: Vec<u8> = vec![0; byte_len];
1099
1100 let copy_width = old_size.width.min(new_size.width) as usize;
1101 let copy_height = old_size.height.min(new_size.height) as usize;
1102 let copy_depth = old_size
1103 .depth_or_array_layers
1104 .min(new_size.depth_or_array_layers) as usize;
1105
1106 let old_row_stride = old_size.width as usize * pixel_size;
1107 let old_layer_stride = old_size.height as usize * old_row_stride;
1108
1109 let new_row_stride = new_size.width as usize * pixel_size;
1110 let new_layer_stride = new_size.height as usize * new_row_stride;
1111
1112 for z in 0..copy_depth {
1113 for y in 0..copy_height {
1114 let old_offset = z * old_layer_stride + y * old_row_stride;
1115 let new_offset = z * new_layer_stride + y * new_row_stride;
1116
1117 let old_range = (old_offset)..(old_offset + copy_width * pixel_size);
1118 let new_range = (new_offset)..(new_offset + copy_width * pixel_size);
1119
1120 new[new_range].copy_from_slice(&data[old_range]);
1121 }
1122 }
1123
1124 self.data = Some(new);
1125 }
1126 }
1127
1128 pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) {
1136 assert_eq!(self.texture_descriptor.dimension, TextureDimension::D2);
1138 assert_eq!(self.texture_descriptor.size.depth_or_array_layers, 1);
1139 assert_eq!(self.height() % layers, 0);
1140
1141 self.reinterpret_size(Extent3d {
1142 width: self.width(),
1143 height: self.height() / layers,
1144 depth_or_array_layers: layers,
1145 });
1146 }
1147
1148 pub fn convert(&self, new_format: TextureFormat) -> Option<Self> {
1157 self.clone()
1158 .try_into_dynamic()
1159 .ok()
1160 .and_then(|img| match new_format {
1161 TextureFormat::R8Unorm => {
1162 Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false))
1163 }
1164 TextureFormat::Rg8Unorm => Some((
1165 image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()),
1166 false,
1167 )),
1168 TextureFormat::Rgba8UnormSrgb => {
1169 Some((image::DynamicImage::ImageRgba8(img.into_rgba8()), true))
1170 }
1171 _ => None,
1172 })
1173 .map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb, self.asset_usage))
1174 }
1175
1176 pub fn from_buffer(
1179 buffer: &[u8],
1180 image_type: ImageType,
1181 #[cfg_attr(
1182 not(any(feature = "basis-universal", feature = "dds", feature = "ktx2")),
1183 expect(unused_variables, reason = "only used with certain features")
1184 )]
1185 supported_compressed_formats: CompressedImageFormats,
1186 is_srgb: bool,
1187 image_sampler: ImageSampler,
1188 asset_usage: RenderAssetUsages,
1189 ) -> Result<Image, TextureError> {
1190 let format = image_type.to_image_format()?;
1191
1192 let mut image = match format {
1199 #[cfg(feature = "basis-universal")]
1200 ImageFormat::Basis => {
1201 basis_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?
1202 }
1203 #[cfg(feature = "dds")]
1204 ImageFormat::Dds => dds_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?,
1205 #[cfg(feature = "ktx2")]
1206 ImageFormat::Ktx2 => {
1207 ktx2_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?
1208 }
1209 #[expect(
1210 clippy::allow_attributes,
1211 reason = "`unreachable_patterns` may not always lint"
1212 )]
1213 #[allow(
1214 unreachable_patterns,
1215 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"
1216 )]
1217 _ => {
1218 let image_crate_format = format
1219 .as_image_crate_format()
1220 .ok_or_else(|| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?;
1221 let mut reader = image::ImageReader::new(std::io::Cursor::new(buffer));
1222 reader.set_format(image_crate_format);
1223 reader.no_limits();
1224 let dyn_img = reader.decode()?;
1225 Self::from_dynamic(dyn_img, is_srgb, asset_usage)
1226 }
1227 };
1228 image.sampler = image_sampler;
1229 Ok(image)
1230 }
1231
1232 pub fn is_compressed(&self) -> bool {
1234 let format_description = self.texture_descriptor.format;
1235 format_description
1236 .required_features()
1237 .contains(Features::TEXTURE_COMPRESSION_ASTC)
1238 || format_description
1239 .required_features()
1240 .contains(Features::TEXTURE_COMPRESSION_BC)
1241 || format_description
1242 .required_features()
1243 .contains(Features::TEXTURE_COMPRESSION_ETC2)
1244 }
1245
1246 #[inline(always)]
1252 pub fn pixel_data_offset(&self, coords: UVec3) -> Option<usize> {
1253 let width = self.texture_descriptor.size.width;
1254 let height = self.texture_descriptor.size.height;
1255 let depth = self.texture_descriptor.size.depth_or_array_layers;
1256
1257 let pixel_size = self.texture_descriptor.format.pixel_size().ok()?;
1258 let pixel_offset = match self.texture_descriptor.dimension {
1259 TextureDimension::D3 | TextureDimension::D2 => {
1260 if coords.x >= width || coords.y >= height || coords.z >= depth {
1261 return None;
1262 }
1263 coords.z * height * width + coords.y * width + coords.x
1264 }
1265 TextureDimension::D1 => {
1266 if coords.x >= width {
1267 return None;
1268 }
1269 coords.x
1270 }
1271 };
1272
1273 Some(pixel_offset as usize * pixel_size)
1274 }
1275
1276 #[inline(always)]
1278 pub fn pixel_bytes(&self, coords: UVec3) -> Option<&[u8]> {
1279 let len = self.texture_descriptor.format.pixel_size().ok()?;
1280 let data = self.data.as_ref()?;
1281 self.pixel_data_offset(coords)
1282 .map(|start| &data[start..(start + len)])
1283 }
1284
1285 #[inline(always)]
1287 pub fn pixel_bytes_mut(&mut self, coords: UVec3) -> Option<&mut [u8]> {
1288 let len = self.texture_descriptor.format.pixel_size().ok()?;
1289 let offset = self.pixel_data_offset(coords);
1290 let data = self.data.as_mut()?;
1291 offset.map(|start| &mut data[start..(start + len)])
1292 }
1293
1294 pub fn clear(&mut self, pixel: &[u8]) {
1299 if let Ok(pixel_size) = self.texture_descriptor.format.pixel_size()
1300 && pixel_size > 0
1301 {
1302 let byte_len = pixel_size * self.texture_descriptor.size.volume();
1303 debug_assert_eq!(
1304 pixel.len() % pixel_size,
1305 0,
1306 "Must not have incomplete pixel data (pixel size is {}B).",
1307 pixel_size,
1308 );
1309 debug_assert!(
1310 pixel.len() <= byte_len,
1311 "Clear data must fit within pixel buffer (expected {byte_len}B).",
1312 );
1313 if let Some(data) = self.data.as_mut() {
1314 for pixel_data in data.chunks_mut(pixel_size) {
1315 pixel_data.copy_from_slice(pixel);
1316 }
1317 }
1318 }
1319 }
1320
1321 #[inline(always)]
1325 pub fn get_color_at_1d(&self, x: u32) -> Result<Color, TextureAccessError> {
1326 if self.texture_descriptor.dimension != TextureDimension::D1 {
1327 return Err(TextureAccessError::WrongDimension);
1328 }
1329 self.get_color_at_internal(UVec3::new(x, 0, 0))
1330 }
1331
1332 #[inline(always)]
1356 pub fn get_color_at(&self, x: u32, y: u32) -> Result<Color, TextureAccessError> {
1357 if self.texture_descriptor.dimension != TextureDimension::D2 {
1358 return Err(TextureAccessError::WrongDimension);
1359 }
1360 self.get_color_at_internal(UVec3::new(x, y, 0))
1361 }
1362
1363 #[inline(always)]
1367 pub fn get_color_at_3d(&self, x: u32, y: u32, z: u32) -> Result<Color, TextureAccessError> {
1368 match (
1369 self.texture_descriptor.dimension,
1370 self.texture_descriptor.size.depth_or_array_layers,
1371 ) {
1372 (TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1373 self.get_color_at_internal(UVec3::new(x, y, z))
1374 }
1375 _ => Err(TextureAccessError::WrongDimension),
1376 }
1377 }
1378
1379 #[inline(always)]
1383 pub fn set_color_at_1d(&mut self, x: u32, color: Color) -> Result<(), TextureAccessError> {
1384 if self.texture_descriptor.dimension != TextureDimension::D1 {
1385 return Err(TextureAccessError::WrongDimension);
1386 }
1387 self.set_color_at_internal(UVec3::new(x, 0, 0), color)
1388 }
1389
1390 #[inline(always)]
1412 pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> {
1413 if self.texture_descriptor.dimension != TextureDimension::D2 {
1414 return Err(TextureAccessError::WrongDimension);
1415 }
1416 self.set_color_at_internal(UVec3::new(x, y, 0), color)
1417 }
1418
1419 #[inline(always)]
1423 pub fn set_color_at_3d(
1424 &mut self,
1425 x: u32,
1426 y: u32,
1427 z: u32,
1428 color: Color,
1429 ) -> Result<(), TextureAccessError> {
1430 match (
1431 self.texture_descriptor.dimension,
1432 self.texture_descriptor.size.depth_or_array_layers,
1433 ) {
1434 (TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1435 self.set_color_at_internal(UVec3::new(x, y, z), color)
1436 }
1437 _ => Err(TextureAccessError::WrongDimension),
1438 }
1439 }
1440
1441 #[inline(always)]
1442 fn get_color_at_internal(&self, coords: UVec3) -> Result<Color, TextureAccessError> {
1443 let Some(bytes) = self.pixel_bytes(coords) else {
1444 return Err(TextureAccessError::OutOfBounds {
1445 x: coords.x,
1446 y: coords.y,
1447 z: coords.z,
1448 });
1449 };
1450
1451 match self.texture_descriptor.format {
1454 TextureFormat::Rgba8UnormSrgb => Ok(Color::srgba(
1455 bytes[0] as f32 / u8::MAX as f32,
1456 bytes[1] as f32 / u8::MAX as f32,
1457 bytes[2] as f32 / u8::MAX as f32,
1458 bytes[3] as f32 / u8::MAX as f32,
1459 )),
1460 TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => Ok(Color::linear_rgba(
1461 bytes[0] as f32 / u8::MAX as f32,
1462 bytes[1] as f32 / u8::MAX as f32,
1463 bytes[2] as f32 / u8::MAX as f32,
1464 bytes[3] as f32 / u8::MAX as f32,
1465 )),
1466 TextureFormat::Bgra8UnormSrgb => Ok(Color::srgba(
1467 bytes[2] as f32 / u8::MAX as f32,
1468 bytes[1] as f32 / u8::MAX as f32,
1469 bytes[0] as f32 / u8::MAX as f32,
1470 bytes[3] as f32 / u8::MAX as f32,
1471 )),
1472 TextureFormat::Bgra8Unorm => Ok(Color::linear_rgba(
1473 bytes[2] as f32 / u8::MAX as f32,
1474 bytes[1] as f32 / u8::MAX as f32,
1475 bytes[0] as f32 / u8::MAX as f32,
1476 bytes[3] as f32 / u8::MAX as f32,
1477 )),
1478 TextureFormat::Rgba32Float => Ok(Color::linear_rgba(
1479 f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1480 f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1481 f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1482 f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1483 )),
1484 TextureFormat::Rgba16Float => Ok(Color::linear_rgba(
1485 half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32(),
1486 half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32(),
1487 half::f16::from_le_bytes([bytes[4], bytes[5]]).to_f32(),
1488 half::f16::from_le_bytes([bytes[6], bytes[7]]).to_f32(),
1489 )),
1490 TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1491 let (r, g, b, a) = (
1492 u16::from_le_bytes([bytes[0], bytes[1]]),
1493 u16::from_le_bytes([bytes[2], bytes[3]]),
1494 u16::from_le_bytes([bytes[4], bytes[5]]),
1495 u16::from_le_bytes([bytes[6], bytes[7]]),
1496 );
1497 Ok(Color::linear_rgba(
1498 (r as f64 / u16::MAX as f64) as f32,
1500 (g as f64 / u16::MAX as f64) as f32,
1501 (b as f64 / u16::MAX as f64) as f32,
1502 (a as f64 / u16::MAX as f64) as f32,
1503 ))
1504 }
1505 TextureFormat::Rgba32Uint => {
1506 let (r, g, b, a) = (
1507 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1508 u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1509 u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1510 u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1511 );
1512 Ok(Color::linear_rgba(
1513 (r as f64 / u32::MAX as f64) as f32,
1515 (g as f64 / u32::MAX as f64) as f32,
1516 (b as f64 / u32::MAX as f64) as f32,
1517 (a as f64 / u32::MAX as f64) as f32,
1518 ))
1519 }
1520 TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1523 let x = bytes[0] as f32 / u8::MAX as f32;
1524 Ok(Color::linear_rgb(x, x, x))
1525 }
1526 TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1527 let x = u16::from_le_bytes([bytes[0], bytes[1]]);
1528 let x = (x as f64 / u16::MAX as f64) as f32;
1530 Ok(Color::linear_rgb(x, x, x))
1531 }
1532 TextureFormat::R32Uint => {
1533 let x = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1534 let x = (x as f64 / u32::MAX as f64) as f32;
1536 Ok(Color::linear_rgb(x, x, x))
1537 }
1538 TextureFormat::R16Float => {
1539 let x = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1540 Ok(Color::linear_rgb(x, x, x))
1541 }
1542 TextureFormat::R32Float => {
1543 let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1544 Ok(Color::linear_rgb(x, x, x))
1545 }
1546 TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1547 let r = bytes[0] as f32 / u8::MAX as f32;
1548 let g = bytes[1] as f32 / u8::MAX as f32;
1549 Ok(Color::linear_rgb(r, g, 0.0))
1550 }
1551 TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1552 let r = u16::from_le_bytes([bytes[0], bytes[1]]);
1553 let g = u16::from_le_bytes([bytes[2], bytes[3]]);
1554 let r = (r as f64 / u16::MAX as f64) as f32;
1556 let g = (g as f64 / u16::MAX as f64) as f32;
1557 Ok(Color::linear_rgb(r, g, 0.0))
1558 }
1559 TextureFormat::Rg32Uint => {
1560 let r = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1561 let g = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1562 let r = (r as f64 / u32::MAX as f64) as f32;
1564 let g = (g as f64 / u32::MAX as f64) as f32;
1565 Ok(Color::linear_rgb(r, g, 0.0))
1566 }
1567 TextureFormat::Rg16Float => {
1568 let r = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1569 let g = half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32();
1570 Ok(Color::linear_rgb(r, g, 0.0))
1571 }
1572 TextureFormat::Rg32Float => {
1573 let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1574 let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1575 Ok(Color::linear_rgb(r, g, 0.0))
1576 }
1577 _ => Err(TextureAccessError::UnsupportedTextureFormat(
1578 self.texture_descriptor.format,
1579 )),
1580 }
1581 }
1582
1583 #[inline(always)]
1584 fn set_color_at_internal(
1585 &mut self,
1586 coords: UVec3,
1587 color: Color,
1588 ) -> Result<(), TextureAccessError> {
1589 let format = self.texture_descriptor.format;
1590
1591 let Some(bytes) = self.pixel_bytes_mut(coords) else {
1592 return Err(TextureAccessError::OutOfBounds {
1593 x: coords.x,
1594 y: coords.y,
1595 z: coords.z,
1596 });
1597 };
1598
1599 match format {
1602 TextureFormat::Rgba8UnormSrgb => {
1603 let [r, g, b, a] = Srgba::from(color).to_f32_array();
1604 bytes[0] = (r * u8::MAX as f32) as u8;
1605 bytes[1] = (g * u8::MAX as f32) as u8;
1606 bytes[2] = (b * u8::MAX as f32) as u8;
1607 bytes[3] = (a * u8::MAX as f32) as u8;
1608 }
1609 TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => {
1610 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1611 bytes[0] = (r * u8::MAX as f32) as u8;
1612 bytes[1] = (g * u8::MAX as f32) as u8;
1613 bytes[2] = (b * u8::MAX as f32) as u8;
1614 bytes[3] = (a * u8::MAX as f32) as u8;
1615 }
1616 TextureFormat::Bgra8UnormSrgb => {
1617 let [r, g, b, a] = Srgba::from(color).to_f32_array();
1618 bytes[0] = (b * u8::MAX as f32) as u8;
1619 bytes[1] = (g * u8::MAX as f32) as u8;
1620 bytes[2] = (r * u8::MAX as f32) as u8;
1621 bytes[3] = (a * u8::MAX as f32) as u8;
1622 }
1623 TextureFormat::Bgra8Unorm => {
1624 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1625 bytes[0] = (b * u8::MAX as f32) as u8;
1626 bytes[1] = (g * u8::MAX as f32) as u8;
1627 bytes[2] = (r * u8::MAX as f32) as u8;
1628 bytes[3] = (a * u8::MAX as f32) as u8;
1629 }
1630 TextureFormat::Rgba16Float => {
1631 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1632 bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1633 bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1634 bytes[4..6].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(b)));
1635 bytes[6..8].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(a)));
1636 }
1637 TextureFormat::Rgba32Float => {
1638 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1639 bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1640 bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1641 bytes[8..12].copy_from_slice(&f32::to_le_bytes(b));
1642 bytes[12..16].copy_from_slice(&f32::to_le_bytes(a));
1643 }
1644 TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1645 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1646 let [r, g, b, a] = [
1647 (r * u16::MAX as f32) as u16,
1648 (g * u16::MAX as f32) as u16,
1649 (b * u16::MAX as f32) as u16,
1650 (a * u16::MAX as f32) as u16,
1651 ];
1652 bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1653 bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1654 bytes[4..6].copy_from_slice(&u16::to_le_bytes(b));
1655 bytes[6..8].copy_from_slice(&u16::to_le_bytes(a));
1656 }
1657 TextureFormat::Rgba32Uint => {
1658 let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1659 let [r, g, b, a] = [
1660 (r * u32::MAX as f32) as u32,
1661 (g * u32::MAX as f32) as u32,
1662 (b * u32::MAX as f32) as u32,
1663 (a * u32::MAX as f32) as u32,
1664 ];
1665 bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1666 bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1667 bytes[8..12].copy_from_slice(&u32::to_le_bytes(b));
1668 bytes[12..16].copy_from_slice(&u32::to_le_bytes(a));
1669 }
1670 TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1671 let linear = LinearRgba::from(color);
1673 let luminance = Xyza::from(linear).y;
1674 let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1675 bytes[0] = (r * u8::MAX as f32) as u8;
1676 }
1677 TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1678 let linear = LinearRgba::from(color);
1680 let luminance = Xyza::from(linear).y;
1681 let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1682 let r = (r * u16::MAX as f32) as u16;
1683 bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1684 }
1685 TextureFormat::R32Uint => {
1686 let linear = LinearRgba::from(color);
1688 let luminance = Xyza::from(linear).y;
1689 let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1690 let r = (r as f64 * u32::MAX as f64) as u32;
1692 bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1693 }
1694 TextureFormat::R16Float => {
1695 let linear = LinearRgba::from(color);
1697 let luminance = Xyza::from(linear).y;
1698 let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1699 let x = half::f16::from_f32(r);
1700 bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(x));
1701 }
1702 TextureFormat::R32Float => {
1703 let linear = LinearRgba::from(color);
1705 let luminance = Xyza::from(linear).y;
1706 let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1707 bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1708 }
1709 TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1710 let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1711 bytes[0] = (r * u8::MAX as f32) as u8;
1712 bytes[1] = (g * u8::MAX as f32) as u8;
1713 }
1714 TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1715 let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1716 let r = (r * u16::MAX as f32) as u16;
1717 let g = (g * u16::MAX as f32) as u16;
1718 bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1719 bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1720 }
1721 TextureFormat::Rg32Uint => {
1722 let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1723 let r = (r as f64 * u32::MAX as f64) as u32;
1725 let g = (g as f64 * u32::MAX as f64) as u32;
1726 bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1727 bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1728 }
1729 TextureFormat::Rg16Float => {
1730 let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1731 bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1732 bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1733 }
1734 TextureFormat::Rg32Float => {
1735 let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1736 bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1737 bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1738 }
1739 _ => {
1740 return Err(TextureAccessError::UnsupportedTextureFormat(
1741 self.texture_descriptor.format,
1742 ));
1743 }
1744 }
1745 Ok(())
1746 }
1747}
1748
1749#[derive(Clone, Copy, Debug)]
1750pub enum DataFormat {
1751 Rgb,
1752 Rgba,
1753 Rrr,
1754 Rrrg,
1755 Rg,
1756}
1757
1758#[derive(Clone, Copy, Debug)]
1760pub enum TranscodeFormat {
1761 Etc1s,
1762 Uastc(DataFormat),
1763 R8UnormSrgb,
1765 Rg8UnormSrgb,
1767 Rgb8,
1769}
1770
1771#[derive(Error, Debug)]
1773pub enum TextureAccessError {
1774 #[error("out of bounds (x: {x}, y: {y}, z: {z})")]
1775 OutOfBounds { x: u32, y: u32, z: u32 },
1776 #[error("unsupported texture format: {0:?}")]
1777 UnsupportedTextureFormat(TextureFormat),
1778 #[error("attempt to access texture with different dimension")]
1779 WrongDimension,
1780}
1781
1782#[derive(Error, Debug)]
1784pub enum TextureError {
1785 #[error("invalid image mime type: {0}")]
1787 InvalidImageMimeType(String),
1788 #[error("invalid image extension: {0}")]
1790 InvalidImageExtension(String),
1791 #[error("failed to load an image: {0}")]
1793 ImageError(#[from] image::ImageError),
1794 #[error("unsupported texture format: {0}")]
1796 UnsupportedTextureFormat(String),
1797 #[error("supercompression not supported: {0}")]
1799 SuperCompressionNotSupported(String),
1800 #[error("failed to decompress an image: {0}")]
1802 SuperDecompressionError(String),
1803 #[error("invalid data: {0}")]
1805 InvalidData(String),
1806 #[error("transcode error: {0}")]
1808 TranscodeError(String),
1809 #[error("format requires transcoding: {0:?}")]
1811 FormatRequiresTranscodingError(TranscodeFormat),
1812 #[error("only cubemaps with six faces are supported")]
1814 IncompleteCubemap,
1815}
1816
1817#[derive(Debug)]
1819pub enum ImageType<'a> {
1820 MimeType(&'a str),
1822 Extension(&'a str),
1824 Format(ImageFormat),
1826}
1827
1828impl<'a> ImageType<'a> {
1829 pub fn to_image_format(&self) -> Result<ImageFormat, TextureError> {
1830 match self {
1831 ImageType::MimeType(mime_type) => ImageFormat::from_mime_type(mime_type)
1832 .ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())),
1833 ImageType::Extension(extension) => ImageFormat::from_extension(extension)
1834 .ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())),
1835 ImageType::Format(format) => Ok(*format),
1836 }
1837 }
1838}
1839
1840pub trait Volume {
1842 fn volume(&self) -> usize;
1843}
1844
1845impl Volume for Extent3d {
1846 fn volume(&self) -> usize {
1848 (self.width * self.height * self.depth_or_array_layers) as usize
1849 }
1850}
1851
1852pub trait TextureFormatPixelInfo {
1854 fn pixel_size(&self) -> Result<usize, TextureAccessError>;
1857}
1858
1859impl TextureFormatPixelInfo for TextureFormat {
1860 fn pixel_size(&self) -> Result<usize, TextureAccessError> {
1861 let info = self;
1862 match info.block_dimensions() {
1863 (1, 1) => Ok(info.block_copy_size(None).unwrap() as usize),
1864 _ => Err(TextureAccessError::UnsupportedTextureFormat(*self)),
1865 }
1866 }
1867}
1868
1869bitflags::bitflags! {
1870 #[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
1871 #[repr(transparent)]
1872 pub struct CompressedImageFormats: u32 {
1873 const NONE = 0;
1874 const ASTC_LDR = 1 << 0;
1875 const BC = 1 << 1;
1876 const ETC2 = 1 << 2;
1877 }
1878}
1879
1880impl CompressedImageFormats {
1881 pub fn from_features(features: Features) -> Self {
1882 let mut supported_compressed_formats = Self::default();
1883 if features.contains(Features::TEXTURE_COMPRESSION_ASTC) {
1884 supported_compressed_formats |= Self::ASTC_LDR;
1885 }
1886 if features.contains(Features::TEXTURE_COMPRESSION_BC) {
1887 supported_compressed_formats |= Self::BC;
1888 }
1889 if features.contains(Features::TEXTURE_COMPRESSION_ETC2) {
1890 supported_compressed_formats |= Self::ETC2;
1891 }
1892 supported_compressed_formats
1893 }
1894
1895 pub fn supports(&self, format: TextureFormat) -> bool {
1896 match format {
1897 TextureFormat::Bc1RgbaUnorm
1898 | TextureFormat::Bc1RgbaUnormSrgb
1899 | TextureFormat::Bc2RgbaUnorm
1900 | TextureFormat::Bc2RgbaUnormSrgb
1901 | TextureFormat::Bc3RgbaUnorm
1902 | TextureFormat::Bc3RgbaUnormSrgb
1903 | TextureFormat::Bc4RUnorm
1904 | TextureFormat::Bc4RSnorm
1905 | TextureFormat::Bc5RgUnorm
1906 | TextureFormat::Bc5RgSnorm
1907 | TextureFormat::Bc6hRgbUfloat
1908 | TextureFormat::Bc6hRgbFloat
1909 | TextureFormat::Bc7RgbaUnorm
1910 | TextureFormat::Bc7RgbaUnormSrgb => self.contains(CompressedImageFormats::BC),
1911 TextureFormat::Etc2Rgb8Unorm
1912 | TextureFormat::Etc2Rgb8UnormSrgb
1913 | TextureFormat::Etc2Rgb8A1Unorm
1914 | TextureFormat::Etc2Rgb8A1UnormSrgb
1915 | TextureFormat::Etc2Rgba8Unorm
1916 | TextureFormat::Etc2Rgba8UnormSrgb
1917 | TextureFormat::EacR11Unorm
1918 | TextureFormat::EacR11Snorm
1919 | TextureFormat::EacRg11Unorm
1920 | TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2),
1921 TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR),
1922 _ => true,
1923 }
1924 }
1925}
1926
1927#[derive(Resource)]
1931pub struct CompressedImageFormatSupport(pub CompressedImageFormats);
1932
1933#[cfg(test)]
1934mod test {
1935 use super::*;
1936
1937 #[test]
1938 fn image_size() {
1939 let size = Extent3d {
1940 width: 200,
1941 height: 100,
1942 depth_or_array_layers: 1,
1943 };
1944 let image = Image::new_fill(
1945 size,
1946 TextureDimension::D2,
1947 &[0, 0, 0, 255],
1948 TextureFormat::Rgba8Unorm,
1949 RenderAssetUsages::MAIN_WORLD,
1950 );
1951 assert_eq!(
1952 Vec2::new(size.width as f32, size.height as f32),
1953 image.size_f32()
1954 );
1955 }
1956
1957 #[test]
1958 fn image_default_size() {
1959 let image = Image::default();
1960 assert_eq!(UVec2::ONE, image.size());
1961 assert_eq!(Vec2::ONE, image.size_f32());
1962 }
1963
1964 #[test]
1965 fn on_edge_pixel_is_invalid() {
1966 let image = Image::new_fill(
1967 Extent3d {
1968 width: 5,
1969 height: 10,
1970 depth_or_array_layers: 1,
1971 },
1972 TextureDimension::D2,
1973 &[0, 0, 0, 255],
1974 TextureFormat::Rgba8Unorm,
1975 RenderAssetUsages::MAIN_WORLD,
1976 );
1977 assert!(matches!(image.get_color_at(4, 9), Ok(Color::BLACK)));
1978 assert!(matches!(
1979 image.get_color_at(0, 10),
1980 Err(TextureAccessError::OutOfBounds { x: 0, y: 10, z: 0 })
1981 ));
1982 assert!(matches!(
1983 image.get_color_at(5, 10),
1984 Err(TextureAccessError::OutOfBounds { x: 5, y: 10, z: 0 })
1985 ));
1986 }
1987
1988 #[test]
1989 fn get_set_pixel_2d_with_layers() {
1990 let mut image = Image::new_fill(
1991 Extent3d {
1992 width: 5,
1993 height: 10,
1994 depth_or_array_layers: 3,
1995 },
1996 TextureDimension::D2,
1997 &[0, 0, 0, 255],
1998 TextureFormat::Rgba8Unorm,
1999 RenderAssetUsages::MAIN_WORLD,
2000 );
2001 image.set_color_at_3d(0, 0, 0, Color::WHITE).unwrap();
2002 assert!(matches!(image.get_color_at_3d(0, 0, 0), Ok(Color::WHITE)));
2003 image.set_color_at_3d(2, 3, 1, Color::WHITE).unwrap();
2004 assert!(matches!(image.get_color_at_3d(2, 3, 1), Ok(Color::WHITE)));
2005 image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
2006 assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
2007 }
2008
2009 #[test]
2010 fn resize_in_place_2d_grow_and_shrink() {
2011 use bevy_color::ColorToPacked;
2012
2013 const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
2014 const GROW_FILL: LinearRgba = LinearRgba::NONE;
2015
2016 let mut image = Image::new_fill(
2017 Extent3d {
2018 width: 2,
2019 height: 2,
2020 depth_or_array_layers: 1,
2021 },
2022 TextureDimension::D2,
2023 &INITIAL_FILL.to_u8_array(),
2024 TextureFormat::Rgba8Unorm,
2025 RenderAssetUsages::MAIN_WORLD,
2026 );
2027
2028 const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
2031 (0, 1, LinearRgba::RED),
2032 (1, 1, LinearRgba::GREEN),
2033 (1, 0, LinearRgba::BLUE),
2034 ];
2035
2036 for (x, y, color) in &TEST_PIXELS {
2037 image.set_color_at(*x, *y, Color::from(*color)).unwrap();
2038 }
2039
2040 image.resize_in_place(Extent3d {
2042 width: 4,
2043 height: 4,
2044 depth_or_array_layers: 1,
2045 });
2046
2047 assert!(matches!(
2049 image.get_color_at(0, 0),
2050 Ok(Color::LinearRgba(INITIAL_FILL))
2051 ));
2052 for (x, y, color) in &TEST_PIXELS {
2053 assert_eq!(
2054 image.get_color_at(*x, *y).unwrap(),
2055 Color::LinearRgba(*color)
2056 );
2057 }
2058
2059 assert!(matches!(
2061 image.get_color_at(3, 3),
2062 Ok(Color::LinearRgba(GROW_FILL))
2063 ));
2064
2065 image.resize_in_place(Extent3d {
2067 width: 1,
2068 height: 1,
2069 depth_or_array_layers: 1,
2070 });
2071
2072 assert!(image.get_color_at(1, 1).is_err());
2074 }
2075
2076 #[test]
2077 fn resize_in_place_array_grow_and_shrink() {
2078 use bevy_color::ColorToPacked;
2079
2080 const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
2081 const GROW_FILL: LinearRgba = LinearRgba::NONE;
2082 const LAYERS: u32 = 4;
2083
2084 let mut image = Image::new_fill(
2085 Extent3d {
2086 width: 2,
2087 height: 2,
2088 depth_or_array_layers: LAYERS,
2089 },
2090 TextureDimension::D2,
2091 &INITIAL_FILL.to_u8_array(),
2092 TextureFormat::Rgba8Unorm,
2093 RenderAssetUsages::MAIN_WORLD,
2094 );
2095
2096 const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
2099 (0, 1, LinearRgba::RED),
2100 (1, 1, LinearRgba::GREEN),
2101 (1, 0, LinearRgba::BLUE),
2102 ];
2103
2104 for z in 0..LAYERS {
2105 for (x, y, color) in &TEST_PIXELS {
2106 image
2107 .set_color_at_3d(*x, *y, z, Color::from(*color))
2108 .unwrap();
2109 }
2110 }
2111
2112 image.resize_in_place(Extent3d {
2114 width: 4,
2115 height: 4,
2116 depth_or_array_layers: LAYERS + 1,
2117 });
2118
2119 assert!(matches!(
2121 image.get_color_at(0, 0),
2122 Ok(Color::LinearRgba(INITIAL_FILL))
2123 ));
2124 for z in 0..LAYERS {
2125 for (x, y, color) in &TEST_PIXELS {
2126 assert_eq!(
2127 image.get_color_at_3d(*x, *y, z).unwrap(),
2128 Color::LinearRgba(*color)
2129 );
2130 }
2131 }
2132
2133 for z in 0..(LAYERS + 1) {
2135 assert!(matches!(
2136 image.get_color_at_3d(3, 3, z),
2137 Ok(Color::LinearRgba(GROW_FILL))
2138 ));
2139 }
2140
2141 image.resize_in_place(Extent3d {
2143 width: 1,
2144 height: 1,
2145 depth_or_array_layers: 1,
2146 });
2147
2148 assert!(image.get_color_at_3d(1, 1, 0).is_err());
2150
2151 assert!(image.get_color_at_3d(0, 0, 1).is_err());
2153
2154 image.resize_in_place(Extent3d {
2156 width: 1,
2157 height: 1,
2158 depth_or_array_layers: 2,
2159 });
2160
2161 assert!(matches!(
2163 image.get_color_at_3d(0, 0, 1),
2164 Ok(Color::LinearRgba(GROW_FILL))
2165 ));
2166 }
2167
2168 #[test]
2169 fn image_clear() {
2170 let mut image = Image::new_fill(
2171 Extent3d {
2172 width: 32,
2173 height: 32,
2174 depth_or_array_layers: 1,
2175 },
2176 TextureDimension::D2,
2177 &[0; 4],
2178 TextureFormat::Rgba8Snorm,
2179 RenderAssetUsages::all(),
2180 );
2181
2182 assert!(image.data.as_ref().unwrap().iter().all(|&p| p == 0));
2183
2184 image.clear(&[255; 4]);
2185
2186 assert!(image.data.as_ref().unwrap().iter().all(|&p| p == 255));
2187 }
2188}