Skip to main content

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//! To add the atmosphere to your scene, spawn an entity with the [`Atmosphere`] component and
13//! [`bevy_transform::components::GlobalTransform`], and add [`AtmosphereSettings`] to each
14//! 3D camera that should render it. Detailed documentation is on the [`Atmosphere`] component.
15//!
16//! Placement and scene scale come from the entity's transform. With several atmospheres in one
17//! scene, each camera picks the atmosphere whose origin is closest in world space.
18//!
19//! Performance-wise, the effect should be fairly cheap since the LUTs (Look
20//! Up Tables) that encode most of the data are small, and take advantage of the
21//! fact that the atmosphere is symmetric. Performance is also proportional to
22//! the number of directional lights in the scene. In order to tune
23//! performance more finely, the [`AtmosphereSettings`] camera component
24//! manages the size of each LUT and the sample count for each ray.
25//!
26//! Given how similar it is to [`crate::volumetric_fog`], it might be expected
27//! that these two modules would work together well. However for now using both
28//! at once is untested, and might not be physically accurate. These may be
29//! integrated into a single module in the future.
30//!
31//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels
32//! through the atmosphere, we use a simpler averaging technique instead of the more
33//! complex blending operations. This difference will be resolved for WebGPU in a future release.
34//!
35//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW
36//!
37//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere
38
39mod environment;
40mod node;
41pub mod resources;
42
43use bevy_app::{App, Plugin};
44use bevy_asset::{embedded_asset, AssetId};
45use bevy_camera::{Camera3d, Hdr};
46use bevy_core_pipeline::{
47    core_3d::{main_opaque_pass_3d, main_transparent_pass_3d},
48    schedule::{Core3d, Core3dSystems},
49};
50use bevy_ecs::{
51    component::Component,
52    entity::Entity,
53    query::{Changed, With},
54    schedule::IntoScheduleConfigs,
55    system::{Commands, Query},
56};
57use bevy_light::{atmosphere::ScatteringMedium, Atmosphere};
58use bevy_math::{Mat4, Quat, UVec2, UVec3, Vec3};
59use bevy_reflect::{std_traits::ReflectDefault, Reflect};
60use bevy_render::{
61    extract_component::{ExtractComponentPlugin, UniformComponentPlugin},
62    render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
63    renderer::RenderDevice,
64    sync_component::{SyncComponent, SyncComponentPlugin},
65    sync_world::RenderEntity,
66    Extract, ExtractSchedule, RenderStartup,
67};
68use bevy_render::{
69    render_resource::{TextureFormat, TextureUsages},
70    renderer::RenderAdapter,
71    GpuResourceAppExt, Render, RenderApp, RenderSystems,
72};
73use bevy_transform::components::GlobalTransform;
74
75use bevy_shader::load_shader_library;
76use environment::{
77    atmosphere_environment, init_atmosphere_probe_layout, init_atmosphere_probe_pipeline,
78    on_insert_atmosphere_environment_map_light, on_remove_atmosphere_environment_map_light,
79    prepare_atmosphere_probe_bind_groups, prepare_probe_textures, AtmosphereEnvironmentMap,
80};
81use node::{atmosphere_luts, render_sky};
82use resources::{
83    prepare_atmosphere_transforms, prepare_atmosphere_uniforms, queue_render_sky_pipelines,
84    AtmosphereTransforms, GpuAtmosphere, RenderSkyBindGroupLayouts,
85};
86use tracing::warn;
87
88use crate::resources::prepare_atmosphere_buffers;
89
90use self::resources::{
91    prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
92    AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereSampler, AtmosphereTextures,
93    AtmosphereTransformsOffset, RenderSkyPipelineId,
94};
95
96#[doc(hidden)]
97pub struct AtmospherePlugin;
98
99impl Plugin for AtmospherePlugin {
100    fn build(&self, app: &mut App) {
101        load_shader_library!(app, "types.wesl");
102        load_shader_library!(app, "functions.wesl");
103        load_shader_library!(app, "bruneton_functions.wesl");
104        load_shader_library!(app, "bindings.wesl");
105
106        embedded_asset!(app, "transmittance_lut.wesl");
107        embedded_asset!(app, "multiscattering_lut.wesl");
108        embedded_asset!(app, "sky_view_lut.wesl");
109        embedded_asset!(app, "aerial_view_lut.wesl");
110        embedded_asset!(app, "render_sky.wesl");
111        embedded_asset!(app, "environment.wesl");
112
113        app.add_plugins((
114            ExtractComponentPlugin::<AtmosphereEnvironmentMap>::default(),
115            SyncComponentPlugin::<AtmosphereSettings>::default(),
116            UniformComponentPlugin::<GpuAtmosphere>::default(),
117            UniformComponentPlugin::<GpuAtmosphereSettings>::default(),
118        ))
119        .add_observer(on_insert_atmosphere_environment_map_light)
120        .add_observer(on_remove_atmosphere_environment_map_light);
121
122        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
123            render_app.add_systems(ExtractSchedule, extract_atmosphere);
124        }
125    }
126
127    fn finish(&self, app: &mut App) {
128        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
129            return;
130        };
131
132        let render_adapter = render_app.world().resource::<RenderAdapter>();
133
134        if !render_adapter
135            .get_downlevel_capabilities()
136            .flags
137            .contains(DownlevelFlags::COMPUTE_SHADERS)
138        {
139            warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
140            return;
141        }
142
143        if !render_adapter
144            .get_texture_format_features(TextureFormat::Rgba16Float)
145            .allowed_usages
146            .contains(TextureUsages::STORAGE_BINDING)
147        {
148            warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
149            return;
150        }
151
152        // Check the `RenderDevice` in addition to the `RenderAdapter`. The
153        // former takes the `WGPU_SETTINGS_PRIO` environment variable into
154        // account, and the latter doesn't.
155        let render_device = render_app.world().resource::<RenderDevice>();
156        if render_device.limits().max_storage_textures_per_shader_stage == 0 {
157            warn!("AtmospherePlugin not loaded. GPU lacks support: `max_storage_textures_per_shader_stage` is 0");
158            return;
159        }
160
161        render_app
162            .insert_resource(AtmosphereBindGroupLayouts::new())
163            .init_gpu_resource::<RenderSkyBindGroupLayouts>()
164            .init_gpu_resource::<AtmosphereSampler>()
165            .init_gpu_resource::<AtmosphereLutPipelines>()
166            .init_gpu_resource::<AtmosphereTransforms>()
167            .init_gpu_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
168            .add_systems(
169                RenderStartup,
170                (init_atmosphere_probe_layout, init_atmosphere_probe_pipeline).chain(),
171            )
172            .add_systems(
173                Render,
174                (
175                    configure_camera_depth_usages.in_set(RenderSystems::PrepareViews),
176                    queue_render_sky_pipelines.in_set(RenderSystems::Queue),
177                    prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources),
178                    prepare_probe_textures
179                        .in_set(RenderSystems::PrepareResources)
180                        .after(prepare_atmosphere_textures),
181                    prepare_atmosphere_uniforms
182                        .in_set(RenderSystems::Prepare)
183                        .before(RenderSystems::PrepareResources),
184                    prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups),
185                    prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources),
186                    prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups),
187                    prepare_atmosphere_buffers.in_set(RenderSystems::PrepareResources),
188                ),
189            )
190            .add_systems(
191                Core3d,
192                (
193                    (atmosphere_luts, atmosphere_environment)
194                        .chain()
195                        .after(Core3dSystems::Prepass)
196                        .before(Core3dSystems::MainPass),
197                    render_sky
198                        .after(main_opaque_pass_3d)
199                        .before(main_transparent_pass_3d),
200                ),
201            );
202    }
203}
204
205/// For each camera with [`AtmosphereSettings`], picks the nearest [`Atmosphere`] by world-space
206/// distance to its origin, copies it as [`ExtractedAtmosphere`], and builds [`GpuAtmosphereSettings`].
207pub fn extract_atmosphere(
208    mut commands: Commands,
209    atmosphere_entities: Extract<Query<(Entity, &Atmosphere, &GlobalTransform)>>,
210    cameras: Extract<
211        Query<(RenderEntity, Option<&AtmosphereSettings>, &GlobalTransform), With<Camera3d>>,
212    >,
213) {
214    let candidates: Vec<(Entity, &Atmosphere, &GlobalTransform)> =
215        atmosphere_entities.iter().collect();
216
217    for (render_entity, settings, cam_global) in &cameras {
218        // Remove any stale render-world state when AtmosphereSettings is removed or candidates is empty
219        let (Some(settings), false) = (settings, candidates.is_empty()) else {
220            commands.entity(render_entity).try_remove::<(
221                ExtractedAtmosphere,
222                GpuAtmosphereSettings,
223                GpuAtmosphere,
224                AtmosphereTextures,
225                AtmosphereTransformsOffset,
226                RenderSkyPipelineId,
227                AtmosphereBindGroups,
228            )>();
229            continue;
230        };
231
232        let cam_world = cam_global.translation();
233        let selected = candidates
234            .iter()
235            .min_by(|(ea, _, gt_a), (eb, _, gt_b)| {
236                let da = cam_world.distance(gt_a.translation());
237                let db = cam_world.distance(gt_b.translation());
238                da.total_cmp(&db).then_with(|| ea.cmp(eb))
239            })
240            .expect("checked non-empty above");
241        let atmo = selected.1;
242        let gt = selected.2;
243        // Atmospheres are spherically symmetric, so ignore their orientation.
244        let (scale, _, translation) = gt.to_scale_rotation_translation();
245
246        let extracted = ExtractedAtmosphere {
247            inner_radius: atmo.inner_radius,
248            outer_radius: atmo.outer_radius,
249            ground_albedo: atmo.ground_albedo,
250            medium: atmo.medium.id(),
251            world_to_atmosphere: Mat4::from_scale_rotation_translation(
252                scale,
253                Quat::IDENTITY,
254                translation,
255            )
256            .inverse(),
257        };
258        commands.entity(render_entity).insert(extracted);
259        commands
260            .entity(render_entity)
261            .insert(GpuAtmosphereSettings::from(settings.clone()));
262    }
263}
264
265/// The render-world representation of an `Atmosphere`, but which
266/// hasn't been converted into shader uniforms yet.
267#[derive(Clone, Component)]
268pub struct ExtractedAtmosphere {
269    pub inner_radius: f32,
270    pub outer_radius: f32,
271    pub ground_albedo: Vec3,
272    pub medium: AssetId<ScatteringMedium>,
273    pub world_to_atmosphere: Mat4,
274}
275
276/// This component controls the resolution of the atmosphere LUTs, and
277/// how many samples are used when computing them.
278///
279/// The transmittance LUT stores the transmittance from a point in the
280/// atmosphere to the outer edge of the atmosphere in any direction,
281/// parametrized by the point's radius and the cosine of the zenith angle
282/// of the ray.
283///
284/// The multiscattering LUT stores the factor representing luminance scattered
285/// towards the camera with scattering order >2, parametrized by the point's radius
286/// and the cosine of the zenith angle of the sun.
287///
288/// The sky-view lut is essentially the actual skybox, storing the light scattered
289/// towards the camera in every direction with a cubemap.
290///
291/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
292/// scattered towards the camera at each point (RGB channels), alongside the average
293/// transmittance to that point (A channel).
294#[derive(Clone, Component, Reflect)]
295#[reflect(Clone, Default)]
296#[require(Hdr)]
297pub struct AtmosphereSettings {
298    /// The size of the transmittance LUT
299    pub transmittance_lut_size: UVec2,
300
301    /// The size of the multiscattering LUT
302    pub multiscattering_lut_size: UVec2,
303
304    /// The size of the sky-view LUT.
305    pub sky_view_lut_size: UVec2,
306
307    /// The size of the aerial-view LUT.
308    pub aerial_view_lut_size: UVec3,
309
310    /// The number of points to sample along each ray when
311    /// computing the transmittance LUT
312    pub transmittance_lut_samples: u32,
313
314    /// The number of rays to sample when computing each
315    /// pixel of the multiscattering LUT
316    pub multiscattering_lut_dirs: u32,
317
318    /// The number of points to sample when integrating along each
319    /// multiscattering ray
320    pub multiscattering_lut_samples: u32,
321
322    /// The number of points to sample along each ray when
323    /// computing the sky-view LUT.
324    pub sky_view_lut_samples: u32,
325
326    /// The number of points to sample for each slice along the z-axis
327    /// of the aerial-view LUT.
328    pub aerial_view_lut_samples: u32,
329
330    /// The maximum distance from the camera to evaluate the
331    /// aerial view LUT. The slices along the z-axis of the
332    /// texture will be distributed linearly from the camera
333    /// to this value.
334    ///
335    /// units: m
336    pub aerial_view_lut_max_distance: f32,
337
338    /// The number of points to sample for each fragment when the using
339    /// ray marching to render the sky
340    pub sky_max_samples: u32,
341
342    /// The rendering method to use for the atmosphere.
343    pub rendering_method: AtmosphereMode,
344}
345
346impl Default for AtmosphereSettings {
347    fn default() -> Self {
348        Self {
349            transmittance_lut_size: UVec2::new(256, 128),
350            transmittance_lut_samples: 40,
351            multiscattering_lut_size: UVec2::new(32, 32),
352            multiscattering_lut_dirs: 64,
353            multiscattering_lut_samples: 20,
354            sky_view_lut_size: UVec2::new(400, 200),
355            sky_view_lut_samples: 16,
356            aerial_view_lut_size: UVec3::new(32, 32, 32),
357            aerial_view_lut_samples: 10,
358            aerial_view_lut_max_distance: 3.2e4,
359            sky_max_samples: 16,
360            rendering_method: AtmosphereMode::LookupTexture,
361        }
362    }
363}
364
365#[derive(Clone, Component, Reflect, ShaderType)]
366#[reflect(Default)]
367pub struct GpuAtmosphereSettings {
368    pub transmittance_lut_size: UVec2,
369    pub multiscattering_lut_size: UVec2,
370    pub sky_view_lut_size: UVec2,
371    pub aerial_view_lut_size: UVec3,
372    pub transmittance_lut_samples: u32,
373    pub multiscattering_lut_dirs: u32,
374    pub multiscattering_lut_samples: u32,
375    pub sky_view_lut_samples: u32,
376    pub aerial_view_lut_samples: u32,
377    pub aerial_view_lut_max_distance: f32,
378    pub sky_max_samples: u32,
379    pub rendering_method: u32,
380}
381
382impl Default for GpuAtmosphereSettings {
383    fn default() -> Self {
384        AtmosphereSettings::default().into()
385    }
386}
387
388impl From<AtmosphereSettings> for GpuAtmosphereSettings {
389    fn from(s: AtmosphereSettings) -> Self {
390        Self {
391            transmittance_lut_size: s.transmittance_lut_size,
392            multiscattering_lut_size: s.multiscattering_lut_size,
393            sky_view_lut_size: s.sky_view_lut_size,
394            aerial_view_lut_size: s.aerial_view_lut_size,
395            transmittance_lut_samples: s.transmittance_lut_samples,
396            multiscattering_lut_dirs: s.multiscattering_lut_dirs,
397            multiscattering_lut_samples: s.multiscattering_lut_samples,
398            sky_view_lut_samples: s.sky_view_lut_samples,
399            aerial_view_lut_samples: s.aerial_view_lut_samples,
400            aerial_view_lut_max_distance: s.aerial_view_lut_max_distance,
401            sky_max_samples: s.sky_max_samples,
402            rendering_method: s.rendering_method as u32,
403        }
404    }
405}
406
407impl SyncComponent<RenderApp> for AtmosphereSettings {
408    type Target = GpuAtmosphereSettings;
409}
410
411fn configure_camera_depth_usages(
412    mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<ExtractedAtmosphere>)>,
413) {
414    for mut camera in &mut cameras {
415        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
416    }
417}
418
419/// Selects how the atmosphere is rendered. Choose based on scene scale and
420/// volumetric shadow quality, and based on performance needs.
421#[repr(u32)]
422#[derive(Clone, Default, Reflect, Copy)]
423pub enum AtmosphereMode {
424    /// High-performance solution tailored to scenes that are mostly inside of the atmosphere.
425    /// Uses a set of lookup textures to approximate scattering integration.
426    /// Slightly less accurate for very long-distance/space views (lighting precision
427    /// tapers as the camera moves far from the scene origin) and for sharp volumetric
428    /// (cloud/fog) shadows.
429    #[default]
430    LookupTexture = 0,
431    /// Slower, more accurate rendering method for any type of scene.
432    /// Integrates the scattering numerically with raymarching and produces sharp volumetric
433    /// (cloud/fog) shadows.
434    /// Best for cinematic shots, planets seen from orbit, and scenes requiring
435    /// accurate long-distance lighting.
436    Raymarched = 1,
437}