diff --git a/Cargo.toml b/Cargo.toml index a35c8ee752..99270a5c0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -561,6 +561,16 @@ description = "Demonstrates use of Physically Based Rendering (PBR) properties" category = "3D Rendering" wasm = true +[[example]] +name = "parallax_mapping" +path = "examples/3d/parallax_mapping.rs" + +[package.metadata.example.parallax_mapping] +name = "Parallax Mapping" +description = "Demonstrates use of a normal map and depth map for parallax mapping" +category = "3D Rendering" +wasm = true + [[example]] name = "render_to_texture" path = "examples/3d/render_to_texture.rs" diff --git a/assets/textures/parallax_example/cube_color.png b/assets/textures/parallax_example/cube_color.png new file mode 100644 index 0000000000..0eefbadc64 Binary files /dev/null and b/assets/textures/parallax_example/cube_color.png differ diff --git a/assets/textures/parallax_example/cube_depth.png b/assets/textures/parallax_example/cube_depth.png new file mode 100644 index 0000000000..4c2c354f09 Binary files /dev/null and b/assets/textures/parallax_example/cube_depth.png differ diff --git a/assets/textures/parallax_example/cube_normal.png b/assets/textures/parallax_example/cube_normal.png new file mode 100644 index 0000000000..9b089bdb02 Binary files /dev/null and b/assets/textures/parallax_example/cube_normal.png differ diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 917fd007c3..bf7af957df 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -8,6 +8,7 @@ mod environment_map; mod fog; mod light; mod material; +mod parallax; mod pbr_material; mod prepass; mod render; @@ -18,6 +19,7 @@ pub use environment_map::EnvironmentMapLight; pub use fog::*; pub use light::*; pub use material::*; +pub use parallax::*; pub use pbr_material::*; pub use prepass::*; pub use render::*; @@ -34,6 +36,7 @@ pub mod prelude { fog::{FogFalloff, FogSettings}, light::{AmbientLight, DirectionalLight, PointLight, SpotLight}, material::{Material, MaterialPlugin}, + parallax::ParallaxMappingMethod, pbr_material::StandardMaterial, }; } @@ -82,6 +85,8 @@ pub const PBR_FUNCTIONS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292); pub const PBR_AMBIENT_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2441520459096337034); +pub const PARALLAX_MAPPING_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17035894873630133905); /// Sets up the entire PBR infrastructure of bevy. pub struct PbrPlugin { @@ -150,6 +155,12 @@ impl Plugin for PbrPlugin { "render/pbr_prepass.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PARALLAX_MAPPING_SHADER_HANDLE, + "render/parallax_mapping.wgsl", + Shader::from_wgsl + ); app.register_asset_reflect::() .register_type::() diff --git a/crates/bevy_pbr/src/parallax.rs b/crates/bevy_pbr/src/parallax.rs new file mode 100644 index 0000000000..b9e9389040 --- /dev/null +++ b/crates/bevy_pbr/src/parallax.rs @@ -0,0 +1,45 @@ +use bevy_reflect::{FromReflect, Reflect}; + +/// The [parallax mapping] method to use to compute depth based on the +/// material's [`depth_map`]. +/// +/// Parallax Mapping uses a depth map texture to give the illusion of depth +/// variation on a mesh surface that is geometrically flat. +/// +/// See the `parallax_mapping.wgsl` shader code for implementation details +/// and explanation of the methods used. +/// +/// [`depth_map`]: crate::StandardMaterial::depth_map +/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect, FromReflect)] +pub enum ParallaxMappingMethod { + /// A simple linear interpolation, using a single texture sample. + /// + /// This method is named "Parallax Occlusion Mapping". + /// + /// Unlike [`ParallaxMappingMethod::Relief`], only requires a single lookup, + /// but may skip small details and result in writhing material artifacts. + #[default] + Occlusion, + /// Discovers the best depth value based on binary search. + /// + /// Each iteration incurs a texture sample. + /// The result has fewer visual artifacts than [`ParallaxMappingMethod::Occlusion`]. + /// + /// This method is named "Relief Mapping". + Relief { + /// How many additional steps to use at most to find the depth value. + max_steps: u32, + }, +} +impl ParallaxMappingMethod { + /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. + pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; + + pub(crate) fn max_steps(&self) -> u32 { + match self { + ParallaxMappingMethod::Occlusion => 0, + ParallaxMappingMethod::Relief { max_steps } => *max_steps, + } + } +} diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index c8be48f8fc..1eadd479af 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,6 +1,6 @@ use crate::{ - AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_PREPASS_SHADER_HANDLE, - PBR_SHADER_HANDLE, + AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, ParallaxMappingMethod, + PBR_PREPASS_SHADER_HANDLE, PBR_SHADER_HANDLE, }; use bevy_asset::Handle; use bevy_math::Vec4; @@ -231,6 +231,84 @@ pub struct StandardMaterial { /// /// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting pub depth_bias: f32, + + /// The depth map used for [parallax mapping]. + /// + /// It is a greyscale image where white represents bottom and black the top. + /// If this field is set, bevy will apply [parallax mapping]. + /// Parallax mapping, unlike simple normal maps, will move the texture + /// coordinate according to the current perspective, + /// giving actual depth to the texture. + /// + /// The visual result is similar to a displacement map, + /// but does not require additional geometry. + /// + /// Use the [`parallax_depth_scale`] field to control the depth of the parallax. + /// + /// ## Limitations + /// + /// - It will look weird on bent/non-planar surfaces. + /// - The depth of the pixel does not reflect its visual position, resulting + /// in artifacts for depth-dependent features such as fog or SSAO. + /// - For the same reason, the the geometry silhouette will always be + /// the one of the actual geometry, not the parallaxed version, resulting + /// in awkward looks on intersecting parallaxed surfaces. + /// + /// ## Performance + /// + /// Parallax mapping requires multiple texture lookups, proportional to + /// [`max_parallax_layer_count`], which might be costly. + /// + /// Use the [`parallax_mapping_method`] and [`max_parallax_layer_count`] fields + /// to tweak the shader, trading graphical quality for performance. + /// + /// To improve performance, set your `depth_map`'s [`Image::sampler_descriptor`] + /// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves + /// performance a bit. + /// + /// To reduce artifacts, avoid steep changes in depth, blurring the depth + /// map helps with this. + /// + /// Larger depth maps haves a disproportionate performance impact. + /// + /// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf + /// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping + /// [`parallax_depth_scale`]: StandardMaterial::parallax_depth_scale + /// [`parallax_mapping_method`]: StandardMaterial::parallax_mapping_method + /// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count + #[texture(11)] + #[sampler(12)] + pub depth_map: Option>, + + /// How deep the offset introduced by the depth map should be. + /// + /// Default is `0.1`, anything over that value may look distorted. + /// Lower values lessen the effect. + /// + /// The depth is relative to texture size. This means that if your texture + /// occupies a surface of `1` world unit, and `parallax_depth_scale` is `0.1`, then + /// the in-world depth will be of `0.1` world units. + /// If the texture stretches for `10` world units, then the final depth + /// will be of `1` world unit. + pub parallax_depth_scale: f32, + + /// Which parallax mapping method to use. + /// + /// We recommend that all objects use the same [`ParallaxMappingMethod`], to avoid + /// duplicating and running two shaders. + pub parallax_mapping_method: ParallaxMappingMethod, + + /// In how many layers to split the depth maps for parallax mapping. + /// + /// If you are seeing jaggy edges, increase this value. + /// However, this incurs a performance cost. + /// + /// Dependent on the situation, switching to [`ParallaxMappingMethod::Relief`] + /// and keeping this value low might have better performance than increasing the + /// layer count while using [`ParallaxMappingMethod::Occlusion`]. + /// + /// Default is `16.0`. + pub max_parallax_layer_count: f32, } impl Default for StandardMaterial { @@ -260,6 +338,10 @@ impl Default for StandardMaterial { fog_enabled: true, alpha_mode: AlphaMode::Opaque, depth_bias: 0.0, + depth_map: None, + parallax_depth_scale: 0.1, + max_parallax_layer_count: 16.0, + parallax_mapping_method: ParallaxMappingMethod::Occlusion, } } } @@ -302,6 +384,7 @@ bitflags::bitflags! { const TWO_COMPONENT_NORMAL_MAP = (1 << 6); const FLIP_NORMAL_MAP_Y = (1 << 7); const FOG_ENABLED = (1 << 8); + const DEPTH_MAP = (1 << 9); // Used for parallax mapping const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. @@ -341,6 +424,16 @@ pub struct StandardMaterialUniform { /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, /// and any below means fully transparent. pub alpha_cutoff: f32, + /// The depth of the [`StandardMaterial::depth_map`] to apply. + pub parallax_depth_scale: f32, + /// In how many layers to split the depth maps for Steep parallax mapping. + /// + /// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges, + /// increase this value. However, this incurs a performance cost. + pub max_parallax_layer_count: f32, + /// Using [`ParallaxMappingMethod::Relief`], how many additional + /// steps to use at most to find the depth value. + pub max_relief_mapping_search_steps: u32, } impl AsBindGroupShaderType for StandardMaterial { @@ -367,6 +460,9 @@ impl AsBindGroupShaderType for StandardMaterial { if self.fog_enabled { flags |= StandardMaterialFlags::FOG_ENABLED; } + if self.depth_map.is_some() { + flags |= StandardMaterialFlags::DEPTH_MAP; + } let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) { @@ -407,15 +503,20 @@ impl AsBindGroupShaderType for StandardMaterial { reflectance: self.reflectance, flags: flags.bits(), alpha_cutoff, + parallax_depth_scale: self.parallax_depth_scale, + max_parallax_layer_count: self.max_parallax_layer_count, + max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), } } } +/// The pipeline key for [`StandardMaterial`]. #[derive(Clone, PartialEq, Eq, Hash)] pub struct StandardMaterialKey { normal_map: bool, cull_mode: Option, depth_bias: i32, + relief_mapping: bool, } impl From<&StandardMaterial> for StandardMaterialKey { @@ -424,6 +525,10 @@ impl From<&StandardMaterial> for StandardMaterialKey { normal_map: material.normal_map_texture.is_some(), cull_mode: material.cull_mode, depth_bias: material.depth_bias as i32, + relief_mapping: matches!( + material.parallax_mapping_method, + ParallaxMappingMethod::Relief { .. } + ), } } } @@ -435,11 +540,14 @@ impl Material for StandardMaterial { _layout: &MeshVertexBufferLayout, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { - if key.bind_group_data.normal_map { - if let Some(fragment) = descriptor.fragment.as_mut() { - fragment - .shader_defs - .push("STANDARDMATERIAL_NORMAL_MAP".into()); + if let Some(fragment) = descriptor.fragment.as_mut() { + let shader_defs = &mut fragment.shader_defs; + + if key.bind_group_data.normal_map { + shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into()); + } + if key.bind_group_data.relief_mapping { + shader_defs.push("RELIEF_MAPPING".into()); } } descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; diff --git a/crates/bevy_pbr/src/render/parallax_mapping.wgsl b/crates/bevy_pbr/src/render/parallax_mapping.wgsl new file mode 100644 index 0000000000..884b5e23c6 --- /dev/null +++ b/crates/bevy_pbr/src/render/parallax_mapping.wgsl @@ -0,0 +1,116 @@ +#define_import_path bevy_pbr::parallax_mapping + +fn sample_depth_map(uv: vec2) -> f32 { + // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 + // backend (Fxc) panics when using "gradient instructions" inside a loop. + // It results in the whole loop being unrolled by the shader compiler, + // which it can't do because the upper limit of the loop in steep parallax + // mapping is a variable set by the user. + // The "gradient instructions" comes from `textureSample` computing MIP level + // based on UV derivative. With `textureSampleLevel`, we provide ourselves + // the MIP level, so no gradient instructions are used, and we can use + // sample_depth_map in our loop. + // See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing + return textureSampleLevel(depth_map_texture, depth_map_sampler, uv, 0.0).r; +} + +// An implementation of parallax mapping, see https://en.wikipedia.org/wiki/Parallax_mapping +// Code derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28 +fn parallaxed_uv( + depth_scale: f32, + max_layer_count: f32, + max_steps: u32, + // The original interpolated uv + uv: vec2, + // The vector from the camera to the fragment at the surface in tangent space + Vt: vec3, +) -> vec2 { + if max_layer_count < 1.0 { + return uv; + } + var uv = uv; + + // Steep Parallax Mapping + // ====================== + // Split the depth map into `layer_count` layers. + // When Vt hits the surface of the mesh (excluding depth displacement), + // if the depth is not below or on surface including depth displacement (textureSample), then + // look forward (+= delta_uv) on depth texture according to + // Vt and distance between hit surface and depth map surface, + // repeat until below the surface. + // + // Where `layer_count` is interpolated between `1.0` and + // `max_layer_count` according to the steepness of Vt. + + let view_steepness = abs(Vt.z); + // We mix with minimum value 1.0 because otherwise, + // with 0.0, we get a division by zero in surfaces parallel to viewport, + // resulting in a singularity. + let layer_count = mix(max_layer_count, 1.0, view_steepness); + let layer_depth = 1.0 / layer_count; + var delta_uv = depth_scale * layer_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + + var current_layer_depth = 0.0; + var texture_depth = sample_depth_map(uv); + + // texture_depth > current_layer_depth means the depth map depth is deeper + // than the depth the ray would be at at this UV offset so the ray has not + // intersected the surface + for (var i: i32 = 0; texture_depth > current_layer_depth && i <= i32(layer_count); i++) { + current_layer_depth += layer_depth; + uv += delta_uv; + texture_depth = sample_depth_map(uv); + } + +#ifdef RELIEF_MAPPING + // Relief Mapping + // ============== + // "Refine" the rough result from Steep Parallax Mapping + // with a **binary search** between the layer selected by steep parallax + // and the next one to find a point closer to the depth map surface. + // This reduces the jaggy step artifacts from steep parallax mapping. + + delta_uv *= 0.5; + var delta_depth = 0.5 * layer_depth; + + uv -= delta_uv; + current_layer_depth -= delta_depth; + + for (var i: u32 = 0u; i < max_steps; i++) { + texture_depth = sample_depth_map(uv); + + // Halve the deltas for the next step + delta_uv *= 0.5; + delta_depth *= 0.5; + + // Step based on whether the current depth is above or below the depth map + if (texture_depth > current_layer_depth) { + uv += delta_uv; + current_layer_depth += delta_depth; + } else { + uv -= delta_uv; + current_layer_depth -= delta_depth; + } + } +#else + // Parallax Occlusion mapping + // ========================== + // "Refine" Steep Parallax Mapping by interpolating between the + // previous layer's depth and the computed layer depth. + // Only requires a single lookup, unlike Relief Mapping, but + // may skip small details and result in writhing material artifacts. + let previous_uv = uv - delta_uv; + let next_depth = texture_depth - current_layer_depth; + let previous_depth = sample_depth_map(previous_uv) - current_layer_depth + layer_depth; + + let weight = next_depth / (next_depth - previous_depth); + + uv = mix(uv, previous_uv, weight); + + current_layer_depth += mix(next_depth, previous_depth, weight); +#endif + + // Note: `current_layer_depth` is not returned, but may be useful + // for light computation later on in future improvements of the pbr shader. + return uv; +} diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index dbd0de51b3..6f36a1c59e 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -9,6 +9,7 @@ #import bevy_pbr::shadows #import bevy_pbr::fog #import bevy_pbr::pbr_functions +#import bevy_pbr::parallax_mapping #import bevy_pbr::prepass_utils @@ -20,13 +21,37 @@ struct FragmentInput { @fragment fn fragment(in: FragmentInput) -> @location(0) vec4 { + let is_orthographic = view.projection[3].w == 1.0; + let V = calculate_view(in.world_position, is_orthographic); + var uv = in.uv; +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS + if ((material.flags & STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) { + let N = in.world_normal; + let T = in.world_tangent.xyz; + let B = in.world_tangent.w * cross(N, T); + // Transform V from fragment to camera in world space to tangent space. + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); + uv = parallaxed_uv( + material.parallax_depth_scale, + material.max_parallax_layer_count, + material.max_relief_mapping_search_steps, + uv, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + ); + } +#endif +#endif var output_color: vec4 = material.base_color; #ifdef VERTEX_COLORS output_color = output_color * in.color; #endif #ifdef VERTEX_UVS if ((material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { - output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv); + output_color = output_color * textureSample(base_color_texture, base_color_sampler, uv); } #endif @@ -45,7 +70,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { var emissive: vec4 = material.emissive; #ifdef VERTEX_UVS if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { - emissive = vec4(emissive.rgb * textureSample(emissive_texture, emissive_sampler, in.uv).rgb, 1.0); + emissive = vec4(emissive.rgb * textureSample(emissive_texture, emissive_sampler, uv).rgb, 1.0); } #endif pbr_input.material.emissive = emissive; @@ -54,7 +79,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { var perceptual_roughness: f32 = material.perceptual_roughness; #ifdef VERTEX_UVS if ((material.flags & STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { - let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, in.uv); + let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, uv); // Sampling from GLTF standard channels for now metallic = metallic * metallic_roughness.b; perceptual_roughness = perceptual_roughness * metallic_roughness.g; @@ -66,7 +91,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { var occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { - occlusion = textureSample(occlusion_texture, occlusion_sampler, in.uv).r; + occlusion = textureSample(occlusion_texture, occlusion_sampler, uv).r; } #endif pbr_input.frag_coord = in.frag_coord; @@ -82,7 +107,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { ); #endif // LOAD_PREPASS_NORMALS - pbr_input.is_orthographic = view.projection[3].w == 1.0; + pbr_input.is_orthographic = is_orthographic; pbr_input.N = apply_normal_mapping( material.flags, @@ -93,10 +118,10 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { #endif #endif #ifdef VERTEX_UVS - in.uv, + uv, #endif ); - pbr_input.V = calculate_view(in.world_position, pbr_input.is_orthographic); + pbr_input.V = V; pbr_input.occlusion = occlusion; pbr_input.flags = mesh.flags; diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index f4e4d34f3b..c5400dd204 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -24,3 +24,7 @@ var occlusion_sampler: sampler; var normal_map_texture: texture_2d; @group(1) @binding(10) var normal_map_sampler: sampler; +@group(1) @binding(11) +var depth_map_texture: texture_2d; +@group(1) @binding(12) +var depth_map_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 6cead285a5..85cbed505f 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -9,6 +9,9 @@ struct StandardMaterial { // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, alpha_cutoff: f32, + parallax_depth_scale: f32, + max_parallax_layer_count: f32, + max_relief_mapping_search_steps: u32, }; const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u; @@ -20,6 +23,7 @@ const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u; const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u; const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 256u; +const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 512u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) @@ -42,6 +46,9 @@ fn standard_material_new() -> StandardMaterial { material.reflectance = 0.5; material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; material.alpha_cutoff = 0.5; + material.parallax_depth_scale = 0.1; + material.max_parallax_layer_count = 16.0; + material.max_relief_mapping_search_steps = 5u; return material; } diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs new file mode 100644 index 0000000000..16a893260a --- /dev/null +++ b/examples/3d/parallax_mapping.rs @@ -0,0 +1,387 @@ +//! 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::{prelude::*, render::render_resource::TextureFormat, window::close_on_esc}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(Normal(None)) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + spin, + update_normal, + move_camera, + update_parallax_depth_scale, + update_parallax_layers, + switch_method, + close_on_esc, + ), + ) + .run(); +} + +#[derive(Component)] +struct Spin { + speed: f32, +} + +/// The camera, used to move camera on click. +#[derive(Component)] +struct CameraController; + +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>, + mut materials: ResMut>, + mut target_depth: Local, + mut depth_update: Local, + mut text: Query<&mut Text>, +) { + if input.just_pressed(KeyCode::Key1) { + target_depth.0 -= DEPTH_UPDATE_STEP; + target_depth.0 = target_depth.0.max(0.0); + *depth_update = true; + } + if input.just_pressed(KeyCode::Key2) { + target_depth.0 += DEPTH_UPDATE_STEP; + target_depth.0 = target_depth.0.min(MAX_DEPTH); + *depth_update = true; + } + if *depth_update { + let mut text = text.single_mut(); + for (_, mat) in materials.iter_mut() { + let current_depth = mat.parallax_depth_scale; + let new_depth = + current_depth * (1.0 - DEPTH_CHANGE_RATE) + (target_depth.0 * DEPTH_CHANGE_RATE); + mat.parallax_depth_scale = new_depth; + text.sections[0].value = format!("Parallax depth scale: {new_depth:.5}\n"); + if (new_depth - current_depth).abs() <= 0.000000001 { + *depth_update = false; + } + } + } +} + +fn switch_method( + input: Res>, + mut materials: ResMut>, + mut text: Query<&mut Text>, + mut current: Local, +) { + if input.just_pressed(KeyCode::Space) { + current.next_method(); + } else { + return; + } + let mut text = text.single_mut(); + text.sections[2].value = format!("Method: {}\n", *current); + + for (_, mat) in materials.iter_mut() { + mat.parallax_mapping_method = current.0; + } +} + +fn update_parallax_layers( + input: Res>, + mut materials: ResMut>, + mut target_layers: Local, + mut text: Query<&mut Text>, +) { + if input.just_pressed(KeyCode::Key3) { + target_layers.0 -= 1.0; + target_layers.0 = target_layers.0.max(0.0); + } else if input.just_pressed(KeyCode::Key4) { + target_layers.0 += 1.0; + } else { + return; + } + let layer_count = target_layers.0.exp2(); + let mut text = text.single_mut(); + text.sections[1].value = format!("Layers: {layer_count:.0}\n"); + + for (_, mat) in materials.iter_mut() { + mat.max_parallax_layer_count = layer_count; + } +} + +fn spin(time: Res