fog/
fog.rs

1//! Distance-based fog visual effects are used in many games to give a soft falloff of visibility to the player for performance and/or visual design reasons. The further away something in a 3D world is from the camera, the more it's mixed or completely overwritten by a given color.
2//!
3//! In Bevy we can add the [`DistanceFog`] component to the same entity as our [`Camera3d`] to apply a distance fog effect. It has fields for color, directional light parameters, and how the fog falls off over distance. And that's it! The distance fog is now applied to the camera.
4//!
5//! The [`FogFalloff`] field controls most of the behavior of the fog through different descriptions of fog "curves". I.e. [`FogFalloff::Linear`] lets us define a start and end distance where up until the start distance none of the fog color is mixed in and by the end distance the fog color is as mixed in as it can be. [`FogFalloff::Exponential`] on the other hand uses an exponential curve to drive how "visible" things are with a density value.
6//!
7//! [Atmospheric fog](https://bevy.org/examples/3d-rendering/atmospheric-fog/) is another fog type that uses this same method of setup, but isn't covered here as it is a kind of fog that is most often used to imply distance and size in clear weather, while the ones shown off here are much more "dense".
8//!
9//! The bulk of this example is spent building a scene that suites showing off that the fog is working as intended by creating a pyramid (a 3D structure with clear delineations), a light source, input handling to modify fog settings, and UI to show what the current fog settings are.
10//!
11//! ## Controls
12//!
13//! | Key Binding        | Action                              |
14//! |:-------------------|:------------------------------------|
15//! | `1` / `2` / `3`    | Fog Falloff Mode                    |
16//! | `A` / `S`          | Move Start Distance (Linear Fog)    |
17//! |                    | Change Density (Exponential Fogs)   |
18//! | `Z` / `X`          | Move End Distance (Linear Fog)      |
19//! | `-` / `=`          | Adjust Fog Red Channel              |
20//! | `[` / `]`          | Adjust Fog Green Channel            |
21//! | `;` / `'`          | Adjust Fog Blue Channel             |
22//! | `.` / `?`          | Adjust Fog Alpha Channel            |
23
24use bevy::{
25    light::{NotShadowCaster, NotShadowReceiver},
26    math::ops,
27    prelude::*,
28};
29
30fn main() {
31    App::new()
32        .insert_resource(AmbientLight::NONE)
33        .add_plugins(DefaultPlugins)
34        .add_systems(
35            Startup,
36            (setup_camera_fog, setup_pyramid_scene, setup_instructions),
37        )
38        .add_systems(Update, update_system)
39        .run();
40}
41
42fn setup_camera_fog(mut commands: Commands) {
43    commands.spawn((
44        Camera3d::default(),
45        DistanceFog {
46            color: Color::srgb(0.25, 0.25, 0.25),
47            falloff: FogFalloff::Linear {
48                start: 5.0,
49                end: 20.0,
50            },
51            ..default()
52        },
53    ));
54}
55
56fn setup_pyramid_scene(
57    mut commands: Commands,
58    mut meshes: ResMut<Assets<Mesh>>,
59    mut materials: ResMut<Assets<StandardMaterial>>,
60) {
61    let stone = materials.add(StandardMaterial {
62        base_color: Srgba::hex("28221B").unwrap().into(),
63        perceptual_roughness: 1.0,
64        ..default()
65    });
66
67    // pillars
68    for (x, z) in &[(-1.5, -1.5), (1.5, -1.5), (1.5, 1.5), (-1.5, 1.5)] {
69        commands.spawn((
70            Mesh3d(meshes.add(Cuboid::new(1.0, 3.0, 1.0))),
71            MeshMaterial3d(stone.clone()),
72            Transform::from_xyz(*x, 1.5, *z),
73        ));
74    }
75
76    // orb
77    commands.spawn((
78        Mesh3d(meshes.add(Sphere::default())),
79        MeshMaterial3d(materials.add(StandardMaterial {
80            base_color: Srgba::hex("126212CC").unwrap().into(),
81            reflectance: 1.0,
82            perceptual_roughness: 0.0,
83            metallic: 0.5,
84            alpha_mode: AlphaMode::Blend,
85            ..default()
86        })),
87        Transform::from_scale(Vec3::splat(1.75)).with_translation(Vec3::new(0.0, 4.0, 0.0)),
88        NotShadowCaster,
89        NotShadowReceiver,
90    ));
91
92    // steps
93    for i in 0..50 {
94        let half_size = i as f32 / 2.0 + 3.0;
95        let y = -i as f32 / 2.0;
96        commands.spawn((
97            Mesh3d(meshes.add(Cuboid::new(2.0 * half_size, 0.5, 2.0 * half_size))),
98            MeshMaterial3d(stone.clone()),
99            Transform::from_xyz(0.0, y + 0.25, 0.0),
100        ));
101    }
102
103    // sky
104    commands.spawn((
105        Mesh3d(meshes.add(Cuboid::new(2.0, 1.0, 1.0))),
106        MeshMaterial3d(materials.add(StandardMaterial {
107            base_color: Srgba::hex("888888").unwrap().into(),
108            unlit: true,
109            cull_mode: None,
110            ..default()
111        })),
112        Transform::from_scale(Vec3::splat(1_000_000.0)),
113    ));
114
115    // light
116    commands.spawn((
117        PointLight {
118            shadows_enabled: true,
119            ..default()
120        },
121        Transform::from_xyz(0.0, 1.0, 0.0),
122    ));
123}
124
125fn setup_instructions(mut commands: Commands) {
126    commands.spawn((
127        Text::default(),
128        Node {
129            position_type: PositionType::Absolute,
130            top: px(12),
131            left: px(12),
132            ..default()
133        },
134    ));
135}
136
137fn update_system(
138    camera: Single<(&mut DistanceFog, &mut Transform)>,
139    mut text: Single<&mut Text>,
140    time: Res<Time>,
141    keycode: Res<ButtonInput<KeyCode>>,
142) {
143    let now = time.elapsed_secs();
144    let delta = time.delta_secs();
145
146    let (mut fog, mut transform) = camera.into_inner();
147
148    // Orbit camera around pyramid
149    let orbit_scale = 8.0 + ops::sin(now / 10.0) * 7.0;
150    *transform = Transform::from_xyz(
151        ops::cos(now / 5.0) * orbit_scale,
152        12.0 - orbit_scale / 2.0,
153        ops::sin(now / 5.0) * orbit_scale,
154    )
155    .looking_at(Vec3::ZERO, Vec3::Y);
156
157    // Fog Information
158    text.0 = format!("Fog Falloff: {:?}\nFog Color: {:?}", fog.falloff, fog.color);
159
160    // Fog Falloff Mode Switching
161    text.push_str("\n\n1 / 2 / 3 - Fog Falloff Mode");
162
163    if keycode.pressed(KeyCode::Digit1) {
164        if let FogFalloff::Linear { .. } = fog.falloff {
165            // No change
166        } else {
167            fog.falloff = FogFalloff::Linear {
168                start: 5.0,
169                end: 20.0,
170            };
171        };
172    }
173
174    if keycode.pressed(KeyCode::Digit2) {
175        if let FogFalloff::Exponential { .. } = fog.falloff {
176            // No change
177        } else if let FogFalloff::ExponentialSquared { density } = fog.falloff {
178            fog.falloff = FogFalloff::Exponential { density };
179        } else {
180            fog.falloff = FogFalloff::Exponential { density: 0.07 };
181        };
182    }
183
184    if keycode.pressed(KeyCode::Digit3) {
185        if let FogFalloff::Exponential { density } = fog.falloff {
186            fog.falloff = FogFalloff::ExponentialSquared { density };
187        } else if let FogFalloff::ExponentialSquared { .. } = fog.falloff {
188            // No change
189        } else {
190            fog.falloff = FogFalloff::ExponentialSquared { density: 0.07 };
191        };
192    }
193
194    // Linear Fog Controls
195    if let FogFalloff::Linear { start, end } = &mut fog.falloff {
196        text.push_str("\nA / S - Move Start Distance\nZ / X - Move End Distance");
197
198        if keycode.pressed(KeyCode::KeyA) {
199            *start -= delta * 3.0;
200        }
201        if keycode.pressed(KeyCode::KeyS) {
202            *start += delta * 3.0;
203        }
204        if keycode.pressed(KeyCode::KeyZ) {
205            *end -= delta * 3.0;
206        }
207        if keycode.pressed(KeyCode::KeyX) {
208            *end += delta * 3.0;
209        }
210    }
211
212    // Exponential Fog Controls
213    if let FogFalloff::Exponential { density } = &mut fog.falloff {
214        text.push_str("\nA / S - Change Density");
215
216        if keycode.pressed(KeyCode::KeyA) {
217            *density -= delta * 0.5 * *density;
218            if *density < 0.0 {
219                *density = 0.0;
220            }
221        }
222        if keycode.pressed(KeyCode::KeyS) {
223            *density += delta * 0.5 * *density;
224        }
225    }
226
227    // ExponentialSquared Fog Controls
228    if let FogFalloff::ExponentialSquared { density } = &mut fog.falloff {
229        text.push_str("\nA / S - Change Density");
230
231        if keycode.pressed(KeyCode::KeyA) {
232            *density -= delta * 0.5 * *density;
233            if *density < 0.0 {
234                *density = 0.0;
235            }
236        }
237        if keycode.pressed(KeyCode::KeyS) {
238            *density += delta * 0.5 * *density;
239        }
240    }
241
242    // RGBA Controls
243    text.push_str("\n\n- / = - Red\n[ / ] - Green\n; / ' - Blue\n. / ? - Alpha");
244
245    // We're performing various operations in the sRGB color space,
246    // so we convert the fog color to sRGB here, then modify it,
247    // and finally when we're done we can convert it back and set it.
248    let mut fog_color = Srgba::from(fog.color);
249    if keycode.pressed(KeyCode::Minus) {
250        fog_color.red = (fog_color.red - 0.1 * delta).max(0.0);
251    }
252
253    if keycode.any_pressed([KeyCode::Equal, KeyCode::NumpadEqual]) {
254        fog_color.red = (fog_color.red + 0.1 * delta).min(1.0);
255    }
256
257    if keycode.pressed(KeyCode::BracketLeft) {
258        fog_color.green = (fog_color.green - 0.1 * delta).max(0.0);
259    }
260
261    if keycode.pressed(KeyCode::BracketRight) {
262        fog_color.green = (fog_color.green + 0.1 * delta).min(1.0);
263    }
264
265    if keycode.pressed(KeyCode::Semicolon) {
266        fog_color.blue = (fog_color.blue - 0.1 * delta).max(0.0);
267    }
268
269    if keycode.pressed(KeyCode::Quote) {
270        fog_color.blue = (fog_color.blue + 0.1 * delta).min(1.0);
271    }
272
273    if keycode.pressed(KeyCode::Period) {
274        fog_color.alpha = (fog_color.alpha - 0.1 * delta).max(0.0);
275    }
276
277    if keycode.pressed(KeyCode::Slash) {
278        fog_color.alpha = (fog_color.alpha + 0.1 * delta).min(1.0);
279    }
280
281    fog.color = Color::from(fog_color);
282}