Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/light_probe_blending.rs
9308 views
1
//! Demonstrates blending between multiple reflection probes.
2
//!
3
//! This example shows a reflective sphere that moves between two rooms, each of
4
//! which contains a reflection probe with a falloff range. Bevy performs a
5
//! blend between the two reflection probes as the sphere moves.
6
7
use std::f32::consts::{FRAC_PI_4, PI};
8
9
use bevy::{
10
camera::Hdr,
11
camera_controller::free_camera::{self, FreeCamera, FreeCameraPlugin},
12
color::palettes::css::{CORNFLOWER_BLUE, CRIMSON, TAN, WHITE},
13
input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll},
14
light::ParallaxCorrection,
15
math::ops::{atan2, cos, sin},
16
prelude::*,
17
window::{CursorGrabMode, CursorOptions},
18
};
19
20
use crate::widgets::{WidgetClickEvent, WidgetClickSender};
21
22
#[path = "../helpers/widgets.rs"]
23
mod widgets;
24
25
/// The settings that the user has chosen.
26
#[derive(Resource, Default)]
27
struct AppStatus {
28
/// Whether the gizmos that show the boundaries of the light probe regions
29
/// are to be shown.
30
gizmos_enabled: GizmosEnabled,
31
/// Which object to show: either a reflective sphere or a reflective prism.
32
object_to_show: ObjectToShow,
33
/// Whether to use an orbital pan/zoom camera or a free camera.
34
camera_mode: CameraMode,
35
}
36
37
/// Whether the gizmos that show the boundaries of the light probe regions are
38
/// to be shown.
39
#[derive(Clone, Copy, Default, PartialEq)]
40
enum GizmosEnabled {
41
/// The gizmos are shown.
42
#[default]
43
On,
44
/// The gizmos are hidden.
45
Off,
46
}
47
48
/// Which reflective object to show.
49
#[derive(Clone, Copy, Default, PartialEq)]
50
enum ObjectToShow {
51
/// A reflective sphere that moves between rooms.
52
#[default]
53
Sphere,
54
/// A reflective prism that is static and stretches across the length of the
55
/// two rooms.
56
Prism,
57
}
58
59
/// How the user can control the camera.
60
#[derive(Clone, Copy, Default, PartialEq)]
61
enum CameraMode {
62
/// The camera is a pan/zoom orbital camera controllable with dragging and
63
/// the mouse wheel.
64
#[default]
65
Orbit,
66
/// The camera is a free camera controllable by clicking and dragging and
67
/// using the WASDEQ controls.
68
Free,
69
}
70
71
/// A marker component for the reflective sphere.
72
#[derive(Clone, Copy, Component, Debug)]
73
struct ReflectiveSphere;
74
75
/// A marker component for the reflective prism.
76
#[derive(Clone, Copy, Component, Debug)]
77
struct ReflectivePrism;
78
79
/// A marker component for the help text at the top of the screen.
80
#[derive(Clone, Copy, Component, Debug)]
81
struct HelpText;
82
83
/// The speed at which the sphere moves, as a ratio of the total distance it
84
/// travels to seconds.
85
///
86
/// Specifically, the value of 0.3 means that it moves 3/10 of the way to the
87
/// other side per second.
88
const SPHERE_MOVEMENT_SPEED: f32 = 0.3;
89
90
/// The side length of each room, in meters.
91
const ROOM_SIDE_LENGTH: f32 = 10.0;
92
93
/// The number of meters that separates the center of each room.
94
const ROOM_SEPARATION: f32 = 11.0;
95
96
/// The side length of the light probe cube, in meters.
97
const LIGHT_PROBE_SIDE_LENGTH: f32 = 15.0;
98
99
/// The distance over which the light probe fades out, expressed as a fraction
100
/// of the side length of the probe.
101
const LIGHT_PROBE_FALLOFF: f32 = 0.5;
102
103
/// The side length of the simulated reflected area for each light probe,
104
/// specified as a half-extent in light probe space.
105
///
106
/// We want this side length, in world space, to be half of the world-space room
107
/// side length. Since the light probe is scaled by `LIGHT_PROBE_SIDE_LENGTH`,
108
/// we divide the room side length by the light probe side length to get this
109
/// value, and multiply by 0.5 to convert from a full extent to a half-extent.
110
/// That way, when Bevy applies the `LIGHT_PROBE_SIDE_LENGTH` scale, the light
111
/// probe side length factor cancels, and we're left with a parallax correction
112
/// side length of `ROOM_SIDE_LENGTH` in world space.
113
///
114
/// A small epsilon value of 0.01 is added in order to ensure that the light
115
/// probe parallax bounds encompass the entire room. Otherwise, unsightly
116
/// Z-fighting can occur on the room walls.
117
const LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH: f32 =
118
ROOM_SIDE_LENGTH / LIGHT_PROBE_SIDE_LENGTH * 0.5 + 0.01;
119
120
/// The number of radians of inclination (pitch) that one pixel of mouse
121
/// movement corresponds to.
122
const CAMERA_ORBIT_SPEED_INCLINATION: f32 = 0.003;
123
124
/// The number of radians of azumith (yaw) that one pixel of mouse movement
125
/// corresponds to.
126
const CAMERA_ORBIT_SPEED_AZIMUTH: f32 = 0.004;
127
128
/// The number of meters that one line of mouse scroll corresponds to.
129
const CAMERA_ZOOM_SPEED: f32 = 0.15;
130
131
/// Information about the orbital pan/zoom camera.
132
///
133
/// These are in [spherical coordinates].
134
///
135
/// [spherical coordinates]: https://en.wikipedia.org/wiki/Spherical_coordinate_system
136
#[derive(Component)]
137
struct OrbitCamera {
138
/// The distance between the camera and the sphere, in meters.
139
radius: f32,
140
/// The camera latitude in radians, relative to the sphere.
141
inclination: f32,
142
/// The camera longitude in radians, relative to the sphere.
143
azimuth: f32,
144
}
145
146
/// The brightness of the light probe.
147
const LIGHT_PROBE_INTENSITY: f32 = 500.0;
148
149
/// The entry point.
150
fn main() {
151
App::new()
152
.add_plugins(DefaultPlugins.set(WindowPlugin {
153
primary_window: Some(Window {
154
title: "Bevy Light Probe Blending Example".into(),
155
..default()
156
}),
157
..default()
158
}))
159
.add_plugins(FreeCameraPlugin)
160
.init_resource::<AppStatus>()
161
.add_message::<WidgetClickEvent<GizmosEnabled>>()
162
.add_message::<WidgetClickEvent<ObjectToShow>>()
163
.add_message::<WidgetClickEvent<CameraMode>>()
164
.add_systems(Startup, setup)
165
.add_systems(Update, (move_sphere, orbit_camera).chain())
166
.add_systems(
167
Update,
168
(
169
widgets::handle_ui_interactions::<GizmosEnabled>,
170
handle_gizmos_enabled_change,
171
)
172
.chain(),
173
)
174
.add_systems(
175
Update,
176
(
177
widgets::handle_ui_interactions::<ObjectToShow>,
178
handle_object_to_show_change,
179
)
180
.chain(),
181
)
182
.add_systems(
183
Update,
184
(
185
widgets::handle_ui_interactions::<CameraMode>,
186
handle_camera_mode_change,
187
)
188
.chain()
189
.after(free_camera::run_freecamera_controller),
190
)
191
.add_systems(
192
Update,
193
update_radio_buttons
194
.after(widgets::handle_ui_interactions::<GizmosEnabled>)
195
.after(widgets::handle_ui_interactions::<ObjectToShow>)
196
.after(widgets::handle_ui_interactions::<CameraMode>),
197
)
198
.add_systems(Update, draw_gizmos)
199
.run();
200
}
201
202
/// Performs initial setup of the scene.
203
fn setup(
204
mut commands: Commands,
205
asset_server: Res<AssetServer>,
206
mut meshes: ResMut<Assets<Mesh>>,
207
mut materials: ResMut<Assets<StandardMaterial>>,
208
mut gizmo_config_store: ResMut<GizmoConfigStore>,
209
) {
210
adjust_gizmo_settings(&mut gizmo_config_store);
211
212
let reflective_material = create_reflective_material(&mut materials);
213
214
spawn_camera(&mut commands);
215
spawn_gltf_scene(&mut commands, &asset_server);
216
spawn_reflective_sphere(&mut commands, &mut meshes, reflective_material.clone());
217
spawn_reflective_prism(&mut commands, &mut meshes, reflective_material);
218
spawn_light_probes(&mut commands, &asset_server);
219
spawn_buttons(&mut commands);
220
spawn_help_text(&mut commands);
221
}
222
223
/// Adjusts the gizmo settings so that the gizmos appear on top of all other
224
/// geometry.
225
///
226
/// If we didn't do this, then the rooms would cover up many of the gizmos.
227
fn adjust_gizmo_settings(gizmo_config_store: &mut GizmoConfigStore) {
228
for (_, gizmo_config, _) in &mut gizmo_config_store.iter_mut() {
229
gizmo_config.depth_bias = -1.0;
230
}
231
}
232
233
/// Creates the perfectly-reflective material that the sphere and prism use.
234
fn create_reflective_material(
235
materials: &mut Assets<StandardMaterial>,
236
) -> Handle<StandardMaterial> {
237
materials.add(StandardMaterial {
238
base_color: WHITE.into(),
239
metallic: 1.0,
240
reflectance: 1.0,
241
perceptual_roughness: 0.0,
242
..default()
243
})
244
}
245
246
/// Spawns the orbital pan/zoom camera.
247
fn spawn_camera(commands: &mut Commands) {
248
commands.spawn((
249
Camera3d::default(),
250
Transform::IDENTITY,
251
Hdr,
252
OrbitCamera {
253
radius: 3.0,
254
inclination: 7.0 * FRAC_PI_4,
255
azimuth: FRAC_PI_4,
256
},
257
));
258
}
259
260
/// Spawns the glTF scene that contains the two rooms.
261
fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) {
262
commands.spawn(SceneRoot(asset_server.load(
263
GltfAssetLabel::Scene(0).from_asset(get_web_asset_url("two_rooms.glb")),
264
)));
265
}
266
267
/// Spawns the reflective sphere, creating its mesh in the process.
268
fn spawn_reflective_sphere(
269
commands: &mut Commands,
270
meshes: &mut Assets<Mesh>,
271
material: Handle<StandardMaterial>,
272
) {
273
// Create a mesh.
274
let sphere = meshes.add(Sphere::default().mesh().uv(32, 18));
275
276
// Spawn the sphere.
277
commands.spawn((
278
Mesh3d(sphere),
279
MeshMaterial3d(material),
280
Transform::IDENTITY,
281
ReflectiveSphere,
282
));
283
}
284
285
/// Spawns the reflective prism, creating its mesh in the process.
286
///
287
/// The reflective prism starts invisible, but the user can toggle it on and off
288
/// as desired.
289
fn spawn_reflective_prism(
290
commands: &mut Commands,
291
meshes: &mut Assets<Mesh>,
292
material: Handle<StandardMaterial>,
293
) {
294
// Create a mesh.
295
let cube = meshes.add(
296
Cuboid {
297
half_size: vec3(2.0, 1.0, 10.0),
298
}
299
.mesh()
300
.build()
301
// We use flat normals so that the surface appears flat, not curved.
302
.with_duplicated_vertices()
303
.with_computed_flat_normals(),
304
);
305
306
// Spawn the cube.
307
commands.spawn((
308
Mesh3d(cube),
309
MeshMaterial3d(material),
310
Transform::from_xyz(0.0, -4.0, -5.5),
311
ReflectivePrism,
312
Visibility::Hidden,
313
));
314
}
315
316
/// Spawns the two light probes, one for each room.
317
fn spawn_light_probes(commands: &mut Commands, asset_server: &AssetServer) {
318
// Spawn the first room's light probe.
319
commands.spawn((
320
LightProbe {
321
falloff: Vec3::splat(LIGHT_PROBE_FALLOFF),
322
},
323
EnvironmentMapLight {
324
diffuse_map: asset_server.load(get_web_asset_url("diffuse_room1.ktx2")),
325
specular_map: asset_server.load(get_web_asset_url("specular_room1.ktx2")),
326
intensity: LIGHT_PROBE_INTENSITY,
327
..default()
328
},
329
Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH)
330
.with_rotation(Quat::from_rotation_x(PI)),
331
ParallaxCorrection::Custom(Vec3::splat(LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH)),
332
));
333
334
// Spawn the second room's light probe.
335
commands.spawn((
336
LightProbe {
337
falloff: Vec3::splat(LIGHT_PROBE_FALLOFF),
338
},
339
EnvironmentMapLight {
340
diffuse_map: asset_server.load(get_web_asset_url("diffuse_room2.ktx2")),
341
specular_map: asset_server.load(get_web_asset_url("specular_room2.ktx2")),
342
intensity: LIGHT_PROBE_INTENSITY,
343
..default()
344
},
345
Transform::from_scale(vec3(1.0, -1.0, 1.0) * LIGHT_PROBE_SIDE_LENGTH)
346
.with_rotation(Quat::from_rotation_x(PI))
347
.with_translation(vec3(0.0, 0.0, -ROOM_SEPARATION)),
348
ParallaxCorrection::Custom(Vec3::splat(LIGHT_PROBE_PARALLAX_CORRECTION_SIDE_LENGTH)),
349
));
350
}
351
352
/// Spawns the radio buttons at the bottom of the screen.
353
fn spawn_buttons(commands: &mut Commands) {
354
commands.spawn((
355
widgets::main_ui_node(),
356
children![
357
widgets::option_buttons(
358
"Gizmos",
359
&[(GizmosEnabled::On, "On"), (GizmosEnabled::Off, "Off"),]
360
),
361
widgets::option_buttons(
362
"Object to Show",
363
&[
364
(ObjectToShow::Sphere, "Sphere"),
365
(ObjectToShow::Prism, "Prism"),
366
]
367
),
368
widgets::option_buttons(
369
"Camera Mode",
370
&[(CameraMode::Orbit, "Orbit"), (CameraMode::Free, "Free"),]
371
),
372
],
373
));
374
}
375
376
/// Spawns the help text at the top of the screen.
377
fn spawn_help_text(commands: &mut Commands) {
378
commands.spawn((
379
Text::new(""),
380
Node {
381
position_type: PositionType::Absolute,
382
top: px(12),
383
left: px(12),
384
..default()
385
},
386
HelpText,
387
));
388
}
389
390
/// Moves the sphere a bit every frame.
391
fn move_sphere(mut spheres: Query<&mut Transform, With<ReflectiveSphere>>, time: Res<Time>) {
392
let Some(t) = SmoothStepCurve
393
.ping_pong()
394
.unwrap()
395
.forever()
396
.unwrap()
397
.sample(time.elapsed_secs() * SPHERE_MOVEMENT_SPEED)
398
else {
399
return;
400
};
401
for mut sphere_transform in &mut spheres {
402
sphere_transform.translation.z = -ROOM_SEPARATION * t;
403
}
404
}
405
406
/// Processes requests from the user to move the camera.
407
fn orbit_camera(
408
mut cameras: Query<(&mut Transform, &mut OrbitCamera)>,
409
spheres: Query<&Transform, (With<ReflectiveSphere>, Without<OrbitCamera>)>,
410
mouse_buttons: Res<ButtonInput<MouseButton>>,
411
mouse_motion: Res<AccumulatedMouseMotion>,
412
mouse_scroll: Res<AccumulatedMouseScroll>,
413
) {
414
// Grab the sphere transform.
415
let Some(sphere_transform) = spheres.iter().next() else {
416
return;
417
};
418
419
for (mut camera_transform, mut orbit_camera) in &mut cameras {
420
// Only pan if the left mouse button is pressed.
421
if mouse_buttons.pressed(MouseButton::Left) {
422
let delta = mouse_motion.delta;
423
orbit_camera.azimuth -= delta.x * CAMERA_ORBIT_SPEED_AZIMUTH;
424
orbit_camera.inclination += delta.y * CAMERA_ORBIT_SPEED_INCLINATION;
425
}
426
427
// Zooming doesn't require a mouse button press, as it uses the mouse
428
// wheel.
429
orbit_camera.radius =
430
(orbit_camera.radius - CAMERA_ZOOM_SPEED * mouse_scroll.delta.y).max(0.01);
431
432
// Calculate the new translation using the [spherical coordinates
433
// formula].
434
//
435
// [spherical coordinates formula]:
436
// https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
437
let new_translation = orbit_camera.radius
438
* vec3(
439
sin(orbit_camera.inclination) * cos(orbit_camera.azimuth),
440
cos(orbit_camera.inclination),
441
sin(orbit_camera.inclination) * sin(orbit_camera.azimuth),
442
);
443
444
// Write in the new transform.
445
*camera_transform =
446
Transform::from_translation(new_translation + sphere_transform.translation)
447
.looking_at(sphere_transform.translation, Vec3::Y);
448
}
449
}
450
451
/// A system that toggles gizmos on or off when the user clicks on one of the
452
/// corresponding radio buttons.
453
fn handle_gizmos_enabled_change(
454
mut help_text_query: Query<&mut Text, With<HelpText>>,
455
mut app_status: ResMut<AppStatus>,
456
mut messages: MessageReader<WidgetClickEvent<GizmosEnabled>>,
457
) {
458
let mut any_changes = false;
459
for message in messages.read() {
460
app_status.gizmos_enabled = **message;
461
any_changes = true;
462
}
463
464
if any_changes {
465
set_help_text(&app_status, &mut help_text_query);
466
}
467
}
468
469
/// A system that toggles object visibility when the user clicks on one of the
470
/// corresponding radio buttons.
471
fn handle_object_to_show_change(
472
mut spheres_query: Query<&mut Visibility, (With<ReflectiveSphere>, Without<ReflectivePrism>)>,
473
mut prisms_query: Query<&mut Visibility, (With<ReflectivePrism>, Without<ReflectiveSphere>)>,
474
mut app_status: ResMut<AppStatus>,
475
mut messages: MessageReader<WidgetClickEvent<ObjectToShow>>,
476
) {
477
for message in messages.read() {
478
app_status.object_to_show = **message;
479
480
for mut sphere_visibility in &mut spheres_query {
481
*sphere_visibility = match **message {
482
ObjectToShow::Sphere => Visibility::Inherited,
483
ObjectToShow::Prism => Visibility::Hidden,
484
}
485
}
486
for mut prism_visibility in &mut prisms_query {
487
*prism_visibility = match **message {
488
ObjectToShow::Sphere => Visibility::Hidden,
489
ObjectToShow::Prism => Visibility::Inherited,
490
}
491
}
492
}
493
}
494
495
/// A system that toggles the camera mode when the user clicks on one of the
496
/// corresponding radio buttons.
497
fn handle_camera_mode_change(
498
mut commands: Commands,
499
cameras_query: Query<(Entity, &Transform), With<Camera3d>>,
500
sphere_query: Query<&Transform, (With<ReflectiveSphere>, Without<Camera3d>)>,
501
mut help_text_query: Query<&mut Text, With<HelpText>>,
502
mut windows_query: Query<&mut CursorOptions>,
503
mut app_status: ResMut<AppStatus>,
504
mut messages: MessageReader<WidgetClickEvent<CameraMode>>,
505
) {
506
let Some(sphere_transform) = sphere_query.iter().next() else {
507
return;
508
};
509
510
let mut any_changes = false;
511
for message in messages.read() {
512
app_status.camera_mode = **message;
513
514
match **message {
515
CameraMode::Orbit => {
516
for (camera_entity, camera_transform) in &cameras_query {
517
// Convert from Cartesian coordinates back to spherical
518
// coordinates.
519
let relative_camera_position =
520
camera_transform.translation - sphere_transform.translation;
521
let radius = relative_camera_position.length();
522
let inclination = atan2(
523
relative_camera_position.xz().length() / radius,
524
relative_camera_position.y / radius,
525
);
526
let azimuth = atan2(
527
relative_camera_position.z * relative_camera_position.xz().length_recip(),
528
relative_camera_position.x * relative_camera_position.xz().length_recip(),
529
);
530
531
commands
532
.entity(camera_entity)
533
.remove::<FreeCamera>()
534
.insert(OrbitCamera {
535
radius,
536
inclination,
537
azimuth,
538
});
539
}
540
}
541
542
CameraMode::Free => {
543
for (camera_entity, _) in &cameras_query {
544
commands
545
.entity(camera_entity)
546
.remove::<OrbitCamera>()
547
.insert(FreeCamera::default());
548
}
549
}
550
}
551
552
any_changes = true;
553
}
554
555
if any_changes {
556
set_help_text(&app_status, &mut help_text_query);
557
558
// Reset the cursor grab mode, because the free camera controller may
559
// have enabled it, and we don't want the cursor to disappear.
560
for mut cursor_options in &mut windows_query {
561
cursor_options.grab_mode = CursorGrabMode::None;
562
cursor_options.visible = true;
563
}
564
}
565
}
566
567
/// A system that updates the radio buttons at the bottom of the screen to
568
/// reflect whether gizmos are enabled or not.
569
fn update_radio_buttons(
570
mut widgets_query: Query<(
571
Entity,
572
Option<&mut BackgroundColor>,
573
Has<Text>,
574
AnyOf<(
575
&WidgetClickSender<GizmosEnabled>,
576
&WidgetClickSender<ObjectToShow>,
577
&WidgetClickSender<CameraMode>,
578
)>,
579
)>,
580
app_status: Res<AppStatus>,
581
mut text_ui_writer: TextUiWriter,
582
) {
583
for (
584
entity,
585
maybe_bg_color,
586
has_text,
587
(maybe_gizmos_enabled, maybe_object_to_show, maybe_camera_mode),
588
) in &mut widgets_query
589
{
590
let selected = if let Some(sender) = maybe_gizmos_enabled {
591
app_status.gizmos_enabled == **sender
592
} else if let Some(sender) = maybe_object_to_show {
593
app_status.object_to_show == **sender
594
} else if let Some(sender) = maybe_camera_mode {
595
app_status.camera_mode == **sender
596
} else {
597
continue;
598
};
599
600
if let Some(mut bg_color) = maybe_bg_color {
601
widgets::update_ui_radio_button(&mut bg_color, selected);
602
}
603
if has_text {
604
widgets::update_ui_radio_button_text(entity, &mut text_ui_writer, selected);
605
}
606
}
607
}
608
609
/// Draws gizmos that show the boundaries of the various boxes associated with
610
/// the light probes in the scene.
611
fn draw_gizmos(
612
light_probes: Query<(&LightProbe, &ParallaxCorrection, &Transform)>,
613
app_status: Res<AppStatus>,
614
mut gizmos: Gizmos,
615
) {
616
// If the user has gizmos disabled, bail.
617
if matches!(app_status.gizmos_enabled, GizmosEnabled::Off) {
618
return;
619
}
620
621
for (light_probe, parallax_correction, transform) in &light_probes {
622
// Draw light probe bounds.
623
gizmos.cube(*transform, TAN);
624
625
// Draw light probe falloff.
626
gizmos.cube(
627
Transform {
628
scale: transform.scale * (Vec3::ONE - light_probe.falloff),
629
..*transform
630
},
631
CRIMSON,
632
);
633
634
// Draw light probe parallax correction bounds.
635
if let ParallaxCorrection::Custom(parallax_correction_bounds) = *parallax_correction {
636
gizmos.cube(
637
Transform {
638
scale: transform.scale * parallax_correction_bounds,
639
..*transform
640
},
641
CORNFLOWER_BLUE,
642
);
643
}
644
}
645
}
646
647
/// Updates the help text at the top of the screen to reflect a change in camera
648
/// or gizmo application settings.
649
fn set_help_text(app_status: &AppStatus, help_text_query: &mut Query<&mut Text, With<HelpText>>) {
650
for mut ui_text in help_text_query {
651
let mut help_text = String::new();
652
match app_status.camera_mode {
653
CameraMode::Orbit => {
654
help_text.push_str(
655
"Click and drag to orbit the camera\nUse the mouse wheel to zoom the camera\n",
656
);
657
}
658
CameraMode::Free => {
659
help_text.push_str(
660
"Click and drag to rotate the camera\nUse WASDEQ to move the camera\n",
661
);
662
}
663
}
664
665
help_text.push('\n');
666
667
if matches!(app_status.gizmos_enabled, GizmosEnabled::On) {
668
help_text.push_str(
669
"\
670
Gizmos:
671
Tan: Light probe bounds
672
Red: Light probe falloff bounds
673
Blue: Parallax correction bounds",
674
);
675
}
676
677
*ui_text = Text::new(help_text);
678
}
679
}
680
681
/// Returns the GitHub download URL for the given asset.
682
///
683
/// The files are expected to be in the `light_probe_blending` directory in the
684
/// [repository].
685
///
686
/// [repository]: https://github.com/bevyengine/bevy_asset_files
687
fn get_web_asset_url(name: &str) -> String {
688
format!(
689
"https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
690
light_probe_blending/{}",
691
name
692
)
693
}
694
695