This commit adds support for *decal projectors* to Bevy, allowing for textures to be projected on top of geometry. Decal projectors are clusterable objects, just as punctual lights and light probes are. This means that decals are only evaluated for objects within the conservative bounds of the projector, and they don't require a second pass. These clustered decals require support for bindless textures and as such currently don't work on WebGL 2, WebGPU, macOS, or iOS. For an alternative that doesn't require bindless, see PR #16600. I believe that both contact projective decals in #16600 and clustered decals are desirable to have in Bevy. Contact projective decals offer broader hardware and driver support, while clustered decals don't require the creation of bounding geometry. A new example, `decal_projectors`, has been added, which demonstrates multiple decals on a rotating object. The decal projectors can be scaled and rotated with the mouse. There are several limitations of this initial patch that can be addressed in follow-ups: 1. There's no way to specify the Z-index of decals. That is, the order in which multiple decals are blended on top of one another is arbitrary. A follow-up could introduce some sort of Z-index field so that artists can specify that some decals should be blended on top of others. 2. Decals don't take the normal of the surface they're projected onto into account. Most decal implementations in other engines have a feature whereby the angle between the decal projector and the normal of the surface must be within some threshold for the decal to appear. Often, artists can specify a fade-off range for a smooth transition between oblique surfaces and aligned surfaces. 3. There's no distance-based fadeoff toward the end of the projector range. Many decal implementations have this. This addresses #2401. ## Showcase 
108 lines
3.4 KiB
WebGPU Shading Language
108 lines
3.4 KiB
WebGPU Shading Language
#import bevy_pbr::{
|
|
pbr_types,
|
|
pbr_functions::alpha_discard,
|
|
pbr_fragment::pbr_input_from_standard_material,
|
|
decal::clustered::apply_decal_base_color,
|
|
}
|
|
|
|
#ifdef PREPASS_PIPELINE
|
|
#import bevy_pbr::{
|
|
prepass_io::{VertexOutput, FragmentOutput},
|
|
pbr_deferred_functions::deferred_output,
|
|
}
|
|
#else
|
|
#import bevy_pbr::{
|
|
forward_io::{VertexOutput, FragmentOutput},
|
|
pbr_functions,
|
|
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
|
|
pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT,
|
|
}
|
|
#endif
|
|
|
|
#ifdef MESHLET_MESH_MATERIAL_PASS
|
|
#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output
|
|
#endif
|
|
|
|
#ifdef OIT_ENABLED
|
|
#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
|
|
vertex_output: VertexOutput,
|
|
@builtin(front_facing) is_front: bool,
|
|
#endif
|
|
) -> FragmentOutput {
|
|
#ifdef MESHLET_MESH_MATERIAL_PASS
|
|
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);
|
|
|
|
// alpha discard
|
|
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
|
|
|
// clustered decals
|
|
pbr_input.material.base_color = apply_decal_base_color(
|
|
in.world_position.xyz,
|
|
in.position.xy,
|
|
pbr_input.material.base_color
|
|
);
|
|
|
|
#ifdef PREPASS_PIPELINE
|
|
// write the gbuffer, lighting pass id, and optionally normal and motion_vector textures
|
|
let out = deferred_output(in, pbr_input);
|
|
#else
|
|
// in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here.
|
|
// in deferred mode the lit color and these effects will be calculated in the deferred lighting shader
|
|
var out: FragmentOutput;
|
|
if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
|
|
out.color = apply_pbr_lighting(pbr_input);
|
|
} else {
|
|
out.color = pbr_input.material.base_color;
|
|
}
|
|
|
|
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
|
|
// note this does not include fullscreen postprocessing effects like bloom.
|
|
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
|
#endif
|
|
|
|
#ifdef OIT_ENABLED
|
|
let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
|
|
if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
|
|
// The fragments will only be drawn during the oit resolve pass.
|
|
oit_draw(in.position, out.color);
|
|
discard;
|
|
}
|
|
#endif // OIT_ENABLED
|
|
|
|
#ifdef FORWARD_DECAL
|
|
out.color.a = min(forward_decal_info.alpha, out.color.a);
|
|
#endif
|
|
|
|
return out;
|
|
}
|