Skip to main content

Crate bevy_lights

Crate bevy_lights 

Source
Expand description

Practical lights for Bevy: the lights a scene’s props actually carry.

A world can have thousands of torches and only afford a dozen real lights. This crate is the three pieces that make that look right:

  • Flicker and Kindle — FlickerPlugin modulates a point or spot light around its base intensity with the doubled sine a fire’s light has, and flares a light up over KINDLE_SECONDS when it is first lit instead of switching it on. Kindled is the message a host hears, for the sound of a lamp catching.
  • LightPool — LightPoolPlugin keeps a fixed number of real lights and hands them to the PoolCandidates nearest the host’s focus entity every frame; the rest of the candidates are virtual. The lights it spawns carry PoolLight, so a host can style them (a fire’s palette recolours them, its flames are not drawn on them).
  • FlameMaterial, FlameSite — FlamePlugin draws a billboarded, additive shader flame standing on every FlameSite, out of one material and one draw per flame: src/flame.wesl.

Under those three is the cinematographer’s half, model::cinematography: key_to_fill and stops for a scene’s key against its fill, fill_for for the sky that makes a night, fire_lumens for the size of a pool, and blackbody_colour and white_balanced for a light’s colour from the temperature it burns at. They touch no light: the same numbers a director of photography sets before a lamp is hung, for a host to size and colour its practicals with.

Every plugin is independent; adding some and not others is supported, and LightPoolPlugin warns (it does not panic) when FlickerPlugin is missing.

App::new()
    .add_plugins((FlickerPlugin, LightPoolPlugin::<MyFocus>::default(), FlamePlugin))
    .add_systems(Startup, |mut commands: Commands| {
        // A brazier: `diameter` is its fuel bed, which sizes the flicker through the puffing law
        // (`f = 1.5/√D` Hz, so 0.34 m is 2.6 Hz). Leave it `None` for the un-sized rate.
        commands.spawn((PointLight { intensity: 60_000.0, range: 12.0, ..default() },
            Flicker { base: 60_000.0, phase: 0.0, diameter: Some(0.34), ..default() },
            Kindle::default(),
            // `diameter` matches the `Flicker`'s, so the drawn flame breathes with the light.
            FlameSite { height: 0.9, phase: 0.0, kindle: 0.0, diameter: Some(0.34),
                depth: FLICKER_DEPTH, depth_fade: false, fire: None }));
        commands.spawn((MyFocus, Transform::default()));
        // A carried fire: one entity holds the `FlameSite` and the `PoolCandidate`, so the light it
        // is handed sits in the flame the host can see. `offset` is the flame's own centre, measured
        // from the site's origin, and the lumens are sized for the height the light will sit at.
        let height = 0.35;
        commands.spawn((PoolCandidate {
                offset: Vec3::from(flame_light_offset(height)),
                intensity: 8_000.0,
                ..default()
            },
            FlameSite { height, phase: 9.0, kindle: 0.0, diameter: None, depth: FLICKER_DEPTH,
                depth_fade: false, fire: None },
            Transform::from_xyz(4.0, 0.0, 2.0)));
    });

The App above is not run by the doctest: FlamePlugin adds a MaterialPlugin, which needs a render app, so running it would need a GPU. tests/flicker.rs and tests/pool.rs are the behavioural proof for the other two plugins, and src/flame.rs’s own tests cover the flame dressing headlessly.

§What this crate does not own

The sun, the moon, the sky, exposure and weather belong to a sky crate; a gameplay illuminance grid, to a light-grid crate; and which lamp is lit and when, to the game or to a society crate. This crate owns the practicals: their flicker, their kindling, the pool that decides which of them are real, and the flames drawn on them. It never adds a directional light and never meters exposure.

§Units

Intensities are Bevy’s: lumens for a PointLight and a SpotLight. Distances are the host’s world units. Nothing in this crate converts, so a host in feet is in feet throughout.

Re-exports§

pub use model::CALM_FLICKER_DEPTH;
pub use model::EMBER_AT;
pub use model::FADE_SECONDS;
pub use model::FLICKER_DEPTH;
pub use model::HIGH_KEY;
pub use model::HYSTERESIS;
pub use model::KINDLE_SECONDS;
pub use model::LOW_KEY;
pub use model::MAX_FLICKER_DEPTH;
pub use model::MOON_KEY;
pub use model::RANGE_MARGIN;
pub use model::WHIPPING_FLICKER_DEPTH;
pub use model::blackbody_colour;
pub use model::blackbody_luminance;
pub use model::char_field;
pub use model::char_radiance;
pub use model::coal_radiance;
pub use model::fade_factor;
pub use model::fill_for;
pub use model::fire_lumens;
pub use model::flame_light_offset;
pub use model::flicker_depth;
pub use model::flicker_factor;
pub use model::flicker_mean;
pub use model::flicker_swing;
pub use model::hand_off;
pub use model::key_to_fill;
pub use model::kindle_gain;
pub use model::pack_flame_tag;
pub use model::preferred;
pub use model::raised_light;
pub use model::stops;
pub use model::unpack_flame_tag;
pub use model::white_balanced;

Modules§

model
The engine-free half of the crate: plain f32 maths with no bevy in it.

Structs§

Flame
The drawn flame: the child mesh FlamePlugin spawns under each FlameSite.
FlameLook
How the scene’s flames look: the fire colour every flame is tinted from, how strongly it adds, and the height scale a FlameSite’s height is measured against.
FlameMaterial
The flame material: the host’s fire colour (.a is the ember’s strength, which scales the flame) and the animation’s clock and shape.
FlamePlugin
FlamePlugin: draws a flame on every FlameSite and ticks their shared clock.
FlameSite
A fire drawn where no light is: a torch’s head, a sconce’s cage, a brazier whose light is a child entity.
Flicker
A light’s mean intensity and the clock phase its flicker runs at.
FlickerPlugin
FlickerPlugin: modulates every Flicker light and advances every Kindle.
Kindle
A light that is taking hold: its intensity flares up over KINDLE_SECONDS instead of switching on.
Kindled
A fire took hold this frame: its Kindle began, so the host can play the sound of it.
LightPool
The pool’s budget and the light it spawns: how many real lights exist, how far a candidate may be from the focus to get one, how brightly the pool burns, and the light every slot starts from.
LightPoolPlugin
LightPoolPlugin: keeps LightPool::size real lights and hands them to the nearest PoolCandidates.
PoolBudget
Which candidates hold a real light right now, and how many wanted one: the pool’s lamp schedule.
PoolCandidate
A prop that would like one of the pool’s real lights: how bright it burns, and where its light sits relative to the prop’s origin.
PoolLight
One of the crate’s spawned real lights. Marks an entity the pool owns and drives.

Enums§

LightsSystems
The order the crate’s systems run in Update.
Rank
How LightPool ranks its candidates — and, since LightPool::hysteresis compares the same quantity, how it decides whether a challenger is worth a hand-off.