//! Serializable project settings loaded from `assets/project.ron`. mod plugin; mod rendering; pub use plugin::{ save_project_settings, sync_sim_tuning, ProjectSettingsChanged, ProjectSettingsIo, ProjectSettingsPlugin, SimTuning, }; pub use rendering::{ effective_auto_exposure, resolve_effective_render_stack, resolve_gi_path, write_effective_render_stack, ActiveCameraRenderProfile, ActiveVolumeContribution, EffectiveRenderStack, ExposureMode, GiMode, GiPath, RenderFallbackReason, RenderingCapabilities, VolumeContribution, }; use bevy::prelude::*; use serde::{Deserialize, Serialize}; pub const DEFAULT_PROJECT_PATH: &str = "assets/project.ron"; pub const SETTINGS_VERSION: u32 = 1; /// Marker for cameras that receive the shared project rendering stack. #[derive(Component, Debug, Clone, Copy, Default)] pub struct ProjectRenderCamera; /// Root project manifest and tunable sections. #[derive(Resource, Debug, Clone, Serialize, Deserialize, Reflect)] #[reflect(Resource, Clone)] pub struct ProjectSettings { pub version: u32, pub name: String, pub default_level: String, pub asset_roots: Vec, pub rendering: RenderingSettings, pub physics: PhysicsSettings, pub input: InputSettings, } impl Default for ProjectSettings { fn default() -> Self { Self { version: SETTINGS_VERSION, name: "Bevy FPS Foundation".into(), default_level: "assets/levels/editor_scene.scn.ron".into(), asset_roots: vec![ "assets/models".into(), "assets/textures".into(), "assets/materials".into(), "assets/levels".into(), ], rendering: RenderingSettings::default(), physics: PhysicsSettings::default(), input: InputSettings::default(), } } } /// Camera post-processing and world lighting defaults. #[derive(Debug, Clone, Serialize, Deserialize, Reflect)] #[reflect(Clone)] pub struct RenderingSettings { #[serde(default)] pub gi_mode: GiMode, pub hdr: bool, #[serde(default)] pub exposure_mode: ExposureMode, pub exposure_ev100: f32, #[serde(default = "default_auto_exposure_speed_brighten")] pub auto_exposure_speed_brighten: f32, #[serde(default = "default_auto_exposure_speed_darken")] pub auto_exposure_speed_darken: f32, pub tonemapping_aces: bool, pub bloom: bool, pub taa: bool, pub ssao: bool, pub atmosphere: bool, pub fog_color: [f32; 3], pub fog_density: f32, pub ambient_color: [f32; 3], pub ambient_brightness: f32, pub sun_illuminance: f32, pub shadow_cascades: u32, pub shadow_first_cascade: f32, pub shadow_max_distance: f32, } fn default_auto_exposure_speed_brighten() -> f32 { 3.0 } fn default_auto_exposure_speed_darken() -> f32 { 1.0 } impl Default for RenderingSettings { fn default() -> Self { Self { gi_mode: GiMode::default(), hdr: true, exposure_mode: ExposureMode::default(), exposure_ev100: 13.0, auto_exposure_speed_brighten: default_auto_exposure_speed_brighten(), auto_exposure_speed_darken: default_auto_exposure_speed_darken(), tonemapping_aces: true, bloom: true, taa: true, ssao: true, atmosphere: true, fog_color: [0.66, 0.75, 0.86], fog_density: 0.0012, ambient_color: [0.6, 0.7, 0.85], ambient_brightness: 60.0, sun_illuminance: 100_000.0, shadow_cascades: 4, shadow_first_cascade: 6.0, shadow_max_distance: 220.0, } } } /// Gameplay movement and capsule constants (mirrors former `sim::tuning`). #[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)] #[reflect(Clone)] pub struct PhysicsSettings { pub player_radius: f32, pub player_length: f32, pub eye_offset_stand: f32, pub eye_offset_crouch: f32, pub gravity: f32, pub walk_speed: f32, pub run_speed: f32, pub crouch_speed: f32, pub jump_speed: f32, pub ground_accel: f32, pub air_accel: f32, pub ground_check_dist: f32, pub skin: f32, pub max_slide_iter: u32, pub ground_normal_y: f32, pub coyote_time: f32, pub jump_buffer: f32, } impl Default for PhysicsSettings { fn default() -> Self { Self { player_radius: 0.4, player_length: 1.0, eye_offset_stand: 0.6, eye_offset_crouch: 0.1, gravity: 22.0, walk_speed: 6.0, run_speed: 9.5, crouch_speed: 3.0, jump_speed: 7.5, ground_accel: 70.0, air_accel: 16.0, ground_check_dist: 0.2, skin: 0.02, max_slide_iter: 4, ground_normal_y: 0.5, coyote_time: 0.12, jump_buffer: 0.12, } } } /// Input sensitivities and editor fly camera speeds. #[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)] #[reflect(Clone)] pub struct InputSettings { pub yaw_sensitivity: f32, pub pitch_sensitivity: f32, pub editor_fly_speed: f32, pub editor_look_sensitivity: f32, pub editor_pan_speed: f32, } impl Default for InputSettings { fn default() -> Self { Self { yaw_sensitivity: 0.003, pitch_sensitivity: 0.0022, editor_fly_speed: 14.0, editor_look_sensitivity: 0.003, editor_pan_speed: 0.015, } } } /// Loads settings from disk, falling back to defaults. pub fn load_project_settings_from_path(path: &str) -> ProjectSettings { match std::fs::read_to_string(path) { Ok(contents) => match ron::from_str(&contents) { Ok(settings) => settings, Err(error) => { warn!("Failed to parse {path}: {error}; using defaults"); ProjectSettings::default() } }, Err(error) => { warn!("Could not read {path}: {error}; using defaults"); ProjectSettings::default() } } } /// Serializes settings to RON. pub fn save_project_settings_to_string(settings: &ProjectSettings) -> Result { ron::ser::to_string_pretty(settings, ron::ser::PrettyConfig::default()) } #[cfg(test)] mod tests { use super::*; #[test] fn settings_round_trip() { let settings = ProjectSettings::default(); let text = save_project_settings_to_string(&settings).expect("serialize"); let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize"); assert_eq!(parsed.name, settings.name); assert_eq!(parsed.physics.walk_speed, settings.physics.walk_speed); assert_eq!(parsed.rendering.gi_mode, settings.rendering.gi_mode); } #[test] fn gi_mode_defaults_auto() { assert_eq!(RenderingSettings::default().gi_mode, GiMode::Auto); } #[test] fn exposure_mode_auto_round_trip() { let mut settings = ProjectSettings::default(); settings.rendering.exposure_mode = ExposureMode::Auto; settings.rendering.auto_exposure_speed_brighten = 4.0; settings.rendering.auto_exposure_speed_darken = 0.5; let text = save_project_settings_to_string(&settings).expect("serialize"); let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize"); assert_eq!(parsed.rendering.exposure_mode, ExposureMode::Auto); assert_eq!(parsed.rendering.auto_exposure_speed_brighten, 4.0); assert_eq!(parsed.rendering.auto_exposure_speed_darken, 0.5); } #[test] fn effective_auto_exposure_requires_hdr_and_compute() { let mut profile = ActiveCameraRenderProfile::default(); profile.exposure_mode = ExposureMode::Auto; profile.hdr = true; let caps = RenderingCapabilities { auto_exposure_supported: true, ..Default::default() }; assert!(effective_auto_exposure(&profile, &caps)); profile.hdr = false; assert!(!effective_auto_exposure(&profile, &caps)); profile.hdr = true; let caps = RenderingCapabilities::default(); assert!(!effective_auto_exposure(&profile, &caps)); } }