use alloc::{borrow::Cow, sync::Arc};1use core::f32::{self, consts::PI};23use bevy_app::{App, Plugin};4use bevy_asset::{Asset, AssetApp, AssetId};5use bevy_ecs::{6resource::Resource,7system::{Commands, Res, SystemParamItem},8};9use bevy_math::{ops, Curve, FloatPow, Vec3, Vec4};10use bevy_reflect::TypePath;11use bevy_render::{12render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin},13render_resource::{14Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDataOrder,15TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,16TextureViewDescriptor,17},18renderer::{RenderDevice, RenderQueue},19RenderApp, RenderStartup,20};21use smallvec::SmallVec;2223#[doc(hidden)]24pub struct ScatteringMediumPlugin;2526impl Plugin for ScatteringMediumPlugin {27fn build(&self, app: &mut App) {28app.init_asset::<ScatteringMedium>()29.add_plugins(RenderAssetPlugin::<GpuScatteringMedium>::default());3031if let Some(render_app) = app.get_sub_app_mut(RenderApp) {32render_app.add_systems(RenderStartup, init_scattering_medium_sampler);33}34}35}3637/// An asset that defines how a material scatters light.38///39/// In order to calculate how light passes through a medium,40/// you need three pieces of information:41/// - how much light the medium *absorbs* per unit length42/// - how much light the medium *scatters* per unit length43/// - what *directions* the medium is likely to scatter light in.44///45/// The first two are fairly simple, and are sometimes referred to together46/// (accurately enough) as the medium's [optical density].47///48/// The last, defined by a [phase function], is the most important in creating49/// the look of a medium. Our brains are very good at noticing (if unconsciously)50/// that a dust storm scatters light differently than a rain cloud, for example.51/// See the docs on [`PhaseFunction`] for more info.52///53/// In reality, media are often composed of multiple elements that scatter light54/// independently, for Earth's atmosphere is composed of the gas itself, but also55/// suspended dust and particulate. These each scatter light differently, and are56/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`],57/// these are each represented by a [`ScatteringTerm`]58///59/// ## Technical Details60///61/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which62/// are re-created every time the asset is modified. See the docs on63/// [`GpuScatteringMedium`] for more info.64///65/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density66/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions67#[derive(TypePath, Asset, Clone)]68pub struct ScatteringMedium {69/// An optional label for the medium, used when creating the LUTs on the GPU.70pub label: Option<Cow<'static, str>>,71/// The resolution at which to sample the falloff distribution of each72/// scattering term. Custom or more detailed distributions may benefit73/// from a higher value, at the cost of more memory use.74pub falloff_resolution: u32,75/// The resolution at which to sample the phase function of each scattering76/// term. Custom or more detailed phase functions may benefit from a higher77/// value, at the cost of more memory use.78pub phase_resolution: u32,79/// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`]80pub terms: SmallVec<[ScatteringTerm; 1]>,81}8283impl ScatteringMedium {84// Returns a scattering medium with a default label and the85// specified scattering terms.86pub fn new(87falloff_resolution: u32,88phase_resolution: u32,89terms: impl IntoIterator<Item = ScatteringTerm>,90) -> Self {91Self {92label: None,93falloff_resolution,94phase_resolution,95terms: terms.into_iter().collect(),96}97}9899// Consumes and returns this scattering medium with a new label.100pub fn with_label(self, label: impl Into<Cow<'static, str>>) -> Self {101Self {102label: Some(label.into()),103..self104}105}106107// Consumes and returns this scattering medium with each scattering terms'108// densities multiplied by `multiplier`.109pub fn with_density_multiplier(mut self, multiplier: f32) -> Self {110self.terms.iter_mut().for_each(|term| {111term.absorption *= multiplier;112term.scattering *= multiplier;113});114115self116}117118/// Returns a scattering medium representing an earthlike atmosphere.119pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self {120Self::new(121falloff_resolution,122phase_resolution,123[124ScatteringTerm {125absorption: Vec3::ZERO,126scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),127falloff: Falloff::Exponential { strength: 12.5 },128phase: PhaseFunction::Rayleigh,129},130ScatteringTerm {131absorption: Vec3::splat(3.996e-6),132scattering: Vec3::splat(0.444e-6),133falloff: Falloff::Exponential { strength: 83.5 },134phase: PhaseFunction::Mie { asymmetry: 0.8 },135},136ScatteringTerm {137absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),138scattering: Vec3::ZERO,139falloff: Falloff::Tent {140center: 0.75,141width: 0.3,142},143phase: PhaseFunction::Isotropic,144},145],146)147.with_label("earthlike_atmosphere")148}149}150151/// An individual element of a [`ScatteringMedium`].152///153/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s,154/// which correspond to an individual element of the medium. For example, Earth's155/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric156/// gases themselves, which extend to the edge of space, and suspended dust particles,157/// which are denser but lie closer to the ground.158#[derive(Default, Clone)]159pub struct ScatteringTerm {160/// This term's optical obsorption density, or how much light of each wavelength161/// it absorbs per meter.162///163/// units: m^-1164pub absorption: Vec3,165/// This term's optical scattering density, or how much light of each wavelength166/// it scatters per meter.167///168/// units: m^-1169pub scattering: Vec3,170/// This term's falloff distribution. See the docs on [`Falloff`] for more info.171pub falloff: Falloff,172/// This term's [phase function], which determines the character of how it173/// scatters light. See the docs on [`PhaseFunction`] for more info.174///175/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions176pub phase: PhaseFunction,177}178179/// Describes how the media in a [`ScatteringTerm`] is distributed.180///181/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and182/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere;183/// near the edge of space Earth's atmosphere is much less dense, and it absorbs184/// and scatters less light.185///186/// [`Falloff`] determines how the density of a medium changes as a function of187/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the188/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium189/// fades away completely, i.e. at the edge of space, and values between scale190/// linearly with distance, so `p = 0.5` would be halfway between the surface191/// and the edge of space.192///193/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values194/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`.195#[derive(Default, Clone)]196pub enum Falloff {197/// A simple linear falloff function, which essentially198/// passes the falloff parameter through unchanged.199///200/// f(1) = 1201/// f(0) = 0202/// f(p) = p203#[default]204Linear,205/// An exponential falloff function with adjustable strength.206///207/// f(1) = 1208/// f(0) = 0209/// f(p) = (e^sp - 1)/(e^s - 1)210Exponential {211/// The "strength" of the exponential falloff. The higher212/// this value is, the quicker the medium's density will213/// decrease with distance.214///215/// domain: (-∞, ∞)216strength: f32,217},218/// A tent-shaped falloff function, which produces a triangular219/// peak at the center and linearly falls off to either side.220///221/// f(`center`) = 1222/// f(`center` +- `width` / 2) = 0223Tent {224/// The center of the tent function peak225///226/// domain: [0, 1]227center: f32,228/// The total width of the tent function peak229///230/// domain: [0, 1]231width: f32,232},233/// A falloff function defined by a custom curve.234///235/// domain: [0, 1],236/// range: [0, 1],237Curve(Arc<dyn Curve<f32> + Send + Sync>),238}239240impl Falloff {241/// Returns a falloff function corresponding to a custom curve.242pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {243Self::Curve(Arc::new(curve))244}245246fn sample(&self, falloff: f32) -> f32 {247match self {248Falloff::Linear => falloff,249Falloff::Exponential { strength } => {250// fill discontinuity at strength == 0251if *strength == 0.0 {252falloff253} else {254let scale_exp_m1 = ops::exp_m1(*strength);255let domain_offset = ops::ln(scale_exp_m1.abs());256let range_offset = scale_exp_m1.recip();257let eval_pos = falloff * strength - domain_offset;258scale_exp_m1.signum() * ops::exp(eval_pos) - range_offset259}260}261Falloff::Tent { center, width } => {262(1.0 - (falloff - center).abs() / (0.5 * width)).max(0.0)263}264Falloff::Curve(curve) => curve.sample(falloff).unwrap_or(0.0),265}266}267}268269/// Describes how a [`ScatteringTerm`] scatters light in different directions.270///271/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0`272/// whose input is the cosine of the angle between an incoming light direction and273/// and outgoing light direction, and whose output is the proportion of the incoming274/// light that is actually scattered in that direction.275///276/// The phase function has an important effect on the "look" of a medium in a scene.277/// Media consisting of particles of a different size or shape scatter light differently,278/// and our brains are very good at telling the difference. A dust cloud, which might279/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different280/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh`281///282/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions283#[derive(Clone)]284pub enum PhaseFunction {285/// A phase function that scatters light evenly in all directions.286Isotropic,287288/// A phase function representing [Rayleigh scattering].289///290/// Rayleigh scattering occurs naturally for particles much smaller than291/// the wavelengths of visible light, such as gas molecules in the atmosphere.292/// It's generally wavelength-dependent, where shorter wavelengths are scattered293/// more strongly, so [scattering](ScatteringTerm::scattering) should have294/// higher values for blue than green and green than red. Particles that295/// participate in Rayleigh scattering don't absorb any light, either.296///297/// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering298Rayleigh,299300/// The [Henyey-Greenstein phase function], which approximates [Mie scattering].301///302/// Mie scattering occurs naturally for spherical particles of dust303/// and aerosols roughly the same size as the wavelengths of visible light,304/// so it's useful for representing dust or sea spray. It's generally305/// wavelength-independent, so [absorption](ScatteringTerm::absorption)306/// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value.307///308/// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering309/// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function310Mie {311/// Whether the Mie scattering function is biased towards scattering312/// light forwards (asymmetry > 0) or backwards (asymmetry < 0).313///314/// domain: [-1, 1]315asymmetry: f32,316},317318/// A phase function defined by a custom curve, where the input319/// is the cosine of the angle between the incoming light ray320/// and the scattered light ray, and the output is the fraction321/// of the incoming light scattered in that direction.322///323/// Note: it's important for photorealism that the phase function324/// be *energy conserving*, meaning that in total no more light can325/// be scattered than actually entered the medium. For this to be326/// the case, the integral of the phase function over its domain must327/// be equal to 1/2π.328///329/// 1330/// ∫ p(x) dx = 1/2π331/// -1332///333/// domain: [-1, 1]334/// range: [0, 1]335Curve(Arc<dyn Curve<f32> + Send + Sync>),336}337338impl PhaseFunction {339/// A phase function defined by a custom curve.340pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {341Self::Curve(Arc::new(curve))342}343344fn sample(&self, neg_l_dot_v: f32) -> f32 {345const FRAC_4_PI: f32 = 0.25 / PI;346const FRAC_3_16_PI: f32 = 0.1875 / PI;347match self {348PhaseFunction::Isotropic => FRAC_4_PI,349PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v),350PhaseFunction::Mie { asymmetry } => {351let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v;352FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt())353}354PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0),355}356}357}358359impl Default for PhaseFunction {360fn default() -> Self {361Self::Mie { asymmetry: 0.8 }362}363}364365/// The GPU representation of a [`ScatteringMedium`].366pub struct GpuScatteringMedium {367/// The terms of the scattering medium.368pub terms: SmallVec<[ScatteringTerm; 1]>,369/// The resolution at which to sample the falloff distribution of each370/// scattering term.371pub falloff_resolution: u32,372/// The resolution at which to sample the phase function of each373/// scattering term.374pub phase_resolution: u32,375/// The `density_lut`, a 2D `falloff_resolution x 2` LUT which contains the376/// medium's optical density with respect to the atmosphere's "falloff parameter",377/// a linear value which is 1.0 at the planet's surface and 0.0 at the edge of378/// space. The first and second rows correspond to absorption density and379/// scattering density respectively.380pub density_lut: Texture,381/// The default [`TextureView`] of the `density_lut`382pub density_lut_view: TextureView,383/// The `scattering_lut`, a 2D `falloff_resolution x phase_resolution` LUT which384/// contains the medium's scattering density multiplied by the phase function, with385/// the U axis corresponding to the falloff parameter and the V axis corresponding386/// to `neg_LdotV * 0.5 + 0.5`, where `neg_LdotV` is the dot product of the light387/// direction and the incoming view vector.388pub scattering_lut: Texture,389/// The default [`TextureView`] of the `scattering_lut`390pub scattering_lut_view: TextureView,391}392393impl RenderAsset for GpuScatteringMedium {394type SourceAsset = ScatteringMedium;395396type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>);397398fn prepare_asset(399source_asset: Self::SourceAsset,400_asset_id: AssetId<Self::SourceAsset>,401(render_device, render_queue): &mut SystemParamItem<Self::Param>,402_previous_asset: Option<&Self>,403) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {404let mut density: Vec<Vec4> =405Vec::with_capacity(2 * source_asset.falloff_resolution as usize);406407density.extend((0..source_asset.falloff_resolution).map(|i| {408let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;409410source_asset411.terms412.iter()413.map(|term| term.absorption.extend(0.0) * term.falloff.sample(falloff))414.sum::<Vec4>()415}));416417density.extend((0..source_asset.falloff_resolution).map(|i| {418let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;419420source_asset421.terms422.iter()423.map(|term| term.scattering.extend(0.0) * term.falloff.sample(falloff))424.sum::<Vec4>()425}));426427let mut scattering: Vec<Vec4> = Vec::with_capacity(428source_asset.falloff_resolution as usize * source_asset.phase_resolution as usize,429);430431scattering.extend(432(0..source_asset.falloff_resolution * source_asset.phase_resolution).map(|raw_i| {433let i = raw_i % source_asset.phase_resolution;434let j = raw_i / source_asset.phase_resolution;435let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;436let phase = (j as f32 + 0.5) / source_asset.phase_resolution as f32;437let neg_l_dot_v = phase * 2.0 - 1.0;438439source_asset440.terms441.iter()442.map(|term| {443term.scattering.extend(0.0)444* term.falloff.sample(falloff)445* term.phase.sample(neg_l_dot_v)446})447.sum::<Vec4>()448}),449);450451let density_lut = render_device.create_texture_with_data(452render_queue,453&TextureDescriptor {454label: source_asset455.label456.as_deref()457.map(|label| format!("{}_density_lut", label))458.as_deref()459.or(Some("scattering_medium_density_lut")),460size: Extent3d {461width: source_asset.falloff_resolution,462height: 2,463depth_or_array_layers: 1,464},465mip_level_count: 1,466sample_count: 1,467dimension: TextureDimension::D2,468format: TextureFormat::Rgba32Float,469usage: TextureUsages::TEXTURE_BINDING,470view_formats: &[],471},472TextureDataOrder::LayerMajor,473bytemuck::cast_slice(density.as_slice()),474);475476let density_lut_view = density_lut.create_view(&TextureViewDescriptor {477label: source_asset478.label479.as_deref()480.map(|label| format!("{}_density_lut_view", label))481.as_deref()482.or(Some("scattering_medium_density_lut_view")),483..Default::default()484});485486let scattering_lut = render_device.create_texture_with_data(487render_queue,488&TextureDescriptor {489label: source_asset490.label491.as_deref()492.map(|label| format!("{}_scattering_lut", label))493.as_deref()494.or(Some("scattering_medium_scattering_lut")),495size: Extent3d {496width: source_asset.falloff_resolution,497height: source_asset.phase_resolution,498depth_or_array_layers: 1,499},500mip_level_count: 1,501sample_count: 1,502dimension: TextureDimension::D2,503format: TextureFormat::Rgba32Float,504usage: TextureUsages::TEXTURE_BINDING,505view_formats: &[],506},507TextureDataOrder::LayerMajor,508bytemuck::cast_slice(scattering.as_slice()),509);510511let scattering_lut_view = scattering_lut.create_view(&TextureViewDescriptor {512label: source_asset513.label514.as_deref()515.map(|label| format!("{}_scattering_lut", label))516.as_deref()517.or(Some("scattering_medium_scattering_lut_view")),518..Default::default()519});520521Ok(Self {522terms: source_asset.terms,523falloff_resolution: source_asset.falloff_resolution,524phase_resolution: source_asset.phase_resolution,525density_lut,526density_lut_view,527scattering_lut,528scattering_lut_view,529})530}531}532533/// The default sampler for all scattering media LUTs.534///535/// Just a bilinear clamp-to-edge sampler, nothing fancy.536#[derive(Resource)]537pub struct ScatteringMediumSampler(Sampler);538539impl ScatteringMediumSampler {540pub fn sampler(&self) -> &Sampler {541&self.0542}543}544545fn init_scattering_medium_sampler(mut commands: Commands, render_device: Res<RenderDevice>) {546let sampler = render_device.create_sampler(&SamplerDescriptor {547label: Some("scattering_medium_sampler"),548mag_filter: FilterMode::Linear,549min_filter: FilterMode::Linear,550..Default::default()551});552553commands.insert_resource(ScatteringMediumSampler(sampler));554}555556557