diff --git a/Cargo.toml b/Cargo.toml index 74b0ebe4ae..b7ab3efcdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/assets/shaders/custom_clustered_decal.wgsl b/assets/shaders/custom_clustered_decal.wgsl index 6aaf408097..13f404cebb 100644 --- a/assets/shaders/custom_clustered_decal.wgsl +++ b/assets/shaders/custom_clustered_decal.wgsl @@ -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. @@ -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 diff --git a/crates/bevy_light/src/cluster/mod.rs b/crates/bevy_light/src/cluster/mod.rs index 92f1c5723e..17e3f1e418 100644 --- a/crates/bevy_light/src/cluster/mod.rs +++ b/crates/bevy_light/src/cluster/mod.rs @@ -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::)] 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, + pub base_color_texture: Option>, - /// An application-specific tag you can use for any purpose you want. + /// 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>, + + /// 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>, + + /// 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>, + + /// 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, diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index ad32c7f571..1acbd2fbf9 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -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::{ @@ -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 @@ -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, u32>, + texture_to_binding_index: HashMap, i32>, /// The information concerning each decal that we provide to the shader. decals: Vec, /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the @@ -87,18 +94,22 @@ impl RenderClusteredDecals { pub fn insert_decal( &mut self, entity: Entity, - image: &AssetId, + images: [Option>; 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); } @@ -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. @@ -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 }, ); @@ -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 { @@ -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) -> u32 { + fn get_or_insert_image(&mut self, image_id: &AssetId) -> 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 }) diff --git a/crates/bevy_pbr/src/decal/clustered.wgsl b/crates/bevy_pbr/src/decal/clustered.wgsl index 874722a2ab..a6404ecde0 100644 --- a/crates/bevy_pbr/src/decal/clustered.wgsl +++ b/crates/bevy_pbr/src/decal/clustered.wgsl @@ -31,14 +31,31 @@ #import bevy_pbr::clustered_forward #import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges #import bevy_pbr::mesh_view_bindings +#import bevy_pbr::pbr_functions +#import bevy_pbr::pbr_types::PbrInput +#import bevy_pbr::utils::porter_duff_over #import bevy_render::maths +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else +#import bevy_pbr::forward_io::VertexOutput +#endif + // An object that allows stepping through all clustered decals that affect a // single fragment. struct ClusteredDecalIterator { // Public fields follow: - // The index of the decal texture in the binding array. - texture_index: i32, + // The index of the decal base color texture in the binding array. + base_color_texture_index: i32, + // The index of the decal normal map texture in the binding array. + normal_map_texture_index: i32, + // The index of the decal metallic-roughness texture in the binding array. + metallic_roughness_texture_index: i32, + // The index of the decal emissive texture in the binding array. + emissive_texture_index: i32, // The UV coordinates at which to sample that decal texture. uv: vec2, // A custom tag you can use for your own purposes. @@ -71,6 +88,9 @@ fn clustered_decal_iterator_new( clusterable_object_index_ranges: ptr ) -> ClusteredDecalIterator { return ClusteredDecalIterator( + -1, + -1, + -1, -1, vec2(0.0), 0u, @@ -103,8 +123,20 @@ fn clustered_decal_iterator_next(iterator: ptr vec4((*iterator).world_position, 1.0)).xyz; if (all(decal_space_vector >= vec3(-0.5)) && all(decal_space_vector <= vec3(0.5))) { - (*iterator).texture_index = - i32(mesh_view_bindings::clustered_decals.decals[decal_index].image_index); + (*iterator).base_color_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].base_color_texture_index + ); + (*iterator).normal_map_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].normal_map_texture_index + ); + (*iterator).metallic_roughness_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[ + decal_index + ].metallic_roughness_texture_index + ); + (*iterator).emissive_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].emissive_texture_index + ); (*iterator).uv = decal_space_vector.xy * vec2(1.0, -1.0) + vec2(0.5); (*iterator).tag = mesh_view_bindings::clustered_decals.decals[decal_index].tag; @@ -135,17 +167,16 @@ fn view_is_orthographic() -> bool { return mesh_view_bindings::view.clip_from_view[3].w == 1.0; } -// Modifies the base color at the given position to account for decals. -// -// Returns the new base color with decals taken into account. If no decals -// overlap the current world position, returns the supplied base color -// unmodified. -fn apply_decal_base_color( - world_position: vec3, - frag_coord: vec2, - initial_base_color: vec4, -) -> vec4 { - var base_color = initial_base_color; +fn apply_decals(pbr_input: ptr) { + let world_position = (*pbr_input).world_position.xyz; + let world_normal = (*pbr_input).world_normal; + let frag_coord = (*pbr_input).frag_coord.xy; + + var base_color = (*pbr_input).material.base_color; + var emissive = (*pbr_input).material.emissive; + var Nt = (*pbr_input).N; + var metallic = (*pbr_input).material.metallic; + var perceptual_roughness = (*pbr_input).material.perceptual_roughness; #ifdef CLUSTERED_DECALS_ARE_USABLE // Fetch the clusterable object index ranges for this world position. @@ -162,22 +193,73 @@ fn apply_decal_base_color( var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); while (clustered_decal_iterator_next(&iterator)) { - // Sample the current decal. - let decal_base_color = textureSampleLevel( - mesh_view_bindings::clustered_decal_textures[iterator.texture_index], - mesh_view_bindings::clustered_decal_sampler, - iterator.uv, - 0.0 - ); + // Apply base color and metallic/roughness. + if (iterator.base_color_texture_index >= 0) { + let decal_base_color = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.base_color_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); - // Blend with the accumulated fragment. - base_color = vec4( - mix(base_color.rgb, decal_base_color.rgb, decal_base_color.a), - base_color.a + decal_base_color.a - ); + // Apply the metallic (blue channel) and the roughness (green channel) map. + if (iterator.metallic_roughness_texture_index >= 0) { + let metallic_roughness_sampler = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[ + iterator.metallic_roughness_texture_index + ], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + // Use OVER compositing using the base color alpha. + metallic = mix( + metallic * base_color.a, + metallic_roughness_sampler.b, + decal_base_color.a + ); + perceptual_roughness = mix( + perceptual_roughness * base_color.a, + metallic_roughness_sampler.g, + decal_base_color.a + ); + } + + // Apply base color with the standard OVER compositing operator. + base_color = porter_duff_over(base_color, decal_base_color); + } + +#ifdef VERTEX_TANGENTS + if (iterator.normal_map_texture_index >= 0) { + let Nd = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.normal_map_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ).rgb * 2.0 - 1.0; + // This is the *Whiteout* normal map blending operator from [1]. + // + // [1]: https://blog.selfshadow.com/publications/blending-in-detail/ + Nt = vec3(Nt.xy + Nd.xy, Nt.z * Nd.z); + } +#endif // VERTEX_TANGENTS + + // Apply emissive. + if (iterator.emissive_texture_index >= 0) { + let decal_emissive = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.emissive_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + emissive += vec4(decal_emissive.rgb, 0.0); + } } #endif // CLUSTERED_DECALS_ARE_USABLE - return base_color; + (*pbr_input).material.base_color = base_color; + (*pbr_input).material.emissive = emissive; + (*pbr_input).N = normalize(Nt); + (*pbr_input).material.metallic = metallic; + (*pbr_input).material.perceptual_roughness = perceptual_roughness; } - diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 19f87b3794..112556ee1e 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -176,10 +176,14 @@ struct OrderIndependentTransparencySettings { struct ClusteredDecal { local_from_world: mat4x4, - image_index: i32, + base_color_texture_index: i32, + normal_map_texture_index: i32, + metallic_roughness_texture_index: i32, + emissive_texture_index: i32, tag: u32, pad_a: u32, pad_b: u32, + pad_c: u32, } struct ClusteredDecals { diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index e4d8fe6379..ea2b33ee0a 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -2,7 +2,7 @@ pbr_types, pbr_functions::alpha_discard, pbr_fragment::pbr_input_from_standard_material, - decal::clustered::apply_decal_base_color, + decal::clustered::apply_decals, } #ifdef PREPASS_PIPELINE @@ -69,11 +69,7 @@ fn fragment( 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 - ); + apply_decals(&pbr_input); #ifdef PREPASS_PIPELINE // write the gbuffer, lighting pass id, and optionally normal and motion_vector textures diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 8e91aeb9c0..706e30fb0b 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -203,3 +203,9 @@ fn dir_to_cube_uv(dir: vec3f) -> CubeUV { // Convert from [-1,1] to [0,1] return CubeUV(uv * 0.5 + 0.5, face); } + +// The Porter-Duff OVER operator on RGBA, correctly computing alpha of the +// result. +fn porter_duff_over(bg: vec4, fg: vec4) -> vec4 { + return vec4(mix(bg.rgb * bg.a, fg.rgb, fg.a), bg.a + fg.a * (1.0 - bg.a)); +} diff --git a/examples/3d/clustered_decal_maps.rs b/examples/3d/clustered_decal_maps.rs new file mode 100644 index 0000000000..f72aa1df40 --- /dev/null +++ b/examples/3d/clustered_decal_maps.rs @@ -0,0 +1,445 @@ +//! Demonstrates the normal map, metallic-roughness map, and emissive features +//! of clustered decals. + +use std::{f32::consts::PI, time::Duration}; + +use bevy::{ + asset::io::web::WebAssetPlugin, + color::palettes::css::{CRIMSON, GOLD}, + image::ImageLoaderSettings, + light::ClusteredDecal, + prelude::*, + render::view::Hdr, +}; +use rand::Rng; + +use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender}; + +#[path = "../helpers/widgets.rs"] +mod widgets; + +/// The demonstration textures that we use. +/// +/// We cache these for efficiency. +#[derive(Resource)] +struct AppTextures { + /// The base color that all our decals have (the Bevy logo). + decal_base_color_texture: Handle, + + /// A normal map that all our decals have. + /// + /// This provides a nice raised embossed look. + decal_normal_map_texture: Handle, + + /// The metallic-roughness map that all our decals have. + /// + /// Metallic is in the blue channel and roughness is in the green channel, + /// like glTF requires. + decal_metallic_roughness_map_texture: Handle, + + /// The emissive texture that can optionally be enabled. + /// + /// This causes the white bird to glow. + decal_emissive_texture: Handle, +} + +impl FromWorld for AppTextures { + fn from_world(world: &mut World) -> Self { + // Load all the decal textures. + let asset_server = world.resource::(); + AppTextures { + decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"), + decal_normal_map_texture: asset_server.load_with_settings( + get_web_asset_url("BevyLogo-Normal.png"), + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ), + decal_metallic_roughness_map_texture: asset_server.load_with_settings( + get_web_asset_url("BevyLogo-MetallicRoughness.png"), + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ), + decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")), + } + } +} + +/// A component that we place on our decals to track them for animation +/// purposes. +#[derive(Component)] +struct ExampleDecal { + /// The width and height of the square decal in meters. + size: f32, + /// What state the decal is in (animating in, idling, or animating out). + state: ExampleDecalState, +} + +/// The animation state of a decal. +/// +/// When each [`Timer`] goes off, the decal advances to the next state. +enum ExampleDecalState { + /// The decal has just been spawned and is animating in. + AnimatingIn(Timer), + /// The decal has animated in and is waiting to animate out. + Idling(Timer), + /// The decal is animating out. + /// + /// When this timer expires, the decal is despawned. + AnimatingOut(Timer), +} + +/// All settings that the user can change. +/// +/// This app only has one: whether newly-spawned decals are emissive. +#[derive(Clone, Copy, PartialEq)] +enum AppSetting { + /// True if newly-spawned decals have an emissive channel (i.e. they glow), + /// or false otherwise. + EmissiveDecals(bool), +} + +/// The current values of the settings that the user can change. +/// +/// This app only has one: whether newly-spawned decals are emissive. +#[derive(Default, Resource)] +struct AppStatus { + /// True if newly-spawned decals have an emissive channel (i.e. they glow), + /// or false otherwise. + emissive_decals: bool, +} + +/// Half of the width and height of the plane onto which the decals are +/// projected. +const PLANE_HALF_SIZE: f32 = 2.0; +/// The minimum width and height that a decal may have. +/// +/// The actual size is determined randomly, using this value as a lower bound. +const DECAL_MIN_SIZE: f32 = 0.5; +/// The maximum width and height that a decal may have. +/// +/// The actual size is determined randomly, using this value as an upper bound. +const DECAL_MAX_SIZE: f32 = 1.5; + +/// How long it takes the decal to grow to its full size when animating in. +const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300); +/// How long a decal stays in the idle state before starting to animate out. +const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10); +/// How long it takes the decal to shrink down to nothing when animating out. +const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300); + +/// The demo entry point. +fn main() { + App::new() + .add_plugins( + DefaultPlugins + .set(WebAssetPlugin { + silence_startup_warning: true, + }) + .set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Clustered Decal Maps Example".into(), + ..default() + }), + ..default() + }), + ) + .add_message::>() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup) + .add_systems(Update, draw_gizmos) + .add_systems(Update, spawn_decal) + .add_systems(Update, animate_decals) + .add_systems( + Update, + ( + widgets::handle_ui_interactions::, + update_radio_buttons, + ), + ) + .add_systems( + Update, + handle_emission_type_change.after(widgets::handle_ui_interactions::), + ) + .run(); +} + +/// Spawns all the objects in the scene. +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + spawn_plane_mesh(&mut commands, &asset_server, &mut meshes, &mut materials); + spawn_light(&mut commands); + spawn_camera(&mut commands); + spawn_buttons(&mut commands); +} + +/// Spawns the plane onto which the decals are projected. +fn spawn_plane_mesh( + commands: &mut Commands, + asset_server: &AssetServer, + meshes: &mut Assets, + materials: &mut Assets, +) { + // Create a plane onto which we project decals. + // + // As the plane has a normal map, we must generate tangents for the + // vertices. + let plane_mesh = meshes.add( + Plane3d { + normal: Dir3::NEG_Z, + half_size: Vec2::splat(PLANE_HALF_SIZE), + } + .mesh() + .build() + .with_duplicated_vertices() + .with_computed_flat_normals() + .with_generated_tangents() + .unwrap(), + ); + + // Give the plane some texture. + // + // Note that, as this is a normal map, we must disable sRGB when loading. + let normal_map_texture = asset_server.load_with_settings( + "textures/ScratchedGold-Normal.png", + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ); + + // Actually spawn the plane. + commands.spawn(( + Mesh3d(plane_mesh), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::from(CRIMSON), + normal_map_texture: Some(normal_map_texture), + ..StandardMaterial::default() + })), + Transform::IDENTITY, + )); +} + +/// Spawns a light to illuminate the scene. +fn spawn_light(commands: &mut Commands) { + commands.spawn(( + PointLight { + intensity: 10_000_000., + range: 100.0, + ..default() + }, + Transform::from_xyz(8.0, 16.0, -8.0), + )); +} + +/// Spawns a camera. +fn spawn_camera(commands: &mut Commands) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(2.0, 0.0, -7.0).looking_at(Vec3::ZERO, Vec3::Y), + Hdr, + )); +} + +/// Spawns all the buttons at the bottom of the screen. +fn spawn_buttons(commands: &mut Commands) { + commands.spawn(( + widgets::main_ui_node(), + children![widgets::option_buttons( + "Emissive Decals", + &[ + (AppSetting::EmissiveDecals(true), "On"), + (AppSetting::EmissiveDecals(false), "Off"), + ], + ),], + )); +} + +/// Draws the outlines that show the bounds of the clustered decals. +fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With>) { + for global_transform in &decals { + gizmos.primitive_3d( + &Cuboid { + // Since the clustered decal is a 1×1×1 cube in model space, its + // half-size is half of the scaling part of its transform. + half_size: global_transform.scale() * 0.5, + }, + Isometry3d { + rotation: global_transform.rotation(), + translation: global_transform.translation_vec3a(), + }, + GOLD, + ); + } +} + +/// A system that spawns new decals at fixed intervals. +fn spawn_decal( + mut commands: Commands, + app_status: Res, + app_textures: Res, + time: Res