bevy_pbr/volumetric_fog/mod.rs
1//! Volumetric fog and volumetric lighting, also known as light shafts or god
2//! rays.
3//!
4//! This module implements a more physically-accurate, but slower, form of fog
5//! than the [`crate::fog`] module does. Notably, this *volumetric fog* allows
6//! for light beams from directional lights to shine through, creating what is
7//! known as *light shafts* or *god rays*.
8//!
9//! To add volumetric fog to a scene, add [`VolumetricFog`] to the
10//! camera, and add [`VolumetricLight`] to directional lights that you wish to
11//! be volumetric. [`VolumetricFog`] feature numerous settings that
12//! allow you to define the accuracy of the simulation, as well as the look of
13//! the fog. Currently, only interaction with directional lights that have
14//! shadow maps is supported. Note that the overhead of the effect scales
15//! directly with the number of directional lights in use, so apply
16//! [`VolumetricLight`] sparingly for the best results.
17//!
18//! The overall algorithm, which is implemented as a postprocessing effect, is a
19//! combination of the techniques described in [Scratchapixel] and [this blog
20//! post]. It uses raymarching in screen space, transformed into shadow map
21//! space for sampling and combined with physically-based modeling of absorption
22//! and scattering. Bevy employs the widely-used [Henyey-Greenstein phase
23//! function] to model asymmetry; this essentially allows light shafts to fade
24//! into and out of existence as the user views them.
25//!
26//! [Scratchapixel]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html
27//!
28//! [this blog post]: https://www.alexandre-pestana.com/volumetric-lights/
29//!
30//! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction
31
32use bevy_app::{App, Plugin};
33use bevy_asset::{load_internal_asset, Assets, Handle};
34use bevy_color::Color;
35use bevy_core_pipeline::core_3d::{
36 graph::{Core3d, Node3d},
37 prepare_core_3d_depth_textures,
38};
39use bevy_ecs::{
40 component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs as _,
41};
42use bevy_image::Image;
43use bevy_math::{
44 primitives::{Cuboid, Plane3d},
45 Vec2, Vec3,
46};
47use bevy_reflect::{std_traits::ReflectDefault, Reflect};
48use bevy_render::{
49 mesh::{Mesh, Meshable},
50 render_graph::{RenderGraphApp, ViewNodeRunner},
51 render_resource::{Shader, SpecializedRenderPipelines},
52 sync_component::SyncComponentPlugin,
53 view::Visibility,
54 ExtractSchedule, Render, RenderApp, RenderSet,
55};
56use bevy_transform::components::Transform;
57use render::{
58 VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer, CUBE_MESH, PLANE_MESH,
59 VOLUMETRIC_FOG_HANDLE,
60};
61
62use crate::graph::NodePbr;
63
64pub mod render;
65
66/// A plugin that implements volumetric fog.
67pub struct VolumetricFogPlugin;
68
69/// Add this component to a [`DirectionalLight`](crate::DirectionalLight) with a shadow map
70/// (`shadows_enabled: true`) to make volumetric fog interact with it.
71///
72/// This allows the light to generate light shafts/god rays.
73#[derive(Clone, Copy, Component, Default, Debug, Reflect)]
74#[reflect(Component, Default, Debug, Clone)]
75pub struct VolumetricLight;
76
77/// When placed on a [`bevy_core_pipeline::core_3d::Camera3d`], enables
78/// volumetric fog and volumetric lighting, also known as light shafts or god
79/// rays.
80#[derive(Clone, Copy, Component, Debug, Reflect)]
81#[reflect(Component, Default, Debug, Clone)]
82pub struct VolumetricFog {
83 /// Color of the ambient light.
84 ///
85 /// This is separate from Bevy's [`AmbientLight`](crate::light::AmbientLight) because an
86 /// [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight) is
87 /// still considered an ambient light for the purposes of volumetric fog. If you're using a
88 /// [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight), for best results,
89 /// this should be a good approximation of the average color of the environment map.
90 ///
91 /// Defaults to white.
92 pub ambient_color: Color,
93
94 /// The brightness of the ambient light.
95 ///
96 /// If there's no [`EnvironmentMapLight`](crate::environment_map::EnvironmentMapLight),
97 /// set this to 0.
98 ///
99 /// Defaults to 0.1.
100 pub ambient_intensity: f32,
101
102 /// The maximum distance to offset the ray origin randomly by, in meters.
103 ///
104 /// This is intended for use with temporal antialiasing. It helps fog look
105 /// less blocky by varying the start position of the ray, using interleaved
106 /// gradient noise.
107 pub jitter: f32,
108
109 /// The number of raymarching steps to perform.
110 ///
111 /// Higher values produce higher-quality results with less banding, but
112 /// reduce performance.
113 ///
114 /// The default value is 64.
115 pub step_count: u32,
116}
117
118#[derive(Clone, Component, Debug, Reflect)]
119#[reflect(Component, Default, Debug, Clone)]
120#[require(Transform, Visibility)]
121pub struct FogVolume {
122 /// The color of the fog.
123 ///
124 /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light
125 /// in order for this color to appear.
126 ///
127 /// Defaults to white.
128 pub fog_color: Color,
129
130 /// The density of fog, which measures how dark the fog is.
131 ///
132 /// The default value is 0.1.
133 pub density_factor: f32,
134
135 /// Optional 3D voxel density texture for the fog.
136 pub density_texture: Option<Handle<Image>>,
137
138 /// Configurable offset of the density texture in UVW coordinates.
139 ///
140 /// This can be used to scroll a repeating density texture in a direction over time
141 /// to create effects like fog moving in the wind. Make sure to configure the texture
142 /// to use `ImageAddressMode::Repeat` if this is your intention.
143 ///
144 /// Has no effect when no density texture is present.
145 ///
146 /// The default value is (0, 0, 0).
147 pub density_texture_offset: Vec3,
148
149 /// The absorption coefficient, which measures what fraction of light is
150 /// absorbed by the fog at each step.
151 ///
152 /// Increasing this value makes the fog darker.
153 ///
154 /// The default value is 0.3.
155 pub absorption: f32,
156
157 /// The scattering coefficient, which measures the fraction of light that's
158 /// scattered toward, and away from, the viewer.
159 ///
160 /// The default value is 0.3.
161 pub scattering: f32,
162
163 /// Measures the fraction of light that's scattered *toward* the camera, as
164 /// opposed to *away* from the camera.
165 ///
166 /// Increasing this value makes light shafts become more prominent when the
167 /// camera is facing toward their source and less prominent when the camera
168 /// is facing away. Essentially, a high value here means the light shafts
169 /// will fade into view as the camera focuses on them and fade away when the
170 /// camera is pointing away.
171 ///
172 /// The default value is 0.8.
173 pub scattering_asymmetry: f32,
174
175 /// Applies a nonphysical color to the light.
176 ///
177 /// This can be useful for artistic purposes but is nonphysical.
178 ///
179 /// The default value is white.
180 pub light_tint: Color,
181
182 /// Scales the light by a fixed fraction.
183 ///
184 /// This can be useful for artistic purposes but is nonphysical.
185 ///
186 /// The default value is 1.0, which results in no adjustment.
187 pub light_intensity: f32,
188}
189
190impl Plugin for VolumetricFogPlugin {
191 fn build(&self, app: &mut App) {
192 load_internal_asset!(
193 app,
194 VOLUMETRIC_FOG_HANDLE,
195 "volumetric_fog.wgsl",
196 Shader::from_wgsl
197 );
198
199 let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
200 meshes.insert(&PLANE_MESH, Plane3d::new(Vec3::Z, Vec2::ONE).mesh().into());
201 meshes.insert(&CUBE_MESH, Cuboid::new(1.0, 1.0, 1.0).mesh().into());
202
203 app.register_type::<VolumetricFog>()
204 .register_type::<VolumetricLight>();
205
206 app.add_plugins(SyncComponentPlugin::<FogVolume>::default());
207
208 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
209 return;
210 };
211
212 render_app
213 .init_resource::<SpecializedRenderPipelines<VolumetricFogPipeline>>()
214 .init_resource::<VolumetricFogUniformBuffer>()
215 .add_systems(ExtractSchedule, render::extract_volumetric_fog)
216 .add_systems(
217 Render,
218 (
219 render::prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare),
220 render::prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare),
221 render::prepare_view_depth_textures_for_volumetric_fog
222 .in_set(RenderSet::Prepare)
223 .before(prepare_core_3d_depth_textures),
224 ),
225 );
226 }
227
228 fn finish(&self, app: &mut App) {
229 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
230 return;
231 };
232
233 render_app
234 .init_resource::<VolumetricFogPipeline>()
235 .add_render_graph_node::<ViewNodeRunner<VolumetricFogNode>>(
236 Core3d,
237 NodePbr::VolumetricFog,
238 )
239 .add_render_graph_edges(
240 Core3d,
241 // Volumetric fog is a postprocessing effect. Run it after the
242 // main pass but before bloom.
243 (Node3d::EndMainPass, NodePbr::VolumetricFog, Node3d::Bloom),
244 );
245 }
246}
247
248impl Default for VolumetricFog {
249 fn default() -> Self {
250 Self {
251 step_count: 64,
252 // Matches `AmbientLight` defaults.
253 ambient_color: Color::WHITE,
254 ambient_intensity: 0.1,
255 jitter: 0.0,
256 }
257 }
258}
259
260impl Default for FogVolume {
261 fn default() -> Self {
262 Self {
263 absorption: 0.3,
264 scattering: 0.3,
265 density_factor: 0.1,
266 density_texture: None,
267 density_texture_offset: Vec3::ZERO,
268 scattering_asymmetry: 0.5,
269 fog_color: Color::WHITE,
270 light_tint: Color::WHITE,
271 light_intensity: 1.0,
272 }
273 }
274}