Skip to content

Commit e8e2426

Browse files
JMS55IceSentry
andauthored
Forward decals (port of bevy_contact_projective_decals) (#16600)
# Objective - Implement ForwardDecal as outlined in #2401 ## Solution - Port https://github.com/naasblod/bevy_contact_projective_decals, and cleanup the API a little. ## Testing - Ran the new decal example. --- ## Showcase ![image](https://github.com/user-attachments/assets/72134af0-724f-4df9-a11f-b0888819a791) ## Changelog * Added ForwardDecal and associated types * Added MaterialExtension::alpha_mode() --------- Co-authored-by: IceSentry <[email protected]>
1 parent 26bb0b4 commit e8e2426

File tree

10 files changed

+336
-5
lines changed

10 files changed

+336
-5
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,17 @@ description = "Illustrates bloom configuration using HDR and emissive materials"
951951
category = "3D Rendering"
952952
wasm = true
953953

954+
[[example]]
955+
name = "decal"
956+
path = "examples/3d/decal.rs"
957+
doc-scrape-examples = true
958+
959+
[package.metadata.example.decal]
960+
name = "Decal"
961+
description = "Decal rendering"
962+
category = "3D Rendering"
963+
wasm = true
964+
954965
[[example]]
955966
name = "deferred_rendering"
956967
path = "examples/3d/deferred_rendering.rs"

crates/bevy_pbr/src/decal/forward.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use crate::{
2+
ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
3+
MaterialPlugin, StandardMaterial,
4+
};
5+
use bevy_app::{App, Plugin};
6+
use bevy_asset::{load_internal_asset, Asset, Assets, Handle};
7+
use bevy_ecs::component::{require, Component};
8+
use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3};
9+
use bevy_reflect::{Reflect, TypePath};
10+
use bevy_render::{
11+
alpha::AlphaMode,
12+
mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable},
13+
render_resource::{
14+
AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader,
15+
SpecializedMeshPipelineError,
16+
},
17+
};
18+
19+
const FORWARD_DECAL_MESH_HANDLE: Handle<Mesh> = Handle::weak_from_u128(19376620402995522466);
20+
const FORWARD_DECAL_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(29376620402995522466);
21+
22+
/// Plugin to render [`ForwardDecal`]s.
23+
pub struct ForwardDecalPlugin;
24+
25+
impl Plugin for ForwardDecalPlugin {
26+
fn build(&self, app: &mut App) {
27+
load_internal_asset!(
28+
app,
29+
FORWARD_DECAL_SHADER_HANDLE,
30+
"forward_decal.wgsl",
31+
Shader::from_wgsl
32+
);
33+
34+
app.register_type::<ForwardDecal>();
35+
36+
app.world_mut().resource_mut::<Assets<Mesh>>().insert(
37+
FORWARD_DECAL_MESH_HANDLE.id(),
38+
Rectangle::from_size(Vec2::ONE)
39+
.mesh()
40+
.build()
41+
.rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y))
42+
.with_generated_tangents()
43+
.unwrap(),
44+
);
45+
46+
app.add_plugins(MaterialPlugin::<ForwardDecalMaterial<StandardMaterial>> {
47+
prepass_enabled: false,
48+
shadows_enabled: false,
49+
..Default::default()
50+
});
51+
}
52+
}
53+
54+
/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying
55+
/// geometry towards the edges.
56+
///
57+
/// Because forward decals are meshes, you can use arbitrary materials to control their appearance.
58+
///
59+
/// # Usage Notes
60+
///
61+
/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`].
62+
/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::DepthPrepass`] component.
63+
/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's
64+
/// texture with extra transparent pixels on the edges.
65+
#[derive(Component, Reflect)]
66+
#[require(Mesh3d(|| Mesh3d(FORWARD_DECAL_MESH_HANDLE)))]
67+
pub struct ForwardDecal;
68+
69+
/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension.
70+
///
71+
/// Make sure to register the [`MaterialPlugin`] for this material in your app setup.
72+
///
73+
/// [`StandardMaterial`] comes with out of the box support for forward decals.
74+
#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")]
75+
pub type ForwardDecalMaterial<B: Material> = ExtendedMaterial<B, ForwardDecalMaterialExt>;
76+
77+
/// Material extension for a [`ForwardDecal`].
78+
///
79+
/// In addition to wrapping your material type with this extension, your shader must use
80+
/// the `bevy_pbr::decal::forward::get_forward_decal_info` function.
81+
///
82+
/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate
83+
/// the forward decal code behind an ifdef.
84+
#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)]
85+
pub struct ForwardDecalMaterialExt {
86+
/// Controls how far away a surface must be before the decal will stop blending with it, and instead render as opaque.
87+
///
88+
/// Decreasing this value will cause the decal to blend only to surfaces closer to it.
89+
///
90+
/// Units are in meters.
91+
#[uniform(200)]
92+
pub depth_fade_factor: f32,
93+
}
94+
95+
impl MaterialExtension for ForwardDecalMaterialExt {
96+
fn alpha_mode() -> Option<AlphaMode> {
97+
Some(AlphaMode::Blend)
98+
}
99+
100+
fn specialize(
101+
_pipeline: &MaterialExtensionPipeline,
102+
descriptor: &mut RenderPipelineDescriptor,
103+
_layout: &MeshVertexBufferLayoutRef,
104+
_key: MaterialExtensionKey<Self>,
105+
) -> Result<(), SpecializedMeshPipelineError> {
106+
descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always;
107+
108+
descriptor.vertex.shader_defs.push("FORWARD_DECAL".into());
109+
110+
if let Some(fragment) = &mut descriptor.fragment {
111+
fragment.shader_defs.push("FORWARD_DECAL".into());
112+
}
113+
114+
if let Some(label) = &mut descriptor.label {
115+
*label = format!("forward_decal_{}", label).into();
116+
}
117+
118+
Ok(())
119+
}
120+
}
121+
122+
impl Default for ForwardDecalMaterialExt {
123+
fn default() -> Self {
124+
Self {
125+
depth_fade_factor: 8.0,
126+
}
127+
}
128+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#define_import_path bevy_pbr::decal::forward
2+
3+
#import bevy_pbr::{
4+
forward_io::VertexOutput,
5+
mesh_functions::get_world_from_local,
6+
mesh_view_bindings::view,
7+
pbr_functions::calculate_tbn_mikktspace,
8+
prepass_utils::prepass_depth,
9+
view_transformations::depth_ndc_to_view_z,
10+
}
11+
#import bevy_render::maths::project_onto
12+
13+
@group(2) @binding(200)
14+
var<uniform> depth_fade_factor: f32;
15+
16+
struct ForwardDecalInformation {
17+
world_position: vec4<f32>,
18+
uv: vec2<f32>,
19+
alpha: f32,
20+
}
21+
22+
fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation {
23+
let world_from_local = get_world_from_local(in.instance_index);
24+
let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz;
25+
let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w);
26+
27+
let V = normalize(view.world_position - in.world_position.xyz);
28+
29+
// Transform V from fragment to camera in world space to tangent space.
30+
let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent);
31+
let T = TBN[0];
32+
let B = TBN[1];
33+
let N = TBN[2];
34+
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
35+
36+
let frag_depth = depth_ndc_to_view_z(in.position.z);
37+
let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u));
38+
let diff_depth = frag_depth - depth_pass_depth;
39+
let diff_depth_abs = abs(diff_depth);
40+
41+
// Apply UV parallax
42+
let contact_on_decal = project_onto(V * diff_depth, in.world_normal);
43+
let normal_depth = length(contact_on_decal);
44+
let view_steepness = abs(Vt.z);
45+
let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness;
46+
let uv = in.uv + delta_uv;
47+
48+
let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w);
49+
let alpha = saturate(1.0 - normal_depth * depth_fade_factor);
50+
51+
return ForwardDecalInformation(world_position, uv, alpha);
52+
}

crates/bevy_pbr/src/decal/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Decal rendering.
2+
//!
3+
//! Decals are a material that render on top of the surface that they're placed above.
4+
//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces.
5+
6+
// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs.
7+
8+
mod forward;
9+
10+
pub use forward::*;

crates/bevy_pbr/src/extended_material.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use bevy_asset::{Asset, Handle};
22
use bevy_ecs::system::SystemParamItem;
33
use bevy_reflect::{impl_type_path, Reflect};
44
use bevy_render::{
5+
alpha::AlphaMode,
56
mesh::MeshVertexBufferLayoutRef,
67
render_resource::{
78
AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader,
@@ -41,6 +42,11 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized {
4142
ShaderRef::Default
4243
}
4344

45+
// Returns this material’s AlphaMode. If None is returned, the base material alpha mode will be used.
46+
fn alpha_mode() -> Option<AlphaMode> {
47+
None
48+
}
49+
4450
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader
4551
/// will be used.
4652
fn prepass_vertex_shader() -> ShaderRef {
@@ -230,8 +236,11 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
230236
}
231237
}
232238

233-
fn alpha_mode(&self) -> crate::AlphaMode {
234-
B::alpha_mode(&self.base)
239+
fn alpha_mode(&self) -> AlphaMode {
240+
match E::alpha_mode() {
241+
Some(specified) => specified,
242+
None => B::alpha_mode(&self.base),
243+
}
235244
}
236245

237246
fn opaque_render_method(&self) -> crate::OpaqueRendererMethod {

crates/bevy_pbr/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod experimental {
2626

2727
mod cluster;
2828
mod components;
29+
pub mod decal;
2930
pub mod deferred;
3031
mod extended_material;
3132
mod fog;
@@ -335,6 +336,7 @@ impl Plugin for PbrPlugin {
335336
ScreenSpaceReflectionsPlugin,
336337
))
337338
.add_plugins((
339+
decal::ForwardDecalPlugin,
338340
SyncComponentPlugin::<DirectionalLight>::default(),
339341
SyncComponentPlugin::<PointLight>::default(),
340342
SyncComponentPlugin::<SpotLight>::default(),

crates/bevy_pbr/src/render/pbr.wgsl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,38 @@
2626
#import bevy_core_pipeline::oit::oit_draw
2727
#endif // OIT_ENABLED
2828

29+
#ifdef FORWARD_DECAL
30+
#import bevy_pbr::decal::forward::get_forward_decal_info
31+
#endif
32+
2933
@fragment
3034
fn fragment(
3135
#ifdef MESHLET_MESH_MATERIAL_PASS
3236
@builtin(position) frag_coord: vec4<f32>,
3337
#else
34-
in: VertexOutput,
38+
vertex_output: VertexOutput,
3539
@builtin(front_facing) is_front: bool,
3640
#endif
3741
) -> FragmentOutput {
3842
#ifdef MESHLET_MESH_MATERIAL_PASS
39-
let in = resolve_vertex_output(frag_coord);
43+
let vertex_output = resolve_vertex_output(frag_coord);
4044
let is_front = true;
4145
#endif
4246

47+
var in = vertex_output;
48+
4349
// If we're in the crossfade section of a visibility range, conditionally
4450
// discard the fragment according to the visibility pattern.
4551
#ifdef VISIBILITY_RANGE_DITHER
4652
pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither);
4753
#endif
4854

55+
#ifdef FORWARD_DECAL
56+
let forward_decal_info = get_forward_decal_info(in);
57+
in.world_position = forward_decal_info.world_position;
58+
in.uv = forward_decal_info.uv;
59+
#endif
60+
4961
// generate a PbrInput struct from the StandardMaterial bindings
5062
var pbr_input = pbr_input_from_standard_material(in, is_front);
5163

@@ -79,5 +91,9 @@ fn fragment(
7991
}
8092
#endif // OIT_ENABLED
8193

82-
return out;
94+
#ifdef FORWARD_DECAL
95+
out.color.a = min(forward_decal_info.alpha, out.color.a);
96+
#endif
97+
98+
return out;
8399
}

crates/bevy_render/src/maths.wgsl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,9 @@ fn sphere_intersects_plane_half_space(
9393
fn powsafe(color: vec3<f32>, power: f32) -> vec3<f32> {
9494
return pow(abs(color), vec3(power)) * sign(color);
9595
}
96+
97+
// https://en.wikipedia.org/wiki/Vector_projection#Vector_projection_2
98+
fn project_onto(lhs: vec3<f32>, rhs: vec3<f32>) -> vec3<f32> {
99+
let other_len_sq_rcp = 1.0 / dot(rhs, rhs);
100+
return rhs * dot(lhs, rhs) * other_len_sq_rcp;
101+
}

0 commit comments

Comments
 (0)