Skip to content

Commit 6814c94

Browse files
authored
feat(world-render): seasons v2 with day-night and seasonal models (#44)
- Ground textures and placeholder trees follow the start date: meadows turn through October, grounds and foliage go under snow from November to March - Track objects may ship optional `autumn_model` and `winter_model` glTF files with their own textures; missing variants fall back to `model` - Lit windows and glowing signs: any glTF node ending in `_NIGHT` switches at dusk (shown when daylight < 0.5), independent of model - Move `Daylight` resource to `world-render` where it's used by the night-node switcher - Season baked at load (not cross-faded), editor builds in summer, run season is scenario's date
1 parent c152730 commit 6814c94

10 files changed

Lines changed: 412 additions & 41 deletions

File tree

MODS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,23 @@ meant, so the editor's object tool drops it correctly with one click:
841841
)
842842
```
843843

844+
**Seasonal variants are optional.** An object may name an autumn or a winter glTF next to
845+
its year-round one; each brings its own textures, and whatever is left out falls back to
846+
`model`. A mast, a board or a lamp names neither and looks the same all year:
847+
848+
```ron
849+
(
850+
name: "Birke",
851+
model: "example/assets/birke.gltf",
852+
autumn_model: Some("example/assets/birke_herbst.gltf"),
853+
winter_model: Some("example/assets/birke_winter.gltf"),
854+
)
855+
```
856+
857+
Which one is spawned follows the scenario's start date (see *Seasons* below): the winter
858+
variant while snow lies, the autumn one while the leaves have turned, the year-round model
859+
otherwise. An object that ships no variant is never treated as a mistake.
860+
844861
A line places instances under `objects:`; each placement stores concrete values (stamped
845862
from the object's defaults, editable per instance), so the file stands on its own:
846863

@@ -868,6 +885,21 @@ blended toward rail height, so a snapped object next to the ballast still meets
868885
editor's selection panel has the checkbox; the app resolves the height, because only it
869886
has the elevation data.
870887

888+
**Lit windows at night.** A node whose name ends in **`_NIGHT`** is shown after dusk and
889+
hidden by day — lit windows in a house, a glowing sign, the light pool under a platform
890+
lamp. Nothing is declared in the RON: model a second window pane with an emissive
891+
material, call it `fenster_NIGHT`, and it switches like a signal's lamp node. The
892+
convention holds for every glTF the world is drawn from (scenery objects, trees, signal
893+
parts, vehicles), and a model without such a node simply never lights up. It is a hard
894+
switch at dusk, not a fade — the glow lives in your material, and it stays yours.
895+
896+
```
897+
Haus.gltf
898+
├── mauern always there
899+
├── fenster the dark pane by day
900+
└── fenster_NIGHT the emissive one, shown after dusk
901+
```
902+
871903
### Vegetation
872904

873905
Trees are ordinary track objects — an `objects/*.ron` with a tree glTF is all a tree mod
@@ -1109,6 +1141,18 @@ the georeferenced line and anchors `Daily` timetables; `Scenario` timetables and
11091141
triggers stay relative to the start of the run. Without the field, a run begins at
11101142
midsummer noon.
11111143

1144+
#### Seasons
1145+
1146+
The same date paints the **season**, and no mod has to do anything for it: the generated
1147+
ground textures and the built-in placeholder trees turn through October and go under snow
1148+
from November to March. On top of that a mod may ship **seasonal variants of its objects**
1149+
(`autumn_model` / `winter_model`, see *Track objects*) — each with its own textures inside
1150+
the glTF, each optional. A tree mod that ships only a summer birch is complete; one that
1151+
adds a winter birch gets it spawned while the snow lies. The season is fixed at load, so a
1152+
run that drives from October into November keeps the world it started in, and the route
1153+
editor always builds in summer — which season a run shows is the scenario's date, not the
1154+
module's.
1155+
11121156
### Line and scenario hooks `on_load(ctx)` / `on_frame(ctx)`
11131157

11141158
Both a `LineSource` and a `Scenario` may name a `script`. `on_load` runs once when the run

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,11 @@ gets its timetable by reference (`timetable: Some("<mod>:<name>")`, a `timetable
448448
in the mod) — without one, only the scenario's own points count. A timetable is either
449449
`kind: Scenario` (times from the start of the run, runs once) or `kind: Daily` (times as
450450
seconds since midnight, wrapping around every 24 h). `start:` sets date and local time
451-
of the run (default: midsummer noon) — it anchors `Daily` timetables and puts the sun
452-
and moon where they belong for the georeferenced line, season included.
451+
of the run (default: midsummer noon) — it anchors `Daily` timetables, puts the sun
452+
and moon where they belong for the georeferenced line, and paints the season: meadows
453+
turn through October, ground and trees go under snow from November to March. A mod's
454+
track object may bring its own `autumn_model`/`winter_model` glTF — optional, and
455+
whatever it leaves out keeps the year-round model.
453456
`SetWeather(Clear | Rain | Snow | Fog)` changes the weather: sky, visibility and
454457
precipitation in the renderer, plus the rail condition the weather implies (rain wets
455458
the rail, snow makes it slippery). `SetRail(Dry | Wet | Slippery)` sets the rail alone

STATUS.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ As of 2026-08-16 · `cargo test --workspace`: **383 tests green** · clippy and
1414
| **M3** | Sifa + PZB 90, signals, editor v1 | **done** — Sifa (time-time, time-distance, RZM) and every intermittent build from the Indusi I 54 to the PZB 90 V2.0 complete with standard-case tests; H/V + Ks signal logic present, and **signal models render the lamp images**: modular glTF assemblies on mount points (Zusi pattern), lamp nodes switched by the current lamp image, placeholder mast with an aspect light for signals without a model; the **route editor** edits the line over the aerial imagery (editor v3 — arc-to-point track drawing, device placement and per-device fields, switch placement that splits the track and wires the turnout facing or trailing with its throw time in the panel, the signal/section/route tables of the interlocking as forms, support-point dragging, rule checking, module boundaries with a ghost neighbour, delete with index remapping, undo/redo, save/open with discard guards); the **vehicle editor** edits base data, glTF model, LOD, moving parts, the 3D cab (eye point + interactive controls), the cab displays and the sound table; the **signal editor** assembles signal models (parts, mount points, lamp bindings, lamp test) |
1515
| **M4** | Interlocking, AI trains, timetable | **done** — routes with locking/release, automatic block, AI stops at signals and platforms |
1616
| **M5** | LZB 80 + AFB, MFA, tap-changer loco | **done** — LZB with guidance, braking curve, end and failure procedures, with and without PZB, full/partial block mode and CIR-ELKE; BR 110 present; **AFB** as vehicle equipment (`VehicleSpec::afb`): holds the dial speed with traction, dynamic brake and — where that does not suffice — the air brake, and under LZB guidance runs down the braking curve because the LZB's v-soll caps the dial; MFA values and lamps ship as indicators — HUD text, `gauge:`/`lamp:` instruments and render-to-texture displays in the 3D cab (see M6) |
17-
| **M6** | Interactive 3D cab, start-up procedure, audio, weather/night | **done** — interactive 3D cab: per-vehicle cab data (eye point + controls binding glTF nodes to a closed input registry incl. wipers, lights and display softkeys), mouse picking with drag/click/scroll gestures per control kind, hover glow, HUD readout, operating clicks via `Control(…)` sound quantities; instruments: gauges/lamps of the safety systems (`gauge:`/`lamp:` indicators, MFA pointers), `digit:` seven-segment counters, and **displays rendered to texture** (declarative widget lists in RON, a Lua `display(ctx)` hook with nested menus and clickable softkeys, or an HTML/CSS/JS page per screen — parsed, flex-laid-out and scripted in-engine by the `html-display` crate, no browser embedded); edited in the vehicle editor with viewport preview; start-up chain operable via keyboard and mouse; **weather rendering**: `Sim::weather` (clear/rain/snow/fog) set by the `SetWeather` scenario action — overcast sky, dimmed sun, distance fog from the weather's visibility, rain/snow particle fields around the camera (their streaks slanted by the relative wind of the player's speed), a `Rain` sound quantity for the sound table, and the implied rail condition on every train (`mods/example/scenarios/regenfahrt.ron` shows it); terrain from the DGM; **day/night cycle**: scenario start clock (date + time), sun and moon computed from the georeferenced location, lighting/sky follow the sun's elevation; **night lighting**: signal lamps glow (HDR + bloom on the main camera, emissive lenses), headlight cones at both train ends follow the light switch, the direction of travel and the darkness, red tail lamps (Zg 101) mark the opposite end, cab light on its own switch (`CabControl::Headlights`/`CabLight`, keys 9/0); **terrain texturing and vegetation** (ch. 14): texture splatting — per-vertex weights from slope and track distance blend three generated ground textures (grass/rock/gravel) in a `StandardMaterial` extension — and vegetation as **line content**: every tree its own `LineSource::trees` entry (3D objects from mods' `objects/*.ron`, placeholder for the unnamed), spawned as children of their terrain tile so they stream with it and batch into instanced draws; woods are baked into single trees by the editor, so each one stays individually editable; no recorded samples (the sources are generated — content, not code) |
17+
| **M6** | Interactive 3D cab, start-up procedure, audio, weather/night | **done** — interactive 3D cab: per-vehicle cab data (eye point + controls binding glTF nodes to a closed input registry incl. wipers, lights and display softkeys), mouse picking with drag/click/scroll gestures per control kind, hover glow, HUD readout, operating clicks via `Control(…)` sound quantities; instruments: gauges/lamps of the safety systems (`gauge:`/`lamp:` indicators, MFA pointers), `digit:` seven-segment counters, and **displays rendered to texture** (declarative widget lists in RON, a Lua `display(ctx)` hook with nested menus and clickable softkeys, or an HTML/CSS/JS page per screen — parsed, flex-laid-out and scripted in-engine by the `html-display` crate, no browser embedded); edited in the vehicle editor with viewport preview; start-up chain operable via keyboard and mouse; **weather rendering**: `Sim::weather` (clear/rain/snow/fog) set by the `SetWeather` scenario action — overcast sky, dimmed sun, distance fog from the weather's visibility, rain/snow particle fields around the camera (their streaks slanted by the relative wind of the player's speed), a `Rain` sound quantity for the sound table, and the implied rail condition on every train (`mods/example/scenarios/regenfahrt.ron` shows it); terrain from the DGM; **day/night cycle**: scenario start clock (date + time), sun and moon computed from the georeferenced location, lighting/sky follow the sun's elevation; **seasons** (ch. 14 "seasons v2"): the same start date colours ground textures and placeholder vegetation — meadows turn through October, ground, gravel and foliage go under snow from November to March — and a mod may add optional `autumn_model`/`winter_model` variants to its track objects, falling back to the year-round model where it ships none; **night lighting**: signal lamps glow (HDR + bloom on the main camera, emissive lenses), headlight cones at both train ends follow the light switch, the direction of travel and the darkness, red tail lamps (Zg 101) mark the opposite end, **mods' `_NIGHT` nodes** (lit windows, glowing signs) switch at dusk in every model, cab light on its own switch (`CabControl::Headlights`/`CabLight`, keys 9/0); **terrain texturing and vegetation** (ch. 14): texture splatting — per-vertex weights from slope and track distance blend three generated ground textures (grass/rock/gravel) in a `StandardMaterial` extension — and vegetation as **line content**: every tree its own `LineSource::trees` entry (3D objects from mods' `objects/*.ron`, placeholder for the unnamed), spawned as children of their terrain tile so they stream with it and batch into instanced draws; woods are baked into single trees by the editor, so each one stays individually editable; no recorded samples (the sources are generated — content, not code) |
1818
| **M7** | Pilot line from OSM/DGM, scenarios, scoring, save/load | **largely done** — scenario system, scoring, save/load and the OSM/DGM importer are in place; only a real pilot line is missing (data procurement) |
1919
| **M8** | Mod runtime: declarative content plus Lua behaviour | **done** — loader with dependency order, vehicles/lines/compositions/scenarios/timetables/signal types/signal models/track types/track objects as RON, signal state machine as data, four Lua hooks (vehicle, signal aspect, line, scenario) with a sandbox, mod manager on the main menu (a toggle applies on start) and under F9; reference mod under `mods/example` incl. a glTF model. Only distribution (`.crails` zip + installer) is still open |
2020

@@ -602,8 +602,27 @@ Every simplification is marked with a `ponytail:` comment at the code site, with
602602
noon) via `Sim::clock()`. It anchors `Daily` timetables (`Timetable::delay`/
603603
`next_occurrence` take the start-of-day offset) and drives sun and moon: positions
604604
from date, time and the georeferenced location (`world_coords::sun`, low-precision
605-
astronomy), so the season falls out of the date. Seasonal *appearance* (vegetation,
606-
snow) remains content/texturing work (ch. 14 "seasons v2").
605+
astronomy), so the season falls out of the date. **The seasonal appearance hangs off
606+
the same date** (`world_render::Season`, ch. 14 "seasons v2"): the generated ground
607+
textures and the placeholder trees are built in the colours of the start day — meadows
608+
turn through October, ground, gravel and foliage go under snow from November to March,
609+
a rock face keeps showing through. A mod may follow along without having to:
610+
`autumn_model`/`winter_model` on a track object name a glTF with its own textures for
611+
that season, each optional, missing ones falling back to the year-round `model` — a
612+
mast names neither, a birch may name both. Two ceilings: the curve is a central
613+
European lowland year (a line in the Alps or south of the equator wants a per-line
614+
climate entry, not a finer curve), and the season is **baked in at load** (a run from
615+
October into November keeps the ground it started on, and a variant either shows or it
616+
does not — cross-fading two glTFs would draw every tree twice). The route editor
617+
builds in summer; which season a run shows is the scenario's date.
618+
- **Night furniture is a node name**: a glTF node ending in `_NIGHT`
619+
(`world_render::NIGHT_SUFFIX`) is shown below half daylight and hidden above it —
620+
lit windows, glowing signs, the light pool under a lamp, in every model the world is
621+
drawn from. Like the `_LOD<level>` convention it needs no entry in any RON, and the
622+
lit look stays in the mod's own emissive material. Two ceilings: it is a hard switch
623+
at dusk, not a fade (fading would mean patching every loaded glTF material per frame),
624+
and there is one threshold for the whole world — a shop that closes earlier than the
625+
street lamp wants a per-node schedule, which is a scenario question, not a render one.
607626

608627
## Sensible next steps
609628

crates/app/src/main.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ use sim_core::Sim;
3232
use sim_core::train::{Train, Vehicle, VehicleSpec, Weather};
3333
use track_model::{EdgeId, TrackPosition};
3434
use world_coords::{RenderOrigin, sun};
35+
// Daylight factor of this frame, 0 (night) … 1 (full day) — written by
36+
// `update_daylight`, read by everything that switches with darkness: the
37+
// headlights here, the mods' `_NIGHT` nodes in `world-render`.
38+
use world_render::Daylight;
3539

3640
/// Menu first, the world only on starting the run — that is what lets a mod toggled on
3741
/// the menu apply without restarting the process.
@@ -46,11 +50,6 @@ pub enum GameState {
4650
#[derive(Resource)]
4751
pub struct SimResource(pub Sim);
4852

49-
/// Daylight factor of this frame, 0 (night) … 1 (full day) — written by
50-
/// `update_daylight`, read by everything that switches with darkness.
51-
#[derive(Resource, Default)]
52-
pub struct Daylight(pub f32);
53-
5453
/// Headlight cone at one end of a train (M6 night lighting). `reverse` marks the
5554
/// cone on the rear end; `update_headlights` lights the one facing the direction
5655
/// of travel while the cab's light switch is on.
@@ -188,7 +187,6 @@ fn main() {
188187
require_markers: true,
189188
..default()
190189
})
191-
.init_resource::<Daylight>()
192190
.init_resource::<ui::CameraState>()
193191
.init_resource::<cab::CabMouse>()
194192
.init_resource::<mods_ui::ModManager>()
@@ -471,6 +469,10 @@ fn setup(
471469
let start = sim.trains[player].vehicles[0].pos.pose(&sim.net).pos;
472470
let origin = RenderOrigin::new(start);
473471

472+
// Ground, scenery and foliage wear the season of the scenario's start date
473+
// — the same date the sun and moon are computed from (plan ch. 14).
474+
let season = render::Season::on(sim.start.month, sim.start.day);
475+
474476
// Terrain: from real elevation data with `--dgm <directory>`, otherwise flat.
475477
// Tiles are not built here but while driving (plan 4.3) — a 100 km line has more
476478
// terrain than fits in memory at once. The builder exists before the scenery,
@@ -543,6 +545,7 @@ fn setup(
543545
&origin,
544546
&mods.mods.objects,
545547
Some(&mut terrain_builder),
548+
season,
546549
);
547550

548551
// Signal models (plan ch. 15.3): the placement's override, otherwise the signal
@@ -592,10 +595,11 @@ fn setup(
592595
&assets,
593596
&mut meshes,
594597
&mut materials,
598+
season,
595599
);
596600
let streamer = streaming::TerrainStreamer::new(
597601
terrain_builder,
598-
render::terrain_material(&mut images, &mut terrain_materials),
602+
render::terrain_material(&mut images, &mut terrain_materials, season),
599603
tree_catalog,
600604
LOAD_RADIUS,
601605
);

crates/app/src/render.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
use bevy::prelude::*;
55
use world_coords::{EnuFrame, RenderOrigin};
66
pub use world_render::{
7-
TerrainMaterial, TreeCatalog, WorldAnchored, spawn_terrain_tile, spawn_track, terrain_material,
8-
tree_catalog,
7+
Season, TerrainMaterial, TreeCatalog, WorldAnchored, spawn_terrain_tile, spawn_track,
8+
terrain_material, tree_catalog,
99
};
1010

1111
/// Reference point of the rendering as a Bevy resource.

crates/content/src/route.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,6 +2398,8 @@ mod tests {
23982398
lateral_offset: -3.5,
23992399
yaw_deg: 0.0,
24002400
height: 0.0,
2401+
autumn_model: None,
2402+
winter_model: None,
24012403
},
24022404
);
24032405
assert!(line.check(&types, &objects).is_empty());

crates/route-editor/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,17 @@ fn setup(
452452
commands.init_resource::<signals::LampImages>();
453453
commands.init_resource::<world_render::SignalModels>();
454454
// Terrain material and the (still empty) tree catalog — the catalog is
455-
// filled from the line on the first frame.
455+
// filled from the line on the first frame. The editor builds in summer
456+
// (`Season::default`); which season a run shows is the scenario's date.
456457
commands.insert_resource(terrain::TerrainView::new(
457-
world_render::terrain_material(&mut images, &mut terrain_materials),
458+
world_render::terrain_material(&mut images, &mut terrain_materials, default()),
458459
world_render::tree_catalog(
459460
&[],
460461
&Default::default(),
461462
&assets,
462463
&mut meshes,
463464
&mut materials,
465+
default(),
464466
),
465467
));
466468
commands.insert_resource(History::new(source.clone()));
@@ -605,6 +607,7 @@ fn rebuild(
605607
&origin.0,
606608
&objects.map,
607609
ground.as_deref_mut(),
610+
default(),
608611
);
609612
}
610613

crates/route-editor/src/terrain.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,14 @@ pub fn update(
139139
.builder_lock()
140140
.map(|b| b.tree_objects().to_vec())
141141
.unwrap_or_default();
142-
view.trees =
143-
world_render::tree_catalog(&names, &objects.map, &assets, &mut meshes, &mut materials);
142+
view.trees = world_render::tree_catalog(
143+
&names,
144+
&objects.map,
145+
&assets,
146+
&mut meshes,
147+
&mut materials,
148+
default(),
149+
);
144150
view.generation += 1;
145151
view.empty.clear();
146152
// A module that carries its ground shows it; one without would only

0 commit comments

Comments
 (0)