diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 6aada19c458e5..796fb0c4e4917 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -346,10 +346,7 @@ pub fn get_lut_bind_group_layout_entries(bindings: [u32; 2]) -> [BindGroupLayout // allow(dead_code) so it doesn't complain when the tonemapping_luts feature is disabled #[allow(dead_code)] fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { - let mut image = - Image::from_buffer(bytes, image_type, CompressedImageFormats::NONE, false).unwrap(); - - image.sampler_descriptor = bevy_render::texture::ImageSampler::Descriptor(SamplerDescriptor { + let image_sampler = bevy_render::texture::ImageSampler::Descriptor(SamplerDescriptor { label: Some("Tonemapping LUT sampler"), address_mode_u: AddressMode::ClampToEdge, address_mode_v: AddressMode::ClampToEdge, @@ -359,8 +356,14 @@ fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { mipmap_filter: FilterMode::Linear, ..default() }); - - image + Image::from_buffer( + bytes, + image_type, + CompressedImageFormats::NONE, + false, + image_sampler, + ) + .unwrap() } pub fn lut_placeholder() -> Image { diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 65ba1dbf9e5d1..0c5a7f43f5f40 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -22,9 +22,10 @@ use bevy_render::{ }, prelude::SpatialBundle, primitives::Aabb, - render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor}, + render_resource::{Face, PrimitiveTopology}, texture::{ - CompressedImageFormats, Image, ImageLoaderSettings, ImageSampler, ImageType, TextureError, + CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings, + ImageSampler, ImageSamplerDescriptor, ImageType, TextureError, }, }; use bevy_scene::Scene; @@ -256,9 +257,14 @@ async fn load_gltf<'a, 'b, 'c>( ) { let handle = match texture { ImageOrPath::Image { label, image } => load_context.add_labeled_asset(label, image), - ImageOrPath::Path { path, is_srgb } => { + ImageOrPath::Path { + path, + is_srgb, + sampler_descriptor, + } => { load_context.load_with_settings(path, move |settings: &mut ImageLoaderSettings| { settings.is_srgb = is_srgb; + settings.sampler_descriptor = sampler_descriptor; }) } }; @@ -667,18 +673,19 @@ async fn load_image<'a, 'b>( supported_compressed_formats: CompressedImageFormats, ) -> Result { let is_srgb = !linear_textures.contains(&gltf_texture.index()); + let sampler_descriptor = texture_sampler(&gltf_texture); match gltf_texture.source().source() { gltf::image::Source::View { view, mime_type } => { let start = view.offset(); let end = view.offset() + view.length(); let buffer = &buffer_data[view.buffer().index()][start..end]; - let mut image = Image::from_buffer( + let image = Image::from_buffer( buffer, ImageType::MimeType(mime_type), supported_compressed_formats, is_srgb, + ImageSampler::Descriptor(sampler_descriptor.into()), )?; - image.sampler_descriptor = ImageSampler::Descriptor(texture_sampler(&gltf_texture)); Ok(ImageOrPath::Image { image, label: texture_label(&gltf_texture), @@ -698,6 +705,7 @@ async fn load_image<'a, 'b>( mime_type.map(ImageType::MimeType).unwrap_or(image_type), supported_compressed_formats, is_srgb, + ImageSampler::Descriptor(sampler_descriptor.into()), )?, label: texture_label(&gltf_texture), }) @@ -706,6 +714,7 @@ async fn load_image<'a, 'b>( Ok(ImageOrPath::Path { path: image_path, is_srgb, + sampler_descriptor, }) } } @@ -1110,32 +1119,32 @@ fn skin_label(skin: &gltf::Skin) -> String { } /// Extracts the texture sampler data from the glTF texture. -fn texture_sampler<'a>(texture: &gltf::Texture) -> SamplerDescriptor<'a> { +fn texture_sampler(texture: &gltf::Texture) -> ImageSamplerDescriptor { let gltf_sampler = texture.sampler(); - SamplerDescriptor { + ImageSamplerDescriptor { address_mode_u: texture_address_mode(&gltf_sampler.wrap_s()), address_mode_v: texture_address_mode(&gltf_sampler.wrap_t()), mag_filter: gltf_sampler .mag_filter() .map(|mf| match mf { - MagFilter::Nearest => FilterMode::Nearest, - MagFilter::Linear => FilterMode::Linear, + MagFilter::Nearest => ImageFilterMode::Nearest, + MagFilter::Linear => ImageFilterMode::Linear, }) - .unwrap_or(SamplerDescriptor::default().mag_filter), + .unwrap_or(ImageSamplerDescriptor::default().mag_filter), min_filter: gltf_sampler .min_filter() .map(|mf| match mf { MinFilter::Nearest | MinFilter::NearestMipmapNearest - | MinFilter::NearestMipmapLinear => FilterMode::Nearest, + | MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest, MinFilter::Linear | MinFilter::LinearMipmapNearest - | MinFilter::LinearMipmapLinear => FilterMode::Linear, + | MinFilter::LinearMipmapLinear => ImageFilterMode::Linear, }) - .unwrap_or(SamplerDescriptor::default().min_filter), + .unwrap_or(ImageSamplerDescriptor::default().min_filter), mipmap_filter: gltf_sampler .min_filter() @@ -1143,23 +1152,23 @@ fn texture_sampler<'a>(texture: &gltf::Texture) -> SamplerDescriptor<'a> { MinFilter::Nearest | MinFilter::Linear | MinFilter::NearestMipmapNearest - | MinFilter::LinearMipmapNearest => FilterMode::Nearest, + | MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest, MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => { - FilterMode::Linear + ImageFilterMode::Linear } }) - .unwrap_or(SamplerDescriptor::default().mipmap_filter), + .unwrap_or(ImageSamplerDescriptor::default().mipmap_filter), ..Default::default() } } /// Maps the texture address mode form glTF to wgpu. -fn texture_address_mode(gltf_address_mode: &gltf::texture::WrappingMode) -> AddressMode { +fn texture_address_mode(gltf_address_mode: &gltf::texture::WrappingMode) -> ImageAddressMode { match gltf_address_mode { - WrappingMode::ClampToEdge => AddressMode::ClampToEdge, - WrappingMode::Repeat => AddressMode::Repeat, - WrappingMode::MirroredRepeat => AddressMode::MirrorRepeat, + WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge, + WrappingMode::Repeat => ImageAddressMode::Repeat, + WrappingMode::MirroredRepeat => ImageAddressMode::MirrorRepeat, } } @@ -1280,8 +1289,15 @@ fn resolve_node_hierarchy( } enum ImageOrPath { - Image { image: Image, label: String }, - Path { path: PathBuf, is_srgb: bool }, + Image { + image: Image, + label: String, + }, + Path { + path: PathBuf, + is_srgb: bool, + sampler_descriptor: ImageSamplerDescriptor, + }, } struct DataUri<'a> { diff --git a/crates/bevy_render/src/texture/compressed_image_saver.rs b/crates/bevy_render/src/texture/compressed_image_saver.rs index a557447db3d46..87f5d235209f5 100644 --- a/crates/bevy_render/src/texture/compressed_image_saver.rs +++ b/crates/bevy_render/src/texture/compressed_image_saver.rs @@ -1,4 +1,7 @@ -use crate::texture::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; +use crate::texture::{ + Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings, ImageSampler, + ImageSamplerDescriptor, +}; use bevy_asset::saver::{AssetSaver, SavedAsset}; use futures_lite::{AsyncWriteExt, FutureExt}; use thiserror::Error; @@ -30,6 +33,10 @@ impl AssetSaver for CompressedImageSaver { compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); compressor_params.set_generate_mipmaps(true); let is_srgb = image.texture_descriptor.format.is_srgb(); + let sampler_descriptor = match &image.sampler_descriptor { + ImageSampler::Default => ImageSamplerDescriptor::default(), + ImageSampler::Descriptor(sampler_descriptor) => sampler_descriptor.clone().into(), + }; let color_space = if is_srgb { basis_universal::ColorSpace::Srgb } else { @@ -55,6 +62,7 @@ impl AssetSaver for CompressedImageSaver { Ok(ImageLoaderSettings { format: ImageFormatSetting::Format(ImageFormat::Basis), is_srgb, + sampler_descriptor, }) } .boxed() diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index b90ec82ce431a..da9779c23f389 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -168,6 +168,276 @@ impl ImageSampler { #[derive(Resource, Debug, Clone, Deref, DerefMut)] pub struct DefaultImageSampler(pub(crate) Sampler); +/// How edges should be handled in texture addressing. +/// +/// This type mirrors [`wgpu::AddressMode`]. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub enum ImageAddressMode { + /// Clamp the value to the edge of the texture. + /// + /// -0.25 -> 0.0 + /// 1.25 -> 1.0 + #[default] + ClampToEdge, + /// Repeat the texture in a tiling fashion. + /// + /// -0.25 -> 0.75 + /// 1.25 -> 0.25 + Repeat, + /// Repeat the texture, mirroring it every repeat. + /// + /// -0.25 -> 0.25 + /// 1.25 -> 0.75 + MirrorRepeat, + /// Clamp the value to the border of the texture + /// Requires the wgpu feature [`wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER`]. + /// + /// -0.25 -> border + /// 1.25 -> border + ClampToBorder, +} + +/// Texel mixing mode when sampling between texels. +/// +/// This type mirrors [`wgpu::FilterMode`]. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub enum ImageFilterMode { + /// Nearest neighbor sampling. + /// + /// This creates a pixelated effect when used as a mag filter. + #[default] + Nearest, + /// Linear Interpolation. + /// + /// This makes textures smooth but blurry when used as a mag filter. + Linear, +} + +/// Comparison function used for depth and stencil operations. +/// +/// This type mirrors [`wgpu::CompareFunction`]. +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum ImageCompareFunction { + /// Function never passes + Never, + /// Function passes if new value less than existing value + Less, + /// Function passes if new value is equal to existing value. When using + /// this compare function, make sure to mark your Vertex Shader's `@builtin(position)` + /// output as `@invariant` to prevent artifacting. + Equal, + /// Function passes if new value is less than or equal to existing value + LessEqual, + /// Function passes if new value is greater than existing value + Greater, + /// Function passes if new value is not equal to existing value. When using + /// this compare function, make sure to mark your Vertex Shader's `@builtin(position)` + /// output as `@invariant` to prevent artifacting. + NotEqual, + /// Function passes if new value is greater than or equal to existing value + GreaterEqual, + /// Function always passes + Always, +} + +/// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`]. +/// +/// This type mirrors [`wgpu::SamplerBorderColor`]. +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum ImageSamplerBorderColor { + /// RGBA color `[0, 0, 0, 0]`. + TransparentBlack, + /// RGBA color `[0, 0, 0, 1]`. + OpaqueBlack, + /// RGBA color `[1, 1, 1, 1]`. + OpaqueWhite, + /// On the Metal wgpu backend, this is equivalent to [`Self::TransparentBlack`] for + /// textures that have an alpha component, and equivalent to [`Self::OpaqueBlack`] + /// for textures that do not have an alpha component. On other backends, + /// this is equivalent to [`Self::TransparentBlack`]. Requires + /// [`wgpu::Features::ADDRESS_MODE_CLAMP_TO_ZERO`]. Not supported on the web. + Zero, +} + +/// Indicates to an [`ImageLoader`](super::ImageLoader) how an [`Image`] should be sampled. +/// As this type is part of the [`ImageLoaderSettings`](super::ImageLoaderSettings), +/// it will be serialized to an image asset `.meta` file which might require a migration in case of +/// a breaking change. +/// +/// This types mirrors [`wgpu::SamplerDescriptor`], but that might change in future versions. +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct ImageSamplerDescriptor { + /// How to deal with out of bounds accesses in the u (i.e. x) direction. + pub address_mode_u: ImageAddressMode, + /// How to deal with out of bounds accesses in the v (i.e. y) direction. + pub address_mode_v: ImageAddressMode, + /// How to deal with out of bounds accesses in the w (i.e. z) direction. + pub address_mode_w: ImageAddressMode, + /// How to filter the texture when it needs to be magnified (made larger). + pub mag_filter: ImageFilterMode, + /// How to filter the texture when it needs to be minified (made smaller). + pub min_filter: ImageFilterMode, + /// How to filter between mip map levels + pub mipmap_filter: ImageFilterMode, + /// Minimum level of detail (i.e. mip level) to use. + pub lod_min_clamp: f32, + /// Maximum level of detail (i.e. mip level) to use. + pub lod_max_clamp: f32, + /// If this is enabled, this is a comparison sampler using the given comparison function. + pub compare: Option, + /// Must be at least 1. If this is not 1, all filter modes must be linear. + pub anisotropy_clamp: u16, + /// Border color to use when `address_mode`` is [`ImageAddressMode::ClampToBorder`]. + pub border_color: Option, +} + +impl Default for ImageSamplerDescriptor { + fn default() -> Self { + Self { + address_mode_u: Default::default(), + address_mode_v: Default::default(), + address_mode_w: Default::default(), + mag_filter: Default::default(), + min_filter: Default::default(), + mipmap_filter: Default::default(), + lod_min_clamp: 0.0, + lod_max_clamp: 32.0, + compare: None, + anisotropy_clamp: 1, + border_color: None, + } + } +} + +impl From for wgpu::AddressMode { + fn from(value: ImageAddressMode) -> Self { + match value { + ImageAddressMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + ImageAddressMode::Repeat => wgpu::AddressMode::Repeat, + ImageAddressMode::MirrorRepeat => wgpu::AddressMode::MirrorRepeat, + ImageAddressMode::ClampToBorder => wgpu::AddressMode::ClampToBorder, + } + } +} + +impl From for wgpu::FilterMode { + fn from(value: ImageFilterMode) -> Self { + match value { + ImageFilterMode::Nearest => wgpu::FilterMode::Nearest, + ImageFilterMode::Linear => wgpu::FilterMode::Linear, + } + } +} + +impl From for wgpu::CompareFunction { + fn from(value: ImageCompareFunction) -> Self { + match value { + ImageCompareFunction::Never => wgpu::CompareFunction::Never, + ImageCompareFunction::Less => wgpu::CompareFunction::Less, + ImageCompareFunction::Equal => wgpu::CompareFunction::Equal, + ImageCompareFunction::LessEqual => wgpu::CompareFunction::LessEqual, + ImageCompareFunction::Greater => wgpu::CompareFunction::Greater, + ImageCompareFunction::NotEqual => wgpu::CompareFunction::NotEqual, + ImageCompareFunction::GreaterEqual => wgpu::CompareFunction::GreaterEqual, + ImageCompareFunction::Always => wgpu::CompareFunction::Always, + } + } +} + +impl From for wgpu::SamplerBorderColor { + fn from(value: ImageSamplerBorderColor) -> Self { + match value { + ImageSamplerBorderColor::TransparentBlack => wgpu::SamplerBorderColor::TransparentBlack, + ImageSamplerBorderColor::OpaqueBlack => wgpu::SamplerBorderColor::OpaqueBlack, + ImageSamplerBorderColor::OpaqueWhite => wgpu::SamplerBorderColor::OpaqueWhite, + ImageSamplerBorderColor::Zero => wgpu::SamplerBorderColor::Zero, + } + } +} + +impl From for wgpu::SamplerDescriptor<'static> { + fn from(value: ImageSamplerDescriptor) -> Self { + wgpu::SamplerDescriptor { + label: None, + address_mode_u: value.address_mode_u.into(), + address_mode_v: value.address_mode_v.into(), + address_mode_w: value.address_mode_w.into(), + mag_filter: value.mag_filter.into(), + min_filter: value.min_filter.into(), + mipmap_filter: value.mipmap_filter.into(), + lod_min_clamp: value.lod_min_clamp, + lod_max_clamp: value.lod_max_clamp, + compare: value.compare.map(Into::into), + anisotropy_clamp: value.anisotropy_clamp, + border_color: value.border_color.map(Into::into), + } + } +} + +impl From for ImageAddressMode { + fn from(value: wgpu::AddressMode) -> Self { + match value { + wgpu::AddressMode::ClampToEdge => ImageAddressMode::ClampToEdge, + wgpu::AddressMode::Repeat => ImageAddressMode::Repeat, + wgpu::AddressMode::MirrorRepeat => ImageAddressMode::MirrorRepeat, + wgpu::AddressMode::ClampToBorder => ImageAddressMode::ClampToBorder, + } + } +} + +impl From for ImageFilterMode { + fn from(value: wgpu::FilterMode) -> Self { + match value { + wgpu::FilterMode::Nearest => ImageFilterMode::Nearest, + wgpu::FilterMode::Linear => ImageFilterMode::Linear, + } + } +} + +impl From for ImageCompareFunction { + fn from(value: wgpu::CompareFunction) -> Self { + match value { + wgpu::CompareFunction::Never => ImageCompareFunction::Never, + wgpu::CompareFunction::Less => ImageCompareFunction::Less, + wgpu::CompareFunction::Equal => ImageCompareFunction::Equal, + wgpu::CompareFunction::LessEqual => ImageCompareFunction::LessEqual, + wgpu::CompareFunction::Greater => ImageCompareFunction::Greater, + wgpu::CompareFunction::NotEqual => ImageCompareFunction::NotEqual, + wgpu::CompareFunction::GreaterEqual => ImageCompareFunction::GreaterEqual, + wgpu::CompareFunction::Always => ImageCompareFunction::Always, + } + } +} + +impl From for ImageSamplerBorderColor { + fn from(value: wgpu::SamplerBorderColor) -> Self { + match value { + wgpu::SamplerBorderColor::TransparentBlack => ImageSamplerBorderColor::TransparentBlack, + wgpu::SamplerBorderColor::OpaqueBlack => ImageSamplerBorderColor::OpaqueBlack, + wgpu::SamplerBorderColor::OpaqueWhite => ImageSamplerBorderColor::OpaqueWhite, + wgpu::SamplerBorderColor::Zero => ImageSamplerBorderColor::Zero, + } + } +} + +impl<'a> From> for ImageSamplerDescriptor { + fn from(value: wgpu::SamplerDescriptor) -> Self { + ImageSamplerDescriptor { + address_mode_u: value.address_mode_u.into(), + address_mode_v: value.address_mode_v.into(), + address_mode_w: value.address_mode_w.into(), + mag_filter: value.mag_filter.into(), + min_filter: value.min_filter.into(), + mipmap_filter: value.mipmap_filter.into(), + lod_min_clamp: value.lod_min_clamp, + lod_max_clamp: value.lod_max_clamp, + compare: value.compare.map(Into::into), + anisotropy_clamp: value.anisotropy_clamp, + border_color: value.border_color.map(Into::into), + } + } +} + impl Default for Image { /// default is a 1x1x1 all '1.0' texture fn default() -> Self { @@ -365,6 +635,7 @@ impl Image { image_type: ImageType, #[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats, is_srgb: bool, + image_sampler: ImageSampler, ) -> Result { let format = image_type.to_image_format()?; @@ -393,7 +664,9 @@ impl Image { reader.set_format(image_crate_format); reader.no_limits(); let dyn_img = reader.decode()?; - Ok(Self::from_dynamic(dyn_img, is_srgb)) + let mut img = Self::from_dynamic(dyn_img, is_srgb); + img.sampler_descriptor = image_sampler; + Ok(img) } } } diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index 8f8a283289481..130fc9ee4a31e 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -7,7 +7,7 @@ use crate::{ texture::{Image, ImageFormat, ImageType, TextureError}, }; -use super::CompressedImageFormats; +use super::{CompressedImageFormats, ImageSampler, ImageSamplerDescriptor}; use serde::{Deserialize, Serialize}; /// Loader for images that can be read by the `image` crate. @@ -56,6 +56,7 @@ pub enum ImageFormatSetting { pub struct ImageLoaderSettings { pub format: ImageFormatSetting, pub is_srgb: bool, + pub sampler_descriptor: ImageSamplerDescriptor, } impl Default for ImageLoaderSettings { @@ -63,6 +64,7 @@ impl Default for ImageLoaderSettings { Self { format: ImageFormatSetting::default(), is_srgb: true, + sampler_descriptor: ImageSamplerDescriptor::default(), } } } @@ -101,6 +103,7 @@ impl AssetLoader for ImageLoader { image_type, self.supported_compressed_formats, settings.is_srgb, + ImageSampler::Descriptor(settings.sampler_descriptor.into()), ) .map_err(|err| FileTextureError { error: err,