From e6047b7073b1d8fd736a9652897f7fe271a2563b Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 23:40:19 +0100 Subject: [PATCH] Light custom map scenes using authored environment and native provenance --- crates/skate-game/src/app.rs | 2 +- crates/skate-game/src/map_render.rs | 71 +++++++++++++++++++++++-- crates/skate-game/src/map_transition.rs | 2 +- crates/skate-game/src/retail_render.rs | 11 ++++ 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/crates/skate-game/src/app.rs b/crates/skate-game/src/app.rs index edae5339..882fd81b 100644 --- a/crates/skate-game/src/app.rs +++ b/crates/skate-game/src/app.rs @@ -37,7 +37,7 @@ pub(crate) fn build( physics: GamePhysics, skater: SkaterRuntime, ) -> App { - let retail_scene = config.map.as_ref().is_some_and(|map| map.materials.iter().any(|m| m.retail_definition.is_some())); + let retail_scene = config.map.as_ref().is_some_and(|map| crate::retail_render::RetailScene::for_map(map)); let mut app = App::new(); crate::custom_models::register_source(&mut app); app.add_plugins( diff --git a/crates/skate-game/src/map_render.rs b/crates/skate-game/src/map_render.rs index 84fd2487..5a0ee8d2 100644 --- a/crates/skate-game/src/map_render.rs +++ b/crates/skate-game/src/map_render.rs @@ -96,9 +96,13 @@ impl PreparedScene { if let Some(map) = map { crate::skate_world::spawn(map, &mut self.commands, &mut self.meshes, &mut self.materials, &mut self.retail, &mut self.images, &crate::retail_render::MaterialTuning::load(root)); - crate::retail_render::spawn_backdrop(&map.name, root, &mut self.commands, &mut self.meshes, &mut self.materials, &mut self.retail, &mut self.images); - crate::retail_render::spawn_sky(&map.name, root, &mut self.commands, - &mut self.meshes, &mut self.images, &mut self.sky, &mut self.retail); + if crate::retail_render::RetailScene::for_map(map) { + crate::retail_render::spawn_backdrop(&map.name, root, &mut self.commands, &mut self.meshes, &mut self.materials, &mut self.retail, &mut self.images); + crate::retail_render::spawn_sky(&map.name, root, &mut self.commands, + &mut self.meshes, &mut self.images, &mut self.sky, &mut self.retail); + } else { + custom_lighting(map, &mut self.commands); + } } else { crate::world::spawn_test_world(&mut self.commands, &mut self.meshes, &mut self.materials); } @@ -122,6 +126,41 @@ pub(crate) struct MapAssets { sky: Vec>, images: Vec>, } + +/// Fixed authored start hour; dynamic refers to real-time surface lighting and +/// shadows, not a new day/night simulation. All lights are scene-owned. +fn custom_lighting(map: &skate_data::skate_map::SkateMap, commands: &mut SceneCommands) { + let e = &map.environment; + let hour = e[10].rem_euclid(24.); + let elevation = ((hour - 6.) * std::f32::consts::PI / 12.).sin(); + let daylight = elevation.max(0.); + let azimuth = e[11]; + let extended = e.len() == 45; + let rgb = |i: usize| Color::linear_rgb(e[i].max(0.), e[i + 1].max(0.), e[i + 2].max(0.)); + let night = elevation < 0.; + let color = if extended { rgb(if night { 35 } else { 32 }) } else { Color::WHITE }; + let strength = if extended { e[if night { 39 } else { 38 }].max(0.) } else { 1. }; + let ambient = if extended { e[41] + (e[40] - e[41]) * daylight } else { 0.32 }; + commands.insert_resource(GlobalAmbientLight { + color: Color::WHITE, brightness: ambient.max(0.) * 1000., ..default() + }); + let horizontal = (1. - elevation * elevation).max(0.).sqrt(); + let direction = Vec3::new(azimuth.cos() * horizontal, elevation.abs().max(0.001), azimuth.sin() * horizontal).normalize(); + commands.spawn(( + Name::new("Custom map sun/moon"), + DirectionalLight { + color, illuminance: 10_000. * strength * elevation.abs(), + shadows_enabled: true, + // Preserve authored indirect/baked diffuse on custom lightmapped meshes. + affects_lightmapped_mesh_diffuse: false, + ..default() + }, + Transform::default().looking_to(-direction, Vec3::Y), + bevy::light::CascadeShadowConfigBuilder { + maximum_distance: 100., first_cascade_far_bound: 10., ..default() + }.build(), + )); +} impl MapAssets { pub fn retire(world: &mut World) { let entities: Vec<_> = world.query_filtered::>().iter(world).collect(); @@ -143,6 +182,32 @@ impl MapAssets { #[cfg(test)] mod tests { use super::*; + #[test] + fn custom_lighting_switches_without_accumulation_and_keeps_pbr() { + let mut world = world(); + let mut map = skate_data::skate_map::SkateMap::parse(include_bytes!("../../../maps/format-demo.skate")).unwrap(); + map.name = "university".into(); // Names cannot select retail rendering. + map.materials[0].retail_definition = None; + map.extensions.clear(); + for retail in [false, true, false, true] { + map.extensions.clear(); + if retail { + // Legacy retail imports can retain provenance without definitions. + map.extensions.push(skate_data::skate_map::Extension { + tag: *b"WMET", schema: 1, payload: b"{}".to_vec(), + }); + } + assert_eq!(crate::retail_render::RetailScene::for_map(&map), retail); + let mut scene = PreparedScene::new(&world); + scene.prepare(Some(&map), std::path::Path::new("unused")); + scene.publish(&mut world); + assert_eq!(world.query::<&DirectionalLight>().iter(&world).count(), usize::from(!retail)); + assert!(world.resource::>().iter().all(|(_, m)| !m.unlit)); + MapAssets::retire(&mut world); + assert_eq!(world.query::<&DirectionalLight>().iter(&world).count(), 0); + } + } + fn world() -> World { let mut world = World::new(); world.init_resource::>(); diff --git a/crates/skate-game/src/map_transition.rs b/crates/skate-game/src/map_transition.rs index 84975a3e..8cc5e50e 100644 --- a/crates/skate-game/src/map_transition.rs +++ b/crates/skate-game/src/map_transition.rs @@ -110,7 +110,7 @@ fn start(world: &World, entry: Entry) -> Result { if let Some(map) = &map { crate::skate_world::validate_runtime(map)?; } let validation_time = validation_started.elapsed(); let metadata = CurrentMap::from_package(selected.path, map.as_ref()); - let retail = map.as_ref().is_some_and(|m| m.materials.iter().any(|m| m.retail_definition.is_some())); + let retail = map.as_ref().is_some_and(|m| crate::retail_render::RetailScene::for_map(m)); // Both builders only read the decoded package. Reserve render handles // on a second worker while the first constructs fresh simulation state; // neither publishes to the live world until both have succeeded. diff --git a/crates/skate-game/src/retail_render.rs b/crates/skate-game/src/retail_render.rs index 71f28f9e..66a5266b 100644 --- a/crates/skate-game/src/retail_render.rs +++ b/crates/skate-game/src/retail_render.rs @@ -76,6 +76,17 @@ pub(crate) use shadow::ShadowState; #[derive(Resource)] pub(crate) struct RetailScene(pub bool); +impl RetailScene { + /// Package evidence, never the editable map name. Older Skate 2 exports + /// can retain native provenance/sky/collision without material definitions. + pub(crate) fn for_map(map: &skate_data::skate_map::SkateMap) -> bool { + map.materials.iter().any(|m| m.retail_definition.is_some()) + || map.extensions.iter().any(|e| matches!(&e.tag, b"WMET" | b"RWCM" | b"SKYB")) + || map.geometry.collision.iter().any(|c| c.native_edges.is_some()) + || map.rails.iter().any(|r| r.native.is_some()) + } +} + #[path = "retail_sky.rs"] mod sky; pub(crate) use sky::{spawn_sky, RetailSkyMaterial};