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, Update};
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, 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    prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components,
79    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::{init_atmosphere_buffer, write_atmosphere_buffer};
89
90use self::resources::{
91    prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
92    AtmosphereLutPipelines, AtmosphereSampler,
93};
94
95#[doc(hidden)]
96pub struct AtmospherePlugin;
97
98impl Plugin for AtmospherePlugin {
99    fn build(&self, app: &mut App) {
100        load_shader_library!(app, "types.wgsl");
101        load_shader_library!(app, "functions.wgsl");
102        load_shader_library!(app, "bruneton_functions.wgsl");
103        load_shader_library!(app, "bindings.wgsl");
104
105        embedded_asset!(app, "transmittance_lut.wgsl");
106        embedded_asset!(app, "multiscattering_lut.wgsl");
107        embedded_asset!(app, "sky_view_lut.wgsl");
108        embedded_asset!(app, "aerial_view_lut.wgsl");
109        embedded_asset!(app, "render_sky.wgsl");
110        embedded_asset!(app, "environment.wgsl");
111
112        app.add_plugins((
113            ExtractComponentPlugin::<AtmosphereEnvironmentMap>::default(),
114            SyncComponentPlugin::<AtmosphereSettings>::default(),
115            UniformComponentPlugin::<GpuAtmosphere>::default(),
116            UniformComponentPlugin::<GpuAtmosphereSettings>::default(),
117        ))
118        .add_systems(Update, prepare_atmosphere_probe_components);
119
120        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
121            render_app.add_systems(ExtractSchedule, extract_atmosphere);
122        }
123    }
124
125    fn finish(&self, app: &mut App) {
126        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
127            return;
128        };
129
130        let render_adapter = render_app.world().resource::<RenderAdapter>();
131
132        if !render_adapter
133            .get_downlevel_capabilities()
134            .flags
135            .contains(DownlevelFlags::COMPUTE_SHADERS)
136        {
137            warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
138            return;
139        }
140
141        if !render_adapter
142            .get_texture_format_features(TextureFormat::Rgba16Float)
143            .allowed_usages
144            .contains(TextureUsages::STORAGE_BINDING)
145        {
146            warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
147            return;
148        }
149
150        // Check the `RenderDevice` in addition to the `RenderAdapter`. The
151        // former takes the `WGPU_SETTINGS_PRIO` environment variable into
152        // account, and the latter doesn't.
153        let render_device = render_app.world().resource::<RenderDevice>();
154        if render_device.limits().max_storage_textures_per_shader_stage == 0 {
155            warn!("AtmospherePlugin not loaded. GPU lacks support: `max_storage_textures_per_shader_stage` is 0");
156            return;
157        }
158
159        render_app
160            .insert_resource(AtmosphereBindGroupLayouts::new())
161            .init_gpu_resource::<RenderSkyBindGroupLayouts>()
162            .init_gpu_resource::<AtmosphereSampler>()
163            .init_gpu_resource::<AtmosphereLutPipelines>()
164            .init_gpu_resource::<AtmosphereTransforms>()
165            .init_gpu_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
166            .add_systems(
167                RenderStartup,
168                (
169                    init_atmosphere_probe_layout,
170                    init_atmosphere_probe_pipeline,
171                    init_atmosphere_buffer,
172                )
173                    .chain(),
174            )
175            .add_systems(
176                Render,
177                (
178                    configure_camera_depth_usages.in_set(RenderSystems::PrepareViews),
179                    queue_render_sky_pipelines.in_set(RenderSystems::Queue),
180                    prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources),
181                    prepare_probe_textures
182                        .in_set(RenderSystems::PrepareResources)
183                        .after(prepare_atmosphere_textures),
184                    prepare_atmosphere_uniforms
185                        .in_set(RenderSystems::Prepare)
186                        .before(RenderSystems::PrepareResources),
187                    prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups),
188                    prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources),
189                    prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups),
190                    write_atmosphere_buffer.in_set(RenderSystems::PrepareResources),
191                ),
192            )
193            .add_systems(
194                Core3d,
195                (
196                    (atmosphere_luts, atmosphere_environment)
197                        .chain()
198                        .after(Core3dSystems::Prepass)
199                        .before(Core3dSystems::MainPass),
200                    render_sky
201                        .after(main_opaque_pass_3d)
202                        .before(main_transparent_pass_3d),
203                ),
204            );
205    }
206}
207
208/// For each camera with [`AtmosphereSettings`], picks the nearest [`Atmosphere`] by world-space
209/// distance to its origin, copies it as [`ExtractedAtmosphere`], and builds [`GpuAtmosphereSettings`].
210pub fn extract_atmosphere(
211    mut commands: Commands,
212    atmosphere_entities: Extract<Query<(Entity, &Atmosphere, &GlobalTransform)>>,
213    cameras: Extract<Query<(RenderEntity, &AtmosphereSettings, &GlobalTransform), With<Camera3d>>>,
214) {
215    let candidates: Vec<(Entity, &Atmosphere, &GlobalTransform)> =
216        atmosphere_entities.iter().collect();
217
218    if candidates.is_empty() {
219        for (render_entity, ..) in &cameras {
220            commands
221                .entity(render_entity)
222                .remove::<ExtractedAtmosphere>();
223            commands
224                .entity(render_entity)
225                .remove::<GpuAtmosphereSettings>();
226        }
227        return;
228    }
229
230    for (render_entity, settings, cam_global) in &cameras {
231        let cam_world = cam_global.translation();
232        let selected = candidates
233            .iter()
234            .min_by(|(ea, _, gt_a), (eb, _, gt_b)| {
235                let da = cam_world.distance(gt_a.translation());
236                let db = cam_world.distance(gt_b.translation());
237                da.total_cmp(&db).then_with(|| ea.cmp(eb))
238            })
239            .expect("checked non-empty above");
240        let atmo = selected.1;
241        let gt = selected.2;
242
243        let extracted = ExtractedAtmosphere {
244            inner_radius: atmo.inner_radius,
245            outer_radius: atmo.outer_radius,
246            ground_albedo: atmo.ground_albedo,
247            medium: atmo.medium.id(),
248            world_to_atmosphere: gt.to_matrix().inverse(),
249        };
250        commands.entity(render_entity).insert(extracted);
251        commands
252            .entity(render_entity)
253            .insert(GpuAtmosphereSettings::from(settings.clone()));
254    }
255}
256
257/// The render-world representation of an `Atmosphere`, but which
258/// hasn't been converted into shader uniforms yet.
259#[derive(Clone, Component)]
260pub struct ExtractedAtmosphere {
261    pub inner_radius: f32,
262    pub outer_radius: f32,
263    pub ground_albedo: Vec3,
264    pub medium: AssetId<ScatteringMedium>,
265    pub world_to_atmosphere: Mat4,
266}
267
268/// This component controls the resolution of the atmosphere LUTs, and
269/// how many samples are used when computing them.
270///
271/// The transmittance LUT stores the transmittance from a point in the
272/// atmosphere to the outer edge of the atmosphere in any direction,
273/// parametrized by the point's radius and the cosine of the zenith angle
274/// of the ray.
275///
276/// The multiscattering LUT stores the factor representing luminance scattered
277/// towards the camera with scattering order >2, parametrized by the point's radius
278/// and the cosine of the zenith angle of the sun.
279///
280/// The sky-view lut is essentially the actual skybox, storing the light scattered
281/// towards the camera in every direction with a cubemap.
282///
283/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
284/// scattered towards the camera at each point (RGB channels), alongside the average
285/// transmittance to that point (A channel).
286#[derive(Clone, Component, Reflect)]
287#[reflect(Clone, Default)]
288#[require(Hdr)]
289pub struct AtmosphereSettings {
290    /// The size of the transmittance LUT
291    pub transmittance_lut_size: UVec2,
292
293    /// The size of the multiscattering LUT
294    pub multiscattering_lut_size: UVec2,
295
296    /// The size of the sky-view LUT.
297    pub sky_view_lut_size: UVec2,
298
299    /// The size of the aerial-view LUT.
300    pub aerial_view_lut_size: UVec3,
301
302    /// The number of points to sample along each ray when
303    /// computing the transmittance LUT
304    pub transmittance_lut_samples: u32,
305
306    /// The number of rays to sample when computing each
307    /// pixel of the multiscattering LUT
308    pub multiscattering_lut_dirs: u32,
309
310    /// The number of points to sample when integrating along each
311    /// multiscattering ray
312    pub multiscattering_lut_samples: u32,
313
314    /// The number of points to sample along each ray when
315    /// computing the sky-view LUT.
316    pub sky_view_lut_samples: u32,
317
318    /// The number of points to sample for each slice along the z-axis
319    /// of the aerial-view LUT.
320    pub aerial_view_lut_samples: u32,
321
322    /// The maximum distance from the camera to evaluate the
323    /// aerial view LUT. The slices along the z-axis of the
324    /// texture will be distributed linearly from the camera
325    /// to this value.
326    ///
327    /// units: m
328    pub aerial_view_lut_max_distance: f32,
329
330    /// The number of points to sample for each fragment when the using
331    /// ray marching to render the sky
332    pub sky_max_samples: u32,
333
334    /// The rendering method to use for the atmosphere.
335    pub rendering_method: AtmosphereMode,
336}
337
338impl Default for AtmosphereSettings {
339    fn default() -> Self {
340        Self {
341            transmittance_lut_size: UVec2::new(256, 128),
342            transmittance_lut_samples: 40,
343            multiscattering_lut_size: UVec2::new(32, 32),
344            multiscattering_lut_dirs: 64,
345            multiscattering_lut_samples: 20,
346            sky_view_lut_size: UVec2::new(400, 200),
347            sky_view_lut_samples: 16,
348            aerial_view_lut_size: UVec3::new(32, 32, 32),
349            aerial_view_lut_samples: 10,
350            aerial_view_lut_max_distance: 3.2e4,
351            sky_max_samples: 16,
352            rendering_method: AtmosphereMode::LookupTexture,
353        }
354    }
355}
356
357#[derive(Clone, Component, Reflect, ShaderType)]
358#[reflect(Default)]
359pub struct GpuAtmosphereSettings {
360    pub transmittance_lut_size: UVec2,
361    pub multiscattering_lut_size: UVec2,
362    pub sky_view_lut_size: UVec2,
363    pub aerial_view_lut_size: UVec3,
364    pub transmittance_lut_samples: u32,
365    pub multiscattering_lut_dirs: u32,
366    pub multiscattering_lut_samples: u32,
367    pub sky_view_lut_samples: u32,
368    pub aerial_view_lut_samples: u32,
369    pub aerial_view_lut_max_distance: f32,
370    pub sky_max_samples: u32,
371    pub rendering_method: u32,
372}
373
374impl Default for GpuAtmosphereSettings {
375    fn default() -> Self {
376        AtmosphereSettings::default().into()
377    }
378}
379
380impl From<AtmosphereSettings> for GpuAtmosphereSettings {
381    fn from(s: AtmosphereSettings) -> Self {
382        Self {
383            transmittance_lut_size: s.transmittance_lut_size,
384            multiscattering_lut_size: s.multiscattering_lut_size,
385            sky_view_lut_size: s.sky_view_lut_size,
386            aerial_view_lut_size: s.aerial_view_lut_size,
387            transmittance_lut_samples: s.transmittance_lut_samples,
388            multiscattering_lut_dirs: s.multiscattering_lut_dirs,
389            multiscattering_lut_samples: s.multiscattering_lut_samples,
390            sky_view_lut_samples: s.sky_view_lut_samples,
391            aerial_view_lut_samples: s.aerial_view_lut_samples,
392            aerial_view_lut_max_distance: s.aerial_view_lut_max_distance,
393            sky_max_samples: s.sky_max_samples,
394            rendering_method: s.rendering_method as u32,
395        }
396    }
397}
398
399impl SyncComponent for AtmosphereSettings {
400    type Target = GpuAtmosphereSettings;
401}
402
403fn configure_camera_depth_usages(
404    mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<ExtractedAtmosphere>)>,
405) {
406    for mut camera in &mut cameras {
407        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
408    }
409}
410
411/// Selects how the atmosphere is rendered. Choose based on scene scale and
412/// volumetric shadow quality, and based on performance needs.
413#[repr(u32)]
414#[derive(Clone, Default, Reflect, Copy)]
415pub enum AtmosphereMode {
416    /// High-performance solution tailored to scenes that are mostly inside of the atmosphere.
417    /// Uses a set of lookup textures to approximate scattering integration.
418    /// Slightly less accurate for very long-distance/space views (lighting precision
419    /// tapers as the camera moves far from the scene origin) and for sharp volumetric
420    /// (cloud/fog) shadows.
421    #[default]
422    LookupTexture = 0,
423    /// Slower, more accurate rendering method for any type of scene.
424    /// Integrates the scattering numerically with raymarching and produces sharp volumetric
425    /// (cloud/fog) shadows.
426    /// Best for cinematic shots, planets seen from orbit, and scenes requiring
427    /// accurate long-distance lighting.
428    Raymarched = 1,
429}