Layering
saddle-world-day-night is split into pure logic plus a thin Bevy integration layer.
Pure Rust:
time.rsphase.rscelestial.rsgradient.rslighting.rs
Bevy-facing:
components.rsmessages.rsconfig.rssystems.rslib.rs
The pure modules own clock math, phase resolution, keyframe interpolation, sun/moon solving, Kelvin conversion, and lighting resolution. The Bevy layer only initializes resources, advances the runtime, emits messages, and applies resolved state to Bevy components and resources.
Runtime Flow
DayNightConfig
-> advance TimeOfDay
-> resolve DayPhase
-> solve CelestialState
-> resolve DayNightLighting
-> emit phase messages
-> apply to Sun / Moon / GlobalAmbientLight / DayNightCamera targets
-> publish DayNightDiagnostics
-> update TimeReactive entities (insert/remove TimeActive markers)DayNightConfig
-> advance TimeOfDay
-> resolve DayPhase
-> solve CelestialState
-> resolve DayNightLighting
-> emit phase messages
-> apply to Sun / Moon / GlobalAmbientLight / DayNightCamera targets
-> publish DayNightDiagnostics
-> update TimeReactive entities (insert/remove TimeActive markers)Schedule Ordering
DayNightSystems is intentionally public and chained in this order:
AdvanceTimeResolveCelestialResolveLightingDetectPhaseTransitionsApplyLightingUpdateTimeReactive
This keeps all downstream reads stable inside one frame:
CelestialStatealways sees the currentTimeOfDayDayNightLightingalways sees the currentCelestialState- phase messages are emitted from the same step that produced the resolved phase
- component writes happen after the pure resources are finalized
The crate accepts injectable activate, deactivate, and update schedules so downstream games can map it into their own state machine or feature pipeline.
Ownership
Lights
Two supported patterns exist:
- Leave
ManagedLightConfig::auto_spawn = trueand let the crate createSun/Moondirectional lights on demand. - Spawn your own directional lights and tag them with
Sun/Moon.
The crate never mutates untagged directional lights.
Cameras
The crate mutates only entities tagged with DayNightCamera.
DayNightCamera controls whether the crate:
- writes
DistanceFog - writes
VolumetricFog - writes
Exposure - writes
AtmosphereEnvironmentMapLight - inserts missing components automatically
- ensures
Atmosphere/AtmosphereSettings
This keeps camera ownership explicit. If a project wants a split between gameplay and cinematic cameras, it can tag only the outdoor camera that should receive day/night state.
Ambient Light
GlobalAmbientLight is optional runtime-owned output.
- With
GlobalAmbientConfig::apply = true(default), the crate writes the resolved ambient color and brightness intoGlobalAmbientLight. - With
GlobalAmbientConfig::apply = false, the crate still resolvesDayNightLighting, but leaves the app's ambient resource untouched.
Atmosphere And Fog
The crate does not implement a custom sky renderer. It integrates with Bevy's built-in atmospheric features:
AtmosphereScatteringMediumAtmosphereSettingsAtmosphereEnvironmentMapLightDistanceFogVolumetricFog
ScatteringMedium assets are cached in an internal resource so repeated camera updates do not churn handles.
Important boundary:
- the crate resolves outdoor lighting and fog hints
- Bevy's atmosphere renderer remains responsible for actual sky and aerial perspective shading
Smoothing And Write Thresholds
Resolved lighting is smoothed in resource space before touching render-facing components. This avoids visible pops during normal time progression without forcing expensive component rewrites every frame.
Threshold checks are applied before writing:
- light direction
- light color
- illuminance
- ambient brightness
- fog values
- exposure
This keeps the runtime cheap in long-running scenes and makes the diagnostics counters meaningful.
Shadow booleans are not smoothed. They switch to the target state immediately so low-angle or noon shadow state cannot get stuck behind an interpolation factor.
Phase Messages
The crate emits:
DawnStartedDayStartedDuskStartedNightStarted
Message behavior is intentionally one-directional:
- forward continuous motion and forward jumps emit crossed phase starts in chronological order
- paused frames emit nothing
- backward scrubs or backward jumps do not emit reverse transition messages
That keeps the message surface simple for gameplay consumers. If a game needs reverse-time semantics, it should interpret TimeOfDay and DayPhase directly instead of relying on inverse messages.
Day Counter Semantics
TimeOfDay::elapsed_days counts completed 24-hour simulation cycles from the current starting point.
Examples:
- start at
0.0, run to24.0worth of elapsed simulation time:elapsed_days += 1 - start at
18.0, run to clock-labeled00:30:elapsed_daysis still0 - start at
18.0, run a full simulated 24 hours:elapsed_days += 1
This makes the counter stable for “days elapsed since this session/start point” style gameplay. If a consumer needs a calendar day that increments on each clock midnight, it should track that separately from TimeOfDay.
Testing Strategy
The crate verifies the pure and Bevy boundaries separately:
- pure unit tests for time, phase, celestial math, gradients, lighting, and Kelvin conversion
- Bevy integration tests for plugin build, resource initialization, messages, and managed light behavior
- standalone examples for focused visual use cases
- crate-local lab scenarios for smoke, full cycle, fixed-time scrubbing, latitude-aware paths, camera hooks, time-reactive entities, weather modulation, phase messages, and write-stability/perf behavior