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}