1use 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
26const SHADER_ASSET_PATH: &str = "shaders/water_material.wgsl";
28
29const 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
34const 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#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
46struct Water {
47 #[texture(100)]
51 #[sampler(101)]
52 normals: Handle<Image>,
53
54 #[uniform(102)]
56 settings: WaterSettings,
57}
58
59#[derive(ShaderType, Debug, Clone)]
61struct WaterSettings {
62 octave_vectors: [Vec4; 2],
65 octave_scales: Vec4,
67 octave_strengths: Vec4,
69}
70
71#[derive(Resource)]
73struct AppSettings {
74 ssr_on: bool,
76 displayed_model: DisplayedModel,
78}
79
80#[derive(Default)]
82enum DisplayedModel {
83 #[default]
85 Cube,
86 FlightHelmet,
88}
89
90#[derive(Component)]
92struct CubeModel;
93
94#[derive(Component)]
96struct FlightHelmetModel;
97
98fn main() {
99 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
120fn 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
146fn 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
166fn 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
179fn 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 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
224fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
226 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
252fn 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
265fn 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
289fn 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
299fn 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 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 for mouse_wheel in mouse_wheel_reader.read() {
323 distance_delta -= mouse_wheel.y * CAMERA_MOUSE_WHEEL_ZOOM_SPEED;
324 }
325
326 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
342fn 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 let mut any_changes = false;
355
356 if keyboard_input.just_pressed(KeyCode::Space) {
358 app_settings.ssr_on = !app_settings.ssr_on;
359 any_changes = true;
360 }
361
362 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 !any_changes {
373 return;
374 }
375
376 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 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 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 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}