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}