Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_image/src/image.rs
6849 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
/// Clears the content of the image with the given pixel. The image needs to be initialized on
1295
/// the cpu otherwise this is a noop.
1296
///
1297
/// This does nothing if the image data is not already initialized
1298
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
/// Read the color of a specific pixel (1D texture).
1322
///
1323
/// See [`get_color_at`](Self::get_color_at) for more details.
1324
#[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
/// Read the color of a specific pixel (2D texture).
1333
///
1334
/// This function will find the raw byte data of a specific pixel and
1335
/// decode it into a user-friendly [`Color`] struct for you.
1336
///
1337
/// Supports many of the common [`TextureFormat`]s:
1338
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1339
/// - 16-bit and 32-bit unsigned integer
1340
/// - 16-bit and 32-bit float
1341
///
1342
/// Be careful: as the data is converted to [`Color`] (which uses `f32` internally),
1343
/// there may be issues with precision when using non-f32 [`TextureFormat`]s.
1344
/// If you read a value you previously wrote using `set_color_at`, it will not match.
1345
/// If you are working with a 32-bit integer [`TextureFormat`], the value will be
1346
/// inaccurate (as `f32` does not have enough bits to represent it exactly).
1347
///
1348
/// Single channel (R) formats are assumed to represent grayscale, so the value
1349
/// will be copied to all three RGB channels in the resulting [`Color`].
1350
///
1351
/// Other [`TextureFormat`]s are unsupported, such as:
1352
/// - block-compressed formats
1353
/// - non-byte-aligned formats like 10-bit
1354
/// - signed integer formats
1355
#[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
/// Read the color of a specific pixel (2D texture with layers or 3D texture).
1364
///
1365
/// See [`get_color_at`](Self::get_color_at) for more details.
1366
#[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
/// Change the color of a specific pixel (1D texture).
1380
///
1381
/// See [`set_color_at`](Self::set_color_at) for more details.
1382
#[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
/// Change the color of a specific pixel (2D texture).
1391
///
1392
/// This function will find the raw byte data of a specific pixel and
1393
/// change it according to a [`Color`] you provide. The [`Color`] struct
1394
/// will be encoded into the [`Image`]'s [`TextureFormat`].
1395
///
1396
/// Supports many of the common [`TextureFormat`]s:
1397
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
1398
/// - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`)
1399
/// - 16-bit and 32-bit float
1400
///
1401
/// Be careful: writing to non-f32 [`TextureFormat`]s is lossy! The data has to be converted,
1402
/// so if you read it back using `get_color_at`, the `Color` you get will not equal the value
1403
/// you used when writing it using this function.
1404
///
1405
/// For R and RG formats, only the respective values from the linear RGB [`Color`] will be used.
1406
///
1407
/// Other [`TextureFormat`]s are unsupported, such as:
1408
/// - block-compressed formats
1409
/// - non-byte-aligned formats like 10-bit
1410
/// - signed integer formats
1411
#[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
/// Change the color of a specific pixel (2D texture with layers or 3D texture).
1420
///
1421
/// See [`set_color_at`](Self::set_color_at) for more details.
1422
#[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
// NOTE: GPUs are always Little Endian.
1452
// Make sure to respect that when we create color values from bytes.
1453
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
// going via f64 to avoid rounding errors with large numbers and division
1499
(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
// going via f64 to avoid rounding errors with large numbers and division
1514
(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
// assume R-only texture format means grayscale (linear)
1521
// copy value to all of RGB in Color
1522
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
// going via f64 to avoid rounding errors with large numbers and division
1529
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
// going via f64 to avoid rounding errors with large numbers and division
1535
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
// going via f64 to avoid rounding errors with large numbers and division
1555
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
// going via f64 to avoid rounding errors with large numbers and division
1563
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
// NOTE: GPUs are always Little Endian.
1600
// Make sure to respect that when we convert color values to bytes.
1601
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
// Convert to grayscale with minimal loss if color is already gray
1672
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
// Convert to grayscale with minimal loss if color is already gray
1679
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
// Convert to grayscale with minimal loss if color is already gray
1687
let linear = LinearRgba::from(color);
1688
let luminance = Xyza::from(linear).y;
1689
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
1690
// go via f64 to avoid imprecision
1691
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
// Convert to grayscale with minimal loss if color is already gray
1696
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
// Convert to grayscale with minimal loss if color is already gray
1704
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
// go via f64 to avoid imprecision
1724
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)]
1750
pub enum DataFormat {
1751
Rgb,
1752
Rgba,
1753
Rrr,
1754
Rrrg,
1755
Rg,
1756
}
1757
1758
/// Texture data need to be transcoded from this format for use with `wgpu`.
1759
#[derive(Clone, Copy, Debug)]
1760
pub enum TranscodeFormat {
1761
Etc1s,
1762
Uastc(DataFormat),
1763
/// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`.
1764
R8UnormSrgb,
1765
/// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`.
1766
Rg8UnormSrgb,
1767
/// Has to be transcoded from `Rgb8` to `Rgba8` for use with `wgpu`.
1768
Rgb8,
1769
}
1770
1771
/// An error that occurs when accessing specific pixels in a texture.
1772
#[derive(Error, Debug)]
1773
pub 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
/// An error that occurs when loading a texture.
1783
#[derive(Error, Debug)]
1784
pub enum TextureError {
1785
/// Image MIME type is invalid.
1786
#[error("invalid image mime type: {0}")]
1787
InvalidImageMimeType(String),
1788
/// Image extension is invalid.
1789
#[error("invalid image extension: {0}")]
1790
InvalidImageExtension(String),
1791
/// Failed to load an image.
1792
#[error("failed to load an image: {0}")]
1793
ImageError(#[from] image::ImageError),
1794
/// Texture format isn't supported.
1795
#[error("unsupported texture format: {0}")]
1796
UnsupportedTextureFormat(String),
1797
/// Supercompression isn't supported.
1798
#[error("supercompression not supported: {0}")]
1799
SuperCompressionNotSupported(String),
1800
/// Failed to decompress an image.
1801
#[error("failed to decompress an image: {0}")]
1802
SuperDecompressionError(String),
1803
/// Invalid data.
1804
#[error("invalid data: {0}")]
1805
InvalidData(String),
1806
/// Transcode error.
1807
#[error("transcode error: {0}")]
1808
TranscodeError(String),
1809
/// Format requires transcoding.
1810
#[error("format requires transcoding: {0:?}")]
1811
FormatRequiresTranscodingError(TranscodeFormat),
1812
/// Only cubemaps with six faces are supported.
1813
#[error("only cubemaps with six faces are supported")]
1814
IncompleteCubemap,
1815
}
1816
1817
/// The type of a raw image buffer.
1818
#[derive(Debug)]
1819
pub enum ImageType<'a> {
1820
/// The mime type of an image, for example `"image/png"`.
1821
MimeType(&'a str),
1822
/// The extension of an image file, for example `"png"`.
1823
Extension(&'a str),
1824
/// The direct format of the image
1825
Format(ImageFormat),
1826
}
1827
1828
impl<'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
1840
/// Used to calculate the volume of an item.
1841
pub trait Volume {
1842
fn volume(&self) -> usize;
1843
}
1844
1845
impl Volume for Extent3d {
1846
/// Calculates the volume of the [`Extent3d`].
1847
fn volume(&self) -> usize {
1848
(self.width * self.height * self.depth_or_array_layers) as usize
1849
}
1850
}
1851
1852
/// Extends the wgpu [`TextureFormat`] with information about the pixel.
1853
pub trait TextureFormatPixelInfo {
1854
/// Returns the size of a pixel in bytes of the format.
1855
/// error with `TextureAccessError::UnsupportedTextureFormat` if the format is compressed.
1856
fn pixel_size(&self) -> Result<usize, TextureAccessError>;
1857
}
1858
1859
impl 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
1869
bitflags::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
1880
impl 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
/// For defining which compressed image formats are supported. This will be initialized from available device features
1928
/// in `finish()` of the bevy `RenderPlugin`, but is left for the user to specify if not using the `RenderPlugin`, or
1929
/// the WGPU backend.
1930
#[derive(Resource)]
1931
pub struct CompressedImageFormatSupport(pub CompressedImageFormats);
1932
1933
#[cfg(test)]
1934
mod 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
// Create a test pattern
2029
2030
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
// Grow image
2041
image.resize_in_place(Extent3d {
2042
width: 4,
2043
height: 4,
2044
depth_or_array_layers: 1,
2045
});
2046
2047
// After growing, the test pattern should be the same.
2048
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
// Pixels in the newly added area should get filled with zeroes.
2060
assert!(matches!(
2061
image.get_color_at(3, 3),
2062
Ok(Color::LinearRgba(GROW_FILL))
2063
));
2064
2065
// Shrink
2066
image.resize_in_place(Extent3d {
2067
width: 1,
2068
height: 1,
2069
depth_or_array_layers: 1,
2070
});
2071
2072
// Images outside of the new dimensions should be clipped
2073
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
// Create a test pattern
2097
2098
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
// Grow image
2113
image.resize_in_place(Extent3d {
2114
width: 4,
2115
height: 4,
2116
depth_or_array_layers: LAYERS + 1,
2117
});
2118
2119
// After growing, the test pattern should be the same.
2120
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
// Pixels in the newly added area should get filled with zeroes.
2134
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
// Shrink
2142
image.resize_in_place(Extent3d {
2143
width: 1,
2144
height: 1,
2145
depth_or_array_layers: 1,
2146
});
2147
2148
// Images outside of the new dimensions should be clipped
2149
assert!(image.get_color_at_3d(1, 1, 0).is_err());
2150
2151
// Higher layers should no longer be present
2152
assert!(image.get_color_at_3d(0, 0, 1).is_err());
2153
2154
// Grow layers
2155
image.resize_in_place(Extent3d {
2156
width: 1,
2157
height: 1,
2158
depth_or_array_layers: 2,
2159
});
2160
2161
// Pixels in the newly added layer should be zeroes.
2162
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
}
2189
2190