bevy_pbr/atmosphere/
mod.rs

1//! Procedural Atmospheric Scattering.
2//!
3//! This plugin implements [Hillaire's 2020 paper](https://sebh.github.io/publications/egsr2020.pdf)
4//! on real-time atmospheric scattering. While it *will* work simply as a
5//! procedural skybox, it also does much more. It supports dynamic time-of-
6//! -day, multiple directional lights, and since it's applied as a post-processing
7//! effect *on top* of the existing skybox, a starry skybox would automatically
8//! show based on the time of day. Scattering in front of terrain (similar
9//! to distance fog, but more complex) is handled as well, and takes into
10//! account the directional light color and direction.
11//!
12//! Adding the [`Atmosphere`] component to a 3d camera will enable the effect,
13//! which by default is set to look similar to Earth's atmosphere. See the
14//! documentation on the component itself for information regarding its fields.
15//!
16//! Performance-wise, the effect should be fairly cheap since the LUTs (Look
17//! Up Tables) that encode most of the data are small, and take advantage of the
18//! fact that the atmosphere is symmetric. Performance is also proportional to
19//! the number of directional lights in the scene. In order to tune
20//! performance more finely, the [`AtmosphereSettings`] camera component
21//! manages the size of each LUT and the sample count for each ray.
22//!
23//! Given how similar it is to [`crate::volumetric_fog`], it might be expected
24//! that these two modules would work together well. However for now using both
25//! at once is untested, and might not be physically accurate. These may be
26//! integrated into a single module in the future.
27//!
28//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels
29//! through the atmosphere, we use a simpler averaging technique instead of the more
30//! complex blending operations. This difference will be resolved for WebGPU in a future release.
31//!
32//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW
33//!
34//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere
35
36mod environment;
37mod node;
38pub mod resources;
39
40use bevy_app::{App, Plugin, Update};
41use bevy_asset::embedded_asset;
42use bevy_camera::Camera3d;
43use bevy_core_pipeline::core_3d::graph::Node3d;
44use bevy_ecs::{
45    component::Component,
46    query::{Changed, QueryItem, With},
47    schedule::IntoScheduleConfigs,
48    system::{lifetimeless::Read, Query},
49};
50use bevy_math::{UVec2, UVec3, Vec3};
51use bevy_reflect::{std_traits::ReflectDefault, Reflect};
52use bevy_render::{
53    extract_component::UniformComponentPlugin,
54    render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
55    view::Hdr,
56    RenderStartup,
57};
58use bevy_render::{
59    extract_component::{ExtractComponent, ExtractComponentPlugin},
60    render_graph::{RenderGraphExt, ViewNodeRunner},
61    render_resource::{TextureFormat, TextureUsages},
62    renderer::RenderAdapter,
63    Render, RenderApp, RenderSystems,
64};
65
66use bevy_core_pipeline::core_3d::graph::Core3d;
67use bevy_shader::load_shader_library;
68use environment::{
69    init_atmosphere_probe_layout, init_atmosphere_probe_pipeline,
70    prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components,
71    prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode,
72};
73use resources::{
74    prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms,
75    RenderSkyBindGroupLayouts,
76};
77use tracing::warn;
78
79use self::{
80    node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode},
81    resources::{
82        prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
83        AtmosphereLutPipelines, AtmosphereSamplers,
84    },
85};
86
87#[doc(hidden)]
88pub struct AtmospherePlugin;
89
90impl Plugin for AtmospherePlugin {
91    fn build(&self, app: &mut App) {
92        load_shader_library!(app, "types.wgsl");
93        load_shader_library!(app, "functions.wgsl");
94        load_shader_library!(app, "bruneton_functions.wgsl");
95        load_shader_library!(app, "bindings.wgsl");
96
97        embedded_asset!(app, "transmittance_lut.wgsl");
98        embedded_asset!(app, "multiscattering_lut.wgsl");
99        embedded_asset!(app, "sky_view_lut.wgsl");
100        embedded_asset!(app, "aerial_view_lut.wgsl");
101        embedded_asset!(app, "render_sky.wgsl");
102        embedded_asset!(app, "environment.wgsl");
103
104        app.add_plugins((
105            ExtractComponentPlugin::<Atmosphere>::default(),
106            ExtractComponentPlugin::<GpuAtmosphereSettings>::default(),
107            ExtractComponentPlugin::<AtmosphereEnvironmentMap>::default(),
108            UniformComponentPlugin::<Atmosphere>::default(),
109            UniformComponentPlugin::<GpuAtmosphereSettings>::default(),
110        ))
111        .add_systems(Update, prepare_atmosphere_probe_components);
112    }
113
114    fn finish(&self, app: &mut App) {
115        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
116            return;
117        };
118
119        let render_adapter = render_app.world().resource::<RenderAdapter>();
120
121        if !render_adapter
122            .get_downlevel_capabilities()
123            .flags
124            .contains(DownlevelFlags::COMPUTE_SHADERS)
125        {
126            warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
127            return;
128        }
129
130        if !render_adapter
131            .get_texture_format_features(TextureFormat::Rgba16Float)
132            .allowed_usages
133            .contains(TextureUsages::STORAGE_BINDING)
134        {
135            warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
136            return;
137        }
138
139        render_app
140            .init_resource::<AtmosphereBindGroupLayouts>()
141            .init_resource::<RenderSkyBindGroupLayouts>()
142            .init_resource::<AtmosphereSamplers>()
143            .init_resource::<AtmosphereLutPipelines>()
144            .init_resource::<AtmosphereTransforms>()
145            .init_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
146            .add_systems(
147                RenderStartup,
148                (init_atmosphere_probe_layout, init_atmosphere_probe_pipeline).chain(),
149            )
150            .add_systems(
151                Render,
152                (
153                    configure_camera_depth_usages.in_set(RenderSystems::ManageViews),
154                    queue_render_sky_pipelines.in_set(RenderSystems::Queue),
155                    prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources),
156                    prepare_probe_textures
157                        .in_set(RenderSystems::PrepareResources)
158                        .after(prepare_atmosphere_textures),
159                    prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups),
160                    prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources),
161                    prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups),
162                ),
163            )
164            .add_render_graph_node::<ViewNodeRunner<AtmosphereLutsNode>>(
165                Core3d,
166                AtmosphereNode::RenderLuts,
167            )
168            .add_render_graph_edges(
169                Core3d,
170                (
171                    // END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS
172                    Node3d::EndPrepasses,
173                    AtmosphereNode::RenderLuts,
174                    Node3d::StartMainPass,
175                ),
176            )
177            .add_render_graph_node::<ViewNodeRunner<RenderSkyNode>>(
178                Core3d,
179                AtmosphereNode::RenderSky,
180            )
181            .add_render_graph_node::<EnvironmentNode>(Core3d, AtmosphereNode::Environment)
182            .add_render_graph_edges(
183                Core3d,
184                (
185                    Node3d::MainOpaquePass,
186                    AtmosphereNode::RenderSky,
187                    Node3d::MainTransparentPass,
188                ),
189            );
190    }
191}
192
193/// This component describes the atmosphere of a planet, and when added to a camera
194/// will enable atmospheric scattering for that camera. This is only compatible with
195/// HDR cameras.
196///
197/// Most atmospheric particles scatter and absorb light in two main ways:
198///
199/// Rayleigh scattering occurs among very small particles, like individual gas
200/// molecules. It's wavelength dependent, and causes colors to separate out as
201/// light travels through the atmosphere. These particles *don't* absorb light.
202///
203/// Mie scattering occurs among slightly larger particles, like dust and sea spray.
204/// These particles *do* absorb light, but Mie scattering and absorption is
205/// *wavelength independent*.
206///
207/// Ozone acts differently from the other two, and is special-cased because
208/// it's very important to the look of Earth's atmosphere. It's wavelength
209/// dependent, but only *absorbs* light. Also, while the density of particles
210/// participating in Rayleigh and Mie scattering falls off roughly exponentially
211/// from the planet's surface, ozone only exists in a band centered at a fairly
212/// high altitude.
213#[derive(Clone, Component, Reflect, ShaderType)]
214#[require(AtmosphereSettings, Hdr)]
215#[reflect(Clone, Default)]
216pub struct Atmosphere {
217    /// Radius of the planet
218    ///
219    /// units: m
220    pub bottom_radius: f32,
221
222    /// Radius at which we consider the atmosphere to 'end' for our
223    /// calculations (from center of planet)
224    ///
225    /// units: m
226    pub top_radius: f32,
227
228    /// An approximation of the average albedo (or color, roughly) of the
229    /// planet's surface. This is used when calculating multiscattering.
230    ///
231    /// units: N/A
232    pub ground_albedo: Vec3,
233
234    /// The rate of falloff of rayleigh particulate with respect to altitude:
235    /// optical density = exp(-rayleigh_density_exp_scale * altitude in meters).
236    ///
237    /// THIS VALUE MUST BE POSITIVE
238    ///
239    /// units: N/A
240    pub rayleigh_density_exp_scale: f32,
241
242    /// The scattering optical density of rayleigh particulate, or how
243    /// much light it scatters per meter
244    ///
245    /// units: m^-1
246    pub rayleigh_scattering: Vec3,
247
248    /// The rate of falloff of mie particulate with respect to altitude:
249    /// optical density = exp(-mie_density_exp_scale * altitude in meters)
250    ///
251    /// THIS VALUE MUST BE POSITIVE
252    ///
253    /// units: N/A
254    pub mie_density_exp_scale: f32,
255
256    /// The scattering optical density of mie particulate, or how much light
257    /// it scatters per meter.
258    ///
259    /// units: m^-1
260    pub mie_scattering: f32,
261
262    /// The absorbing optical density of mie particulate, or how much light
263    /// it absorbs per meter.
264    ///
265    /// units: m^-1
266    pub mie_absorption: f32,
267
268    /// The "asymmetry" of mie scattering, or how much light tends to scatter
269    /// forwards, rather than backwards or to the side.
270    ///
271    /// domain: (-1, 1)
272    /// units: N/A
273    pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1)
274
275    /// The altitude at which the ozone layer is centered.
276    ///
277    /// units: m
278    pub ozone_layer_altitude: f32,
279
280    /// The width of the ozone layer
281    ///
282    /// units: m
283    pub ozone_layer_width: f32,
284
285    /// The optical density of ozone, or how much of each wavelength of
286    /// light it absorbs per meter.
287    ///
288    /// units: m^-1
289    pub ozone_absorption: Vec3,
290}
291
292impl Atmosphere {
293    pub const EARTH: Atmosphere = Atmosphere {
294        bottom_radius: 6_360_000.0,
295        top_radius: 6_460_000.0,
296        ground_albedo: Vec3::splat(0.3),
297        rayleigh_density_exp_scale: 1.0 / 8_000.0,
298        rayleigh_scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
299        mie_density_exp_scale: 1.0 / 1_200.0,
300        mie_scattering: 3.996e-6,
301        mie_absorption: 0.444e-6,
302        mie_asymmetry: 0.8,
303        ozone_layer_altitude: 25_000.0,
304        ozone_layer_width: 30_000.0,
305        ozone_absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
306    };
307
308    pub fn with_density_multiplier(mut self, mult: f32) -> Self {
309        self.rayleigh_scattering *= mult;
310        self.mie_scattering *= mult;
311        self.mie_absorption *= mult;
312        self.ozone_absorption *= mult;
313        self
314    }
315}
316
317impl Default for Atmosphere {
318    fn default() -> Self {
319        Self::EARTH
320    }
321}
322
323impl ExtractComponent for Atmosphere {
324    type QueryData = Read<Atmosphere>;
325
326    type QueryFilter = With<Camera3d>;
327
328    type Out = Atmosphere;
329
330    fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
331        Some(item.clone())
332    }
333}
334
335/// This component controls the resolution of the atmosphere LUTs, and
336/// how many samples are used when computing them.
337///
338/// The transmittance LUT stores the transmittance from a point in the
339/// atmosphere to the outer edge of the atmosphere in any direction,
340/// parametrized by the point's radius and the cosine of the zenith angle
341/// of the ray.
342///
343/// The multiscattering LUT stores the factor representing luminance scattered
344/// towards the camera with scattering order >2, parametrized by the point's radius
345/// and the cosine of the zenith angle of the sun.
346///
347/// The sky-view lut is essentially the actual skybox, storing the light scattered
348/// towards the camera in every direction with a cubemap.
349///
350/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
351/// scattered towards the camera at each point (RGB channels), alongside the average
352/// transmittance to that point (A channel).
353#[derive(Clone, Component, Reflect)]
354#[reflect(Clone, Default)]
355pub struct AtmosphereSettings {
356    /// The size of the transmittance LUT
357    pub transmittance_lut_size: UVec2,
358
359    /// The size of the multiscattering LUT
360    pub multiscattering_lut_size: UVec2,
361
362    /// The size of the sky-view LUT.
363    pub sky_view_lut_size: UVec2,
364
365    /// The size of the aerial-view LUT.
366    pub aerial_view_lut_size: UVec3,
367
368    /// The number of points to sample along each ray when
369    /// computing the transmittance LUT
370    pub transmittance_lut_samples: u32,
371
372    /// The number of rays to sample when computing each
373    /// pixel of the multiscattering LUT
374    pub multiscattering_lut_dirs: u32,
375
376    /// The number of points to sample when integrating along each
377    /// multiscattering ray
378    pub multiscattering_lut_samples: u32,
379
380    /// The number of points to sample along each ray when
381    /// computing the sky-view LUT.
382    pub sky_view_lut_samples: u32,
383
384    /// The number of points to sample for each slice along the z-axis
385    /// of the aerial-view LUT.
386    pub aerial_view_lut_samples: u32,
387
388    /// The maximum distance from the camera to evaluate the
389    /// aerial view LUT. The slices along the z-axis of the
390    /// texture will be distributed linearly from the camera
391    /// to this value.
392    ///
393    /// units: m
394    pub aerial_view_lut_max_distance: f32,
395
396    /// A conversion factor between scene units and meters, used to
397    /// ensure correctness at different length scales.
398    pub scene_units_to_m: f32,
399
400    /// The number of points to sample for each fragment when the using
401    /// ray marching to render the sky
402    pub sky_max_samples: u32,
403
404    /// The rendering method to use for the atmosphere.
405    pub rendering_method: AtmosphereMode,
406}
407
408impl Default for AtmosphereSettings {
409    fn default() -> Self {
410        Self {
411            transmittance_lut_size: UVec2::new(256, 128),
412            transmittance_lut_samples: 40,
413            multiscattering_lut_size: UVec2::new(32, 32),
414            multiscattering_lut_dirs: 64,
415            multiscattering_lut_samples: 20,
416            sky_view_lut_size: UVec2::new(400, 200),
417            sky_view_lut_samples: 16,
418            aerial_view_lut_size: UVec3::new(32, 32, 32),
419            aerial_view_lut_samples: 10,
420            aerial_view_lut_max_distance: 3.2e4,
421            scene_units_to_m: 1.0,
422            sky_max_samples: 16,
423            rendering_method: AtmosphereMode::LookupTexture,
424        }
425    }
426}
427
428#[derive(Clone, Component, Reflect, ShaderType)]
429#[reflect(Default)]
430pub struct GpuAtmosphereSettings {
431    pub transmittance_lut_size: UVec2,
432    pub multiscattering_lut_size: UVec2,
433    pub sky_view_lut_size: UVec2,
434    pub aerial_view_lut_size: UVec3,
435    pub transmittance_lut_samples: u32,
436    pub multiscattering_lut_dirs: u32,
437    pub multiscattering_lut_samples: u32,
438    pub sky_view_lut_samples: u32,
439    pub aerial_view_lut_samples: u32,
440    pub aerial_view_lut_max_distance: f32,
441    pub scene_units_to_m: f32,
442    pub sky_max_samples: u32,
443    pub rendering_method: u32,
444}
445
446impl Default for GpuAtmosphereSettings {
447    fn default() -> Self {
448        AtmosphereSettings::default().into()
449    }
450}
451
452impl From<AtmosphereSettings> for GpuAtmosphereSettings {
453    fn from(s: AtmosphereSettings) -> Self {
454        Self {
455            transmittance_lut_size: s.transmittance_lut_size,
456            multiscattering_lut_size: s.multiscattering_lut_size,
457            sky_view_lut_size: s.sky_view_lut_size,
458            aerial_view_lut_size: s.aerial_view_lut_size,
459            transmittance_lut_samples: s.transmittance_lut_samples,
460            multiscattering_lut_dirs: s.multiscattering_lut_dirs,
461            multiscattering_lut_samples: s.multiscattering_lut_samples,
462            sky_view_lut_samples: s.sky_view_lut_samples,
463            aerial_view_lut_samples: s.aerial_view_lut_samples,
464            aerial_view_lut_max_distance: s.aerial_view_lut_max_distance,
465            scene_units_to_m: s.scene_units_to_m,
466            sky_max_samples: s.sky_max_samples,
467            rendering_method: s.rendering_method as u32,
468        }
469    }
470}
471
472impl ExtractComponent for GpuAtmosphereSettings {
473    type QueryData = Read<AtmosphereSettings>;
474
475    type QueryFilter = (With<Camera3d>, With<Atmosphere>);
476
477    type Out = GpuAtmosphereSettings;
478
479    fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
480        Some(item.clone().into())
481    }
482}
483
484fn configure_camera_depth_usages(
485    mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<Atmosphere>)>,
486) {
487    for mut camera in &mut cameras {
488        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
489    }
490}
491
492/// Selects how the atmosphere is rendered. Choose based on scene scale and
493/// volumetric shadow quality, and based on performance needs.
494#[repr(u32)]
495#[derive(Clone, Default, Reflect, Copy)]
496pub enum AtmosphereMode {
497    /// High-performance solution tailored to scenes that are mostly inside of the atmosphere.
498    /// Uses a set of lookup textures to approximate scattering integration.
499    /// Slightly less accurate for very long-distance/space views (lighting precision
500    /// tapers as the camera moves far from the scene origin) and for sharp volumetric
501    /// (cloud/fog) shadows.
502    #[default]
503    LookupTexture = 0,
504    /// Slower, more accurate rendering method for any type of scene.
505    /// Integrates the scattering numerically with raymarching and produces sharp volumetric
506    /// (cloud/fog) shadows.
507    /// Best for cinematic shots, planets seen from orbit, and scenes requiring
508    /// accurate long-distance lighting.
509    Raymarched = 1,
510}