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