Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4900,3 +4900,15 @@ name = "Pan Camera"
description = "Example Pan-Camera Styled Camera Controller for 2D scenes"
category = "Camera"
wasm = true

[[example]]
name = "clustered_decal_maps"
path = "examples/3d/clustered_decal_maps.rs"
doc-scrape-examples = true
required-features = ["pbr_clustered_decals", "https"]

[package.metadata.example.clustered_decal_maps]
name = "Clustered Decal Maps"
description = "Demonstrates normal and metallic-roughness maps of decals"
category = "3D Rendering"
wasm = false
8 changes: 2 additions & 6 deletions assets/shaders/custom_clustered_decal.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ fn fragment(
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
);
clustered::apply_decals(&pbr_input);

// 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.
Expand All @@ -42,7 +38,7 @@ fn fragment(
);
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_textures[decal_iterator.base_color_texture_index],
mesh_view_bindings::clustered_decal_sampler,
decal_iterator.uv,
0.0
Expand Down
59 changes: 52 additions & 7 deletions crates/bevy_light/src/cluster/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,26 +148,71 @@ pub struct ClusterableObjectCounts {
/// An object that projects a decal onto surfaces within its bounds.
///
/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus
/// you may find [`Transform::looking_at`] useful).
/// projects its images onto surfaces in the -Z direction (thus you may find
/// [`Transform::looking_at`] useful).
///
/// Each decal may project any of a base color texture, a normal map, a
/// metallic/roughness map, and/or a texture that specifies emissive light. In
/// addition, you may associate an arbitrary integer [`Self::tag`] with each
/// clustered decal, which Bevy doesn't use, but that you can use in your
/// shaders in order to associate application-specific data with your decals.
///
/// Clustered decals are the highest-quality types of decals that Bevy supports,
/// but they require bindless textures. This means that they presently can't be
/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
/// with forward or deferred rendering and don't require a prepass.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Debug, Clone)]
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Debug, Clone, Default)]
#[require(Transform, Visibility, VisibilityClass)]
#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
pub struct ClusteredDecal {
/// The image that the clustered decal projects.
/// The image that the clustered decal projects onto the base color of the
/// surface material.
///
/// This must be a 2D image. If it has an alpha channel, it'll be alpha
/// blended with the underlying surface and/or other decals. All decal
/// images in the scene must use the same sampler.
pub image: Handle<Image>,
pub base_color_texture: Option<Handle<Image>>,

/// The normal map that the clustered decal projects onto surfaces.
///
/// Bevy uses the *Whiteout* method to combine normal maps from decals with
/// any normal map that the surface has, as described in the
/// [*Blending in Detail* article].
///
/// Note that the normal map must be three-channel and must be in OpenGL
/// format, not DirectX format. That is, the green channel must point up,
/// not down.
///
/// [*Blending in Detail* article]: https://blog.selfshadow.com/publications/blending-in-detail/
pub normal_map_texture: Option<Handle<Image>>,

/// The metallic-roughness map that the clustered decal projects onto
/// surfaces.
///
/// Metallic and roughness PBR parameters are blended onto the base surface
/// using the alpha channel of the base color.
///
/// Metallic is expected to be in the blue channel, while roughness is
/// expected to be in the green channel, following glTF conventions.
pub metallic_roughness_texture: Option<Handle<Image>>,

/// The emissive map that the clustered decal projects onto surfaces.
///
/// Including this texture effectively causes the decal to glow. The
/// emissive component is blended onto the surface according to the alpha
/// channel.
pub emissive_texture: Option<Handle<Image>>,

/// An application-specific tag you can use for any purpose you want.
/// An application-specific tag you can use for any purpose you want, in
/// conjunction with a custom shader.
///
/// This value is exposed to the shader via the iterator API
/// (`bevy_pbr::decal::clustered::clustered_decal_iterator_new` and
/// `bevy_pbr::decal::clustered::clustered_decal_iterator_next`).
///
/// For example, you might use the tag to restrict the set of surfaces to
/// which a decal can be rendered.
///
/// See the `clustered_decals` example for an example of use.
pub tag: u32,
Expand Down
141 changes: 117 additions & 24 deletions crates/bevy_pbr/src/decal/clustered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@
//! used on WebGL 2 or WebGPU. Bevy's clustered decals can be used
//! with forward or deferred rendering and don't require a prepass.
//!
//! On their own, clustered decals only project the base color of a texture. You
//! can, however, use the built-in *tag* field to customize the appearance of a
//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for
//! more information and the `clustered_decals` example for an example of use.
//! Each clustered decal may contain up to 4 textures. By default, the 4
//! textures correspond to the base color, a normal map, a metallic-roughness
//! map, and an emissive map respectively. However, with a custom shader, you
//! can use these 4 textures for whatever you wish. Additionally, you can use
//! the built-in *tag* field to store additional application-specific data; by
//! reading the tag in the shader, you can modify the appearance of a clustered
//! decal arbitrarily. See the documentation in `clustered.wgsl` for more
//! information and the `clustered_decals` example for an example of use.

use core::{num::NonZero, ops::Deref};

use bevy_app::{App, Plugin};
use bevy_asset::AssetId;
use bevy_asset::{AssetId, Handle};
use bevy_camera::visibility::ViewVisibility;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
Expand Down Expand Up @@ -50,6 +54,9 @@ use bytemuck::{Pod, Zeroable};

use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta};

/// The number of textures that can be associated with each clustered decal.
const IMAGES_PER_DECAL: usize = 4;

/// A plugin that adds support for clustered decals.
///
/// In environments where bindless textures aren't available, clustered decals
Expand All @@ -66,7 +73,7 @@ pub struct RenderClusteredDecals {
/// Maps a decal image to the shader binding array.
///
/// [`Self::binding_index_to_textures`] holds the inverse mapping.
texture_to_binding_index: HashMap<AssetId<Image>, u32>,
texture_to_binding_index: HashMap<AssetId<Image>, i32>,
/// The information concerning each decal that we provide to the shader.
decals: Vec<RenderClusteredDecal>,
/// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
Expand All @@ -87,18 +94,22 @@ impl RenderClusteredDecals {
pub fn insert_decal(
&mut self,
entity: Entity,
image: &AssetId<Image>,
images: [Option<AssetId<Image>>; IMAGES_PER_DECAL],
local_from_world: Mat4,
tag: u32,
) {
let image_index = self.get_or_insert_image(image);
let image_indices = images.map(|maybe_image_id| match maybe_image_id {
Some(ref image_id) => self.get_or_insert_image(image_id),
None => -1,
});
let decal_index = self.decals.len();
self.decals.push(RenderClusteredDecal {
local_from_world,
image_index,
image_indices,
tag,
pad_a: 0,
pad_b: 0,
pad_c: 0,
});
self.entity_to_decal_index.insert(entity, decal_index);
}
Expand Down Expand Up @@ -183,14 +194,23 @@ pub struct RenderClusteredDecal {
/// The shader uses this in order to back-transform world positions into
/// model space.
local_from_world: Mat4,
/// The index of the decal texture in the binding array.
image_index: u32,
/// The index of each decal texture in the binding array.
///
/// These are in the order of the base color texture, the normal map
/// texture, the metallic-roughness map texture, and finally the emissive
/// texture.
///
/// If the decal doesn't have a texture assigned to a slot, the index at
/// that slot will be -1.
image_indices: [i32; 4],
/// A custom tag available for application-defined purposes.
tag: u32,
/// Padding.
pad_a: u32,
/// Padding.
pad_b: u32,
/// Padding.
pad_c: u32,
}

/// Extracts decals from the main world into the render world.
Expand Down Expand Up @@ -232,58 +252,129 @@ pub fn extract_decals(
// Clear out the `RenderDecals` in preparation for a new frame.
render_decals.clear();

extract_clustered_decals(&decals, &mut render_decals);
extract_spot_light_textures(&spot_light_textures, &mut render_decals);
extract_point_light_textures(&point_light_textures, &mut render_decals);
extract_directional_light_textures(&directional_light_textures, &mut render_decals);
}

/// Extracts all clustered decals and light textures from the scene and transfers
/// them to the render world.
fn extract_clustered_decals(
decals: &Extract<
Query<(
RenderEntity,
&ClusteredDecal,
&GlobalTransform,
&ViewVisibility,
)>,
>,
render_decals: &mut RenderClusteredDecals,
) {
// Loop over each decal.
for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals {
for (decal_entity, clustered_decal, global_transform, view_visibility) in decals {
// If the decal is invisible, skip it.
if !view_visibility.get() {
continue;
}

// Insert the decal, grabbing the ID of every associated texture as we
// do.
render_decals.insert_decal(
decal_entity,
&clustered_decal.image.id(),
[
clustered_decal.base_color_texture.as_ref().map(Handle::id),
clustered_decal.normal_map_texture.as_ref().map(Handle::id),
clustered_decal
.metallic_roughness_texture
.as_ref()
.map(Handle::id),
clustered_decal.emissive_texture.as_ref().map(Handle::id),
],
global_transform.affine().inverse().into(),
clustered_decal.tag,
);
}
}

for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures {
// If the decal is invisible, skip it.
/// Extracts all textures from spot lights from the main world to the render
/// world as clustered decals.
fn extract_spot_light_textures(
spot_light_textures: &Extract<
Query<(
RenderEntity,
&SpotLightTexture,
&GlobalTransform,
&ViewVisibility,
)>,
>,
render_decals: &mut RenderClusteredDecals,
) {
for (decal_entity, texture, global_transform, view_visibility) in spot_light_textures {
// If the texture is invisible, skip it.
if !view_visibility.get() {
continue;
}

render_decals.insert_decal(
decal_entity,
&texture.image.id(),
[Some(texture.image.id()), None, None, None],
global_transform.affine().inverse().into(),
0,
);
}
}

for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures {
// If the decal is invisible, skip it.
/// Extracts all textures from point lights from the main world to the render
/// world as clustered decals.
fn extract_point_light_textures(
point_light_textures: &Extract<
Query<(
RenderEntity,
&PointLightTexture,
&GlobalTransform,
&ViewVisibility,
)>,
>,
render_decals: &mut RenderClusteredDecals,
) {
for (decal_entity, texture, global_transform, view_visibility) in point_light_textures {
// If the texture is invisible, skip it.
if !view_visibility.get() {
continue;
}

render_decals.insert_decal(
decal_entity,
&texture.image.id(),
[Some(texture.image.id()), None, None, None],
global_transform.affine().inverse().into(),
texture.cubemap_layout as u32,
);
}
}

for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures {
// If the decal is invisible, skip it.
/// Extracts all textures from directional lights from the main world to the
/// render world as clustered decals.
fn extract_directional_light_textures(
directional_light_textures: &Extract<
Query<(
RenderEntity,
&DirectionalLightTexture,
&GlobalTransform,
&ViewVisibility,
)>,
>,
render_decals: &mut RenderClusteredDecals,
) {
for (decal_entity, texture, global_transform, view_visibility) in directional_light_textures {
// If the texture is invisible, skip it.
if !view_visibility.get() {
continue;
}

render_decals.insert_decal(
decal_entity,
&texture.image.id(),
[Some(texture.image.id()), None, None, None],
global_transform.affine().inverse().into(),
if texture.tiled { 1 } else { 0 },
);
Expand Down Expand Up @@ -376,6 +467,8 @@ impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
while texture_views.len() < max_view_decals as usize {
texture_views.push(&*fallback_image.d2.texture_view);
}
} else if texture_views.is_empty() {
texture_views.push(&*fallback_image.d2.texture_view);
}

Some(RenderViewClusteredDecalBindGroupEntries {
Expand All @@ -389,12 +482,12 @@ impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
impl RenderClusteredDecals {
/// Returns the index of the given image in the decal texture binding array,
/// adding it to the list if necessary.
fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> u32 {
fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> i32 {
*self
.texture_to_binding_index
.entry(*image_id)
.or_insert_with(|| {
let index = self.binding_index_to_textures.len() as u32;
let index = self.binding_index_to_textures.len() as i32;
self.binding_index_to_textures.push(*image_id);
index
})
Expand Down
Loading