1use 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
28const SHADER_ASSET_PATH: &str = "shaders/custom_clustered_decal.wgsl";
31
32const CUBE_ROTATION_SPEED: f32 = 0.02;
34
35const MOVE_SPEED: f32 = 0.008;
38const SCALE_SPEED: f32 = 0.05;
40const ROLL_SPEED: f32 = 0.01;
42
43#[derive(Resource, Default)]
45struct AppStatus {
46 selection: Selection,
49 drag_mode: DragMode,
52}
53
54#[derive(Clone, Copy, Component, Default, PartialEq)]
56enum Selection {
57 #[default]
61 Camera,
62 DecalA,
64 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#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
81enum DragMode {
82 #[default]
84 Move,
85 Scale,
89 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#[derive(Clone, Copy, Component)]
107struct HelpText;
108
109#[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
120fn 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
153fn 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 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
177fn spawn_cube(
179 commands: &mut Commands,
180 meshes: &mut Assets<Mesh>,
181 materials: &mut Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>,
182) {
183 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
200fn 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
208fn 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 .insert(Selection::Camera);
215}
216
217fn 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 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 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
242fn spawn_buttons(commands: &mut Commands) {
244 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 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
276fn 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
294fn 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
308fn 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 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
335fn 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
344fn 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
351fn 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
373fn 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
383fn 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 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 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 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 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 transform.translation =
423 radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
424
425 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
433fn 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 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
452fn 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 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
476fn 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
484fn 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 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
523fn 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
531fn 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}