1use 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
21const SHADER_ASSET_PATH: &str = "shaders/water_material.wgsl";
23
24const 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
29const 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#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
41struct Water {
42 #[texture(100)]
46 #[sampler(101)]
47 normals: Handle<Image>,
48
49 #[uniform(102)]
51 settings: WaterSettings,
52}
53
54#[derive(ShaderType, Debug, Clone)]
56struct WaterSettings {
57 octave_vectors: [Vec4; 2],
60 octave_scales: Vec4,
62 octave_strengths: Vec4,
64}
65
66#[derive(Resource)]
68struct AppSettings {
69 ssr_on: bool,
71 displayed_model: DisplayedModel,
73}
74
75#[derive(Default)]
77enum DisplayedModel {
78 #[default]
80 Cube,
81 FlightHelmet,
83}
84
85#[derive(Component)]
87struct CubeModel;
88
89#[derive(Component)]
91struct FlightHelmetModel;
92
93fn main() {
94 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
115fn 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
141fn 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
161fn 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
174fn 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 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
219fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
221 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
250fn 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
263fn 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
287fn 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
297fn 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 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 for mouse_wheel_event in mouse_wheel_input.read() {
321 distance_delta -= mouse_wheel_event.y * CAMERA_MOUSE_WHEEL_ZOOM_SPEED;
322 }
323
324 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#[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 let mut any_changes = false;
354
355 if keyboard_input.just_pressed(KeyCode::Space) {
357 app_settings.ssr_on = !app_settings.ssr_on;
358 any_changes = true;
359 }
360
361 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 !any_changes {
372 return;
373 }
374
375 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 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 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 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}