Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/light_textures.rs
6592 views
1
//! Demonstrates light textures, which modulate light sources.
2
3
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI};
4
use std::fmt::{self, Formatter};
5
6
use bevy::{
7
camera::primitives::CubemapLayout,
8
color::palettes::css::{SILVER, YELLOW},
9
input::mouse::AccumulatedMouseMotion,
10
light::{DirectionalLightTexture, NotShadowCaster, PointLightTexture, SpotLightTexture},
11
pbr::decal,
12
prelude::*,
13
render::renderer::{RenderAdapter, RenderDevice},
14
window::{CursorIcon, SystemCursorIcon},
15
};
16
use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE};
17
use ops::{acos, cos, sin};
18
use widgets::{
19
WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
20
BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
21
};
22
23
#[path = "../helpers/widgets.rs"]
24
mod widgets;
25
26
/// The speed at which the cube rotates, in radians per frame.
27
const CUBE_ROTATION_SPEED: f32 = 0.02;
28
29
/// The speed at which the selection can be moved, in spherical coordinate
30
/// radians per mouse unit.
31
const MOVE_SPEED: f32 = 0.008;
32
/// The speed at which the selection can be scaled, in reciprocal mouse units.
33
const SCALE_SPEED: f32 = 0.05;
34
/// The speed at which the selection can be scaled, in radians per mouse unit.
35
const ROLL_SPEED: f32 = 0.01;
36
37
/// Various settings for the demo.
38
#[derive(Resource, Default)]
39
struct AppStatus {
40
/// The object that will be moved, scaled, or rotated when the mouse is
41
/// dragged.
42
selection: Selection,
43
/// What happens when the mouse is dragged: one of a move, rotate, or scale
44
/// operation.
45
drag_mode: DragMode,
46
}
47
48
/// The object that will be moved, scaled, or rotated when the mouse is dragged.
49
#[derive(Clone, Copy, Component, Default, PartialEq)]
50
enum Selection {
51
/// The camera.
52
///
53
/// The camera can only be moved, not scaled or rotated.
54
#[default]
55
Camera,
56
/// The spotlight, which uses a torch-like light texture
57
SpotLight,
58
/// The point light, which uses a light texture cubemap constructed from the faces mesh
59
PointLight,
60
/// The directional light, which uses a caustic-like texture
61
DirectionalLight,
62
}
63
64
impl fmt::Display for Selection {
65
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
66
match *self {
67
Selection::Camera => f.write_str("camera"),
68
Selection::SpotLight => f.write_str("spotlight"),
69
Selection::PointLight => f.write_str("point light"),
70
Selection::DirectionalLight => f.write_str("directional light"),
71
}
72
}
73
}
74
75
/// What happens when the mouse is dragged: one of a move, rotate, or scale
76
/// operation.
77
#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
78
enum DragMode {
79
/// The mouse moves the current selection.
80
#[default]
81
Move,
82
/// The mouse scales the current selection.
83
///
84
/// This only applies to decals, not cameras.
85
Scale,
86
/// The mouse rotates the current selection around its local Z axis.
87
///
88
/// This only applies to decals, not cameras.
89
Roll,
90
}
91
92
impl fmt::Display for DragMode {
93
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94
match *self {
95
DragMode::Move => f.write_str("move"),
96
DragMode::Scale => f.write_str("scale"),
97
DragMode::Roll => f.write_str("roll"),
98
}
99
}
100
}
101
102
/// A marker component for the help text in the top left corner of the window.
103
#[derive(Clone, Copy, Component)]
104
struct HelpText;
105
106
/// Entry point.
107
fn main() {
108
App::new()
109
.add_plugins(DefaultPlugins.set(WindowPlugin {
110
primary_window: Some(Window {
111
title: "Bevy Light Textures Example".into(),
112
..default()
113
}),
114
..default()
115
}))
116
.init_resource::<AppStatus>()
117
.add_event::<WidgetClickEvent<Selection>>()
118
.add_event::<WidgetClickEvent<Visibility>>()
119
.add_systems(Startup, setup)
120
.add_systems(Update, draw_gizmos)
121
.add_systems(Update, rotate_cube)
122
.add_systems(Update, hide_shadows)
123
.add_systems(Update, widgets::handle_ui_interactions::<Selection>)
124
.add_systems(Update, widgets::handle_ui_interactions::<Visibility>)
125
.add_systems(
126
Update,
127
(handle_selection_change, update_radio_buttons)
128
.after(widgets::handle_ui_interactions::<Selection>)
129
.after(widgets::handle_ui_interactions::<Visibility>),
130
)
131
.add_systems(Update, toggle_visibility)
132
.add_systems(Update, update_directional_light)
133
.add_systems(Update, process_move_input)
134
.add_systems(Update, process_scale_input)
135
.add_systems(Update, process_roll_input)
136
.add_systems(Update, switch_drag_mode)
137
.add_systems(Update, update_help_text)
138
.add_systems(Update, update_button_visibility)
139
.run();
140
}
141
142
/// Creates the scene.
143
fn setup(
144
mut commands: Commands,
145
asset_server: Res<AssetServer>,
146
app_status: Res<AppStatus>,
147
render_device: Res<RenderDevice>,
148
render_adapter: Res<RenderAdapter>,
149
mut meshes: ResMut<Assets<Mesh>>,
150
mut materials: ResMut<Assets<StandardMaterial>>,
151
) {
152
// Error out if clustered decals (and so light textures) aren't supported on the current platform.
153
if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
154
error!("Light textures aren't usable on this platform.");
155
commands.write_event(AppExit::error());
156
}
157
158
spawn_cubes(&mut commands, &mut meshes, &mut materials);
159
spawn_camera(&mut commands);
160
spawn_light(&mut commands, &asset_server);
161
spawn_buttons(&mut commands);
162
spawn_help_text(&mut commands, &app_status);
163
spawn_light_textures(&mut commands, &asset_server, &mut meshes, &mut materials);
164
}
165
166
#[derive(Component)]
167
struct Rotate;
168
169
/// Spawns the cube onto which the decals are projected.
170
fn spawn_cubes(
171
commands: &mut Commands,
172
meshes: &mut Assets<Mesh>,
173
materials: &mut Assets<StandardMaterial>,
174
) {
175
// Rotate the cube a bit just to make it more interesting.
176
let mut transform = Transform::IDENTITY;
177
transform.rotate_y(FRAC_PI_3);
178
179
commands.spawn((
180
Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
181
MeshMaterial3d(materials.add(StandardMaterial {
182
base_color: SILVER.into(),
183
..default()
184
})),
185
transform,
186
Rotate,
187
));
188
189
commands.spawn((
190
Mesh3d(meshes.add(Cuboid::new(-13.0, -13.0, -13.0))),
191
MeshMaterial3d(materials.add(StandardMaterial {
192
base_color: SILVER.into(),
193
..default()
194
})),
195
transform,
196
));
197
}
198
199
/// Spawns the directional light.
200
fn spawn_light(commands: &mut Commands, asset_server: &AssetServer) {
201
commands.spawn((
202
Visibility::Hidden,
203
Transform::from_xyz(8.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
204
Selection::DirectionalLight,
205
children![(
206
DirectionalLight {
207
illuminance: AMBIENT_DAYLIGHT,
208
..default()
209
},
210
DirectionalLightTexture {
211
image: asset_server.load("lightmaps/caustic_directional_texture.png"),
212
tiled: true,
213
},
214
Visibility::Visible,
215
)],
216
));
217
}
218
219
/// Spawns the camera.
220
fn spawn_camera(commands: &mut Commands) {
221
commands
222
.spawn(Camera3d::default())
223
.insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
224
// Tag the camera with `Selection::Camera`.
225
.insert(Selection::Camera);
226
}
227
228
fn spawn_light_textures(
229
commands: &mut Commands,
230
asset_server: &AssetServer,
231
meshes: &mut Assets<Mesh>,
232
materials: &mut Assets<StandardMaterial>,
233
) {
234
commands.spawn((
235
SpotLight {
236
color: Color::srgb(1.0, 1.0, 0.8),
237
intensity: 10e6,
238
outer_angle: 0.25,
239
inner_angle: 0.25,
240
shadows_enabled: true,
241
..default()
242
},
243
Transform::from_translation(Vec3::new(6.0, 1.0, 2.0)).looking_at(Vec3::ZERO, Vec3::Y),
244
SpotLightTexture {
245
image: asset_server.load("lightmaps/torch_spotlight_texture.png"),
246
},
247
Visibility::Inherited,
248
Selection::SpotLight,
249
));
250
251
commands.spawn((
252
Visibility::Hidden,
253
Transform::from_translation(Vec3::new(0.0, 1.8, 0.01)).with_scale(Vec3::splat(0.1)),
254
Selection::PointLight,
255
children![
256
SceneRoot(
257
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/Faces/faces.glb")),
258
),
259
(
260
Mesh3d(meshes.add(Sphere::new(1.0))),
261
MeshMaterial3d(materials.add(StandardMaterial {
262
emissive: Color::srgb(0.0, 0.0, 300.0).to_linear(),
263
..default()
264
})),
265
),
266
(
267
PointLight {
268
color: Color::srgb(0.0, 0.0, 1.0),
269
intensity: 1e6,
270
shadows_enabled: true,
271
..default()
272
},
273
PointLightTexture {
274
image: asset_server.load("lightmaps/faces_pointlight_texture_blurred.png"),
275
cubemap_layout: CubemapLayout::CrossVertical,
276
},
277
)
278
],
279
));
280
}
281
282
/// Spawns the buttons at the bottom of the screen.
283
fn spawn_buttons(commands: &mut Commands) {
284
// Spawn the radio buttons that allow the user to select an object to
285
// control.
286
commands.spawn((
287
widgets::main_ui_node(),
288
children![widgets::option_buttons(
289
"Drag to Move",
290
&[
291
(Selection::Camera, "Camera"),
292
(Selection::SpotLight, "Spotlight"),
293
(Selection::PointLight, "Point Light"),
294
(Selection::DirectionalLight, "Directional Light"),
295
],
296
)],
297
));
298
299
// Spawn the drag buttons that allow the user to control the scale and roll
300
// of the selected object.
301
commands.spawn((
302
Node {
303
flex_direction: FlexDirection::Row,
304
position_type: PositionType::Absolute,
305
right: px(10),
306
bottom: px(10),
307
column_gap: px(6),
308
..default()
309
},
310
children![
311
widgets::option_buttons(
312
"",
313
&[
314
(Visibility::Inherited, "Show"),
315
(Visibility::Hidden, "Hide"),
316
],
317
),
318
(drag_button("Scale"), DragMode::Scale),
319
(drag_button("Roll"), DragMode::Roll),
320
],
321
));
322
}
323
324
/// Spawns a button that the user can drag to change a parameter.
325
fn drag_button(label: &str) -> impl Bundle {
326
(
327
Node {
328
border: BUTTON_BORDER,
329
justify_content: JustifyContent::Center,
330
align_items: AlignItems::Center,
331
padding: BUTTON_PADDING,
332
..default()
333
},
334
Button,
335
BackgroundColor(Color::BLACK),
336
BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
337
BUTTON_BORDER_COLOR,
338
children![widgets::ui_text(label, Color::WHITE),],
339
)
340
}
341
342
/// Spawns the help text at the top of the screen.
343
fn 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
356
/// Draws the outlines that show the bounds of the spotlight.
357
fn draw_gizmos(mut gizmos: Gizmos, spotlight: Query<(&GlobalTransform, &SpotLight, &Visibility)>) {
358
if let Ok((global_transform, spotlight, visibility)) = spotlight.single()
359
&& visibility != Visibility::Hidden
360
{
361
gizmos.primitive_3d(
362
&Cone::new(7.0 * spotlight.outer_angle, 7.0),
363
Isometry3d {
364
rotation: global_transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
365
translation: global_transform.translation_vec3a() * 0.5,
366
},
367
YELLOW,
368
);
369
}
370
}
371
372
/// Rotates the cube a bit every frame.
373
fn rotate_cube(mut meshes: Query<&mut Transform, With<Rotate>>) {
374
for mut transform in &mut meshes {
375
transform.rotate_y(CUBE_ROTATION_SPEED);
376
}
377
}
378
379
/// Hide shadows on all meshes except the main cube
380
fn hide_shadows(
381
mut commands: Commands,
382
meshes: Query<Entity, (With<Mesh3d>, Without<NotShadowCaster>, Without<Rotate>)>,
383
) {
384
for ent in &meshes {
385
commands.entity(ent).insert(NotShadowCaster);
386
}
387
}
388
389
/// Updates the state of the radio buttons when the user clicks on one.
390
fn update_radio_buttons(
391
mut widgets: Query<(
392
Entity,
393
Option<&mut BackgroundColor>,
394
Has<Text>,
395
&WidgetClickSender<Selection>,
396
)>,
397
app_status: Res<AppStatus>,
398
mut writer: TextUiWriter,
399
visible: Query<(&Visibility, &Selection)>,
400
mut visibility_widgets: Query<
401
(
402
Entity,
403
Option<&mut BackgroundColor>,
404
Has<Text>,
405
&WidgetClickSender<Visibility>,
406
),
407
Without<WidgetClickSender<Selection>>,
408
>,
409
) {
410
for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
411
let selected = app_status.selection == **sender;
412
if let Some(mut bg_color) = maybe_bg_color {
413
widgets::update_ui_radio_button(&mut bg_color, selected);
414
}
415
if has_text {
416
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
417
}
418
}
419
420
let visibility = visible
421
.iter()
422
.filter(|(_, selection)| **selection == app_status.selection)
423
.map(|(visibility, _)| *visibility)
424
.next()
425
.unwrap_or_default();
426
for (entity, maybe_bg_color, has_text, sender) in &mut visibility_widgets {
427
if let Some(mut bg_color) = maybe_bg_color {
428
widgets::update_ui_radio_button(&mut bg_color, **sender == visibility);
429
}
430
if has_text {
431
widgets::update_ui_radio_button_text(entity, &mut writer, **sender == visibility);
432
}
433
}
434
}
435
436
/// Changes the selection when the user clicks a radio button.
437
fn handle_selection_change(
438
mut events: EventReader<WidgetClickEvent<Selection>>,
439
mut app_status: ResMut<AppStatus>,
440
) {
441
for event in events.read() {
442
app_status.selection = **event;
443
}
444
}
445
446
fn toggle_visibility(
447
mut events: EventReader<WidgetClickEvent<Visibility>>,
448
app_status: Res<AppStatus>,
449
mut visibility: Query<(&mut Visibility, &Selection)>,
450
) {
451
if let Some(vis) = events.read().last() {
452
for (mut visibility, selection) in visibility.iter_mut() {
453
if selection == &app_status.selection {
454
*visibility = **vis;
455
}
456
}
457
}
458
}
459
460
/// Process a drag event that moves the selected object.
461
fn process_move_input(
462
mut selections: Query<(&mut Transform, &Selection)>,
463
mouse_buttons: Res<ButtonInput<MouseButton>>,
464
mouse_motion: Res<AccumulatedMouseMotion>,
465
app_status: Res<AppStatus>,
466
) {
467
// Only process drags when movement is selected.
468
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
469
return;
470
}
471
472
for (mut transform, selection) in &mut selections {
473
if app_status.selection != *selection {
474
continue;
475
}
476
477
// use simple movement for the point light
478
if *selection == Selection::PointLight {
479
transform.translation +=
480
(mouse_motion.delta * Vec2::new(1.0, -1.0) * MOVE_SPEED).extend(0.0);
481
return;
482
}
483
484
let position = transform.translation;
485
486
// Convert to spherical coordinates.
487
let radius = position.length();
488
let mut theta = acos(position.y / radius);
489
let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
490
491
// Camera movement is the inverse of object movement.
492
let (phi_factor, theta_factor) = match *selection {
493
Selection::Camera => (1.0, -1.0),
494
_ => (-1.0, 1.0),
495
};
496
497
// Adjust the spherical coordinates. Clamp the inclination to (0, π).
498
phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
499
theta = f32::clamp(
500
theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
501
0.001,
502
PI - 0.001,
503
);
504
505
// Convert spherical coordinates back to Cartesian coordinates.
506
transform.translation =
507
radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
508
509
// Look at the center, but preserve the previous roll angle.
510
let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
511
transform.look_at(Vec3::ZERO, Vec3::Y);
512
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
513
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
514
}
515
}
516
517
/// Processes a drag event that scales the selected target.
518
fn process_scale_input(
519
mut scale_selections: Query<(&mut Transform, &Selection)>,
520
mut spotlight_selections: Query<(&mut SpotLight, &Selection)>,
521
mouse_buttons: Res<ButtonInput<MouseButton>>,
522
mouse_motion: Res<AccumulatedMouseMotion>,
523
app_status: Res<AppStatus>,
524
) {
525
// Only process drags when the scaling operation is selected.
526
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
527
return;
528
}
529
530
for (mut transform, selection) in &mut scale_selections {
531
if app_status.selection == *selection {
532
transform.scale = (transform.scale * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
533
.clamp(Vec3::splat(0.01), Vec3::splat(5.0));
534
}
535
}
536
537
for (mut spotlight, selection) in &mut spotlight_selections {
538
if app_status.selection == *selection {
539
spotlight.outer_angle = (spotlight.outer_angle
540
* (1.0 + mouse_motion.delta.x * SCALE_SPEED))
541
.clamp(0.01, FRAC_PI_4);
542
spotlight.inner_angle = spotlight.outer_angle;
543
}
544
}
545
}
546
547
/// Processes a drag event that rotates the selected target along its local Z
548
/// axis.
549
fn process_roll_input(
550
mut selections: Query<(&mut Transform, &Selection)>,
551
mouse_buttons: Res<ButtonInput<MouseButton>>,
552
mouse_motion: Res<AccumulatedMouseMotion>,
553
app_status: Res<AppStatus>,
554
) {
555
// Only process drags when the rolling operation is selected.
556
if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
557
return;
558
}
559
560
for (mut transform, selection) in &mut selections {
561
if app_status.selection != *selection {
562
continue;
563
}
564
565
let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
566
roll += mouse_motion.delta.x * ROLL_SPEED;
567
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
568
}
569
}
570
571
/// Creates the help string at the top left of the screen.
572
fn create_help_string(app_status: &AppStatus) -> String {
573
format!(
574
"Click and drag to {} {}",
575
app_status.drag_mode, app_status.selection
576
)
577
}
578
579
/// Changes the drag mode when the user hovers over the "Scale" and "Roll"
580
/// buttons in the lower right.
581
///
582
/// If the user is hovering over no such button, this system changes the drag
583
/// mode back to its default value of [`DragMode::Move`].
584
fn switch_drag_mode(
585
mut commands: Commands,
586
mut interactions: Query<(&Interaction, &DragMode)>,
587
mut windows: Query<Entity, With<Window>>,
588
mouse_buttons: Res<ButtonInput<MouseButton>>,
589
mut app_status: ResMut<AppStatus>,
590
) {
591
if mouse_buttons.pressed(MouseButton::Left) {
592
return;
593
}
594
595
for (interaction, drag_mode) in &mut interactions {
596
if *interaction != Interaction::Hovered {
597
continue;
598
}
599
600
app_status.drag_mode = *drag_mode;
601
602
// Set the cursor to provide the user with a nice visual hint.
603
for window in &mut windows {
604
commands
605
.entity(window)
606
.insert(CursorIcon::from(SystemCursorIcon::EwResize));
607
}
608
return;
609
}
610
611
app_status.drag_mode = DragMode::Move;
612
613
for window in &mut windows {
614
commands.entity(window).remove::<CursorIcon>();
615
}
616
}
617
618
/// Updates the help text in the top left of the screen to reflect the current
619
/// selection and drag mode.
620
fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
621
for mut text in &mut help_text {
622
text.0 = create_help_string(&app_status);
623
}
624
}
625
626
/// Updates the visibility of the drag mode buttons so that they aren't visible
627
/// if the camera is selected.
628
fn update_button_visibility(
629
mut nodes: Query<&mut Visibility, Or<(With<DragMode>, With<WidgetClickSender<Visibility>>)>>,
630
app_status: Res<AppStatus>,
631
) {
632
for mut visibility in &mut nodes {
633
*visibility = match app_status.selection {
634
Selection::Camera => Visibility::Hidden,
635
_ => Visibility::Visible,
636
};
637
}
638
}
639
640
fn update_directional_light(
641
mut commands: Commands,
642
asset_server: Res<AssetServer>,
643
selections: Query<(&Selection, &Visibility)>,
644
mut light: Query<(
645
Entity,
646
&mut DirectionalLight,
647
Option<&DirectionalLightTexture>,
648
)>,
649
) {
650
let directional_visible = selections
651
.iter()
652
.filter(|(selection, _)| **selection == Selection::DirectionalLight)
653
.any(|(_, visibility)| visibility != Visibility::Hidden);
654
let any_texture_light_visible = selections
655
.iter()
656
.filter(|(selection, _)| {
657
**selection == Selection::PointLight || **selection == Selection::SpotLight
658
})
659
.any(|(_, visibility)| visibility != Visibility::Hidden);
660
661
let (entity, mut light, maybe_texture) = light
662
.single_mut()
663
.expect("there should be a single directional light");
664
665
if directional_visible {
666
light.illuminance = AMBIENT_DAYLIGHT;
667
if maybe_texture.is_none() {
668
commands.entity(entity).insert(DirectionalLightTexture {
669
image: asset_server.load("lightmaps/caustic_directional_texture.png"),
670
tiled: true,
671
});
672
}
673
} else if any_texture_light_visible {
674
light.illuminance = CLEAR_SUNRISE;
675
if maybe_texture.is_some() {
676
commands.entity(entity).remove::<DirectionalLightTexture>();
677
}
678
} else {
679
light.illuminance = AMBIENT_DAYLIGHT;
680
if maybe_texture.is_some() {
681
commands.entity(entity).remove::<DirectionalLightTexture>();
682
}
683
}
684
}
685
686