Skip to content

Commit 8df014f

Browse files
nicopapsuperdump
andauthored
Add parallax mapping to bevy PBR (#5928)
# Objective Add a [parallax mapping] shader to bevy. Please note that this is a 3d technique, NOT a 2d sidescroller feature. ## Solution - Add related fields to `StandardMaterial` - update the pbr shader - Add an example taking advantage of parallax mapping A pre-existing implementation exists at: https://github.com/nicopap/bevy_mod_paramap/ The implementation is derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28 Further discussion on literature is found in the `bevy_mod_paramap` README. ### Limitations - The mesh silhouette isn't affected by the depth map. - The depth of the pixel does not reflect its visual position, resulting in artifacts for depth-dependent features such as fog or SSAO - GLTF does not define a height map texture, so somehow the user will always need to work around this limitation, though [an extension is in the works][gltf] ### Future work - It's possible to update the depth in the depth buffer to follow the parallaxed texture. This would enable interop with depth-based visual effects, it also allows `discard`ing pixels of materials when computed depth is higher than the one in depth buffer - Cheap lower quality single-sample method using [offset limiting] - Add distance fading, to disable parallaxing (relatively expensive) on distant objects - GLTF extension to allow defining height maps. Or a workaround implemented through a blender plugin to the GLTF exporter that uses the `extras` field to add height map. - [Quadratic surface vertex attributes][oliveira_3] to enable parallax mapping on bending surfaces and allow clean silhouetting. - noise based sampling, to limit the pancake artifacts. - Cone mapping ([GPU gems], [Simcity (2013)][simcity]). Requires preprocessing, increase depth map size, reduces sample count greatly. - [Quadtree parallax mapping][qpm] (also requires preprocessing) - Self-shadowing of parallax-mapped surfaces by modifying the shadow map - Generate depth map from normal map [link to slides], [blender question] https://user-images.githubusercontent.com/26321040/223563792-dffcc6ab-70e8-4ff9-90d1-b36c338695ad.mp4 [blender question]: https://blender.stackexchange.com/questions/89278/how-to-get-a-smooth-curvature-map-from-a-normal-map [link to slides]: https://developer.download.nvidia.com/assets/gamedev/docs/nmap2displacement.pdf [oliveira_3]: https://www.inf.ufrgs.br/~oliveira/pubs_files/Oliveira_Policarpo_RP-351_Jan_2005.pdf [GPU gems]: https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-18-relaxed-cone-stepping-relief-mapping [simcity]: https://community.simtropolis.com/omnibus/other-games/building-and-rendering-simcity-2013-r247/ [offset limiting]: https://raw.githubusercontent.com/marcusstenbeck/tncg14-parallax-mapping/master/documents/Parallax%20Mapping%20with%20Offset%20Limiting%20-%20A%20Per-Pixel%20Approximation%20of%20Uneven%20Surfaces.pdf [gltf]: KhronosGroup/glTF#2196 [qpm]: https://www.gamedevs.org/uploads/quadtree-displacement-mapping-with-height-blending.pdf --- ## Changelog - Add a `depth_map` field to the `StandardMaterial`, it is a grayscale image where white represents bottom and black the top. If `depth_map` is set, bevy's pbr shader will use it to do [parallax mapping] to give an increased feel of depth to the material. This is similar to a displacement map, but with infinite precision at fairly low cost. - The fields `parallax_mapping_method`, `parallax_depth_scale` and `max_parallax_layer_count` allow finer grained control over the behavior of the parallax shader. - Add the `parallax_mapping` example to show off the effect. [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping --------- Co-authored-by: Robert Swain <[email protected]>
1 parent 1074a41 commit 8df014f

File tree

13 files changed

+728
-14
lines changed

13 files changed

+728
-14
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,16 @@ description = "Demonstrates use of Physically Based Rendering (PBR) properties"
561561
category = "3D Rendering"
562562
wasm = true
563563

564+
[[example]]
565+
name = "parallax_mapping"
566+
path = "examples/3d/parallax_mapping.rs"
567+
568+
[package.metadata.example.parallax_mapping]
569+
name = "Parallax Mapping"
570+
description = "Demonstrates use of a normal map and depth map for parallax mapping"
571+
category = "3D Rendering"
572+
wasm = true
573+
564574
[[example]]
565575
name = "render_to_texture"
566576
path = "examples/3d/render_to_texture.rs"
670 KB
Loading
10.5 KB
Loading
32.9 KB
Loading

crates/bevy_pbr/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod environment_map;
88
mod fog;
99
mod light;
1010
mod material;
11+
mod parallax;
1112
mod pbr_material;
1213
mod prepass;
1314
mod render;
@@ -18,6 +19,7 @@ pub use environment_map::EnvironmentMapLight;
1819
pub use fog::*;
1920
pub use light::*;
2021
pub use material::*;
22+
pub use parallax::*;
2123
pub use pbr_material::*;
2224
pub use prepass::*;
2325
pub use render::*;
@@ -34,6 +36,7 @@ pub mod prelude {
3436
fog::{FogFalloff, FogSettings},
3537
light::{AmbientLight, DirectionalLight, PointLight, SpotLight},
3638
material::{Material, MaterialPlugin},
39+
parallax::ParallaxMappingMethod,
3740
pbr_material::StandardMaterial,
3841
};
3942
}
@@ -82,6 +85,8 @@ pub const PBR_FUNCTIONS_HANDLE: HandleUntyped =
8285
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292);
8386
pub const PBR_AMBIENT_HANDLE: HandleUntyped =
8487
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2441520459096337034);
88+
pub const PARALLAX_MAPPING_SHADER_HANDLE: HandleUntyped =
89+
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17035894873630133905);
8590

8691
/// Sets up the entire PBR infrastructure of bevy.
8792
pub struct PbrPlugin {
@@ -150,6 +155,12 @@ impl Plugin for PbrPlugin {
150155
"render/pbr_prepass.wgsl",
151156
Shader::from_wgsl
152157
);
158+
load_internal_asset!(
159+
app,
160+
PARALLAX_MAPPING_SHADER_HANDLE,
161+
"render/parallax_mapping.wgsl",
162+
Shader::from_wgsl
163+
);
153164

154165
app.register_asset_reflect::<StandardMaterial>()
155166
.register_type::<AmbientLight>()

crates/bevy_pbr/src/parallax.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use bevy_reflect::{FromReflect, Reflect};
2+
3+
/// The [parallax mapping] method to use to compute depth based on the
4+
/// material's [`depth_map`].
5+
///
6+
/// Parallax Mapping uses a depth map texture to give the illusion of depth
7+
/// variation on a mesh surface that is geometrically flat.
8+
///
9+
/// See the `parallax_mapping.wgsl` shader code for implementation details
10+
/// and explanation of the methods used.
11+
///
12+
/// [`depth_map`]: crate::StandardMaterial::depth_map
13+
/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping
14+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect, FromReflect)]
15+
pub enum ParallaxMappingMethod {
16+
/// A simple linear interpolation, using a single texture sample.
17+
///
18+
/// This method is named "Parallax Occlusion Mapping".
19+
///
20+
/// Unlike [`ParallaxMappingMethod::Relief`], only requires a single lookup,
21+
/// but may skip small details and result in writhing material artifacts.
22+
#[default]
23+
Occlusion,
24+
/// Discovers the best depth value based on binary search.
25+
///
26+
/// Each iteration incurs a texture sample.
27+
/// The result has fewer visual artifacts than [`ParallaxMappingMethod::Occlusion`].
28+
///
29+
/// This method is named "Relief Mapping".
30+
Relief {
31+
/// How many additional steps to use at most to find the depth value.
32+
max_steps: u32,
33+
},
34+
}
35+
impl ParallaxMappingMethod {
36+
/// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default.
37+
pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 };
38+
39+
pub(crate) fn max_steps(&self) -> u32 {
40+
match self {
41+
ParallaxMappingMethod::Occlusion => 0,
42+
ParallaxMappingMethod::Relief { max_steps } => *max_steps,
43+
}
44+
}
45+
}

crates/bevy_pbr/src/pbr_material.rs

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
2-
AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_PREPASS_SHADER_HANDLE,
3-
PBR_SHADER_HANDLE,
2+
AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, ParallaxMappingMethod,
3+
PBR_PREPASS_SHADER_HANDLE, PBR_SHADER_HANDLE,
44
};
55
use bevy_asset::Handle;
66
use bevy_math::Vec4;
@@ -231,6 +231,84 @@ pub struct StandardMaterial {
231231
///
232232
/// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting
233233
pub depth_bias: f32,
234+
235+
/// The depth map used for [parallax mapping].
236+
///
237+
/// It is a greyscale image where white represents bottom and black the top.
238+
/// If this field is set, bevy will apply [parallax mapping].
239+
/// Parallax mapping, unlike simple normal maps, will move the texture
240+
/// coordinate according to the current perspective,
241+
/// giving actual depth to the texture.
242+
///
243+
/// The visual result is similar to a displacement map,
244+
/// but does not require additional geometry.
245+
///
246+
/// Use the [`parallax_depth_scale`] field to control the depth of the parallax.
247+
///
248+
/// ## Limitations
249+
///
250+
/// - It will look weird on bent/non-planar surfaces.
251+
/// - The depth of the pixel does not reflect its visual position, resulting
252+
/// in artifacts for depth-dependent features such as fog or SSAO.
253+
/// - For the same reason, the the geometry silhouette will always be
254+
/// the one of the actual geometry, not the parallaxed version, resulting
255+
/// in awkward looks on intersecting parallaxed surfaces.
256+
///
257+
/// ## Performance
258+
///
259+
/// Parallax mapping requires multiple texture lookups, proportional to
260+
/// [`max_parallax_layer_count`], which might be costly.
261+
///
262+
/// Use the [`parallax_mapping_method`] and [`max_parallax_layer_count`] fields
263+
/// to tweak the shader, trading graphical quality for performance.
264+
///
265+
/// To improve performance, set your `depth_map`'s [`Image::sampler_descriptor`]
266+
/// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves
267+
/// performance a bit.
268+
///
269+
/// To reduce artifacts, avoid steep changes in depth, blurring the depth
270+
/// map helps with this.
271+
///
272+
/// Larger depth maps haves a disproportionate performance impact.
273+
///
274+
/// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf
275+
/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping
276+
/// [`parallax_depth_scale`]: StandardMaterial::parallax_depth_scale
277+
/// [`parallax_mapping_method`]: StandardMaterial::parallax_mapping_method
278+
/// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count
279+
#[texture(11)]
280+
#[sampler(12)]
281+
pub depth_map: Option<Handle<Image>>,
282+
283+
/// How deep the offset introduced by the depth map should be.
284+
///
285+
/// Default is `0.1`, anything over that value may look distorted.
286+
/// Lower values lessen the effect.
287+
///
288+
/// The depth is relative to texture size. This means that if your texture
289+
/// occupies a surface of `1` world unit, and `parallax_depth_scale` is `0.1`, then
290+
/// the in-world depth will be of `0.1` world units.
291+
/// If the texture stretches for `10` world units, then the final depth
292+
/// will be of `1` world unit.
293+
pub parallax_depth_scale: f32,
294+
295+
/// Which parallax mapping method to use.
296+
///
297+
/// We recommend that all objects use the same [`ParallaxMappingMethod`], to avoid
298+
/// duplicating and running two shaders.
299+
pub parallax_mapping_method: ParallaxMappingMethod,
300+
301+
/// In how many layers to split the depth maps for parallax mapping.
302+
///
303+
/// If you are seeing jaggy edges, increase this value.
304+
/// However, this incurs a performance cost.
305+
///
306+
/// Dependent on the situation, switching to [`ParallaxMappingMethod::Relief`]
307+
/// and keeping this value low might have better performance than increasing the
308+
/// layer count while using [`ParallaxMappingMethod::Occlusion`].
309+
///
310+
/// Default is `16.0`.
311+
pub max_parallax_layer_count: f32,
234312
}
235313

236314
impl Default for StandardMaterial {
@@ -260,6 +338,10 @@ impl Default for StandardMaterial {
260338
fog_enabled: true,
261339
alpha_mode: AlphaMode::Opaque,
262340
depth_bias: 0.0,
341+
depth_map: None,
342+
parallax_depth_scale: 0.1,
343+
max_parallax_layer_count: 16.0,
344+
parallax_mapping_method: ParallaxMappingMethod::Occlusion,
263345
}
264346
}
265347
}
@@ -302,6 +384,7 @@ bitflags::bitflags! {
302384
const TWO_COMPONENT_NORMAL_MAP = (1 << 6);
303385
const FLIP_NORMAL_MAP_Y = (1 << 7);
304386
const FOG_ENABLED = (1 << 8);
387+
const DEPTH_MAP = (1 << 9); // Used for parallax mapping
305388
const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode`
306389
const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into
307390
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 {
341424
/// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque,
342425
/// and any below means fully transparent.
343426
pub alpha_cutoff: f32,
427+
/// The depth of the [`StandardMaterial::depth_map`] to apply.
428+
pub parallax_depth_scale: f32,
429+
/// In how many layers to split the depth maps for Steep parallax mapping.
430+
///
431+
/// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges,
432+
/// increase this value. However, this incurs a performance cost.
433+
pub max_parallax_layer_count: f32,
434+
/// Using [`ParallaxMappingMethod::Relief`], how many additional
435+
/// steps to use at most to find the depth value.
436+
pub max_relief_mapping_search_steps: u32,
344437
}
345438

346439
impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
@@ -367,6 +460,9 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
367460
if self.fog_enabled {
368461
flags |= StandardMaterialFlags::FOG_ENABLED;
369462
}
463+
if self.depth_map.is_some() {
464+
flags |= StandardMaterialFlags::DEPTH_MAP;
465+
}
370466
let has_normal_map = self.normal_map_texture.is_some();
371467
if has_normal_map {
372468
if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) {
@@ -407,15 +503,20 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
407503
reflectance: self.reflectance,
408504
flags: flags.bits(),
409505
alpha_cutoff,
506+
parallax_depth_scale: self.parallax_depth_scale,
507+
max_parallax_layer_count: self.max_parallax_layer_count,
508+
max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(),
410509
}
411510
}
412511
}
413512

513+
/// The pipeline key for [`StandardMaterial`].
414514
#[derive(Clone, PartialEq, Eq, Hash)]
415515
pub struct StandardMaterialKey {
416516
normal_map: bool,
417517
cull_mode: Option<Face>,
418518
depth_bias: i32,
519+
relief_mapping: bool,
419520
}
420521

421522
impl From<&StandardMaterial> for StandardMaterialKey {
@@ -424,6 +525,10 @@ impl From<&StandardMaterial> for StandardMaterialKey {
424525
normal_map: material.normal_map_texture.is_some(),
425526
cull_mode: material.cull_mode,
426527
depth_bias: material.depth_bias as i32,
528+
relief_mapping: matches!(
529+
material.parallax_mapping_method,
530+
ParallaxMappingMethod::Relief { .. }
531+
),
427532
}
428533
}
429534
}
@@ -435,11 +540,14 @@ impl Material for StandardMaterial {
435540
_layout: &MeshVertexBufferLayout,
436541
key: MaterialPipelineKey<Self>,
437542
) -> Result<(), SpecializedMeshPipelineError> {
438-
if key.bind_group_data.normal_map {
439-
if let Some(fragment) = descriptor.fragment.as_mut() {
440-
fragment
441-
.shader_defs
442-
.push("STANDARDMATERIAL_NORMAL_MAP".into());
543+
if let Some(fragment) = descriptor.fragment.as_mut() {
544+
let shader_defs = &mut fragment.shader_defs;
545+
546+
if key.bind_group_data.normal_map {
547+
shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into());
548+
}
549+
if key.bind_group_data.relief_mapping {
550+
shader_defs.push("RELIEF_MAPPING".into());
443551
}
444552
}
445553
descriptor.primitive.cull_mode = key.bind_group_data.cull_mode;

0 commit comments

Comments
 (0)