parallax_mapping/
parallax_mapping.rs

1//! A simple 3D scene with a spinning cube with a normal map and depth map to demonstrate parallax mapping.
2//! Press left mouse button to cycle through different views.
3
4use std::fmt;
5
6use bevy::{image::ImageLoaderSettings, math::ops, prelude::*};
7
8fn main() {
9    App::new()
10        .add_plugins(DefaultPlugins)
11        .add_systems(Startup, setup)
12        .add_systems(
13            Update,
14            (
15                spin,
16                move_camera,
17                update_parallax_depth_scale,
18                update_parallax_layers,
19                switch_method,
20            ),
21        )
22        .run();
23}
24
25#[derive(Component)]
26struct Spin {
27    speed: f32,
28}
29
30/// The camera, used to move camera on click.
31#[derive(Component)]
32struct CameraController;
33
34const DEPTH_CHANGE_RATE: f32 = 0.1;
35const DEPTH_UPDATE_STEP: f32 = 0.03;
36const MAX_DEPTH: f32 = 0.3;
37
38struct TargetDepth(f32);
39impl Default for TargetDepth {
40    fn default() -> Self {
41        TargetDepth(0.09)
42    }
43}
44struct TargetLayers(f32);
45impl Default for TargetLayers {
46    fn default() -> Self {
47        TargetLayers(5.0)
48    }
49}
50struct CurrentMethod(ParallaxMappingMethod);
51impl Default for CurrentMethod {
52    fn default() -> Self {
53        CurrentMethod(ParallaxMappingMethod::Relief { max_steps: 4 })
54    }
55}
56impl fmt::Display for CurrentMethod {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        match self.0 {
59            ParallaxMappingMethod::Occlusion => write!(f, "Parallax Occlusion Mapping"),
60            ParallaxMappingMethod::Relief { max_steps } => {
61                write!(f, "Relief Mapping with {max_steps} steps")
62            }
63        }
64    }
65}
66impl CurrentMethod {
67    fn next_method(&mut self) {
68        use ParallaxMappingMethod::*;
69        self.0 = match self.0 {
70            Occlusion => Relief { max_steps: 2 },
71            Relief { max_steps } if max_steps < 3 => Relief { max_steps: 4 },
72            Relief { max_steps } if max_steps < 5 => Relief { max_steps: 8 },
73            Relief { .. } => Occlusion,
74        }
75    }
76}
77
78fn update_parallax_depth_scale(
79    input: Res<ButtonInput<KeyCode>>,
80    mut materials: ResMut<Assets<StandardMaterial>>,
81    mut target_depth: Local<TargetDepth>,
82    mut depth_update: Local<bool>,
83    mut writer: TextUiWriter,
84    text: Single<Entity, With<Text>>,
85) {
86    if input.just_pressed(KeyCode::Digit1) {
87        target_depth.0 -= DEPTH_UPDATE_STEP;
88        target_depth.0 = target_depth.0.max(0.0);
89        *depth_update = true;
90    }
91    if input.just_pressed(KeyCode::Digit2) {
92        target_depth.0 += DEPTH_UPDATE_STEP;
93        target_depth.0 = target_depth.0.min(MAX_DEPTH);
94        *depth_update = true;
95    }
96    if *depth_update {
97        for (_, mat) in materials.iter_mut() {
98            let current_depth = mat.parallax_depth_scale;
99            let new_depth = current_depth.lerp(target_depth.0, DEPTH_CHANGE_RATE);
100            mat.parallax_depth_scale = new_depth;
101            *writer.text(*text, 1) = format!("Parallax depth scale: {new_depth:.5}\n");
102            if (new_depth - current_depth).abs() <= 0.000000001 {
103                *depth_update = false;
104            }
105        }
106    }
107}
108
109fn switch_method(
110    input: Res<ButtonInput<KeyCode>>,
111    mut materials: ResMut<Assets<StandardMaterial>>,
112    text: Single<Entity, With<Text>>,
113    mut writer: TextUiWriter,
114    mut current: Local<CurrentMethod>,
115) {
116    if input.just_pressed(KeyCode::Space) {
117        current.next_method();
118    } else {
119        return;
120    }
121    let text_entity = *text;
122    *writer.text(text_entity, 3) = format!("Method: {}\n", *current);
123
124    for (_, mat) in materials.iter_mut() {
125        mat.parallax_mapping_method = current.0;
126    }
127}
128
129fn update_parallax_layers(
130    input: Res<ButtonInput<KeyCode>>,
131    mut materials: ResMut<Assets<StandardMaterial>>,
132    mut target_layers: Local<TargetLayers>,
133    text: Single<Entity, With<Text>>,
134    mut writer: TextUiWriter,
135) {
136    if input.just_pressed(KeyCode::Digit3) {
137        target_layers.0 -= 1.0;
138        target_layers.0 = target_layers.0.max(0.0);
139    } else if input.just_pressed(KeyCode::Digit4) {
140        target_layers.0 += 1.0;
141    } else {
142        return;
143    }
144    let layer_count = ops::exp2(target_layers.0);
145    let text_entity = *text;
146    *writer.text(text_entity, 2) = format!("Layers: {layer_count:.0}\n");
147
148    for (_, mat) in materials.iter_mut() {
149        mat.max_parallax_layer_count = layer_count;
150    }
151}
152
153fn spin(time: Res<Time>, mut query: Query<(&mut Transform, &Spin)>) {
154    for (mut transform, spin) in query.iter_mut() {
155        transform.rotate_local_y(spin.speed * time.delta_secs());
156        transform.rotate_local_x(spin.speed * time.delta_secs());
157        transform.rotate_local_z(-spin.speed * time.delta_secs());
158    }
159}
160
161// Camera positions to cycle through when left-clicking.
162const CAMERA_POSITIONS: &[Transform] = &[
163    Transform {
164        translation: Vec3::new(1.5, 1.5, 1.5),
165        rotation: Quat::from_xyzw(-0.279, 0.364, 0.115, 0.880),
166        scale: Vec3::ONE,
167    },
168    Transform {
169        translation: Vec3::new(2.4, 0.0, 0.2),
170        rotation: Quat::from_xyzw(0.094, 0.676, 0.116, 0.721),
171        scale: Vec3::ONE,
172    },
173    Transform {
174        translation: Vec3::new(2.4, 2.6, -4.3),
175        rotation: Quat::from_xyzw(0.170, 0.908, 0.308, 0.225),
176        scale: Vec3::ONE,
177    },
178    Transform {
179        translation: Vec3::new(-1.0, 0.8, -1.2),
180        rotation: Quat::from_xyzw(-0.004, 0.909, 0.247, -0.335),
181        scale: Vec3::ONE,
182    },
183];
184
185fn move_camera(
186    mut camera: Single<&mut Transform, With<CameraController>>,
187    mut current_view: Local<usize>,
188    button: Res<ButtonInput<MouseButton>>,
189) {
190    if button.just_pressed(MouseButton::Left) {
191        *current_view = (*current_view + 1) % CAMERA_POSITIONS.len();
192    }
193    let target = CAMERA_POSITIONS[*current_view];
194    camera.translation = camera.translation.lerp(target.translation, 0.2);
195    camera.rotation = camera.rotation.slerp(target.rotation, 0.2);
196}
197
198fn setup(
199    mut commands: Commands,
200    mut materials: ResMut<Assets<StandardMaterial>>,
201    mut meshes: ResMut<Assets<Mesh>>,
202    asset_server: Res<AssetServer>,
203) {
204    // The normal map. Note that to generate it in the GIMP image editor, you should
205    // open the depth map, and do Filters → Generic → Normal Map
206    // You should enable the "flip X" checkbox.
207    let normal_handle = asset_server.load_with_settings(
208        "textures/parallax_example/cube_normal.png",
209        // The normal map texture is in linear color space. Lighting won't look correct
210        // if `is_srgb` is `true`, which is the default.
211        |settings: &mut ImageLoaderSettings| settings.is_srgb = false,
212    );
213
214    // Camera
215    commands.spawn((
216        Camera3d::default(),
217        Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::ZERO, Vec3::Y),
218        CameraController,
219    ));
220
221    // light
222    commands
223        .spawn((
224            PointLight {
225                shadows_enabled: true,
226                ..default()
227            },
228            Transform::from_xyz(2.0, 1.0, -1.1),
229        ))
230        .with_children(|commands| {
231            // represent the light source as a sphere
232            let mesh = meshes.add(Sphere::new(0.05).mesh().ico(3).unwrap());
233            commands.spawn((Mesh3d(mesh), MeshMaterial3d(materials.add(Color::WHITE))));
234        });
235
236    // Plane
237    commands.spawn((
238        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
239        MeshMaterial3d(materials.add(StandardMaterial {
240            // standard material derived from dark green, but
241            // with roughness and reflectance set.
242            perceptual_roughness: 0.45,
243            reflectance: 0.18,
244            ..Color::srgb_u8(0, 80, 0).into()
245        })),
246        Transform::from_xyz(0.0, -1.0, 0.0),
247    ));
248
249    let parallax_depth_scale = TargetDepth::default().0;
250    let max_parallax_layer_count = ops::exp2(TargetLayers::default().0);
251    let parallax_mapping_method = CurrentMethod::default();
252    let parallax_material = materials.add(StandardMaterial {
253        perceptual_roughness: 0.4,
254        base_color_texture: Some(asset_server.load("textures/parallax_example/cube_color.png")),
255        normal_map_texture: Some(normal_handle),
256        // The depth map is a grayscale texture where black is the highest level and
257        // white the lowest.
258        depth_map: Some(asset_server.load("textures/parallax_example/cube_depth.png")),
259        parallax_depth_scale,
260        parallax_mapping_method: parallax_mapping_method.0,
261        max_parallax_layer_count,
262        ..default()
263    });
264    commands.spawn((
265        Mesh3d(
266            meshes.add(
267                // NOTE: for normal maps and depth maps to work, the mesh
268                // needs tangents generated.
269                Mesh::from(Cuboid::default())
270                    .with_generated_tangents()
271                    .unwrap(),
272            ),
273        ),
274        MeshMaterial3d(parallax_material.clone()),
275        Spin { speed: 0.3 },
276    ));
277
278    let background_cube = meshes.add(
279        Mesh::from(Cuboid::new(40.0, 40.0, 40.0))
280            .with_generated_tangents()
281            .unwrap(),
282    );
283
284    let background_cube_bundle = |translation| {
285        (
286            Mesh3d(background_cube.clone()),
287            MeshMaterial3d(parallax_material.clone()),
288            Transform::from_translation(translation),
289            Spin { speed: -0.1 },
290        )
291    };
292    commands.spawn(background_cube_bundle(Vec3::new(45., 0., 0.)));
293    commands.spawn(background_cube_bundle(Vec3::new(-45., 0., 0.)));
294    commands.spawn(background_cube_bundle(Vec3::new(0., 0., 45.)));
295    commands.spawn(background_cube_bundle(Vec3::new(0., 0., -45.)));
296
297    // example instructions
298    commands
299        .spawn((
300            Text::default(),
301            Node {
302                position_type: PositionType::Absolute,
303                top: Val::Px(12.0),
304                left: Val::Px(12.0),
305                ..default()
306            },
307        ))
308        .with_children(|p| {
309            p.spawn(TextSpan(format!(
310                "Parallax depth scale: {parallax_depth_scale:.5}\n"
311            )));
312            p.spawn(TextSpan(format!("Layers: {max_parallax_layer_count:.0}\n")));
313            p.spawn(TextSpan(format!("{parallax_mapping_method}\n")));
314            p.spawn(TextSpan::new("\n\n"));
315            p.spawn(TextSpan::new("Controls:\n"));
316            p.spawn(TextSpan::new("Left click - Change view angle\n"));
317            p.spawn(TextSpan::new(
318                "1/2 - Decrease/Increase parallax depth scale\n",
319            ));
320            p.spawn(TextSpan::new("3/4 - Decrease/Increase layer count\n"));
321            p.spawn(TextSpan::new("Space - Switch parallaxing algorithm\n"));
322        });
323}