docs/bevy/examples/3d/fog.rs 9.7 K · 282 lines · raw · history

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
24 use bevy::{
25 light::{NotShadowCaster, NotShadowReceiver},
26 math::ops,
27 prelude::*,
28 };
29
30 fn main() {
31 App::new()
32 .insert_resource(GlobalAmbientLight::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
42 fn 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
56 fn 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 shadow_maps_enabled: true,
119 ..default()
120 },
121 Transform::from_xyz(0.0, 1.0, 0.0),
122 ));
123 }
124
125 fn 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
137 fn 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 }