ssr/
ssr.rs

1//! Demonstrates screen space reflections in deferred rendering.
2
3use std::ops::Range;
4
5use bevy::{
6    color::palettes::css::{BLACK, WHITE},
7    core_pipeline::{fxaa::Fxaa, Skybox},
8    image::{
9        ImageAddressMode, ImageFilterMode, ImageLoaderSettings, ImageSampler,
10        ImageSamplerDescriptor,
11    },
12    input::mouse::MouseWheel,
13    math::{vec3, vec4},
14    pbr::{
15        DefaultOpaqueRendererMethod, ExtendedMaterial, MaterialExtension, ScreenSpaceReflections,
16    },
17    prelude::*,
18    render::render_resource::{AsBindGroup, ShaderRef, ShaderType},
19};
20
21/// This example uses a shader source file from the assets subdirectory
22const SHADER_ASSET_PATH: &str = "shaders/water_material.wgsl";
23
24// The speed of camera movement.
25const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.1;
26const CAMERA_KEYBOARD_ORBIT_SPEED: f32 = 0.02;
27const CAMERA_MOUSE_WHEEL_ZOOM_SPEED: f32 = 0.25;
28
29// We clamp camera distances to this range.
30const CAMERA_ZOOM_RANGE: Range<f32> = 2.0..12.0;
31
32static TURN_SSR_OFF_HELP_TEXT: &str = "Press Space to turn screen-space reflections off";
33static TURN_SSR_ON_HELP_TEXT: &str = "Press Space to turn screen-space reflections on";
34static MOVE_CAMERA_HELP_TEXT: &str =
35    "Press WASD or use the mouse wheel to pan and orbit the camera";
36static SWITCH_TO_FLIGHT_HELMET_HELP_TEXT: &str = "Press Enter to switch to the flight helmet model";
37static SWITCH_TO_CUBE_HELP_TEXT: &str = "Press Enter to switch to the cube model";
38
39/// A custom [`ExtendedMaterial`] that creates animated water ripples.
40#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
41struct Water {
42    /// The normal map image.
43    ///
44    /// Note that, like all normal maps, this must not be loaded as sRGB.
45    #[texture(100)]
46    #[sampler(101)]
47    normals: Handle<Image>,
48
49    // Parameters to the water shader.
50    #[uniform(102)]
51    settings: WaterSettings,
52}
53
54/// Parameters to the water shader.
55#[derive(ShaderType, Debug, Clone)]
56struct WaterSettings {
57    /// How much to displace each octave each frame, in the u and v directions.
58    /// Two octaves are packed into each `vec4`.
59    octave_vectors: [Vec4; 2],
60    /// How wide the waves are in each octave.
61    octave_scales: Vec4,
62    /// How high the waves are in each octave.
63    octave_strengths: Vec4,
64}
65
66/// The current settings that the user has chosen.
67#[derive(Resource)]
68struct AppSettings {
69    /// Whether screen space reflections are on.
70    ssr_on: bool,
71    /// Which model is being displayed.
72    displayed_model: DisplayedModel,
73}
74
75/// Which model is being displayed.
76#[derive(Default)]
77enum DisplayedModel {
78    /// The cube is being displayed.
79    #[default]
80    Cube,
81    /// The flight helmet is being displayed.
82    FlightHelmet,
83}
84
85/// A marker component for the cube model.
86#[derive(Component)]
87struct CubeModel;
88
89/// A marker component for the flight helmet model.
90#[derive(Component)]
91struct FlightHelmetModel;
92
93fn main() {
94    // Enable deferred rendering, which is necessary for screen-space
95    // reflections at this time. Disable multisampled antialiasing, as deferred
96    // rendering doesn't support that.
97    App::new()
98        .insert_resource(DefaultOpaqueRendererMethod::deferred())
99        .init_resource::<AppSettings>()
100        .add_plugins(DefaultPlugins.set(WindowPlugin {
101            primary_window: Some(Window {
102                title: "Bevy Screen Space Reflections Example".into(),
103                ..default()
104            }),
105            ..default()
106        }))
107        .add_plugins(MaterialPlugin::<ExtendedMaterial<StandardMaterial, Water>>::default())
108        .add_systems(Startup, setup)
109        .add_systems(Update, rotate_model)
110        .add_systems(Update, move_camera)
111        .add_systems(Update, adjust_app_settings)
112        .run();
113}
114
115// Set up the scene.
116fn setup(
117    mut commands: Commands,
118    mut meshes: ResMut<Assets<Mesh>>,
119    mut standard_materials: ResMut<Assets<StandardMaterial>>,
120    mut water_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, Water>>>,
121    asset_server: Res<AssetServer>,
122    app_settings: Res<AppSettings>,
123) {
124    spawn_cube(
125        &mut commands,
126        &asset_server,
127        &mut meshes,
128        &mut standard_materials,
129    );
130    spawn_flight_helmet(&mut commands, &asset_server);
131    spawn_water(
132        &mut commands,
133        &asset_server,
134        &mut meshes,
135        &mut water_materials,
136    );
137    spawn_camera(&mut commands, &asset_server);
138    spawn_text(&mut commands, &app_settings);
139}
140
141// Spawns the rotating cube.
142fn spawn_cube(
143    commands: &mut Commands,
144    asset_server: &AssetServer,
145    meshes: &mut Assets<Mesh>,
146    standard_materials: &mut Assets<StandardMaterial>,
147) {
148    commands
149        .spawn((
150            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
151            MeshMaterial3d(standard_materials.add(StandardMaterial {
152                base_color: Color::from(WHITE),
153                base_color_texture: Some(asset_server.load("branding/icon.png")),
154                ..default()
155            })),
156            Transform::from_xyz(0.0, 0.5, 0.0),
157        ))
158        .insert(CubeModel);
159}
160
161// Spawns the flight helmet.
162fn spawn_flight_helmet(commands: &mut Commands, asset_server: &AssetServer) {
163    commands.spawn((
164        SceneRoot(
165            asset_server
166                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
167        ),
168        Transform::from_scale(Vec3::splat(2.5)),
169        FlightHelmetModel,
170        Visibility::Hidden,
171    ));
172}
173
174// Spawns the water plane.
175fn spawn_water(
176    commands: &mut Commands,
177    asset_server: &AssetServer,
178    meshes: &mut Assets<Mesh>,
179    water_materials: &mut Assets<ExtendedMaterial<StandardMaterial, Water>>,
180) {
181    commands.spawn((
182        Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))),
183        MeshMaterial3d(water_materials.add(ExtendedMaterial {
184            base: StandardMaterial {
185                base_color: BLACK.into(),
186                perceptual_roughness: 0.0,
187                ..default()
188            },
189            extension: Water {
190                normals: asset_server.load_with_settings::<Image, ImageLoaderSettings>(
191                    "textures/water_normals.png",
192                    |settings| {
193                        settings.is_srgb = false;
194                        settings.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
195                            address_mode_u: ImageAddressMode::Repeat,
196                            address_mode_v: ImageAddressMode::Repeat,
197                            mag_filter: ImageFilterMode::Linear,
198                            min_filter: ImageFilterMode::Linear,
199                            ..default()
200                        });
201                    },
202                ),
203                // These water settings are just random values to create some
204                // variety.
205                settings: WaterSettings {
206                    octave_vectors: [
207                        vec4(0.080, 0.059, 0.073, -0.062),
208                        vec4(0.153, 0.138, -0.149, -0.195),
209                    ],
210                    octave_scales: vec4(1.0, 2.1, 7.9, 14.9) * 5.0,
211                    octave_strengths: vec4(0.16, 0.18, 0.093, 0.044),
212                },
213            },
214        })),
215        Transform::from_scale(Vec3::splat(100.0)),
216    ));
217}
218
219// Spawns the camera.
220fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
221    // Create the camera. Add an environment map and skybox so the water has
222    // something interesting to reflect, other than the cube. Enable deferred
223    // rendering by adding depth and deferred prepasses. Turn on FXAA to make
224    // the scene look a little nicer. Finally, add screen space reflections.
225    commands
226        .spawn((
227            Camera3d::default(),
228            Transform::from_translation(vec3(-1.25, 2.25, 4.5)).looking_at(Vec3::ZERO, Vec3::Y),
229            Camera {
230                hdr: true,
231                ..default()
232            },
233            Msaa::Off,
234        ))
235        .insert(EnvironmentMapLight {
236            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
237            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
238            intensity: 5000.0,
239            ..default()
240        })
241        .insert(Skybox {
242            image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
243            brightness: 5000.0,
244            ..default()
245        })
246        .insert(ScreenSpaceReflections::default())
247        .insert(Fxaa::default());
248}
249
250// Spawns the help text.
251fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) {
252    commands.spawn((
253        create_text(app_settings),
254        Node {
255            position_type: PositionType::Absolute,
256            bottom: Val::Px(12.0),
257            left: Val::Px(12.0),
258            ..default()
259        },
260    ));
261}
262
263// Creates or recreates the help text.
264fn create_text(app_settings: &AppSettings) -> Text {
265    format!(
266        "{}\n{}\n{}",
267        match app_settings.displayed_model {
268            DisplayedModel::Cube => SWITCH_TO_FLIGHT_HELMET_HELP_TEXT,
269            DisplayedModel::FlightHelmet => SWITCH_TO_CUBE_HELP_TEXT,
270        },
271        if app_settings.ssr_on {
272            TURN_SSR_OFF_HELP_TEXT
273        } else {
274            TURN_SSR_ON_HELP_TEXT
275        },
276        MOVE_CAMERA_HELP_TEXT
277    )
278    .into()
279}
280
281impl MaterialExtension for Water {
282    fn deferred_fragment_shader() -> ShaderRef {
283        SHADER_ASSET_PATH.into()
284    }
285}
286
287/// Rotates the model on the Y axis a bit every frame.
288fn rotate_model(
289    mut query: Query<&mut Transform, Or<(With<CubeModel>, With<FlightHelmetModel>)>>,
290    time: Res<Time>,
291) {
292    for mut transform in query.iter_mut() {
293        transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, time.elapsed_secs(), 0.0);
294    }
295}
296
297// Processes input related to camera movement.
298fn move_camera(
299    keyboard_input: Res<ButtonInput<KeyCode>>,
300    mut mouse_wheel_input: EventReader<MouseWheel>,
301    mut cameras: Query<&mut Transform, With<Camera>>,
302) {
303    let (mut distance_delta, mut theta_delta) = (0.0, 0.0);
304
305    // Handle keyboard events.
306    if keyboard_input.pressed(KeyCode::KeyW) {
307        distance_delta -= CAMERA_KEYBOARD_ZOOM_SPEED;
308    }
309    if keyboard_input.pressed(KeyCode::KeyS) {
310        distance_delta += CAMERA_KEYBOARD_ZOOM_SPEED;
311    }
312    if keyboard_input.pressed(KeyCode::KeyA) {
313        theta_delta += CAMERA_KEYBOARD_ORBIT_SPEED;
314    }
315    if keyboard_input.pressed(KeyCode::KeyD) {
316        theta_delta -= CAMERA_KEYBOARD_ORBIT_SPEED;
317    }
318
319    // Handle mouse events.
320    for mouse_wheel_event in mouse_wheel_input.read() {
321        distance_delta -= mouse_wheel_event.y * CAMERA_MOUSE_WHEEL_ZOOM_SPEED;
322    }
323
324    // Update transforms.
325    for mut camera_transform in cameras.iter_mut() {
326        let local_z = camera_transform.local_z().as_vec3().normalize_or_zero();
327        if distance_delta != 0.0 {
328            camera_transform.translation = (camera_transform.translation.length() + distance_delta)
329                .clamp(CAMERA_ZOOM_RANGE.start, CAMERA_ZOOM_RANGE.end)
330                * local_z;
331        }
332        if theta_delta != 0.0 {
333            camera_transform
334                .translate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, theta_delta));
335            camera_transform.look_at(Vec3::ZERO, Vec3::Y);
336        }
337    }
338}
339
340// Adjusts app settings per user input.
341#[allow(clippy::too_many_arguments)]
342fn adjust_app_settings(
343    mut commands: Commands,
344    keyboard_input: Res<ButtonInput<KeyCode>>,
345    mut app_settings: ResMut<AppSettings>,
346    mut cameras: Query<Entity, With<Camera>>,
347    mut cube_models: Query<&mut Visibility, (With<CubeModel>, Without<FlightHelmetModel>)>,
348    mut flight_helmet_models: Query<&mut Visibility, (Without<CubeModel>, With<FlightHelmetModel>)>,
349    mut text: Query<&mut Text>,
350) {
351    // If there are no changes, we're going to bail for efficiency. Record that
352    // here.
353    let mut any_changes = false;
354
355    // If the user pressed Space, toggle SSR.
356    if keyboard_input.just_pressed(KeyCode::Space) {
357        app_settings.ssr_on = !app_settings.ssr_on;
358        any_changes = true;
359    }
360
361    // If the user pressed Enter, switch models.
362    if keyboard_input.just_pressed(KeyCode::Enter) {
363        app_settings.displayed_model = match app_settings.displayed_model {
364            DisplayedModel::Cube => DisplayedModel::FlightHelmet,
365            DisplayedModel::FlightHelmet => DisplayedModel::Cube,
366        };
367        any_changes = true;
368    }
369
370    // If there were no changes, bail.
371    if !any_changes {
372        return;
373    }
374
375    // Update SSR settings.
376    for camera in cameras.iter_mut() {
377        if app_settings.ssr_on {
378            commands
379                .entity(camera)
380                .insert(ScreenSpaceReflections::default());
381        } else {
382            commands.entity(camera).remove::<ScreenSpaceReflections>();
383        }
384    }
385
386    // Set cube model visibility.
387    for mut cube_visibility in cube_models.iter_mut() {
388        *cube_visibility = match app_settings.displayed_model {
389            DisplayedModel::Cube => Visibility::Visible,
390            _ => Visibility::Hidden,
391        }
392    }
393
394    // Set flight helmet model visibility.
395    for mut flight_helmet_visibility in flight_helmet_models.iter_mut() {
396        *flight_helmet_visibility = match app_settings.displayed_model {
397            DisplayedModel::FlightHelmet => Visibility::Visible,
398            _ => Visibility::Hidden,
399        };
400    }
401
402    // Update the help text.
403    for mut text in text.iter_mut() {
404        *text = create_text(&app_settings);
405    }
406}
407
408impl Default for AppSettings {
409    fn default() -> Self {
410        Self {
411            ssr_on: true,
412            displayed_model: default(),
413        }
414    }
415}