lighting/
lighting.rs

1//! Illustrates different lights of various types and colors, some static, some moving over
2//! a simple scene.
3
4use std::f32::consts::PI;
5
6use bevy::{
7    camera::{Exposure, PhysicalCameraParameters},
8    color::palettes::css::*,
9    light::CascadeShadowConfigBuilder,
10    prelude::*,
11};
12
13fn main() {
14    App::new()
15        .add_plugins(DefaultPlugins)
16        .insert_resource(Parameters(PhysicalCameraParameters {
17            aperture_f_stops: 1.0,
18            shutter_speed_s: 1.0 / 125.0,
19            sensitivity_iso: 100.0,
20            sensor_height: 0.01866,
21        }))
22        .add_systems(Startup, setup)
23        .add_systems(
24            Update,
25            (
26                update_exposure,
27                toggle_ambient_light,
28                movement,
29                animate_light_direction,
30            ),
31        )
32        .run();
33}
34
35#[derive(Resource, Default, Deref, DerefMut)]
36struct Parameters(PhysicalCameraParameters);
37
38#[derive(Component)]
39struct Movable;
40
41/// set up a simple 3D scene
42fn setup(
43    parameters: Res<Parameters>,
44    mut commands: Commands,
45    mut meshes: ResMut<Assets<Mesh>>,
46    mut materials: ResMut<Assets<StandardMaterial>>,
47    asset_server: Res<AssetServer>,
48) {
49    // ground plane
50    commands.spawn((
51        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
52        MeshMaterial3d(materials.add(StandardMaterial {
53            base_color: Color::WHITE,
54            perceptual_roughness: 1.0,
55            ..default()
56        })),
57    ));
58
59    // left wall
60    let mut transform = Transform::from_xyz(2.5, 2.5, 0.0);
61    transform.rotate_z(PI / 2.);
62    commands.spawn((
63        Mesh3d(meshes.add(Cuboid::new(5.0, 0.15, 5.0))),
64        MeshMaterial3d(materials.add(StandardMaterial {
65            base_color: INDIGO.into(),
66            perceptual_roughness: 1.0,
67            ..default()
68        })),
69        transform,
70    ));
71    // back (right) wall
72    let mut transform = Transform::from_xyz(0.0, 2.5, -2.5);
73    transform.rotate_x(PI / 2.);
74    commands.spawn((
75        Mesh3d(meshes.add(Cuboid::new(5.0, 0.15, 5.0))),
76        MeshMaterial3d(materials.add(StandardMaterial {
77            base_color: INDIGO.into(),
78            perceptual_roughness: 1.0,
79            ..default()
80        })),
81        transform,
82    ));
83
84    // Bevy logo to demonstrate alpha mask shadows
85    let mut transform = Transform::from_xyz(-2.2, 0.5, 1.0);
86    transform.rotate_y(PI / 8.);
87    commands.spawn((
88        Mesh3d(meshes.add(Rectangle::new(2.0, 0.5))),
89        MeshMaterial3d(materials.add(StandardMaterial {
90            base_color_texture: Some(asset_server.load("branding/bevy_logo_light.png")),
91            perceptual_roughness: 1.0,
92            alpha_mode: AlphaMode::Mask(0.5),
93            cull_mode: None,
94            ..default()
95        })),
96        transform,
97        Movable,
98    ));
99
100    // cube
101    commands.spawn((
102        Mesh3d(meshes.add(Cuboid::default())),
103        MeshMaterial3d(materials.add(StandardMaterial {
104            base_color: DEEP_PINK.into(),
105            ..default()
106        })),
107        Transform::from_xyz(0.0, 0.5, 0.0),
108        Movable,
109    ));
110    // sphere
111    commands.spawn((
112        Mesh3d(meshes.add(Sphere::new(0.5).mesh().uv(32, 18))),
113        MeshMaterial3d(materials.add(StandardMaterial {
114            base_color: LIMEGREEN.into(),
115            ..default()
116        })),
117        Transform::from_xyz(1.5, 1.0, 1.5),
118        Movable,
119    ));
120
121    // ambient light
122    // ambient lights' brightnesses are measured in candela per meter square, calculable as (color * brightness)
123    commands.insert_resource(AmbientLight {
124        color: ORANGE_RED.into(),
125        brightness: 200.0,
126        ..default()
127    });
128
129    // red point light
130    commands.spawn((
131        PointLight {
132            intensity: 100_000.0,
133            color: RED.into(),
134            shadows_enabled: true,
135            ..default()
136        },
137        Transform::from_xyz(1.0, 2.0, 0.0),
138        children![(
139            Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))),
140            MeshMaterial3d(materials.add(StandardMaterial {
141                base_color: RED.into(),
142                emissive: LinearRgba::new(4.0, 0.0, 0.0, 0.0),
143                ..default()
144            })),
145        )],
146    ));
147
148    // green spot light
149    commands.spawn((
150        SpotLight {
151            intensity: 100_000.0,
152            color: LIME.into(),
153            shadows_enabled: true,
154            inner_angle: 0.6,
155            outer_angle: 0.8,
156            ..default()
157        },
158        Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z),
159        children![(
160            Mesh3d(meshes.add(Capsule3d::new(0.1, 0.125))),
161            MeshMaterial3d(materials.add(StandardMaterial {
162                base_color: LIME.into(),
163                emissive: LinearRgba::new(0.0, 4.0, 0.0, 0.0),
164                ..default()
165            })),
166            Transform::from_rotation(Quat::from_rotation_x(PI / 2.0)),
167        )],
168    ));
169
170    // blue point light
171    commands.spawn((
172        PointLight {
173            intensity: 100_000.0,
174            color: BLUE.into(),
175            shadows_enabled: true,
176            ..default()
177        },
178        Transform::from_xyz(0.0, 4.0, 0.0),
179        children![(
180            Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))),
181            MeshMaterial3d(materials.add(StandardMaterial {
182                base_color: BLUE.into(),
183                emissive: LinearRgba::new(0.0, 0.0, 713.0, 0.0),
184                ..default()
185            })),
186        )],
187    ));
188
189    // directional 'sun' light
190    commands.spawn((
191        DirectionalLight {
192            illuminance: light_consts::lux::OVERCAST_DAY,
193            shadows_enabled: true,
194            ..default()
195        },
196        Transform {
197            translation: Vec3::new(0.0, 2.0, 0.0),
198            rotation: Quat::from_rotation_x(-PI / 4.),
199            ..default()
200        },
201        // The default cascade config is designed to handle large scenes.
202        // As this example has a much smaller world, we can tighten the shadow
203        // bounds for better visual quality.
204        CascadeShadowConfigBuilder {
205            first_cascade_far_bound: 4.0,
206            maximum_distance: 10.0,
207            ..default()
208        }
209        .build(),
210    ));
211
212    // example instructions
213
214    commands.spawn((
215        Text::default(),
216        Node {
217            position_type: PositionType::Absolute,
218            top: px(12),
219            left: px(12),
220            ..default()
221        },
222        children![
223            TextSpan::new("Ambient light is on\n"),
224            TextSpan(format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops,)),
225            TextSpan(format!(
226                "Shutter speed: 1/{:.0}s\n",
227                1.0 / parameters.shutter_speed_s
228            )),
229            TextSpan(format!(
230                "Sensitivity: ISO {:.0}\n",
231                parameters.sensitivity_iso
232            )),
233            TextSpan::new("\n\n"),
234            TextSpan::new("Controls\n"),
235            TextSpan::new("---------------\n"),
236            TextSpan::new("Arrow keys - Move objects\n"),
237            TextSpan::new("Space - Toggle ambient light\n"),
238            TextSpan::new("1/2 - Decrease/Increase aperture\n"),
239            TextSpan::new("3/4 - Decrease/Increase shutter speed\n"),
240            TextSpan::new("5/6 - Decrease/Increase sensitivity\n"),
241            TextSpan::new("R - Reset exposure"),
242        ],
243    ));
244
245    // camera
246    commands.spawn((
247        Camera3d::default(),
248        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
249        Exposure::from_physical_camera(**parameters),
250    ));
251}
252
253fn update_exposure(
254    key_input: Res<ButtonInput<KeyCode>>,
255    mut parameters: ResMut<Parameters>,
256    mut exposure: Single<&mut Exposure>,
257    text: Single<Entity, With<Text>>,
258    mut writer: TextUiWriter,
259) {
260    // TODO: Clamp values to a reasonable range
261    let entity = *text;
262    if key_input.just_pressed(KeyCode::Digit2) {
263        parameters.aperture_f_stops *= 2.0;
264    } else if key_input.just_pressed(KeyCode::Digit1) {
265        parameters.aperture_f_stops *= 0.5;
266    }
267    if key_input.just_pressed(KeyCode::Digit4) {
268        parameters.shutter_speed_s *= 2.0;
269    } else if key_input.just_pressed(KeyCode::Digit3) {
270        parameters.shutter_speed_s *= 0.5;
271    }
272    if key_input.just_pressed(KeyCode::Digit6) {
273        parameters.sensitivity_iso += 100.0;
274    } else if key_input.just_pressed(KeyCode::Digit5) {
275        parameters.sensitivity_iso -= 100.0;
276    }
277    if key_input.just_pressed(KeyCode::KeyR) {
278        *parameters = Parameters::default();
279    }
280
281    *writer.text(entity, 2) = format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops);
282    *writer.text(entity, 3) = format!(
283        "Shutter speed: 1/{:.0}s\n",
284        1.0 / parameters.shutter_speed_s
285    );
286    *writer.text(entity, 4) = format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso);
287
288    **exposure = Exposure::from_physical_camera(**parameters);
289}
290
291fn toggle_ambient_light(
292    key_input: Res<ButtonInput<KeyCode>>,
293    mut ambient_light: ResMut<AmbientLight>,
294    text: Single<Entity, With<Text>>,
295    mut writer: TextUiWriter,
296) {
297    if key_input.just_pressed(KeyCode::Space) {
298        if ambient_light.brightness > 1. {
299            ambient_light.brightness = 0.;
300        } else {
301            ambient_light.brightness = 200.;
302        }
303
304        let entity = *text;
305        let ambient_light_state_text: &str = match ambient_light.brightness {
306            0. => "off",
307            _ => "on",
308        };
309        *writer.text(entity, 1) = format!("Ambient light is {ambient_light_state_text}\n");
310    }
311}
312
313fn animate_light_direction(
314    time: Res<Time>,
315    mut query: Query<&mut Transform, With<DirectionalLight>>,
316) {
317    for mut transform in &mut query {
318        transform.rotate_y(time.delta_secs() * 0.5);
319    }
320}
321
322fn movement(
323    input: Res<ButtonInput<KeyCode>>,
324    time: Res<Time>,
325    mut query: Query<&mut Transform, With<Movable>>,
326) {
327    for mut transform in &mut query {
328        let mut direction = Vec3::ZERO;
329        if input.pressed(KeyCode::ArrowUp) {
330            direction.y += 1.0;
331        }
332        if input.pressed(KeyCode::ArrowDown) {
333            direction.y -= 1.0;
334        }
335        if input.pressed(KeyCode::ArrowLeft) {
336            direction.x -= 1.0;
337        }
338        if input.pressed(KeyCode::ArrowRight) {
339            direction.x += 1.0;
340        }
341
342        transform.translation += time.delta_secs() * 2.0 * direction;
343    }
344}