mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
Add support for normal maps, metallic-roughness maps, and emissive maps to clustered decals. (#22039)
This commit expands the number of textures associated with each clustered decal from 1 to 4. The additional 3 textures apply normal maps, metallic-roughness maps, and emissive maps respectively to the surfaces onto which decals are projected. Normal maps are combined using the [*Whiteout* blending method] from SIGGRAPH 2007. This approach was chosen because, subjectively, it appeared better than the more complex [*reoriented normal mapping* (RNM)] approach. Additionally, *Whiteout* normal map blending is commutative and associative, unlike RNM, which is a useful property for our decals, which are currently applied in an unspecified order. (The fact that the order in which our decals are applied is unspecified is unfortunate, but is a long-standing issue and should probably be fixed in a followup.) In particular, commutativity is desirable because otherwise one must specify which normal map is the *base* normal map and which normal map is the *detail* normal map, but that's not a policy decision that Bevy can unconditionally make, as decals aren't necessary more detailed than the base normal map. (For instance, consider a bullet hole decal embedded in a wall with a subtle rough texture; one might reasonably argue that the base material's normal map is the detail map and the bullet hole is the base map, even though the bullet hole's normal map comes from a decal.) Note that, with a custom material shader, it's possible for application code to use the decal images for arbitrary other purposes. For example, with a custom shader an application might use the metallic-roughness map as a clearcoat map instead if it has no need for a metallic-roughness map on a decal. And, of course, a custom material shader could adopt RNM blending for decals if it wishes. A new example, `clustered_decal_maps`, has been added. This example demonstrates the new maps by spawning clustered decals with maps randomly over time and projecting them onto a wall. <img width="2564" height="1500" alt="Screenshot 2025-12-05 095953" src="https://github.com/user-attachments/assets/255fca64-2b42-4794-a367-14336d023310" />
This commit is contained in:
+12
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
/// 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<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, 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,
|
||||
|
||||
@@ -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<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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<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
|
||||
})
|
||||
|
||||
@@ -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<f32>,
|
||||
// A custom tag you can use for your own purposes.
|
||||
@@ -71,6 +88,9 @@ fn clustered_decal_iterator_new(
|
||||
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>
|
||||
) -> ClusteredDecalIterator {
|
||||
return ClusteredDecalIterator(
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
vec2(0.0),
|
||||
0u,
|
||||
@@ -103,8 +123,20 @@ fn clustered_decal_iterator_next(iterator: ptr<function, ClusteredDecalIterator>
|
||||
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<f32>,
|
||||
frag_coord: vec2<f32>,
|
||||
initial_base_color: vec4<f32>,
|
||||
) -> vec4<f32> {
|
||||
var base_color = initial_base_color;
|
||||
fn apply_decals(pbr_input: ptr<function, PbrInput>) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,10 +176,14 @@ struct OrderIndependentTransparencySettings {
|
||||
|
||||
struct ClusteredDecal {
|
||||
local_from_world: mat4x4<f32>,
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<f32>, fg: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(mix(bg.rgb * bg.a, fg.rgb, fg.a), bg.a + fg.a * (1.0 - bg.a));
|
||||
}
|
||||
|
||||
@@ -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<Image>,
|
||||
|
||||
/// A normal map that all our decals have.
|
||||
///
|
||||
/// This provides a nice raised embossed look.
|
||||
decal_normal_map_texture: Handle<Image>,
|
||||
|
||||
/// 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<Image>,
|
||||
|
||||
/// The emissive texture that can optionally be enabled.
|
||||
///
|
||||
/// This causes the white bird to glow.
|
||||
decal_emissive_texture: Handle<Image>,
|
||||
}
|
||||
|
||||
impl FromWorld for AppTextures {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
// Load all the decal textures.
|
||||
let asset_server = world.resource::<AssetServer>();
|
||||
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::<WidgetClickEvent<AppSetting>>()
|
||||
.init_resource::<AppStatus>()
|
||||
.init_resource::<AppTextures>()
|
||||
.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::<AppSetting>,
|
||||
update_radio_buttons,
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_emission_type_change.after(widgets::handle_ui_interactions::<AppSetting>),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Spawns all the objects in the scene.
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<Mesh>,
|
||||
materials: &mut Assets<StandardMaterial>,
|
||||
) {
|
||||
// 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<ClusteredDecal>>) {
|
||||
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<AppStatus>,
|
||||
app_textures: Res<AppTextures>,
|
||||
time: Res<Time>,
|
||||
mut decal_spawn_timer: Local<Option<Timer>>,
|
||||
) {
|
||||
// Tick the decal spawn timer. Check to see if we should spawn a new decal,
|
||||
// and bail out if it's not yet time to.
|
||||
let decal_spawn_timer = decal_spawn_timer
|
||||
.get_or_insert_with(|| Timer::new(Duration::from_millis(1000), TimerMode::Repeating));
|
||||
decal_spawn_timer.tick(time.delta());
|
||||
if !decal_spawn_timer.just_finished() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a random position along the plane.
|
||||
let mut rng = rand::rng();
|
||||
let decal_position = vec3(
|
||||
rng.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
|
||||
rng.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
|
||||
0.0,
|
||||
);
|
||||
|
||||
// Generate a random size for the decal.
|
||||
let decal_size = rng.random_range(DECAL_MIN_SIZE..DECAL_MAX_SIZE);
|
||||
|
||||
// Generate a random rotation for the decal.
|
||||
let theta = rng.random_range(0.0f32..PI);
|
||||
|
||||
// Now spawn the decal.
|
||||
commands.spawn((
|
||||
// Apply the textures.
|
||||
ClusteredDecal {
|
||||
base_color_texture: Some(app_textures.decal_base_color_texture.clone()),
|
||||
normal_map_texture: Some(app_textures.decal_normal_map_texture.clone()),
|
||||
metallic_roughness_texture: Some(
|
||||
app_textures.decal_metallic_roughness_map_texture.clone(),
|
||||
),
|
||||
emissive_texture: if app_status.emissive_decals {
|
||||
Some(app_textures.decal_emissive_texture.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..ClusteredDecal::default()
|
||||
},
|
||||
// Spawn the decal at the right place. Note that the scale is initially
|
||||
// zero; we'll animate it later.
|
||||
Transform::from_translation(decal_position)
|
||||
.with_scale(Vec3::ZERO)
|
||||
.looking_to(Vec3::Z, Vec3::ZERO.with_xy(Vec2::from_angle(theta))),
|
||||
// Create the component that tracks the animation state.
|
||||
ExampleDecal {
|
||||
size: decal_size,
|
||||
state: ExampleDecalState::AnimatingIn(Timer::new(
|
||||
DECAL_ANIMATE_IN_DURATION,
|
||||
TimerMode::Once,
|
||||
)),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// A system that animates the decals growing as they enter and shrinking as
|
||||
/// they leave.
|
||||
fn animate_decals(
|
||||
mut commands: Commands,
|
||||
mut decals_query: Query<(Entity, &mut ExampleDecal, &mut Transform)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (decal_entity, mut example_decal, mut decal_transform) in decals_query.iter_mut() {
|
||||
// Update the animation timers, and advance the animation state if the
|
||||
// timer has expired.
|
||||
match example_decal.state {
|
||||
ExampleDecalState::AnimatingIn(ref mut timer) => {
|
||||
timer.tick(time.delta());
|
||||
if timer.just_finished() {
|
||||
example_decal.state =
|
||||
ExampleDecalState::Idling(Timer::new(DECAL_IDLE_DURATION, TimerMode::Once));
|
||||
}
|
||||
}
|
||||
ExampleDecalState::Idling(ref mut timer) => {
|
||||
timer.tick(time.delta());
|
||||
if timer.just_finished() {
|
||||
example_decal.state = ExampleDecalState::AnimatingOut(Timer::new(
|
||||
DECAL_ANIMATE_OUT_DURATION,
|
||||
TimerMode::Once,
|
||||
));
|
||||
}
|
||||
}
|
||||
ExampleDecalState::AnimatingOut(ref mut timer) => {
|
||||
timer.tick(time.delta());
|
||||
if timer.just_finished() {
|
||||
commands.entity(decal_entity).despawn();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actually animate the decal by adjusting its transform.
|
||||
// All we have to do here is to compute the decal's scale as a fraction
|
||||
// of its full size.
|
||||
let new_decal_scale_factor = match example_decal.state {
|
||||
ExampleDecalState::AnimatingIn(ref timer) => timer.fraction(),
|
||||
ExampleDecalState::Idling(_) => 1.0,
|
||||
ExampleDecalState::AnimatingOut(ref timer) => timer.fraction_remaining(),
|
||||
};
|
||||
decal_transform.scale =
|
||||
Vec3::splat(example_decal.size * new_decal_scale_factor).with_z(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the appearance of the radio buttons to reflect the current
|
||||
/// application status.
|
||||
fn update_radio_buttons(
|
||||
mut widgets: Query<
|
||||
(
|
||||
Entity,
|
||||
Option<&mut BackgroundColor>,
|
||||
Has<Text>,
|
||||
&WidgetClickSender<AppSetting>,
|
||||
),
|
||||
Or<(With<RadioButton>, With<RadioButtonText>)>,
|
||||
>,
|
||||
app_status: Res<AppStatus>,
|
||||
mut writer: TextUiWriter,
|
||||
) {
|
||||
for (entity, image, has_text, sender) in widgets.iter_mut() {
|
||||
// We only have one setting in this particular application.
|
||||
let selected = match **sender {
|
||||
AppSetting::EmissiveDecals(emissive_decals) => {
|
||||
emissive_decals == app_status.emissive_decals
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut bg_color) = image {
|
||||
// Update the colors of the button itself.
|
||||
widgets::update_ui_radio_button(&mut bg_color, selected);
|
||||
}
|
||||
if has_text {
|
||||
// Update the colors of the button text.
|
||||
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the user's clicks on the radio button that determines whether the
|
||||
/// newly-spawned decals have an emissive map.
|
||||
fn handle_emission_type_change(
|
||||
mut app_status: ResMut<AppStatus>,
|
||||
mut events: MessageReader<WidgetClickEvent<AppSetting>>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
let AppSetting::EmissiveDecals(on) = **event;
|
||||
app_status.emissive_decals = on;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the GitHub download URL for the given asset.
|
||||
///
|
||||
/// The files are expected to be in the `clustered_decal_maps` directory in the
|
||||
/// [repository].
|
||||
///
|
||||
/// [repository]: https://github.com/bevyengine/bevy_asset_files
|
||||
fn get_web_asset_url(name: &str) -> String {
|
||||
format!(
|
||||
"https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
|
||||
clustered_decal_maps/{}",
|
||||
name
|
||||
)
|
||||
}
|
||||
@@ -216,13 +216,14 @@ fn spawn_camera(commands: &mut Commands) {
|
||||
|
||||
/// Spawns the actual clustered decals.
|
||||
fn spawn_decals(commands: &mut Commands, asset_server: &AssetServer) {
|
||||
let image = asset_server.load("branding/icon.png");
|
||||
let base_color_texture = asset_server.load("branding/icon.png");
|
||||
|
||||
commands.spawn((
|
||||
ClusteredDecal {
|
||||
image: image.clone(),
|
||||
base_color_texture: Some(base_color_texture.clone()),
|
||||
// Tint with red.
|
||||
tag: 1,
|
||||
..ClusteredDecal::default()
|
||||
},
|
||||
calculate_initial_decal_transform(vec3(1.0, 3.0, 5.0), Vec3::ZERO, Vec2::splat(1.1)),
|
||||
Selection::DecalA,
|
||||
@@ -230,9 +231,10 @@ fn spawn_decals(commands: &mut Commands, asset_server: &AssetServer) {
|
||||
|
||||
commands.spawn((
|
||||
ClusteredDecal {
|
||||
image: image.clone(),
|
||||
base_color_texture: Some(base_color_texture.clone()),
|
||||
// Tint with blue.
|
||||
tag: 2,
|
||||
..ClusteredDecal::default()
|
||||
},
|
||||
calculate_initial_decal_transform(vec3(-2.0, -1.0, 4.0), Vec3::ZERO, Vec2::splat(2.0)),
|
||||
Selection::DecalB,
|
||||
|
||||
@@ -152,6 +152,7 @@ Example | Description
|
||||
[Built-in postprocessing](../examples/3d/post_processing.rs) | Demonstrates the built-in postprocessing features
|
||||
[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
|
||||
[Clustered Decal Maps](../examples/3d/clustered_decal_maps.rs) | Demonstrates normal and metallic-roughness maps of decals
|
||||
[Clustered Decals](../examples/3d/clustered_decals.rs) | Demonstrates clustered decals
|
||||
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
|
||||
[Decal](../examples/3d/decal.rs) | Decal rendering
|
||||
|
||||
@@ -23,6 +23,7 @@ ba = "ba" # Part of an accessor in WGSL - weights.ba
|
||||
fLenOt = "fLenOt"
|
||||
iFO_a = "iFO_a"
|
||||
iFO_b = "iFO_b"
|
||||
Nd = "Nd" # Normal of Decal
|
||||
ser = "ser" # ron::ser - Serializer
|
||||
toi = "toi" # Time of impact
|
||||
vOt = "vOt"
|
||||
|
||||
Reference in New Issue
Block a user