
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 
194 lines
6.0 KiB
Rust
194 lines
6.0 KiB
Rust
//! Simple widgets for example UI.
|
|
|
|
use bevy::{ecs::system::EntityCommands, prelude::*};
|
|
|
|
/// An event that's sent whenever the user changes one of the settings by
|
|
/// clicking a radio button.
|
|
#[derive(Clone, Event, Deref, DerefMut)]
|
|
pub struct WidgetClickEvent<T>(T);
|
|
|
|
/// A marker component that we place on all widgets that send
|
|
/// [`WidgetClickEvent`]s of the given type.
|
|
#[derive(Clone, Component, Deref, DerefMut)]
|
|
pub struct WidgetClickSender<T>(T)
|
|
where
|
|
T: Clone + Send + Sync + 'static;
|
|
|
|
/// A marker component that we place on all radio `Button`s.
|
|
#[derive(Clone, Copy, Component)]
|
|
pub struct RadioButton;
|
|
|
|
/// A marker component that we place on all `Text` inside radio buttons.
|
|
#[derive(Clone, Copy, Component)]
|
|
pub struct RadioButtonText;
|
|
|
|
/// The size of the border that surrounds buttons.
|
|
pub const BUTTON_BORDER: UiRect = UiRect::all(Val::Px(1.0));
|
|
|
|
/// The color of the border that surrounds buttons.
|
|
pub const BUTTON_BORDER_COLOR: BorderColor = BorderColor(Color::WHITE);
|
|
|
|
/// The amount of rounding to apply to button corners.
|
|
pub const BUTTON_BORDER_RADIUS_SIZE: Val = Val::Px(6.0);
|
|
|
|
/// The amount of space between the edge of the button and its label.
|
|
pub const BUTTON_PADDING: UiRect = UiRect::axes(Val::Px(12.0), Val::Px(6.0));
|
|
|
|
/// Returns a [`Node`] appropriate for the outer main UI node.
|
|
///
|
|
/// This UI is in the bottom left corner and has flex column support
|
|
pub fn main_ui_node() -> Node {
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
position_type: PositionType::Absolute,
|
|
row_gap: Val::Px(6.0),
|
|
left: Val::Px(10.0),
|
|
bottom: Val::Px(10.0),
|
|
..default()
|
|
}
|
|
}
|
|
|
|
/// Spawns a single radio button that allows configuration of a setting.
|
|
///
|
|
/// The type parameter specifies the value that will be packaged up and sent in
|
|
/// a [`WidgetClickEvent`] when the radio button is clicked.
|
|
pub fn spawn_option_button<T>(
|
|
parent: &mut ChildSpawnerCommands,
|
|
option_value: T,
|
|
option_name: &str,
|
|
is_selected: bool,
|
|
is_first: bool,
|
|
is_last: bool,
|
|
) where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
let (bg_color, fg_color) = if is_selected {
|
|
(Color::WHITE, Color::BLACK)
|
|
} else {
|
|
(Color::BLACK, Color::WHITE)
|
|
};
|
|
|
|
// Add the button node.
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
Node {
|
|
border: BUTTON_BORDER.with_left(if is_first { Val::Px(1.0) } else { Val::Px(0.0) }),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
padding: BUTTON_PADDING,
|
|
..default()
|
|
},
|
|
BUTTON_BORDER_COLOR,
|
|
BorderRadius::ZERO
|
|
.with_left(if is_first {
|
|
BUTTON_BORDER_RADIUS_SIZE
|
|
} else {
|
|
Val::Px(0.0)
|
|
})
|
|
.with_right(if is_last {
|
|
BUTTON_BORDER_RADIUS_SIZE
|
|
} else {
|
|
Val::Px(0.0)
|
|
}),
|
|
BackgroundColor(bg_color),
|
|
))
|
|
.insert(RadioButton)
|
|
.insert(WidgetClickSender(option_value.clone()))
|
|
.with_children(|parent| {
|
|
spawn_ui_text(parent, option_name, fg_color)
|
|
.insert(RadioButtonText)
|
|
.insert(WidgetClickSender(option_value));
|
|
});
|
|
}
|
|
|
|
/// Spawns the buttons that allow configuration of a setting.
|
|
///
|
|
/// The user may change the setting to any one of the labeled `options`. The
|
|
/// value of the given type parameter will be packaged up and sent as a
|
|
/// [`WidgetClickEvent`] when one of the radio buttons is clicked.
|
|
pub fn spawn_option_buttons<T>(
|
|
parent: &mut ChildSpawnerCommands,
|
|
title: &str,
|
|
options: &[(T, &str)],
|
|
) where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
// Add the parent node for the row.
|
|
parent
|
|
.spawn(Node {
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
spawn_ui_text(parent, title, Color::BLACK).insert(Node {
|
|
width: Val::Px(125.0),
|
|
..default()
|
|
});
|
|
|
|
for (option_index, (option_value, option_name)) in options.iter().cloned().enumerate() {
|
|
spawn_option_button(
|
|
parent,
|
|
option_value,
|
|
option_name,
|
|
option_index == 0,
|
|
option_index == 0,
|
|
option_index == options.len() - 1,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Spawns text for the UI.
|
|
///
|
|
/// Returns the `EntityCommands`, which allow further customization of the text
|
|
/// style.
|
|
pub fn spawn_ui_text<'a>(
|
|
parent: &'a mut ChildSpawnerCommands,
|
|
label: &str,
|
|
color: Color,
|
|
) -> EntityCommands<'a> {
|
|
parent.spawn((
|
|
Text::new(label),
|
|
TextFont {
|
|
font_size: 18.0,
|
|
..default()
|
|
},
|
|
TextColor(color),
|
|
))
|
|
}
|
|
|
|
/// Checks for clicks on the radio buttons and sends `RadioButtonChangeEvent`s
|
|
/// as necessary.
|
|
pub fn handle_ui_interactions<T>(
|
|
mut interactions: Query<
|
|
(&Interaction, &WidgetClickSender<T>),
|
|
(With<Button>, With<RadioButton>),
|
|
>,
|
|
mut widget_click_events: EventWriter<WidgetClickEvent<T>>,
|
|
) where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
for (interaction, click_event) in interactions.iter_mut() {
|
|
if *interaction == Interaction::Pressed {
|
|
widget_click_events.send(WidgetClickEvent((**click_event).clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Updates the style of the button part of a radio button to reflect its
|
|
/// selected status.
|
|
pub fn update_ui_radio_button(background_color: &mut BackgroundColor, selected: bool) {
|
|
background_color.0 = if selected { Color::WHITE } else { Color::BLACK };
|
|
}
|
|
|
|
/// Updates the color of the label of a radio button to reflect its selected
|
|
/// status.
|
|
pub fn update_ui_radio_button_text(entity: Entity, writer: &mut TextUiWriter, selected: bool) {
|
|
let text_color = if selected { Color::BLACK } else { Color::WHITE };
|
|
|
|
writer.for_each_color(entity, |mut color| {
|
|
color.0 = text_color;
|
|
});
|
|
}
|