3D Rendering / Parallax Mapping

Back to examples View in GitHub

A simple 3D scene with a spinning cube with a normal map and depth map to demonstrate parallax mapping. Press left mouse button to cycle through different views.

use std::fmt;  use bevy::{image::ImageLoaderSettings, math::ops, prelude::*};  fn main() {     App::new()         .add_plugins(DefaultPlugins)         .add_systems(Startup, setup)         .add_systems(             Update,             (                 spin,                 move_camera,                 update_parallax_depth_scale,                 update_parallax_layers,                 switch_method,             ),         )         .run(); }  #[derive(Component)] struct Spin {     speed: f32, }  /// The camera, used to move camera on click. #[derive(Component)] struct FreeCameraController;  const DEPTH_CHANGE_RATE: f32 = 0.1; const DEPTH_UPDATE_STEP: f32 = 0.03; const MAX_DEPTH: f32 = 0.3;  struct TargetDepth(f32); impl Default for TargetDepth {     fn default() -> Self {         TargetDepth(0.09)     } } struct TargetLayers(f32); impl Default for TargetLayers {     fn default() -> Self {         TargetLayers(5.0)     } } struct CurrentMethod(ParallaxMappingMethod); impl Default for CurrentMethod {     fn default() -> Self {         CurrentMethod(ParallaxMappingMethod::Relief { max_steps: 4 })     } }  impl fmt::Display for CurrentMethod {     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {         match self.0 {             ParallaxMappingMethod::Occlusion => write!(f, "Parallax Occlusion Mapping"),             ParallaxMappingMethod::Relief { max_steps } => {                 write!(f, "Relief Mapping with {max_steps} steps")             }         }     } }  impl CurrentMethod {     fn next_method(&mut self) {         use ParallaxMappingMethod::*;         self.0 = match self.0 {             Occlusion => Relief { max_steps: 2 },             Relief { max_steps } if max_steps < 3 => Relief { max_steps: 4 },             Relief { max_steps } if max_steps < 5 => Relief { max_steps: 8 },             Relief { .. } => Occlusion,         }     } }  fn update_parallax_depth_scale(     input: Res<ButtonInput<KeyCode>>,     mut materials: ResMut<Assets<StandardMaterial>>,     mut target_depth: Local<TargetDepth>,     mut depth_update: Local<bool>,     mut writer: TextUiWriter,     text: Single<Entity, With<Text>>, ) {     if input.just_pressed(KeyCode::Digit1) {         target_depth.0 -= DEPTH_UPDATE_STEP;         target_depth.0 = target_depth.0.max(0.0);         *depth_update = true;     }     if input.just_pressed(KeyCode::Digit2) {         target_depth.0 += DEPTH_UPDATE_STEP;         target_depth.0 = target_depth.0.min(MAX_DEPTH);         *depth_update = true;     }     if *depth_update {         for (_, mat) in materials.iter_mut() {             let current_depth = mat.parallax_depth_scale;             let new_depth = current_depth.lerp(target_depth.0, DEPTH_CHANGE_RATE);             mat.parallax_depth_scale = new_depth;             *writer.text(*text, 1) = format!("Parallax depth scale: {new_depth:.5}\n");             if (new_depth - current_depth).abs() <= 0.000000001 {                 *depth_update = false;             }         }     } }  fn switch_method(     input: Res<ButtonInput<KeyCode>>,     mut materials: ResMut<Assets<StandardMaterial>>,     text: Single<Entity, With<Text>>,     mut writer: TextUiWriter,     mut current: Local<CurrentMethod>, ) {     if input.just_pressed(KeyCode::Space) {         current.next_method();     } else {         return;     }     let text_entity = *text;     *writer.text(text_entity, 3) = format!("Method: {}\n", *current);      for (_, mat) in materials.iter_mut() {         mat.parallax_mapping_method = current.0;     } }  fn update_parallax_layers(     input: Res<ButtonInput<KeyCode>>,     mut materials: ResMut<Assets<StandardMaterial>>,     mut target_layers: Local<TargetLayers>,     text: Single<Entity, With<Text>>,     mut writer: TextUiWriter, ) {     if input.just_pressed(KeyCode::Digit3) {         target_layers.0 -= 1.0;         target_layers.0 = target_layers.0.max(0.0);     } else if input.just_pressed(KeyCode::Digit4) {         target_layers.0 += 1.0;     } else {         return;     }     let layer_count = ops::exp2(target_layers.0);     let text_entity = *text;     *writer.text(text_entity, 2) = format!("Layers: {layer_count:.0}\n");      for (_, mat) in materials.iter_mut() {         mat.max_parallax_layer_count = layer_count;     } }  fn spin(time: Res<Time>, mut query: Query<(&mut Transform, &Spin)>) {     for (mut transform, spin) in query.iter_mut() {         transform.rotate_local_y(spin.speed * time.delta_secs());         transform.rotate_local_x(spin.speed * time.delta_secs());         transform.rotate_local_z(-spin.speed * time.delta_secs());     } }  // Camera positions to cycle through when left-clicking. const CAMERA_POSITIONS: &[Transform] = &[     Transform {         translation: Vec3::new(1.5, 1.5, 1.5),         rotation: Quat::from_xyzw(-0.279, 0.364, 0.115, 0.880),         scale: Vec3::ONE,     },     Transform {         translation: Vec3::new(2.4, 0.0, 0.2),         rotation: Quat::from_xyzw(0.094, 0.676, 0.116, 0.721),         scale: Vec3::ONE,     },     Transform {         translation: Vec3::new(2.4, 2.6, -4.3),         rotation: Quat::from_xyzw(0.170, 0.908, 0.308, 0.225),         scale: Vec3::ONE,     },     Transform {         translation: Vec3::new(-1.0, 0.8, -1.2),         rotation: Quat::from_xyzw(-0.004, 0.909, 0.247, -0.335),         scale: Vec3::ONE,     }, ];  fn move_camera(     mut camera: Single<&mut Transform, With<FreeCameraController>>,     mut current_view: Local<usize>,     button: Res<ButtonInput<MouseButton>>, ) {     if button.just_pressed(MouseButton::Left) {         *current_view = (*current_view + 1) % CAMERA_POSITIONS.len();     }     let target = CAMERA_POSITIONS[*current_view];     camera.translation = camera.translation.lerp(target.translation, 0.2);     camera.rotation = camera.rotation.slerp(target.rotation, 0.2); }  fn setup(     mut commands: Commands,     mut materials: ResMut<Assets<StandardMaterial>>,     mut meshes: ResMut<Assets<Mesh>>,     asset_server: Res<AssetServer>, ) {     // The normal map. Note that to generate it in the GIMP image editor, you should     // open the depth map, and do Filters → Generic → Normal Map     // You should enable the "flip X" checkbox.     let normal_handle = asset_server.load_with_settings(         "textures/parallax_example/cube_normal.png",         // The normal map texture is in linear color space. Lighting won't look correct         // if `is_srgb` is `true`, which is the default.         |settings: &mut ImageLoaderSettings| settings.is_srgb = false,     );      // Camera     commands.spawn((         Camera3d::default(),         Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::ZERO, Vec3::Y),         FreeCameraController,     ));      // represent the light source as a sphere     let mesh = meshes.add(Sphere::new(0.05).mesh().ico(3).unwrap());      // light     commands.spawn((         PointLight {             shadows_enabled: true,             ..default()         },         Transform::from_xyz(2.0, 1.0, -1.1),         children![(Mesh3d(mesh), MeshMaterial3d(materials.add(Color::WHITE)))],     ));      // Plane     commands.spawn((         Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),         MeshMaterial3d(materials.add(StandardMaterial {             // standard material derived from dark green, but             // with roughness and reflectance set.             perceptual_roughness: 0.45,             reflectance: 0.18,             ..Color::srgb_u8(0, 80, 0).into()         })),         Transform::from_xyz(0.0, -1.0, 0.0),     ));      let parallax_depth_scale = TargetDepth::default().0;     let max_parallax_layer_count = ops::exp2(TargetLayers::default().0);     let parallax_mapping_method = CurrentMethod::default();     let parallax_material = materials.add(StandardMaterial {         perceptual_roughness: 0.4,         base_color_texture: Some(asset_server.load("textures/parallax_example/cube_color.png")),         normal_map_texture: Some(normal_handle),         // The depth map is a grayscale texture where black is the highest level and         // white the lowest.         depth_map: Some(asset_server.load("textures/parallax_example/cube_depth.png")),         parallax_depth_scale,         parallax_mapping_method: parallax_mapping_method.0,         max_parallax_layer_count,         ..default()     });     commands.spawn((         Mesh3d(             meshes.add(                 // NOTE: for normal maps and depth maps to work, the mesh                 // needs tangents generated.                 Mesh::from(Cuboid::default())                     .with_generated_tangents()                     .unwrap(),             ),         ),         MeshMaterial3d(parallax_material.clone()),         Spin { speed: 0.3 },     ));      let background_cube = meshes.add(         Mesh::from(Cuboid::new(40.0, 40.0, 40.0))             .with_generated_tangents()             .unwrap(),     );      let background_cube_bundle = |translation| {         (             Mesh3d(background_cube.clone()),             MeshMaterial3d(parallax_material.clone()),             Transform::from_translation(translation),             Spin { speed: -0.1 },         )     };     commands.spawn(background_cube_bundle(Vec3::new(45., 0., 0.)));     commands.spawn(background_cube_bundle(Vec3::new(-45., 0., 0.)));     commands.spawn(background_cube_bundle(Vec3::new(0., 0., 45.)));     commands.spawn(background_cube_bundle(Vec3::new(0., 0., -45.)));      // example instructions     commands.spawn((         Text::default(),         Node {             position_type: PositionType::Absolute,             top: px(12),             left: px(12),             ..default()         },         children![             (TextSpan(format!("Parallax depth scale: {parallax_depth_scale:.5}\n"))),             (TextSpan(format!("Layers: {max_parallax_layer_count:.0}\n"))),             (TextSpan(format!("{parallax_mapping_method}\n"))),             (TextSpan::new("\n\n")),             (TextSpan::new("Controls:\n")),             (TextSpan::new("Left click - Change view angle\n")),             (TextSpan::new("1/2 - Decrease/Increase parallax depth scale\n",)),             (TextSpan::new("3/4 - Decrease/Increase layer count\n")),             (TextSpan::new("Space - Switch parallaxing algorithm\n")),         ],     )); }