From e8e24260580c1ee81cea50d135e3072549572bb5 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:31:30 -0800 Subject: [PATCH] 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 --- Cargo.toml | 11 ++ crates/bevy_pbr/src/decal/forward.rs | 128 +++++++++++++++++++ crates/bevy_pbr/src/decal/forward_decal.wgsl | 52 ++++++++ crates/bevy_pbr/src/decal/mod.rs | 10 ++ crates/bevy_pbr/src/extended_material.rs | 13 +- crates/bevy_pbr/src/lib.rs | 2 + crates/bevy_pbr/src/render/pbr.wgsl | 22 +++- crates/bevy_render/src/maths.wgsl | 6 + examples/3d/decal.rs | 96 ++++++++++++++ examples/README.md | 1 + 10 files changed, 336 insertions(+), 5 deletions(-) create mode 100644 crates/bevy_pbr/src/decal/forward.rs create mode 100644 crates/bevy_pbr/src/decal/forward_decal.wgsl create mode 100644 crates/bevy_pbr/src/decal/mod.rs create mode 100644 examples/3d/decal.rs diff --git a/Cargo.toml b/Cargo.toml index 21d3dbacdd..d491ab2244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs new file mode 100644 index 0000000000..e580ae922a --- /dev/null +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -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 = Handle::weak_from_u128(19376620402995522466); +const FORWARD_DECAL_SHADER_HANDLE: Handle = 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::(); + + app.world_mut().resource_mut::>().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::> { + 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 = ExtendedMaterial; + +/// 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 { + Some(AlphaMode::Blend) + } + + fn specialize( + _pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: MaterialExtensionKey, + ) -> 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, + } + } +} diff --git a/crates/bevy_pbr/src/decal/forward_decal.wgsl b/crates/bevy_pbr/src/decal/forward_decal.wgsl new file mode 100644 index 0000000000..dbc6bbc1c4 --- /dev/null +++ b/crates/bevy_pbr/src/decal/forward_decal.wgsl @@ -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 depth_fade_factor: f32; + +struct ForwardDecalInformation { + world_position: vec4, + uv: vec2, + 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); +} diff --git a/crates/bevy_pbr/src/decal/mod.rs b/crates/bevy_pbr/src/decal/mod.rs new file mode 100644 index 0000000000..e78f23c52a --- /dev/null +++ b/crates/bevy_pbr/src/decal/mod.rs @@ -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::*; diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 35cb1129d2..17ea201561 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -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 material’s AlphaMode. If None is returned, the base material alpha mode will be used. + fn alpha_mode() -> Option { + 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 Material for ExtendedMaterial { } } - 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 { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 3aa35561c2..5377dfba3b 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -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::::default(), SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 652fa5ac4e..12083f1b3a 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -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, #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; } diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index a9cb80c0fc..0098c8237c 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -93,3 +93,9 @@ fn sphere_intersects_plane_half_space( fn powsafe(color: vec3, power: f32) -> vec3 { return pow(abs(color), vec3(power)) * sign(color); } + +// https://en.wikipedia.org/wiki/Vector_projection#Vector_projection_2 +fn project_onto(lhs: vec3, rhs: vec3) -> vec3 { + let other_len_sq_rcp = 1.0 / dot(rhs, rhs); + return rhs * dot(lhs, rhs) * other_len_sq_rcp; +} diff --git a/examples/3d/decal.rs b/examples/3d/decal.rs new file mode 100644 index 0000000000..2f27af3bb6 --- /dev/null +++ b/examples/3d/decal.rs @@ -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>, + mut standard_materials: ResMut>, + mut decal_standard_materials: ResMut>>, + asset_server: Res, +) { + // 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), + )); +} diff --git a/examples/README.md b/examples/README.md index aef50d9627..c7a5d6b86d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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