clustered_decals/
clustered_decals.rs

1//! Demonstrates clustered decals, which affix decals to surfaces.
2
3use std::f32::consts::{FRAC_PI_3, PI};
4use std::fmt::{self, Formatter};
5
6use bevy::{
7    color::palettes::css::{LIME, ORANGE_RED, SILVER},
8    input::mouse::AccumulatedMouseMotion,
9    light::ClusteredDecal,
10    pbr::{decal, ExtendedMaterial, MaterialExtension},
11    prelude::*,
12    render::{
13        render_resource::AsBindGroup,
14        renderer::{RenderAdapter, RenderDevice},
15    },
16    shader::ShaderRef,
17    window::{CursorIcon, SystemCursorIcon},
18};
19use ops::{acos, cos, sin};
20use widgets::{
21    WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
22    BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
23};
24
25#[path = "../helpers/widgets.rs"]
26mod widgets;
27
28/// The custom material shader that we use to demonstrate how to use the decal
29/// `tag` field.
30const SHADER_ASSET_PATH: &str = "shaders/custom_clustered_decal.wgsl";
31
32/// The speed at which the cube rotates, in radians per frame.
33const CUBE_ROTATION_SPEED: f32 = 0.02;
34
35/// The speed at which the selection can be moved, in spherical coordinate
36/// radians per mouse unit.
37const MOVE_SPEED: f32 = 0.008;
38/// The speed at which the selection can be scaled, in reciprocal mouse units.
39const SCALE_SPEED: f32 = 0.05;
40/// The speed at which the selection can be scaled, in radians per mouse unit.
41const ROLL_SPEED: f32 = 0.01;
42
43/// Various settings for the demo.
44#[derive(Resource, Default)]
45struct AppStatus {
46    /// The object that will be moved, scaled, or rotated when the mouse is
47    /// dragged.
48    selection: Selection,
49    /// What happens when the mouse is dragged: one of a move, rotate, or scale
50    /// operation.
51    drag_mode: DragMode,
52}
53
54/// The object that will be moved, scaled, or rotated when the mouse is dragged.
55#[derive(Clone, Copy, Component, Default, PartialEq)]
56enum Selection {
57    /// The camera.
58    ///
59    /// The camera can only be moved, not scaled or rotated.
60    #[default]
61    Camera,
62    /// The first decal, which an orange bounding box surrounds.
63    DecalA,
64    /// The second decal, which a lime green bounding box surrounds.
65    DecalB,
66}
67
68impl fmt::Display for Selection {
69    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
70        match *self {
71            Selection::Camera => f.write_str("camera"),
72            Selection::DecalA => f.write_str("decal A"),
73            Selection::DecalB => f.write_str("decal B"),
74        }
75    }
76}
77
78/// What happens when the mouse is dragged: one of a move, rotate, or scale
79/// operation.
80#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
81enum DragMode {
82    /// The mouse moves the current selection.
83    #[default]
84    Move,
85    /// The mouse scales the current selection.
86    ///
87    /// This only applies to decals, not cameras.
88    Scale,
89    /// The mouse rotates the current selection around its local Z axis.
90    ///
91    /// This only applies to decals, not cameras.
92    Roll,
93}
94
95impl fmt::Display for DragMode {
96    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
97        match *self {
98            DragMode::Move => f.write_str("move"),
99            DragMode::Scale => f.write_str("scale"),
100            DragMode::Roll => f.write_str("roll"),
101        }
102    }
103}
104
105/// A marker component for the help text in the top left corner of the window.
106#[derive(Clone, Copy, Component)]
107struct HelpText;
108
109/// A shader extension that demonstrates how to use the `tag` field to customize
110/// the appearance of your decals.
111#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
112struct CustomDecalExtension {}
113
114impl MaterialExtension for CustomDecalExtension {
115    fn fragment_shader() -> ShaderRef {
116        SHADER_ASSET_PATH.into()
117    }
118}
119
120/// Entry point.
121fn main() {
122    App::new()
123        .add_plugins(DefaultPlugins.set(WindowPlugin {
124            primary_window: Some(Window {
125                title: "Bevy Clustered Decals Example".into(),
126                ..default()
127            }),
128            ..default()
129        }))
130        .add_plugins(MaterialPlugin::<
131            ExtendedMaterial<StandardMaterial, CustomDecalExtension>,
132        >::default())
133        .init_resource::<AppStatus>()
134        .add_message::<WidgetClickEvent<Selection>>()
135        .add_systems(Startup, setup)
136        .add_systems(Update, draw_gizmos)
137        .add_systems(Update, rotate_cube)
138        .add_systems(Update, widgets::handle_ui_interactions::<Selection>)
139        .add_systems(
140            Update,
141            (handle_selection_change, update_radio_buttons)
142                .after(widgets::handle_ui_interactions::<Selection>),
143        )
144        .add_systems(Update, process_move_input)
145        .add_systems(Update, process_scale_input)
146        .add_systems(Update, process_roll_input)
147        .add_systems(Update, switch_drag_mode)
148        .add_systems(Update, update_help_text)
149        .add_systems(Update, update_button_visibility)
150        .run();
151}
152
153/// Creates the scene.
154fn setup(
155    mut commands: Commands,
156    asset_server: Res<AssetServer>,
157    app_status: Res<AppStatus>,
158    render_device: Res<RenderDevice>,
159    render_adapter: Res<RenderAdapter>,
160    mut meshes: ResMut<Assets<Mesh>>,
161    mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>>,
162) {
163    // Error out if clustered decals aren't supported on the current platform.
164    if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
165        error!("Clustered decals aren't usable on this platform.");
166        commands.write_message(AppExit::error());
167    }
168
169    spawn_cube(&mut commands, &mut meshes, &mut materials);
170    spawn_camera(&mut commands);
171    spawn_light(&mut commands);
172    spawn_decals(&mut commands, &asset_server);
173    spawn_buttons(&mut commands);
174    spawn_help_text(&mut commands, &app_status);
175}
176
177/// Spawns the cube onto which the decals are projected.
178fn spawn_cube(
179    commands: &mut Commands,
180    meshes: &mut Assets<Mesh>,
181    materials: &mut Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>,
182) {
183    // Rotate the cube a bit just to make it more interesting.
184    let mut transform = Transform::IDENTITY;
185    transform.rotate_y(FRAC_PI_3);
186
187    commands.spawn((
188        Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
189        MeshMaterial3d(materials.add(ExtendedMaterial {
190            base: StandardMaterial {
191                base_color: SILVER.into(),
192                ..default()
193            },
194            extension: CustomDecalExtension {},
195        })),
196        transform,
197    ));
198}
199
200/// Spawns the directional light.
201fn spawn_light(commands: &mut Commands) {
202    commands.spawn((
203        DirectionalLight::default(),
204        Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
205    ));
206}
207
208/// Spawns the camera.
209fn spawn_camera(commands: &mut Commands) {
210    commands
211        .spawn(Camera3d::default())
212        .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
213        // Tag the camera with `Selection::Camera`.
214        .insert(Selection::Camera);
215}
216
217/// Spawns the actual clustered decals.
218fn spawn_decals(commands: &mut Commands, asset_server: &AssetServer) {
219    let image = asset_server.load("branding/icon.png");
220
221    commands.spawn((
222        ClusteredDecal {
223            image: image.clone(),
224            // Tint with red.
225            tag: 1,
226        },
227        calculate_initial_decal_transform(vec3(1.0, 3.0, 5.0), Vec3::ZERO, Vec2::splat(1.1)),
228        Selection::DecalA,
229    ));
230
231    commands.spawn((
232        ClusteredDecal {
233            image: image.clone(),
234            // Tint with blue.
235            tag: 2,
236        },
237        calculate_initial_decal_transform(vec3(-2.0, -1.0, 4.0), Vec3::ZERO, Vec2::splat(2.0)),
238        Selection::DecalB,
239    ));
240}
241
242/// Spawns the buttons at the bottom of the screen.
243fn spawn_buttons(commands: &mut Commands) {
244    // Spawn the radio buttons that allow the user to select an object to
245    // control.
246    commands.spawn((
247        widgets::main_ui_node(),
248        children![widgets::option_buttons(
249            "Drag to Move",
250            &[
251                (Selection::Camera, "Camera"),
252                (Selection::DecalA, "Decal A"),
253                (Selection::DecalB, "Decal B"),
254            ],
255        )],
256    ));
257
258    // Spawn the drag buttons that allow the user to control the scale and roll
259    // of the selected object.
260    commands.spawn((
261        Node {
262            flex_direction: FlexDirection::Row,
263            position_type: PositionType::Absolute,
264            right: px(10),
265            bottom: px(10),
266            column_gap: px(6),
267            ..default()
268        },
269        children![
270            (drag_button("Scale"), DragMode::Scale),
271            (drag_button("Roll"), DragMode::Roll),
272        ],
273    ));
274}
275
276/// Spawns a button that the user can drag to change a parameter.
277fn drag_button(label: &str) -> impl Bundle {
278    (
279        Node {
280            border: BUTTON_BORDER,
281            justify_content: JustifyContent::Center,
282            align_items: AlignItems::Center,
283            padding: BUTTON_PADDING,
284            ..default()
285        },
286        Button,
287        BackgroundColor(Color::BLACK),
288        BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
289        BUTTON_BORDER_COLOR,
290        children![widgets::ui_text(label, Color::WHITE)],
291    )
292}
293
294/// Spawns the help text at the top of the screen.
295fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
296    commands.spawn((
297        Text::new(create_help_string(app_status)),
298        Node {
299            position_type: PositionType::Absolute,
300            top: px(12),
301            left: px(12),
302            ..default()
303        },
304        HelpText,
305    ));
306}
307
308/// Draws the outlines that show the bounds of the clustered decals.
309fn draw_gizmos(
310    mut gizmos: Gizmos,
311    decals: Query<(&GlobalTransform, &Selection), With<ClusteredDecal>>,
312) {
313    for (global_transform, selection) in &decals {
314        let color = match *selection {
315            Selection::Camera => continue,
316            Selection::DecalA => ORANGE_RED,
317            Selection::DecalB => LIME,
318        };
319
320        gizmos.primitive_3d(
321            &Cuboid {
322                // Since the clustered decal is a 1×1×1 cube in model space, its
323                // half-size is half of the scaling part of its transform.
324                half_size: global_transform.scale() * 0.5,
325            },
326            Isometry3d {
327                rotation: global_transform.rotation(),
328                translation: global_transform.translation_vec3a(),
329            },
330            color,
331        );
332    }
333}
334
335/// Calculates the initial transform of the clustered decal.
336fn calculate_initial_decal_transform(start: Vec3, looking_at: Vec3, size: Vec2) -> Transform {
337    let direction = looking_at - start;
338    let center = start + direction * 0.5;
339    Transform::from_translation(center)
340        .with_scale((size * 0.5).extend(direction.length()))
341        .looking_to(direction, Vec3::Y)
342}
343
344/// Rotates the cube a bit every frame.
345fn rotate_cube(mut meshes: Query<&mut Transform, With<Mesh3d>>) {
346    for mut transform in &mut meshes {
347        transform.rotate_y(CUBE_ROTATION_SPEED);
348    }
349}
350
351/// Updates the state of the radio buttons when the user clicks on one.
352fn update_radio_buttons(
353    mut widgets: Query<(
354        Entity,
355        Option<&mut BackgroundColor>,
356        Has<Text>,
357        &WidgetClickSender<Selection>,
358    )>,
359    app_status: Res<AppStatus>,
360    mut writer: TextUiWriter,
361) {
362    for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
363        let selected = app_status.selection == **sender;
364        if let Some(mut bg_color) = maybe_bg_color {
365            widgets::update_ui_radio_button(&mut bg_color, selected);
366        }
367        if has_text {
368            widgets::update_ui_radio_button_text(entity, &mut writer, selected);
369        }
370    }
371}
372
373/// Changes the selection when the user clicks a radio button.
374fn handle_selection_change(
375    mut events: MessageReader<WidgetClickEvent<Selection>>,
376    mut app_status: ResMut<AppStatus>,
377) {
378    for event in events.read() {
379        app_status.selection = **event;
380    }
381}
382
383/// Process a drag event that moves the selected object.
384fn process_move_input(
385    mut selections: Query<(&mut Transform, &Selection)>,
386    mouse_buttons: Res<ButtonInput<MouseButton>>,
387    mouse_motion: Res<AccumulatedMouseMotion>,
388    app_status: Res<AppStatus>,
389) {
390    // Only process drags when movement is selected.
391    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
392        return;
393    }
394
395    for (mut transform, selection) in &mut selections {
396        if app_status.selection != *selection {
397            continue;
398        }
399
400        let position = transform.translation;
401
402        // Convert to spherical coordinates.
403        let radius = position.length();
404        let mut theta = acos(position.y / radius);
405        let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
406
407        // Camera movement is the inverse of object movement.
408        let (phi_factor, theta_factor) = match *selection {
409            Selection::Camera => (1.0, -1.0),
410            Selection::DecalA | Selection::DecalB => (-1.0, 1.0),
411        };
412
413        // Adjust the spherical coordinates. Clamp the inclination to (0, π).
414        phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
415        theta = f32::clamp(
416            theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
417            0.001,
418            PI - 0.001,
419        );
420
421        // Convert spherical coordinates back to Cartesian coordinates.
422        transform.translation =
423            radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
424
425        // Look at the center, but preserve the previous roll angle.
426        let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
427        transform.look_at(Vec3::ZERO, Vec3::Y);
428        let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
429        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
430    }
431}
432
433/// Processes a drag event that scales the selected target.
434fn process_scale_input(
435    mut selections: Query<(&mut Transform, &Selection)>,
436    mouse_buttons: Res<ButtonInput<MouseButton>>,
437    mouse_motion: Res<AccumulatedMouseMotion>,
438    app_status: Res<AppStatus>,
439) {
440    // Only process drags when the scaling operation is selected.
441    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
442        return;
443    }
444
445    for (mut transform, selection) in &mut selections {
446        if app_status.selection == *selection {
447            transform.scale *= 1.0 + mouse_motion.delta.x * SCALE_SPEED;
448        }
449    }
450}
451
452/// Processes a drag event that rotates the selected target along its local Z
453/// axis.
454fn process_roll_input(
455    mut selections: Query<(&mut Transform, &Selection)>,
456    mouse_buttons: Res<ButtonInput<MouseButton>>,
457    mouse_motion: Res<AccumulatedMouseMotion>,
458    app_status: Res<AppStatus>,
459) {
460    // Only process drags when the rolling operation is selected.
461    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
462        return;
463    }
464
465    for (mut transform, selection) in &mut selections {
466        if app_status.selection != *selection {
467            continue;
468        }
469
470        let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
471        roll += mouse_motion.delta.x * ROLL_SPEED;
472        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
473    }
474}
475
476/// Creates the help string at the top left of the screen.
477fn create_help_string(app_status: &AppStatus) -> String {
478    format!(
479        "Click and drag to {} {}",
480        app_status.drag_mode, app_status.selection
481    )
482}
483
484/// Changes the drag mode when the user hovers over the "Scale" and "Roll"
485/// buttons in the lower right.
486///
487/// If the user is hovering over no such button, this system changes the drag
488/// mode back to its default value of [`DragMode::Move`].
489fn switch_drag_mode(
490    mut commands: Commands,
491    mut interactions: Query<(&Interaction, &DragMode)>,
492    mut windows: Query<Entity, With<Window>>,
493    mouse_buttons: Res<ButtonInput<MouseButton>>,
494    mut app_status: ResMut<AppStatus>,
495) {
496    if mouse_buttons.pressed(MouseButton::Left) {
497        return;
498    }
499
500    for (interaction, drag_mode) in &mut interactions {
501        if *interaction != Interaction::Hovered {
502            continue;
503        }
504
505        app_status.drag_mode = *drag_mode;
506
507        // Set the cursor to provide the user with a nice visual hint.
508        for window in &mut windows {
509            commands
510                .entity(window)
511                .insert(CursorIcon::from(SystemCursorIcon::EwResize));
512        }
513        return;
514    }
515
516    app_status.drag_mode = DragMode::Move;
517
518    for window in &mut windows {
519        commands.entity(window).remove::<CursorIcon>();
520    }
521}
522
523/// Updates the help text in the top left of the screen to reflect the current
524/// selection and drag mode.
525fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
526    for mut text in &mut help_text {
527        text.0 = create_help_string(&app_status);
528    }
529}
530
531/// Updates the visibility of the drag mode buttons so that they aren't visible
532/// if the camera is selected.
533fn update_button_visibility(
534    mut nodes: Query<&mut Visibility, With<DragMode>>,
535    app_status: Res<AppStatus>,
536) {
537    for mut visibility in &mut nodes {
538        *visibility = match app_status.selection {
539            Selection::Camera => Visibility::Hidden,
540            Selection::DecalA | Selection::DecalB => Visibility::Visible,
541        };
542    }
543}