Skip to content

Commit a2f47b9

Browse files
nicopapsuperdump
andcommitted
Add parallax mapping to bevy_pbr
Add a [parallax mapping] shader to bevy. Please note that this is a 3d technique, NOT a 2d sidescroller feature. - 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 https://user-images.githubusercontent.com/26321040/223563792-dffcc6ab-70e8-4ff9-90d1-b36c338695ad.mp4 --- - Add a `depth_map` field to the `StandardMaterial`, it is a greyscale 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` 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 [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 Co-authored-by: Robert Swain <robert.swain@gmail.com>
1 parent 7a9e77c commit a2f47b9

File tree

13 files changed

+645
-14
lines changed

13 files changed

+645
-14
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,17 @@ 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+
required-features = [ "jpeg" ]
568+
569+
[package.metadata.example.parallax_mapping]
570+
name = "Parallax Mapping"
571+
description = "Demonstrates use of a normal map and height map for parallax mapping"
572+
category = "3D Rendering"
573+
wasm = true
574+
564575
[[example]]
565576
name = "render_to_texture"
566577
path = "examples/3d/render_to_texture.rs"
70.5 KB
Loading
5.82 KB
Loading
35.2 KB
Loading

crates/bevy_pbr/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod environment_map;
66
mod fog;
77
mod light;
88
mod material;
9+
mod parallax;
910
mod pbr_material;
1011
mod prepass;
1112
mod render;
@@ -16,6 +17,7 @@ pub use environment_map::EnvironmentMapLight;
1617
pub use fog::*;
1718
pub use light::*;
1819
pub use material::*;
20+
pub use parallax::*;
1921
pub use pbr_material::*;
2022
pub use prepass::*;
2123
pub use render::*;
@@ -32,6 +34,7 @@ pub mod prelude {
3234
fog::{FogFalloff, FogSettings},
3335
light::{AmbientLight, DirectionalLight, PointLight, SpotLight},
3436
material::{Material, MaterialPlugin},
37+
parallax::ParallaxMappingMethod,
3538
pbr_material::StandardMaterial,
3639
};
3740
}
@@ -80,6 +83,8 @@ pub const PBR_FUNCTIONS_HANDLE: HandleUntyped =
8083
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292);
8184
pub const PBR_AMBIENT_HANDLE: HandleUntyped =
8285
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2441520459096337034);
86+
pub const PARALLAX_MAPPING_SHADER_HANDLE: HandleUntyped =
87+
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17035894873630133905);
8388

8489
/// Sets up the entire PBR infrastructure of bevy.
8590
pub struct PbrPlugin {
@@ -148,6 +153,12 @@ impl Plugin for PbrPlugin {
148153
"render/pbr_prepass.wgsl",
149154
Shader::from_wgsl
150155
);
156+
load_internal_asset!(
157+
app,
158+
PARALLAX_MAPPING_SHADER_HANDLE,
159+
"render/parallax_mapping.wgsl",
160+
Shader::from_wgsl
161+
);
151162

152163
app.register_asset_reflect::<StandardMaterial>()
153164
.register_type::<AmbientLight>()

crates/bevy_pbr/src/parallax.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use bevy_reflect::{FromReflect, Reflect};
2+
3+
/// The parallax mapping method to use to compute a displacement based on the
4+
/// material's [`depth_map`].
5+
///
6+
/// See the `parallax_mapping.wgsl` shader code for implementation details
7+
/// and explanation of the methods used.
8+
///
9+
/// [`depth_map`]: crate::StandardMaterial::depth_map
10+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect, FromReflect)]
11+
pub enum ParallaxMappingMethod {
12+
/// A simple linear interpolation, using a single texture sample.
13+
#[default]
14+
ParallaxOcclusionMapping,
15+
/// A discovery of 5 iterations of the best displacement
16+
/// value. Each iteration incurs a texture sample.
17+
///
18+
/// The result has fewer visual artifacts than `ParallaxOcclusionMapping`.
19+
ReliefMapping { n_steps: u32 },
20+
}

crates/bevy_pbr/src/pbr_material.rs

Lines changed: 111 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`] 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`]: StandardMaterial::parallax_depth
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` 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: 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::ReliefMapping`]
307+
/// and keeping this value low might have better performance than increasing the
308+
/// layer count while using [`ParallaxMappingMethod::ParallaxOcclusionMapping`].
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: 0.1,
343+
max_parallax_layer_count: 16.0,
344+
parallax_mapping_method: ParallaxMappingMethod::ParallaxOcclusionMapping,
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,13 @@ 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: f32,
429+
/// In how many layers to split the depth maps for Steep parallax mapping.
430+
///
431+
/// If your `parallax_depth` 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,
344434
}
345435

346436
impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
@@ -367,6 +457,9 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
367457
if self.fog_enabled {
368458
flags |= StandardMaterialFlags::FOG_ENABLED;
369459
}
460+
if self.depth_map.is_some() {
461+
flags |= StandardMaterialFlags::DEPTH_MAP;
462+
}
370463
let has_normal_map = self.normal_map_texture.is_some();
371464
if has_normal_map {
372465
if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) {
@@ -407,15 +500,19 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
407500
reflectance: self.reflectance,
408501
flags: flags.bits(),
409502
alpha_cutoff,
503+
parallax_depth: self.parallax_depth,
504+
max_parallax_layer_count: self.max_parallax_layer_count,
410505
}
411506
}
412507
}
413508

509+
/// The pipeline key for [`StandardMaterial`].
414510
#[derive(Clone, PartialEq, Eq, Hash)]
415511
pub struct StandardMaterialKey {
416512
normal_map: bool,
417513
cull_mode: Option<Face>,
418514
depth_bias: i32,
515+
relief_mapping: bool,
419516
}
420517

421518
impl From<&StandardMaterial> for StandardMaterialKey {
@@ -424,6 +521,10 @@ impl From<&StandardMaterial> for StandardMaterialKey {
424521
normal_map: material.normal_map_texture.is_some(),
425522
cull_mode: material.cull_mode,
426523
depth_bias: material.depth_bias as i32,
524+
relief_mapping: matches!(
525+
material.parallax_mapping_method,
526+
ParallaxMappingMethod::ReliefMapping { .. }
527+
),
427528
}
428529
}
429530
}
@@ -435,11 +536,14 @@ impl Material for StandardMaterial {
435536
_layout: &MeshVertexBufferLayout,
436537
key: MaterialPipelineKey<Self>,
437538
) -> 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());
539+
if let Some(fragment) = descriptor.fragment.as_mut() {
540+
let shader_defs = &mut fragment.shader_defs;
541+
542+
if key.bind_group_data.normal_map {
543+
shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into());
544+
}
545+
if key.bind_group_data.relief_mapping {
546+
shader_defs.push("RELIEF_MAPPING".into());
443547
}
444548
}
445549
descriptor.primitive.cull_mode = key.bind_group_data.cull_mode;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#define_import_path bevy_pbr::parallax_mapping
2+
3+
fn sample_depth_map(uv: vec2<f32>) -> f32 {
4+
return textureSample(depth_map_texture, depth_map_sampler, uv).r;
5+
}
6+
7+
// An implementation of parallax mapping, see https://en.wikipedia.org/wiki/Parallax_mapping
8+
// Code derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28
9+
fn parallaxed_uv(
10+
depth: f32,
11+
max_layer_count: f32,
12+
// The original uv
13+
uv: vec2<f32>,
14+
// The vector from camera to the surface of material
15+
V: vec3<f32>,
16+
) -> vec2<f32> {
17+
var uv = uv;
18+
if max_layer_count < 1.0 {
19+
return uv;
20+
}
21+
22+
// Steep Parallax Mapping
23+
// ======================
24+
// Split the depth map into `layer_count` layers.
25+
// When V hits the surface of the mesh (excluding depth displacement),
26+
// if the depth is not below or on surface including depth displacement (textureSample), then
27+
// look forward (-= delta_uv) according to V and distance between hit surface and
28+
// depth map surface, repeat until below the surface.
29+
//
30+
// Where `layer_count` is interpolated between `min_layer_count` and
31+
// `max_layer_count` according to the steepness of V.
32+
33+
let view_steepness = abs(dot(vec3<f32>(0.0, 0.0, 1.0), V));
34+
// We mix with minimum value 1.0 because otherwise, with 0.0, we get
35+
// a nice division by zero in surfaces parallel to viewport, resulting
36+
// in a singularity.
37+
let layer_count = mix(max_layer_count, 1.0, view_steepness);
38+
let layer_height = 1.0 / layer_count;
39+
var delta_uv = depth * V.xy / V.z / layer_count;
40+
41+
var current_layer_height = 0.0;
42+
var current_height = sample_depth_map(uv);
43+
44+
// This at most runs layer_count times
45+
while true {
46+
if (current_height <= current_layer_height) {
47+
break;
48+
}
49+
current_layer_height += layer_height;
50+
uv -= delta_uv;
51+
current_height = sample_depth_map(uv);
52+
}
53+
54+
#ifdef RELIEF_MAPPING
55+
// Relief Mapping
56+
// ==============
57+
// "Refine" the rough result from Steep Parallax Mapping
58+
// with a binary search between the layer selected by steep parallax
59+
// and the next one to find a point closer to the depth map surface.
60+
// This reduces the jaggy step artifacts from steep parallax mapping.
61+
let MAX_STEPS: i32 = 5;
62+
63+
delta_uv *= 0.5;
64+
var delta_height = 0.5 * layer_height;
65+
uv += delta_uv;
66+
current_layer_height -= delta_height;
67+
for (var i: i32 = 0; i < MAX_STEPS; i++) {
68+
// Sample depth at current offset
69+
current_height = sample_depth_map(uv);
70+
71+
// Halve the deltas for the next step
72+
delta_uv *= 0.5;
73+
delta_height *= 0.5;
74+
75+
// Step based on whether the current depth is above or below the depth map
76+
if (current_height > current_layer_height) {
77+
uv -= delta_uv;
78+
current_layer_height += delta_height;
79+
} else {
80+
uv += delta_uv;
81+
current_layer_height -= delta_height;
82+
}
83+
}
84+
#else
85+
// Parallax Occlusion mapping
86+
// ==========================
87+
// "Refine" Steep Parallax Mapping by interpolating between the
88+
// previous layer's height and the computed layer height.
89+
// Only requires a single lookup, unlike Relief Mapping, but
90+
// may incur artifacts on very steep relief.
91+
let previous_uv = uv + delta_uv;
92+
let next_height = current_height - current_layer_height;
93+
let previous_height = sample_depth_map(previous_uv) - current_layer_height + layer_height;
94+
95+
let weight = next_height / (next_height - previous_height);
96+
97+
uv = mix(uv, previous_uv, weight);
98+
99+
current_layer_height += mix(next_height, previous_height, weight);
100+
#endif
101+
102+
// Note: `current_layer_height` is not returned, but may be useful
103+
// for light computation later on in future improvements of the pbr shader.
104+
return uv;
105+
}

0 commit comments

Comments
 (0)