Skip to content

Add image sampler configuration in GLTF loader #17875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions crates/bevy_gltf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about making this constructor pub(crate)? This is supposed to be a resource only GltfPlugin would need to insert. Any attempt of inserting one's own DefaultGltfImageSampler would be a conflict with GltfPlugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have to @alice-i-cecile
Because otherwise it's ready for merge

Copy link
Member

@alice-i-cecile alice-i-cecile May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the simpler and more flexible fully public design here.

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 {
Expand Down Expand Up @@ -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,
});
}
}
76 changes: 38 additions & 38 deletions crates/bevy_gltf/src/loader/gltf_ext/texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),

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),
sampler.address_mode_u = address_mode(&gltf_sampler.wrap_s());
sampler.address_mode_v = address_mode(&gltf_sampler.wrap_t());

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 {
Expand Down
35 changes: 29 additions & 6 deletions crates/bevy_gltf/src/loader/mod.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -191,6 +201,8 @@ impl Default for GltfLoaderSettings {
load_cameras: true,
load_lights: true,
include_source: false,
default_sampler: None,
override_sampler: false,
}
}
}
Expand Down Expand Up @@ -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.
//
Expand All @@ -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);
Expand All @@ -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
});
Expand Down Expand Up @@ -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 } => {
Expand All @@ -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,
Expand All @@ -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()),
})
Expand Down