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}
56
57impl fmt::Display for CurrentMethod {
58    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59        match self.0 {
60            ParallaxMappingMethod::Occlusion => write!(f, "Parallax Occlusion Mapping"),
61            ParallaxMappingMethod::Relief { max_steps } => {
62                write!(f, "Relief Mapping with {max_steps} steps")
63            }
64        }
65    }
66}
67
68impl CurrentMethod {
69    fn next_method(&mut self) {
70        use ParallaxMappingMethod::*;
71        self.0 = match self.0 {
72            Occlusion => Relief { max_steps: 2 },
73            Relief { max_steps } if max_steps < 3 => Relief { max_steps: 4 },
74            Relief { max_steps } if max_steps < 5 => Relief { max_steps: 8 },
75            Relief { .. } => Occlusion,
76        }
77    }
78}
79
80fn update_parallax_depth_scale(
81    input: Res<ButtonInput<KeyCode>>,
82    mut materials: ResMut<Assets<StandardMaterial>>,
83    mut target_depth: Local<TargetDepth>,
84    mut depth_update: Local<bool>,
85    mut writer: TextUiWriter,
86    text: Single<Entity, With<Text>>,
87) {
88    if input.just_pressed(KeyCode::Digit1) {
89        target_depth.0 -= DEPTH_UPDATE_STEP;
90        target_depth.0 = target_depth.0.max(0.0);
91        *depth_update = true;
92    }
93    if input.just_pressed(KeyCode::Digit2) {
94        target_depth.0 += DEPTH_UPDATE_STEP;
95        target_depth.0 = target_depth.0.min(MAX_DEPTH);
96        *depth_update = true;
97    }
98    if *depth_update {
99        for (_, mat) in materials.iter_mut() {
100            let current_depth = mat.parallax_depth_scale;
101            let new_depth = current_depth.lerp(target_depth.0, DEPTH_CHANGE_RATE);
102            mat.parallax_depth_scale = new_depth;
103            *writer.text(*text, 1) = format!("Parallax depth scale: {new_depth:.5}\n");
104            if (new_depth - current_depth).abs() <= 0.000000001 {
105                *depth_update = false;
106            }
107        }
108    }
109}
110
111fn switch_method(
112    input: Res<ButtonInput<KeyCode>>,
113    mut materials: ResMut<Assets<StandardMaterial>>,
114    text: Single<Entity, With<Text>>,
115    mut writer: TextUiWriter,
116    mut current: Local<CurrentMethod>,
117) {
118    if input.just_pressed(KeyCode::Space) {
119        current.next_method();
120    } else {
121        return;
122    }
123    let text_entity = *text;
124    *writer.text(text_entity, 3) = format!("Method: {}\n", *current);
125
126    for (_, mat) in materials.iter_mut() {
127        mat.parallax_mapping_method = current.0;
128    }
129}
130
131fn update_parallax_layers(
132    input: Res<ButtonInput<KeyCode>>,
133    mut materials: ResMut<Assets<StandardMaterial>>,
134    mut target_layers: Local<TargetLayers>,
135    text: Single<Entity, With<Text>>,
136    mut writer: TextUiWriter,
137) {
138    if input.just_pressed(KeyCode::Digit3) {
139        target_layers.0 -= 1.0;
140        target_layers.0 = target_layers.0.max(0.0);
141    } else if input.just_pressed(KeyCode::Digit4) {
142        target_layers.0 += 1.0;
143    } else {
144        return;
145    }
146    let layer_count = ops::exp2(target_layers.0);
147    let text_entity = *text;
148    *writer.text(text_entity, 2) = format!("Layers: {layer_count:.0}\n");
149
150    for (_, mat) in materials.iter_mut() {
151        mat.max_parallax_layer_count = layer_count;
152    }
153}
154
155fn spin(time: Res<Time>, mut query: Query<(&mut Transform, &Spin)>) {
156    for (mut transform, spin) in query.iter_mut() {
157        transform.rotate_local_y(spin.speed * time.delta_secs());
158        transform.rotate_local_x(spin.speed * time.delta_secs());
159        transform.rotate_local_z(-spin.speed * time.delta_secs());
160    }
161}
162
163// Camera positions to cycle through when left-clicking.
164const CAMERA_POSITIONS: &[Transform] = &[
165    Transform {
166        translation: Vec3::new(1.5, 1.5, 1.5),
167        rotation: Quat::from_xyzw(-0.279, 0.364, 0.115, 0.880),
168        scale: Vec3::ONE,
169    },
170    Transform {
171        translation: Vec3::new(2.4, 0.0, 0.2),
172        rotation: Quat::from_xyzw(0.094, 0.676, 0.116, 0.721),
173        scale: Vec3::ONE,
174    },
175    Transform {
176        translation: Vec3::new(2.4, 2.6, -4.3),
177        rotation: Quat::from_xyzw(0.170, 0.908, 0.308, 0.225),
178        scale: Vec3::ONE,
179    },
180    Transform {
181        translation: Vec3::new(-1.0, 0.8, -1.2),
182        rotation: Quat::from_xyzw(-0.004, 0.909, 0.247, -0.335),
183        scale: Vec3::ONE,
184    },
185];
186
187fn move_camera(
188    mut camera: Single<&mut Transform, With<CameraController>>,
189    mut current_view: Local<usize>,
190    button: Res<ButtonInput<MouseButton>>,
191) {
192    if button.just_pressed(MouseButton::Left) {
193        *current_view = (*current_view + 1) % CAMERA_POSITIONS.len();
194    }
195    let target = CAMERA_POSITIONS[*current_view];
196    camera.translation = camera.translation.lerp(target.translation, 0.2);
197    camera.rotation = camera.rotation.slerp(target.rotation, 0.2);
198}
199
200fn setup(
201    mut commands: Commands,
202    mut materials: ResMut<Assets<StandardMaterial>>,
203    mut meshes: ResMut<Assets<Mesh>>,
204    asset_server: Res<AssetServer>,
205) {
206    // The normal map. Note that to generate it in the GIMP image editor, you should
207    // open the depth map, and do Filters → Generic → Normal Map
208    // You should enable the "flip X" checkbox.
209    let normal_handle = asset_server.load_with_settings(
210        "textures/parallax_example/cube_normal.png",
211        // The normal map texture is in linear color space. Lighting won't look correct
212        // if `is_srgb` is `true`, which is the default.
213        |settings: &mut ImageLoaderSettings| settings.is_srgb = false,
214    );
215
216    // Camera
217    commands.spawn((
218        Camera3d::default(),
219        Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::ZERO, Vec3::Y),
220        CameraController,
221    ));
222
223    // represent the light source as a sphere
224    let mesh = meshes.add(Sphere::new(0.05).mesh().ico(3).unwrap());
225
226    // light
227    commands.spawn((
228        PointLight {
229            shadows_enabled: true,
230            ..default()
231        },
232        Transform::from_xyz(2.0, 1.0, -1.1),
233        children![(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.spawn((
299        Text::default(),
300        Node {
301            position_type: PositionType::Absolute,
302            top: px(12),
303            left: px(12),
304            ..default()
305        },
306        children![
307            (TextSpan(format!("Parallax depth scale: {parallax_depth_scale:.5}\n"))),
308            (TextSpan(format!("Layers: {max_parallax_layer_count:.0}\n"))),
309            (TextSpan(format!("{parallax_mapping_method}\n"))),
310            (TextSpan::new("\n\n")),
311            (TextSpan::new("Controls:\n")),
312            (TextSpan::new("Left click - Change view angle\n")),
313            (TextSpan::new("1/2 - Decrease/Increase parallax depth scale\n",)),
314            (TextSpan::new("3/4 - Decrease/Increase layer count\n")),
315            (TextSpan::new("Space - Switch parallaxing algorithm\n")),
316        ],
317    ));
318}