mod gpu;
#[cfg(test)]
mod gpu_tests;
#[cfg(test)]
mod model;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use bevy::asset::{RenderAssetUsages, embedded_asset};
use bevy::camera::Exposure;
use bevy::image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
use bevy::light::Atmosphere;
use bevy::light::light_consts::lux;
use bevy::math::DVec2;
use bevy::pbr::{DistanceFog, FogFalloff};
use bevy::prelude::*;
use bevy::render::RenderApp;
use bevy::render::extract_component::ExtractComponent;
use bevy::render::render_resource::{
Extent3d, ShaderType, TextureDimension, TextureFormat, TextureUsages,
};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::load_shader_library;
use crate::celestial::{
CelestialFrame, CloudLight, MOON_TINT, SunState, beam_transmittance, cloud_ambient,
scene_illuminance, sky_gain,
};
use crate::climate::Weather;
use crate::flash::LightningFlash;
use crate::gpu::grid::storage_buffer;
use crate::gpu::readback::{Readbacks, decode};
use crate::plugin::{ElementsQuality, ElementsSystems, ElementsView, ensure_gpu_core};
use crate::query::EnvironmentSnapshot;
fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
pub struct CloudLayer {
pub base_m: f32,
pub top_m: f32,
}
impl Default for CloudLayer {
fn default() -> Self {
Self {
base_m: 1500.0,
top_m: 3300.0,
}
}
}
impl CloudLayer {
pub fn from_weather(weather: &Weather) -> Self {
let rh = weather.relative_humidity;
let rain = weather.precipitation_mm_h;
let low = (smoothstep(0.9, 0.98, rh) * (1.0 - smoothstep(0.1, 0.5, rain)))
.max(smoothstep(0.1, 0.3, rain) * (1.0 - smoothstep(0.8, 1.5, rain)));
let base_m = 1500.0 + (600.0 - 1500.0) * low;
let kind = Self::kind_from_weather(weather);
let thickness = if kind <= 0.5 {
800.0 + 1000.0 * (kind / 0.5).min(1.0)
} else {
1800.0 + 6200.0 * (kind - 0.5) / 0.5
};
Self {
base_m,
top_m: base_m + thickness,
}
}
pub fn kind_from_weather(weather: &Weather) -> f32 {
let c = weather.cloud_cover;
let stratus = smoothstep(0.5, 0.9, c).max(smoothstep(0.9, 0.98, weather.relative_humidity));
let towering = (weather.storminess / 6.0)
.clamp(0.0, 1.0)
.max(smoothstep(12.0, 16.0, weather.wind_speed_ms) * smoothstep(0.9, 1.0, c));
0.5 * (1.0 - stratus) * (1.0 - towering) + towering
}
pub fn precipitation_from_weather(weather: &Weather) -> f32 {
(weather.precipitation_mm_h / 25.0).clamp(0.0, 1.0)
}
}
#[derive(Resource, Clone, Debug)]
pub struct CloudWeatherMap {
pub map: Handle<Image>,
pub origin: Vec2,
pub extent: f32,
}
#[derive(Resource, Clone, Debug)]
pub struct CloudShadows {
pub map: Handle<Image>,
pub origin: Vec2,
pub extent: f32,
pub light_direction: Vec3,
pub camera_transmittance: f32,
}
pub(crate) const SHADOW_MIN_LIGHT_SINE: f32 = 0.1;
impl CloudShadows {
pub fn generation_direction(&self) -> Vec3 {
let l = self.light_direction;
if l.y >= SHADOW_MIN_LIGHT_SINE {
return l;
}
let flat = l.xz().normalize_or(Vec2::X);
let c = (1.0 - SHADOW_MIN_LIGHT_SINE * SHADOW_MIN_LIGHT_SINE).sqrt();
Vec3::new(flat.x * c, SHADOW_MIN_LIGHT_SINE, flat.y * c)
}
pub fn uv(&self, position: Vec3) -> Vec2 {
let l = self.generation_direction();
(position.xz() - l.xz() * (position.y / l.y) - self.origin) / self.extent
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CloudQuality {
pub block: u32,
pub steps: u32,
pub light_samples: u32,
pub window: u32,
}
impl CloudQuality {
pub fn of(quality: ElementsQuality) -> Self {
let (block, steps) = match quality {
ElementsQuality::Low => (4, 32),
ElementsQuality::Medium => (4, 48),
ElementsQuality::High => (2, 64),
ElementsQuality::Ultra => (2, 96),
};
Self {
block,
steps,
light_samples: 6,
window: block * block,
}
}
}
pub(crate) const WEATHER_TEXELS: u32 = 512;
pub(crate) const WEATHER_EXTENT_M: f32 = 32_000.0;
pub(crate) const SHADOW_TEXELS: u32 = 1024;
pub(crate) const SHADOW_EXTENT_M: f32 = 8_000.0;
pub(crate) const NOISE_BASE_TEXELS: u32 = 128;
pub(crate) const NOISE_DETAIL_TEXELS: u32 = 32;
const BASE_TILE_M: f64 = 8192.0;
const DETAIL_TILE_M: f64 = 1024.0;
const DRIFT_WRAP_M: f64 = 524_288_000.0;
const DRIFT_WIND_FACTOR: f64 = 2.0;
const SUN_HANDOVER_DEG: f32 = -4.0;
const GROUND_ALBEDO: f32 = 0.3;
const AIRLIGHT_PER_SKY_RADIANCE: f32 = (1.0 + GROUND_ALBEDO) / 4.0;
const CAMERA_TRANSMITTANCE_TAU_S: f32 = 1.0;
#[derive(Clone, Copy, Debug, Default, ShaderType)]
pub(crate) struct CloudParams {
pub camera: Vec4,
pub atmo: Vec4,
pub sky: Vec4,
pub layer: Vec4,
pub light: Vec4,
pub light_e: Vec4,
pub drift: Vec4,
pub drift_detail: Vec4,
pub weather: Vec4,
pub shadow: Vec4,
pub flash: Vec4,
pub counts: UVec4,
pub sizes: UVec4,
pub misc: Vec4,
pub tail: UVec4,
pub haze: Vec4,
}
#[derive(Clone, Debug)]
pub(crate) struct CloudViewImages {
pub size: UVec2,
pub march: Handle<Image>,
pub history: [Handle<Image>; 2],
}
#[derive(Resource, Clone)]
pub(crate) struct CloudGpu {
pub base: Handle<Image>,
pub detail: Handle<Image>,
pub weather: Handle<Image>,
pub shadow: Handle<Image>,
pub histogram: Handle<ShaderBuffer>,
pub threshold: Handle<ShaderBuffer>,
pub camera_t: Handle<ShaderBuffer>,
pub view: Option<CloudViewImages>,
pub params: CloudParams,
pub active: bool,
pub live: Arc<AtomicBool>,
}
#[derive(Component, Clone, Copy, Debug, Default, ExtractComponent)]
#[extract_app(RenderApp)]
pub(crate) struct CloudView;
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct CloudGpuUpdate;
#[derive(Resource, Default)]
struct CloudRuntime {
drift: DVec2,
frame: u32,
quality: Option<ElementsQuality>,
reset_frames: u32,
exposure: f32,
}
#[cfg(test)]
#[derive(Resource, Clone, Debug, Default)]
pub(crate) struct CloudDebug {
pub mode: f32,
pub sigma: f32,
pub layer: Option<(f32, f32)>,
pub light: Option<Vec3>,
pub light_e: Option<Vec3>,
pub kind: Option<f32>,
pub cover: Option<f32>,
pub drift: Option<Vec2>,
}
pub struct CloudPlugin;
impl Plugin for CloudPlugin {
fn build(&self, app: &mut App) {
ensure_gpu_core(app);
load_shader_library!(app, "common.wesl");
embedded_asset!(app, "generate.wesl");
embedded_asset!(app, "weather.wesl");
embedded_asset!(app, "shadow.wesl");
embedded_asset!(app, "march.wesl");
embedded_asset!(app, "resolve.wesl");
embedded_asset!(app, "composite.wesl");
app.init_resource::<LightningFlash>()
.init_resource::<CloudLayer>()
.init_resource::<CloudRuntime>()
.add_systems(Startup, create_resources)
.add_systems(
Update,
(
smooth_camera_transmittance
.in_set(ElementsSystems::Celestial)
.before(crate::celestial::update_sky),
(
mark_view,
update_layer,
advance_drift,
update_views,
update_gpu
.after(crate::lightning::write_flash)
.in_set(CloudGpuUpdate),
)
.chain()
.in_set(ElementsSystems::Simulation),
read_camera_transmittance.in_set(ElementsSystems::Readback),
),
);
#[cfg(test)]
app.add_systems(
Update,
apply_debug
.in_set(ElementsSystems::Simulation)
.after(update_gpu),
);
gpu::plugin(app);
}
}
fn texture(
images: &mut Assets<Image>,
size: Extent3d,
dimension: TextureDimension,
format: TextureFormat,
pixel: &[u8],
address: ImageAddressMode,
) -> Handle<Image> {
let mut image = Image::new_fill(
size,
dimension,
pixel,
format,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage = TextureUsages::STORAGE_BINDING
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_SRC
| TextureUsages::COPY_DST;
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: address,
address_mode_v: address,
address_mode_w: address,
mag_filter: ImageFilterMode::Linear,
min_filter: ImageFilterMode::Linear,
mipmap_filter: ImageFilterMode::Linear,
..default()
});
images.add(image)
}
const HALF_ONE: [u8; 2] = [0x00, 0x3C];
fn create_resources(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
) {
let volume = |n: u32| Extent3d {
width: n,
height: n,
depth_or_array_layers: n,
};
let plane = |n: u32| Extent3d {
width: n,
height: n,
depth_or_array_layers: 1,
};
let base = texture(
&mut images,
volume(NOISE_BASE_TEXELS),
TextureDimension::D3,
TextureFormat::Rgba8Unorm,
&[0; 4],
ImageAddressMode::Repeat,
);
let detail = texture(
&mut images,
volume(NOISE_DETAIL_TEXELS),
TextureDimension::D3,
TextureFormat::Rgba8Unorm,
&[0; 4],
ImageAddressMode::Repeat,
);
let weather = texture(
&mut images,
plane(WEATHER_TEXELS),
TextureDimension::D2,
TextureFormat::Rgba8Unorm,
&[0; 4],
ImageAddressMode::Repeat,
);
let shadow = texture(
&mut images,
plane(SHADOW_TEXELS),
TextureDimension::D2,
TextureFormat::Rgba16Float,
&[HALF_ONE[0], HALF_ONE[1], 0, 0, 0, 0, 0, 0],
ImageAddressMode::ClampToEdge,
);
commands.insert_resource(CloudWeatherMap {
map: weather.clone(),
origin: Vec2::ZERO,
extent: WEATHER_EXTENT_M,
});
commands.insert_resource(CloudShadows {
map: shadow.clone(),
origin: Vec2::ZERO,
extent: SHADOW_EXTENT_M,
light_direction: Vec3::Y,
camera_transmittance: 1.0,
});
commands.insert_resource(CloudGpu {
base,
detail,
weather,
shadow,
histogram: storage_buffer(&mut buffers, "elements_clouds_histogram", vec![0u32; 256]),
threshold: storage_buffer(&mut buffers, "elements_clouds_threshold", vec![0f32; 4]),
camera_t: storage_buffer(&mut buffers, "elements_clouds_camera_t", vec![0f32; 4]),
view: None,
params: CloudParams::default(),
active: false,
live: Arc::new(AtomicBool::new(false)),
});
}
fn mark_view(
mut commands: Commands,
view: Res<ElementsView>,
marked: Query<Entity, With<CloudView>>,
) {
for entity in &marked {
if Some(entity) != view.entity {
commands.entity(entity).remove::<CloudView>();
}
}
if let Some(entity) = view.entity
&& !marked.contains(entity)
{
commands.entity(entity).insert(CloudView);
}
}
fn update_layer(weather: Res<Weather>, mut layer: ResMut<CloudLayer>) {
let next = CloudLayer::from_weather(&weather);
if *layer != next {
*layer = next;
}
}
fn advance_drift(
time: Res<Time<Virtual>>,
weather: Res<Weather>,
mut runtime: ResMut<CloudRuntime>,
) {
let wind = weather.wind_direction.as_dvec2() * f64::from(weather.wind_speed_ms);
let next = runtime.drift + wind * DRIFT_WIND_FACTOR * time.delta_secs_f64();
runtime.drift = next.rem_euclid(DVec2::splat(DRIFT_WRAP_M));
}
fn update_views(
view: Res<ElementsView>,
quality: Res<ElementsQuality>,
cameras: Query<&Camera>,
mut images: ResMut<Assets<Image>>,
mut gpu: ResMut<CloudGpu>,
mut runtime: ResMut<CloudRuntime>,
) {
let size = view
.entity
.and_then(|e| cameras.get(e).ok())
.and_then(Camera::physical_target_size)
.filter(|s| s.x > 0 && s.y > 0);
let Some(size) = size else {
if gpu.view.is_some() {
gpu.view = None;
}
return;
};
let up_to_date =
gpu.view.as_ref().is_some_and(|v| v.size == size) && runtime.quality == Some(*quality);
if up_to_date {
return;
}
let block = CloudQuality::of(*quality).block;
let low = (size + UVec2::splat(block - 1)) / block;
let plane = |s: UVec2| Extent3d {
width: s.x,
height: s.y,
depth_or_array_layers: 1,
};
let clear = [0, 0, 0, 0, 0, 0, HALF_ONE[0], HALF_ONE[1]];
let mut make = |s: UVec2| {
texture(
&mut images,
plane(s),
TextureDimension::D2,
TextureFormat::Rgba16Float,
&clear,
ImageAddressMode::ClampToEdge,
)
};
gpu.view = Some(CloudViewImages {
size,
march: make(low),
history: [make(size), make(size)],
});
runtime.quality = Some(*quality);
runtime.reset_frames = 2;
}
fn pick_light(sun: &SunState, frame: &CelestialFrame) -> (Vec3, Vec3) {
if sun.altitude_deg > SUN_HANDOVER_DEG {
let weight = smoothstep(SUN_HANDOVER_DEG, SUN_HANDOVER_DEG + 1.0, sun.altitude_deg);
(sun.direction, Vec3::splat(lux::RAW_SUNLIGHT * weight))
} else {
(sun.moon_direction, MOON_TINT * frame.moon_lux)
}
}
fn snap(value: Vec2, step: f32) -> Vec2 {
(value / step).round() * step
}
#[allow(clippy::too_many_arguments)]
fn update_gpu(
view: Res<ElementsView>,
weather: Res<Weather>,
sun: Res<SunState>,
frame: Res<CelestialFrame>,
flash: Res<LightningFlash>,
quality: Res<ElementsQuality>,
layer: Res<CloudLayer>,
atmospheres: Query<&Atmosphere>,
exposures: Query<&Exposure>,
fogs: Query<&DistanceFog>,
mut runtime: ResMut<CloudRuntime>,
mut gpu: ResMut<CloudGpu>,
mut map: ResMut<CloudWeatherMap>,
mut shadows: ResMut<CloudShadows>,
) {
let Some(entity) = view.entity else {
gpu.active = false;
return;
};
let camera = view.position;
let (inner, outer) = atmospheres
.iter()
.next()
.map_or((6_360_000.0, 6_460_000.0), |a| {
(a.inner_radius, a.outer_radius)
});
let (light_dir, light_e) = pick_light(&sun, &frame);
let sky_share = 1.0 + (0.15 - 1.0) * smoothstep(-2.0, 10.0, sun.altitude_deg);
let ambient = sky_share * scene_illuminance(&sun, frame.moon_lux, CloudLight::CLEAR)
/ (2.0 * std::f32::consts::PI);
let weather_texel = WEATHER_EXTENT_M / WEATHER_TEXELS as f32;
let shadow_texel = SHADOW_EXTENT_M / SHADOW_TEXELS as f32;
map.origin = snap(
camera.xz() - Vec2::splat(WEATHER_EXTENT_M * 0.5),
weather_texel,
);
shadows.origin = snap(
camera.xz() - Vec2::splat(SHADOW_EXTENT_M * 0.5),
shadow_texel,
);
shadows.light_direction = light_dir;
let exposure = exposures.get(entity).map_or(1.0, Exposure::exposure);
let exposure_ratio = if runtime.exposure > 0.0 && exposure > 0.0 {
runtime.exposure / exposure
} else {
1.0
};
runtime.exposure = exposure;
runtime.frame = runtime.frame.wrapping_add(1);
let reset = runtime.reset_frames > 0;
runtime.reset_frames = runtime.reset_frames.saturating_sub(1);
let q = CloudQuality::of(*quality);
let size = gpu.view.as_ref().map_or(UVec2::ONE, |v| v.size);
let low = (size + UVec2::splat(q.block - 1)) / q.block;
let drift = runtime.drift;
let wrap = |period: f64| {
Vec2::new(
drift.x.rem_euclid(period) as f32,
drift.y.rem_euclid(period) as f32,
)
};
let field = wrap(f64::from(WEATHER_EXTENT_M));
let base = wrap(BASE_TILE_M);
let detail = wrap(DETAIL_TILE_M);
let wind = weather.wind_direction.normalize_or(Vec2::X);
gpu.active = true;
let cloud = CloudLight::from_beam(shadows.camera_transmittance);
let (diffuse_colour, diffuse_brightness) = cloud_ambient(&sun, frame.moon_lux, cloud);
let airlight = diffuse_colour * (diffuse_brightness * AIRLIGHT_PER_SKY_RADIANCE);
gpu.params = CloudParams {
camera: camera.extend(inner),
atmo: Vec4::new(outer, 32_000.0, 32.0, 0.0),
sky: Vec4::new(sky_gain(&sun, cloud), airlight.x, airlight.y, airlight.z),
layer: Vec4::new(
layer.base_m,
layer.top_m,
CloudLayer::kind_from_weather(&weather),
weather.cloud_cover.clamp(0.0, 1.0),
),
light: light_dir.extend(0.0),
light_e: light_e.extend(ambient),
drift: Vec4::new(field.x, field.y, base.x, base.y),
drift_detail: Vec4::new(detail.x, detail.y, wind.x, wind.y),
weather: Vec4::new(
map.origin.x,
map.origin.y,
WEATHER_EXTENT_M,
CloudLayer::precipitation_from_weather(&weather),
),
shadow: Vec4::new(shadows.origin.x, shadows.origin.y, SHADOW_EXTENT_M, 0.0),
flash: flash.position.extend(flash.intensity_cd.max(0.0)),
counts: UVec4::new(runtime.frame, q.steps, q.block, q.light_samples),
sizes: UVec4::new(size.x, size.y, low.x, low.y),
misc: Vec4::new(exposure_ratio, 0.0, 0.0, if reset { 1.0 } else { 0.0 }),
tail: UVec4::ZERO,
haze: fogs.get(entity).map_or(Vec4::ZERO, haze),
};
}
fn haze(fog: &DistanceFog) -> Vec4 {
let extinction = match fog.falloff {
FogFalloff::Atmospheric { extinction, .. } => extinction.x,
FogFalloff::Exponential { density } => density,
FogFalloff::ExponentialSquared { .. } | FogFalloff::Linear { .. } => 0.0,
};
let c = fog.color.to_linear();
Vec4::new(c.red, c.green, c.blue, extinction)
}
#[cfg(test)]
fn apply_debug(debug: Option<Res<CloudDebug>>, mut gpu: ResMut<CloudGpu>) {
let Some(debug) = debug else {
return;
};
let p = &mut gpu.params;
p.misc.y = debug.mode;
p.misc.z = debug.sigma;
if let Some((base, top)) = debug.layer {
p.layer.x = base;
p.layer.y = top;
}
if let Some(kind) = debug.kind {
p.layer.z = kind;
}
if let Some(cover) = debug.cover {
p.layer.w = cover;
}
if let Some(light) = debug.light {
p.light = light.extend(0.0);
}
if let Some(e) = debug.light_e {
p.light_e = e.extend(p.light_e.w);
}
if let Some(drift) = debug.drift {
p.drift.x = drift.x.rem_euclid(WEATHER_EXTENT_M);
p.drift.y = drift.y.rem_euclid(WEATHER_EXTENT_M);
p.drift.z = drift.x.rem_euclid(BASE_TILE_M as f32);
p.drift.w = drift.y.rem_euclid(BASE_TILE_M as f32);
p.drift_detail.x = drift.x.rem_euclid(DETAIL_TILE_M as f32);
p.drift_detail.y = drift.y.rem_euclid(DETAIL_TILE_M as f32);
}
}
fn smooth_camera_transmittance(
time: Res<Time<Real>>,
weather: Res<Weather>,
snapshot: Res<EnvironmentSnapshot>,
shadows: Option<ResMut<CloudShadows>>,
) {
let Some(mut shadows) = shadows else {
return;
};
match snapshot.cloud_transmittance {
None => {
shadows.camera_transmittance = beam_transmittance(weather.cloud_cover);
}
Some(measured) => {
let k = 1.0 - (-time.delta_secs() / CAMERA_TRANSMITTANCE_TAU_S).exp();
let now = shadows.camera_transmittance;
shadows.camera_transmittance = now + (measured.clamp(0.0, 1.0) - now) * k;
}
}
}
fn read_camera_transmittance(gpu: Option<Res<CloudGpu>>, mut readbacks: Readbacks) {
let Some(gpu) = gpu else {
return;
};
if !gpu.active || !gpu.live.load(Ordering::Relaxed) {
return;
}
readbacks.request("cloud_camera_t", gpu.camera_t.clone(), |bytes, snapshot| {
if let Some(values) = decode::<f32>(bytes)
&& let Some(&t) = values.first()
&& t.is_finite()
{
snapshot.cloud_transmittance = Some(t);
}
});
}