use alloc::{borrow::Cow, sync::Arc};
use core::f32::{self, consts::PI};
use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp, AssetId};
use bevy_ecs::{
resource::Resource,
system::{Commands, Res, SystemParamItem},
};
use bevy_math::{ops, Curve, FloatPow, Vec3, Vec4};
use bevy_reflect::TypePath;
use bevy_render::{
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin},
render_resource::{
Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDataOrder,
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
TextureViewDescriptor,
},
renderer::{RenderDevice, RenderQueue},
RenderApp, RenderStartup,
};
use smallvec::SmallVec;
#[doc(hidden)]
pub struct ScatteringMediumPlugin;
impl Plugin for ScatteringMediumPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<ScatteringMedium>()
.add_plugins(RenderAssetPlugin::<GpuScatteringMedium>::default());
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(RenderStartup, init_scattering_medium_sampler);
}
}
}
#[derive(TypePath, Asset, Clone)]
pub struct ScatteringMedium {
pub label: Option<Cow<'static, str>>,
pub falloff_resolution: u32,
pub phase_resolution: u32,
pub terms: SmallVec<[ScatteringTerm; 1]>,
}
impl Default for ScatteringMedium {
fn default() -> Self {
ScatteringMedium::earthlike(256, 256)
}
}
impl ScatteringMedium {
pub fn new(
falloff_resolution: u32,
phase_resolution: u32,
terms: impl IntoIterator<Item = ScatteringTerm>,
) -> Self {
Self {
label: None,
falloff_resolution,
phase_resolution,
terms: terms.into_iter().collect(),
}
}
pub fn with_label(self, label: impl Into<Cow<'static, str>>) -> Self {
Self {
label: Some(label.into()),
..self
}
}
pub fn with_density_multiplier(mut self, multiplier: f32) -> Self {
self.terms.iter_mut().for_each(|term| {
term.absorption *= multiplier;
term.scattering *= multiplier;
});
self
}
pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self {
Self::new(
falloff_resolution,
phase_resolution,
[
ScatteringTerm {
absorption: Vec3::ZERO,
scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
falloff: Falloff::Exponential { scale: 8.0 / 60.0 },
phase: PhaseFunction::Rayleigh,
},
ScatteringTerm {
absorption: Vec3::splat(3.996e-6),
scattering: Vec3::splat(0.444e-6),
falloff: Falloff::Exponential { scale: 1.2 / 60.0 },
phase: PhaseFunction::Mie { asymmetry: 0.8 },
},
ScatteringTerm {
absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
scattering: Vec3::ZERO,
falloff: Falloff::Tent {
center: 0.75,
width: 0.3,
},
phase: PhaseFunction::Isotropic,
},
],
)
.with_label("earthlike_atmosphere")
}
}
#[derive(Default, Clone)]
pub struct ScatteringTerm {
pub absorption: Vec3,
pub scattering: Vec3,
pub falloff: Falloff,
pub phase: PhaseFunction,
}
#[derive(Default, Clone)]
pub enum Falloff {
#[default]
Linear,
Exponential {
scale: f32,
},
Tent {
center: f32,
width: f32,
},
Curve(Arc<dyn Curve<f32> + Send + Sync>),
}
impl Falloff {
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
Self::Curve(Arc::new(curve))
}
fn sample(&self, p: f32) -> f32 {
match self {
Falloff::Linear => p,
Falloff::Exponential { scale } => {
if *scale == 0.0 {
p
} else {
let s = -1.0 / scale;
let exp_p_s = ops::exp((1.0 - p) * s);
let exp_s = ops::exp(s);
(exp_p_s - exp_s) / (1.0 - exp_s)
}
}
Falloff::Tent { center, width } => (1.0 - (p - center).abs() / (0.5 * width)).max(0.0),
Falloff::Curve(curve) => curve.sample(p).unwrap_or(0.0),
}
}
}
#[derive(Clone)]
pub enum PhaseFunction {
Isotropic,
Rayleigh,
Mie {
asymmetry: f32,
},
Curve(Arc<dyn Curve<f32> + Send + Sync>),
}
impl PhaseFunction {
pub fn from_curve(curve: impl Curve<f32> + Send + Sync + 'static) -> Self {
Self::Curve(Arc::new(curve))
}
fn sample(&self, neg_l_dot_v: f32) -> f32 {
const FRAC_4_PI: f32 = 0.25 / PI;
const FRAC_3_16_PI: f32 = 0.1875 / PI;
match self {
PhaseFunction::Isotropic => FRAC_4_PI,
PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v),
PhaseFunction::Mie { asymmetry } => {
let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v;
FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt())
}
PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0),
}
}
}
impl Default for PhaseFunction {
fn default() -> Self {
Self::Mie { asymmetry: 0.8 }
}
}
pub struct GpuScatteringMedium {
pub terms: SmallVec<[ScatteringTerm; 1]>,
pub falloff_resolution: u32,
pub phase_resolution: u32,
pub density_lut: Texture,
pub density_lut_view: TextureView,
pub scattering_lut: Texture,
pub scattering_lut_view: TextureView,
}
impl RenderAsset for GpuScatteringMedium {
type SourceAsset = ScatteringMedium;
type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>);
fn prepare_asset(
source_asset: Self::SourceAsset,
_asset_id: AssetId<Self::SourceAsset>,
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
_previous_asset: Option<&Self>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let mut density: Vec<Vec4> =
Vec::with_capacity(2 * source_asset.falloff_resolution as usize);
density.extend((0..source_asset.falloff_resolution).map(|i| {
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
source_asset
.terms
.iter()
.map(|term| term.absorption.extend(0.0) * term.falloff.sample(falloff))
.sum::<Vec4>()
}));
density.extend((0..source_asset.falloff_resolution).map(|i| {
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
source_asset
.terms
.iter()
.map(|term| term.scattering.extend(0.0) * term.falloff.sample(falloff))
.sum::<Vec4>()
}));
let mut scattering: Vec<Vec4> = Vec::with_capacity(
source_asset.falloff_resolution as usize * source_asset.phase_resolution as usize,
);
scattering.extend(
(0..source_asset.falloff_resolution * source_asset.phase_resolution).map(|raw_i| {
let i = raw_i % source_asset.phase_resolution;
let j = raw_i / source_asset.phase_resolution;
let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32;
let phase = (j as f32 + 0.5) / source_asset.phase_resolution as f32;
let neg_l_dot_v = phase * 2.0 - 1.0;
source_asset
.terms
.iter()
.map(|term| {
term.scattering.extend(0.0)
* term.falloff.sample(falloff)
* term.phase.sample(neg_l_dot_v)
})
.sum::<Vec4>()
}),
);
let density_lut = render_device.create_texture_with_data(
render_queue,
&TextureDescriptor {
label: source_asset
.label
.as_deref()
.map(|label| format!("{}_density_lut", label))
.as_deref()
.or(Some("scattering_medium_density_lut")),
size: Extent3d {
width: source_asset.falloff_resolution,
height: 2,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba32Float,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
TextureDataOrder::LayerMajor,
bytemuck::cast_slice(density.as_slice()),
);
let density_lut_view = density_lut.create_view(&TextureViewDescriptor {
label: source_asset
.label
.as_deref()
.map(|label| format!("{}_density_lut_view", label))
.as_deref()
.or(Some("scattering_medium_density_lut_view")),
..Default::default()
});
let scattering_lut = render_device.create_texture_with_data(
render_queue,
&TextureDescriptor {
label: source_asset
.label
.as_deref()
.map(|label| format!("{}_scattering_lut", label))
.as_deref()
.or(Some("scattering_medium_scattering_lut")),
size: Extent3d {
width: source_asset.falloff_resolution,
height: source_asset.phase_resolution,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba32Float,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
TextureDataOrder::LayerMajor,
bytemuck::cast_slice(scattering.as_slice()),
);
let scattering_lut_view = scattering_lut.create_view(&TextureViewDescriptor {
label: source_asset
.label
.as_deref()
.map(|label| format!("{}_scattering_lut", label))
.as_deref()
.or(Some("scattering_medium_scattering_lut_view")),
..Default::default()
});
Ok(Self {
terms: source_asset.terms,
falloff_resolution: source_asset.falloff_resolution,
phase_resolution: source_asset.phase_resolution,
density_lut,
density_lut_view,
scattering_lut,
scattering_lut_view,
})
}
}
#[derive(Resource)]
pub struct ScatteringMediumSampler(Sampler);
impl ScatteringMediumSampler {
pub fn sampler(&self) -> &Sampler {
&self.0
}
}
fn init_scattering_medium_sampler(mut commands: Commands, render_device: Res<RenderDevice>) {
let sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("scattering_medium_sampler"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
});
commands.insert_resource(ScatteringMediumSampler(sampler));
}