Skip to main content

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 base_color_texture = asset_server.load("branding/icon.png");
220
221    commands.spawn((
222        ClusteredDecal {
223            base_color_texture: Some(base_color_texture.clone()),
224            // Tint with red.
225            tag: 1,
226            ..ClusteredDecal::default()
227        },
228        calculate_initial_decal_transform(vec3(1.0, 3.0, 5.0), Vec3::ZERO, Vec2::splat(1.1)),
229        Selection::DecalA,
230    ));
231
232    commands.spawn((
233        ClusteredDecal {
234            base_color_texture: Some(base_color_texture.clone()),
235            // Tint with blue.
236            tag: 2,
237            ..ClusteredDecal::default()
238        },
239        calculate_initial_decal_transform(vec3(-2.0, -1.0, 4.0), Vec3::ZERO, Vec2::splat(2.0)),
240        Selection::DecalB,
241    ));
242}
243
244/// Spawns the buttons at the bottom of the screen.
245fn spawn_buttons(commands: &mut Commands) {
246    // Spawn the radio buttons that allow the user to select an object to
247    // control.
248    commands.spawn((
249        widgets::main_ui_node(),
250        children![widgets::option_buttons(
251            "Drag to Move",
252            &[
253                (Selection::Camera, "Camera"),
254                (Selection::DecalA, "Decal A"),
255                (Selection::DecalB, "Decal B"),
256            ],
257        )],
258    ));
259
260    // Spawn the drag buttons that allow the user to control the scale and roll
261    // of the selected object.
262    commands.spawn((
263        Node {
264            flex_direction: FlexDirection::Row,
265            position_type: PositionType::Absolute,
266            right: px(10),
267            bottom: px(10),
268            column_gap: px(6),
269            ..default()
270        },
271        children![
272            (drag_button("Scale"), DragMode::Scale),
273            (drag_button("Roll"), DragMode::Roll),
274        ],
275    ));
276}
277
278/// Spawns a button that the user can drag to change a parameter.
279fn drag_button(label: &str) -> impl Bundle {
280    (
281        Node {
282            border: BUTTON_BORDER,
283            justify_content: JustifyContent::Center,
284            align_items: AlignItems::Center,
285            padding: BUTTON_PADDING,
286            border_radius: BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
287            ..default()
288        },
289        Button,
290        BackgroundColor(Color::BLACK),
291        BUTTON_BORDER_COLOR,
292        children![widgets::ui_text(label, Color::WHITE)],
293    )
294}
295
296/// Spawns the help text at the top of the screen.
297fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
298    commands.spawn((
299        Text::new(create_help_string(app_status)),
300        Node {
301            position_type: PositionType::Absolute,
302            top: px(12),
303            left: px(12),
304            ..default()
305        },
306        HelpText,
307    ));
308}
309
310/// Draws the outlines that show the bounds of the clustered decals.
311fn draw_gizmos(
312    mut gizmos: Gizmos,
313    decals: Query<(&GlobalTransform, &Selection), With<ClusteredDecal>>,
314) {
315    for (global_transform, selection) in &decals {
316        let color = match *selection {
317            Selection::Camera => continue,
318            Selection::DecalA => ORANGE_RED,
319            Selection::DecalB => LIME,
320        };
321
322        gizmos.primitive_3d(
323            &Cuboid {
324                // Since the clustered decal is a 1×1×1 cube in model space, its
325                // half-size is half of the scaling part of its transform.
326                half_size: global_transform.scale() * 0.5,
327            },
328            Isometry3d {
329                rotation: global_transform.rotation(),
330                translation: global_transform.translation_vec3a(),
331            },
332            color,
333        );
334    }
335}
336
337/// Calculates the initial transform of the clustered decal.
338fn calculate_initial_decal_transform(start: Vec3, looking_at: Vec3, size: Vec2) -> Transform {
339    let direction = looking_at - start;
340    let center = start + direction * 0.5;
341    Transform::from_translation(center)
342        .with_scale((size * 0.5).extend(direction.length()))
343        .looking_to(direction, Vec3::Y)
344}
345
346/// Rotates the cube a bit every frame.
347fn rotate_cube(mut meshes: Query<&mut Transform, With<Mesh3d>>) {
348    for mut transform in &mut meshes {
349        transform.rotate_y(CUBE_ROTATION_SPEED);
350    }
351}
352
353/// Updates the state of the radio buttons when the user clicks on one.
354fn update_radio_buttons(
355    mut widgets: Query<(
356        Entity,
357        Option<&mut BackgroundColor>,
358        Has<Text>,
359        &WidgetClickSender<Selection>,
360    )>,
361    app_status: Res<AppStatus>,
362    mut writer: TextUiWriter,
363) {
364    for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
365        let selected = app_status.selection == **sender;
366        if let Some(mut bg_color) = maybe_bg_color {
367            widgets::update_ui_radio_button(&mut bg_color, selected);
368        }
369        if has_text {
370            widgets::update_ui_radio_button_text(entity, &mut writer, selected);
371        }
372    }
373}
374
375/// Changes the selection when the user clicks a radio button.
376fn handle_selection_change(
377    mut events: MessageReader<WidgetClickEvent<Selection>>,
378    mut app_status: ResMut<AppStatus>,
379) {
380    for event in events.read() {
381        app_status.selection = **event;
382    }
383}
384
385/// Process a drag event that moves the selected object.
386fn process_move_input(
387    mut selections: Query<(&mut Transform, &Selection)>,
388    mouse_buttons: Res<ButtonInput<MouseButton>>,
389    mouse_motion: Res<AccumulatedMouseMotion>,
390    app_status: Res<AppStatus>,
391) {
392    // Only process drags when movement is selected.
393    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
394        return;
395    }
396
397    for (mut transform, selection) in &mut selections {
398        if app_status.selection != *selection {
399            continue;
400        }
401
402        let position = transform.translation;
403
404        // Convert to spherical coordinates.
405        let radius = position.length();
406        let mut theta = acos(position.y / radius);
407        let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
408
409        // Camera movement is the inverse of object movement.
410        let (phi_factor, theta_factor) = match *selection {
411            Selection::Camera => (1.0, -1.0),
412            Selection::DecalA | Selection::DecalB => (-1.0, 1.0),
413        };
414
415        // Adjust the spherical coordinates. Clamp the inclination to (0, π).
416        phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
417        theta = f32::clamp(
418            theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
419            0.001,
420            PI - 0.001,
421        );
422
423        // Convert spherical coordinates back to Cartesian coordinates.
424        transform.translation =
425            radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
426
427        // Look at the center, but preserve the previous roll angle.
428        let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
429        transform.look_at(Vec3::ZERO, Vec3::Y);
430        let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
431        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
432    }
433}
434
435/// Processes a drag event that scales the selected target.
436fn process_scale_input(
437    mut selections: Query<(&mut Transform, &Selection)>,
438    mouse_buttons: Res<ButtonInput<MouseButton>>,
439    mouse_motion: Res<AccumulatedMouseMotion>,
440    app_status: Res<AppStatus>,
441) {
442    // Only process drags when the scaling operation is selected.
443    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
444        return;
445    }
446
447    for (mut transform, selection) in &mut selections {
448        if app_status.selection == *selection {
449            transform.scale *= 1.0 + mouse_motion.delta.x * SCALE_SPEED;
450        }
451    }
452}
453
454/// Processes a drag event that rotates the selected target along its local Z
455/// axis.
456fn process_roll_input(
457    mut selections: Query<(&mut Transform, &Selection)>,
458    mouse_buttons: Res<ButtonInput<MouseButton>>,
459    mouse_motion: Res<AccumulatedMouseMotion>,
460    app_status: Res<AppStatus>,
461) {
462    // Only process drags when the rolling operation is selected.
463    if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
464        return;
465    }
466
467    for (mut transform, selection) in &mut selections {
468        if app_status.selection != *selection {
469            continue;
470        }
471
472        let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
473        roll += mouse_motion.delta.x * ROLL_SPEED;
474        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
475    }
476}
477
478/// Creates the help string at the top left of the screen.
479fn create_help_string(app_status: &AppStatus) -> String {
480    format!(
481        "Click and drag to {} {}",
482        app_status.drag_mode, app_status.selection
483    )
484}
485
486/// Changes the drag mode when the user hovers over the "Scale" and "Roll"
487/// buttons in the lower right.
488///
489/// If the user is hovering over no such button, this system changes the drag
490/// mode back to its default value of [`DragMode::Move`].
491fn switch_drag_mode(
492    mut commands: Commands,
493    mut interactions: Query<(&Interaction, &DragMode)>,
494    mut windows: Query<Entity, With<Window>>,
495    mouse_buttons: Res<ButtonInput<MouseButton>>,
496    mut app_status: ResMut<AppStatus>,
497) {
498    if mouse_buttons.pressed(MouseButton::Left) {
499        return;
500    }
501
502    for (interaction, drag_mode) in &mut interactions {
503        if *interaction != Interaction::Hovered {
504            continue;
505        }
506
507        app_status.drag_mode = *drag_mode;
508
509        // Set the cursor to provide the user with a nice visual hint.
510        for window in &mut windows {
511            commands
512                .entity(window)
513                .insert(CursorIcon::from(SystemCursorIcon::EwResize));
514        }
515        return;
516    }
517
518    app_status.drag_mode = DragMode::Move;
519
520    for window in &mut windows {
521        commands.entity(window).remove::<CursorIcon>();
522    }
523}
524
525/// Updates the help text in the top left of the screen to reflect the current
526/// selection and drag mode.
527fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
528    for mut text in &mut help_text {
529        text.0 = create_help_string(&app_status);
530    }
531}
532
533/// Updates the visibility of the drag mode buttons so that they aren't visible
534/// if the camera is selected.
535fn update_button_visibility(
536    mut nodes: Query<&mut Visibility, With<DragMode>>,
537    app_status: Res<AppStatus>,
538) {
539    for mut visibility in &mut nodes {
540        *visibility = match app_status.selection {
541            Selection::Camera => Visibility::Hidden,
542            Selection::DecalA | Selection::DecalB => Visibility::Visible,
543        };
544    }
545}