//! Procedural Atmospheric Scattering.1//!2//! This plugin implements [Hillaire's 2020 paper](https://sebh.github.io/publications/egsr2020.pdf)3//! on real-time atmospheric scattering. While it *will* work simply as a4//! procedural skybox, it also does much more. It supports dynamic time-of-5//! -day, multiple directional lights, and since it's applied as a post-processing6//! effect *on top* of the existing skybox, a starry skybox would automatically7//! show based on the time of day. Scattering in front of terrain (similar8//! to distance fog, but more complex) is handled as well, and takes into9//! account the directional light color and direction.10//!11//! Adding the [`Atmosphere`] component to a 3d camera will enable the effect,12//! which by default is set to look similar to Earth's atmosphere. See the13//! documentation on the component itself for information regarding its fields.14//!15//! Performance-wise, the effect should be fairly cheap since the LUTs (Look16//! Up Tables) that encode most of the data are small, and take advantage of the17//! fact that the atmosphere is symmetric. Performance is also proportional to18//! the number of directional lights in the scene. In order to tune19//! performance more finely, the [`AtmosphereSettings`] camera component20//! manages the size of each LUT and the sample count for each ray.21//!22//! Given how similar it is to [`crate::volumetric_fog`], it might be expected23//! that these two modules would work together well. However for now using both24//! at once is untested, and might not be physically accurate. These may be25//! integrated into a single module in the future.26//!27//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels28//! through the atmosphere, we use a simpler averaging technique instead of the more29//! complex blending operations. This difference will be resolved for WebGPU in a future release.30//!31//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW32//!33//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere3435mod environment;36mod node;37pub mod resources;3839use bevy_app::{App, Plugin, Update};40use bevy_asset::embedded_asset;41use bevy_camera::Camera3d;42use bevy_core_pipeline::core_3d::graph::Node3d;43use bevy_ecs::{44component::Component,45query::{Changed, QueryItem, With},46schedule::IntoScheduleConfigs,47system::{lifetimeless::Read, Query},48};49use bevy_math::{UVec2, UVec3, Vec3};50use bevy_reflect::{std_traits::ReflectDefault, Reflect};51use bevy_render::{52extract_component::UniformComponentPlugin,53render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},54view::Hdr,55RenderStartup,56};57use bevy_render::{58extract_component::{ExtractComponent, ExtractComponentPlugin},59render_graph::{RenderGraphExt, ViewNodeRunner},60render_resource::{TextureFormat, TextureUsages},61renderer::RenderAdapter,62Render, RenderApp, RenderSystems,63};6465use bevy_core_pipeline::core_3d::graph::Core3d;66use bevy_shader::load_shader_library;67use environment::{68init_atmosphere_probe_layout, init_atmosphere_probe_pipeline,69prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components,70prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode,71};72use resources::{73prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms,74RenderSkyBindGroupLayouts,75};76use tracing::warn;7778use self::{79node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode},80resources::{81prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,82AtmosphereLutPipelines, AtmosphereSamplers,83},84};8586#[doc(hidden)]87pub struct AtmospherePlugin;8889impl Plugin for AtmospherePlugin {90fn build(&self, app: &mut App) {91load_shader_library!(app, "types.wgsl");92load_shader_library!(app, "functions.wgsl");93load_shader_library!(app, "bruneton_functions.wgsl");94load_shader_library!(app, "bindings.wgsl");9596embedded_asset!(app, "transmittance_lut.wgsl");97embedded_asset!(app, "multiscattering_lut.wgsl");98embedded_asset!(app, "sky_view_lut.wgsl");99embedded_asset!(app, "aerial_view_lut.wgsl");100embedded_asset!(app, "render_sky.wgsl");101embedded_asset!(app, "environment.wgsl");102103app.add_plugins((104ExtractComponentPlugin::<Atmosphere>::default(),105ExtractComponentPlugin::<GpuAtmosphereSettings>::default(),106ExtractComponentPlugin::<AtmosphereEnvironmentMap>::default(),107UniformComponentPlugin::<Atmosphere>::default(),108UniformComponentPlugin::<GpuAtmosphereSettings>::default(),109))110.add_systems(Update, prepare_atmosphere_probe_components);111}112113fn finish(&self, app: &mut App) {114let Some(render_app) = app.get_sub_app_mut(RenderApp) else {115return;116};117118let render_adapter = render_app.world().resource::<RenderAdapter>();119120if !render_adapter121.get_downlevel_capabilities()122.flags123.contains(DownlevelFlags::COMPUTE_SHADERS)124{125warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");126return;127}128129if !render_adapter130.get_texture_format_features(TextureFormat::Rgba16Float)131.allowed_usages132.contains(TextureUsages::STORAGE_BINDING)133{134warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");135return;136}137138render_app139.init_resource::<AtmosphereBindGroupLayouts>()140.init_resource::<RenderSkyBindGroupLayouts>()141.init_resource::<AtmosphereSamplers>()142.init_resource::<AtmosphereLutPipelines>()143.init_resource::<AtmosphereTransforms>()144.init_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()145.add_systems(146RenderStartup,147(init_atmosphere_probe_layout, init_atmosphere_probe_pipeline).chain(),148)149.add_systems(150Render,151(152configure_camera_depth_usages.in_set(RenderSystems::ManageViews),153queue_render_sky_pipelines.in_set(RenderSystems::Queue),154prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources),155prepare_probe_textures156.in_set(RenderSystems::PrepareResources)157.after(prepare_atmosphere_textures),158prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups),159prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources),160prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups),161),162)163.add_render_graph_node::<ViewNodeRunner<AtmosphereLutsNode>>(164Core3d,165AtmosphereNode::RenderLuts,166)167.add_render_graph_edges(168Core3d,169(170// END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS171Node3d::EndPrepasses,172AtmosphereNode::RenderLuts,173Node3d::StartMainPass,174),175)176.add_render_graph_node::<ViewNodeRunner<RenderSkyNode>>(177Core3d,178AtmosphereNode::RenderSky,179)180.add_render_graph_node::<EnvironmentNode>(Core3d, AtmosphereNode::Environment)181.add_render_graph_edges(182Core3d,183(184Node3d::MainOpaquePass,185AtmosphereNode::RenderSky,186Node3d::MainTransparentPass,187),188);189}190}191192/// This component describes the atmosphere of a planet, and when added to a camera193/// will enable atmospheric scattering for that camera. This is only compatible with194/// HDR cameras.195///196/// Most atmospheric particles scatter and absorb light in two main ways:197///198/// Rayleigh scattering occurs among very small particles, like individual gas199/// molecules. It's wavelength dependent, and causes colors to separate out as200/// light travels through the atmosphere. These particles *don't* absorb light.201///202/// Mie scattering occurs among slightly larger particles, like dust and sea spray.203/// These particles *do* absorb light, but Mie scattering and absorption is204/// *wavelength independent*.205///206/// Ozone acts differently from the other two, and is special-cased because207/// it's very important to the look of Earth's atmosphere. It's wavelength208/// dependent, but only *absorbs* light. Also, while the density of particles209/// participating in Rayleigh and Mie scattering falls off roughly exponentially210/// from the planet's surface, ozone only exists in a band centered at a fairly211/// high altitude.212#[derive(Clone, Component, Reflect, ShaderType)]213#[require(AtmosphereSettings, Hdr)]214#[reflect(Clone, Default)]215pub struct Atmosphere {216/// Radius of the planet217///218/// units: m219pub bottom_radius: f32,220221/// Radius at which we consider the atmosphere to 'end' for our222/// calculations (from center of planet)223///224/// units: m225pub top_radius: f32,226227/// An approximation of the average albedo (or color, roughly) of the228/// planet's surface. This is used when calculating multiscattering.229///230/// units: N/A231pub ground_albedo: Vec3,232233/// The rate of falloff of rayleigh particulate with respect to altitude:234/// optical density = exp(-rayleigh_density_exp_scale * altitude in meters).235///236/// THIS VALUE MUST BE POSITIVE237///238/// units: N/A239pub rayleigh_density_exp_scale: f32,240241/// The scattering optical density of rayleigh particulate, or how242/// much light it scatters per meter243///244/// units: m^-1245pub rayleigh_scattering: Vec3,246247/// The rate of falloff of mie particulate with respect to altitude:248/// optical density = exp(-mie_density_exp_scale * altitude in meters)249///250/// THIS VALUE MUST BE POSITIVE251///252/// units: N/A253pub mie_density_exp_scale: f32,254255/// The scattering optical density of mie particulate, or how much light256/// it scatters per meter.257///258/// units: m^-1259pub mie_scattering: f32,260261/// The absorbing optical density of mie particulate, or how much light262/// it absorbs per meter.263///264/// units: m^-1265pub mie_absorption: f32,266267/// The "asymmetry" of mie scattering, or how much light tends to scatter268/// forwards, rather than backwards or to the side.269///270/// domain: (-1, 1)271/// units: N/A272pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1)273274/// The altitude at which the ozone layer is centered.275///276/// units: m277pub ozone_layer_altitude: f32,278279/// The width of the ozone layer280///281/// units: m282pub ozone_layer_width: f32,283284/// The optical density of ozone, or how much of each wavelength of285/// light it absorbs per meter.286///287/// units: m^-1288pub ozone_absorption: Vec3,289}290291impl Atmosphere {292pub const EARTH: Atmosphere = Atmosphere {293bottom_radius: 6_360_000.0,294top_radius: 6_460_000.0,295ground_albedo: Vec3::splat(0.3),296rayleigh_density_exp_scale: 1.0 / 8_000.0,297rayleigh_scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),298mie_density_exp_scale: 1.0 / 1_200.0,299mie_scattering: 3.996e-6,300mie_absorption: 0.444e-6,301mie_asymmetry: 0.8,302ozone_layer_altitude: 25_000.0,303ozone_layer_width: 30_000.0,304ozone_absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),305};306307pub fn with_density_multiplier(mut self, mult: f32) -> Self {308self.rayleigh_scattering *= mult;309self.mie_scattering *= mult;310self.mie_absorption *= mult;311self.ozone_absorption *= mult;312self313}314}315316impl Default for Atmosphere {317fn default() -> Self {318Self::EARTH319}320}321322impl ExtractComponent for Atmosphere {323type QueryData = Read<Atmosphere>;324325type QueryFilter = With<Camera3d>;326327type Out = Atmosphere;328329fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {330Some(item.clone())331}332}333334/// This component controls the resolution of the atmosphere LUTs, and335/// how many samples are used when computing them.336///337/// The transmittance LUT stores the transmittance from a point in the338/// atmosphere to the outer edge of the atmosphere in any direction,339/// parametrized by the point's radius and the cosine of the zenith angle340/// of the ray.341///342/// The multiscattering LUT stores the factor representing luminance scattered343/// towards the camera with scattering order >2, parametrized by the point's radius344/// and the cosine of the zenith angle of the sun.345///346/// The sky-view lut is essentially the actual skybox, storing the light scattered347/// towards the camera in every direction with a cubemap.348///349/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance350/// scattered towards the camera at each point (RGB channels), alongside the average351/// transmittance to that point (A channel).352#[derive(Clone, Component, Reflect)]353#[reflect(Clone, Default)]354pub struct AtmosphereSettings {355/// The size of the transmittance LUT356pub transmittance_lut_size: UVec2,357358/// The size of the multiscattering LUT359pub multiscattering_lut_size: UVec2,360361/// The size of the sky-view LUT.362pub sky_view_lut_size: UVec2,363364/// The size of the aerial-view LUT.365pub aerial_view_lut_size: UVec3,366367/// The number of points to sample along each ray when368/// computing the transmittance LUT369pub transmittance_lut_samples: u32,370371/// The number of rays to sample when computing each372/// pixel of the multiscattering LUT373pub multiscattering_lut_dirs: u32,374375/// The number of points to sample when integrating along each376/// multiscattering ray377pub multiscattering_lut_samples: u32,378379/// The number of points to sample along each ray when380/// computing the sky-view LUT.381pub sky_view_lut_samples: u32,382383/// The number of points to sample for each slice along the z-axis384/// of the aerial-view LUT.385pub aerial_view_lut_samples: u32,386387/// The maximum distance from the camera to evaluate the388/// aerial view LUT. The slices along the z-axis of the389/// texture will be distributed linearly from the camera390/// to this value.391///392/// units: m393pub aerial_view_lut_max_distance: f32,394395/// A conversion factor between scene units and meters, used to396/// ensure correctness at different length scales.397pub scene_units_to_m: f32,398399/// The number of points to sample for each fragment when the using400/// ray marching to render the sky401pub sky_max_samples: u32,402403/// The rendering method to use for the atmosphere.404pub rendering_method: AtmosphereMode,405}406407impl Default for AtmosphereSettings {408fn default() -> Self {409Self {410transmittance_lut_size: UVec2::new(256, 128),411transmittance_lut_samples: 40,412multiscattering_lut_size: UVec2::new(32, 32),413multiscattering_lut_dirs: 64,414multiscattering_lut_samples: 20,415sky_view_lut_size: UVec2::new(400, 200),416sky_view_lut_samples: 16,417aerial_view_lut_size: UVec3::new(32, 32, 32),418aerial_view_lut_samples: 10,419aerial_view_lut_max_distance: 3.2e4,420scene_units_to_m: 1.0,421sky_max_samples: 16,422rendering_method: AtmosphereMode::LookupTexture,423}424}425}426427#[derive(Clone, Component, Reflect, ShaderType)]428#[reflect(Default)]429pub struct GpuAtmosphereSettings {430pub transmittance_lut_size: UVec2,431pub multiscattering_lut_size: UVec2,432pub sky_view_lut_size: UVec2,433pub aerial_view_lut_size: UVec3,434pub transmittance_lut_samples: u32,435pub multiscattering_lut_dirs: u32,436pub multiscattering_lut_samples: u32,437pub sky_view_lut_samples: u32,438pub aerial_view_lut_samples: u32,439pub aerial_view_lut_max_distance: f32,440pub scene_units_to_m: f32,441pub sky_max_samples: u32,442pub rendering_method: u32,443}444445impl Default for GpuAtmosphereSettings {446fn default() -> Self {447AtmosphereSettings::default().into()448}449}450451impl From<AtmosphereSettings> for GpuAtmosphereSettings {452fn from(s: AtmosphereSettings) -> Self {453Self {454transmittance_lut_size: s.transmittance_lut_size,455multiscattering_lut_size: s.multiscattering_lut_size,456sky_view_lut_size: s.sky_view_lut_size,457aerial_view_lut_size: s.aerial_view_lut_size,458transmittance_lut_samples: s.transmittance_lut_samples,459multiscattering_lut_dirs: s.multiscattering_lut_dirs,460multiscattering_lut_samples: s.multiscattering_lut_samples,461sky_view_lut_samples: s.sky_view_lut_samples,462aerial_view_lut_samples: s.aerial_view_lut_samples,463aerial_view_lut_max_distance: s.aerial_view_lut_max_distance,464scene_units_to_m: s.scene_units_to_m,465sky_max_samples: s.sky_max_samples,466rendering_method: s.rendering_method as u32,467}468}469}470471impl ExtractComponent for GpuAtmosphereSettings {472type QueryData = Read<AtmosphereSettings>;473474type QueryFilter = (With<Camera3d>, With<Atmosphere>);475476type Out = GpuAtmosphereSettings;477478fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {479Some(item.clone().into())480}481}482483fn configure_camera_depth_usages(484mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<Atmosphere>)>,485) {486for mut camera in &mut cameras {487camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();488}489}490491/// Selects how the atmosphere is rendered. Choose based on scene scale and492/// volumetric shadow quality, and based on performance needs.493#[repr(u32)]494#[derive(Clone, Default, Reflect, Copy)]495pub enum AtmosphereMode {496/// High-performance solution tailored to scenes that are mostly inside of the atmosphere.497/// Uses a set of lookup textures to approximate scattering integration.498/// Slightly less accurate for very long-distance/space views (lighting precision499/// tapers as the camera moves far from the scene origin) and for sharp volumetric500/// (cloud/fog) shadows.501#[default]502LookupTexture = 0,503/// Slower, more accurate rendering method for any type of scene.504/// Integrates the scattering numerically with raymarching and produces sharp volumetric505/// (cloud/fog) shadows.506/// Best for cinematic shots, planets seen from orbit, and scenes requiring507/// accurate long-distance lighting.508Raymarched = 1,509}510511512