Skip to content

Commit 775fae5

Browse files
Add image sampler configuration in GLTF loader (#17875)
I can't underrate anisotropic filtering. # Objective - Allow easily enabling anisotropic filtering on glTF assets. - Allow selecting `ImageFilterMode` for glTF assets at runtime. ## Solution - Added a Resource `DefaultGltfImageSampler`: it stores `Arc<Mutex<ImageSamplerDescriptor>>` and the same `Arc` is stored in `GltfLoader`. The default is independent from provided to `ImagePlugin` and is set in the same way but with `GltfPlugin`. It can then be modified at runtime with `DefaultGltfImageSampler::set`. - Added two fields to `GltfLoaderSettings`: `default_sampler: Option<ImageSamplerDescriptor>` to override aforementioned global default descriptor and `override_sampler: bool` to ignore glTF sampler data. ## Showcase Enabling anisotropic filtering as easy as: ```rust app.add_plugins(DefaultPlugins.set(GltfPlugin{ default_sampler: ImageSamplerDescriptor { min_filter: ImageFilterMode::Linear, mag_filter: ImageFilterMode::Linear, mipmap_filter: ImageFilterMode::Linear, anisotropy_clamp: 16, ..default() }, ..default() })) ``` Use code below to ignore both the global default sampler and glTF data, having `your_shiny_sampler` used directly for all textures instead: ```rust commands.spawn(SceneRoot(asset_server.load_with_settings( GltfAssetLabel::Scene(0).from_asset("models/test-scene.gltf"), |settings: &mut GltfLoaderSettings| { settings.default_sampler = Some(your_shiny_sampler); settings.override_sampler = true; } ))); ``` Remove either setting to get different result! They don't come in pair! Scene rendered with trillinear texture filtering: ![Trillinear](https://github.com/user-attachments/assets/be4c417f-910c-4806-9e64-fd2c21b9fd8d) Scene rendered with 16x anisotropic texture filtering: ![Anisotropic Filtering x16](https://github.com/user-attachments/assets/68190be8-aabd-4bef-8e97-d1b5124cce60) ## Migration Guide - The new fields in `GltfLoaderSettings` have their default values replicate previous behavior. --------- Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com>
1 parent 7e51f60 commit 775fae5

File tree

3 files changed

+125
-47
lines changed

3 files changed

+125
-47
lines changed

crates/bevy_gltf/src/lib.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,15 @@ mod vertex_attributes;
9797

9898
extern crate alloc;
9999

100+
use alloc::sync::Arc;
101+
use std::sync::Mutex;
102+
100103
use bevy_platform::collections::HashMap;
101104

102105
use bevy_app::prelude::*;
103106
use bevy_asset::AssetApp;
104-
use bevy_image::CompressedImageFormats;
107+
use bevy_ecs::prelude::Resource;
108+
use bevy_image::{CompressedImageFormats, ImageSamplerDescriptor};
105109
use bevy_mesh::MeshVertexAttribute;
106110
use bevy_render::renderer::RenderDevice;
107111

@@ -115,10 +119,57 @@ pub mod prelude {
115119

116120
pub use {assets::*, label::GltfAssetLabel, loader::*};
117121

122+
// Has to store an Arc<Mutex<...>> as there is no other way to mutate fields of asset loaders.
123+
/// Stores default [`ImageSamplerDescriptor`] in main world.
124+
#[derive(Resource)]
125+
pub struct DefaultGltfImageSampler(Arc<Mutex<ImageSamplerDescriptor>>);
126+
127+
impl DefaultGltfImageSampler {
128+
/// Creates a new [`DefaultGltfImageSampler`].
129+
pub fn new(descriptor: &ImageSamplerDescriptor) -> Self {
130+
Self(Arc::new(Mutex::new(descriptor.clone())))
131+
}
132+
133+
/// Returns the current default [`ImageSamplerDescriptor`].
134+
pub fn get(&self) -> ImageSamplerDescriptor {
135+
self.0.lock().unwrap().clone()
136+
}
137+
138+
/// Makes a clone of internal [`Arc`] pointer.
139+
///
140+
/// Intended only to be used by code with no access to ECS.
141+
pub fn get_internal(&self) -> Arc<Mutex<ImageSamplerDescriptor>> {
142+
self.0.clone()
143+
}
144+
145+
/// Replaces default [`ImageSamplerDescriptor`].
146+
///
147+
/// Doesn't apply to samplers already built on top of it, i.e. `GltfLoader`'s output.
148+
/// Assets need to manually be reloaded.
149+
pub fn set(&self, descriptor: &ImageSamplerDescriptor) {
150+
*self.0.lock().unwrap() = descriptor.clone();
151+
}
152+
}
153+
118154
/// Adds support for glTF file loading to the app.
119-
#[derive(Default)]
120155
pub struct GltfPlugin {
121-
custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
156+
/// The default image sampler to lay glTF sampler data on top of.
157+
///
158+
/// Can be modified with [`DefaultGltfImageSampler`] resource.
159+
pub default_sampler: ImageSamplerDescriptor,
160+
/// Registry for custom vertex attributes.
161+
///
162+
/// To specify, use [`GltfPlugin::add_custom_vertex_attribute`].
163+
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
164+
}
165+
166+
impl Default for GltfPlugin {
167+
fn default() -> Self {
168+
GltfPlugin {
169+
default_sampler: ImageSamplerDescriptor::linear(),
170+
custom_vertex_attributes: HashMap::default(),
171+
}
172+
}
122173
}
123174

124175
impl GltfPlugin {
@@ -157,9 +208,13 @@ impl Plugin for GltfPlugin {
157208
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
158209
None => CompressedImageFormats::NONE,
159210
};
211+
let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler);
212+
let default_sampler = default_sampler_resource.get_internal();
213+
app.insert_resource(default_sampler_resource);
160214
app.register_asset_loader(GltfLoader {
161215
supported_compressed_formats,
162216
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
217+
default_sampler,
163218
});
164219
}
165220
}

crates/bevy_gltf/src/loader/gltf_ext/texture.rs

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,48 +39,48 @@ pub(crate) fn texture_handle(
3939
}
4040

4141
/// Extracts the texture sampler data from the glTF [`Texture`].
42-
pub(crate) fn texture_sampler(texture: &Texture<'_>) -> ImageSamplerDescriptor {
42+
pub(crate) fn texture_sampler(
43+
texture: &Texture<'_>,
44+
default_sampler: &ImageSamplerDescriptor,
45+
) -> ImageSamplerDescriptor {
4346
let gltf_sampler = texture.sampler();
47+
let mut sampler = default_sampler.clone();
4448

45-
ImageSamplerDescriptor {
46-
address_mode_u: address_mode(&gltf_sampler.wrap_s()),
47-
address_mode_v: address_mode(&gltf_sampler.wrap_t()),
48-
49-
mag_filter: gltf_sampler
50-
.mag_filter()
51-
.map(|mf| match mf {
52-
MagFilter::Nearest => ImageFilterMode::Nearest,
53-
MagFilter::Linear => ImageFilterMode::Linear,
54-
})
55-
.unwrap_or(ImageSamplerDescriptor::default().mag_filter),
56-
57-
min_filter: gltf_sampler
58-
.min_filter()
59-
.map(|mf| match mf {
60-
MinFilter::Nearest
61-
| MinFilter::NearestMipmapNearest
62-
| MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest,
63-
MinFilter::Linear
64-
| MinFilter::LinearMipmapNearest
65-
| MinFilter::LinearMipmapLinear => ImageFilterMode::Linear,
66-
})
67-
.unwrap_or(ImageSamplerDescriptor::default().min_filter),
49+
sampler.address_mode_u = address_mode(&gltf_sampler.wrap_s());
50+
sampler.address_mode_v = address_mode(&gltf_sampler.wrap_t());
6851

69-
mipmap_filter: gltf_sampler
70-
.min_filter()
71-
.map(|mf| match mf {
72-
MinFilter::Nearest
73-
| MinFilter::Linear
74-
| MinFilter::NearestMipmapNearest
75-
| MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest,
76-
MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => {
77-
ImageFilterMode::Linear
78-
}
79-
})
80-
.unwrap_or(ImageSamplerDescriptor::default().mipmap_filter),
81-
82-
..Default::default()
52+
// Shouldn't parse filters when anisotropic filtering is on, because trilinear is then required by wgpu.
53+
// We also trust user to have provided a valid sampler.
54+
if sampler.anisotropy_clamp != 1 {
55+
if let Some(mag_filter) = gltf_sampler.mag_filter().map(|mf| match mf {
56+
MagFilter::Nearest => ImageFilterMode::Nearest,
57+
MagFilter::Linear => ImageFilterMode::Linear,
58+
}) {
59+
sampler.mag_filter = mag_filter;
60+
}
61+
if let Some(min_filter) = gltf_sampler.min_filter().map(|mf| match mf {
62+
MinFilter::Nearest
63+
| MinFilter::NearestMipmapNearest
64+
| MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest,
65+
MinFilter::Linear | MinFilter::LinearMipmapNearest | MinFilter::LinearMipmapLinear => {
66+
ImageFilterMode::Linear
67+
}
68+
}) {
69+
sampler.min_filter = min_filter;
70+
}
71+
if let Some(mipmap_filter) = gltf_sampler.min_filter().map(|mf| match mf {
72+
MinFilter::Nearest
73+
| MinFilter::Linear
74+
| MinFilter::NearestMipmapNearest
75+
| MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest,
76+
MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => {
77+
ImageFilterMode::Linear
78+
}
79+
}) {
80+
sampler.mipmap_filter = mipmap_filter;
81+
}
8382
}
83+
sampler
8484
}
8585

8686
pub(crate) fn texture_label(texture: &Texture<'_>) -> GltfAssetLabel {

crates/bevy_gltf/src/loader/mod.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod extensions;
22
mod gltf_ext;
33

4+
use alloc::sync::Arc;
45
use std::{
56
io::Error,
67
path::{Path, PathBuf},
8+
sync::Mutex,
79
};
810

911
#[cfg(feature = "bevy_animation")]
@@ -146,6 +148,8 @@ pub struct GltfLoader {
146148
/// See [this section of the glTF specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview)
147149
/// for additional details on custom attributes.
148150
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
151+
/// Arc to default [`ImageSamplerDescriptor`].
152+
pub default_sampler: Arc<Mutex<ImageSamplerDescriptor>>,
149153
}
150154

151155
/// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of
@@ -181,6 +185,12 @@ pub struct GltfLoaderSettings {
181185
pub load_lights: bool,
182186
/// If true, the loader will include the root of the gltf root node.
183187
pub include_source: bool,
188+
/// Overrides the default sampler. Data from sampler node is added on top of that.
189+
///
190+
/// If None, uses global default which is stored in `DefaultGltfImageSampler` resource.
191+
pub default_sampler: Option<ImageSamplerDescriptor>,
192+
/// If true, the loader will ignore sampler data from gltf and use the default sampler.
193+
pub override_sampler: bool,
184194
}
185195

186196
impl Default for GltfLoaderSettings {
@@ -191,6 +201,8 @@ impl Default for GltfLoaderSettings {
191201
load_cameras: true,
192202
load_lights: true,
193203
include_source: false,
204+
default_sampler: None,
205+
override_sampler: false,
194206
}
195207
}
196208
}
@@ -506,6 +518,10 @@ async fn load_gltf<'a, 'b, 'c>(
506518
(animations, named_animations, animation_roots)
507519
};
508520

521+
let default_sampler = match settings.default_sampler.as_ref() {
522+
Some(sampler) => sampler,
523+
None => &loader.default_sampler.lock().unwrap().clone(),
524+
};
509525
// We collect handles to ensure loaded images from paths are not unloaded before they are used elsewhere
510526
// in the loader. This prevents "reloads", but it also prevents dropping the is_srgb context on reload.
511527
//
@@ -522,7 +538,8 @@ async fn load_gltf<'a, 'b, 'c>(
522538
&linear_textures,
523539
parent_path,
524540
loader.supported_compressed_formats,
525-
settings.load_materials,
541+
default_sampler,
542+
settings,
526543
)
527544
.await?;
528545
image.process_loaded_texture(load_context, &mut _texture_handles);
@@ -542,7 +559,8 @@ async fn load_gltf<'a, 'b, 'c>(
542559
linear_textures,
543560
parent_path,
544561
loader.supported_compressed_formats,
545-
settings.load_materials,
562+
default_sampler,
563+
settings,
546564
)
547565
.await
548566
});
@@ -958,10 +976,15 @@ async fn load_image<'a, 'b>(
958976
linear_textures: &HashSet<usize>,
959977
parent_path: &'b Path,
960978
supported_compressed_formats: CompressedImageFormats,
961-
render_asset_usages: RenderAssetUsages,
979+
default_sampler: &ImageSamplerDescriptor,
980+
settings: &GltfLoaderSettings,
962981
) -> Result<ImageOrPath, GltfError> {
963982
let is_srgb = !linear_textures.contains(&gltf_texture.index());
964-
let sampler_descriptor = texture_sampler(&gltf_texture);
983+
let sampler_descriptor = if settings.override_sampler {
984+
default_sampler.clone()
985+
} else {
986+
texture_sampler(&gltf_texture, default_sampler)
987+
};
965988

966989
match gltf_texture.source().source() {
967990
Source::View { view, mime_type } => {
@@ -974,7 +997,7 @@ async fn load_image<'a, 'b>(
974997
supported_compressed_formats,
975998
is_srgb,
976999
ImageSampler::Descriptor(sampler_descriptor),
977-
render_asset_usages,
1000+
settings.load_materials,
9781001
)?;
9791002
Ok(ImageOrPath::Image {
9801003
image,
@@ -996,7 +1019,7 @@ async fn load_image<'a, 'b>(
9961019
supported_compressed_formats,
9971020
is_srgb,
9981021
ImageSampler::Descriptor(sampler_descriptor),
999-
render_asset_usages,
1022+
settings.load_materials,
10001023
)?,
10011024
label: GltfAssetLabel::Texture(gltf_texture.index()),
10021025
})

0 commit comments

Comments
 (0)