diff --git a/Cargo.toml b/Cargo.toml index a3d3a2ab63e51..3e2cb1ac4d7a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1410,6 +1410,14 @@ doc-scrape-examples = true [package.metadata.example.no_prepass] hidden = true +[[example]] +name = "test_mipmap_filter" +path = "tests/3d/test_mipmap_filter.rs" +doc-scrape-examples = true + +[package.metadata.example.test_mipmap_filter] +hidden = true + # Animation [[example]] name = "animation_events" diff --git a/assets/models/checkerboard/checkerboard.bin b/assets/models/checkerboard/checkerboard.bin new file mode 100644 index 0000000000000..9bc116e4c07d5 Binary files /dev/null and b/assets/models/checkerboard/checkerboard.bin differ diff --git a/assets/models/checkerboard/checkerboard.gltf b/assets/models/checkerboard/checkerboard.gltf new file mode 100644 index 0000000000000..632ff35372d12 --- /dev/null +++ b/assets/models/checkerboard/checkerboard.gltf @@ -0,0 +1,143 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.3.47", + "version":"2.0" + }, + "extensionsUsed":[ + "KHR_materials_unlit" + ], + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Plane" + } + ], + "materials":[ + { + "doubleSided":true, + "extensions":{ + "KHR_materials_unlit":{} + }, + "name":"Material.001", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0, + "roughnessFactor":0.9 + } + } + ], + "meshes":[ + { + "name":"Plane", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"checkerboard.ktx2", + "uri":"checkerboard.ktx2" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":140, + "uri":"checkerboard.bin" + } + ] +} diff --git a/assets/models/checkerboard/checkerboard.ktx2 b/assets/models/checkerboard/checkerboard.ktx2 new file mode 100644 index 0000000000000..fa758298183ca Binary files /dev/null and b/assets/models/checkerboard/checkerboard.ktx2 differ diff --git a/tests/3d/test_mipmap_filter.rs b/tests/3d/test_mipmap_filter.rs new file mode 100644 index 0000000000000..3942431c63f40 --- /dev/null +++ b/tests/3d/test_mipmap_filter.rs @@ -0,0 +1,245 @@ +//! Tests `ImageSamplerDescription::mipmap_filter`. Loads a checkerboard model +//! and lets the user switch between various presets. This also serves as a test +//! of `GltfLoaderSettings::override_sampler`. +//! +//! CAUTION: This test currently fails due to - +//! subsequent loads of the same gltf will ignore the test's custom sampler settings. + +use bevy::{ + gltf::GltfLoaderSettings, + image::{ImageAddressMode, ImageFilterMode, ImageSamplerDescriptor}, + prelude::*, +}; + +#[derive(Resource)] +struct Combos(Vec); + +fn main() { + let default_sampler = ImageSamplerDescriptor { + address_mode_u: ImageAddressMode::Repeat, + address_mode_v: ImageAddressMode::Repeat, + mag_filter: ImageFilterMode::Linear, + min_filter: ImageFilterMode::Linear, + mipmap_filter: ImageFilterMode::Nearest, + ..Default::default() + }; + + let combos = Combos(vec![ + Combo { + key: KeyCode::Digit1, + label: "1: None", + sampler: ImageSamplerDescriptor { + lod_max_clamp: 0.0, + ..default_sampler.clone() + }, + }, + Combo { + key: KeyCode::Digit2, + label: "2: Nearest", + sampler: default_sampler.clone(), + }, + Combo { + key: KeyCode::Digit3, + label: "3: Linear", + sampler: ImageSamplerDescriptor { + mipmap_filter: ImageFilterMode::Linear, + ..default_sampler.clone() + }, + }, + Combo { + key: KeyCode::Digit4, + label: "4: Linear, Anisotropic", + sampler: ImageSamplerDescriptor { + mipmap_filter: ImageFilterMode::Linear, + anisotropy_clamp: 16, + ..default_sampler.clone() + }, + }, + ]); + + let mut app = App::new(); + + app.insert_resource(combos) + .insert_resource(VisibleCombo(KeyCode::Digit1)) + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + update_controls, + update_visibility, + update_text, + update_camera, + ), + ); + + #[cfg(feature = "bevy_ci_testing")] + app.insert_resource(ci::Capture::default()) + .add_systems(Startup, ci::setup) + .add_systems(Update, ci::update); + + app.run(); +} + +fn setup(mut commands: Commands, asset_server: Res, combos: Res) { + for combo in combos.0.iter() { + let asset = GltfAssetLabel::Scene(0).from_asset("models/checkerboard/checkerboard.gltf"); + + let sampler = combo.sampler.clone(); + + let settings = move |settings: &mut GltfLoaderSettings| { + settings.default_sampler = Some(sampler.clone()); + settings.override_sampler = true; + }; + + commands.spawn(( + SceneRoot(asset_server.load_with_settings(asset, settings)), + combo.clone(), + )); + } + + commands.spawn(( + Camera3d::default(), + Projection::from(PerspectiveProjection { + near: 0.001, + ..Default::default() + }), + )); + + commands.spawn(( + Text::default(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..Default::default() + }, + )); +} + +#[derive(Component, Clone)] +struct Combo { + key: KeyCode, + label: &'static str, + sampler: ImageSamplerDescriptor, +} + +#[derive(Resource, PartialEq)] +struct VisibleCombo(KeyCode); + +fn update_controls( + keyboard_input: Res>, + mut time: ResMut>, + combos: Query<&Combo>, + mut visible_combo: ResMut, +) { + for combo in &combos { + if keyboard_input.just_pressed(combo.key) { + *visible_combo = VisibleCombo(combo.key); + } + } + + if keyboard_input.just_pressed(KeyCode::Space) { + if time.is_paused() { + time.unpause(); + } else { + time.pause(); + } + } +} + +fn update_visibility( + mut combos: Query<(&Combo, &mut Visibility)>, + visible_combo: Res, +) { + for (combo, mut visibility) in &mut combos { + *visibility = if *visible_combo == VisibleCombo(combo.key) { + Visibility::Visible + } else { + Visibility::Hidden + }; + } +} + +fn update_text( + mut text: Single<&mut Text>, + time: Res>, + combos: Query<(&Combo, &Visibility)>, + visible_combo: Res, +) { + text.clear(); + + text.push_str(&format!( + "Space: {}\n\n", + if time.is_paused() { "Unpause" } else { "Pause" } + )); + + text.push_str("Mipmap filter:\n"); + + for (combo, _) in &combos { + let visible = *visible_combo == VisibleCombo(combo.key); + + text.push_str(&format!( + "{}{}\n", + if visible { "> " } else { " " }, + combo.label, + )); + } +} + +fn update_camera(_time: Res