mod gpu;
#[cfg(test)]
mod gpu_tests;
#[cfg(test)]
mod model;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use bevy::asset::{RenderAssetUsages, embedded_asset};
use bevy::camera::Exposure;
use bevy::image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
use bevy::light::light_consts::lux;
use bevy::light::{Atmosphere, GeneratedEnvironmentMapLight};
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, TextureViewDescriptor,
TextureViewDimension,
};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::load_shader_library;
use crate::celestial::{
CelestialFrame, CelestialSettings, CloudLight, MOON_TINT, NIGHT_SKY_LUMINANCE, Sun, SunState,
beam_transmittance, cloud_ambient, scene_illuminance, sky_clear_gain, sky_gain,
};
use crate::climate::Weather;
use crate::flash::LightningFlash;
use crate::fog::{HORIZON_RING, HorizonRing};
use crate::gpu::grid::storage_buffer;
use crate::gpu::readback::{Readbacks, decode};
use crate::plugin::{
ElementsQuality, ElementsSystems, ElementsUnits, ElementsView, ensure_gpu_core,
needs_depth_prepass,
};
use crate::query::EnvironmentSnapshot;
use crate::terrain::TerrainGrid;
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 mean_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,
pub probe_edge: u32,
}
impl CloudQuality {
pub fn of(quality: ElementsQuality) -> Self {
let (block, steps, probe_edge) = match quality {
ElementsQuality::Low => (4, 32, 32),
ElementsQuality::Medium => (4, 48, 32),
ElementsQuality::High => (2, 64, 32),
ElementsQuality::Ultra => (2, 96, 64),
};
Self {
block,
steps,
light_samples: 6,
window: block * block,
probe_edge,
}
}
pub(crate) fn probe_block(&self, size: u32) -> u32 {
(size / self.probe_edge.max(1))
.clamp(1, 4)
.next_power_of_two()
.min(4)
}
}
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 SHADOW_COARSE_STRIDE: u32 = 4;
pub(crate) const SHADOW_COARSE_CELLS: u32 = SHADOW_TEXELS / SHADOW_COARSE_STRIDE;
pub(crate) const NOISE_BASE_TEXELS: u32 = 128;
pub(crate) const NOISE_DETAIL_TEXELS: u32 = 32;
const BASE_TILE_M: f64 = 4096.0;
const DETAIL_TILE_M: f64 = 640.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 MEAN_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,
pub air: Vec4,
pub units: Vec4,
pub probe: Vec4,
pub sea: Vec4,
pub sea_box: 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 shadow_coarse: Handle<ShaderBuffer>,
pub horizon: Handle<ShaderBuffer>,
pub sky_probe: Handle<Image>,
pub neutral_sky: Handle<Image>,
pub view: Option<CloudViewImages>,
pub probe: Option<SkyProbeImages>,
pub probe_frames: Arc<AtomicU32>,
pub params: CloudParams,
pub active: bool,
pub live: Arc<AtomicBool>,
}
#[derive(Clone, Debug)]
pub(crate) struct SkyProbeImages {
pub size: u32,
pub block: u32,
pub atmosphere: Handle<Image>,
pub state: [Handle<Image>; 2],
pub composed: Handle<Image>,
}
#[derive(Component, Clone, Debug)]
pub(crate) struct SkyProbe {
pub atmosphere: Handle<Image>,
pub composed: Handle<Image>,
pub lit: bool,
}
#[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 air: Option<f32>,
}
pub struct CloudPlugin;
impl Plugin for CloudPlugin {
fn build(&self, app: &mut App) {
ensure_gpu_core(app);
needs_depth_prepass(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>()
.init_resource::<CelestialFrame>()
.add_systems(Startup, create_resources)
.add_systems(
Update,
(
smooth_mean_transmittance
.in_set(ElementsSystems::Celestial)
.before(crate::celestial::update_sky),
(
mark_view,
update_layer,
advance_drift,
update_views,
update_sky_probe,
update_gpu
.after(crate::lightning::write_flash)
.in_set(CloudGpuUpdate),
)
.chain()
.in_set(ElementsSystems::Simulation),
read_shadows.in_set(ElementsSystems::Readback),
read_horizon.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 neutral_sky_cube(images: &mut Assets<Image>) -> Handle<Image> {
let texel = [HALF_ONE[0], HALF_ONE[1]].repeat(4);
let mut image = Image::new_fill(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 6,
},
TextureDimension::D2,
&texel,
TextureFormat::Rgba16Float,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::Cube),
..default()
});
images.add(image)
}
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,
);
let neutral_sky = neutral_sky_cube(&mut images);
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,
mean_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]),
shadow_coarse: storage_buffer(
&mut buffers,
"elements_clouds_shadow_coarse",
vec![1f32; (SHADOW_COARSE_CELLS * SHADOW_COARSE_CELLS) as usize],
),
horizon: storage_buffer(
&mut buffers,
"elements_clouds_horizon",
vec![[0f32; 4]; HORIZON_RING],
),
sky_probe: neutral_sky.clone(),
neutral_sky,
view: None,
probe: None,
probe_frames: Arc::new(AtomicU32::new(0)),
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;
}
const PROBE_HISTORY_WEIGHT: f32 = 0.25;
fn update_sky_probe(
mut commands: Commands,
view: Res<ElementsView>,
quality: Res<ElementsQuality>,
mut images: ResMut<Assets<Image>>,
mut gpu: ResMut<CloudGpu>,
mut cameras: Query<(
Entity,
&mut GeneratedEnvironmentMapLight,
Option<&mut SkyProbe>,
)>,
orphans: Query<Entity, (With<SkyProbe>, Without<GeneratedEnvironmentMapLight>)>,
) {
for entity in &orphans {
commands.entity(entity).remove::<SkyProbe>();
}
for (entity, mut light, probe) in &mut cameras {
if Some(entity) == view.entity {
continue;
}
if let Some(probe) = probe {
if light.environment_map == probe.composed {
light.environment_map = probe.atmosphere.clone();
}
commands.entity(entity).remove::<SkyProbe>();
}
}
let Some((entity, mut light, probe)) = view.entity.and_then(|e| cameras.get_mut(e).ok()) else {
gpu.probe = None;
return;
};
let atmosphere = match probe.as_deref() {
Some(p) if light.environment_map == p.composed => p.atmosphere.clone(),
_ => light.environment_map.clone(),
};
let Some(size) = images
.get(&atmosphere)
.map(|image| image.texture_descriptor.size.width)
else {
gpu.probe = None;
return;
};
let block = CloudQuality::of(*quality).probe_block(size);
let current = gpu
.probe
.as_ref()
.is_some_and(|p| p.atmosphere == atmosphere && p.size == size && p.block == block);
if !current {
let fresh = sky_probe_images(&mut images, atmosphere.clone(), size, block);
gpu.probe_frames.store(0, Ordering::Relaxed);
if light.environment_map != atmosphere {
light.environment_map = atmosphere.clone();
}
commands.entity(entity).insert(SkyProbe {
atmosphere,
composed: fresh.composed.clone(),
lit: false,
});
gpu.probe = Some(fresh);
return;
}
if let Some(mut probe) = probe
&& !probe.lit
&& gpu.probe_frames.load(Ordering::Relaxed) >= block * block
{
light.environment_map = probe.composed.clone();
probe.lit = true;
}
}
fn sky_probe_images(
images: &mut Assets<Image>,
atmosphere: Handle<Image>,
size: u32,
block: u32,
) -> SkyProbeImages {
let mut cube = |pixel: &[u8], dimension: TextureViewDimension| {
let mut image = Image::new_fill(
Extent3d {
width: size,
height: size,
depth_or_array_layers: 6,
},
TextureDimension::D2,
pixel,
TextureFormat::Rgba16Float,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage = TextureUsages::STORAGE_BINDING
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_SRC;
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(dimension),
..default()
});
images.add(image)
};
let clear = [0, 0, 0, 0, 0, 0, HALF_ONE[0], HALF_ONE[1]];
let state = [
cube(&clear, TextureViewDimension::D2Array),
cube(&clear, TextureViewDimension::D2Array),
];
let composed = cube(&[0; 8], TextureViewDimension::Cube);
SkyProbeImages {
size,
block,
atmosphere,
state,
composed,
}
}
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
}
type SeaInputs<'w> = (
Option<Res<'w, crate::StillWater>>,
Option<Res<'w, crate::TerrainState>>,
);
#[allow(clippy::too_many_arguments)]
fn update_gpu(
view: Res<ElementsView>,
weather: Res<Weather>,
sun: Res<SunState>,
frame: Res<CelestialFrame>,
(flash, units, sea): (Res<LightningFlash>, Res<ElementsUnits>, SeaInputs<'_>),
quality: Res<ElementsQuality>,
layer: Res<CloudLayer>,
atmospheres: Query<&Atmosphere>,
exposures: Query<&Exposure>,
fogs: Query<&DistanceFog>,
probes: Query<&GeneratedEnvironmentMapLight>,
mut runtime: ResMut<CloudRuntime>,
mut gpu: ResMut<CloudGpu>,
mut map: ResMut<CloudWeatherMap>,
mut shadows: ResMut<CloudShadows>,
own_sun: (
Option<Res<CelestialSettings>>,
Query<&GlobalTransform, With<Sun>>,
),
) {
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 (mut light_dir, light_e) = pick_light(&sun, &frame);
let (celestial, suns) = &own_sun;
if celestial.as_ref().is_some_and(|c| !c.lights)
&& let Some(own) = suns.iter().next()
{
light_dir = *own.back();
}
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;
gpu.sky_probe = match &gpu.probe {
Some(probe) => probe.atmosphere.clone(),
None => probes
.get(entity)
.map_or_else(|_| gpu.neutral_sky.clone(), |p| p.environment_map.clone()),
};
let probe = gpu.probe.as_ref().map_or(Vec4::ZERO, |p| {
let fresh = gpu.probe_frames.load(Ordering::Relaxed) < p.block * p.block;
Vec4::new(
exposure,
p.block as f32,
p.size as f32,
if fresh { 1.0 } else { PROBE_HISTORY_WEIGHT },
)
});
let cloud = CloudLight::from_beam(shadows.mean_transmittance);
let (diffuse_colour, diffuse_brightness) = cloud_ambient(&sun, frame.moon_lux, cloud);
let airlight = diffuse_colour * (diffuse_brightness * AIRLIGHT_PER_SKY_RADIANCE);
let (sea, sea_box) = match (&sea.0, &sea.1) {
(Some(still), Some(terrain)) => {
let reach = crate::hydrology::WATER_SKIRT_REACH_M;
let lo = terrain.grid.origin - Vec2::splat(reach);
let hi = terrain.grid.origin + terrain.grid.extent() + Vec2::splat(reach);
(
Vec4::new(
terrain.grid.y0 + still.level / units.per_metre(),
1.0,
0.0,
0.0,
),
Vec4::new(lo.x, lo.y, hi.x, hi.y),
)
}
_ => (Vec4::ZERO, Vec4::ZERO),
};
gpu.params = CloudParams {
camera: camera.extend(inner),
atmo: Vec4::new(outer, 32_000.0, 32.0, NIGHT_SKY_LUMINANCE),
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: units
.point_to_metres(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, |fog| haze(fog, units.per_metre())),
air: Vec4::new(
fogs.get(entity).map_or(0.0, |fog| {
crate::fog::sky_air_extinction(fog_extinction(fog, units.per_metre()))
}),
sky_clear_gain(&sun, cloud),
0.0,
0.0,
),
units: Vec4::new(units.per_metre(), 0.0, 0.0, 0.0),
probe,
sea,
sea_box,
};
}
fn haze(fog: &DistanceFog, units: f32) -> Vec4 {
let c = fog.color.to_linear();
Vec4::new(c.red, c.green, c.blue, fog_extinction(fog, units))
}
fn fog_extinction(fog: &DistanceFog, units: f32) -> f32 {
let per_world_unit = match fog.falloff {
FogFalloff::Atmospheric { extinction, .. } => extinction.x,
FogFalloff::Exponential { density } => density,
FogFalloff::ExponentialSquared { .. } | FogFalloff::Linear { .. } => 0.0,
};
per_world_unit * units
}
#[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(air) = debug.air {
p.air.x = air;
}
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_mean_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_shadow {
None => {
shadows.mean_transmittance = beam_transmittance(weather.cloud_cover);
}
Some(measured) => {
let k = 1.0 - (-time.delta_secs() / MEAN_TRANSMITTANCE_TAU_S).exp();
let now = shadows.mean_transmittance;
shadows.mean_transmittance = now + (measured.mean - now) * k;
}
}
}
fn read_shadows(
gpu: Option<Res<CloudGpu>>,
shadows: Option<Res<CloudShadows>>,
mut readbacks: Readbacks,
) {
let (Some(gpu), Some(shadows)) = (gpu, shadows) else {
return;
};
if !gpu.active || !gpu.live.load(Ordering::Relaxed) {
return;
}
let grid = CloudShadowSnapshot::grid(shadows.origin);
let light = shadows.generation_direction();
readbacks.request(
"cloud_shadow",
gpu.shadow_coarse.clone(),
move |bytes, snapshot| {
let cells = (SHADOW_COARSE_CELLS * SHADOW_COARSE_CELLS) as usize;
if let Some(values) = decode::<f32>(bytes)
&& values.len() >= cells
{
let values = &values[..cells];
let (sum, count) = values
.iter()
.filter(|t| t.is_finite())
.fold((0.0f64, 0u32), |(s, n), &t| {
(s + f64::from(t.clamp(0.0, 1.0)), n + 1)
});
snapshot.cloud_shadow = Some(CloudShadowSnapshot {
grid,
light,
transmittance: values.into(),
mean: if count > 0 {
(sum / f64::from(count)) as f32
} else {
1.0
},
});
}
},
);
}
fn read_horizon(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_horizon",
gpu.horizon.clone(),
move |bytes, snapshot| {
if let Some(values) = decode::<[f32; 4]>(bytes)
&& values.len() >= HORIZON_RING
&& values[..HORIZON_RING].iter().all(|v| v[3] > 0.5)
{
let mut ring = [Vec3::ZERO; HORIZON_RING];
for (slot, v) in ring.iter_mut().zip(&values) {
*slot = Vec3::new(v[0], v[1], v[2]);
}
snapshot.horizon = Some(HorizonRing(ring));
}
},
);
}
#[derive(Clone, Debug)]
pub(crate) struct CloudShadowSnapshot {
pub grid: TerrainGrid,
pub light: Vec3,
pub transmittance: Arc<[f32]>,
pub mean: f32,
}
impl CloudShadowSnapshot {
pub(crate) fn grid(origin: Vec2) -> TerrainGrid {
let texel = SHADOW_EXTENT_M / SHADOW_TEXELS as f32;
TerrainGrid {
origin: origin + Vec2::splat(0.5 * texel),
cell_m: texel * SHADOW_COARSE_STRIDE as f32,
size: UVec2::splat(SHADOW_COARSE_CELLS),
y0: 0.0,
}
}
pub(crate) fn transmittance_at(&self, position: Vec3) -> f32 {
let q = position.xz() - self.light.xz() * (position.y / self.light.y);
if !self.grid.contains(q) {
return 1.0;
}
self.grid.sample(&self.transmittance, q).clamp(0.0, 1.0)
}
}