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>
This commit is contained in:
axlitEels 2025-05-06 08:48:13 +03:00 committed by GitHub
parent 7e51f60de1
commit 775fae5b62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 47 deletions

View File

@ -97,11 +97,15 @@ mod vertex_attributes;
extern crate alloc;
use alloc::sync::Arc;
use std::sync::Mutex;
use bevy_platform::collections::HashMap;
use bevy_app::prelude::*;
use bevy_asset::AssetApp;
use bevy_image::CompressedImageFormats;
use bevy_ecs::prelude::Resource;
use bevy_image::{CompressedImageFormats, ImageSamplerDescriptor};
use bevy_mesh::MeshVertexAttribute;
use bevy_render::renderer::RenderDevice;
@ -115,10 +119,57 @@ pub mod prelude {
pub use {assets::*, label::GltfAssetLabel, loader::*};
// Has to store an Arc<Mutex<...>> as there is no other way to mutate fields of asset loaders.
/// Stores default [`ImageSamplerDescriptor`] in main world.
#[derive(Resource)]
pub struct DefaultGltfImageSampler(Arc<Mutex<ImageSamplerDescriptor>>);
impl DefaultGltfImageSampler {
/// Creates a new [`DefaultGltfImageSampler`].
pub fn new(descriptor: &ImageSamplerDescriptor) -> Self {
Self(Arc::new(Mutex::new(descriptor.clone())))
}
/// Returns the current default [`ImageSamplerDescriptor`].
pub fn get(&self) -> ImageSamplerDescriptor {
self.0.lock().unwrap().clone()
}
/// Makes a clone of internal [`Arc`] pointer.
///
/// Intended only to be used by code with no access to ECS.
pub fn get_internal(&self) -> Arc<Mutex<ImageSamplerDescriptor>> {
self.0.clone()
}
/// Replaces default [`ImageSamplerDescriptor`].
///
/// Doesn't apply to samplers already built on top of it, i.e. `GltfLoader`'s output.
/// Assets need to manually be reloaded.
pub fn set(&self, descriptor: &ImageSamplerDescriptor) {
*self.0.lock().unwrap() = descriptor.clone();
}
}
/// Adds support for glTF file loading to the app.
#[derive(Default)]
pub struct GltfPlugin {
custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
/// The default image sampler to lay glTF sampler data on top of.
///
/// Can be modified with [`DefaultGltfImageSampler`] resource.
pub default_sampler: ImageSamplerDescriptor,
/// Registry for custom vertex attributes.
///
/// To specify, use [`GltfPlugin::add_custom_vertex_attribute`].
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
}
impl Default for GltfPlugin {
fn default() -> Self {
GltfPlugin {
default_sampler: ImageSamplerDescriptor::linear(),
custom_vertex_attributes: HashMap::default(),
}
}
}
impl GltfPlugin {
@ -157,9 +208,13 @@ impl Plugin for GltfPlugin {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::NONE,
};
let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler);
let default_sampler = default_sampler_resource.get_internal();
app.insert_resource(default_sampler_resource);
app.register_asset_loader(GltfLoader {
supported_compressed_formats,
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
default_sampler,
});
}
}

View File

@ -39,48 +39,48 @@ pub(crate) fn texture_handle(
}
/// Extracts the texture sampler data from the glTF [`Texture`].
pub(crate) fn texture_sampler(texture: &Texture<'_>) -> ImageSamplerDescriptor {
pub(crate) fn texture_sampler(
texture: &Texture<'_>,
default_sampler: &ImageSamplerDescriptor,
) -> ImageSamplerDescriptor {
let gltf_sampler = texture.sampler();
let mut sampler = default_sampler.clone();
ImageSamplerDescriptor {
address_mode_u: address_mode(&gltf_sampler.wrap_s()),
address_mode_v: address_mode(&gltf_sampler.wrap_t()),
sampler.address_mode_u = address_mode(&gltf_sampler.wrap_s());
sampler.address_mode_v = address_mode(&gltf_sampler.wrap_t());
mag_filter: gltf_sampler
.mag_filter()
.map(|mf| match mf {
MagFilter::Nearest => ImageFilterMode::Nearest,
MagFilter::Linear => ImageFilterMode::Linear,
})
.unwrap_or(ImageSamplerDescriptor::default().mag_filter),
min_filter: gltf_sampler
.min_filter()
.map(|mf| match mf {
MinFilter::Nearest
| MinFilter::NearestMipmapNearest
| MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest,
MinFilter::Linear
| MinFilter::LinearMipmapNearest
| MinFilter::LinearMipmapLinear => ImageFilterMode::Linear,
})
.unwrap_or(ImageSamplerDescriptor::default().min_filter),
mipmap_filter: gltf_sampler
.min_filter()
.map(|mf| match mf {
MinFilter::Nearest
| MinFilter::Linear
| MinFilter::NearestMipmapNearest
| MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest,
MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => {
ImageFilterMode::Linear
}
})
.unwrap_or(ImageSamplerDescriptor::default().mipmap_filter),
..Default::default()
// Shouldn't parse filters when anisotropic filtering is on, because trilinear is then required by wgpu.
// We also trust user to have provided a valid sampler.
if sampler.anisotropy_clamp != 1 {
if let Some(mag_filter) = gltf_sampler.mag_filter().map(|mf| match mf {
MagFilter::Nearest => ImageFilterMode::Nearest,
MagFilter::Linear => ImageFilterMode::Linear,
}) {
sampler.mag_filter = mag_filter;
}
if let Some(min_filter) = gltf_sampler.min_filter().map(|mf| match mf {
MinFilter::Nearest
| MinFilter::NearestMipmapNearest
| MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest,
MinFilter::Linear | MinFilter::LinearMipmapNearest | MinFilter::LinearMipmapLinear => {
ImageFilterMode::Linear
}
}) {
sampler.min_filter = min_filter;
}
if let Some(mipmap_filter) = gltf_sampler.min_filter().map(|mf| match mf {
MinFilter::Nearest
| MinFilter::Linear
| MinFilter::NearestMipmapNearest
| MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest,
MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => {
ImageFilterMode::Linear
}
}) {
sampler.mipmap_filter = mipmap_filter;
}
}
sampler
}
pub(crate) fn texture_label(texture: &Texture<'_>) -> GltfAssetLabel {

View File

@ -1,9 +1,11 @@
mod extensions;
mod gltf_ext;
use alloc::sync::Arc;
use std::{
io::Error,
path::{Path, PathBuf},
sync::Mutex,
};
#[cfg(feature = "bevy_animation")]
@ -146,6 +148,8 @@ pub struct GltfLoader {
/// See [this section of the glTF specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview)
/// for additional details on custom attributes.
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
/// Arc to default [`ImageSamplerDescriptor`].
pub default_sampler: Arc<Mutex<ImageSamplerDescriptor>>,
}
/// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of
@ -181,6 +185,12 @@ pub struct GltfLoaderSettings {
pub load_lights: bool,
/// If true, the loader will include the root of the gltf root node.
pub include_source: bool,
/// Overrides the default sampler. Data from sampler node is added on top of that.
///
/// If None, uses global default which is stored in `DefaultGltfImageSampler` resource.
pub default_sampler: Option<ImageSamplerDescriptor>,
/// If true, the loader will ignore sampler data from gltf and use the default sampler.
pub override_sampler: bool,
}
impl Default for GltfLoaderSettings {
@ -191,6 +201,8 @@ impl Default for GltfLoaderSettings {
load_cameras: true,
load_lights: true,
include_source: false,
default_sampler: None,
override_sampler: false,
}
}
}
@ -506,6 +518,10 @@ async fn load_gltf<'a, 'b, 'c>(
(animations, named_animations, animation_roots)
};
let default_sampler = match settings.default_sampler.as_ref() {
Some(sampler) => sampler,
None => &loader.default_sampler.lock().unwrap().clone(),
};
// We collect handles to ensure loaded images from paths are not unloaded before they are used elsewhere
// in the loader. This prevents "reloads", but it also prevents dropping the is_srgb context on reload.
//
@ -522,7 +538,8 @@ async fn load_gltf<'a, 'b, 'c>(
&linear_textures,
parent_path,
loader.supported_compressed_formats,
settings.load_materials,
default_sampler,
settings,
)
.await?;
image.process_loaded_texture(load_context, &mut _texture_handles);
@ -542,7 +559,8 @@ async fn load_gltf<'a, 'b, 'c>(
linear_textures,
parent_path,
loader.supported_compressed_formats,
settings.load_materials,
default_sampler,
settings,
)
.await
});
@ -958,10 +976,15 @@ async fn load_image<'a, 'b>(
linear_textures: &HashSet<usize>,
parent_path: &'b Path,
supported_compressed_formats: CompressedImageFormats,
render_asset_usages: RenderAssetUsages,
default_sampler: &ImageSamplerDescriptor,
settings: &GltfLoaderSettings,
) -> Result<ImageOrPath, GltfError> {
let is_srgb = !linear_textures.contains(&gltf_texture.index());
let sampler_descriptor = texture_sampler(&gltf_texture);
let sampler_descriptor = if settings.override_sampler {
default_sampler.clone()
} else {
texture_sampler(&gltf_texture, default_sampler)
};
match gltf_texture.source().source() {
Source::View { view, mime_type } => {
@ -974,7 +997,7 @@ async fn load_image<'a, 'b>(
supported_compressed_formats,
is_srgb,
ImageSampler::Descriptor(sampler_descriptor),
render_asset_usages,
settings.load_materials,
)?;
Ok(ImageOrPath::Image {
image,
@ -996,7 +1019,7 @@ async fn load_image<'a, 'b>(
supported_compressed_formats,
is_srgb,
ImageSampler::Descriptor(sampler_descriptor),
render_asset_usages,
settings.load_materials,
)?,
label: GltfAssetLabel::Texture(gltf_texture.index()),
})