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