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