ssr/
ssr.rs

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