Skip to main content

volumetric_fog/
volumetric_fog.rs

1//! Demonstrates volumetric fog and lighting (light shafts or god rays).
2//! Note: On Wasm, this example only runs on WebGPU
3
4use bevy::{
5    color::palettes::css::RED,
6    core_pipeline::tonemapping::Tonemapping,
7    light::Skybox,
8    light::{FogVolume, VolumetricFog, VolumetricLight},
9    math::vec3,
10    post_process::bloom::Bloom,
11    prelude::*,
12};
13
14const DIRECTIONAL_LIGHT_MOVEMENT_SPEED: f32 = 0.02;
15
16/// The current settings that the user has chosen.
17#[derive(Resource)]
18struct AppSettings {
19    /// Whether volumetric spot light is on.
20    volumetric_spotlight: bool,
21    /// Whether volumetric point light is on.
22    volumetric_pointlight: bool,
23}
24
25impl Default for AppSettings {
26    fn default() -> Self {
27        Self {
28            volumetric_spotlight: true,
29            volumetric_pointlight: true,
30        }
31    }
32}
33
34// Define a struct to store parameters for the point light's movement.
35#[derive(Component)]
36struct MoveBackAndForthHorizontally {
37    min_x: f32,
38    max_x: f32,
39    speed: f32,
40}
41
42fn main() {
43    App::new()
44        .add_plugins(DefaultPlugins)
45        .insert_resource(ClearColor(Color::Srgba(Srgba {
46            red: 0.02,
47            green: 0.02,
48            blue: 0.02,
49            alpha: 1.0,
50        })))
51        .insert_resource(GlobalAmbientLight::NONE)
52        .init_resource::<AppSettings>()
53        .add_systems(Startup, setup)
54        .add_systems(Update, tweak_scene)
55        .add_systems(Update, (move_directional_light, move_point_light))
56        .add_systems(Update, adjust_app_settings)
57        .run();
58}
59
60/// Initializes the scene.
61fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
62    // Spawn the glTF scene.
63    commands.spawn(WorldAssetRoot(asset_server.load(
64        GltfAssetLabel::Scene(0).from_asset("models/VolumetricFogExample/VolumetricFogExample.glb"),
65    )));
66
67    // Spawn the camera.
68    commands
69        .spawn((
70            Camera3d::default(),
71            Transform::from_xyz(-1.7, 1.5, 4.5).looking_at(vec3(-1.5, 1.7, 3.5), Vec3::Y),
72            Tonemapping::TonyMcMapface,
73            Bloom::default(),
74        ))
75        .insert(Skybox {
76            image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
77            brightness: 1000.0,
78            ..default()
79        })
80        .insert(VolumetricFog {
81            // This value is explicitly set to 0 since we have no environment map light
82            ambient_intensity: 0.0,
83            ..default()
84        });
85
86    // Add the point light
87    commands.spawn((
88        Transform::from_xyz(-0.4, 1.9, 1.0),
89        PointLight {
90            shadow_maps_enabled: true,
91            range: 150.0,
92            color: RED.into(),
93            intensity: 10_000.0,
94            ..default()
95        },
96        VolumetricLight,
97        MoveBackAndForthHorizontally {
98            min_x: -1.93,
99            max_x: -0.4,
100            speed: -0.2,
101        },
102    ));
103
104    // Add the spot light
105    commands.spawn((
106        Transform::from_xyz(-1.8, 3.9, -2.7).looking_at(Vec3::ZERO, Vec3::Y),
107        SpotLight {
108            intensity: 50_000.0, // lumens
109            color: Color::WHITE,
110            shadow_maps_enabled: true,
111            inner_angle: 0.76,
112            outer_angle: 0.94,
113            ..default()
114        },
115        VolumetricLight,
116    ));
117
118    // Add the fog volume.
119    commands.spawn((
120        FogVolume::default(),
121        Transform::from_scale(Vec3::splat(35.0)),
122    ));
123
124    // Add the help text.
125    commands.spawn((
126        create_text(&app_settings),
127        Node {
128            position_type: PositionType::Absolute,
129            top: px(12),
130            left: px(12),
131            ..default()
132        },
133    ));
134}
135
136fn create_text(app_settings: &AppSettings) -> Text {
137    format!(
138        "{}\n{}\n{}",
139        "Press WASD or the arrow keys to change the direction of the directional light",
140        if app_settings.volumetric_pointlight {
141            "Press P to turn volumetric point light off"
142        } else {
143            "Press P to turn volumetric point light on"
144        },
145        if app_settings.volumetric_spotlight {
146            "Press L to turn volumetric spot light off"
147        } else {
148            "Press L to turn volumetric spot light on"
149        }
150    )
151    .into()
152}
153
154/// A system that makes directional lights in the glTF scene into volumetric
155/// lights with shadows.
156fn tweak_scene(
157    mut commands: Commands,
158    mut lights: Query<(Entity, &mut DirectionalLight), Changed<DirectionalLight>>,
159) {
160    for (light, mut directional_light) in lights.iter_mut() {
161        // Shadows are needed for volumetric lights to work.
162        directional_light.shadow_maps_enabled = true;
163        commands.entity(light).insert(VolumetricLight);
164    }
165}
166
167/// Processes user requests to move the directional light.
168fn move_directional_light(
169    input: Res<ButtonInput<KeyCode>>,
170    mut directional_lights: Query<&mut Transform, With<DirectionalLight>>,
171) {
172    let mut delta_theta = Vec2::ZERO;
173    if input.pressed(KeyCode::KeyW) || input.pressed(KeyCode::ArrowUp) {
174        delta_theta.y += DIRECTIONAL_LIGHT_MOVEMENT_SPEED;
175    }
176    if input.pressed(KeyCode::KeyS) || input.pressed(KeyCode::ArrowDown) {
177        delta_theta.y -= DIRECTIONAL_LIGHT_MOVEMENT_SPEED;
178    }
179    if input.pressed(KeyCode::KeyA) || input.pressed(KeyCode::ArrowLeft) {
180        delta_theta.x += DIRECTIONAL_LIGHT_MOVEMENT_SPEED;
181    }
182    if input.pressed(KeyCode::KeyD) || input.pressed(KeyCode::ArrowRight) {
183        delta_theta.x -= DIRECTIONAL_LIGHT_MOVEMENT_SPEED;
184    }
185
186    if delta_theta == Vec2::ZERO {
187        return;
188    }
189
190    let delta_quat = Quat::from_euler(EulerRot::XZY, delta_theta.y, 0.0, delta_theta.x);
191    for mut transform in directional_lights.iter_mut() {
192        transform.rotate(delta_quat);
193    }
194}
195
196// Toggle point light movement between left and right.
197fn move_point_light(
198    timer: Res<Time>,
199    mut objects: Query<(&mut Transform, &mut MoveBackAndForthHorizontally)>,
200) {
201    for (mut transform, mut move_data) in objects.iter_mut() {
202        let mut translation = transform.translation;
203        let mut need_toggle = false;
204        translation.x += move_data.speed * timer.delta_secs();
205        if translation.x > move_data.max_x {
206            translation.x = move_data.max_x;
207            need_toggle = true;
208        } else if translation.x < move_data.min_x {
209            translation.x = move_data.min_x;
210            need_toggle = true;
211        }
212        if need_toggle {
213            move_data.speed = -move_data.speed;
214        }
215        transform.translation = translation;
216    }
217}
218
219// Adjusts app settings per user input.
220fn adjust_app_settings(
221    mut commands: Commands,
222    keyboard_input: Res<ButtonInput<KeyCode>>,
223    mut app_settings: ResMut<AppSettings>,
224    mut point_lights: Query<Entity, With<PointLight>>,
225    mut spot_lights: Query<Entity, With<SpotLight>>,
226    mut text: Query<&mut Text>,
227) {
228    // If there are no changes, we're going to bail for efficiency. Record that
229    // here.
230    let mut any_changes = false;
231
232    // If the user pressed P, toggle volumetric state of the point light.
233    if keyboard_input.just_pressed(KeyCode::KeyP) {
234        app_settings.volumetric_pointlight = !app_settings.volumetric_pointlight;
235        any_changes = true;
236    }
237    // If the user pressed L, toggle volumetric state of the spot light.
238    if keyboard_input.just_pressed(KeyCode::KeyL) {
239        app_settings.volumetric_spotlight = !app_settings.volumetric_spotlight;
240        any_changes = true;
241    }
242
243    // If there were no changes, bail out.
244    if !any_changes {
245        return;
246    }
247
248    // Update volumetric settings.
249    for point_light in point_lights.iter_mut() {
250        if app_settings.volumetric_pointlight {
251            commands.entity(point_light).insert(VolumetricLight);
252        } else {
253            commands.entity(point_light).remove::<VolumetricLight>();
254        }
255    }
256    for spot_light in spot_lights.iter_mut() {
257        if app_settings.volumetric_spotlight {
258            commands.entity(spot_light).insert(VolumetricLight);
259        } else {
260            commands.entity(spot_light).remove::<VolumetricLight>();
261        }
262    }
263
264    // Update the help text.
265    for mut text in text.iter_mut() {
266        *text = create_text(&app_settings);
267    }
268}