//! Provides types to specify atmosphere lighting, scattering terms, etc.12use alloc::{borrow::Cow, sync::Arc};3use bevy_asset::{Asset, Handle};4use bevy_camera::Hdr;5use bevy_ecs::component::Component;6use bevy_math::{ops, Curve, FloatPow, Vec3};7use bevy_reflect::TypePath;8use core::f32::{self, consts::PI};9use smallvec::SmallVec;1011/// Enables atmospheric scattering for an HDR camera.12#[derive(Clone, Component)]13#[require(Hdr)]14pub struct Atmosphere {15/// Radius of the planet16///17/// units: m18pub bottom_radius: f32,1920/// Radius at which we consider the atmosphere to 'end' for our21/// calculations (from center of planet)22///23/// units: m24pub top_radius: f32,2526/// An approximation of the average albedo (or color, roughly) of the27/// planet's surface. This is used when calculating multiscattering.28///29/// units: N/A30pub ground_albedo: Vec3,3132/// A handle to a [`ScatteringMedium`], which describes the substance33/// of the atmosphere and how it scatters light.34pub medium: Handle<ScatteringMedium>,35}3637impl Atmosphere {38/// An atmosphere like that of earth; use this with a [`ScatteringMedium::earthlike`] handle.39pub fn earthlike(medium: Handle<ScatteringMedium>) -> Self {40const EARTH_BOTTOM_RADIUS: f32 = 6_360_000.0;41const EARTH_TOP_RADIUS: f32 = 6_460_000.0;42const EARTH_ALBEDO: Vec3 = Vec3::splat(0.3);43Self {44bottom_radius: EARTH_BOTTOM_RADIUS,45top_radius: EARTH_TOP_RADIUS,46ground_albedo: EARTH_ALBEDO,47medium,48}49}50}5152/// An asset that defines how a material scatters light.53///54/// In order to calculate how light passes through a medium,55/// you need three pieces of information:56/// - how much light the medium *absorbs* per unit length57/// - how much light the medium *scatters* per unit length58/// - what *directions* the medium is likely to scatter light in.59///60/// The first two are fairly simple, and are sometimes referred to together61/// (accurately enough) as the medium's [optical density].62///63/// The last, defined by a [phase function], is the most important in creating64/// the look of a medium. Our brains are very good at noticing (if unconsciously)65/// that a dust storm scatters light differently than a rain cloud, for example.66/// See the docs on [`PhaseFunction`] for more info.67///68/// In reality, media are often composed of multiple elements that scatter light69/// independently, for Earth's atmosphere is composed of the gas itself, but also70/// suspended dust and particulate. These each scatter light differently, and are71/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`],72/// these are each represented by a [`ScatteringTerm`]73///74/// ## Technical Details75///76/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which77/// are re-created every time the asset is modified. See the docs on78/// `bevy_pbr::GpuScatteringMedium` for more info.79///80/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density81/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions82#[derive(TypePath, Asset, Clone)]83pub struct ScatteringMedium {84/// An optional label for the medium, used when creating the LUTs on the GPU.85pub label: Option<Cow<'static, str>>,86/// The resolution at which to sample the falloff distribution of each87/// scattering term. Custom or more detailed distributions may benefit88/// from a higher value, at the cost of more memory use.89pub falloff_resolution: u32,90/// The resolution at which to sample the phase function of each scattering91/// term. Custom or more detailed phase functions may benefit from a higher92/// value, at the cost of more memory use.93pub phase_resolution: u32,94/// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`]95pub terms: SmallVec<[ScatteringTerm; 1]>,96}9798impl Default for ScatteringMedium {99fn default() -> Self {100ScatteringMedium::earthlike(256, 256)101}102}103104impl ScatteringMedium {105/// Returns a scattering medium with a default label and the106/// specified scattering terms.107pub fn new(108falloff_resolution: u32,109phase_resolution: u32,110terms: impl IntoIterator<Item = ScatteringTerm>,111) -> Self {112Self {113label: None,114falloff_resolution,115phase_resolution,116terms: terms.into_iter().collect(),117}118}119120/// Consumes and returns this scattering medium with a new label.121pub fn with_label(self, label: impl Into<Cow<'static, str>>) -> Self {122Self {123label: Some(label.into()),124..self125}126}127128/// Consumes and returns this scattering medium with each scattering terms'129/// densities multiplied by `multiplier`.130pub fn with_density_multiplier(mut self, multiplier: f32) -> Self {131self.terms.iter_mut().for_each(|term| {132term.absorption *= multiplier;133term.scattering *= multiplier;134});135136self137}138139/// Returns a scattering medium representing an earthlike atmosphere.140///141/// Uses physically-based scale heights from Earth's atmosphere, assuming142/// a 60 km atmosphere height:143/// - Rayleigh (molecular) scattering: 8 km scale height144/// - Mie (aerosol) scattering: 1.2 km scale height145pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self {146Self::new(147falloff_resolution,148phase_resolution,149[150// Rayleigh scattering Term151ScatteringTerm {152absorption: Vec3::ZERO,153scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),154falloff: Falloff::Exponential { scale: 8.0 / 60.0 },155phase: PhaseFunction::Rayleigh,156},157// Mie scattering Term158ScatteringTerm {159absorption: Vec3::splat(3.996e-6),160scattering: Vec3::splat(0.444e-6),161falloff: Falloff::Exponential { scale: 1.2 / 60.0 },162phase: PhaseFunction::Mie { asymmetry: 0.8 },163},164// Ozone scattering Term165ScatteringTerm {166absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),167scattering: Vec3::ZERO,168falloff: Falloff::Tent {169center: 0.75,170width: 0.3,171},172phase: PhaseFunction::Isotropic,173},174],175)176.with_label("earthlike_atmosphere")177}178}179180/// An individual element of a [`ScatteringMedium`].181///182/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s,183/// which correspond to an individual element of the medium. For example, Earth's184/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric185/// gases themselves, which extend to the edge of space, and suspended dust particles,186/// which are denser but lie closer to the ground.187#[derive(Default, Clone)]188pub struct ScatteringTerm {189/// This term's optical absorption density, or how much light of each wavelength190/// it absorbs per meter.191///192/// units: m^-1193pub absorption: Vec3,194/// This term's optical scattering density, or how much light of each wavelength195/// it scatters per meter.196///197/// units: m^-1198pub scattering: Vec3,199/// This term's falloff distribution. See the docs on [`Falloff`] for more info.200pub falloff: Falloff,201/// This term's [phase function], which determines the character of how it202/// scatters light. See the docs on [`PhaseFunction`] for more info.203///204/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions205pub phase: PhaseFunction,206}207208/// Describes how the media in a [`ScatteringTerm`] is distributed.209///210/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and211/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere;212/// near the edge of space Earth's atmosphere is much less dense, and it absorbs213/// and scatters less light.214///215/// [`Falloff`] determines how the density of a medium changes as a function of216/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the217/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium218/// fades away completely, i.e. at the edge of space, and values between scale219/// linearly with distance, so `p = 0.5` would be halfway between the surface220/// and the edge of space.221///222/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values223/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`.224#[derive(Default, Clone)]225pub enum Falloff {226/// A simple linear falloff function, which essentially227/// passes the falloff parameter through unchanged.228///229/// f(1) = 1230/// f(0) = 0231/// f(p) = p232#[default]233Linear,234/// An exponential falloff function parametrized by a proportional scale.235/// When paired with an absolute "falloff distance" like the distance from236/// Earth's surface to the edge of space, this is analogous to the "height237/// scale" value common in atmospheric scattering literature, though it will238/// diverge from this for large or negative `scale` values.239///240/// f(1) = 1241/// f(0) = 0242/// f(p) = (e^((1-p)/s) - e^(1/s))/(e - e^(1/s))243Exponential {244/// The "scale" of the exponential falloff. Values closer to zero will245/// produce steeper falloff, and values farther from zero will produce246/// gentler falloff, approaching linear falloff as scale goes to `+-∞`.247///248/// Negative values change the *concavity* of the falloff function:249/// rather than an initial narrow region of steep falloff followed by a250/// wide region of gentle falloff, there will be an initial wide region251/// of gentle falloff followed by a narrow region of steep falloff.252///253/// domain: (-∞, ∞)254///255/// NOTE, this function is not defined when `scale == 0`.256/// In that case, it will fall back to linear falloff.257scale: f32,258},259/// A tent-shaped falloff function, which produces a triangular260/// peak at the center and linearly falls off to either side.261///262/// f(`center`) = 1263/// f(`center` +- `width` / 2) = 0264Tent {265/// The center of the tent function peak266///267/// domain: [0, 1]268center: f32,269/// The total width of the tent function peak270///271/// domain: [0, 1]272width: f32,273},274/// A falloff function defined by a custom curve.275///276/// domain: [0, 1],277/// range: [0, 1],278Curve(Arc<dyn Curve<f32> + Send + Sync>),279}280281impl Falloff {282/// Returns a falloff function corresponding to a custom curve.283pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {284Self::Curve(Arc::new(curve))285}286287/// Evaluates the falloff function at the given coordinate.288pub fn sample(&self, p: f32) -> f32 {289match self {290Falloff::Linear => p,291Falloff::Exponential { scale } => {292// fill discontinuity at scale == 0,293// arbitrarily choose linear falloff294if *scale == 0.0 {295p296} else {297let s = -1.0 / scale;298let exp_p_s = ops::exp((1.0 - p) * s);299let exp_s = ops::exp(s);300(exp_p_s - exp_s) / (1.0 - exp_s)301}302}303Falloff::Tent { center, width } => (1.0 - (p - center).abs() / (0.5 * width)).max(0.0),304Falloff::Curve(curve) => curve.sample(p).unwrap_or(0.0),305}306}307}308309/// Describes how a [`ScatteringTerm`] scatters light in different directions.310///311/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0`312/// whose input is the cosine of the angle between an incoming light direction and313/// and outgoing light direction, and whose output is the proportion of the incoming314/// light that is actually scattered in that direction.315///316/// The phase function has an important effect on the "look" of a medium in a scene.317/// Media consisting of particles of a different size or shape scatter light differently,318/// and our brains are very good at telling the difference. A dust cloud, which might319/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different320/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh`321///322/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions323#[derive(Clone)]324pub enum PhaseFunction {325/// A phase function that scatters light evenly in all directions.326Isotropic,327328/// A phase function representing [Rayleigh scattering].329///330/// Rayleigh scattering occurs naturally for particles much smaller than331/// the wavelengths of visible light, such as gas molecules in the atmosphere.332/// It's generally wavelength-dependent, where shorter wavelengths are scattered333/// more strongly, so [scattering](ScatteringTerm::scattering) should have334/// higher values for blue than green and green than red. Particles that335/// participate in Rayleigh scattering don't absorb any light, either.336///337/// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering338Rayleigh,339340/// The [Henyey-Greenstein phase function], which approximates [Mie scattering].341///342/// Mie scattering occurs naturally for spherical particles of dust343/// and aerosols roughly the same size as the wavelengths of visible light,344/// so it's useful for representing dust or sea spray. It's generally345/// wavelength-independent, so [absorption](ScatteringTerm::absorption)346/// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value.347///348/// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering349/// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function350Mie {351/// Whether the Mie scattering function is biased towards scattering352/// light forwards (asymmetry > 0) or backwards (asymmetry < 0).353///354/// domain: [-1, 1]355asymmetry: f32,356},357358/// A phase function defined by a custom curve, where the input359/// is the cosine of the angle between the incoming light ray360/// and the scattered light ray, and the output is the fraction361/// of the incoming light scattered in that direction.362///363/// Note: it's important for photorealism that the phase function364/// be *energy conserving*, meaning that in total no more light can365/// be scattered than actually entered the medium. For this to be366/// the case, the integral of the phase function over its domain must367/// be equal to 1/2π.368///369/// 1370/// ∫ p(x) dx = 1/2π371/// -1372///373/// domain: [-1, 1]374/// range: [0, 1]375Curve(Arc<dyn Curve<f32> + Send + Sync>),376}377378impl PhaseFunction {379/// A phase function defined by a custom curve.380pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {381Self::Curve(Arc::new(curve))382}383384/// Samples the phase function at the given value in [-1, 1], output is in [0, 1].385pub fn sample(&self, neg_l_dot_v: f32) -> f32 {386const FRAC_4_PI: f32 = 0.25 / PI;387const FRAC_3_16_PI: f32 = 0.1875 / PI;388match self {389PhaseFunction::Isotropic => FRAC_4_PI,390PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v),391PhaseFunction::Mie { asymmetry } => {392let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v;393FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt())394}395PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0),396}397}398}399400impl Default for PhaseFunction {401fn default() -> Self {402Self::Mie { asymmetry: 0.8 }403}404}405406407