docs/bevy/examples/3d/atmosphere.rs 11.2 K · 324 lines · raw · history

1 //! This example showcases pbr atmospheric scattering
2 #[cfg(feature = "free_camera")]
3 use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin};
4 use std::f32::consts::PI;
5
6 use bevy::{
7 anti_alias::taa::TemporalAntiAliasing,
8 camera::Exposure,
9 color::palettes::css::BLACK,
10 core_pipeline::tonemapping::Tonemapping,
11 image::{
12 ImageAddressMode, ImageFilterMode, ImageLoaderSettings, ImageSampler,
13 ImageSamplerDescriptor,
14 },
15 input::keyboard::KeyCode,
16 light::{
17 atmosphere::ScatteringMedium, light_consts::lux, Atmosphere, AtmosphereEnvironmentMapLight,
18 FogVolume, SunDisk, VolumetricFog, VolumetricLight,
19 },
20 pbr::{
21 AtmosphereMode, AtmosphereSettings, DefaultOpaqueRendererMethod, ExtendedMaterial,
22 MaterialExtension, ScreenSpaceReflections,
23 },
24 post_process::bloom::Bloom,
25 prelude::*,
26 render::render_resource::{AsBindGroup, ShaderType},
27 shader::ShaderRef,
28 };
29
30 #[derive(Resource, Default)]
31 struct GameState {
32 paused: bool,
33 }
34
35 #[derive(Resource)]
36 struct AtmospherePresets {
37 earth: Handle<ScatteringMedium>,
38 mars: Handle<ScatteringMedium>,
39 }
40
41 fn main() {
42 App::new()
43 .insert_resource(DefaultOpaqueRendererMethod::deferred())
44 .insert_resource(ClearColor(Color::BLACK))
45 .insert_resource(GameState::default())
46 .insert_resource(GlobalAmbientLight::NONE)
47 .add_plugins((
48 DefaultPlugins,
49 #[cfg(feature = "free_camera")]
50 FreeCameraPlugin,
51 ))
52 .add_plugins(MaterialPlugin::<ExtendedMaterial<StandardMaterial, Water>>::default())
53 .add_systems(
54 Startup,
55 (setup_camera_fog, setup_terrain_scene, print_controls),
56 )
57 .add_systems(Update, (dynamic_scene, atmosphere_controls))
58 .run();
59 }
60
61 fn print_controls() {
62 println!("Atmosphere Example Controls:");
63 println!(" 1 - Switch to lookup texture rendering method");
64 println!(" 2 - Switch to raymarched rendering method");
65 println!(" 3 - Switch to Earth atmosphere");
66 println!(" 4 - Switch to Mars atmosphere");
67 println!(" Enter - Pause/Resume sun motion");
68 println!(" Up/Down - Increase/Decrease exposure");
69 }
70
71 fn atmosphere_controls(
72 keyboard_input: Res<ButtonInput<KeyCode>>,
73 mut planet_atmosphere: Query<(&mut Atmosphere, &mut GlobalTransform)>,
74 mut camera_settings: Query<&mut AtmosphereSettings, With<Camera3d>>,
75 mut sun_disks: Query<&mut SunDisk, With<DirectionalLight>>,
76 atmosphere_presets: Res<AtmospherePresets>,
77 mut game_state: ResMut<GameState>,
78 mut camera_exposure: Query<&mut Exposure, With<Camera3d>>,
79 time: Res<Time>,
80 ) {
81 if keyboard_input.just_pressed(KeyCode::Digit3) {
82 for (mut atmosphere, mut transform) in &mut planet_atmosphere {
83 *atmosphere = Atmosphere::earth(atmosphere_presets.earth.clone());
84 *transform = GlobalTransform::from_translation(-Vec3::Y * atmosphere.inner_radius);
85 }
86 for mut sun_disk in &mut sun_disks {
87 sun_disk.angular_size = SunDisk::EARTH.angular_size;
88 }
89 println!("Switched to Earth atmosphere");
90 }
91
92 if keyboard_input.just_pressed(KeyCode::Digit4) {
93 for (mut atmosphere, mut transform) in &mut planet_atmosphere {
94 *atmosphere = Atmosphere::mars(atmosphere_presets.mars.clone());
95 *transform = GlobalTransform::from_translation(-Vec3::Y * atmosphere.inner_radius);
96 }
97 for mut sun_disk in &mut sun_disks {
98 sun_disk.angular_size = SunDisk::MARS.angular_size;
99 }
100 println!("Switched to Mars atmosphere");
101 }
102
103 if keyboard_input.just_pressed(KeyCode::Digit1) {
104 for mut settings in &mut camera_settings {
105 settings.rendering_method = AtmosphereMode::LookupTexture;
106 println!("Switched to lookup texture rendering method");
107 }
108 }
109
110 if keyboard_input.just_pressed(KeyCode::Digit2) {
111 for mut settings in &mut camera_settings {
112 settings.rendering_method = AtmosphereMode::Raymarched;
113 println!("Switched to raymarched rendering method");
114 }
115 }
116
117 if keyboard_input.just_pressed(KeyCode::Enter) {
118 game_state.paused = !game_state.paused;
119 }
120
121 if keyboard_input.pressed(KeyCode::ArrowUp) {
122 for mut exposure in &mut camera_exposure {
123 exposure.ev100 -= time.delta_secs() * 2.0;
124 }
125 }
126
127 if keyboard_input.pressed(KeyCode::ArrowDown) {
128 for mut exposure in &mut camera_exposure {
129 exposure.ev100 += time.delta_secs() * 2.0;
130 }
131 }
132 }
133
134 fn setup_camera_fog(
135 mut commands: Commands,
136 mut scattering_mediums: ResMut<Assets<ScatteringMedium>>,
137 asset_server: Res<AssetServer>,
138 ) {
139 let earth_medium = scattering_mediums.add(ScatteringMedium::earth(256, 256));
140 let mars_phase = asset_server.load("textures/mars_mie_phase.ktx2");
141 let mars_medium = scattering_mediums.add(ScatteringMedium::mars(256, 256, mars_phase));
142
143 commands.insert_resource(AtmospherePresets {
144 earth: earth_medium.clone(),
145 mars: mars_medium.clone(),
146 });
147
148 // Spawn earth atmosphere
149 commands.spawn(Atmosphere::earth(earth_medium));
150
151 commands.spawn((
152 Camera3d::default(),
153 Transform::from_xyz(-2.8, 0.045, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
154 // Can be adjusted to change the rendering quality
155 AtmosphereSettings::default(),
156 // The directional light illuminance used in this scene
157 // (the one recommended for use with this feature) is
158 // quite bright, so raising the exposure compensation helps
159 // bring the scene to a nicer brightness range.
160 Exposure { ev100: 13.0 },
161 // Tonemapper chosen just because it looked good with the scene, any
162 // tonemapper would be fine :)
163 Tonemapping::AcesFitted,
164 // Bloom gives the sun a much more natural look.
165 Bloom::NATURAL,
166 // Enables the atmosphere to drive reflections and ambient lighting (IBL) for this view
167 AtmosphereEnvironmentMapLight::default(),
168 #[cfg(feature = "free_camera")]
169 FreeCamera::default(),
170 VolumetricFog {
171 ambient_intensity: 0.0,
172 ..default()
173 },
174 Msaa::Off,
175 TemporalAntiAliasing::default(),
176 ScreenSpaceReflections {
177 min_perceptual_roughness: 0.0..0.0,
178 ..default()
179 },
180 ));
181 }
182
183 #[derive(Component)]
184 struct Terrain;
185
186 /// A custom [`ExtendedMaterial`] that creates animated water ripples.
187 #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
188 struct Water {
189 /// The normal map image.
190 ///
191 /// Note that, like all normal maps, this must not be loaded as sRGB.
192 #[texture(100)]
193 #[sampler(101)]
194 normals: Handle<Image>,
195
196 // Parameters to the water shader.
197 #[uniform(102)]
198 settings: WaterSettings,
199 }
200
201 /// Parameters to the water shader.
202 #[derive(ShaderType, Debug, Clone)]
203 struct WaterSettings {
204 /// How much to displace each octave each frame, in the u and v directions.
205 /// Two octaves are packed into each `vec4`.
206 octave_vectors: [Vec4; 2],
207 /// How wide the waves are in each octave.
208 octave_scales: Vec4,
209 /// How high the waves are in each octave.
210 octave_strengths: Vec4,
211 }
212
213 impl MaterialExtension for Water {
214 fn deferred_fragment_shader() -> ShaderRef {
215 "shaders/water_material.wesl".into()
216 }
217 }
218
219 fn setup_terrain_scene(
220 mut commands: Commands,
221 mut meshes: ResMut<Assets<Mesh>>,
222 mut water_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, Water>>>,
223 asset_server: Res<AssetServer>,
224 ) {
225 // Sun
226 commands.spawn((
227 DirectionalLight {
228 shadow_maps_enabled: true,
229 // lux::RAW_SUNLIGHT is recommended for use with this feature, since
230 // other values approximate sunlight *post-scattering* in various
231 // conditions. RAW_SUNLIGHT in comparison is the illuminance of the
232 // sun unfiltered by the atmosphere, so it is the proper input for
233 // sunlight to be filtered by the atmosphere.
234 illuminance: lux::RAW_SUNLIGHT,
235 ..default()
236 },
237 Transform::from_xyz(1.0, 0.4, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
238 VolumetricLight,
239 SunDisk::EARTH,
240 ));
241
242 // spawn the fog volume
243 commands.spawn((
244 FogVolume::default(),
245 Transform::from_scale(Vec3::new(10.0, 1.0, 10.0)).with_translation(Vec3::Y * 0.5),
246 ));
247
248 // Terrain
249 commands.spawn((
250 Terrain,
251 WorldAssetRoot(
252 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/terrain/terrain.glb")),
253 ),
254 Transform::from_xyz(-1.0, 0.0, -0.5)
255 .with_scale(Vec3::splat(0.5))
256 .with_rotation(Quat::from_rotation_y(PI / 2.0)),
257 ));
258
259 spawn_water(
260 &mut commands,
261 &asset_server,
262 &mut meshes,
263 &mut water_materials,
264 );
265 }
266
267 // Spawns the water plane.
268 fn spawn_water(
269 commands: &mut Commands,
270 asset_server: &AssetServer,
271 meshes: &mut Assets<Mesh>,
272 water_materials: &mut Assets<ExtendedMaterial<StandardMaterial, Water>>,
273 ) {
274 commands.spawn((
275 Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))),
276 MeshMaterial3d(
277 water_materials.add(ExtendedMaterial {
278 base: StandardMaterial {
279 base_color: BLACK.into(),
280 perceptual_roughness: 0.0,
281 ..default()
282 },
283 extension: Water {
284 normals: asset_server
285 .load_builder()
286 .with_settings(|settings: &mut ImageLoaderSettings| {
287 settings.is_srgb = false;
288 settings.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
289 address_mode_u: ImageAddressMode::Repeat,
290 address_mode_v: ImageAddressMode::Repeat,
291 mag_filter: ImageFilterMode::Linear,
292 min_filter: ImageFilterMode::Linear,
293 ..default()
294 });
295 })
296 .load("textures/water_normals.png"),
297 // These water settings are just random values to create some
298 // variety.
299 settings: WaterSettings {
300 octave_vectors: [
301 vec4(0.080, 0.059, 0.073, -0.062),
302 vec4(0.153, 0.138, -0.149, -0.195),
303 ],
304 octave_scales: vec4(1.0, 2.1, 7.9, 14.9) * 500.0,
305 octave_strengths: vec4(0.16, 0.18, 0.093, 0.044) * 0.2,
306 },
307 },
308 }),
309 ),
310 Transform::from_scale(Vec3::splat(100.0)),
311 ));
312 }
313
314 fn dynamic_scene(
315 mut suns: Query<&mut Transform, With<DirectionalLight>>,
316 time: Res<Time>,
317 sun_motion_state: Res<GameState>,
318 ) {
319 // Only rotate the sun if motion is not paused
320 if !sun_motion_state.paused {
321 suns.iter_mut()
322 .for_each(|mut tf| tf.rotate_x(-time.delta_secs() * PI / 10.0));
323 }
324 }