Skip to main content

pccm/
pccm.rs

1//! Demonstrates parallax-corrected cubemap reflections.
2
3use core::f32;
4
5use bevy::{
6    camera::Hdr,
7    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
8    feathers::{theme::UiTheme, FeathersPlugins},
9    light::ParallaxCorrection,
10    prelude::*,
11    ui_widgets::{radio_self_update, ValueChange},
12};
13
14#[path = "../helpers/radio.rs"]
15mod radio;
16
17#[path = "../helpers/theme.rs"]
18mod theme;
19
20use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
21use crate::theme::basic_example_theme;
22
23/// A marker component for the inner rotating reflective cube.
24#[derive(Clone, Component)]
25struct InnerCube;
26
27/// The brightness of the cubemap.
28///
29/// Since the cubemap image was baked in Blender, which uses a different
30/// exposure setting than that of Bevy, we need this factor in order to make the
31/// exposure of the baked image match ours.
32const ENVIRONMENT_MAP_INTENSITY: f32 = 100.0;
33
34const OUTER_CUBE_URL: &str =
35    "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/main/pccm_example/outer_cube.glb#Scene0";
36const ENV_DIFFUSE_URL: &str =
37    "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/main/pccm_example/env_diffuse.ktx2";
38const ENV_SPECULAR_URL: &str =
39    "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/main/pccm_example/env_specular.ktx2";
40
41/// The current value of user-customizable settings for this demo.
42#[derive(Resource, Default)]
43struct AppStatus {
44    /// Whether parallax correction is enabled.
45    pccm_enabled: PccmEnableStatus,
46}
47
48/// Whether parallax correction is enabled.
49#[derive(Clone, Copy, PartialEq, Default)]
50enum PccmEnableStatus {
51    /// Parallax correction is enabled.
52    #[default]
53    Enabled,
54    /// Parallax correction is disabled.
55    Disabled,
56}
57
58/// The example entry point.
59fn main() {
60    App::new()
61        .insert_resource(UiTheme(basic_example_theme(Color::WHITE)))
62        .add_plugins((
63            DefaultPlugins.set(WindowPlugin {
64                primary_window: Some(Window {
65                    title: "Bevy Parallax-Corrected Cubemaps Example".into(),
66                    ..default()
67                }),
68                ..default()
69            }),
70            FreeCameraPlugin,
71            FeathersPlugins,
72        ))
73        .init_resource::<AppStatus>()
74        .add_systems(Startup, setup)
75        .add_observer(handle_pccm_enable_change)
76        .add_observer(radio_self_update)
77        .run();
78}
79
80/// Creates the initial scene.
81fn setup(
82    mut commands: Commands,
83    asset_server: Res<AssetServer>,
84    mut meshes: ResMut<Assets<Mesh>>,
85    mut materials: ResMut<Assets<StandardMaterial>>,
86) {
87    // Spawn the glTF scene.
88    commands.spawn(WorldAssetRoot(asset_server.load(OUTER_CUBE_URL)));
89
90    spawn_camera(&mut commands);
91    spawn_inner_cube(&mut commands, &mut meshes, &mut materials);
92    spawn_reflection_probe(&mut commands, &asset_server);
93    spawn_buttons(&mut commands);
94}
95
96/// Spawns the camera.
97fn spawn_camera(commands: &mut Commands) {
98    commands.spawn((
99        Camera3d::default(),
100        FreeCamera::default(),
101        Transform::from_xyz(0.0, 0.0, 4.0).looking_at(Vec3::new(0.0, -2.5, 0.0), Dir3::Y),
102        Hdr,
103    ));
104}
105
106/// Spawns the inner reflective cube in the scene.
107fn spawn_inner_cube(
108    commands: &mut Commands,
109    meshes: &mut Assets<Mesh>,
110    materials: &mut Assets<StandardMaterial>,
111) {
112    let cube_mesh = meshes.add(
113        Cuboid {
114            half_size: Vec3::new(5.0, 1.0, 2.0),
115        }
116        .mesh()
117        .build()
118        .with_duplicated_vertices()
119        .with_computed_flat_normals(),
120    );
121    let cube_material = materials.add(StandardMaterial {
122        base_color: Color::WHITE,
123        metallic: 1.0,
124        reflectance: 1.0,
125        perceptual_roughness: 0.0,
126        ..default()
127    });
128
129    commands.spawn((
130        Mesh3d(cube_mesh),
131        MeshMaterial3d(cube_material),
132        Transform::from_xyz(0.0, -4.0, -2.5),
133        InnerCube,
134    ));
135}
136
137/// Spawns the reflection probe (i.e. cubemap reflection) in the center of the scene.
138fn spawn_reflection_probe(commands: &mut Commands, asset_server: &AssetServer) {
139    let diffuse_map = asset_server.load(ENV_DIFFUSE_URL);
140    let specular_map = asset_server.load(ENV_SPECULAR_URL);
141    commands.spawn((
142        LightProbe::default(),
143        EnvironmentMapLight {
144            diffuse_map,
145            specular_map,
146            intensity: ENVIRONMENT_MAP_INTENSITY,
147            ..default()
148        },
149        // HACK: slightly larger than 10.0 to avoid z-fighting from the outer cube
150        // faces being partially inside and partially outside the light probe influence
151        // volume. We should have a smooth falloff probe transition option at some point.
152        Transform::from_scale(Vec3::splat(10.01)),
153    ));
154}
155
156/// Spawns the buttons at the bottom of the screen.
157fn spawn_buttons(commands: &mut Commands) {
158    commands.spawn_scene(bsn! {
159        @main_ui_node_scene()
160        Children [
161            @feathers_option_buttons(
162                "Parallax Correction",
163                &[
164                    (PccmEnableStatus::Enabled, "On"),
165                    (PccmEnableStatus::Disabled, "Off"),
166                ],
167                0,
168            )
169        ]
170    });
171}
172
173/// Handles a change to the parallax correction setting UI.
174fn handle_pccm_enable_change(
175    event: On<ValueChange<Entity>>,
176    new_value_query: Query<&RadioButtonOptionValue<PccmEnableStatus>>,
177    mut commands: Commands,
178    light_probe_query: Query<Entity, With<LightProbe>>,
179    mut app_status: ResMut<AppStatus>,
180) {
181    let Ok(RadioButtonOptionValue(pccm_enabled)) = new_value_query.get(event.value) else {
182        return;
183    };
184    app_status.pccm_enabled = *pccm_enabled;
185
186    let Some(light_probe_entity) = light_probe_query.iter().next() else {
187        return;
188    };
189    // Add the appropriate variant of the `ParallaxCorrection` component.
190    match app_status.pccm_enabled {
191        PccmEnableStatus::Enabled => {
192            commands
193                .entity(light_probe_entity)
194                .insert(ParallaxCorrection::Auto);
195        }
196        PccmEnableStatus::Disabled => {
197            commands
198                .entity(light_probe_entity)
199                .insert(ParallaxCorrection::None);
200        }
201    }
202}