//! Demonstrates blending between multiple reflection probes. //! //! This example shows a reflective sphere that moves between two rooms, each of //! which contains a reflection probe with a falloff range. Bevy performs a //! blend between the two reflection probes as the sphere moves. use std::f32::consts::{FRAC_PI_4, PI}; use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue}; use crate::theme::basic_example_theme; use bevy::{ camera::Hdr, camera_controller::free_camera::{FreeCamera, FreeCameraPlugin}, color::palettes::css::{CORNFLOWER_BLUE, CRIMSON, TAN, WHITE}, feathers::{theme::UiTheme, FeathersPlugins}, input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll}, light::ParallaxCorrection, math::ops::{atan2, cos, sin}, prelude::*, ui_widgets::{radio_self_update, ValueChange}, window::{CursorGrabMode, CursorOptions}, }; #[path = "../helpers/radio.rs"] mod radio; #[path = "../helpers/theme.rs"] mod theme; /// The settings that the user has chosen. #[derive(Resource, Default)] struct AppStatus { /// Whether the gizmos that show the boundaries of the light probe regions /// are to be shown. gizmos_enabled: GizmosEnabled, /// Which object to show: either a reflective sphere or a reflective prism. object_to_show: ObjectToShow, /// Whether to use an orbital pan/zoom camera or a free camera. camera_mode: CameraMode, } /// Whether the gizmos that show the boundaries of the light probe regions are /// to be shown. #[derive(Clone, Copy, Default, PartialEq)] enum GizmosEnabled { /// The gizmos are shown. #[default] On, /// The gizmos are hidden. Off, } /// Which reflective object to show. #[derive(Clone, Copy, Default, PartialEq)] enum ObjectToShow { /// A reflective sphere that moves between rooms. #[default] Sphere, /// A reflective prism that is static and stretches across the length of the /// two rooms. Prism, } /// How the user can control the camera. #[derive(Clone, Copy, Default, PartialEq)] enum CameraMode { /// The camera is a pan/zoom orbital camera controllable with dragging and /// the mouse wheel. #[default] Orbit, /// The camera is a free camera controllable by clicking and dragging and /// using the WASDEQ controls. Free, } /// A marker component for the reflective sphere. #[derive(Clone, Copy, Component, Debug)] struct ReflectiveSphere; /// A marker component for the reflective prism. #[derive(Clone, Copy, Component, Debug)] struct ReflectivePrism; /// A marker component for the help text at the top of the screen. #[derive(Clone, Copy, Component, Debug)] struct HelpText; /// The speed at which the sphere moves, as a ratio of the total distance it /// travels to seconds. /// /// Specifically, the value of 0.3 means that it moves 3/10 of the way to the /// other side per second. const SPHERE_MOVEMENT_SPEED: f32 = 0.3; /// The side length of each room, in meters. const ROOM_SIDE_LENGTH: f32 = 10.0; /// The number of meters that separates the center of each room. const ROOM_SEPARATION: f32 = 11.0; /// The side length of the light probe cube, in meters. const LIGHT_PROBE_SIDE_LENGTH: f32 = 15.0; /// The distance over which the light probe fades out, expressed as a fraction /// of the side length of the probe. const LIGHT_PROBE_FALLOFF: f32 = 0.5; /// The side length of the simulated reflected area for each light probe, /// specified as a half-extent in light probe space. /// /// We want this side length, in world space, to be half of the world-space room /// side length. Since the light probe is scaled by `LIGHT_PROBE_SIDE_LENGTH`, /// we divide the room side length by the light probe side length to get this /// value, and multiply by 0.5 to convert from a full extent to a half-extent. /// That way, when Bevy applies the `LIGHT_PROBE_SIDE_LENGTH` scale, the light /// probe side length factor cancels, and we're left with a parallax correction /// side length of `ROOM_SIDE_LENGTH` in world space. /// /// A small epsilon value of 0.01 is added in order to ensure that the light /// probe parallax bounds encompass the entire room. Otherwise, unsightly /// Z-fighting can occur on the room walls. const LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH: f32 = ROOM_SIDE_LENGTH / LIGHT_PROBE_SIDE_LENGTH * 0.5 + 0.01; /// The number of radians of inclination (pitch) that one pixel of mouse /// movement corresponds to. const CAMERA_ORBIT_SPEED_INCLINATION: f32 = 0.003; /// The number of radians of azumith (yaw) that one pixel of mouse movement /// corresponds to. const CAMERA_ORBIT_SPEED_AZIMUTH: f32 = 0.004; /// The number of meters that one line of mouse scroll corresponds to. const CAMERA_ZOOM_SPEED: f32 = 0.15; /// Information about the orbital pan/zoom camera. /// /// These are in [spherical coordinates]. /// /// [spherical coordinates]: https://en.wikipedia.org/wiki/Spherical_coordinate_system #[derive(Component)] struct OrbitCamera { /// The distance between the camera and the sphere, in meters. radius: f32, /// The camera latitude in radians, relative to the sphere. inclination: f32, /// The camera longitude in radians, relative to the sphere. azimuth: f32, } /// The brightness of the light probe. const LIGHT_PROBE_INTENSITY: f32 = 500.0; /// The entry point. fn main() { App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Bevy Light Probe Blending Example".into(), ..default() }), ..default() }), FeathersPlugins, )) .add_plugins(FreeCameraPlugin) .insert_resource(UiTheme(basic_example_theme(Color::WHITE))) .init_resource::() .add_systems(Startup, setup) .add_systems(Update, (move_sphere, orbit_camera).chain()) .add_observer(handle_gizmos_enabled_change) .add_observer(handle_object_to_show_change) .add_observer(handle_camera_mode_change) .add_observer(radio_self_update) .add_systems(Update, draw_gizmos) .run(); } /// Performs initial setup of the scene. fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, mut gizmo_config_store: ResMut, ) { adjust_gizmo_settings(&mut gizmo_config_store); let reflective_material = create_reflective_material(&mut materials); spawn_camera(&mut commands); spawn_gltf_scene(&mut commands, &asset_server); spawn_reflective_sphere(&mut commands, &mut meshes, reflective_material.clone()); spawn_reflective_prism(&mut commands, &mut meshes, reflective_material); spawn_light_probes(&mut commands, &asset_server); spawn_buttons(&mut commands); spawn_help_text(&mut commands); } /// Adjusts the gizmo settings so that the gizmos appear on top of all other /// geometry. /// /// If we didn't do this, then the rooms would cover up many of the gizmos. fn adjust_gizmo_settings(gizmo_config_store: &mut GizmoConfigStore) { for (_, gizmo_config, _) in &mut gizmo_config_store.iter_mut() { gizmo_config.depth_bias = -1.0; } } /// Creates the perfectly-reflective material that the sphere and prism use. fn create_reflective_material( materials: &mut Assets, ) -> Handle { materials.add(StandardMaterial { base_color: WHITE.into(), metallic: 1.0, reflectance: 1.0, perceptual_roughness: 0.0, ..default() }) } /// Spawns the orbital pan/zoom camera. fn spawn_camera(commands: &mut Commands) { commands.spawn(( Camera3d::default(), Transform::IDENTITY, Hdr, OrbitCamera { radius: 3.0, inclination: 7.0 * FRAC_PI_4, azimuth: FRAC_PI_4, }, )); } /// Spawns the glTF scene that contains the two rooms. fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) { commands.spawn(WorldAssetRoot(asset_server.load( GltfAssetLabel::Scene(0).from_asset(get_web_asset_url("two_rooms.glb")), ))); } /// Spawns the reflective sphere, creating its mesh in the process. fn spawn_reflective_sphere( commands: &mut Commands, meshes: &mut Assets, material: Handle, ) { // Create a mesh. let sphere = meshes.add(Sphere::default().mesh().uv(32, 18)); // Spawn the sphere. commands.spawn(( Mesh3d(sphere), MeshMaterial3d(material), Transform::IDENTITY, ReflectiveSphere, )); } /// Spawns the reflective prism, creating its mesh in the process. /// /// The reflective prism starts invisible, but the user can toggle it on and off /// as desired. fn spawn_reflective_prism( commands: &mut Commands, meshes: &mut Assets, material: Handle, ) { // Create a mesh. let cube = meshes.add( Cuboid { half_size: vec3(2.0, 1.0, 10.0), } .mesh() .build() // We use flat normals so that the surface appears flat, not curved. .with_duplicated_vertices() .with_computed_flat_normals(), ); // Spawn the cube. commands.spawn(( Mesh3d(cube), MeshMaterial3d(material), Transform::from_xyz(0.0, -4.0, -5.5), ReflectivePrism, Visibility::Hidden, )); } /// Spawns the two light probes, one for each room. fn spawn_light_probes(commands: &mut Commands, asset_server: &AssetServer) { // Spawn the first room's light probe. commands.spawn(( LightProbe { falloff: Vec3::splat(LIGHT_PROBE_FALLOFF), }, EnvironmentMapLight { diffuse_map: asset_server.load(get_web_asset_url("diffuse_room1.ktx2")), specular_map: asset_server.load(get_web_asset_url("specular_room1.ktx2")), intensity: LIGHT_PROBE_INTENSITY, ..default() }, Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH) .with_rotation(Quat::from_rotation_x(PI)), ParallaxCorrection::Custom(Vec3::splat(LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH)), )); // Spawn the second room's light probe. commands.spawn(( LightProbe { falloff: Vec3::splat(LIGHT_PROBE_FALLOFF), }, EnvironmentMapLight { diffuse_map: asset_server.load(get_web_asset_url("diffuse_room2.ktx2")), specular_map: asset_server.load(get_web_asset_url("specular_room2.ktx2")), intensity: LIGHT_PROBE_INTENSITY, ..default() }, Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH) .with_rotation(Quat::from_rotation_x(PI)) .with_translation(vec3(0.0, 0.0, -ROOM_SEPARATION)), ParallaxCorrection::Custom(Vec3::splat(LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH)), )); } /// Spawns the radio buttons at the bottom of the screen. fn spawn_buttons(commands: &mut Commands) { commands.spawn_scene(bsn! { @main_ui_node_scene() Children [ @feathers_option_buttons( "Gizmos", &[(GizmosEnabled::On, "On"), (GizmosEnabled::Off, "Off"),], 0, ) -- @feathers_option_buttons( "Object to Show", &[ (ObjectToShow::Sphere, "Sphere"), (ObjectToShow::Prism, "Prism"), ], 0, ) -- @feathers_option_buttons( "Camera Mode", &[(CameraMode::Orbit, "Orbit"), (CameraMode::Free, "Free"),], 0, ) ] }); } /// Spawns the help text at the top of the screen. fn spawn_help_text(commands: &mut Commands) { commands.spawn(( Text::new(""), Node { position_type: PositionType::Absolute, top: px(12), left: px(12), ..default() }, HelpText, )); } /// Moves the sphere a bit every frame. fn move_sphere(mut spheres: Query<&mut Transform, With>, time: Res