Path: blob/main/examples/large_scenes/mipmap_generator/src/lib.rs
9332 views
#[cfg(feature = "compress")]1use std::{2fs::{self, File},3hash::{DefaultHasher, Hash, Hasher},4io::{Read, Write},5path::Path,6};78use anyhow::anyhow;9use fast_image_resize::{ResizeAlg, ResizeOptions, Resizer};10use tracing::warn;1112use bevy::{13asset::RenderAssetUsages,14image::{ImageSampler, ImageSamplerDescriptor},15pbr::{ExtendedMaterial, MaterialExtension},16platform::collections::HashMap,17prelude::*,18render::render_resource::{Extent3d, TextureDataOrder, TextureDimension, TextureFormat},19tasks::{AsyncComputeTaskPool, Task},20};21use futures_lite::future;22use image::{imageops::FilterType, DynamicImage, ImageBuffer};2324#[derive(Resource, Deref)]25pub struct DefaultSampler(ImageSamplerDescriptor);2627#[derive(Resource, Clone)]28pub struct MipmapGeneratorSettings {29/// Valid values: 1, 2, 4, 8, and 16.30pub anisotropic_filtering: u16,31pub filter_type: FilterType,32pub minimum_mip_resolution: u32,33/// Set to Some(CompressionSpeed) to enable compression.34/// The compress feature also needs to be enabled. Only BCn currently supported.35/// Compression can take a long time, CompressionSpeed::UltraFast (default) is recommended.36/// Currently supported conversions:37///- R8Unorm -> Bc4RUnorm38///- Rg8Unorm -> Bc5RgUnorm39///- Rgba8Unorm -> Bc7RgbaUnorm40///- Rgba8UnormSrgb -> Bc7RgbaUnormSrgb41pub compression: Option<CompressionSpeed>,42/// If set, raw compressed image data will be cached in this directory.43/// Images that are not BCn compressed are not cached.44pub compressed_image_data_cache_path: Option<std::path::PathBuf>,45/// 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.46/// When low quality is set, compression is generally faster than CompressionSpeed::UltraFast and CompressionSpeed is ignored.47// TODO: low_quality normals should probably use BC5 or BC7 as they looks quite bad at BC148pub low_quality: bool,49}5051impl Default for MipmapGeneratorSettings {52fn default() -> Self {53Self {54// Default to 8x anisotropic filtering55anisotropic_filtering: 8,56filter_type: FilterType::Triangle,57minimum_mip_resolution: 1,58compression: None,59compressed_image_data_cache_path: None,60low_quality: false,61}62}63}6465#[derive(Default, Clone, Copy, Hash)]66pub enum CompressionSpeed {67#[default]68UltraFast,69VeryFast,70Fast,71Medium,72Slow,73}7475impl CompressionSpeed {76#[cfg(feature = "compress")]77fn get_bc7_encoder(&self, has_alpha: bool) -> intel_tex_2::bc7::EncodeSettings {78if has_alpha {79match self {80CompressionSpeed::UltraFast => intel_tex_2::bc7::alpha_ultra_fast_settings(),81CompressionSpeed::VeryFast => intel_tex_2::bc7::alpha_very_fast_settings(),82CompressionSpeed::Fast => intel_tex_2::bc7::alpha_fast_settings(),83CompressionSpeed::Medium => intel_tex_2::bc7::alpha_basic_settings(),84CompressionSpeed::Slow => intel_tex_2::bc7::alpha_slow_settings(),85}86} else {87match self {88CompressionSpeed::UltraFast => intel_tex_2::bc7::opaque_ultra_fast_settings(),89CompressionSpeed::VeryFast => intel_tex_2::bc7::opaque_very_fast_settings(),90CompressionSpeed::Fast => intel_tex_2::bc7::opaque_fast_settings(),91CompressionSpeed::Medium => intel_tex_2::bc7::opaque_basic_settings(),92CompressionSpeed::Slow => intel_tex_2::bc7::opaque_slow_settings(),93}94}95}96}9798///Mipmaps will not be generated for materials found on entities that also have the `NoMipmapGeneration` component.99#[derive(Component)]100pub struct NoMipmapGeneration;101102#[derive(Resource, Default)]103pub struct MipmapGenerationProgress {104pub processed: u32,105pub total: u32,106/// Tracks the amount of bytes that have been cached since startup.107/// Used to warn at 1GB increments to avoid continuously caching images that change every frame.108pub cached_data_size_bytes: usize,109}110111fn format_bytes_size(size_in_bytes: usize) -> String {112if size_in_bytes < 1_000 {113format!("{}B", size_in_bytes)114} else if size_in_bytes < 1_000_000 {115format!("{:.2}KB", size_in_bytes as f64 / 1e3)116} else if size_in_bytes < 1_000_000_000 {117format!("{:.2}MB", size_in_bytes as f64 / 1e6)118} else {119format!("{:.2}GB", size_in_bytes as f64 / 1e9)120}121}122123pub struct MipmapGeneratorPlugin;124impl Plugin for MipmapGeneratorPlugin {125fn build(&self, app: &mut App) {126if let Some(image_plugin) = app127.init_resource::<MipmapGenerationProgress>()128.get_added_plugins::<ImagePlugin>()129.first()130{131let default_sampler = image_plugin.default_sampler.clone();132app.insert_resource(DefaultSampler(default_sampler))133.init_resource::<MipmapGeneratorSettings>();134} else {135warn!("No ImagePlugin found. Try adding MipmapGeneratorPlugin after DefaultPlugins");136}137}138}139140#[derive(Clone, Resource)]141#[cfg(feature = "debug_text")]142pub struct MipmapGeneratorDebugTextPlugin;143#[cfg(feature = "debug_text")]144impl Plugin for MipmapGeneratorDebugTextPlugin {145fn build(&self, app: &mut App) {146app.insert_resource(self.clone())147.add_systems(Startup, init_loading_text)148.add_systems(Update, update_loading_text);149}150}151152#[cfg(feature = "debug_text")]153fn init_loading_text(mut commands: Commands) {154commands155.spawn((156Node {157left: px(1.5),158top: px(1.5),159..default()160},161GlobalZIndex(-1),162))163.with_children(|parent| {164parent.spawn((165Text::new(""),166TextFont {167font_size: FontSize::Px(18.0),168..default()169},170TextColor(Color::BLACK),171MipmapGeneratorDebugLoadingText,172));173});174commands.spawn(Node::default()).with_children(|parent| {175parent.spawn((176Text::new(""),177TextFont {178font_size: FontSize::Px(18.0),179..default()180},181TextColor(Color::WHITE),182MipmapGeneratorDebugLoadingText,183));184});185}186187#[cfg(feature = "debug_text")]188#[derive(Component)]189pub struct MipmapGeneratorDebugLoadingText;190#[cfg(feature = "debug_text")]191fn update_loading_text(192mut texts: Query<(&mut Text, &mut TextColor), With<MipmapGeneratorDebugLoadingText>>,193progress: Res<MipmapGenerationProgress>,194time: Res<Time>,195) {196for (mut text, mut color) in &mut texts {197text.0 = format!(198"bevy_mod_mipmap_generator progress: {} / {}\n{}",199progress.processed,200progress.total,201if progress.cached_data_size_bytes > 0 {202format!(203"Cached this run: {}",204format_bytes_size(progress.cached_data_size_bytes)205)206} else {207String::new()208}209);210let alpha = if progress.processed == progress.total {211(color.0.alpha() - time.delta_secs() * 0.25).max(0.0)212} else {2131.0214};215color.0.set_alpha(alpha);216}217}218219pub struct TaskData {220added_cache_size: usize,221image: Image,222}223224#[derive(Resource, Default, Deref, DerefMut)]225#[allow(clippy::type_complexity)]226pub struct MipmapTasks<M: Material + GetImages>(227HashMap<Handle<Image>, (Task<TaskData>, Vec<AssetId<M>>)>,228);229230#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)]231pub struct MaterialHandle<M: Material + GetImages>(pub Handle<M>);232233#[allow(clippy::too_many_arguments)]234pub fn generate_mipmaps<M: Material + GetImages>(235mut commands: Commands,236mut material_events: MessageReader<AssetEvent<M>>,237mut materials: ResMut<Assets<M>>,238no_mipmap: Query<&MaterialHandle<M>, With<NoMipmapGeneration>>,239mut images: ResMut<Assets<Image>>,240default_sampler: Res<DefaultSampler>,241mut progress: ResMut<MipmapGenerationProgress>,242settings: Res<MipmapGeneratorSettings>,243mut tasks_res: Option<ResMut<MipmapTasks<M>>>,244) {245let mut new_tasks = MipmapTasks(HashMap::new());246247let tasks = if let Some(ref mut tasks) = tasks_res {248tasks249} else {250&mut new_tasks251};252253let thread_pool = AsyncComputeTaskPool::get();254'outer: for event in material_events.read() {255let material_h = match event {256AssetEvent::Added { id } => id,257AssetEvent::LoadedWithDependencies { id } => id,258_ => continue,259};260for m in no_mipmap.iter() {261if m.id() == *material_h {262continue 'outer;263}264}265// get_mut(material_h) here so we see the filtering right away266// and even if mipmaps aren't made, we still get the filtering267if let Some(material) = materials.get_mut(*material_h) {268for image_h in material.get_images().into_iter() {269if let Some((_, material_handles)) = tasks.get_mut(image_h) {270material_handles.push(*material_h);271continue; //There is already a task for this image272}273if let Some(image) = images.get_mut(image_h) {274let mut descriptor = match image.sampler.clone() {275ImageSampler::Default => default_sampler.0.clone(),276ImageSampler::Descriptor(descriptor) => descriptor,277};278descriptor.anisotropy_clamp = settings.anisotropic_filtering;279image.sampler = ImageSampler::Descriptor(descriptor);280if image.texture_descriptor.mip_level_count == 1281&& check_image_compatible(image).is_ok()282{283let mut image = image.clone();284let settings = settings.clone();285let mut added_cache_size = 0;286let task = thread_pool.spawn(async move {287match generate_mips_texture(288&mut image,289&settings.clone(),290&mut added_cache_size,291) {292Ok(_) => (),293Err(e) => warn!("{}", e),294}295TaskData {296added_cache_size,297image,298}299});300tasks.insert(image_h.clone(), (task, vec![*material_h]));301progress.total += 1;302}303}304}305}306}307308fn bytes_to_gb(bytes: usize) -> usize {309bytes / 1024_usize.pow(3)310}311312tasks.retain(|image_h, (task, material_handles)| {313match future::block_on(future::poll_once(task)) {314Some(task_data) => {315if let Some(image) = images.get_mut(image_h) {316*image = task_data.image;317progress.processed += 1;318let prev_cached_data_gb = bytes_to_gb(progress.cached_data_size_bytes);319progress.cached_data_size_bytes += task_data.added_cache_size;320let current_cached_data_gb = bytes_to_gb(progress.cached_data_size_bytes);321if current_cached_data_gb > prev_cached_data_gb {322warn!(323"Generated cached texture data from just this run is {}",324format_bytes_size(progress.cached_data_size_bytes)325);326}327// Touch material to trigger change detection328for material_h in material_handles.iter() {329let _ = materials.get_mut(*material_h);330}331}332false333}334None => true,335}336});337338if tasks_res.is_none() {339commands.insert_resource(new_tasks);340}341}342343/// `added_cache_size` is for tracking the amount of data that was cached by this call.344/// Compressed BCn data is cached on disk if cache_compressed_image_data is enabled.345pub fn generate_mips_texture(346image: &mut Image,347settings: &MipmapGeneratorSettings,348#[allow(unused)] added_cache_size: &mut usize,349) -> anyhow::Result<()> {350check_image_compatible(image)?;351match try_into_dynamic(image.clone()) {352Ok(mut dyn_image) => {353#[allow(unused_mut)]354let mut has_alpha = false;355#[cfg(feature = "compress")]356if let Some(img) = dyn_image.as_rgba8() {357for px in img.pixels() {358if px.0[3] != 255 {359has_alpha = true;360break;361}362}363}364365#[cfg(feature = "compress")]366let mut compressed_format = None;367#[allow(unused_mut)]368let mut compression_speed = settings.compression;369#[cfg(feature = "compress")]370{371if let Some(encoder_setting) = settings.compression {372compressed_format = bcn_equivalent_format_of_dyn_image(373&dyn_image,374image.texture_descriptor.format.is_srgb(),375settings.low_quality,376has_alpha,377)378.ok();379compression_speed = compressed_format.map(|_| encoder_setting);380}381}382383#[cfg(feature = "compress")]384let mut input_hash = u64::MAX;385#[allow(unused_mut)]386let mut loaded_from_cache = false;387let mut new_image_data = Vec::new();388389#[cfg(feature = "compress")]390if compression_speed.is_some()391&& compressed_format.is_some()392&& let Some(cache_path) = &settings.compressed_image_data_cache_path393{394input_hash = calculate_hash(image, settings);395if let Some(compressed_image_data) = load_from_cache(input_hash, cache_path) {396new_image_data = compressed_image_data;397loaded_from_cache = true;398}399}400401let mip_count = calculate_mip_count(402dyn_image.width(),403dyn_image.height(),404settings.minimum_mip_resolution,405u32::MAX,406compression_speed,407);408409if !loaded_from_cache {410new_image_data = generate_mips(&mut dyn_image, has_alpha, mip_count, settings);411#[cfg(feature = "compress")]412if let Some(cache_path) = &settings.compressed_image_data_cache_path413&& compression_speed.is_some()414&& compressed_format.is_some()415{416*added_cache_size += new_image_data.len();417save_to_cache(input_hash, &new_image_data, cache_path).unwrap();418}419}420421image.texture_descriptor.mip_level_count = mip_count;422#[cfg(feature = "compress")]423if let Some(format) = compressed_format {424image.texture_descriptor.format = format;425// Remove view formats for compressed textures.426// TODO Is this an issue? A bit difficult to work around since it's &['static]427image.texture_descriptor.view_formats = &[];428}429430image.data = Some(new_image_data);431Ok(())432}433Err(e) => Err(e),434}435}436437/// Returns a vec of bytes containing the image data for all generated mips.438/// Use `calculate_mip_count()` to find the value for `mip_count`.439pub fn generate_mips(440dyn_image: &mut DynamicImage,441has_alpha: bool,442mip_count: u32,443settings: &MipmapGeneratorSettings,444) -> Vec<u8> {445let mut width = dyn_image.width();446let mut height = dyn_image.height();447448#[allow(unused_mut)]449let mut compressed_image_data = None;450#[cfg(feature = "compress")]451if let Some(compression_settings) = settings.compression {452compressed_image_data = bcn_compress_dyn_image(453compression_settings,454dyn_image,455has_alpha,456settings.low_quality,457)458.ok();459}460461#[cfg(not(feature = "compress"))]462if settings.compression.is_some() {463warn!("Compression is Some but compress feature is disabled. Falling back to generating mips without compression.")464}465466let mut image_data = compressed_image_data.unwrap_or(dyn_image.as_bytes().to_vec());467468#[cfg(feature = "compress")]469let min = if settings.compression.is_some() { 4 } else { 1 };470#[cfg(not(feature = "compress"))]471let min = 1;472473let mut resizer = Resizer::new();474475let resize_alg = ResizeOptions::new()476.resize_alg(match settings.filter_type {477FilterType::Nearest => ResizeAlg::Nearest,478FilterType::Triangle => ResizeAlg::Convolution(fast_image_resize::FilterType::Bilinear),479FilterType::CatmullRom => {480ResizeAlg::Convolution(fast_image_resize::FilterType::CatmullRom)481}482FilterType::Gaussian => ResizeAlg::Convolution(fast_image_resize::FilterType::Gaussian),483FilterType::Lanczos3 => ResizeAlg::Convolution(fast_image_resize::FilterType::Lanczos3),484})485.use_alpha(has_alpha);486487for _ in 0..mip_count {488width /= 2;489height /= 2;490491// *dyn_image = dyn_image.resize_exact(width, height, settings.filter_type); // Ex: Resizing with Image crate492493let mut new = DynamicImage::new(width, height, dyn_image.color());494resizer.resize(dyn_image, &mut new, &resize_alg).unwrap();495*dyn_image = new;496497#[allow(unused_mut)]498let mut compressed_image_data = None;499#[cfg(feature = "compress")]500if let Some(compression_speed) = settings.compression {501// https://github.com/bevyengine/bevy/issues/21490502if width >= 4 && height >= 4 {503compressed_image_data = bcn_compress_dyn_image(504compression_speed,505dyn_image,506has_alpha,507settings.low_quality,508)509.ok();510}511}512image_data.append(&mut compressed_image_data.unwrap_or(dyn_image.as_bytes().to_vec()));513if width <= min || height <= min {514break;515}516}517518image_data519}520521/// Returns the number of mip levels522/// The `max_mip_count` includes the first input mip level. So setting this to 2 will523/// result in a single additional mip level being generated, for a total of 2 levels.524pub fn calculate_mip_count(525mut width: u32,526mut height: u32,527minimum_mip_resolution: u32,528max_mip_count: u32,529#[allow(unused)] compression: Option<CompressionSpeed>,530) -> u32 {531let mut mip_level_count = 1;532533#[cfg(feature = "compress")]534let min = if compression.is_some() { 4 } else { 1 };535#[cfg(not(feature = "compress"))]536let min = 1;537538// Use log to avoid loop? Are there edge cases with rounding?539540while width / 2 >= minimum_mip_resolution.max(min)541&& height / 2 >= minimum_mip_resolution.max(min)542&& mip_level_count < max_mip_count543{544width /= 2;545height /= 2;546mip_level_count += 1;547}548549mip_level_count550}551552/// Extract a specific individual mip level as a new image.553pub fn extract_mip_level(image: &Image, mip_level: u32) -> anyhow::Result<Image> {554check_image_compatible(image)?;555556let descriptor = &image.texture_descriptor;557558if descriptor.mip_level_count < mip_level {559return Err(anyhow!(560"Mip level {mip_level} requested, but only {} are available.",561descriptor.mip_level_count562));563}564565let block_size = descriptor.format.block_copy_size(None).unwrap() as usize;566567//let mip_factor = 2u32.pow(mip_level - 1);568//let final_width = descriptor.size.width/mip_factor;569//let final_height = descriptor.size.height/mip_factor;570571let mut width = descriptor.size.width as usize;572let mut height = descriptor.size.height as usize;573574let mut byte_offset = 0usize;575576for _ in 0..mip_level - 1 {577byte_offset += width * block_size * height;578width /= 2;579height /= 2;580}581582let mut new_descriptor = descriptor.clone();583584new_descriptor.mip_level_count = 1;585new_descriptor.size = Extent3d {586width: width as u32,587height: height as u32,588depth_or_array_layers: 1,589};590591Ok(Image {592data: image593.data594.as_ref()595.map(|data| data[byte_offset..byte_offset + (width * block_size * height)].to_vec()),596data_order: TextureDataOrder::default(),597texture_descriptor: new_descriptor,598sampler: image.sampler.clone(),599texture_view_descriptor: image.texture_view_descriptor.clone(),600asset_usage: RenderAssetUsages::default(),601copy_on_resize: false,602})603}604605pub fn check_image_compatible(image: &Image) -> anyhow::Result<()> {606if image.data.is_none() {607return Err(anyhow!(608"Image is a GPU storage texture which is not supported."609));610}611612if image.is_compressed() {613return Err(anyhow!("Compressed images not supported"));614}615616let descriptor = &image.texture_descriptor;617618if descriptor.dimension != TextureDimension::D2 {619return Err(anyhow!(620"Image has dimension {:?} but only TextureDimension::D2 is supported.",621descriptor.dimension622));623}624625if descriptor.size.depth_or_array_layers != 1 {626return Err(anyhow!(627"Image contains {} layers only a single layer is supported.",628descriptor.size.depth_or_array_layers629));630}631632Ok(())633}634635// Implement the GetImages trait for any materials that need conversion636pub trait GetImages {637fn get_images(&self) -> Vec<&Handle<Image>>;638}639640impl GetImages for StandardMaterial {641fn get_images(&self) -> Vec<&Handle<Image>> {642vec![643&self.base_color_texture,644&self.emissive_texture,645&self.metallic_roughness_texture,646&self.normal_map_texture,647&self.occlusion_texture,648]649.into_iter()650.flatten()651.collect()652}653}654655impl<T: GetImages + MaterialExtension> GetImages for ExtendedMaterial<StandardMaterial, T> {656fn get_images(&self) -> Vec<&Handle<Image>> {657let mut images: Vec<&Handle<Image>> = vec![658&self.base.base_color_texture,659&self.base.emissive_texture,660&self.base.metallic_roughness_texture,661&self.base.normal_map_texture,662&self.base.occlusion_texture,663&self.base.depth_map,664#[cfg(feature = "pbr_transmission_textures")]665&self.base.diffuse_transmission_texture,666#[cfg(feature = "pbr_transmission_textures")]667&self.base.specular_transmission_texture,668#[cfg(feature = "pbr_transmission_textures")]669&self.base.thickness_texture,670#[cfg(feature = "pbr_multi_layer_material_textures")]671&self.base.clearcoat_texture,672#[cfg(feature = "pbr_multi_layer_material_textures")]673&self.base.clearcoat_roughness_texture,674#[cfg(feature = "pbr_multi_layer_material_textures")]675&self.base.clearcoat_normal_texture,676#[cfg(feature = "pbr_anisotropy_texture")]677&self.base.anisotropy_texture,678#[cfg(feature = "pbr_specular_textures")]679&self.base.specular_texture,680#[cfg(feature = "pbr_specular_textures")]681&self.base.specular_tint_texture,682]683.into_iter()684.flatten()685.collect();686images.append(&mut self.extension.get_images());687images688}689}690691pub fn try_into_dynamic(image: Image) -> anyhow::Result<DynamicImage> {692let Some(image_data) = image.data else {693return Err(anyhow!(694"Conversion into dynamic image not supported for GPU storage texture."695));696};697698match image.texture_descriptor.format {699TextureFormat::R8Unorm => ImageBuffer::from_raw(700image.texture_descriptor.size.width,701image.texture_descriptor.size.height,702image_data,703)704.map(DynamicImage::ImageLuma8),705TextureFormat::Rg8Unorm => ImageBuffer::from_raw(706image.texture_descriptor.size.width,707image.texture_descriptor.size.height,708image_data,709)710.map(DynamicImage::ImageLumaA8),711TextureFormat::Rgba8UnormSrgb => ImageBuffer::from_raw(712image.texture_descriptor.size.width,713image.texture_descriptor.size.height,714image_data,715)716.map(DynamicImage::ImageRgba8),717TextureFormat::Rgba8Unorm => ImageBuffer::from_raw(718image.texture_descriptor.size.width,719image.texture_descriptor.size.height,720image_data,721)722.map(DynamicImage::ImageRgba8),723// Throw and error if conversion isn't supported724texture_format => {725return Err(anyhow!(726"Conversion into dynamic image not supported for {:?}.",727texture_format728))729}730}731.ok_or_else(|| {732anyhow!(733"Failed to convert into {:?}.",734image.texture_descriptor.format735)736})737}738739#[cfg(feature = "compress")]740fn bcn_compress_dyn_image(741compression_speed: CompressionSpeed,742dyn_image: &DynamicImage,743has_alpha: bool,744low_quality: bool,745) -> anyhow::Result<Vec<u8>> {746use image::Rgba;747748let width = dyn_image.width();749let height = dyn_image.height();750let mut image_data;751if low_quality {752match dyn_image {753DynamicImage::ImageLuma8(data) => {754image_data = vec![0u8; intel_tex_2::bc4::calc_output_size(width, height)];755let surface = intel_tex_2::RSurface {756width,757height,758stride: width,759data,760};761intel_tex_2::bc4::compress_blocks_into(&surface, &mut image_data);762}763DynamicImage::ImageLumaA8(data) => {764let mut rgba =765ImageBuffer::<Rgba<u8>, Vec<u8>>::new(dyn_image.width(), dyn_image.height());766for (rgba_px, rg_px) in rgba.pixels_mut().zip(data.pixels()) {767rgba_px.0[0] = rg_px.0[0];768rgba_px.0[1] = rg_px.0[1];769}770image_data = vec![0u8; intel_tex_2::bc1::calc_output_size(width, height)];771let surface = intel_tex_2::RgbaSurface {772width,773height,774stride: width * 4,775data: rgba.as_raw(),776};777intel_tex_2::bc1::compress_blocks_into(&surface, &mut image_data);778}779DynamicImage::ImageRgba8(data) => {780if has_alpha {781image_data = vec![0u8; intel_tex_2::bc3::calc_output_size(width, height)];782let surface = intel_tex_2::RgbaSurface {783width,784height,785stride: width * 4,786data,787};788intel_tex_2::bc3::compress_blocks_into(&surface, &mut image_data);789} else {790image_data = vec![0u8; intel_tex_2::bc1::calc_output_size(width, height)];791let surface = intel_tex_2::RgbaSurface {792width,793height,794stride: width * 4,795data,796};797intel_tex_2::bc1::compress_blocks_into(&surface, &mut image_data);798}799}800// Throw and error if conversion isn't supported801dyn_image => {802return Err(anyhow!(803"Conversion into dynamic image not supported for {:?}.",804dyn_image805))806}807};808} else {809match dyn_image {810DynamicImage::ImageLuma8(data) => {811image_data = vec![0u8; intel_tex_2::bc4::calc_output_size(width, height)];812let surface = intel_tex_2::RSurface {813width,814height,815stride: width,816data,817};818intel_tex_2::bc4::compress_blocks_into(&surface, &mut image_data);819}820DynamicImage::ImageLumaA8(data) => {821image_data = vec![0u8; intel_tex_2::bc5::calc_output_size(width, height)];822let surface = intel_tex_2::RgSurface {823width,824height,825stride: width * 2,826data,827};828intel_tex_2::bc5::compress_blocks_into(&surface, &mut image_data);829}830DynamicImage::ImageRgba8(data) => {831image_data = vec![0u8; intel_tex_2::bc7::calc_output_size(width, height)];832let surface = intel_tex_2::RgbaSurface {833width,834height,835stride: width * 4,836data,837};838intel_tex_2::bc7::compress_blocks_into(839&compression_speed.get_bc7_encoder(has_alpha),840&surface,841&mut image_data,842);843}844// Throw and error if conversion isn't supported845dyn_image => {846return Err(anyhow!(847"Conversion into dynamic image not supported for {:?}.",848dyn_image849))850}851};852}853854Ok(image_data)855}856857/// If low_quality is set, only 0.5 byte/px formats will be used (BC1, BC4) unless alpha is being used (BC3)858pub fn bcn_equivalent_format_of_dyn_image(859dyn_image: &DynamicImage,860is_srgb: bool,861low_quality: bool,862has_alpha: bool,863) -> anyhow::Result<TextureFormat> {864if dyn_image.width() < 4 || dyn_image.height() < 4 {865return Err(anyhow!("Image size too small for BCn compression"));866}867if low_quality {868match dyn_image {869DynamicImage::ImageLuma8(_) => Ok(TextureFormat::Bc4RUnorm),870DynamicImage::ImageLumaA8(_) => Ok(TextureFormat::Bc1RgbaUnorm),871DynamicImage::ImageRgba8(_) => Ok(if has_alpha {872if is_srgb {873TextureFormat::Bc3RgbaUnormSrgb874} else {875TextureFormat::Bc3RgbaUnorm876}877} else if is_srgb {878TextureFormat::Bc1RgbaUnormSrgb879} else {880TextureFormat::Bc1RgbaUnorm881}),882// Throw and error if conversion isn't supported883dyn_image => Err(anyhow!(884"Conversion into dynamic image not supported for {:?}.",885dyn_image886)),887}888} else {889match dyn_image {890DynamicImage::ImageLuma8(_) => Ok(TextureFormat::Bc4RUnorm),891DynamicImage::ImageLumaA8(_) => Ok(TextureFormat::Bc5RgUnorm),892DynamicImage::ImageRgba8(_) => Ok(if is_srgb {893TextureFormat::Bc7RgbaUnormSrgb894} else {895TextureFormat::Bc7RgbaUnorm896}),897// Throw and error if conversion isn't supported898dyn_image => Err(anyhow!(899"Conversion into dynamic image not supported for {:?}.",900dyn_image901)),902}903}904}905906/// Calculate the hash for the non-compressed non-mipmapped image.907#[cfg(feature = "compress")]908fn calculate_hash(image: &Image, settings: &MipmapGeneratorSettings) -> u64 {909let mut hasher = DefaultHasher::new();910image.data.hash(&mut hasher);911if settings.low_quality {912(934870234u32).hash(&mut hasher);913}914settings.compression.hash(&mut hasher);915match settings.filter_type {916FilterType::Nearest => (934870234u32).hash(&mut hasher),917FilterType::Triangle => (46345624u32).hash(&mut hasher),918FilterType::CatmullRom => (54676234u32).hash(&mut hasher),919FilterType::Gaussian => (623455643u32).hash(&mut hasher),920FilterType::Lanczos3 => (675856584u32).hash(&mut hasher),921}922image.texture_descriptor.hash(&mut hasher);923hasher.finish()924}925926/// Save raw image bytes to disk cache927#[cfg(feature = "compress")]928fn save_to_cache(hash: u64, bytes: &[u8], cache_dir: &Path) -> std::io::Result<()> {929if !cache_dir.exists() {930fs::create_dir(cache_dir)?;931}932let file_path = cache_dir.join(format!("{:x}", hash));933let mut file = File::create(file_path)?;934file.write_all(&zstd::encode_all(bytes, 0).unwrap())?;935Ok(())936}937938/// Load from disk cache for matching input hash939#[cfg(feature = "compress")]940fn load_from_cache(hash: u64, cache_dir: &Path) -> Option<Vec<u8>> {941let file_path = cache_dir.join(format!("{:x}", hash));942if !file_path.exists() {943return None;944}945let Ok(mut file) = File::open(file_path) else {946return None;947};948let mut cached_bytes = Vec::new();949if file.read_to_end(&mut cached_bytes).is_err() {950return None;951};952zstd::decode_all(cached_bytes.as_slice()).ok()953}954955956