Skip to main content

ssr/
ssr.rs

1//! Demonstrates screen space reflections in deferred rendering.
2
3use std::fmt;
4use std::ops::Range;
5
6use bevy::{
7    anti_alias::taa::TemporalAntiAliasing,
8    camera::Hdr,
9    color::palettes::css::{BLACK, WHITE},
10    feathers::{
11        controls::{FeathersNumberInput, HardLimit, NumberInputPrecision, NumberInputValue},
12        display::{label, label_small},
13        theme::UiTheme,
14        FeathersPlugins,
15    },
16    image::{
17        ImageAddressMode, ImageFilterMode, ImageLoaderSettings, ImageSampler,
18        ImageSamplerDescriptor,
19    },
20    input::mouse::MouseWheel,
21    light::Skybox,
22    math::{vec3, vec4},
23    pbr::{
24        DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension,
25        ScreenSpaceAmbientOcclusion, ScreenSpaceReflections,
26    },
27    prelude::*,
28    render::render_resource::{AsBindGroup, ShaderType},
29    shader::ShaderRef,
30    ui_widgets::{radio_self_update, ValueChange},
31};
32
33#[path = "../helpers/radio.rs"]
34mod radio;
35
36#[path = "../helpers/theme.rs"]
37mod theme;
38
39use radio::{feathers_option_buttons, main_ui_node_scene};
40use theme::basic_example_theme;
41
42/// This example uses a shader source file from the assets subdirectory
43const SHADER_ASSET_PATH: &str = "shaders/water_material.wesl";
44
45// The speed of camera movement.
46const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.1;
47const CAMERA_KEYBOARD_ORBIT_SPEED: f32 = 0.02;
48const CAMERA_MOUSE_WHEEL_ZOOM_SPEED: f32 = 0.25;
49
50// We clamp camera distances to this range.
51const CAMERA_ZOOM_RANGE: Range<f32> = 2.0..12.0;
52
53/// A custom [`ExtendedMaterial`] that creates animated water ripples.
54#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
55struct Water {
56    /// The normal map image.
57    ///
58    /// Note that, like all normal maps, this must not be loaded as sRGB.
59    #[texture(100)]
60    #[sampler(101)]
61    normals: Handle<Image>,
62
63    // Parameters to the water shader.
64    #[uniform(102)]
65    settings: WaterSettings,
66}
67
68/// Parameters to the water shader.
69#[derive(ShaderType, Debug, Clone)]
70struct WaterSettings {
71    /// How much to displace each octave each frame, in the u and v directions.
72    /// Two octaves are packed into each `vec4`.
73    octave_vectors: [Vec4; 2],
74    /// How wide the waves are in each octave.
75    octave_scales: Vec4,
76    /// How high the waves are in each octave.
77    octave_strengths: Vec4,
78}
79
80/// The current settings that the user has chosen.
81#[derive(Resource)]
82struct AppSettings {
83    /// Whether screen space reflections are on.
84    ssr_on: SsrOn,
85    /// Which model is being displayed.
86    displayed_model: DisplayedModel,
87    /// Which base is being displayed.
88    displayed_base: DisplayedBase,
89    /// The perceptual roughness range over which SSR begins to fade in.
90    min_perceptual_roughness: Range<f32>,
91    /// The perceptual roughness range over which SSR begins to fade out.
92    max_perceptual_roughness: Range<f32>,
93    /// The range over which SSR begins to fade out at the edges of the screen.
94    edge_fadeout: Range<f32>,
95}
96
97impl Default for AppSettings {
98    fn default() -> Self {
99        Self {
100            ssr_on: default(),
101            displayed_model: default(),
102            displayed_base: default(),
103            min_perceptual_roughness: 0.0..0.01,
104            max_perceptual_roughness: 0.99..1.0,
105            edge_fadeout: 0.0..0.0,
106        }
107    }
108}
109
110/// Whether screen space reflections are on.
111#[derive(Clone, Copy, Deref, PartialEq)]
112pub struct SsrOn(bool);
113
114impl Default for SsrOn {
115    fn default() -> Self {
116        Self(true)
117    }
118}
119
120/// Which model is being displayed.
121#[derive(Default, PartialEq, Copy, Clone)]
122enum DisplayedModel {
123    /// The cube is being displayed.
124    #[default]
125    Cube,
126    /// The flight helmet is being displayed.
127    FlightHelmet,
128    /// The capsules are being displayed.
129    Capsules,
130}
131
132/// Which base is being displayed.
133#[derive(Default, PartialEq, Copy, Clone)]
134enum DisplayedBase {
135    /// The water base is being displayed.
136    #[default]
137    Water,
138    /// A slightly rough metallic base is being displayed.
139    Metallic,
140    /// A very rough non-metallic base is being displayed.
141    RedPlane,
142}
143
144impl fmt::Display for DisplayedModel {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        let name = match self {
147            DisplayedModel::Cube => "Cube",
148            DisplayedModel::FlightHelmet => "Flight Helmet",
149            DisplayedModel::Capsules => "Capsules",
150        };
151        write!(f, "{}", name)
152    }
153}
154
155impl fmt::Display for DisplayedBase {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        let name = match self {
158            DisplayedBase::Water => "Water",
159            DisplayedBase::Metallic => "Metallic",
160            DisplayedBase::RedPlane => "Red Plane",
161        };
162        write!(f, "{}", name)
163    }
164}
165
166/// A marker component for the single cube model.
167#[derive(Component)]
168struct CubeModel;
169
170/// A marker component for the flight helmet model.
171#[derive(Component)]
172struct FlightHelmetModel;
173
174/// A marker component for the row of capsules model.
175#[derive(Component)]
176struct CapsuleModel;
177
178/// A marker component for the row of capsules parent.
179#[derive(Component)]
180struct CapsulesParent;
181
182/// A marker component for the metallic base.
183#[derive(Component)]
184struct MetallicBaseModel;
185
186/// A marker component for the non-metallic base.
187#[derive(Component)]
188struct RedPlaneBaseModel;
189
190/// A marker component for the water model.
191#[derive(Component)]
192struct WaterModel;
193
194/// A marker component for the range value so that
195/// the correct value in `AppSettings` can be updated.
196/// This derives `Default` so that it is easily compatible for use in bsn!
197#[derive(Component, Clone, Copy, PartialEq, Default)]
198enum AppNumberInput {
199    #[default]
200    MinRoughnessStart,
201    MinRoughnessEnd,
202    MaxRoughnessStart,
203    MaxRoughnessEnd,
204    EdgeFadeoutStart,
205    EdgeFadeoutEnd,
206}
207
208#[derive(bevy::ecs::system::SystemParam)]
209struct ModelQueries<'w, 's> {
210    cube_models: Query<'w, 's, Entity, With<CubeModel>>,
211    flight_helmet_models: Query<'w, 's, Entity, With<FlightHelmetModel>>,
212    capsule_models: Query<'w, 's, Entity, Or<(With<CapsuleModel>, With<CapsulesParent>)>>,
213    metallic_base_models: Query<'w, 's, Entity, With<MetallicBaseModel>>,
214    non_metallic_base_models: Query<'w, 's, Entity, With<RedPlaneBaseModel>>,
215    water_models: Query<'w, 's, Entity, With<WaterModel>>,
216}
217
218fn main() {
219    // Enable deferred rendering, which is necessary for screen-space
220    // reflections at this time. Disable multisampled antialiasing, as deferred
221    // rendering doesn't support that.
222    App::new()
223        .insert_resource(DefaultOpaqueRendererMethod::deferred())
224        .insert_resource(UiTheme(basic_example_theme(Color::WHITE)))
225        .init_resource::<AppSettings>()
226        .add_plugins((
227            DefaultPlugins.set(WindowPlugin {
228                primary_window: Some(Window {
229                    title: "Bevy Screen Space Reflections Example".into(),
230                    ..default()
231                }),
232                ..default()
233            }),
234            FeathersPlugins,
235        ))
236        .add_plugins(MaterialPlugin::<ExtendedMaterial<StandardMaterial, Water>>::default())
237        .add_systems(Startup, setup)
238        .add_systems(Update, rotate_model)
239        .add_systems(Update, move_camera)
240        .add_observer(handle_value_change_ssr_on)
241        .add_observer(handle_value_change_displayed_model)
242        .add_observer(handle_value_change_displayed_base)
243        .add_observer(radio_self_update)
244        .run();
245}
246
247// Set up the scene.
248fn setup(
249    mut commands: Commands,
250    mut meshes: ResMut<Assets<Mesh>>,
251    mut standard_materials: ResMut<Assets<StandardMaterial>>,
252    mut water_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, Water>>>,
253    asset_server: Res<AssetServer>,
254    app_settings: Res<AppSettings>,
255) {
256    spawn_cube(
257        &mut commands,
258        &asset_server,
259        &mut meshes,
260        &mut standard_materials,
261    );
262    spawn_flight_helmet(&mut commands, &asset_server);
263    spawn_capsules(&mut commands, &mut meshes, &mut standard_materials);
264    spawn_metallic_base(&mut commands, &mut meshes, &mut standard_materials);
265    spawn_non_metallic_base(&mut commands, &mut meshes, &mut standard_materials);
266    spawn_water(
267        &mut commands,
268        &asset_server,
269        &mut meshes,
270        &mut water_materials,
271    );
272    spawn_camera(&mut commands, &asset_server, &app_settings);
273    spawn_buttons(&mut commands, &app_settings);
274}
275
276// Spawns the rotating cube.
277fn spawn_cube(
278    commands: &mut Commands,
279    asset_server: &AssetServer,
280    meshes: &mut Assets<Mesh>,
281    standard_materials: &mut Assets<StandardMaterial>,
282) {
283    commands
284        .spawn((
285            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
286            MeshMaterial3d(standard_materials.add(StandardMaterial {
287                base_color: Color::from(WHITE),
288                base_color_texture: Some(asset_server.load("branding/icon.png")),
289                ..default()
290            })),
291            Transform::from_xyz(0.0, 0.5, 0.0),
292        ))
293        .insert(CubeModel);
294}
295
296// Spawns the flight helmet.
297fn spawn_flight_helmet(commands: &mut Commands, asset_server: &AssetServer) {
298    commands.spawn((
299        WorldAssetRoot(
300            asset_server
301                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
302        ),
303        Transform::from_scale(Vec3::splat(2.5)),
304        FlightHelmetModel,
305        Visibility::Hidden,
306    ));
307}
308
309// Spawns the row of capsules.
310fn spawn_capsules(
311    commands: &mut Commands,
312    meshes: &mut Assets<Mesh>,
313    standard_materials: &mut Assets<StandardMaterial>,
314) {
315    let capsule_mesh = meshes.add(Capsule3d::new(0.4, 0.5));
316    let parent = commands
317        .spawn((
318            Transform::from_xyz(0.0, 0.5, 0.0),
319            Visibility::Hidden,
320            CapsulesParent,
321        ))
322        .id();
323
324    for i in 0..5 {
325        let roughness = i as f32 * 0.25;
326        let child = commands
327            .spawn((
328                Mesh3d(capsule_mesh.clone()),
329                MeshMaterial3d(standard_materials.add(StandardMaterial {
330                    base_color: Color::BLACK,
331                    perceptual_roughness: roughness.max(0.08),
332                    ..default()
333                })),
334                Transform::from_xyz(i as f32 * 1.1 - (1.1 * 2.0), 0.5, 0.0),
335                CapsuleModel,
336            ))
337            .id();
338        commands.entity(parent).add_child(child);
339    }
340}
341
342// Spawns the metallic base.
343fn spawn_metallic_base(
344    commands: &mut Commands,
345    meshes: &mut Assets<Mesh>,
346    standard_materials: &mut Assets<StandardMaterial>,
347) {
348    commands.spawn((
349        Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))),
350        MeshMaterial3d(standard_materials.add(StandardMaterial {
351            base_color: Color::from(bevy::color::palettes::css::DARK_GRAY),
352            metallic: 1.0,
353            perceptual_roughness: 0.3,
354            ..default()
355        })),
356        Transform::from_scale(Vec3::splat(100.0)),
357        MetallicBaseModel,
358        Visibility::Hidden,
359    ));
360}
361
362// Spawns the non-metallic base.
363fn spawn_non_metallic_base(
364    commands: &mut Commands,
365    meshes: &mut Assets<Mesh>,
366    standard_materials: &mut Assets<StandardMaterial>,
367) {
368    commands.spawn((
369        Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))),
370        MeshMaterial3d(standard_materials.add(StandardMaterial {
371            base_color: Color::from(bevy::color::palettes::css::RED),
372            metallic: 0.0,
373            perceptual_roughness: 0.2,
374            ..default()
375        })),
376        Transform::from_scale(Vec3::splat(100.0)),
377        RedPlaneBaseModel,
378        Visibility::Hidden,
379    ));
380}
381
382// Spawns the water plane.
383fn spawn_water(
384    commands: &mut Commands,
385    asset_server: &AssetServer,
386    meshes: &mut Assets<Mesh>,
387    water_materials: &mut Assets<ExtendedMaterial<StandardMaterial, Water>>,
388) {
389    commands.spawn((
390        Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))),
391        MeshMaterial3d(
392            water_materials.add(ExtendedMaterial {
393                base: StandardMaterial {
394                    base_color: BLACK.into(),
395                    perceptual_roughness: 0.09,
396                    ..default()
397                },
398                extension: Water {
399                    normals: asset_server
400                        .load_builder()
401                        .with_settings::<ImageLoaderSettings>(|settings| {
402                            settings.is_srgb = false;
403                            settings.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
404                                address_mode_u: ImageAddressMode::Repeat,
405                                address_mode_v: ImageAddressMode::Repeat,
406                                mag_filter: ImageFilterMode::Linear,
407                                min_filter: ImageFilterMode::Linear,
408                                ..default()
409                            });
410                        })
411                        .load("textures/water_normals.png"),
412                    // These water settings are just random values to create some
413                    // variety.
414                    settings: WaterSettings {
415                        octave_vectors: [
416                            vec4(0.080, 0.059, 0.073, -0.062),
417                            vec4(0.153, 0.138, -0.149, -0.195),
418                        ],
419                        octave_scales: vec4(1.0, 2.1, 7.9, 14.9) * 5.0,
420                        octave_strengths: vec4(0.16, 0.18, 0.093, 0.044),
421                    },
422                },
423            }),
424        ),
425        Transform::from_scale(Vec3::splat(100.0)),
426        WaterModel,
427    ));
428}
429
430// Spawns the camera.
431fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer, app_settings: &AppSettings) {
432    // Create the camera. Add an environment map and skybox so the water has
433    // something interesting to reflect, other than the cube. Enable deferred
434    // rendering by adding depth and deferred prepasses. Turn on FXAA to make
435    // the scene look a little nicer. Finally, add screen space reflections.
436    commands.spawn((
437        Camera3d::default(),
438        Transform::from_translation(vec3(-1.25, 2.25, 4.5)).looking_at(Vec3::ZERO, Vec3::Y),
439        Hdr,
440        Msaa::Off,
441        TemporalAntiAliasing::default(),
442        ScreenSpaceReflections {
443            min_perceptual_roughness: app_settings.min_perceptual_roughness.clone(),
444            max_perceptual_roughness: app_settings.max_perceptual_roughness.clone(),
445            edge_fadeout: app_settings.edge_fadeout.clone(),
446            ..default()
447        },
448        ScreenSpaceAmbientOcclusion::default(),
449        EnvironmentMapLight {
450            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
451            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
452            intensity: 5000.0,
453            ..default()
454        },
455        Skybox {
456            image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
457            brightness: 5000.0,
458            ..default()
459        },
460    ));
461}
462
463fn spawn_buttons(commands: &mut Commands, app_settings: &AppSettings) {
464    commands.spawn_scene(bsn! {
465        @main_ui_node_scene()
466        Children[
467            @feathers_option_buttons(
468                "SSR",
469                &[
470                    (SsrOn(true), "On"),
471                    (SsrOn(false), "Off"),
472                ],
473                0,
474            )
475            --
476            @feathers_option_buttons(
477                "Model",
478                &[
479                    (DisplayedModel::Cube, "Cube"),
480                    (
481                        DisplayedModel::FlightHelmet,
482                        "Flight Helmet",
483                    ),
484                    (DisplayedModel::Capsules, "Capsules"),
485                ],
486                0,
487            )
488            --
489            @feathers_option_buttons(
490                "Base",
491                &[
492                    (DisplayedBase::Water, "Water"),
493                    (DisplayedBase::Metallic, "Metallic"),
494                    (DisplayedBase::RedPlane, "Red Plane"),
495                ],
496                0,
497            )
498            --
499            @range_row(
500                "Min Roughness",
501                app_settings.min_perceptual_roughness.start,
502                app_settings.min_perceptual_roughness.end,
503                AppNumberInput::MinRoughnessStart,
504                AppNumberInput::MinRoughnessEnd,
505            )
506            --
507            @range_row(
508                "Max Roughness",
509                app_settings.max_perceptual_roughness.start,
510                app_settings.max_perceptual_roughness.end,
511                AppNumberInput::MaxRoughnessStart,
512                AppNumberInput::MaxRoughnessEnd,
513            )
514            --
515            @range_row(
516                "Edge Fadeout",
517                app_settings.edge_fadeout.start,
518                app_settings.edge_fadeout.end,
519                AppNumberInput::EdgeFadeoutStart,
520                AppNumberInput::EdgeFadeoutEnd,
521            )
522        ]
523    });
524}
525
526fn range_row(
527    title: &str,
528    start_value: f32,
529    end_value: f32,
530    start_number_input: AppNumberInput,
531    end_number_input: AppNumberInput,
532) -> impl Scene {
533    bsn! {
534        Node {
535            align_items: AlignItems::Center,
536        }
537        Children[
538            Node {
539                width: px(150),
540            }
541            Children[
542                @label(title.to_string())
543            ]
544            --
545            @range_controls(
546                start_value,
547                start_number_input
548            )
549            --
550            Node {
551                margin: UiRect::horizontal(px(10)),
552            }
553            Children [
554                @label_small("to".to_string())
555            ]
556            --
557            @range_controls(end_value, end_number_input)
558        ]
559    }
560}
561
562fn range_controls(value: f32, app_number_input: AppNumberInput) -> impl Scene {
563    bsn! {
564        @FeathersNumberInput
565        NumberInputValue::F32(value)
566        app_number_input
567        NumberInputPrecision(3)
568        HardLimit::f32(0.0..=1.0)
569        Node {
570            align_items: AlignItems::Center,
571        }
572        on(handle_value_change_number_input)
573    }
574}
575
576fn rotate_model(
577    mut query: Query<&mut Transform, Or<(With<CubeModel>, With<FlightHelmetModel>)>>,
578    time: Res<Time>,
579) {
580    for mut transform in query.iter_mut() {
581        // Models rotate on the Y axis.
582        transform.rotation = Quat::from_rotation_y(time.elapsed_secs());
583    }
584}
585
586// Processes input related to camera movement.
587fn move_camera(
588    keyboard_input: Res<ButtonInput<KeyCode>>,
589    mut mouse_wheel_reader: MessageReader<MouseWheel>,
590    mut cameras: Query<&mut Transform, With<Camera>>,
591) {
592    let (mut distance_delta, mut theta_delta) = (0.0, 0.0);
593
594    // Handle keyboard events.
595    if keyboard_input.pressed(KeyCode::KeyW) {
596        distance_delta -= CAMERA_KEYBOARD_ZOOM_SPEED;
597    }
598    if keyboard_input.pressed(KeyCode::KeyS) {
599        distance_delta += CAMERA_KEYBOARD_ZOOM_SPEED;
600    }
601    if keyboard_input.pressed(KeyCode::KeyA) {
602        theta_delta += CAMERA_KEYBOARD_ORBIT_SPEED;
603    }
604    if keyboard_input.pressed(KeyCode::KeyD) {
605        theta_delta -= CAMERA_KEYBOARD_ORBIT_SPEED;
606    }
607
608    // Handle mouse events.
609    for mouse_wheel in mouse_wheel_reader.read() {
610        distance_delta -= mouse_wheel.y * CAMERA_MOUSE_WHEEL_ZOOM_SPEED;
611    }
612
613    // Update transforms.
614    for mut camera_transform in cameras.iter_mut() {
615        let local_z = camera_transform.local_z().as_vec3().normalize_or_zero();
616        if distance_delta != 0.0 {
617            camera_transform.translation = (camera_transform.translation.length() + distance_delta)
618                .clamp(CAMERA_ZOOM_RANGE.start, CAMERA_ZOOM_RANGE.end)
619                * local_z;
620        }
621        if theta_delta != 0.0 {
622            camera_transform
623                .translate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, theta_delta));
624            camera_transform.look_at(Vec3::ZERO, Vec3::Y);
625        }
626    }
627}
628
629/// Handles changes to the `SsrOn` radio group.
630fn handle_value_change_ssr_on(
631    event: On<ValueChange<Entity>>,
632    new_value_query: Query<&radio::RadioButtonOptionValue<SsrOn>>,
633    commands: Commands,
634    mut app_settings: ResMut<AppSettings>,
635    cameras: Query<Entity, With<Camera>>,
636) {
637    let Ok(radio::RadioButtonOptionValue(ssr_on)) = new_value_query.get(event.value) else {
638        return;
639    };
640    app_settings.ssr_on = *ssr_on;
641
642    update_views(commands, app_settings, cameras);
643}
644
645/// Update the camera views, particularly after `SsrOn` or any of the number inputs change
646fn update_views(
647    mut commands: Commands,
648    app_settings: ResMut<AppSettings>,
649    mut cameras: Query<Entity, With<Camera>>,
650) {
651    for camera in cameras.iter_mut() {
652        if app_settings.ssr_on.0 {
653            commands.entity(camera).insert(ScreenSpaceReflections {
654                min_perceptual_roughness: app_settings.min_perceptual_roughness.clone(),
655                max_perceptual_roughness: app_settings.max_perceptual_roughness.clone(),
656                edge_fadeout: app_settings.edge_fadeout.clone(),
657                ..default()
658            });
659        } else {
660            commands.entity(camera).remove::<ScreenSpaceReflections>();
661        }
662    }
663}
664
665/// Handles changes to the `DisplayedModel` radio group.
666fn handle_value_change_displayed_model(
667    event: On<ValueChange<Entity>>,
668    new_value_query: Query<&radio::RadioButtonOptionValue<DisplayedModel>>,
669    mut app_settings: ResMut<AppSettings>,
670    model_queries: ModelQueries,
671    mut visibilities: Query<&mut Visibility>,
672) {
673    let Ok(radio::RadioButtonOptionValue(displayed_model)) = new_value_query.get(event.value)
674    else {
675        return;
676    };
677    app_settings.displayed_model = *displayed_model;
678
679    for entity in model_queries.cube_models.iter() {
680        if let Ok(mut visibility) = visibilities.get_mut(entity) {
681            *visibility = if app_settings.displayed_model == DisplayedModel::Cube {
682                Visibility::Visible
683            } else {
684                Visibility::Hidden
685            };
686        }
687    }
688    for entity in model_queries.flight_helmet_models.iter() {
689        if let Ok(mut visibility) = visibilities.get_mut(entity) {
690            *visibility = if app_settings.displayed_model == DisplayedModel::FlightHelmet {
691                Visibility::Visible
692            } else {
693                Visibility::Hidden
694            };
695        }
696    }
697    for entity in model_queries.capsule_models.iter() {
698        if let Ok(mut visibility) = visibilities.get_mut(entity) {
699            *visibility = if app_settings.displayed_model == DisplayedModel::Capsules {
700                Visibility::Visible
701            } else {
702                Visibility::Hidden
703            };
704        }
705    }
706}
707
708/// Handles changes to the `DisplayedBase` radio group.
709fn handle_value_change_displayed_base(
710    event: On<ValueChange<Entity>>,
711    new_value_query: Query<&radio::RadioButtonOptionValue<DisplayedBase>>,
712    mut app_settings: ResMut<AppSettings>,
713    model_queries: ModelQueries,
714    mut visibilities: Query<&mut Visibility>,
715) {
716    let Ok(radio::RadioButtonOptionValue(displayed_base)) = new_value_query.get(event.value) else {
717        return;
718    };
719    app_settings.displayed_base = *displayed_base;
720
721    for entity in model_queries.metallic_base_models.iter() {
722        if let Ok(mut visibility) = visibilities.get_mut(entity) {
723            *visibility = if app_settings.displayed_base == DisplayedBase::Metallic {
724                Visibility::Visible
725            } else {
726                Visibility::Hidden
727            };
728        }
729    }
730    for entity in model_queries.non_metallic_base_models.iter() {
731        if let Ok(mut visibility) = visibilities.get_mut(entity) {
732            *visibility = if app_settings.displayed_base == DisplayedBase::RedPlane {
733                Visibility::Visible
734            } else {
735                Visibility::Hidden
736            };
737        }
738    }
739    for entity in model_queries.water_models.iter() {
740        if let Ok(mut visibility) = visibilities.get_mut(entity) {
741            *visibility = if app_settings.displayed_base == DisplayedBase::Water {
742                Visibility::Visible
743            } else {
744                Visibility::Hidden
745            };
746        }
747    }
748}
749
750/// Observer that handles changes to number inputs and updates state and the app accordingly.
751fn handle_value_change_number_input(
752    value_change: On<ValueChange<f32>>,
753    mut commands: Commands,
754    number_input_q: Query<&AppNumberInput, With<FeathersNumberInput>>,
755    mut app_settings: ResMut<AppSettings>,
756    cameras: Query<Entity, With<Camera>>,
757) {
758    if let Ok(app_number_input) = number_input_q.get(value_change.source) {
759        match app_number_input {
760            AppNumberInput::MinRoughnessStart => {
761                app_settings.min_perceptual_roughness.start = value_change.value;
762            }
763            AppNumberInput::MinRoughnessEnd => {
764                app_settings.min_perceptual_roughness.end = value_change.value;
765            }
766            AppNumberInput::MaxRoughnessStart => {
767                app_settings.max_perceptual_roughness.start = value_change.value;
768            }
769            AppNumberInput::MaxRoughnessEnd => {
770                app_settings.max_perceptual_roughness.end = value_change.value;
771            }
772            AppNumberInput::EdgeFadeoutStart => {
773                app_settings.edge_fadeout.start = value_change.value;
774            }
775            AppNumberInput::EdgeFadeoutEnd => {
776                app_settings.edge_fadeout.end = value_change.value;
777            }
778        }
779
780        commands
781            .entity(value_change.source)
782            .insert(NumberInputValue::F32(value_change.value));
783
784        update_views(commands, app_settings, cameras);
785    }
786}
787
788impl MaterialExtension for Water {
789    fn deferred_fragment_shader() -> ShaderRef {
790        SHADER_ASSET_PATH.into()
791    }
792}