Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/clustered_decals.rs
9358 views
1
//! Demonstrates clustered decals, which affix decals to surfaces.
2
3
use std::f32::consts::{FRAC_PI_3, PI};
4
use std::fmt::{self, Formatter};
5
6
use 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
};
19
use ops::{acos, cos, sin};
20
use widgets::{
21
WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
22
BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
23
};
24
25
#[path = "../helpers/widgets.rs"]
26
mod widgets;
27
28
/// The custom material shader that we use to demonstrate how to use the decal
29
/// `tag` field.
30
const SHADER_ASSET_PATH: &str = "shaders/custom_clustered_decal.wgsl";
31
32
/// The speed at which the cube rotates, in radians per frame.
33
const 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.
37
const MOVE_SPEED: f32 = 0.008;
38
/// The speed at which the selection can be scaled, in reciprocal mouse units.
39
const SCALE_SPEED: f32 = 0.05;
40
/// The speed at which the selection can be scaled, in radians per mouse unit.
41
const ROLL_SPEED: f32 = 0.01;
42
43
/// Various settings for the demo.
44
#[derive(Resource, Default)]
45
struct 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)]
56
enum 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
68
impl 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)]
81
enum 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
95
impl 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)]
107
struct 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)]
112
struct CustomDecalExtension {}
113
114
impl MaterialExtension for CustomDecalExtension {
115
fn fragment_shader() -> ShaderRef {
116
SHADER_ASSET_PATH.into()
117
}
118
}
119
120
/// Entry point.
121
fn 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.
154
fn 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.
178
fn 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.
201
fn 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.
209
fn 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.
218
fn 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.
245
fn 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.
279
fn 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.
297
fn 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.
311
fn 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.
338
fn 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.
347
fn 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.
354
fn 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.
376
fn 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.
386
fn 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.
436
fn 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.
456
fn 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.
479
fn 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`].
491
fn 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.
527
fn 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.
535
fn 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
}
546
547