1use std::f32::consts::{FRAC_PI_3, PI};
4use std::fmt::{self, Formatter};
5
6use bevy::feathers::dark_theme::create_dark_theme;
7use bevy::ui_widgets::radio_self_update;
8use bevy::{
9 color::palettes::css::{LIME, ORANGE_RED, SILVER},
10 feathers::{
11 controls::{FeathersNumberInput, NumberInputPrecision, NumberInputValue},
12 theme::UiTheme,
13 FeathersPlugins,
14 },
15 input::mouse::AccumulatedMouseMotion,
16 light::ClusteredDecal,
17 pbr::{decal, ExtendedMaterial, MaterialExtension},
18 prelude::*,
19 render::{
20 render_resource::AsBindGroup,
21 renderer::{RenderAdapter, RenderDevice},
22 },
23 shader::ShaderRef,
24 ui_widgets::ValueChange,
25};
26use ops::{acos, cos, sin};
27
28#[path = "../helpers/radio.rs"]
29mod radio;
30
31#[path = "../helpers/number_input_f32.rs"]
32mod number_input_f32;
33
34use number_input_f32::number_input_f32;
35
36const SHADER_ASSET_PATH: &str = "shaders/custom_clustered_decal.wesl";
39
40const CUBE_ROTATION_SPEED: f32 = 0.02;
42
43const MOVE_SPEED: f32 = 0.008;
46
47#[derive(Resource, Default)]
49struct AppStatus {
50 selection: Selection,
53}
54
55#[derive(Clone, Copy, Component, Default, PartialEq)]
57enum Selection {
58 #[default]
62 Camera,
63 DecalA,
65 DecalB,
67}
68
69impl fmt::Display for Selection {
70 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
71 match *self {
72 Selection::Camera => f.write_str("camera"),
73 Selection::DecalA => f.write_str("decal A"),
74 Selection::DecalB => f.write_str("decal B"),
75 }
76 }
77}
78
79#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
81enum AppNumberInput {
82 #[default]
84 Scale,
85 Roll,
87}
88
89#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
91struct BaseScale(Vec3);
92
93#[derive(Clone, Copy, Component)]
95struct HelpText;
96
97#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
100struct CustomDecalExtension {}
101
102impl MaterialExtension for CustomDecalExtension {
103 fn fragment_shader() -> ShaderRef {
104 SHADER_ASSET_PATH.into()
105 }
106}
107
108fn main() {
110 App::new()
111 .insert_resource(UiTheme(create_dark_theme()))
112 .add_plugins((
113 DefaultPlugins.set(WindowPlugin {
114 primary_window: Some(Window {
115 title: "Bevy Clustered Decals Example".into(),
116 ..default()
117 }),
118 ..default()
119 }),
120 FeathersPlugins,
121 ))
122 .add_plugins(MaterialPlugin::<
123 ExtendedMaterial<StandardMaterial, CustomDecalExtension>,
124 >::default())
125 .init_resource::<AppStatus>()
126 .add_systems(Startup, setup)
127 .add_systems(Update, draw_gizmos)
128 .add_systems(Update, rotate_cube)
129 .add_systems(Update, update_help_text)
130 .add_observer(handle_drag_as_movement)
131 .add_observer(handle_selection_change)
132 .add_observer(handle_value_change_number_input)
133 .add_observer(radio_self_update)
134 .run();
135}
136
137fn setup(
139 mut commands: Commands,
140 asset_server: Res<AssetServer>,
141 app_status: Res<AppStatus>,
142 render_device: Res<RenderDevice>,
143 render_adapter: Res<RenderAdapter>,
144 mut meshes: ResMut<Assets<Mesh>>,
145 mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>>,
146) {
147 if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
149 error!("Clustered decals aren't usable on this platform.");
150 commands.write_message(AppExit::error());
151 }
152
153 spawn_cube(&mut commands, &mut meshes, &mut materials);
154 spawn_camera(&mut commands);
155 spawn_light(&mut commands);
156 spawn_decals(&mut commands, &asset_server);
157 spawn_buttons(&mut commands);
158 spawn_help_text(&mut commands, &app_status);
159}
160
161fn spawn_cube(
163 commands: &mut Commands,
164 meshes: &mut Assets<Mesh>,
165 materials: &mut Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>,
166) {
167 let mut transform = Transform::IDENTITY;
169 transform.rotate_y(FRAC_PI_3);
170
171 commands.spawn((
172 Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
173 MeshMaterial3d(materials.add(ExtendedMaterial {
174 base: StandardMaterial {
175 base_color: SILVER.into(),
176 ..default()
177 },
178 extension: CustomDecalExtension {},
179 })),
180 transform,
181 ));
182}
183
184fn spawn_light(commands: &mut Commands) {
186 commands.spawn((
187 DirectionalLight::default(),
188 Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
189 ));
190}
191
192fn spawn_camera(commands: &mut Commands) {
194 commands
195 .spawn(Camera3d::default())
196 .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
197 .insert(Selection::Camera);
199}
200
201fn spawn_decals(commands: &mut Commands, asset_server: &AssetServer) {
203 let base_color_texture = asset_server.load("branding/icon.png");
204
205 commands.spawn((
206 ClusteredDecal {
207 base_color_texture: Some(base_color_texture.clone()),
208 tag: 1,
210 ..ClusteredDecal::default()
211 },
212 calculate_initial_decal_transform(vec3(1.0, 3.0, 5.0), Vec3::ZERO, Vec2::splat(1.1)),
213 Selection::DecalA,
214 ));
215
216 commands.spawn((
217 ClusteredDecal {
218 base_color_texture: Some(base_color_texture.clone()),
219 tag: 2,
221 ..ClusteredDecal::default()
222 },
223 calculate_initial_decal_transform(vec3(-2.0, -1.0, 4.0), Vec3::ZERO, Vec2::splat(2.0)),
224 Selection::DecalB,
225 ));
226}
227
228fn spawn_buttons(commands: &mut Commands) {
230 commands.spawn_scene(bsn! {
234 @radio::main_ui_node_scene()
235 Children [
236 @radio::feathers_option_buttons("Drag to Move",
237 &[
238 (Selection::Camera, "Camera"),
239 (Selection::DecalA, "Decal A"),
240 (Selection::DecalB, "Decal B"),
241 ], 0)
242 --
243 Visibility::Hidden
245 @number_input_f32("Scale Multiplier", Some(AppNumberInput::Scale), 1.0, NumberInputPrecision(2), 0.05..=10.)
246 --
247 Visibility::Hidden
248 @number_input_f32("Roll (-π to π)", Some(AppNumberInput::Roll), 0.0, NumberInputPrecision(2), -PI + f32::EPSILON ..=PI.next_down())
250 ]
251 });
252}
253
254fn handle_value_change_number_input(
257 value_change: On<ValueChange<f32>>,
258 mut commands: Commands,
259 number_input_q: Query<&AppNumberInput, With<FeathersNumberInput>>,
260 app_status: ResMut<AppStatus>,
261 mut selections: Query<(&mut Transform, &BaseScale, &Selection)>,
262) {
263 if app_status.selection == Selection::Camera {
264 return;
265 }
266 if let Ok(app_number_input) = number_input_q.get(value_change.source) {
267 for (mut transform, base_scale, selection) in &mut selections {
268 if app_status.selection != *selection {
269 continue;
270 }
271 match app_number_input {
272 AppNumberInput::Scale => {
273 transform.scale = base_scale.0 * value_change.value;
274 }
275 AppNumberInput::Roll => {
276 let (yaw, pitch, mut _roll) = transform.rotation.to_euler(EulerRot::YXZ);
277 transform.rotation =
279 Quat::from_euler(EulerRot::YXZ, yaw, pitch, value_change.value);
280 }
281 }
282 }
283 commands
284 .entity(value_change.source)
285 .insert(NumberInputValue::F32(value_change.value));
286 }
287}
288
289fn handle_selection_change(
293 event: On<ValueChange<Entity>>,
294 new_value_query: Query<&radio::RadioButtonOptionValue<Selection>>,
295 mut app_status: ResMut<AppStatus>,
296 mut commands: Commands,
297 selections: Query<(&Transform, &BaseScale, &Selection)>,
298 number_inputs: Query<(Entity, &ChildOf, &AppNumberInput)>,
299) {
300 let Ok(radio::RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
301 return;
302 };
303 app_status.selection = *selection;
304
305 for (input_entity, child_of, app_number_input) in number_inputs.iter() {
308 match app_status.selection {
309 Selection::Camera => {
310 commands
311 .entity(child_of.parent())
312 .insert(Visibility::Hidden);
313 }
314 Selection::DecalA | Selection::DecalB => {
315 commands
316 .entity(child_of.parent())
317 .insert(Visibility::Inherited);
318
319 for (transform, base_scale, _selection) in selections
321 .iter()
322 .filter(|&(_, _, selection)| *selection == app_status.selection)
323 {
324 if AppNumberInput::Scale == *app_number_input {
325 let scale_multiplier = transform.scale.x / base_scale.0.x;
327 commands
328 .entity(input_entity)
329 .insert(NumberInputValue::F32(scale_multiplier));
330 } else {
331 let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
332 commands
333 .entity(input_entity)
334 .insert(NumberInputValue::F32(roll));
335 }
336 }
337 }
338 };
339 }
340}
341
342fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
344 commands.spawn((
345 Text::new(create_help_string(app_status)),
346 Node {
347 position_type: PositionType::Absolute,
348 top: px(12),
349 left: px(12),
350 ..default()
351 },
352 HelpText,
353 ));
354}
355
356fn draw_gizmos(
358 mut gizmos: Gizmos,
359 decals: Query<(&GlobalTransform, &Selection), With<ClusteredDecal>>,
360) {
361 for (global_transform, selection) in &decals {
362 let color = match *selection {
363 Selection::Camera => continue,
364 Selection::DecalA => ORANGE_RED,
365 Selection::DecalB => LIME,
366 };
367
368 gizmos.primitive_3d(
369 &Cuboid {
370 half_size: global_transform.scale() * 0.5,
373 },
374 Isometry3d {
375 rotation: global_transform.rotation(),
376 translation: global_transform.translation_vec3a(),
377 },
378 color,
379 );
380 }
381}
382
383fn calculate_initial_decal_transform(start: Vec3, looking_at: Vec3, size: Vec2) -> impl Bundle {
385 let direction = looking_at - start;
386 let center = start + direction * 0.5;
387 let base_scale = (size * 0.5).extend(direction.length());
388 (
389 Transform::from_translation(center)
390 .with_scale(base_scale)
391 .looking_to(direction, Vec3::Y),
392 BaseScale(base_scale),
393 )
394}
395
396fn rotate_cube(mut meshes: Query<&mut Transform, With<Mesh3d>>) {
398 for mut transform in &mut meshes {
399 transform.rotate_y(CUBE_ROTATION_SPEED);
400 }
401}
402
403fn handle_drag_as_movement(
405 event: On<PointerDrag>,
406 parent_q: Query<&ChildOf>,
407 number_input_q: Query<(), With<FeathersNumberInput>>,
408 mut selections: Query<(&mut Transform, &Selection)>,
409 mouse_motion: Res<AccumulatedMouseMotion>,
410 app_status: Res<AppStatus>,
411) {
412 if parent_q
415 .iter_ancestors(event.entity)
416 .any(|parent| number_input_q.contains(parent))
417 {
418 return;
419 }
420 for (mut transform, selection) in &mut selections {
421 if app_status.selection != *selection {
422 continue;
423 }
424
425 let position = transform.translation;
426
427 let radius = position.length();
429 let mut theta = acos(position.y / radius);
430 let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
431
432 let (phi_factor, theta_factor) = match *selection {
434 Selection::Camera => (1.0, -1.0),
435 Selection::DecalA | Selection::DecalB => (-1.0, 1.0),
436 };
437
438 phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
440 theta = f32::clamp(
441 theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
442 0.001,
443 PI - 0.001,
444 );
445
446 transform.translation =
448 radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
449
450 let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
452 transform.look_at(Vec3::ZERO, Vec3::Y);
453 let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
454 transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
455 }
456}
457
458fn create_help_string(app_status: &AppStatus) -> String {
460 if app_status.selection == Selection::Camera {
461 format!("Click and drag to move {}.", app_status.selection)
462 } else {
463 format!(
464 "Click and drag to move/scale/rotate {}.\n\
465 To scale/rotate, start the drag within the corresponding number input.\n\
466 To move, start the drag anywhere else in the example.",
467 app_status.selection
468 )
469 }
470}
471
472fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
475 for mut text in &mut help_text {
476 text.0 = create_help_string(&app_status);
477 }
478}