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 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 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 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
244fn spawn_buttons(commands: &mut Commands) {
246 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 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
278fn 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
296fn 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
310fn 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 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
337fn 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
346fn 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
353fn 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
375fn 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
385fn 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 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 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 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 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 transform.translation =
425 radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
426
427 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
435fn 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 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
454fn 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 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
478fn 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
486fn 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 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
525fn 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
533fn 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}