Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/large_scenes/mipmap_generator/src/lib.rs
9332 views
1
#[cfg(feature = "compress")]
2
use std::{
3
fs::{self, File},
4
hash::{DefaultHasher, Hash, Hasher},
5
io::{Read, Write},
6
path::Path,
7
};
8
9
use anyhow::anyhow;
10
use fast_image_resize::{ResizeAlg, ResizeOptions, Resizer};
11
use tracing::warn;
12
13
use bevy::{
14
asset::RenderAssetUsages,
15
image::{ImageSampler, ImageSamplerDescriptor},
16
pbr::{ExtendedMaterial, MaterialExtension},
17
platform::collections::HashMap,
18
prelude::*,
19
render::render_resource::{Extent3d, TextureDataOrder, TextureDimension, TextureFormat},
20
tasks::{AsyncComputeTaskPool, Task},
21
};
22
use futures_lite::future;
23
use image::{imageops::FilterType, DynamicImage, ImageBuffer};
24
25
#[derive(Resource, Deref)]
26
pub struct DefaultSampler(ImageSamplerDescriptor);
27
28
#[derive(Resource, Clone)]
29
pub struct MipmapGeneratorSettings {
30
/// Valid values: 1, 2, 4, 8, and 16.
31
pub anisotropic_filtering: u16,
32
pub filter_type: FilterType,
33
pub minimum_mip_resolution: u32,
34
/// Set to Some(CompressionSpeed) to enable compression.
35
/// The compress feature also needs to be enabled. Only BCn currently supported.
36
/// Compression can take a long time, CompressionSpeed::UltraFast (default) is recommended.
37
/// Currently supported conversions:
38
///- R8Unorm -> Bc4RUnorm
39
///- Rg8Unorm -> Bc5RgUnorm
40
///- Rgba8Unorm -> Bc7RgbaUnorm
41
///- Rgba8UnormSrgb -> Bc7RgbaUnormSrgb
42
pub compression: Option<CompressionSpeed>,
43
/// If set, raw compressed image data will be cached in this directory.
44
/// Images that are not BCn compressed are not cached.
45
pub compressed_image_data_cache_path: Option<std::path::PathBuf>,
46
/// If low_quality is set, only 0.5 byte/px formats will be used (BC1, BC4) unless the alpha channel is in use, then BC3 will be used.
47
/// When low quality is set, compression is generally faster than CompressionSpeed::UltraFast and CompressionSpeed is ignored.
48
// TODO: low_quality normals should probably use BC5 or BC7 as they looks quite bad at BC1
49
pub low_quality: bool,
50
}
51
52
impl Default for MipmapGeneratorSettings {
53
fn default() -> Self {
54
Self {
55
// Default to 8x anisotropic filtering
56
anisotropic_filtering: 8,
57
filter_type: FilterType::Triangle,
58
minimum_mip_resolution: 1,
59
compression: None,
60
compressed_image_data_cache_path: None,
61
low_quality: false,
62
}
63
}
64
}
65
66
#[derive(Default, Clone, Copy, Hash)]
67
pub enum CompressionSpeed {
68
#[default]
69
UltraFast,
70
VeryFast,
71
Fast,
72
Medium,
73
Slow,
74
}
75
76
impl CompressionSpeed {
77
#[cfg(feature = "compress")]
78
fn get_bc7_encoder(&self, has_alpha: bool) -> intel_tex_2::bc7::EncodeSettings {
79
if has_alpha {
80
match self {
81
CompressionSpeed::UltraFast => intel_tex_2::bc7::alpha_ultra_fast_settings(),
82
CompressionSpeed::VeryFast => intel_tex_2::bc7::alpha_very_fast_settings(),
83
CompressionSpeed::Fast => intel_tex_2::bc7::alpha_fast_settings(),
84
CompressionSpeed::Medium => intel_tex_2::bc7::alpha_basic_settings(),
85
CompressionSpeed::Slow => intel_tex_2::bc7::alpha_slow_settings(),
86
}
87
} else {
88
match self {
89
CompressionSpeed::UltraFast => intel_tex_2::bc7::opaque_ultra_fast_settings(),
90
CompressionSpeed::VeryFast => intel_tex_2::bc7::opaque_very_fast_settings(),
91
CompressionSpeed::Fast => intel_tex_2::bc7::opaque_fast_settings(),
92
CompressionSpeed::Medium => intel_tex_2::bc7::opaque_basic_settings(),
93
CompressionSpeed::Slow => intel_tex_2::bc7::opaque_slow_settings(),
94
}
95
}
96
}
97
}
98
99
///Mipmaps will not be generated for materials found on entities that also have the `NoMipmapGeneration` component.
100
#[derive(Component)]
101
pub struct NoMipmapGeneration;
102
103
#[derive(Resource, Default)]
104
pub struct MipmapGenerationProgress {
105
pub processed: u32,
106
pub total: u32,
107
/// Tracks the amount of bytes that have been cached since startup.
108
/// Used to warn at 1GB increments to avoid continuously caching images that change every frame.
109
pub cached_data_size_bytes: usize,
110
}
111
112
fn format_bytes_size(size_in_bytes: usize) -> String {
113
if size_in_bytes < 1_000 {
114
format!("{}B", size_in_bytes)
115
} else if size_in_bytes < 1_000_000 {
116
format!("{:.2}KB", size_in_bytes as f64 / 1e3)
117
} else if size_in_bytes < 1_000_000_000 {
118
format!("{:.2}MB", size_in_bytes as f64 / 1e6)
119
} else {
120
format!("{:.2}GB", size_in_bytes as f64 / 1e9)
121
}
122
}
123
124
pub struct MipmapGeneratorPlugin;
125
impl Plugin for MipmapGeneratorPlugin {
126
fn build(&self, app: &mut App) {
127
if let Some(image_plugin) = app
128
.init_resource::<MipmapGenerationProgress>()
129
.get_added_plugins::<ImagePlugin>()
130
.first()
131
{
132
let default_sampler = image_plugin.default_sampler.clone();
133
app.insert_resource(DefaultSampler(default_sampler))
134
.init_resource::<MipmapGeneratorSettings>();
135
} else {
136
warn!("No ImagePlugin found. Try adding MipmapGeneratorPlugin after DefaultPlugins");
137
}
138
}
139
}
140
141
#[derive(Clone, Resource)]
142
#[cfg(feature = "debug_text")]
143
pub struct MipmapGeneratorDebugTextPlugin;
144
#[cfg(feature = "debug_text")]
145
impl Plugin for MipmapGeneratorDebugTextPlugin {
146
fn build(&self, app: &mut App) {
147
app.insert_resource(self.clone())
148
.add_systems(Startup, init_loading_text)
149
.add_systems(Update, update_loading_text);
150
}
151
}
152
153
#[cfg(feature = "debug_text")]
154
fn init_loading_text(mut commands: Commands) {
155
commands
156
.spawn((
157
Node {
158
left: px(1.5),
159
top: px(1.5),
160
..default()
161
},
162
GlobalZIndex(-1),
163
))
164
.with_children(|parent| {
165
parent.spawn((
166
Text::new(""),
167
TextFont {
168
font_size: FontSize::Px(18.0),
169
..default()
170
},
171
TextColor(Color::BLACK),
172
MipmapGeneratorDebugLoadingText,
173
));
174
});
175
commands.spawn(Node::default()).with_children(|parent| {
176
parent.spawn((
177
Text::new(""),
178
TextFont {
179
font_size: FontSize::Px(18.0),
180
..default()
181
},
182
TextColor(Color::WHITE),
183
MipmapGeneratorDebugLoadingText,
184
));
185
});
186
}
187
188
#[cfg(feature = "debug_text")]
189
#[derive(Component)]
190
pub struct MipmapGeneratorDebugLoadingText;
191
#[cfg(feature = "debug_text")]
192
fn update_loading_text(
193
mut texts: Query<(&mut Text, &mut TextColor), With<MipmapGeneratorDebugLoadingText>>,
194
progress: Res<MipmapGenerationProgress>,
195
time: Res<Time>,
196
) {
197
for (mut text, mut color) in &mut texts {
198
text.0 = format!(
199
"bevy_mod_mipmap_generator progress: {} / {}\n{}",
200
progress.processed,
201
progress.total,
202
if progress.cached_data_size_bytes > 0 {
203
format!(
204
"Cached this run: {}",
205
format_bytes_size(progress.cached_data_size_bytes)
206
)
207
} else {
208
String::new()
209
}
210
);
211
let alpha = if progress.processed == progress.total {
212
(color.0.alpha() - time.delta_secs() * 0.25).max(0.0)
213
} else {
214
1.0
215
};
216
color.0.set_alpha(alpha);
217
}
218
}
219
220
pub struct TaskData {
221
added_cache_size: usize,
222
image: Image,
223
}
224
225
#[derive(Resource, Default, Deref, DerefMut)]
226
#[allow(clippy::type_complexity)]
227
pub struct MipmapTasks<M: Material + GetImages>(
228
HashMap<Handle<Image>, (Task<TaskData>, Vec<AssetId<M>>)>,
229
);
230
231
#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)]
232
pub struct MaterialHandle<M: Material + GetImages>(pub Handle<M>);
233
234
#[allow(clippy::too_many_arguments)]
235
pub fn generate_mipmaps<M: Material + GetImages>(
236
mut commands: Commands,
237
mut material_events: MessageReader<AssetEvent<M>>,
238
mut materials: ResMut<Assets<M>>,
239
no_mipmap: Query<&MaterialHandle<M>, With<NoMipmapGeneration>>,
240
mut images: ResMut<Assets<Image>>,
241
default_sampler: Res<DefaultSampler>,
242
mut progress: ResMut<MipmapGenerationProgress>,
243
settings: Res<MipmapGeneratorSettings>,
244
mut tasks_res: Option<ResMut<MipmapTasks<M>>>,
245
) {
246
let mut new_tasks = MipmapTasks(HashMap::new());
247
248
let tasks = if let Some(ref mut tasks) = tasks_res {
249
tasks
250
} else {
251
&mut new_tasks
252
};
253
254
let thread_pool = AsyncComputeTaskPool::get();
255
'outer: for event in material_events.read() {
256
let material_h = match event {
257
AssetEvent::Added { id } => id,
258
AssetEvent::LoadedWithDependencies { id } => id,
259
_ => continue,
260
};
261
for m in no_mipmap.iter() {
262
if m.id() == *material_h {
263
continue 'outer;
264
}
265
}
266
// get_mut(material_h) here so we see the filtering right away
267
// and even if mipmaps aren't made, we still get the filtering
268
if let Some(material) = materials.get_mut(*material_h) {
269
for image_h in material.get_images().into_iter() {
270
if let Some((_, material_handles)) = tasks.get_mut(image_h) {
271
material_handles.push(*material_h);
272
continue; //There is already a task for this image
273
}
274
if let Some(image) = images.get_mut(image_h) {
275
let mut descriptor = match image.sampler.clone() {
276
ImageSampler::Default => default_sampler.0.clone(),
277
ImageSampler::Descriptor(descriptor) => descriptor,
278
};
279
descriptor.anisotropy_clamp = settings.anisotropic_filtering;
280
image.sampler = ImageSampler::Descriptor(descriptor);
281
if image.texture_descriptor.mip_level_count == 1
282
&& check_image_compatible(image).is_ok()
283
{
284
let mut image = image.clone();
285
let settings = settings.clone();
286
let mut added_cache_size = 0;
287
let task = thread_pool.spawn(async move {
288
match generate_mips_texture(
289
&mut image,
290
&settings.clone(),
291
&mut added_cache_size,
292
) {
293
Ok(_) => (),
294
Err(e) => warn!("{}", e),
295
}
296
TaskData {
297
added_cache_size,
298
image,
299
}
300
});
301
tasks.insert(image_h.clone(), (task, vec![*material_h]));
302
progress.total += 1;
303
}
304
}
305
}
306
}
307
}
308
309
fn bytes_to_gb(bytes: usize) -> usize {
310
bytes / 1024_usize.pow(3)
311
}
312
313
tasks.retain(|image_h, (task, material_handles)| {
314
match future::block_on(future::poll_once(task)) {
315
Some(task_data) => {
316
if let Some(image) = images.get_mut(image_h) {
317
*image = task_data.image;
318
progress.processed += 1;
319
let prev_cached_data_gb = bytes_to_gb(progress.cached_data_size_bytes);
320
progress.cached_data_size_bytes += task_data.added_cache_size;
321
let current_cached_data_gb = bytes_to_gb(progress.cached_data_size_bytes);
322
if current_cached_data_gb > prev_cached_data_gb {
323
warn!(
324
"Generated cached texture data from just this run is {}",
325
format_bytes_size(progress.cached_data_size_bytes)
326
);
327
}
328
// Touch material to trigger change detection
329
for material_h in material_handles.iter() {
330
let _ = materials.get_mut(*material_h);
331
}
332
}
333
false
334
}
335
None => true,
336
}
337
});
338
339
if tasks_res.is_none() {
340
commands.insert_resource(new_tasks);
341
}
342
}
343
344
/// `added_cache_size` is for tracking the amount of data that was cached by this call.
345
/// Compressed BCn data is cached on disk if cache_compressed_image_data is enabled.
346
pub fn generate_mips_texture(
347
image: &mut Image,
348
settings: &MipmapGeneratorSettings,
349
#[allow(unused)] added_cache_size: &mut usize,
350
) -> anyhow::Result<()> {
351
check_image_compatible(image)?;
352
match try_into_dynamic(image.clone()) {
353
Ok(mut dyn_image) => {
354
#[allow(unused_mut)]
355
let mut has_alpha = false;
356
#[cfg(feature = "compress")]
357
if let Some(img) = dyn_image.as_rgba8() {
358
for px in img.pixels() {
359
if px.0[3] != 255 {
360
has_alpha = true;
361
break;
362
}
363
}
364
}
365
366
#[cfg(feature = "compress")]
367
let mut compressed_format = None;
368
#[allow(unused_mut)]
369
let mut compression_speed = settings.compression;
370
#[cfg(feature = "compress")]
371
{
372
if let Some(encoder_setting) = settings.compression {
373
compressed_format = bcn_equivalent_format_of_dyn_image(
374
&dyn_image,
375
image.texture_descriptor.format.is_srgb(),
376
settings.low_quality,
377
has_alpha,
378
)
379
.ok();
380
compression_speed = compressed_format.map(|_| encoder_setting);
381
}
382
}
383
384
#[cfg(feature = "compress")]
385
let mut input_hash = u64::MAX;
386
#[allow(unused_mut)]
387
let mut loaded_from_cache = false;
388
let mut new_image_data = Vec::new();
389
390
#[cfg(feature = "compress")]
391
if compression_speed.is_some()
392
&& compressed_format.is_some()
393
&& let Some(cache_path) = &settings.compressed_image_data_cache_path
394
{
395
input_hash = calculate_hash(image, settings);
396
if let Some(compressed_image_data) = load_from_cache(input_hash, cache_path) {
397
new_image_data = compressed_image_data;
398
loaded_from_cache = true;
399
}
400
}
401
402
let mip_count = calculate_mip_count(
403
dyn_image.width(),
404
dyn_image.height(),
405
settings.minimum_mip_resolution,
406
u32::MAX,
407
compression_speed,
408
);
409
410
if !loaded_from_cache {
411
new_image_data = generate_mips(&mut dyn_image, has_alpha, mip_count, settings);
412
#[cfg(feature = "compress")]
413
if let Some(cache_path) = &settings.compressed_image_data_cache_path
414
&& compression_speed.is_some()
415
&& compressed_format.is_some()
416
{
417
*added_cache_size += new_image_data.len();
418
save_to_cache(input_hash, &new_image_data, cache_path).unwrap();
419
}
420
}
421
422
image.texture_descriptor.mip_level_count = mip_count;
423
#[cfg(feature = "compress")]
424
if let Some(format) = compressed_format {
425
image.texture_descriptor.format = format;
426
// Remove view formats for compressed textures.
427
// TODO Is this an issue? A bit difficult to work around since it's &['static]
428
image.texture_descriptor.view_formats = &[];
429
}
430
431
image.data = Some(new_image_data);
432
Ok(())
433
}
434
Err(e) => Err(e),
435
}
436
}
437
438
/// Returns a vec of bytes containing the image data for all generated mips.
439
/// Use `calculate_mip_count()` to find the value for `mip_count`.
440
pub fn generate_mips(
441
dyn_image: &mut DynamicImage,
442
has_alpha: bool,
443
mip_count: u32,
444
settings: &MipmapGeneratorSettings,
445
) -> Vec<u8> {
446
let mut width = dyn_image.width();
447
let mut height = dyn_image.height();
448
449
#[allow(unused_mut)]
450
let mut compressed_image_data = None;
451
#[cfg(feature = "compress")]
452
if let Some(compression_settings) = settings.compression {
453
compressed_image_data = bcn_compress_dyn_image(
454
compression_settings,
455
dyn_image,
456
has_alpha,
457
settings.low_quality,
458
)
459
.ok();
460
}
461
462
#[cfg(not(feature = "compress"))]
463
if settings.compression.is_some() {
464
warn!("Compression is Some but compress feature is disabled. Falling back to generating mips without compression.")
465
}
466
467
let mut image_data = compressed_image_data.unwrap_or(dyn_image.as_bytes().to_vec());
468
469
#[cfg(feature = "compress")]
470
let min = if settings.compression.is_some() { 4 } else { 1 };
471
#[cfg(not(feature = "compress"))]
472
let min = 1;
473
474
let mut resizer = Resizer::new();
475
476
let resize_alg = ResizeOptions::new()
477
.resize_alg(match settings.filter_type {
478
FilterType::Nearest => ResizeAlg::Nearest,
479
FilterType::Triangle => ResizeAlg::Convolution(fast_image_resize::FilterType::Bilinear),
480
FilterType::CatmullRom => {
481
ResizeAlg::Convolution(fast_image_resize::FilterType::CatmullRom)
482
}
483
FilterType::Gaussian => ResizeAlg::Convolution(fast_image_resize::FilterType::Gaussian),
484
FilterType::Lanczos3 => ResizeAlg::Convolution(fast_image_resize::FilterType::Lanczos3),
485
})
486
.use_alpha(has_alpha);
487
488
for _ in 0..mip_count {
489
width /= 2;
490
height /= 2;
491
492
// *dyn_image = dyn_image.resize_exact(width, height, settings.filter_type); // Ex: Resizing with Image crate
493
494
let mut new = DynamicImage::new(width, height, dyn_image.color());
495
resizer.resize(dyn_image, &mut new, &resize_alg).unwrap();
496
*dyn_image = new;
497
498
#[allow(unused_mut)]
499
let mut compressed_image_data = None;
500
#[cfg(feature = "compress")]
501
if let Some(compression_speed) = settings.compression {
502
// https://github.com/bevyengine/bevy/issues/21490
503
if width >= 4 && height >= 4 {
504
compressed_image_data = bcn_compress_dyn_image(
505
compression_speed,
506
dyn_image,
507
has_alpha,
508
settings.low_quality,
509
)
510
.ok();
511
}
512
}
513
image_data.append(&mut compressed_image_data.unwrap_or(dyn_image.as_bytes().to_vec()));
514
if width <= min || height <= min {
515
break;
516
}
517
}
518
519
image_data
520
}
521
522
/// Returns the number of mip levels
523
/// The `max_mip_count` includes the first input mip level. So setting this to 2 will
524
/// result in a single additional mip level being generated, for a total of 2 levels.
525
pub fn calculate_mip_count(
526
mut width: u32,
527
mut height: u32,
528
minimum_mip_resolution: u32,
529
max_mip_count: u32,
530
#[allow(unused)] compression: Option<CompressionSpeed>,
531
) -> u32 {
532
let mut mip_level_count = 1;
533
534
#[cfg(feature = "compress")]
535
let min = if compression.is_some() { 4 } else { 1 };
536
#[cfg(not(feature = "compress"))]
537
let min = 1;
538
539
// Use log to avoid loop? Are there edge cases with rounding?
540
541
while width / 2 >= minimum_mip_resolution.max(min)
542
&& height / 2 >= minimum_mip_resolution.max(min)
543
&& mip_level_count < max_mip_count
544
{
545
width /= 2;
546
height /= 2;
547
mip_level_count += 1;
548
}
549
550
mip_level_count
551
}
552
553
/// Extract a specific individual mip level as a new image.
554
pub fn extract_mip_level(image: &Image, mip_level: u32) -> anyhow::Result<Image> {
555
check_image_compatible(image)?;
556
557
let descriptor = &image.texture_descriptor;
558
559
if descriptor.mip_level_count < mip_level {
560
return Err(anyhow!(
561
"Mip level {mip_level} requested, but only {} are available.",
562
descriptor.mip_level_count
563
));
564
}
565
566
let block_size = descriptor.format.block_copy_size(None).unwrap() as usize;
567
568
//let mip_factor = 2u32.pow(mip_level - 1);
569
//let final_width = descriptor.size.width/mip_factor;
570
//let final_height = descriptor.size.height/mip_factor;
571
572
let mut width = descriptor.size.width as usize;
573
let mut height = descriptor.size.height as usize;
574
575
let mut byte_offset = 0usize;
576
577
for _ in 0..mip_level - 1 {
578
byte_offset += width * block_size * height;
579
width /= 2;
580
height /= 2;
581
}
582
583
let mut new_descriptor = descriptor.clone();
584
585
new_descriptor.mip_level_count = 1;
586
new_descriptor.size = Extent3d {
587
width: width as u32,
588
height: height as u32,
589
depth_or_array_layers: 1,
590
};
591
592
Ok(Image {
593
data: image
594
.data
595
.as_ref()
596
.map(|data| data[byte_offset..byte_offset + (width * block_size * height)].to_vec()),
597
data_order: TextureDataOrder::default(),
598
texture_descriptor: new_descriptor,
599
sampler: image.sampler.clone(),
600
texture_view_descriptor: image.texture_view_descriptor.clone(),
601
asset_usage: RenderAssetUsages::default(),
602
copy_on_resize: false,
603
})
604
}
605
606
pub fn check_image_compatible(image: &Image) -> anyhow::Result<()> {
607
if image.data.is_none() {
608
return Err(anyhow!(
609
"Image is a GPU storage texture which is not supported."
610
));
611
}
612
613
if image.is_compressed() {
614
return Err(anyhow!("Compressed images not supported"));
615
}
616
617
let descriptor = &image.texture_descriptor;
618
619
if descriptor.dimension != TextureDimension::D2 {
620
return Err(anyhow!(
621
"Image has dimension {:?} but only TextureDimension::D2 is supported.",
622
descriptor.dimension
623
));
624
}
625
626
if descriptor.size.depth_or_array_layers != 1 {
627
return Err(anyhow!(
628
"Image contains {} layers only a single layer is supported.",
629
descriptor.size.depth_or_array_layers
630
));
631
}
632
633
Ok(())
634
}
635
636
// Implement the GetImages trait for any materials that need conversion
637
pub trait GetImages {
638
fn get_images(&self) -> Vec<&Handle<Image>>;
639
}
640
641
impl GetImages for StandardMaterial {
642
fn get_images(&self) -> Vec<&Handle<Image>> {
643
vec![
644
&self.base_color_texture,
645
&self.emissive_texture,
646
&self.metallic_roughness_texture,
647
&self.normal_map_texture,
648
&self.occlusion_texture,
649
]
650
.into_iter()
651
.flatten()
652
.collect()
653
}
654
}
655
656
impl<T: GetImages + MaterialExtension> GetImages for ExtendedMaterial<StandardMaterial, T> {
657
fn get_images(&self) -> Vec<&Handle<Image>> {
658
let mut images: Vec<&Handle<Image>> = vec![
659
&self.base.base_color_texture,
660
&self.base.emissive_texture,
661
&self.base.metallic_roughness_texture,
662
&self.base.normal_map_texture,
663
&self.base.occlusion_texture,
664
&self.base.depth_map,
665
#[cfg(feature = "pbr_transmission_textures")]
666
&self.base.diffuse_transmission_texture,
667
#[cfg(feature = "pbr_transmission_textures")]
668
&self.base.specular_transmission_texture,
669
#[cfg(feature = "pbr_transmission_textures")]
670
&self.base.thickness_texture,
671
#[cfg(feature = "pbr_multi_layer_material_textures")]
672
&self.base.clearcoat_texture,
673
#[cfg(feature = "pbr_multi_layer_material_textures")]
674
&self.base.clearcoat_roughness_texture,
675
#[cfg(feature = "pbr_multi_layer_material_textures")]
676
&self.base.clearcoat_normal_texture,
677
#[cfg(feature = "pbr_anisotropy_texture")]
678
&self.base.anisotropy_texture,
679
#[cfg(feature = "pbr_specular_textures")]
680
&self.base.specular_texture,
681
#[cfg(feature = "pbr_specular_textures")]
682
&self.base.specular_tint_texture,
683
]
684
.into_iter()
685
.flatten()
686
.collect();
687
images.append(&mut self.extension.get_images());
688
images
689
}
690
}
691
692
pub fn try_into_dynamic(image: Image) -> anyhow::Result<DynamicImage> {
693
let Some(image_data) = image.data else {
694
return Err(anyhow!(
695
"Conversion into dynamic image not supported for GPU storage texture."
696
));
697
};
698
699
match image.texture_descriptor.format {
700
TextureFormat::R8Unorm => ImageBuffer::from_raw(
701
image.texture_descriptor.size.width,
702
image.texture_descriptor.size.height,
703
image_data,
704
)
705
.map(DynamicImage::ImageLuma8),
706
TextureFormat::Rg8Unorm => ImageBuffer::from_raw(
707
image.texture_descriptor.size.width,
708
image.texture_descriptor.size.height,
709
image_data,
710
)
711
.map(DynamicImage::ImageLumaA8),
712
TextureFormat::Rgba8UnormSrgb => ImageBuffer::from_raw(
713
image.texture_descriptor.size.width,
714
image.texture_descriptor.size.height,
715
image_data,
716
)
717
.map(DynamicImage::ImageRgba8),
718
TextureFormat::Rgba8Unorm => ImageBuffer::from_raw(
719
image.texture_descriptor.size.width,
720
image.texture_descriptor.size.height,
721
image_data,
722
)
723
.map(DynamicImage::ImageRgba8),
724
// Throw and error if conversion isn't supported
725
texture_format => {
726
return Err(anyhow!(
727
"Conversion into dynamic image not supported for {:?}.",
728
texture_format
729
))
730
}
731
}
732
.ok_or_else(|| {
733
anyhow!(
734
"Failed to convert into {:?}.",
735
image.texture_descriptor.format
736
)
737
})
738
}
739
740
#[cfg(feature = "compress")]
741
fn bcn_compress_dyn_image(
742
compression_speed: CompressionSpeed,
743
dyn_image: &DynamicImage,
744
has_alpha: bool,
745
low_quality: bool,
746
) -> anyhow::Result<Vec<u8>> {
747
use image::Rgba;
748
749
let width = dyn_image.width();
750
let height = dyn_image.height();
751
let mut image_data;
752
if low_quality {
753
match dyn_image {
754
DynamicImage::ImageLuma8(data) => {
755
image_data = vec![0u8; intel_tex_2::bc4::calc_output_size(width, height)];
756
let surface = intel_tex_2::RSurface {
757
width,
758
height,
759
stride: width,
760
data,
761
};
762
intel_tex_2::bc4::compress_blocks_into(&surface, &mut image_data);
763
}
764
DynamicImage::ImageLumaA8(data) => {
765
let mut rgba =
766
ImageBuffer::<Rgba<u8>, Vec<u8>>::new(dyn_image.width(), dyn_image.height());
767
for (rgba_px, rg_px) in rgba.pixels_mut().zip(data.pixels()) {
768
rgba_px.0[0] = rg_px.0[0];
769
rgba_px.0[1] = rg_px.0[1];
770
}
771
image_data = vec![0u8; intel_tex_2::bc1::calc_output_size(width, height)];
772
let surface = intel_tex_2::RgbaSurface {
773
width,
774
height,
775
stride: width * 4,
776
data: rgba.as_raw(),
777
};
778
intel_tex_2::bc1::compress_blocks_into(&surface, &mut image_data);
779
}
780
DynamicImage::ImageRgba8(data) => {
781
if has_alpha {
782
image_data = vec![0u8; intel_tex_2::bc3::calc_output_size(width, height)];
783
let surface = intel_tex_2::RgbaSurface {
784
width,
785
height,
786
stride: width * 4,
787
data,
788
};
789
intel_tex_2::bc3::compress_blocks_into(&surface, &mut image_data);
790
} else {
791
image_data = vec![0u8; intel_tex_2::bc1::calc_output_size(width, height)];
792
let surface = intel_tex_2::RgbaSurface {
793
width,
794
height,
795
stride: width * 4,
796
data,
797
};
798
intel_tex_2::bc1::compress_blocks_into(&surface, &mut image_data);
799
}
800
}
801
// Throw and error if conversion isn't supported
802
dyn_image => {
803
return Err(anyhow!(
804
"Conversion into dynamic image not supported for {:?}.",
805
dyn_image
806
))
807
}
808
};
809
} else {
810
match dyn_image {
811
DynamicImage::ImageLuma8(data) => {
812
image_data = vec![0u8; intel_tex_2::bc4::calc_output_size(width, height)];
813
let surface = intel_tex_2::RSurface {
814
width,
815
height,
816
stride: width,
817
data,
818
};
819
intel_tex_2::bc4::compress_blocks_into(&surface, &mut image_data);
820
}
821
DynamicImage::ImageLumaA8(data) => {
822
image_data = vec![0u8; intel_tex_2::bc5::calc_output_size(width, height)];
823
let surface = intel_tex_2::RgSurface {
824
width,
825
height,
826
stride: width * 2,
827
data,
828
};
829
intel_tex_2::bc5::compress_blocks_into(&surface, &mut image_data);
830
}
831
DynamicImage::ImageRgba8(data) => {
832
image_data = vec![0u8; intel_tex_2::bc7::calc_output_size(width, height)];
833
let surface = intel_tex_2::RgbaSurface {
834
width,
835
height,
836
stride: width * 4,
837
data,
838
};
839
intel_tex_2::bc7::compress_blocks_into(
840
&compression_speed.get_bc7_encoder(has_alpha),
841
&surface,
842
&mut image_data,
843
);
844
}
845
// Throw and error if conversion isn't supported
846
dyn_image => {
847
return Err(anyhow!(
848
"Conversion into dynamic image not supported for {:?}.",
849
dyn_image
850
))
851
}
852
};
853
}
854
855
Ok(image_data)
856
}
857
858
/// If low_quality is set, only 0.5 byte/px formats will be used (BC1, BC4) unless alpha is being used (BC3)
859
pub fn bcn_equivalent_format_of_dyn_image(
860
dyn_image: &DynamicImage,
861
is_srgb: bool,
862
low_quality: bool,
863
has_alpha: bool,
864
) -> anyhow::Result<TextureFormat> {
865
if dyn_image.width() < 4 || dyn_image.height() < 4 {
866
return Err(anyhow!("Image size too small for BCn compression"));
867
}
868
if low_quality {
869
match dyn_image {
870
DynamicImage::ImageLuma8(_) => Ok(TextureFormat::Bc4RUnorm),
871
DynamicImage::ImageLumaA8(_) => Ok(TextureFormat::Bc1RgbaUnorm),
872
DynamicImage::ImageRgba8(_) => Ok(if has_alpha {
873
if is_srgb {
874
TextureFormat::Bc3RgbaUnormSrgb
875
} else {
876
TextureFormat::Bc3RgbaUnorm
877
}
878
} else if is_srgb {
879
TextureFormat::Bc1RgbaUnormSrgb
880
} else {
881
TextureFormat::Bc1RgbaUnorm
882
}),
883
// Throw and error if conversion isn't supported
884
dyn_image => Err(anyhow!(
885
"Conversion into dynamic image not supported for {:?}.",
886
dyn_image
887
)),
888
}
889
} else {
890
match dyn_image {
891
DynamicImage::ImageLuma8(_) => Ok(TextureFormat::Bc4RUnorm),
892
DynamicImage::ImageLumaA8(_) => Ok(TextureFormat::Bc5RgUnorm),
893
DynamicImage::ImageRgba8(_) => Ok(if is_srgb {
894
TextureFormat::Bc7RgbaUnormSrgb
895
} else {
896
TextureFormat::Bc7RgbaUnorm
897
}),
898
// Throw and error if conversion isn't supported
899
dyn_image => Err(anyhow!(
900
"Conversion into dynamic image not supported for {:?}.",
901
dyn_image
902
)),
903
}
904
}
905
}
906
907
/// Calculate the hash for the non-compressed non-mipmapped image.
908
#[cfg(feature = "compress")]
909
fn calculate_hash(image: &Image, settings: &MipmapGeneratorSettings) -> u64 {
910
let mut hasher = DefaultHasher::new();
911
image.data.hash(&mut hasher);
912
if settings.low_quality {
913
(934870234u32).hash(&mut hasher);
914
}
915
settings.compression.hash(&mut hasher);
916
match settings.filter_type {
917
FilterType::Nearest => (934870234u32).hash(&mut hasher),
918
FilterType::Triangle => (46345624u32).hash(&mut hasher),
919
FilterType::CatmullRom => (54676234u32).hash(&mut hasher),
920
FilterType::Gaussian => (623455643u32).hash(&mut hasher),
921
FilterType::Lanczos3 => (675856584u32).hash(&mut hasher),
922
}
923
image.texture_descriptor.hash(&mut hasher);
924
hasher.finish()
925
}
926
927
/// Save raw image bytes to disk cache
928
#[cfg(feature = "compress")]
929
fn save_to_cache(hash: u64, bytes: &[u8], cache_dir: &Path) -> std::io::Result<()> {
930
if !cache_dir.exists() {
931
fs::create_dir(cache_dir)?;
932
}
933
let file_path = cache_dir.join(format!("{:x}", hash));
934
let mut file = File::create(file_path)?;
935
file.write_all(&zstd::encode_all(bytes, 0).unwrap())?;
936
Ok(())
937
}
938
939
/// Load from disk cache for matching input hash
940
#[cfg(feature = "compress")]
941
fn load_from_cache(hash: u64, cache_dir: &Path) -> Option<Vec<u8>> {
942
let file_path = cache_dir.join(format!("{:x}", hash));
943
if !file_path.exists() {
944
return None;
945
}
946
let Ok(mut file) = File::open(file_path) else {
947
return None;
948
};
949
let mut cached_bytes = Vec::new();
950
if file.read_to_end(&mut cached_bytes).is_err() {
951
return None;
952
};
953
zstd::decode_all(cached_bytes.as_slice()).ok()
954
}
955
956