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 node;
37pub mod resources;
38
39use bevy_app::{App, Plugin};
40use bevy_asset::load_internal_asset;
41use bevy_core_pipeline::core_3d::graph::Node3d;
42use bevy_ecs::{
43    component::Component,
44    query::{Changed, QueryItem, With},
45    schedule::IntoScheduleConfigs,
46    system::{lifetimeless::Read, Query},
47};
48use bevy_math::{UVec2, UVec3, Vec3};
49use bevy_reflect::{std_traits::ReflectDefault, Reflect};
50use bevy_render::{
51    extract_component::UniformComponentPlugin,
52    render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
53};
54use bevy_render::{
55    extract_component::{ExtractComponent, ExtractComponentPlugin},
56    render_graph::{RenderGraphApp, ViewNodeRunner},
57    render_resource::{Shader, TextureFormat, TextureUsages},
58    renderer::RenderAdapter,
59    Render, RenderApp, RenderSet,
60};
61
62use bevy_core_pipeline::core_3d::{graph::Core3d, Camera3d};
63use resources::{
64    prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms,
65    RenderSkyBindGroupLayouts,
66};
67use tracing::warn;
68
69use self::{
70    node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode},
71    resources::{
72        prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
73        AtmosphereLutPipelines, AtmosphereSamplers,
74    },
75};
76
77mod shaders {
78    use bevy_asset::{weak_handle, Handle};
79    use bevy_render::render_resource::Shader;
80
81    pub const TYPES: Handle<Shader> = weak_handle!("ef7e147e-30a0-4513-bae3-ddde2a6c20c5");
82    pub const FUNCTIONS: Handle<Shader> = weak_handle!("7ff93872-2ee9-4598-9f88-68b02fef605f");
83    pub const BRUNETON_FUNCTIONS: Handle<Shader> =
84        weak_handle!("e2dccbb0-7322-444a-983b-e74d0a08bcda");
85    pub const BINDINGS: Handle<Shader> = weak_handle!("bcc55ce5-0fc4-451e-8393-1b9efd2612c4");
86
87    pub const TRANSMITTANCE_LUT: Handle<Shader> =
88        weak_handle!("a4187282-8cb1-42d3-889c-cbbfb6044183");
89    pub const MULTISCATTERING_LUT: Handle<Shader> =
90        weak_handle!("bde3a71a-73e9-49fe-a379-a81940c67a1e");
91    pub const SKY_VIEW_LUT: Handle<Shader> = weak_handle!("f87e007a-bf4b-4f99-9ef0-ac21d369f0e5");
92    pub const AERIAL_VIEW_LUT: Handle<Shader> =
93        weak_handle!("a3daf030-4b64-49ae-a6a7-354489597cbe");
94    pub const RENDER_SKY: Handle<Shader> = weak_handle!("09422f46-d0f7-41c1-be24-121c17d6e834");
95}
96
97#[doc(hidden)]
98pub struct AtmospherePlugin;
99
100impl Plugin for AtmospherePlugin {
101    fn build(&self, app: &mut App) {
102        load_internal_asset!(app, shaders::TYPES, "types.wgsl", Shader::from_wgsl);
103        load_internal_asset!(app, shaders::FUNCTIONS, "functions.wgsl", Shader::from_wgsl);
104        load_internal_asset!(
105            app,
106            shaders::BRUNETON_FUNCTIONS,
107            "bruneton_functions.wgsl",
108            Shader::from_wgsl
109        );
110
111        load_internal_asset!(app, shaders::BINDINGS, "bindings.wgsl", Shader::from_wgsl);
112
113        load_internal_asset!(
114            app,
115            shaders::TRANSMITTANCE_LUT,
116            "transmittance_lut.wgsl",
117            Shader::from_wgsl
118        );
119
120        load_internal_asset!(
121            app,
122            shaders::MULTISCATTERING_LUT,
123            "multiscattering_lut.wgsl",
124            Shader::from_wgsl
125        );
126
127        load_internal_asset!(
128            app,
129            shaders::SKY_VIEW_LUT,
130            "sky_view_lut.wgsl",
131            Shader::from_wgsl
132        );
133
134        load_internal_asset!(
135            app,
136            shaders::AERIAL_VIEW_LUT,
137            "aerial_view_lut.wgsl",
138            Shader::from_wgsl
139        );
140
141        load_internal_asset!(
142            app,
143            shaders::RENDER_SKY,
144            "render_sky.wgsl",
145            Shader::from_wgsl
146        );
147
148        app.register_type::<Atmosphere>()
149            .register_type::<AtmosphereSettings>()
150            .add_plugins((
151                ExtractComponentPlugin::<Atmosphere>::default(),
152                ExtractComponentPlugin::<AtmosphereSettings>::default(),
153                UniformComponentPlugin::<Atmosphere>::default(),
154                UniformComponentPlugin::<AtmosphereSettings>::default(),
155            ));
156    }
157
158    fn finish(&self, app: &mut App) {
159        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
160            return;
161        };
162
163        let render_adapter = render_app.world().resource::<RenderAdapter>();
164
165        if !render_adapter
166            .get_downlevel_capabilities()
167            .flags
168            .contains(DownlevelFlags::COMPUTE_SHADERS)
169        {
170            warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
171            return;
172        }
173
174        if !render_adapter
175            .get_texture_format_features(TextureFormat::Rgba16Float)
176            .allowed_usages
177            .contains(TextureUsages::STORAGE_BINDING)
178        {
179            warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
180            return;
181        }
182
183        render_app
184            .init_resource::<AtmosphereBindGroupLayouts>()
185            .init_resource::<RenderSkyBindGroupLayouts>()
186            .init_resource::<AtmosphereSamplers>()
187            .init_resource::<AtmosphereLutPipelines>()
188            .init_resource::<AtmosphereTransforms>()
189            .init_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
190            .add_systems(
191                Render,
192                (
193                    configure_camera_depth_usages.in_set(RenderSet::ManageViews),
194                    queue_render_sky_pipelines.in_set(RenderSet::Queue),
195                    prepare_atmosphere_textures.in_set(RenderSet::PrepareResources),
196                    prepare_atmosphere_transforms.in_set(RenderSet::PrepareResources),
197                    prepare_atmosphere_bind_groups.in_set(RenderSet::PrepareBindGroups),
198                ),
199            )
200            .add_render_graph_node::<ViewNodeRunner<AtmosphereLutsNode>>(
201                Core3d,
202                AtmosphereNode::RenderLuts,
203            )
204            .add_render_graph_edges(
205                Core3d,
206                (
207                    // END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS
208                    Node3d::EndPrepasses,
209                    AtmosphereNode::RenderLuts,
210                    Node3d::StartMainPass,
211                ),
212            )
213            .add_render_graph_node::<ViewNodeRunner<RenderSkyNode>>(
214                Core3d,
215                AtmosphereNode::RenderSky,
216            )
217            .add_render_graph_edges(
218                Core3d,
219                (
220                    Node3d::MainOpaquePass,
221                    AtmosphereNode::RenderSky,
222                    Node3d::MainTransparentPass,
223                ),
224            );
225    }
226}
227
228/// This component describes the atmosphere of a planet, and when added to a camera
229/// will enable atmospheric scattering for that camera. This is only compatible with
230/// HDR cameras.
231///
232/// Most atmospheric particles scatter and absorb light in two main ways:
233///
234/// Rayleigh scattering occurs among very small particles, like individual gas
235/// molecules. It's wavelength dependent, and causes colors to separate out as
236/// light travels through the atmosphere. These particles *don't* absorb light.
237///
238/// Mie scattering occurs among slightly larger particles, like dust and sea spray.
239/// These particles *do* absorb light, but Mie scattering and absorption is
240/// *wavelength independent*.
241///
242/// Ozone acts differently from the other two, and is special-cased because
243/// it's very important to the look of Earth's atmosphere. It's wavelength
244/// dependent, but only *absorbs* light. Also, while the density of particles
245/// participating in Rayleigh and Mie scattering falls off roughly exponentially
246/// from the planet's surface, ozone only exists in a band centered at a fairly
247/// high altitude.
248#[derive(Clone, Component, Reflect, ShaderType)]
249#[require(AtmosphereSettings)]
250#[reflect(Clone, Default)]
251pub struct Atmosphere {
252    /// Radius of the planet
253    ///
254    /// units: m
255    pub bottom_radius: f32,
256
257    /// Radius at which we consider the atmosphere to 'end' for our
258    /// calculations (from center of planet)
259    ///
260    /// units: m
261    pub top_radius: f32,
262
263    /// An approximation of the average albedo (or color, roughly) of the
264    /// planet's surface. This is used when calculating multiscattering.
265    ///
266    /// units: N/A
267    pub ground_albedo: Vec3,
268
269    /// The rate of falloff of rayleigh particulate with respect to altitude:
270    /// optical density = exp(-rayleigh_density_exp_scale * altitude in meters).
271    ///
272    /// THIS VALUE MUST BE POSITIVE
273    ///
274    /// units: N/A
275    pub rayleigh_density_exp_scale: f32,
276
277    /// The scattering optical density of rayleigh particulate, or how
278    /// much light it scatters per meter
279    ///
280    /// units: m^-1
281    pub rayleigh_scattering: Vec3,
282
283    /// The rate of falloff of mie particulate with respect to altitude:
284    /// optical density = exp(-mie_density_exp_scale * altitude in meters)
285    ///
286    /// THIS VALUE MUST BE POSITIVE
287    ///
288    /// units: N/A
289    pub mie_density_exp_scale: f32,
290
291    /// The scattering optical density of mie particulate, or how much light
292    /// it scatters per meter.
293    ///
294    /// units: m^-1
295    pub mie_scattering: f32,
296
297    /// The absorbing optical density of mie particulate, or how much light
298    /// it absorbs per meter.
299    ///
300    /// units: m^-1
301    pub mie_absorption: f32,
302
303    /// The "asymmetry" of mie scattering, or how much light tends to scatter
304    /// forwards, rather than backwards or to the side.
305    ///
306    /// domain: (-1, 1)
307    /// units: N/A
308    pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1)
309
310    /// The altitude at which the ozone layer is centered.
311    ///
312    /// units: m
313    pub ozone_layer_altitude: f32,
314
315    /// The width of the ozone layer
316    ///
317    /// units: m
318    pub ozone_layer_width: f32,
319
320    /// The optical density of ozone, or how much of each wavelength of
321    /// light it absorbs per meter.
322    ///
323    /// units: m^-1
324    pub ozone_absorption: Vec3,
325}
326
327impl Atmosphere {
328    pub const EARTH: Atmosphere = Atmosphere {
329        bottom_radius: 6_360_000.0,
330        top_radius: 6_460_000.0,
331        ground_albedo: Vec3::splat(0.3),
332        rayleigh_density_exp_scale: 1.0 / 8_000.0,
333        rayleigh_scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
334        mie_density_exp_scale: 1.0 / 1_200.0,
335        mie_scattering: 3.996e-6,
336        mie_absorption: 0.444e-6,
337        mie_asymmetry: 0.8,
338        ozone_layer_altitude: 25_000.0,
339        ozone_layer_width: 30_000.0,
340        ozone_absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
341    };
342
343    pub fn with_density_multiplier(mut self, mult: f32) -> Self {
344        self.rayleigh_scattering *= mult;
345        self.mie_scattering *= mult;
346        self.mie_absorption *= mult;
347        self.ozone_absorption *= mult;
348        self
349    }
350}
351
352impl Default for Atmosphere {
353    fn default() -> Self {
354        Self::EARTH
355    }
356}
357
358impl ExtractComponent for Atmosphere {
359    type QueryData = Read<Atmosphere>;
360
361    type QueryFilter = With<Camera3d>;
362
363    type Out = Atmosphere;
364
365    fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
366        Some(item.clone())
367    }
368}
369
370/// This component controls the resolution of the atmosphere LUTs, and
371/// how many samples are used when computing them.
372///
373/// The transmittance LUT stores the transmittance from a point in the
374/// atmosphere to the outer edge of the atmosphere in any direction,
375/// parametrized by the point's radius and the cosine of the zenith angle
376/// of the ray.
377///
378/// The multiscattering LUT stores the factor representing luminance scattered
379/// towards the camera with scattering order >2, parametrized by the point's radius
380/// and the cosine of the zenith angle of the sun.
381///
382/// The sky-view lut is essentially the actual skybox, storing the light scattered
383/// towards the camera in every direction with a cubemap.
384///
385/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
386/// scattered towards the camera at each point (RGB channels), alongside the average
387/// transmittance to that point (A channel).
388#[derive(Clone, Component, Reflect, ShaderType)]
389#[reflect(Clone, Default)]
390pub struct AtmosphereSettings {
391    /// The size of the transmittance LUT
392    pub transmittance_lut_size: UVec2,
393
394    /// The size of the multiscattering LUT
395    pub multiscattering_lut_size: UVec2,
396
397    /// The size of the sky-view LUT.
398    pub sky_view_lut_size: UVec2,
399
400    /// The size of the aerial-view LUT.
401    pub aerial_view_lut_size: UVec3,
402
403    /// The number of points to sample along each ray when
404    /// computing the transmittance LUT
405    pub transmittance_lut_samples: u32,
406
407    /// The number of rays to sample when computing each
408    /// pixel of the multiscattering LUT
409    pub multiscattering_lut_dirs: u32,
410
411    /// The number of points to sample when integrating along each
412    /// multiscattering ray
413    pub multiscattering_lut_samples: u32,
414
415    /// The number of points to sample along each ray when
416    /// computing the sky-view LUT.
417    pub sky_view_lut_samples: u32,
418
419    /// The number of points to sample for each slice along the z-axis
420    /// of the aerial-view LUT.
421    pub aerial_view_lut_samples: u32,
422
423    /// The maximum distance from the camera to evaluate the
424    /// aerial view LUT. The slices along the z-axis of the
425    /// texture will be distributed linearly from the camera
426    /// to this value.
427    ///
428    /// units: m
429    pub aerial_view_lut_max_distance: f32,
430
431    /// A conversion factor between scene units and meters, used to
432    /// ensure correctness at different length scales.
433    pub scene_units_to_m: f32,
434}
435
436impl Default for AtmosphereSettings {
437    fn default() -> Self {
438        Self {
439            transmittance_lut_size: UVec2::new(256, 128),
440            transmittance_lut_samples: 40,
441            multiscattering_lut_size: UVec2::new(32, 32),
442            multiscattering_lut_dirs: 64,
443            multiscattering_lut_samples: 20,
444            sky_view_lut_size: UVec2::new(400, 200),
445            sky_view_lut_samples: 16,
446            aerial_view_lut_size: UVec3::new(32, 32, 32),
447            aerial_view_lut_samples: 10,
448            aerial_view_lut_max_distance: 3.2e4,
449            scene_units_to_m: 1.0,
450        }
451    }
452}
453
454impl ExtractComponent for AtmosphereSettings {
455    type QueryData = Read<AtmosphereSettings>;
456
457    type QueryFilter = (With<Camera3d>, With<Atmosphere>);
458
459    type Out = AtmosphereSettings;
460
461    fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
462        Some(item.clone())
463    }
464}
465
466fn configure_camera_depth_usages(
467    mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<Atmosphere>)>,
468) {
469    for mut camera in &mut cameras {
470        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
471    }
472}