Skip to main content

Crate bevy_elements

Crate bevy_elements 

Source
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.

Solstice noon at latitude 45°: a 68° sun, short shadows19:30 on the solstice: a 2° sun, long warm shadows, the sky graded to the horizon23:00: stars from the Yale Bright Star Catalogue, a half moon low in the east, moon shadows
Noon, 21 June, 45° NSunset 19:3023:00, half moon
The Fog preset: a bright ground-fog bank hiding everything past 100 m, blue sky overheadThe Clear preset of the same scenebevy_flora trees in autumn colour moved by this crate’s wind and rain
Fog presetClear presetwith 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

PluginModuleWhat it does
ClimatePluginclimateClimate presets, seasons, a seeded hourly weather scheduler, WeatherDirector to force and release weather, SurfaceWeathering (wetness, snow cover)
CelestialPlugincelestialWorldClock, GeoLocation, Meeus-accuracy sun and moon (astro), sun and moon DirectionalLights for Bevy’s atmosphere, the bright-star sky, a shaded moon disc, exposure
WindPluginwindA camera-centred stable-fluids wind field on the GPU, curl-noise turbulence, travelling gusts, a public WGSL import
FogPluginfogDistance fog from the weather’s visibility and a camera-following ground-fog volume written by compute
ElementsFloraPlugin (feature flora)floraDrives 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 (feature flora): add ElementsFloraPlugin next to FloraPlugin. It writes FloraWind (direction, strength = 10 m speed / 20 m/s, turbulence, gust strength, and every WindGust as a GustFront), FloraSeason (autumn, leaf fall, blossom, snow = SurfaceWeathering::snow_cover) and FloraWetness. Trees and their sway stay bevy_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::wetness into a blood-drying rate, or Environment::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).

QualityWind gridJacobi iterationsWindGround-fog densityTotal
Low32 × 8 × 32100.087 ms0.037 ms0.124 ms
Medium48 × 12 × 48100.119 ms0.037 ms0.156 ms
High64 × 16 × 64120.233 ms0.037 ms0.270 ms
Ultra96 × 24 × 96401.704 ms0.037 ms1.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_flora owns them.
  • Isosurface and voxel meshing: isomesh owns 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_elementsbevy
0.10.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.

ExampleShows
quickstartThe quick start with a test scene
day_nightA solstice day at 45° N in two minutes; ELEMENTS_HOUR=19.5 holds an hour
weather_tourEvery preset for 20 s; ELEMENTS_PRESET=Fog holds one
flora_windbevy_flora trees in this crate’s wind, season and rain (--features flora)
bench_elementsPer-pass GPU time; ELEMENTS_QUALITY, ELEMENTS_RESOLUTION

§License

MIT OR Apache-2.0, at your option.

Modules§

astro
Positional astronomy at Meeus accuracy: pure f64 functions, no ECS.

Structs§

CelestialPlugin
Sun, moon and exposure.
CivilDate
A proleptic Gregorian calendar date.
Climate
The long-run character of a place. Drives the weather scheduler and the temperature model.
ClimatePlugin
Climate, seasons, weather and surface weathering.
ElementsCamera
Marks the camera the environment follows and renders for.
ElementsFloraPlugin
Drives bevy_flora’s FloraWind, FloraSeason and FloraWetness from this crate.
ElementsPlugins
Every domain plugin that exists in this version, in dependency order.
ElementsReadback
How often GPU state is copied back for gameplay queries.
Environment
Ask about the environment from gameplay systems. Order such systems .after(ElementsSystems::Readback).
EnvironmentSnapshot
CPU copies of GPU simulation state, filled by the throttled readback (ElementsReadback). Read it through Environment.
FogPlugin
Distance fog on every ElementsCamera and a ground-fog volume around the followed one.
GeoLocation
Where the observer stands on Earth.
GroundFog
Marks the camera-following ground-fog FogVolume this crate spawns.
Moon
Marks the moon’s DirectionalLight.
SeasonChanged
The season changed. Also written once on the first frame, so a reader can sync without polling SeasonState.
SeasonState
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.
SurfaceWeathering
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.
WeatherChanged
A weather transition started: by the scheduler, by WeatherDirector::force or by WeatherDirector::release.
WeatherDirector
Who decides the weather: the seeded scheduler, or the game through force.
WindFieldParams
The uniform the bevy_elements::wind WGSL import takes. Bind it with #[uniform(N)] wind: WindFieldParams and fill it from WindState::field_params.
WindGust
A gust band started: it travels from origin along direction.
WindPlugin
The GPU wind field.
WindState
The wind, for shaders, sibling crates and gameplay.
WorldClock
The game’s calendar and time of day. Advances in ElementsSystems::Clock by Time<Virtual> delta × time_scale.

Enums§

ElementsQuality
Resolution and step counts for every domain. Read once per frame; changing it at runtime re-creates the affected GPU resources.
ElementsSystems
The phases of one environment frame, chained in this order in Update.
Season
A meteorological season: whole months, December–February is northern winter.
WeatherPreset
A named weather state the scheduler moves between.

Constants§

FLORA_FULL_WIND_MS
Wind speed at 10 m, m/s, that bevy_flora’s FloraWind::strength of 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 dust rises.
log_wind_speed
Mean wind speed at height_m above ground for u10 at 10 m, m/s: the neutral log law u10 · ln(max(z, z0)/z0) / ln(10/z0) with z0 = 0.03 m.
volume_light_compensation
FogVolume::light_intensity that makes the volume’s sunlit in-scattering right at the camera.