
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 
87 lines
3.1 KiB
WebGPU Shading Language
87 lines
3.1 KiB
WebGPU Shading Language
// This shader, a part of the `clustered_decals` example, shows how to use the
|
|
// decal `tag` field to apply arbitrary decal effects.
|
|
|
|
#import bevy_pbr::{
|
|
clustered_forward,
|
|
decal::clustered,
|
|
forward_io::{VertexOutput, FragmentOutput},
|
|
mesh_view_bindings,
|
|
pbr_fragment::pbr_input_from_standard_material,
|
|
pbr_functions::{alpha_discard, apply_pbr_lighting, main_pass_post_lighting_processing},
|
|
}
|
|
|
|
@fragment
|
|
fn fragment(
|
|
in: VertexOutput,
|
|
@builtin(front_facing) is_front: bool,
|
|
) -> FragmentOutput {
|
|
// 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);
|
|
|
|
// Apply the normal decals.
|
|
pbr_input.material.base_color = clustered::apply_decal_base_color(
|
|
in.world_position.xyz,
|
|
in.position.xy,
|
|
pbr_input.material.base_color
|
|
);
|
|
|
|
// Here we tint the color based on the tag of the decal.
|
|
// We could optionally do other things, such as adjust the normal based on a normal map.
|
|
let view_z = clustered::get_view_z(in.world_position.xyz);
|
|
let is_orthographic = clustered::view_is_orthographic();
|
|
let cluster_index =
|
|
clustered_forward::fragment_cluster_index(in.position.xy, view_z, is_orthographic);
|
|
var clusterable_object_index_ranges =
|
|
clustered_forward::unpack_clusterable_object_index_ranges(cluster_index);
|
|
var decal_iterator = clustered::clustered_decal_iterator_new(
|
|
in.world_position.xyz,
|
|
&clusterable_object_index_ranges
|
|
);
|
|
while (clustered::clustered_decal_iterator_next(&decal_iterator)) {
|
|
var decal_base_color = textureSampleLevel(
|
|
mesh_view_bindings::clustered_decal_textures[decal_iterator.texture_index],
|
|
mesh_view_bindings::clustered_decal_sampler,
|
|
decal_iterator.uv,
|
|
0.0
|
|
);
|
|
|
|
switch (decal_iterator.tag) {
|
|
case 1u: {
|
|
// Tint with red.
|
|
decal_base_color = vec4(
|
|
mix(pbr_input.material.base_color.rgb, vec3(1.0, 0.0, 0.0), 0.5),
|
|
decal_base_color.a,
|
|
);
|
|
}
|
|
case 2u: {
|
|
// Tint with blue.
|
|
decal_base_color = vec4(
|
|
mix(pbr_input.material.base_color.rgb, vec3(0.0, 0.0, 1.0), 0.5),
|
|
decal_base_color.a,
|
|
);
|
|
}
|
|
default: {}
|
|
}
|
|
|
|
pbr_input.material.base_color = vec4(
|
|
mix(pbr_input.material.base_color.rgb, decal_base_color.rgb, decal_base_color.a),
|
|
pbr_input.material.base_color.a + decal_base_color.a
|
|
);
|
|
}
|
|
|
|
// Apply lighting.
|
|
var out: FragmentOutput;
|
|
out.color = apply_pbr_lighting(pbr_input);
|
|
|
|
// 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);
|
|
|
|
return out;
|
|
}
|
|
|