Forward decals (port of bevy_contact_projective_decals) (#16600)

# Objective

- Implement ForwardDecal as outlined in
https://github.com/bevyengine/bevy/issues/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 <IceSentry@users.noreply.github.com>
This commit is contained in:
JMS55 2025-01-14 18:31:30 -08:00 committed by GitHub
parent 26bb0b40d2
commit e8e2426058
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 336 additions and 5 deletions

View File

@ -951,6 +951,17 @@ description = "Illustrates bloom configuration using HDR and emissive materials"
category = "3D Rendering"
wasm = true
[[example]]
name = "decal"
path = "examples/3d/decal.rs"
doc-scrape-examples = true
[package.metadata.example.decal]
name = "Decal"
description = "Decal rendering"
category = "3D Rendering"
wasm = true
[[example]]
name = "deferred_rendering"
path = "examples/3d/deferred_rendering.rs"

View File

@ -0,0 +1,128 @@
use crate::{
ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
MaterialPlugin, StandardMaterial,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Asset, Assets, Handle};
use bevy_ecs::component::{require, Component};
use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3};
use bevy_reflect::{Reflect, TypePath};
use bevy_render::{
alpha::AlphaMode,
mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable},
render_resource::{
AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader,
SpecializedMeshPipelineError,
},
};
const FORWARD_DECAL_MESH_HANDLE: Handle<Mesh> = Handle::weak_from_u128(19376620402995522466);
const FORWARD_DECAL_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(29376620402995522466);
/// Plugin to render [`ForwardDecal`]s.
pub struct ForwardDecalPlugin;
impl Plugin for ForwardDecalPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
FORWARD_DECAL_SHADER_HANDLE,
"forward_decal.wgsl",
Shader::from_wgsl
);
app.register_type::<ForwardDecal>();
app.world_mut().resource_mut::<Assets<Mesh>>().insert(
FORWARD_DECAL_MESH_HANDLE.id(),
Rectangle::from_size(Vec2::ONE)
.mesh()
.build()
.rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y))
.with_generated_tangents()
.unwrap(),
);
app.add_plugins(MaterialPlugin::<ForwardDecalMaterial<StandardMaterial>> {
prepass_enabled: false,
shadows_enabled: false,
..Default::default()
});
}
}
/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying
/// geometry towards the edges.
///
/// Because forward decals are meshes, you can use arbitrary materials to control their appearance.
///
/// # Usage Notes
///
/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`].
/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::DepthPrepass`] component.
/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's
/// texture with extra transparent pixels on the edges.
#[derive(Component, Reflect)]
#[require(Mesh3d(|| Mesh3d(FORWARD_DECAL_MESH_HANDLE)))]
pub struct ForwardDecal;
/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension.
///
/// Make sure to register the [`MaterialPlugin`] for this material in your app setup.
///
/// [`StandardMaterial`] comes with out of the box support for forward decals.
#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")]
pub type ForwardDecalMaterial<B: Material> = ExtendedMaterial<B, ForwardDecalMaterialExt>;
/// Material extension for a [`ForwardDecal`].
///
/// In addition to wrapping your material type with this extension, your shader must use
/// the `bevy_pbr::decal::forward::get_forward_decal_info` function.
///
/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate
/// the forward decal code behind an ifdef.
#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)]
pub struct ForwardDecalMaterialExt {
/// Controls how far away a surface must be before the decal will stop blending with it, and instead render as opaque.
///
/// Decreasing this value will cause the decal to blend only to surfaces closer to it.
///
/// Units are in meters.
#[uniform(200)]
pub depth_fade_factor: f32,
}
impl MaterialExtension for ForwardDecalMaterialExt {
fn alpha_mode() -> Option<AlphaMode> {
Some(AlphaMode::Blend)
}
fn specialize(
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always;
descriptor.vertex.shader_defs.push("FORWARD_DECAL".into());
if let Some(fragment) = &mut descriptor.fragment {
fragment.shader_defs.push("FORWARD_DECAL".into());
}
if let Some(label) = &mut descriptor.label {
*label = format!("forward_decal_{}", label).into();
}
Ok(())
}
}
impl Default for ForwardDecalMaterialExt {
fn default() -> Self {
Self {
depth_fade_factor: 8.0,
}
}
}

View File

@ -0,0 +1,52 @@
#define_import_path bevy_pbr::decal::forward
#import bevy_pbr::{
forward_io::VertexOutput,
mesh_functions::get_world_from_local,
mesh_view_bindings::view,
pbr_functions::calculate_tbn_mikktspace,
prepass_utils::prepass_depth,
view_transformations::depth_ndc_to_view_z,
}
#import bevy_render::maths::project_onto
@group(2) @binding(200)
var<uniform> depth_fade_factor: f32;
struct ForwardDecalInformation {
world_position: vec4<f32>,
uv: vec2<f32>,
alpha: f32,
}
fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation {
let world_from_local = get_world_from_local(in.instance_index);
let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz;
let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w);
let V = normalize(view.world_position - in.world_position.xyz);
// Transform V from fragment to camera in world space to tangent space.
let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent);
let T = TBN[0];
let B = TBN[1];
let N = TBN[2];
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
let frag_depth = depth_ndc_to_view_z(in.position.z);
let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u));
let diff_depth = frag_depth - depth_pass_depth;
let diff_depth_abs = abs(diff_depth);
// Apply UV parallax
let contact_on_decal = project_onto(V * diff_depth, in.world_normal);
let normal_depth = length(contact_on_decal);
let view_steepness = abs(Vt.z);
let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness;
let uv = in.uv + delta_uv;
let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w);
let alpha = saturate(1.0 - normal_depth * depth_fade_factor);
return ForwardDecalInformation(world_position, uv, alpha);
}

View File

@ -0,0 +1,10 @@
//! Decal rendering.
//!
//! Decals are a material that render on top of the surface that they're placed above.
//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces.
// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs.
mod forward;
pub use forward::*;

View File

@ -2,6 +2,7 @@ use bevy_asset::{Asset, Handle};
use bevy_ecs::system::SystemParamItem;
use bevy_reflect::{impl_type_path, Reflect};
use bevy_render::{
alpha::AlphaMode,
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader,
@ -41,6 +42,11 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized {
ShaderRef::Default
}
// Returns this materials AlphaMode. If None is returned, the base material alpha mode will be used.
fn alpha_mode() -> Option<AlphaMode> {
None
}
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader
/// will be used.
fn prepass_vertex_shader() -> ShaderRef {
@ -230,8 +236,11 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
}
}
fn alpha_mode(&self) -> crate::AlphaMode {
B::alpha_mode(&self.base)
fn alpha_mode(&self) -> AlphaMode {
match E::alpha_mode() {
Some(specified) => specified,
None => B::alpha_mode(&self.base),
}
}
fn opaque_render_method(&self) -> crate::OpaqueRendererMethod {

View File

@ -26,6 +26,7 @@ pub mod experimental {
mod cluster;
mod components;
pub mod decal;
pub mod deferred;
mod extended_material;
mod fog;
@ -335,6 +336,7 @@ impl Plugin for PbrPlugin {
ScreenSpaceReflectionsPlugin,
))
.add_plugins((
decal::ForwardDecalPlugin,
SyncComponentPlugin::<DirectionalLight>::default(),
SyncComponentPlugin::<PointLight>::default(),
SyncComponentPlugin::<SpotLight>::default(),

View File

@ -26,26 +26,38 @@
#import bevy_core_pipeline::oit::oit_draw
#endif // OIT_ENABLED
#ifdef FORWARD_DECAL
#import bevy_pbr::decal::forward::get_forward_decal_info
#endif
@fragment
fn fragment(
#ifdef MESHLET_MESH_MATERIAL_PASS
@builtin(position) frag_coord: vec4<f32>,
#else
in: VertexOutput,
vertex_output: VertexOutput,
@builtin(front_facing) is_front: bool,
#endif
) -> FragmentOutput {
#ifdef MESHLET_MESH_MATERIAL_PASS
let in = resolve_vertex_output(frag_coord);
let vertex_output = resolve_vertex_output(frag_coord);
let is_front = true;
#endif
var in = vertex_output;
// If we're in the crossfade section of a visibility range, conditionally
// discard the fragment according to the visibility pattern.
#ifdef VISIBILITY_RANGE_DITHER
pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither);
#endif
#ifdef FORWARD_DECAL
let forward_decal_info = get_forward_decal_info(in);
in.world_position = forward_decal_info.world_position;
in.uv = forward_decal_info.uv;
#endif
// generate a PbrInput struct from the StandardMaterial bindings
var pbr_input = pbr_input_from_standard_material(in, is_front);
@ -79,5 +91,9 @@ fn fragment(
}
#endif // OIT_ENABLED
return out;
#ifdef FORWARD_DECAL
out.color.a = min(forward_decal_info.alpha, out.color.a);
#endif
return out;
}

View File

@ -93,3 +93,9 @@ fn sphere_intersects_plane_half_space(
fn powsafe(color: vec3<f32>, power: f32) -> vec3<f32> {
return pow(abs(color), vec3(power)) * sign(color);
}
// https://en.wikipedia.org/wiki/Vector_projection#Vector_projection_2
fn project_onto(lhs: vec3<f32>, rhs: vec3<f32>) -> vec3<f32> {
let other_len_sq_rcp = 1.0 / dot(rhs, rhs);
return rhs * dot(lhs, rhs) * other_len_sq_rcp;
}

96
examples/3d/decal.rs Normal file
View File

@ -0,0 +1,96 @@
//! Decal rendering.
#[path = "../helpers/camera_controller.rs"]
mod camera_controller;
use bevy::{
core_pipeline::prepass::DepthPrepass,
pbr::decal::{ForwardDecal, ForwardDecalMaterial, ForwardDecalMaterialExt},
prelude::*,
};
use camera_controller::{CameraController, CameraControllerPlugin};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CameraControllerPlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut standard_materials: ResMut<Assets<StandardMaterial>>,
mut decal_standard_materials: ResMut<Assets<ForwardDecalMaterial<StandardMaterial>>>,
asset_server: Res<AssetServer>,
) {
// Spawn the forward decal
commands.spawn((
Name::new("Decal"),
ForwardDecal,
MeshMaterial3d(decal_standard_materials.add(ForwardDecalMaterial {
base: StandardMaterial {
base_color_texture: Some(asset_server.load("textures/uv_checker_bw.png")),
..default()
},
extension: ForwardDecalMaterialExt {
depth_fade_factor: 1.0,
},
})),
Transform::from_scale(Vec3::splat(4.0)),
));
commands.spawn((
Name::new("Camera"),
Camera3d::default(),
CameraController::default(),
DepthPrepass, // Must enable the depth prepass to render forward decals
Transform::from_xyz(2.0, 9.5, 2.5).looking_at(Vec3::ZERO, Vec3::Y),
));
let white_material = standard_materials.add(Color::WHITE);
commands.spawn((
Name::new("Floor"),
Mesh3d(meshes.add(Rectangle::from_length(10.0))),
MeshMaterial3d(white_material.clone()),
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
));
// Spawn a few cube with random rotations to showcase how the decals behave with non-flat geometry
let num_obs = 10;
let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
for i in 0..num_obs {
for j in 0..num_obs {
let rotation_axis: [f32; 3] = rng.gen();
let rotation_vec: Vec3 = rotation_axis.into();
let rotation: u32 = rng.gen_range(0..360);
let transform = Transform::from_xyz(
(-num_obs + 1) as f32 / 2.0 + i as f32,
-0.2,
(-num_obs + 1) as f32 / 2.0 + j as f32,
)
.with_rotation(Quat::from_axis_angle(
rotation_vec.normalize_or_zero(),
(rotation as f32).to_radians(),
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::from_length(0.6))),
MeshMaterial3d(white_material.clone()),
transform,
));
}
}
commands.spawn((
Name::new("Light"),
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
}

View File

@ -145,6 +145,7 @@ Example | Description
[Camera sub view](../examples/3d/camera_sub_view.rs) | Demonstrates using different sub view effects on a camera
[Clearcoat](../examples/3d/clearcoat.rs) | Demonstrates the clearcoat PBR feature
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
[Decal](../examples/3d/decal.rs) | Decal rendering
[Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines
[Depth of field](../examples/3d/depth_of_field.rs) | Demonstrates depth of field
[Fog](../examples/3d/fog.rs) | A scene showcasing the distance fog effect