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, AssetId, Assets, Handle};
42use bevy_camera::Camera3d;
43use bevy_core_pipeline::core_3d::graph::Node3d;
44use bevy_ecs::{
45    component::Component,
46    query::{Changed, QueryItem, With},
47    resource::Resource,
48    schedule::IntoScheduleConfigs,
49    system::{lifetimeless::Read, Query},
50};
51use bevy_math::{UVec2, UVec3, Vec3};
52use bevy_reflect::{std_traits::ReflectDefault, Reflect};
53use bevy_render::{
54    extract_component::UniformComponentPlugin,
55    render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
56    view::Hdr,
57    RenderStartup,
58};
59use bevy_render::{
60    extract_component::{ExtractComponent, ExtractComponentPlugin},
61    render_graph::{RenderGraphExt, ViewNodeRunner},
62    render_resource::{TextureFormat, TextureUsages},
63    renderer::RenderAdapter,
64    Render, RenderApp, RenderSystems,
65};
66
67use bevy_core_pipeline::core_3d::graph::Core3d;
68use bevy_shader::load_shader_library;
69use environment::{
70    init_atmosphere_probe_layout, init_atmosphere_probe_pipeline,
71    prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components,
72    prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode,
73};
74use resources::{
75    prepare_atmosphere_transforms, prepare_atmosphere_uniforms, queue_render_sky_pipelines,
76    AtmosphereTransforms, GpuAtmosphere, RenderSkyBindGroupLayouts,
77};
78use tracing::warn;
79
80use crate::{
81    medium::ScatteringMedium,
82    resources::{init_atmosphere_buffer, write_atmosphere_buffer},
83};
84
85use self::{
86    node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode},
87    resources::{
88        prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
89        AtmosphereLutPipelines, AtmosphereSampler,
90    },
91};
92
93#[doc(hidden)]
94pub struct AtmospherePlugin;
95
96impl Plugin for AtmospherePlugin {
97    fn build(&self, app: &mut App) {
98        load_shader_library!(app, "types.wgsl");
99        load_shader_library!(app, "functions.wgsl");
100        load_shader_library!(app, "bruneton_functions.wgsl");
101        load_shader_library!(app, "bindings.wgsl");
102
103        embedded_asset!(app, "transmittance_lut.wgsl");
104        embedded_asset!(app, "multiscattering_lut.wgsl");
105        embedded_asset!(app, "sky_view_lut.wgsl");
106        embedded_asset!(app, "aerial_view_lut.wgsl");
107        embedded_asset!(app, "render_sky.wgsl");
108        embedded_asset!(app, "environment.wgsl");
109
110        app.add_plugins((
111            ExtractComponentPlugin::<Atmosphere>::default(),
112            ExtractComponentPlugin::<GpuAtmosphereSettings>::default(),
113            ExtractComponentPlugin::<AtmosphereEnvironmentMap>::default(),
114            UniformComponentPlugin::<GpuAtmosphere>::default(),
115            UniformComponentPlugin::<GpuAtmosphereSettings>::default(),
116        ))
117        .add_systems(Update, prepare_atmosphere_probe_components);
118
119        let world = app.world_mut();
120        let earthlike_medium = world
121            .resource_mut::<Assets<ScatteringMedium>>()
122            .add(ScatteringMedium::earthlike(256, 256));
123        world.insert_resource(EarthlikeAtmosphere(Atmosphere::earthlike(earthlike_medium)));
124    }
125
126    fn finish(&self, app: &mut App) {
127        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
128            return;
129        };
130
131        let render_adapter = render_app.world().resource::<RenderAdapter>();
132
133        if !render_adapter
134            .get_downlevel_capabilities()
135            .flags
136            .contains(DownlevelFlags::COMPUTE_SHADERS)
137        {
138            warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
139            return;
140        }
141
142        if !render_adapter
143            .get_texture_format_features(TextureFormat::Rgba16Float)
144            .allowed_usages
145            .contains(TextureUsages::STORAGE_BINDING)
146        {
147            warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
148            return;
149        }
150
151        render_app
152            .insert_resource(AtmosphereBindGroupLayouts::new())
153            .init_resource::<RenderSkyBindGroupLayouts>()
154            .init_resource::<AtmosphereSampler>()
155            .init_resource::<AtmosphereLutPipelines>()
156            .init_resource::<AtmosphereTransforms>()
157            .init_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
158            .add_systems(
159                RenderStartup,
160                (
161                    init_atmosphere_probe_layout,
162                    init_atmosphere_probe_pipeline,
163                    init_atmosphere_buffer,
164                )
165                    .chain(),
166            )
167            .add_systems(
168                Render,
169                (
170                    configure_camera_depth_usages.in_set(RenderSystems::ManageViews),
171                    queue_render_sky_pipelines.in_set(RenderSystems::Queue),
172                    prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources),
173                    prepare_probe_textures
174                        .in_set(RenderSystems::PrepareResources)
175                        .after(prepare_atmosphere_textures),
176                    prepare_atmosphere_uniforms
177                        .before(RenderSystems::PrepareResources)
178                        .after(RenderSystems::PrepareAssets),
179                    prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups),
180                    prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources),
181                    prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups),
182                    write_atmosphere_buffer.in_set(RenderSystems::PrepareResources),
183                ),
184            )
185            .add_render_graph_node::<ViewNodeRunner<AtmosphereLutsNode>>(
186                Core3d,
187                AtmosphereNode::RenderLuts,
188            )
189            .add_render_graph_edges(
190                Core3d,
191                (
192                    // END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS
193                    Node3d::EndPrepasses,
194                    AtmosphereNode::RenderLuts,
195                    Node3d::StartMainPass,
196                ),
197            )
198            .add_render_graph_node::<ViewNodeRunner<RenderSkyNode>>(
199                Core3d,
200                AtmosphereNode::RenderSky,
201            )
202            .add_render_graph_node::<EnvironmentNode>(Core3d, AtmosphereNode::Environment)
203            .add_render_graph_edges(
204                Core3d,
205                (
206                    Node3d::MainOpaquePass,
207                    AtmosphereNode::RenderSky,
208                    Node3d::MainTransparentPass,
209                ),
210            );
211    }
212}
213
214#[derive(Resource)]
215pub struct EarthlikeAtmosphere(Atmosphere);
216
217impl EarthlikeAtmosphere {
218    pub fn get(&self) -> Atmosphere {
219        self.0.clone()
220    }
221}
222
223/// Enables atmospheric scattering for an HDR camera.
224#[derive(Clone, Component)]
225#[require(AtmosphereSettings, Hdr)]
226pub struct Atmosphere {
227    /// Radius of the planet
228    ///
229    /// units: m
230    pub bottom_radius: f32,
231
232    /// Radius at which we consider the atmosphere to 'end' for our
233    /// calculations (from center of planet)
234    ///
235    /// units: m
236    pub top_radius: f32,
237
238    /// An approximation of the average albedo (or color, roughly) of the
239    /// planet's surface. This is used when calculating multiscattering.
240    ///
241    /// units: N/A
242    pub ground_albedo: Vec3,
243
244    /// A handle to a [`ScatteringMedium`], which describes the substance
245    /// of the atmosphere and how it scatters light.
246    pub medium: Handle<ScatteringMedium>,
247}
248
249impl Atmosphere {
250    pub fn earthlike(medium: Handle<ScatteringMedium>) -> Self {
251        const EARTH_BOTTOM_RADIUS: f32 = 6_360_000.0;
252        const EARTH_TOP_RADIUS: f32 = 6_460_000.0;
253        const EARTH_ALBEDO: Vec3 = Vec3::splat(0.3);
254        Self {
255            bottom_radius: EARTH_BOTTOM_RADIUS,
256            top_radius: EARTH_TOP_RADIUS,
257            ground_albedo: EARTH_ALBEDO,
258            medium,
259        }
260    }
261}
262
263impl ExtractComponent for Atmosphere {
264    type QueryData = Read<Atmosphere>;
265
266    type QueryFilter = With<Camera3d>;
267
268    type Out = ExtractedAtmosphere;
269
270    fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
271        Some(ExtractedAtmosphere {
272            bottom_radius: item.bottom_radius,
273            top_radius: item.top_radius,
274            ground_albedo: item.ground_albedo,
275            medium: item.medium.id(),
276        })
277    }
278}
279
280/// The render-world representation of an `Atmosphere`, but which
281/// hasn't been converted into shader uniforms yet.
282#[derive(Clone, Component)]
283pub struct ExtractedAtmosphere {
284    pub bottom_radius: f32,
285    pub top_radius: f32,
286    pub ground_albedo: Vec3,
287    pub medium: AssetId<ScatteringMedium>,
288}
289
290/// This component controls the resolution of the atmosphere LUTs, and
291/// how many samples are used when computing them.
292///
293/// The transmittance LUT stores the transmittance from a point in the
294/// atmosphere to the outer edge of the atmosphere in any direction,
295/// parametrized by the point's radius and the cosine of the zenith angle
296/// of the ray.
297///
298/// The multiscattering LUT stores the factor representing luminance scattered
299/// towards the camera with scattering order >2, parametrized by the point's radius
300/// and the cosine of the zenith angle of the sun.
301///
302/// The sky-view lut is essentially the actual skybox, storing the light scattered
303/// towards the camera in every direction with a cubemap.
304///
305/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
306/// scattered towards the camera at each point (RGB channels), alongside the average
307/// transmittance to that point (A channel).
308#[derive(Clone, Component, Reflect)]
309#[reflect(Clone, Default)]
310pub struct AtmosphereSettings {
311    /// The size of the transmittance LUT
312    pub transmittance_lut_size: UVec2,
313
314    /// The size of the multiscattering LUT
315    pub multiscattering_lut_size: UVec2,
316
317    /// The size of the sky-view LUT.
318    pub sky_view_lut_size: UVec2,
319
320    /// The size of the aerial-view LUT.
321    pub aerial_view_lut_size: UVec3,
322
323    /// The number of points to sample along each ray when
324    /// computing the transmittance LUT
325    pub transmittance_lut_samples: u32,
326
327    /// The number of rays to sample when computing each
328    /// pixel of the multiscattering LUT
329    pub multiscattering_lut_dirs: u32,
330
331    /// The number of points to sample when integrating along each
332    /// multiscattering ray
333    pub multiscattering_lut_samples: u32,
334
335    /// The number of points to sample along each ray when
336    /// computing the sky-view LUT.
337    pub sky_view_lut_samples: u32,
338
339    /// The number of points to sample for each slice along the z-axis
340    /// of the aerial-view LUT.
341    pub aerial_view_lut_samples: u32,
342
343    /// The maximum distance from the camera to evaluate the
344    /// aerial view LUT. The slices along the z-axis of the
345    /// texture will be distributed linearly from the camera
346    /// to this value.
347    ///
348    /// units: m
349    pub aerial_view_lut_max_distance: f32,
350
351    /// A conversion factor between scene units and meters, used to
352    /// ensure correctness at different length scales.
353    pub scene_units_to_m: f32,
354
355    /// The number of points to sample for each fragment when the using
356    /// ray marching to render the sky
357    pub sky_max_samples: u32,
358
359    /// The rendering method to use for the atmosphere.
360    pub rendering_method: AtmosphereMode,
361}
362
363impl Default for AtmosphereSettings {
364    fn default() -> Self {
365        Self {
366            transmittance_lut_size: UVec2::new(256, 128),
367            transmittance_lut_samples: 40,
368            multiscattering_lut_size: UVec2::new(32, 32),
369            multiscattering_lut_dirs: 64,
370            multiscattering_lut_samples: 20,
371            sky_view_lut_size: UVec2::new(400, 200),
372            sky_view_lut_samples: 16,
373            aerial_view_lut_size: UVec3::new(32, 32, 32),
374            aerial_view_lut_samples: 10,
375            aerial_view_lut_max_distance: 3.2e4,
376            scene_units_to_m: 1.0,
377            sky_max_samples: 16,
378            rendering_method: AtmosphereMode::LookupTexture,
379        }
380    }
381}
382
383#[derive(Clone, Component, Reflect, ShaderType)]
384#[reflect(Default)]
385pub struct GpuAtmosphereSettings {
386    pub transmittance_lut_size: UVec2,
387    pub multiscattering_lut_size: UVec2,
388    pub sky_view_lut_size: UVec2,
389    pub aerial_view_lut_size: UVec3,
390    pub transmittance_lut_samples: u32,
391    pub multiscattering_lut_dirs: u32,
392    pub multiscattering_lut_samples: u32,
393    pub sky_view_lut_samples: u32,
394    pub aerial_view_lut_samples: u32,
395    pub aerial_view_lut_max_distance: f32,
396    pub scene_units_to_m: f32,
397    pub sky_max_samples: u32,
398    pub rendering_method: u32,
399}
400
401impl Default for GpuAtmosphereSettings {
402    fn default() -> Self {
403        AtmosphereSettings::default().into()
404    }
405}
406
407impl From<AtmosphereSettings> for GpuAtmosphereSettings {
408    fn from(s: AtmosphereSettings) -> Self {
409        Self {
410            transmittance_lut_size: s.transmittance_lut_size,
411            multiscattering_lut_size: s.multiscattering_lut_size,
412            sky_view_lut_size: s.sky_view_lut_size,
413            aerial_view_lut_size: s.aerial_view_lut_size,
414            transmittance_lut_samples: s.transmittance_lut_samples,
415            multiscattering_lut_dirs: s.multiscattering_lut_dirs,
416            multiscattering_lut_samples: s.multiscattering_lut_samples,
417            sky_view_lut_samples: s.sky_view_lut_samples,
418            aerial_view_lut_samples: s.aerial_view_lut_samples,
419            aerial_view_lut_max_distance: s.aerial_view_lut_max_distance,
420            scene_units_to_m: s.scene_units_to_m,
421            sky_max_samples: s.sky_max_samples,
422            rendering_method: s.rendering_method as u32,
423        }
424    }
425}
426
427impl ExtractComponent for GpuAtmosphereSettings {
428    type QueryData = Read<AtmosphereSettings>;
429
430    type QueryFilter = (With<Camera3d>, With<Atmosphere>);
431
432    type Out = GpuAtmosphereSettings;
433
434    fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
435        Some(item.clone().into())
436    }
437}
438
439fn configure_camera_depth_usages(
440    mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<ExtractedAtmosphere>)>,
441) {
442    for mut camera in &mut cameras {
443        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
444    }
445}
446
447/// Selects how the atmosphere is rendered. Choose based on scene scale and
448/// volumetric shadow quality, and based on performance needs.
449#[repr(u32)]
450#[derive(Clone, Default, Reflect, Copy)]
451pub enum AtmosphereMode {
452    /// High-performance solution tailored to scenes that are mostly inside of the atmosphere.
453    /// Uses a set of lookup textures to approximate scattering integration.
454    /// Slightly less accurate for very long-distance/space views (lighting precision
455    /// tapers as the camera moves far from the scene origin) and for sharp volumetric
456    /// (cloud/fog) shadows.
457    #[default]
458    LookupTexture = 0,
459    /// Slower, more accurate rendering method for any type of scene.
460    /// Integrates the scattering numerically with raymarching and produces sharp volumetric
461    /// (cloud/fog) shadows.
462    /// Best for cinematic shots, planets seen from orbit, and scenes requiring
463    /// accurate long-distance lighting.
464    Raymarched = 1,
465}