Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_image/src/image.rs
6595 views
1
use crate::ImageLoader;
2
3
#[cfg(feature = "basis-universal")]
4
use super::basis::*;
5
#[cfg(feature = "dds")]
6
use super::dds::*;
7
#[cfg(feature = "ktx2")]
8
use super::ktx2::*;
9
use bevy_app::{App, Plugin};
10
#[cfg(not(feature = "bevy_reflect"))]
11
use bevy_reflect::TypePath;
12
#[cfg(feature = "bevy_reflect")]
13
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
14
15
use bevy_asset::{uuid_handle, Asset, AssetApp, Assets, Handle, RenderAssetUsages};
16
use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza};
17
use bevy_ecs::resource::Resource;
18
use bevy_math::{AspectRatio, UVec2, UVec3, Vec2};
19
use core::hash::Hash;
20
use serde::{Deserialize, Serialize};
21
use thiserror::Error;
22
use wgpu_types::{
23
AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor,
24
SamplerDescriptor, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat,
25
TextureUsages, TextureViewDescriptor,
26
};
27
28
/// Trait used to provide default values for Bevy-external types that
29
/// do not implement [`Default`].
30
pub trait BevyDefault {
31
/// Returns the default value for a type.
32
fn bevy_default() -> Self;
33
}
34
35
impl BevyDefault for TextureFormat {
36
fn bevy_default() -> Self {
37
TextureFormat::Rgba8UnormSrgb
38
}
39
}
40
41
/// A handle to a 1 x 1 transparent white image.
42
///
43
/// Like [`Handle<Image>::default`], this is a handle to a fallback image asset.
44
/// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image.
45
// Number randomly selected by fair WolframAlpha query. Totally arbitrary.
46
pub const TRANSPARENT_IMAGE_HANDLE: Handle<Image> =
47
uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46");
48
49
/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU.
50
pub struct ImagePlugin {
51
/// The default image sampler to use when [`ImageSampler`] is set to `Default`.
52
pub default_sampler: ImageSamplerDescriptor,
53
}
54
55
impl Default for ImagePlugin {
56
fn default() -> Self {
57
ImagePlugin::default_linear()
58
}
59
}
60
61
impl ImagePlugin {
62
/// Creates image settings with linear sampling by default.
63
pub fn default_linear() -> ImagePlugin {
64
ImagePlugin {
65
default_sampler: ImageSamplerDescriptor::linear(),
66
}
67
}
68
69
/// Creates image settings with nearest sampling by default.
70
pub fn default_nearest() -> ImagePlugin {
71
ImagePlugin {
72
default_sampler: ImageSamplerDescriptor::nearest(),
73
}
74
}
75
}
76
77
impl 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
119
pub const TEXTURE_ASSET_INDEX: u64 = 0;
120
pub const SAMPLER_ASSET_INDEX: u64 = 1;
121
122
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
123
pub 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
158
macro_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
170
impl ImageFormat {
171
/// Gets the file extensions for a given format.
172
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
// FIXME: https://github.com/rust-lang/rust/issues/129031
207
#[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
/// Gets the MIME types for a given format.
220
///
221
/// If a format doesn't have any dedicated MIME types, this list will be empty.
222
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
// FIXME: https://github.com/rust-lang/rust/issues/129031
262
#[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
// note: farbfeld does not have a MIME type
285
"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
// FIXME: https://github.com/rust-lang/rust/issues/129031
380
#[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
421
pub trait ToExtents {
422
fn to_extents(self) -> Extent3d;
423
}
424
impl 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
}
433
impl 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
/// An image, optimized for usage in rendering.
444
///
445
/// ## Remote Inspection
446
///
447
/// To transmit an [`Image`] between two running Bevy apps, e.g. through BRP, use [`SerializedImage`](crate::SerializedImage).
448
/// This type is only meant for short-term transmission between same versions and should not be stored anywhere.
449
#[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))]
456
pub struct Image {
457
/// Raw pixel data.
458
/// If the image is being used as a storage texture which doesn't need to be initialized by the
459
/// CPU, then this should be `None`.
460
/// Otherwise, it should always be `Some`.
461
pub data: Option<Vec<u8>>,
462
/// For texture data with layers and mips, this field controls how wgpu interprets the buffer layout.
463
///
464
/// Use [`TextureDataOrder::default()`] for all other cases.
465
pub data_order: TextureDataOrder,
466
// TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors.
467
/// Describes the data layout of the GPU texture.\
468
/// For example, whether a texture contains 1D/2D/3D data, and what the format of the texture data is.
469
///
470
/// ## Field Usage Notes
471
/// - [`TextureDescriptor::label`] is used for caching purposes when not using `Asset<Image>`.\
472
/// If you use assets, the label is purely a debugging aid.
473
/// - [`TextureDescriptor::view_formats`] is currently unused by Bevy.
474
pub texture_descriptor: TextureDescriptor<Option<&'static str>, &'static [TextureFormat]>,
475
/// The [`ImageSampler`] to use during rendering.
476
pub sampler: ImageSampler,
477
/// Describes how the GPU texture should be interpreted.\
478
/// For example, 2D image data could be read as plain 2D, an array texture of layers of 2D with the same dimensions (and the number of layers in that case),
479
/// a cube map, an array of cube maps, etc.
480
///
481
/// ## Field Usage Notes
482
/// - [`TextureViewDescriptor::label`] is used for caching purposes when not using `Asset<Image>`.\
483
/// If you use assets, the label is purely a debugging aid.
484
pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
485
pub asset_usage: RenderAssetUsages,
486
/// Whether this image should be copied on the GPU when resized.
487
pub copy_on_resize: bool,
488
}
489
490
/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
491
/// [`ImageSampler::Default`], will read the sampler from the `ImagePlugin` at setup.
492
/// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`].
493
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
494
pub enum ImageSampler {
495
/// Default image sampler, derived from the `ImagePlugin` setup.
496
#[default]
497
Default,
498
/// Custom sampler for this image which will override global default.
499
Descriptor(ImageSamplerDescriptor),
500
}
501
502
impl ImageSampler {
503
/// Returns an image sampler with [`ImageFilterMode::Linear`] min and mag filters
504
#[inline]
505
pub fn linear() -> ImageSampler {
506
ImageSampler::Descriptor(ImageSamplerDescriptor::linear())
507
}
508
509
/// Returns an image sampler with [`ImageFilterMode::Nearest`] min and mag filters
510
#[inline]
511
pub fn nearest() -> ImageSampler {
512
ImageSampler::Descriptor(ImageSamplerDescriptor::nearest())
513
}
514
515
/// Initialize the descriptor if it is not already initialized.
516
///
517
/// Descriptor is typically initialized by Bevy when the image is loaded,
518
/// so this is convenient shortcut for updating the descriptor.
519
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
/// How edges should be handled in texture addressing.
534
///
535
/// See [`ImageSamplerDescriptor`] for information how to configure this.
536
///
537
/// This type mirrors [`AddressMode`].
538
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
539
pub enum ImageAddressMode {
540
/// Clamp the value to the edge of the texture.
541
///
542
/// -0.25 -> 0.0
543
/// 1.25 -> 1.0
544
#[default]
545
ClampToEdge,
546
/// Repeat the texture in a tiling fashion.
547
///
548
/// -0.25 -> 0.75
549
/// 1.25 -> 0.25
550
Repeat,
551
/// Repeat the texture, mirroring it every repeat.
552
///
553
/// -0.25 -> 0.25
554
/// 1.25 -> 0.75
555
MirrorRepeat,
556
/// Clamp the value to the border of the texture
557
/// Requires the wgpu feature [`Features::ADDRESS_MODE_CLAMP_TO_BORDER`].
558
///
559
/// -0.25 -> border
560
/// 1.25 -> border
561
ClampToBorder,
562
}
563
564
/// Texel mixing mode when sampling between texels.
565
///
566
/// This type mirrors [`FilterMode`].
567
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
568
pub enum ImageFilterMode {
569
/// Nearest neighbor sampling.
570
///
571
/// This creates a pixelated effect when used as a mag filter.
572
#[default]
573
Nearest,
574
/// Linear Interpolation.
575
///
576
/// This makes textures smooth but blurry when used as a mag filter.
577
Linear,
578
}
579
580
/// Comparison function used for depth and stencil operations.
581
///
582
/// This type mirrors [`CompareFunction`].
583
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
584
pub enum ImageCompareFunction {
585
/// Function never passes
586
Never,
587
/// Function passes if new value less than existing value
588
Less,
589
/// Function passes if new value is equal to existing value. When using
590
/// this compare function, make sure to mark your Vertex Shader's `@builtin(position)`
591
/// output as `@invariant` to prevent artifacting.
592
Equal,
593
/// Function passes if new value is less than or equal to existing value
594
LessEqual,
595
/// Function passes if new value is greater than existing value
596
Greater,
597
/// Function passes if new value is not equal to existing value. When using
598
/// this compare function, make sure to mark your Vertex Shader's `@builtin(position)`
599
/// output as `@invariant` to prevent artifacting.
600
NotEqual,
601
/// Function passes if new value is greater than or equal to existing value
602
GreaterEqual,
603
/// Function always passes
604
Always,
605
}
606
607
/// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`].
608
///
609
/// This type mirrors [`SamplerBorderColor`].
610
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
611
pub enum ImageSamplerBorderColor {
612
/// RGBA color `[0, 0, 0, 0]`.
613
TransparentBlack,
614
/// RGBA color `[0, 0, 0, 1]`.
615
OpaqueBlack,
616
/// RGBA color `[1, 1, 1, 1]`.
617
OpaqueWhite,
618
/// On the Metal wgpu backend, this is equivalent to [`Self::TransparentBlack`] for
619
/// textures that have an alpha component, and equivalent to [`Self::OpaqueBlack`]
620
/// for textures that do not have an alpha component. On other backends,
621
/// this is equivalent to [`Self::TransparentBlack`]. Requires
622
/// [`Features::ADDRESS_MODE_CLAMP_TO_ZERO`]. Not supported on the web.
623
Zero,
624
}
625
626
/// Indicates to an `ImageLoader` how an [`Image`] should be sampled.
627
///
628
/// As this type is part of the `ImageLoaderSettings`,
629
/// it will be serialized to an image asset `.meta` file which might require a migration in case of
630
/// a breaking change.
631
///
632
/// This types mirrors [`SamplerDescriptor`], but that might change in future versions.
633
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
634
pub struct ImageSamplerDescriptor {
635
pub label: Option<String>,
636
/// How to deal with out of bounds accesses in the u (i.e. x) direction.
637
pub address_mode_u: ImageAddressMode,
638
/// How to deal with out of bounds accesses in the v (i.e. y) direction.
639
pub address_mode_v: ImageAddressMode,
640
/// How to deal with out of bounds accesses in the w (i.e. z) direction.
641
pub address_mode_w: ImageAddressMode,
642
/// How to filter the texture when it needs to be magnified (made larger).
643
pub mag_filter: ImageFilterMode,
644
/// How to filter the texture when it needs to be minified (made smaller).
645
pub min_filter: ImageFilterMode,
646
/// How to filter between mip map levels
647
pub mipmap_filter: ImageFilterMode,
648
/// Minimum level of detail (i.e. mip level) to use.
649
pub lod_min_clamp: f32,
650
/// Maximum level of detail (i.e. mip level) to use.
651
pub lod_max_clamp: f32,
652
/// If this is enabled, this is a comparison sampler using the given comparison function.
653
pub compare: Option<ImageCompareFunction>,
654
/// Must be at least 1. If this is not 1, all filter modes must be linear.
655
pub anisotropy_clamp: u16,
656
/// Border color to use when `address_mode` is [`ImageAddressMode::ClampToBorder`].
657
pub border_color: Option<ImageSamplerBorderColor>,
658
}
659
660
impl 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
679
impl ImageSamplerDescriptor {
680
/// Returns a sampler descriptor with [`Linear`](ImageFilterMode::Linear) min and mag filters
681
#[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
/// Returns a sampler descriptor with [`Nearest`](ImageFilterMode::Nearest) min and mag filters
692
#[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
720
impl 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
731
impl 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
740
impl 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
755
impl 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
766
impl 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
777
impl 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
786
impl 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
801
impl 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
812
impl 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
831
impl Default for Image {
832
/// default is a 1x1x1 all '1.0' texture
833
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
847
impl Image {
848
/// Creates a new image from raw binary data and the corresponding metadata.
849
///
850
/// # Panics
851
/// Panics if the length of the `data`, volume of the `size` and the size of the `format`
852
/// do not match.
853
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
/// Exactly the same as [`Image::new`], but doesn't initialize the image
873
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
/// A transparent white 1x1x1 image.
902
///
903
/// Contrast to [`Image::default`], which is opaque.
904
pub fn transparent() -> Image {
905
// We rely on the default texture format being RGBA8UnormSrgb
906
// when constructing a transparent color from bytes.
907
// If this changes, this function will need to be updated.
908
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
/// Creates a new uninitialized 1x1x1 image
922
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
/// Creates a new image from raw binary data and the corresponding metadata, by filling
932
/// the image data with the `pixel` data repeated multiple times.
933
///
934
/// # Panics
935
/// Panics if the size of the `format` is not a multiple of the length of the `pixel` data.
936
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
/// Create a new zero-filled image with a given size, which can be rendered to.
965
/// Useful for mirrors, UI, or exporting images for example.
966
/// This is primarily for use as a render target for a [`Camera`].
967
/// See [`RenderTarget::Image`].
968
///
969
/// For Standard Dynamic Range (SDR) images you can use [`TextureFormat::Rgba8UnormSrgb`].
970
/// For High Dynamic Range (HDR) images you can use [`TextureFormat::Rgba16Float`].
971
///
972
/// The default [`TextureUsages`] are
973
/// [`TEXTURE_BINDING`](TextureUsages::TEXTURE_BINDING),
974
/// [`COPY_DST`](TextureUsages::COPY_DST),
975
/// [`RENDER_ATTACHMENT`](TextureUsages::RENDER_ATTACHMENT).
976
///
977
/// The default [`RenderAssetUsages`] is [`MAIN_WORLD | RENDER_WORLD`](RenderAssetUsages::default)
978
/// so that it is accessible from the CPU and GPU.
979
/// You can customize this by changing the [`asset_usage`](Image::asset_usage) field.
980
///
981
/// [`Camera`]: https://docs.rs/bevy/latest/bevy/render/camera/struct.Camera.html
982
/// [`RenderTarget::Image`]: https://docs.rs/bevy/latest/bevy/render/camera/enum.RenderTarget.html#variant.Image
983
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
// You need to set these texture usage flags in order to use the image as a render target
990
let usage = TextureUsages::TEXTURE_BINDING
991
| TextureUsages::COPY_DST
992
| TextureUsages::RENDER_ATTACHMENT;
993
// Fill with zeroes
994
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
/// Returns the width of a 2D image.
1022
#[inline]
1023
pub fn width(&self) -> u32 {
1024
self.texture_descriptor.size.width
1025
}
1026
1027
/// Returns the height of a 2D image.
1028
#[inline]
1029
pub fn height(&self) -> u32 {
1030
self.texture_descriptor.size.height
1031
}
1032
1033
/// Returns the aspect ratio (width / height) of a 2D image.
1034
#[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
/// Returns the size of a 2D image as f32.
1042
#[inline]
1043
pub fn size_f32(&self) -> Vec2 {
1044
Vec2::new(self.width() as f32, self.height() as f32)
1045
}
1046
1047
/// Returns the size of a 2D image.
1048
#[inline]
1049
pub fn size(&self) -> UVec2 {
1050
UVec2::new(self.width(), self.height())
1051
}
1052
1053
/// Resizes the image to the new size, by removing information or appending 0 to the `data`.
1054
/// Does not properly scale the contents of the image.
1055
///
1056
/// If you need to keep pixel data intact, use [`Image::resize_in_place`].
1057
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
/// Changes the `size`, asserting that the total number of data elements (pixels) remains the
1067
/// same.
1068
///
1069
/// # Panics
1070
/// Panics if the `new_size` does not have the same volume as to old one.
1071
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
/// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left.
1084
/// When growing, the new space is filled with 0. When shrinking, the image is clipped.
1085
///
1086
/// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`].
1087
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
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
1129
/// it as a 2D array texture, where each of the stacked images becomes one layer of the
1130
/// array. This is primarily for use with the `texture2DArray` shader uniform type.
1131
///
1132
/// # Panics
1133
/// Panics if the texture is not 2D, has more than one layers or is not evenly dividable into
1134
/// the `layers`.
1135
pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) {
1136
// Must be a stacked image, and the height must be divisible by layers.
1137
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
/// Convert a texture from a format to another. Only a few formats are
1149
/// supported as input and output:
1150
/// - `TextureFormat::R8Unorm`
1151
/// - `TextureFormat::Rg8Unorm`
1152
/// - `TextureFormat::Rgba8UnormSrgb`
1153
///
1154
/// To get [`Image`] as a [`image::DynamicImage`] see:
1155
/// [`Image::try_into_dynamic`].
1156
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
/// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image`
1177
/// crate
1178
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
// Load the image in the expected format.
1193
// Some formats like PNG allow for R or RG textures too, so the texture
1194
// format needs to be determined. For RGB textures an alpha channel
1195
// needs to be added, so the image data needs to be converted in those
1196
// cases.
1197
1198
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
/// Whether the texture format is compressed or uncompressed
1233
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
/// Compute the byte offset where the data of a specific pixel is stored
1247
///
1248
/// Returns None if the provided coordinates are out of bounds.
1249
///
1250
/// For 2D textures, Z is the layer number. For 1D textures, Y and Z are ignored.
1251
#[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
/// Get a reference to the data bytes where a specific pixel's value is stored
1277
#[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
/// Get a mutable reference to the data bytes where a specific pixel's value is stored
1286
#[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
/// Read the color of a specific pixel (1D texture).
1295
///
1296
/// See [`get_color_at`](Self::get_color_at) for more details.
1297
#[inline(always)]
1298
pub fn get_color_at_1d(&self, x: u32) -> Result<Color, TextureAccessError> {
1299
if self.texture_descriptor.dimension != TextureDimension::D1 {
1300
return Err(TextureAccessError::WrongDimension);
1301
}
1302
self.get_color_at_internal(UVec3::new(x, 0, 0))
1303
}
1304
1305
/// Read the color of a specific pixel (2D texture).
1306
///
1307
/// This function will find the raw byte data of a specific pixel and
1308
/// decode it into a user-friendly [`Color`] struct for you.
1309
///
1310
/// Supports many of the common [`TextureFormat`]s:
1311
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1312
/// - 16-bit and 32-bit unsigned integer
1313
/// - 16-bit and 32-bit float
1314
///
1315
/// Be careful: as the data is converted to [`Color`] (which uses `f32` internally),
1316
/// there may be issues with precision when using non-f32 [`TextureFormat`]s.
1317
/// If you read a value you previously wrote using `set_color_at`, it will not match.
1318
/// If you are working with a 32-bit integer [`TextureFormat`], the value will be
1319
/// inaccurate (as `f32` does not have enough bits to represent it exactly).
1320
///
1321
/// Single channel (R) formats are assumed to represent grayscale, so the value
1322
/// will be copied to all three RGB channels in the resulting [`Color`].
1323
///
1324
/// Other [`TextureFormat`]s are unsupported, such as:
1325
/// - block-compressed formats
1326
/// - non-byte-aligned formats like 10-bit
1327
/// - signed integer formats
1328
#[inline(always)]
1329
pub fn get_color_at(&self, x: u32, y: u32) -> Result<Color, TextureAccessError> {
1330
if self.texture_descriptor.dimension != TextureDimension::D2 {
1331
return Err(TextureAccessError::WrongDimension);
1332
}
1333
self.get_color_at_internal(UVec3::new(x, y, 0))
1334
}
1335
1336
/// Read the color of a specific pixel (2D texture with layers or 3D texture).
1337
///
1338
/// See [`get_color_at`](Self::get_color_at) for more details.
1339
#[inline(always)]
1340
pub fn get_color_at_3d(&self, x: u32, y: u32, z: u32) -> Result<Color, TextureAccessError> {
1341
match (
1342
self.texture_descriptor.dimension,
1343
self.texture_descriptor.size.depth_or_array_layers,
1344
) {
1345
(TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1346
self.get_color_at_internal(UVec3::new(x, y, z))
1347
}
1348
_ => Err(TextureAccessError::WrongDimension),
1349
}
1350
}
1351
1352
/// Change the color of a specific pixel (1D texture).
1353
///
1354
/// See [`set_color_at`](Self::set_color_at) for more details.
1355
#[inline(always)]
1356
pub fn set_color_at_1d(&mut self, x: u32, color: Color) -> Result<(), TextureAccessError> {
1357
if self.texture_descriptor.dimension != TextureDimension::D1 {
1358
return Err(TextureAccessError::WrongDimension);
1359
}
1360
self.set_color_at_internal(UVec3::new(x, 0, 0), color)
1361
}
1362
1363
/// Change the color of a specific pixel (2D texture).
1364
///
1365
/// This function will find the raw byte data of a specific pixel and
1366
/// change it according to a [`Color`] you provide. The [`Color`] struct
1367
/// will be encoded into the [`Image`]'s [`TextureFormat`].
1368
///
1369
/// Supports many of the common [`TextureFormat`]s:
1370
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1371
/// - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`)
1372
/// - 16-bit and 32-bit float
1373
///
1374
/// Be careful: writing to non-f32 [`TextureFormat`]s is lossy! The data has to be converted,
1375
/// so if you read it back using `get_color_at`, the `Color` you get will not equal the value
1376
/// you used when writing it using this function.
1377
///
1378
/// For R and RG formats, only the respective values from the linear RGB [`Color`] will be used.
1379
///
1380
/// Other [`TextureFormat`]s are unsupported, such as:
1381
/// - block-compressed formats
1382
/// - non-byte-aligned formats like 10-bit
1383
/// - signed integer formats
1384
#[inline(always)]
1385
pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> {
1386
if self.texture_descriptor.dimension != TextureDimension::D2 {
1387
return Err(TextureAccessError::WrongDimension);
1388
}
1389
self.set_color_at_internal(UVec3::new(x, y, 0), color)
1390
}
1391
1392
/// Change the color of a specific pixel (2D texture with layers or 3D texture).
1393
///
1394
/// See [`set_color_at`](Self::set_color_at) for more details.
1395
#[inline(always)]
1396
pub fn set_color_at_3d(
1397
&mut self,
1398
x: u32,
1399
y: u32,
1400
z: u32,
1401
color: Color,
1402
) -> Result<(), TextureAccessError> {
1403
match (
1404
self.texture_descriptor.dimension,
1405
self.texture_descriptor.size.depth_or_array_layers,
1406
) {
1407
(TextureDimension::D3, _) | (TextureDimension::D2, 2..) => {
1408
self.set_color_at_internal(UVec3::new(x, y, z), color)
1409
}
1410
_ => Err(TextureAccessError::WrongDimension),
1411
}
1412
}
1413
1414
#[inline(always)]
1415
fn get_color_at_internal(&self, coords: UVec3) -> Result<Color, TextureAccessError> {
1416
let Some(bytes) = self.pixel_bytes(coords) else {
1417
return Err(TextureAccessError::OutOfBounds {
1418
x: coords.x,
1419
y: coords.y,
1420
z: coords.z,
1421
});
1422
};
1423
1424
// NOTE: GPUs are always Little Endian.
1425
// Make sure to respect that when we create color values from bytes.
1426
match self.texture_descriptor.format {
1427
TextureFormat::Rgba8UnormSrgb => Ok(Color::srgba(
1428
bytes[0] as f32 / u8::MAX as f32,
1429
bytes[1] as f32 / u8::MAX as f32,
1430
bytes[2] as f32 / u8::MAX as f32,
1431
bytes[3] as f32 / u8::MAX as f32,
1432
)),
1433
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => Ok(Color::linear_rgba(
1434
bytes[0] as f32 / u8::MAX as f32,
1435
bytes[1] as f32 / u8::MAX as f32,
1436
bytes[2] as f32 / u8::MAX as f32,
1437
bytes[3] as f32 / u8::MAX as f32,
1438
)),
1439
TextureFormat::Bgra8UnormSrgb => Ok(Color::srgba(
1440
bytes[2] as f32 / u8::MAX as f32,
1441
bytes[1] as f32 / u8::MAX as f32,
1442
bytes[0] as f32 / u8::MAX as f32,
1443
bytes[3] as f32 / u8::MAX as f32,
1444
)),
1445
TextureFormat::Bgra8Unorm => Ok(Color::linear_rgba(
1446
bytes[2] as f32 / u8::MAX as f32,
1447
bytes[1] as f32 / u8::MAX as f32,
1448
bytes[0] as f32 / u8::MAX as f32,
1449
bytes[3] as f32 / u8::MAX as f32,
1450
)),
1451
TextureFormat::Rgba32Float => Ok(Color::linear_rgba(
1452
f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1453
f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1454
f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1455
f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1456
)),
1457
TextureFormat::Rgba16Float => Ok(Color::linear_rgba(
1458
half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32(),
1459
half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32(),
1460
half::f16::from_le_bytes([bytes[4], bytes[5]]).to_f32(),
1461
half::f16::from_le_bytes([bytes[6], bytes[7]]).to_f32(),
1462
)),
1463
TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1464
let (r, g, b, a) = (
1465
u16::from_le_bytes([bytes[0], bytes[1]]),
1466
u16::from_le_bytes([bytes[2], bytes[3]]),
1467
u16::from_le_bytes([bytes[4], bytes[5]]),
1468
u16::from_le_bytes([bytes[6], bytes[7]]),
1469
);
1470
Ok(Color::linear_rgba(
1471
// going via f64 to avoid rounding errors with large numbers and division
1472
(r as f64 / u16::MAX as f64) as f32,
1473
(g as f64 / u16::MAX as f64) as f32,
1474
(b as f64 / u16::MAX as f64) as f32,
1475
(a as f64 / u16::MAX as f64) as f32,
1476
))
1477
}
1478
TextureFormat::Rgba32Uint => {
1479
let (r, g, b, a) = (
1480
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
1481
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1482
u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
1483
u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
1484
);
1485
Ok(Color::linear_rgba(
1486
// going via f64 to avoid rounding errors with large numbers and division
1487
(r as f64 / u32::MAX as f64) as f32,
1488
(g as f64 / u32::MAX as f64) as f32,
1489
(b as f64 / u32::MAX as f64) as f32,
1490
(a as f64 / u32::MAX as f64) as f32,
1491
))
1492
}
1493
// assume R-only texture format means grayscale (linear)
1494
// copy value to all of RGB in Color
1495
TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1496
let x = bytes[0] as f32 / u8::MAX as f32;
1497
Ok(Color::linear_rgb(x, x, x))
1498
}
1499
TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1500
let x = u16::from_le_bytes([bytes[0], bytes[1]]);
1501
// going via f64 to avoid rounding errors with large numbers and division
1502
let x = (x as f64 / u16::MAX as f64) as f32;
1503
Ok(Color::linear_rgb(x, x, x))
1504
}
1505
TextureFormat::R32Uint => {
1506
let x = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1507
// going via f64 to avoid rounding errors with large numbers and division
1508
let x = (x as f64 / u32::MAX as f64) as f32;
1509
Ok(Color::linear_rgb(x, x, x))
1510
}
1511
TextureFormat::R16Float => {
1512
let x = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1513
Ok(Color::linear_rgb(x, x, x))
1514
}
1515
TextureFormat::R32Float => {
1516
let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1517
Ok(Color::linear_rgb(x, x, x))
1518
}
1519
TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1520
let r = bytes[0] as f32 / u8::MAX as f32;
1521
let g = bytes[1] as f32 / u8::MAX as f32;
1522
Ok(Color::linear_rgb(r, g, 0.0))
1523
}
1524
TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1525
let r = u16::from_le_bytes([bytes[0], bytes[1]]);
1526
let g = u16::from_le_bytes([bytes[2], bytes[3]]);
1527
// going via f64 to avoid rounding errors with large numbers and division
1528
let r = (r as f64 / u16::MAX as f64) as f32;
1529
let g = (g as f64 / u16::MAX as f64) as f32;
1530
Ok(Color::linear_rgb(r, g, 0.0))
1531
}
1532
TextureFormat::Rg32Uint => {
1533
let r = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1534
let g = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1535
// going via f64 to avoid rounding errors with large numbers and division
1536
let r = (r as f64 / u32::MAX as f64) as f32;
1537
let g = (g as f64 / u32::MAX as f64) as f32;
1538
Ok(Color::linear_rgb(r, g, 0.0))
1539
}
1540
TextureFormat::Rg16Float => {
1541
let r = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
1542
let g = half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32();
1543
Ok(Color::linear_rgb(r, g, 0.0))
1544
}
1545
TextureFormat::Rg32Float => {
1546
let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1547
let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1548
Ok(Color::linear_rgb(r, g, 0.0))
1549
}
1550
_ => Err(TextureAccessError::UnsupportedTextureFormat(
1551
self.texture_descriptor.format,
1552
)),
1553
}
1554
}
1555
1556
#[inline(always)]
1557
fn set_color_at_internal(
1558
&mut self,
1559
coords: UVec3,
1560
color: Color,
1561
) -> Result<(), TextureAccessError> {
1562
let format = self.texture_descriptor.format;
1563
1564
let Some(bytes) = self.pixel_bytes_mut(coords) else {
1565
return Err(TextureAccessError::OutOfBounds {
1566
x: coords.x,
1567
y: coords.y,
1568
z: coords.z,
1569
});
1570
};
1571
1572
// NOTE: GPUs are always Little Endian.
1573
// Make sure to respect that when we convert color values to bytes.
1574
match format {
1575
TextureFormat::Rgba8UnormSrgb => {
1576
let [r, g, b, a] = Srgba::from(color).to_f32_array();
1577
bytes[0] = (r * u8::MAX as f32) as u8;
1578
bytes[1] = (g * u8::MAX as f32) as u8;
1579
bytes[2] = (b * u8::MAX as f32) as u8;
1580
bytes[3] = (a * u8::MAX as f32) as u8;
1581
}
1582
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => {
1583
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1584
bytes[0] = (r * u8::MAX as f32) as u8;
1585
bytes[1] = (g * u8::MAX as f32) as u8;
1586
bytes[2] = (b * u8::MAX as f32) as u8;
1587
bytes[3] = (a * u8::MAX as f32) as u8;
1588
}
1589
TextureFormat::Bgra8UnormSrgb => {
1590
let [r, g, b, a] = Srgba::from(color).to_f32_array();
1591
bytes[0] = (b * u8::MAX as f32) as u8;
1592
bytes[1] = (g * u8::MAX as f32) as u8;
1593
bytes[2] = (r * u8::MAX as f32) as u8;
1594
bytes[3] = (a * u8::MAX as f32) as u8;
1595
}
1596
TextureFormat::Bgra8Unorm => {
1597
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1598
bytes[0] = (b * u8::MAX as f32) as u8;
1599
bytes[1] = (g * u8::MAX as f32) as u8;
1600
bytes[2] = (r * u8::MAX as f32) as u8;
1601
bytes[3] = (a * u8::MAX as f32) as u8;
1602
}
1603
TextureFormat::Rgba16Float => {
1604
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1605
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1606
bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1607
bytes[4..6].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(b)));
1608
bytes[6..8].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(a)));
1609
}
1610
TextureFormat::Rgba32Float => {
1611
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1612
bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1613
bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1614
bytes[8..12].copy_from_slice(&f32::to_le_bytes(b));
1615
bytes[12..16].copy_from_slice(&f32::to_le_bytes(a));
1616
}
1617
TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
1618
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1619
let [r, g, b, a] = [
1620
(r * u16::MAX as f32) as u16,
1621
(g * u16::MAX as f32) as u16,
1622
(b * u16::MAX as f32) as u16,
1623
(a * u16::MAX as f32) as u16,
1624
];
1625
bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1626
bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1627
bytes[4..6].copy_from_slice(&u16::to_le_bytes(b));
1628
bytes[6..8].copy_from_slice(&u16::to_le_bytes(a));
1629
}
1630
TextureFormat::Rgba32Uint => {
1631
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
1632
let [r, g, b, a] = [
1633
(r * u32::MAX as f32) as u32,
1634
(g * u32::MAX as f32) as u32,
1635
(b * u32::MAX as f32) as u32,
1636
(a * u32::MAX as f32) as u32,
1637
];
1638
bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1639
bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1640
bytes[8..12].copy_from_slice(&u32::to_le_bytes(b));
1641
bytes[12..16].copy_from_slice(&u32::to_le_bytes(a));
1642
}
1643
TextureFormat::R8Unorm | TextureFormat::R8Uint => {
1644
// Convert to grayscale with minimal loss if color is already gray
1645
let linear = LinearRgba::from(color);
1646
let luminance = Xyza::from(linear).y;
1647
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1648
bytes[0] = (r * u8::MAX as f32) as u8;
1649
}
1650
TextureFormat::R16Unorm | TextureFormat::R16Uint => {
1651
// Convert to grayscale with minimal loss if color is already gray
1652
let linear = LinearRgba::from(color);
1653
let luminance = Xyza::from(linear).y;
1654
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1655
let r = (r * u16::MAX as f32) as u16;
1656
bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1657
}
1658
TextureFormat::R32Uint => {
1659
// Convert to grayscale with minimal loss if color is already gray
1660
let linear = LinearRgba::from(color);
1661
let luminance = Xyza::from(linear).y;
1662
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1663
// go via f64 to avoid imprecision
1664
let r = (r as f64 * u32::MAX as f64) as u32;
1665
bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1666
}
1667
TextureFormat::R16Float => {
1668
// Convert to grayscale with minimal loss if color is already gray
1669
let linear = LinearRgba::from(color);
1670
let luminance = Xyza::from(linear).y;
1671
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1672
let x = half::f16::from_f32(r);
1673
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(x));
1674
}
1675
TextureFormat::R32Float => {
1676
// Convert to grayscale with minimal loss if color is already gray
1677
let linear = LinearRgba::from(color);
1678
let luminance = Xyza::from(linear).y;
1679
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1680
bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1681
}
1682
TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => {
1683
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1684
bytes[0] = (r * u8::MAX as f32) as u8;
1685
bytes[1] = (g * u8::MAX as f32) as u8;
1686
}
1687
TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => {
1688
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1689
let r = (r * u16::MAX as f32) as u16;
1690
let g = (g * u16::MAX as f32) as u16;
1691
bytes[0..2].copy_from_slice(&u16::to_le_bytes(r));
1692
bytes[2..4].copy_from_slice(&u16::to_le_bytes(g));
1693
}
1694
TextureFormat::Rg32Uint => {
1695
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1696
// go via f64 to avoid imprecision
1697
let r = (r as f64 * u32::MAX as f64) as u32;
1698
let g = (g as f64 * u32::MAX as f64) as u32;
1699
bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
1700
bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
1701
}
1702
TextureFormat::Rg16Float => {
1703
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1704
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
1705
bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
1706
}
1707
TextureFormat::Rg32Float => {
1708
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
1709
bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
1710
bytes[4..8].copy_from_slice(&f32::to_le_bytes(g));
1711
}
1712
_ => {
1713
return Err(TextureAccessError::UnsupportedTextureFormat(
1714
self.texture_descriptor.format,
1715
));
1716
}
1717
}
1718
Ok(())
1719
}
1720
}
1721
1722
#[derive(Clone, Copy, Debug)]
1723
pub enum DataFormat {
1724
Rgb,
1725
Rgba,
1726
Rrr,
1727
Rrrg,
1728
Rg,
1729
}
1730
1731
/// Texture data need to be transcoded from this format for use with `wgpu`.
1732
#[derive(Clone, Copy, Debug)]
1733
pub enum TranscodeFormat {
1734
Etc1s,
1735
Uastc(DataFormat),
1736
/// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`.
1737
R8UnormSrgb,
1738
/// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`.
1739
Rg8UnormSrgb,
1740
/// Has to be transcoded from `Rgb8` to `Rgba8` for use with `wgpu`.
1741
Rgb8,
1742
}
1743
1744
/// An error that occurs when accessing specific pixels in a texture.
1745
#[derive(Error, Debug)]
1746
pub enum TextureAccessError {
1747
#[error("out of bounds (x: {x}, y: {y}, z: {z})")]
1748
OutOfBounds { x: u32, y: u32, z: u32 },
1749
#[error("unsupported texture format: {0:?}")]
1750
UnsupportedTextureFormat(TextureFormat),
1751
#[error("attempt to access texture with different dimension")]
1752
WrongDimension,
1753
}
1754
1755
/// An error that occurs when loading a texture.
1756
#[derive(Error, Debug)]
1757
pub enum TextureError {
1758
/// Image MIME type is invalid.
1759
#[error("invalid image mime type: {0}")]
1760
InvalidImageMimeType(String),
1761
/// Image extension is invalid.
1762
#[error("invalid image extension: {0}")]
1763
InvalidImageExtension(String),
1764
/// Failed to load an image.
1765
#[error("failed to load an image: {0}")]
1766
ImageError(#[from] image::ImageError),
1767
/// Texture format isn't supported.
1768
#[error("unsupported texture format: {0}")]
1769
UnsupportedTextureFormat(String),
1770
/// Supercompression isn't supported.
1771
#[error("supercompression not supported: {0}")]
1772
SuperCompressionNotSupported(String),
1773
/// Failed to decompress an image.
1774
#[error("failed to decompress an image: {0}")]
1775
SuperDecompressionError(String),
1776
/// Invalid data.
1777
#[error("invalid data: {0}")]
1778
InvalidData(String),
1779
/// Transcode error.
1780
#[error("transcode error: {0}")]
1781
TranscodeError(String),
1782
/// Format requires transcoding.
1783
#[error("format requires transcoding: {0:?}")]
1784
FormatRequiresTranscodingError(TranscodeFormat),
1785
/// Only cubemaps with six faces are supported.
1786
#[error("only cubemaps with six faces are supported")]
1787
IncompleteCubemap,
1788
}
1789
1790
/// The type of a raw image buffer.
1791
#[derive(Debug)]
1792
pub enum ImageType<'a> {
1793
/// The mime type of an image, for example `"image/png"`.
1794
MimeType(&'a str),
1795
/// The extension of an image file, for example `"png"`.
1796
Extension(&'a str),
1797
/// The direct format of the image
1798
Format(ImageFormat),
1799
}
1800
1801
impl<'a> ImageType<'a> {
1802
pub fn to_image_format(&self) -> Result<ImageFormat, TextureError> {
1803
match self {
1804
ImageType::MimeType(mime_type) => ImageFormat::from_mime_type(mime_type)
1805
.ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())),
1806
ImageType::Extension(extension) => ImageFormat::from_extension(extension)
1807
.ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())),
1808
ImageType::Format(format) => Ok(*format),
1809
}
1810
}
1811
}
1812
1813
/// Used to calculate the volume of an item.
1814
pub trait Volume {
1815
fn volume(&self) -> usize;
1816
}
1817
1818
impl Volume for Extent3d {
1819
/// Calculates the volume of the [`Extent3d`].
1820
fn volume(&self) -> usize {
1821
(self.width * self.height * self.depth_or_array_layers) as usize
1822
}
1823
}
1824
1825
/// Extends the wgpu [`TextureFormat`] with information about the pixel.
1826
pub trait TextureFormatPixelInfo {
1827
/// Returns the size of a pixel in bytes of the format.
1828
/// error with `TextureAccessError::UnsupportedTextureFormat` if the format is compressed.
1829
fn pixel_size(&self) -> Result<usize, TextureAccessError>;
1830
}
1831
1832
impl TextureFormatPixelInfo for TextureFormat {
1833
fn pixel_size(&self) -> Result<usize, TextureAccessError> {
1834
let info = self;
1835
match info.block_dimensions() {
1836
(1, 1) => Ok(info.block_copy_size(None).unwrap() as usize),
1837
_ => Err(TextureAccessError::UnsupportedTextureFormat(*self)),
1838
}
1839
}
1840
}
1841
1842
bitflags::bitflags! {
1843
#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
1844
#[repr(transparent)]
1845
pub struct CompressedImageFormats: u32 {
1846
const NONE = 0;
1847
const ASTC_LDR = 1 << 0;
1848
const BC = 1 << 1;
1849
const ETC2 = 1 << 2;
1850
}
1851
}
1852
1853
impl CompressedImageFormats {
1854
pub fn from_features(features: Features) -> Self {
1855
let mut supported_compressed_formats = Self::default();
1856
if features.contains(Features::TEXTURE_COMPRESSION_ASTC) {
1857
supported_compressed_formats |= Self::ASTC_LDR;
1858
}
1859
if features.contains(Features::TEXTURE_COMPRESSION_BC) {
1860
supported_compressed_formats |= Self::BC;
1861
}
1862
if features.contains(Features::TEXTURE_COMPRESSION_ETC2) {
1863
supported_compressed_formats |= Self::ETC2;
1864
}
1865
supported_compressed_formats
1866
}
1867
1868
pub fn supports(&self, format: TextureFormat) -> bool {
1869
match format {
1870
TextureFormat::Bc1RgbaUnorm
1871
| TextureFormat::Bc1RgbaUnormSrgb
1872
| TextureFormat::Bc2RgbaUnorm
1873
| TextureFormat::Bc2RgbaUnormSrgb
1874
| TextureFormat::Bc3RgbaUnorm
1875
| TextureFormat::Bc3RgbaUnormSrgb
1876
| TextureFormat::Bc4RUnorm
1877
| TextureFormat::Bc4RSnorm
1878
| TextureFormat::Bc5RgUnorm
1879
| TextureFormat::Bc5RgSnorm
1880
| TextureFormat::Bc6hRgbUfloat
1881
| TextureFormat::Bc6hRgbFloat
1882
| TextureFormat::Bc7RgbaUnorm
1883
| TextureFormat::Bc7RgbaUnormSrgb => self.contains(CompressedImageFormats::BC),
1884
TextureFormat::Etc2Rgb8Unorm
1885
| TextureFormat::Etc2Rgb8UnormSrgb
1886
| TextureFormat::Etc2Rgb8A1Unorm
1887
| TextureFormat::Etc2Rgb8A1UnormSrgb
1888
| TextureFormat::Etc2Rgba8Unorm
1889
| TextureFormat::Etc2Rgba8UnormSrgb
1890
| TextureFormat::EacR11Unorm
1891
| TextureFormat::EacR11Snorm
1892
| TextureFormat::EacRg11Unorm
1893
| TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2),
1894
TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR),
1895
_ => true,
1896
}
1897
}
1898
}
1899
1900
/// For defining which compressed image formats are supported. This will be initialized from available device features
1901
/// in `finish()` of the bevy `RenderPlugin`, but is left for the user to specify if not using the `RenderPlugin`, or
1902
/// the WGPU backend.
1903
#[derive(Resource)]
1904
pub struct CompressedImageFormatSupport(pub CompressedImageFormats);
1905
1906
#[cfg(test)]
1907
mod test {
1908
use super::*;
1909
1910
#[test]
1911
fn image_size() {
1912
let size = Extent3d {
1913
width: 200,
1914
height: 100,
1915
depth_or_array_layers: 1,
1916
};
1917
let image = Image::new_fill(
1918
size,
1919
TextureDimension::D2,
1920
&[0, 0, 0, 255],
1921
TextureFormat::Rgba8Unorm,
1922
RenderAssetUsages::MAIN_WORLD,
1923
);
1924
assert_eq!(
1925
Vec2::new(size.width as f32, size.height as f32),
1926
image.size_f32()
1927
);
1928
}
1929
1930
#[test]
1931
fn image_default_size() {
1932
let image = Image::default();
1933
assert_eq!(UVec2::ONE, image.size());
1934
assert_eq!(Vec2::ONE, image.size_f32());
1935
}
1936
1937
#[test]
1938
fn on_edge_pixel_is_invalid() {
1939
let image = Image::new_fill(
1940
Extent3d {
1941
width: 5,
1942
height: 10,
1943
depth_or_array_layers: 1,
1944
},
1945
TextureDimension::D2,
1946
&[0, 0, 0, 255],
1947
TextureFormat::Rgba8Unorm,
1948
RenderAssetUsages::MAIN_WORLD,
1949
);
1950
assert!(matches!(image.get_color_at(4, 9), Ok(Color::BLACK)));
1951
assert!(matches!(
1952
image.get_color_at(0, 10),
1953
Err(TextureAccessError::OutOfBounds { x: 0, y: 10, z: 0 })
1954
));
1955
assert!(matches!(
1956
image.get_color_at(5, 10),
1957
Err(TextureAccessError::OutOfBounds { x: 5, y: 10, z: 0 })
1958
));
1959
}
1960
1961
#[test]
1962
fn get_set_pixel_2d_with_layers() {
1963
let mut image = Image::new_fill(
1964
Extent3d {
1965
width: 5,
1966
height: 10,
1967
depth_or_array_layers: 3,
1968
},
1969
TextureDimension::D2,
1970
&[0, 0, 0, 255],
1971
TextureFormat::Rgba8Unorm,
1972
RenderAssetUsages::MAIN_WORLD,
1973
);
1974
image.set_color_at_3d(0, 0, 0, Color::WHITE).unwrap();
1975
assert!(matches!(image.get_color_at_3d(0, 0, 0), Ok(Color::WHITE)));
1976
image.set_color_at_3d(2, 3, 1, Color::WHITE).unwrap();
1977
assert!(matches!(image.get_color_at_3d(2, 3, 1), Ok(Color::WHITE)));
1978
image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
1979
assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
1980
}
1981
1982
#[test]
1983
fn resize_in_place_2d_grow_and_shrink() {
1984
use bevy_color::ColorToPacked;
1985
1986
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
1987
const GROW_FILL: LinearRgba = LinearRgba::NONE;
1988
1989
let mut image = Image::new_fill(
1990
Extent3d {
1991
width: 2,
1992
height: 2,
1993
depth_or_array_layers: 1,
1994
},
1995
TextureDimension::D2,
1996
&INITIAL_FILL.to_u8_array(),
1997
TextureFormat::Rgba8Unorm,
1998
RenderAssetUsages::MAIN_WORLD,
1999
);
2000
2001
// Create a test pattern
2002
2003
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
2004
(0, 1, LinearRgba::RED),
2005
(1, 1, LinearRgba::GREEN),
2006
(1, 0, LinearRgba::BLUE),
2007
];
2008
2009
for (x, y, color) in &TEST_PIXELS {
2010
image.set_color_at(*x, *y, Color::from(*color)).unwrap();
2011
}
2012
2013
// Grow image
2014
image.resize_in_place(Extent3d {
2015
width: 4,
2016
height: 4,
2017
depth_or_array_layers: 1,
2018
});
2019
2020
// After growing, the test pattern should be the same.
2021
assert!(matches!(
2022
image.get_color_at(0, 0),
2023
Ok(Color::LinearRgba(INITIAL_FILL))
2024
));
2025
for (x, y, color) in &TEST_PIXELS {
2026
assert_eq!(
2027
image.get_color_at(*x, *y).unwrap(),
2028
Color::LinearRgba(*color)
2029
);
2030
}
2031
2032
// Pixels in the newly added area should get filled with zeroes.
2033
assert!(matches!(
2034
image.get_color_at(3, 3),
2035
Ok(Color::LinearRgba(GROW_FILL))
2036
));
2037
2038
// Shrink
2039
image.resize_in_place(Extent3d {
2040
width: 1,
2041
height: 1,
2042
depth_or_array_layers: 1,
2043
});
2044
2045
// Images outside of the new dimensions should be clipped
2046
assert!(image.get_color_at(1, 1).is_err());
2047
}
2048
2049
#[test]
2050
fn resize_in_place_array_grow_and_shrink() {
2051
use bevy_color::ColorToPacked;
2052
2053
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
2054
const GROW_FILL: LinearRgba = LinearRgba::NONE;
2055
const LAYERS: u32 = 4;
2056
2057
let mut image = Image::new_fill(
2058
Extent3d {
2059
width: 2,
2060
height: 2,
2061
depth_or_array_layers: LAYERS,
2062
},
2063
TextureDimension::D2,
2064
&INITIAL_FILL.to_u8_array(),
2065
TextureFormat::Rgba8Unorm,
2066
RenderAssetUsages::MAIN_WORLD,
2067
);
2068
2069
// Create a test pattern
2070
2071
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
2072
(0, 1, LinearRgba::RED),
2073
(1, 1, LinearRgba::GREEN),
2074
(1, 0, LinearRgba::BLUE),
2075
];
2076
2077
for z in 0..LAYERS {
2078
for (x, y, color) in &TEST_PIXELS {
2079
image
2080
.set_color_at_3d(*x, *y, z, Color::from(*color))
2081
.unwrap();
2082
}
2083
}
2084
2085
// Grow image
2086
image.resize_in_place(Extent3d {
2087
width: 4,
2088
height: 4,
2089
depth_or_array_layers: LAYERS + 1,
2090
});
2091
2092
// After growing, the test pattern should be the same.
2093
assert!(matches!(
2094
image.get_color_at(0, 0),
2095
Ok(Color::LinearRgba(INITIAL_FILL))
2096
));
2097
for z in 0..LAYERS {
2098
for (x, y, color) in &TEST_PIXELS {
2099
assert_eq!(
2100
image.get_color_at_3d(*x, *y, z).unwrap(),
2101
Color::LinearRgba(*color)
2102
);
2103
}
2104
}
2105
2106
// Pixels in the newly added area should get filled with zeroes.
2107
for z in 0..(LAYERS + 1) {
2108
assert!(matches!(
2109
image.get_color_at_3d(3, 3, z),
2110
Ok(Color::LinearRgba(GROW_FILL))
2111
));
2112
}
2113
2114
// Shrink
2115
image.resize_in_place(Extent3d {
2116
width: 1,
2117
height: 1,
2118
depth_or_array_layers: 1,
2119
});
2120
2121
// Images outside of the new dimensions should be clipped
2122
assert!(image.get_color_at_3d(1, 1, 0).is_err());
2123
2124
// Higher layers should no longer be present
2125
assert!(image.get_color_at_3d(0, 0, 1).is_err());
2126
2127
// Grow layers
2128
image.resize_in_place(Extent3d {
2129
width: 1,
2130
height: 1,
2131
depth_or_array_layers: 2,
2132
});
2133
2134
// Pixels in the newly added layer should be zeroes.
2135
assert!(matches!(
2136
image.get_color_at_3d(0, 0, 1),
2137
Ok(Color::LinearRgba(GROW_FILL))
2138
));
2139
}
2140
}
2141
2142