Expand description
§bevy_elements
⚠️ Vibe Coded — written by an AI agent working from a human’s direction. It is used in a shipping game and covered by tests, but it has had no line-by-line human audit. Read it before you trust it.
Living environments for Bevy 0.20. A sun and moon placed by real ephemerides, the bright-star sky, a seeded climate that schedules hourly weather, a GPU wind field, distance and ground fog, and exposure that keeps night looking like night. Simulation runs in compute shaders; gameplay asks the Environment system parameter and is answered from a throttled async readback.
![]() | ![]() | ![]() |
| Noon, 21 June, 45° N | Sunset 19:30 | 23:00, half moon |
![]() | ![]() | ![]() |
Fog preset | Clear preset | with bevy_flora |
Every system was studied in the home-still paper library first. The note for each one, in docs/research/, names the papers, the equations adopted, every deviation from the plan with its source, and the numbers the tests assert.
§What is in it
| Plugin | Module | What it does |
|---|---|---|
ClimatePlugin | climate | Climate presets, seasons, a seeded hourly weather scheduler, WeatherDirector to force and release weather, SurfaceWeathering (wetness, snow cover) |
CelestialPlugin | celestial | WorldClock, GeoLocation, Meeus-accuracy sun and moon (astro), sun and moon DirectionalLights for Bevy’s atmosphere, the bright-star sky, a shaded moon disc, exposure |
WindPlugin | wind | A camera-centred stable-fluids wind field on the GPU, curl-noise turbulence, travelling gusts, a public WGSL import |
FogPlugin | fog | Distance fog from the weather’s visibility and a camera-following ground-fog volume written by compute |
ElementsFloraPlugin (feature flora) | flora | Drives bevy_flora’s wind, gusts, season and wetness |
ElementsPlugins adds the first four. Each is public on its own, so ElementsPlugins.build().disable::<FogPlugin>() works.
§Install
[dependencies]
bevy_elements = "0.1"§Quick start
use bevy::prelude::*;
use bevy_elements::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, ElementsPlugins))
.insert_resource(
WorldClock::new(CivilDate::new(2026, 6, 21))
.at_hour(7.0)
.with_time_scale(120.0),
)
.insert_resource(Climate::temperate())
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// The environment follows and renders for the `ElementsCamera`.
commands.spawn((
Camera3d::default(),
ElementsCamera::default(),
Transform::from_xyz(0.0, 30.0, 0.0).looking_at(Vec3::new(100.0, 10.0, 0.0), Vec3::Y),
));
// Terrain arrives in 0.3; until then, any ground.
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(2000.0, 2000.0))),
MeshMaterial3d(materials.add(StandardMaterial::default())),
));
}§The API
§Frame order
Everything runs in Update in the chained sets ElementsSystems::{Clock, Weather, Celestial, Simulation, Readback}. Systems that write WorldClock, WeatherDirector or settings run .before(ElementsSystems::Clock); systems that read Environment run .after(ElementsSystems::Readback).
§The camera
ElementsCamera marks the camera the environment follows. Adding it inserts, where absent: AtmosphereSettings, AtmosphereEnvironmentMapLight, DepthPrepass, Msaa::Off, VolumetricFog, DistanceFog, Exposure, and a zero per-camera AmbientLight (sky light comes from the atmosphere’s environment map), and it spawns one Earth Atmosphere if there is none. With auto_exposure (the default) the exposure follows the sun, sky and moon: illuminance-metered like Ghost of Tsushima, mapped with Ward’s contrast-based scale factor over Larson et al.’s rod-and-cone thresholds, so a moonlit scene stays dark instead of being exposed up to daylight. ElementsQuality (Low, Medium, High, Ultra) sets every resolution and step count.
§Clock, sun, moon and stars
WorldClock holds the local date and hour and advances by Time<Virtual> × time_scale; GeoLocation places the observer. SunState has the sun’s and moon’s directions, true altitudes, the synodic phase and the lit fraction. The astro module exposes the ephemeris functions (Meeus ch. 7–48: sidereal time, nutation, the sun at 0.01°, the moon from a 51 + 34-term series, phase, refraction, the USNO total-illuminance factor), each tested against the book’s worked examples and the SPA reference case. Stars are the 8404 Yale Bright Star Catalogue stars to V = 6.5, precessed to the date, coloured from B−V; the moon is a Lommel–Seeliger-shaded disc from NASA’s LROC map. See src/celestial/PROVENANCE.md.
§Climate, seasons and weather
Climate (temperate, alpine, desert, tropical, tundra) drives an hourly Markov scheduler over ten WeatherPresets, seeded and frozen by a golden test, with temperature from annual and diurnal harmonics and a −6.5 °C/km lapse rate. Weather is the blended state every domain reads (cover, precipitation, temperature, humidity, wind, gustiness, visibility, storminess, dust). WeatherDirector::force(preset, transition) overrides it and release(transition) hands it back to the scheduler, which kept stepping meanwhile. SeasonState carries meteorological seasons and 0–1 autumn, leaf-fall and blossom curves; SurfaceWeathering the wetness, snow cover and snow depth of exposed ground. Messages: WeatherChanged, SeasonChanged.
§Wind
WindState has the prevailing wind, the 10 m speed, the gust factor, the wind at the camera and the simulated field: a rgba16float 3D texture, toroidally addressed, around the camera (64 × 16 × 64 cells of 4 m at High). Shaders sample it with the supported WGSL import:
import bevy_elements::wind::{WindFieldParams, elements_wind};
let v = elements_wind(wind_field, wind_sampler, wind_params, world_position);bound to WindState::field and WindState::field_params(). WindGust is written when a gust band starts. bevy_elements::noise (gradient noise with analytic derivatives, fBm, Worley, curl) and bevy_elements::grid (bilinear taps into the crate’s 2D grids) are also supported imports.
§Fog
DistanceFog follows Weather::visibility_m (Koschmieder, 2 % contrast, grey extinction) with a colour lit like the scene; a 240 × 120 × 240 m ground-fog FogVolume around the camera thickens toward the ground and drifts with the wind.
§Gameplay queries
use bevy::prelude::*;
use bevy_elements::*;
fn seek_shelter(env: Environment) {
let weather = env.weather();
if weather.precipitation_mm_h > 2.0 || (!env.is_daylight() && env.temperature_c() < 0.0) {
// ...
}
let gust_at_head = env.wind_at(Vec3::new(10.0, 1.8, -4.0));
let _ = (gust_at_head, env.snapshot_age());
}wind_at answers from the GPU field as of the last readback (4 Hz by default, ElementsReadback) and falls back to the prevailing wind outside it or before the first readback; snapshot_age says how old the answer is.
§Plays well with
bevy_flora(featureflora): addElementsFloraPluginnext toFloraPlugin. It writesFloraWind(direction, strength = 10 m speed / 20 m/s, turbulence, gust strength, and everyWindGustas aGustFront),FloraSeason(autumn, leaf fall, blossom, snow =SurfaceWeathering::snow_cover) andFloraWetness. Trees and their sway staybevy_flora’s.isomesh/bevy_isomesh: volumetric snow on voxel ground arrives in 0.5.bevy_carnage,bevy_light_grid(Bevy 0.19): no dependency either way. Glue in the game copies plain data across, e.g.SurfaceWeathering::wetnessinto a blood-drying rate, orEnvironment::sun()into a light grid’s sky term.
§Performance
GPU time per frame on an RTX 3090 (Vulkan), the minimum over Bevy’s diagnostic history so it is the full-clock cost (examples/bench_elements.rs; a lightly loaded GPU idles at 210–450 MHz and reads 5–10× slower).
| Quality | Wind grid | Jacobi iterations | Wind | Ground-fog density | Total |
|---|---|---|---|---|---|
| Low | 32 × 8 × 32 | 10 | 0.087 ms | 0.037 ms | 0.124 ms |
| Medium | 48 × 12 × 48 | 10 | 0.119 ms | 0.037 ms | 0.156 ms |
| High | 64 × 16 × 64 | 12 | 0.233 ms | 0.037 ms | 0.270 ms |
| Ultra | 96 × 24 × 96 | 40 | 1.704 ms | 0.037 ms | 1.741 ms |
The budget for wind + fog at High is 0.3 ms. Ultra keeps 40 Jacobi iterations and the plan’s grid; it is for large budgets.
§Platforms
Native (Vulkan, Metal, DX12) and wasm WebGPU. WebGL2 is unsupported: the simulation needs compute. Simulation state lives in storage buffers and published fields in write-only rgba16float storage textures: the wind kernels share one layout of six storage buffers and one storage texture, the fog kernel one storage texture, inside WebGPU’s default limits of 8 and 4 per stage (devices limited to the downlevel 4 storage buffers are not supported). GPU timestamps for the bench exist on Vulkan and DX12 only.
§What it deliberately does not do
- Trees, foliage, leaf colour and snow on branches:
bevy_floraowns them. - Isosurface and voxel meshing:
isomeshowns it. - Oceans and FFT waves.
- Bit-deterministic GPU simulation. Astronomy, the weather scheduler and (from 0.2) lightning are deterministic and seeded; GPU results are not.
- A Purkinje shift or other low-light colour change: exposure keeps night dark, but colour handling is left to the tonemapper.
§Compatibility
| bevy_elements | bevy |
|---|---|
| 0.1 | 0.20 |
§Research
- Bevy 0.20 render APIs — the verified paths and signatures this crate builds on.
- What the home-still corpus holds — the reading list per domain.
- Celestial — Meeus, Reda & Andreas (SPA), Krisciunas & Schaefer, Jensen et al., Ballesteros, USNO Circular 171, Larson et al. (tone reproduction).
- Climate and weather — Richardson 1981, ISO 2533, Jennings et al. 2018.
- Wind — Stam 1999, Harris 2004, Bridson et al. 2007, Kim et al. 2008, WMO.
- Fog — Wronski 2014, Hillaire 2015, Wenzel 2006, Real-Time Rendering.
§Examples
Run with cargo run --release --example <name>. Every example takes ELEMENTS_FRAMES=N (exit after N frames) and ELEMENTS_SHOT=/path.png (screenshot on the last frame). Keys: WASD + Q/E to fly (Shift ×5), right mouse to look, [/] time scale, T +1 hour, 1–0 force a preset, R release.
| Example | Shows |
|---|---|
quickstart | The quick start with a test scene |
day_night | A solstice day at 45° N in two minutes; ELEMENTS_HOUR=19.5 holds an hour |
weather_tour | Every preset for 20 s; ELEMENTS_PRESET=Fog holds one |
flora_wind | bevy_flora trees in this crate’s wind, season and rain (--features flora) |
bench_elements | Per-pass GPU time; ELEMENTS_QUALITY, ELEMENTS_RESOLUTION |
§License
MIT OR Apache-2.0, at your option.
Modules§
- astro
- Positional astronomy at Meeus accuracy: pure
f64functions, no ECS.
Structs§
- Celestial
Plugin - Sun, moon and exposure.
- Civil
Date - A proleptic Gregorian calendar date.
- Climate
- The long-run character of a place. Drives the weather scheduler and the temperature model.
- Climate
Plugin - Climate, seasons, weather and surface weathering.
- Elements
Camera - Marks the camera the environment follows and renders for.
- Elements
Flora Plugin - Drives
bevy_flora’sFloraWind,FloraSeasonandFloraWetnessfrom this crate. - Elements
Plugins - Every domain plugin that exists in this version, in dependency order.
- Elements
Readback - How often GPU state is copied back for gameplay queries.
- Environment
- Ask about the environment from gameplay systems. Order such systems
.after(ElementsSystems::Readback). - Environment
Snapshot - CPU copies of GPU simulation state, filled by the throttled readback
(
ElementsReadback). Read it throughEnvironment. - FogPlugin
- Distance fog on every
ElementsCameraand a ground-fog volume around the followed one. - GeoLocation
- Where the observer stands on Earth.
- Ground
Fog - Marks the camera-following ground-fog
FogVolumethis crate spawns. - Moon
- Marks the moon’s
DirectionalLight. - Season
Changed - The season changed. Also written once on the first frame, so a reader can sync without
polling
SeasonState. - Season
State - Where the year is, for foliage and anything else that follows the seasons.
- Sun
- Marks the sun’s
DirectionalLight. - SunState
- Where the sun and moon are and how much of the moon is lit.
- Surface
Weathering - How wet and snowy exposed ground is around the camera. Feeds foliage and any material that wants to look rained or snowed on.
- Weather
- The weather now, blended between presets. Read by every domain.
- Weather
Changed - A weather transition started: by the scheduler, by
WeatherDirector::forceor byWeatherDirector::release. - Weather
Director - Who decides the weather: the seeded scheduler, or the game through
force. - Wind
Field Params - The uniform the
bevy_elements::windWGSL import takes. Bind it with#[uniform(N)] wind: WindFieldParamsand fill it fromWindState::field_params. - Wind
Gust - A gust band started: it travels from
originalongdirection. - Wind
Plugin - The GPU wind field.
- Wind
State - The wind, for shaders, sibling crates and gameplay.
- World
Clock - The game’s calendar and time of day. Advances in
ElementsSystems::ClockbyTime<Virtual>delta ×time_scale.
Enums§
- Elements
Quality - Resolution and step counts for every domain. Read once per frame; changing it at runtime re-creates the affected GPU resources.
- Elements
Systems - The phases of one environment frame, chained in this order in
Update. - Season
- A meteorological season: whole months, December–February is northern winter.
- Weather
Preset - A named weather state the scheduler moves between.
Constants§
- FLORA_
FULL_ WIND_ MS - Wind speed at 10 m, m/s, that
bevy_flora’sFloraWind::strengthof 1 (“a strong wind”) stands for. Gust strengths are converted on the same scale. - GROUND_
FOG_ EXTENT - Size of the ground-fog volume, m: the near field, around the camera; distance fog carries
the far field. Kept small because Bevy attenuates the light reaching every fog sample by the
volume’s bounding radius (see
volume_light_compensation). - GROUND_
FOG_ TEXELS - Texels of the ground-fog density texture.
- KOSCHMIEDER
- Koschmieder’s constant for a 2 % contrast threshold: extinction = 3.912 / visibility.
- LAPSE_
RATE_ C_ PER_ M - Standard-atmosphere temperature lapse rate, °C per m (ISO 2533: −6.5 K/km).
- MAX_
GUSTS - Most gust bands active at once.
- WIND_
CELL_ M - Wind cell edge, m (horizontal and vertical).
- WIND_
ROUGHNESS_ M - Roughness length of the forcing profile, m (WMO open terrain).
Functions§
- distance_
fog_ falloff - Distance fog for a visibility: grey extinction σ_V in every channel (fog, haze and dust
extinguish independently of wavelength), so every colour reaches 2 % transmittance at exactly
visibility_m; in-scattering at the same rate, so scene and fog weights sum to 1 and a fully fogged pixel takes the fog colour. - extinction_
for_ visibility - Extinction coefficient for a visibility, m⁻¹ (Koschmieder, 2 % contrast).
- fog_hue
- Linear-RGB fog colour before brightness: the fog hue, tinted toward sunset orange as the sun
drops below 10°, fully at the horizon, fading out by the end of civil twilight (−6°) so
moonlit fog is not orange; and toward dust as
dustrises. - log_
wind_ speed - Mean wind speed at
height_mabove ground foru10at 10 m, m/s: the neutral log lawu10 · ln(max(z, z0)/z0) / ln(10/z0)with z0 = 0.03 m. - volume_
light_ compensation FogVolume::light_intensitythat makes the volume’s sunlit in-scattering right at the camera.





