Skip to main content

atmosphere/
atmosphere.rs

1//! This example showcases pbr atmospheric scattering
2#[cfg(feature = "free_camera")]
3use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin};
4use std::f32::consts::PI;
5
6use 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)]
31struct GameState {
32    paused: bool,
33}
34
35#[derive(Resource)]
36struct AtmospherePresets {
37    earth: Handle<ScatteringMedium>,
38    mars: Handle<ScatteringMedium>,
39}
40
41fn 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
61fn 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
71fn 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
134fn 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)]
184struct Terrain;
185
186/// A custom [`ExtendedMaterial`] that creates animated water ripples.
187#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
188struct 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)]
203struct 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
213impl MaterialExtension for Water {
214    fn deferred_fragment_shader() -> ShaderRef {
215        "shaders/water_material.wesl".into()
216    }
217}
218
219fn 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.
268fn 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
314fn 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}