depth_of_field/
depth_of_field.rs

1//! Demonstrates depth of field (DOF).
2//!
3//! The depth of field effect simulates the blur that a real camera produces on
4//! objects that are out of focus.
5//!
6//! The test scene is inspired by [a blog post on depth of field in Unity].
7//! However, the technique used in Bevy has little to do with that blog post,
8//! and all the assets are original.
9//!
10//! [a blog post on depth of field in Unity]: https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/
11
12use bevy::{
13    camera::PhysicalCameraParameters,
14    core_pipeline::tonemapping::Tonemapping,
15    gltf::GltfMeshName,
16    pbr::Lightmap,
17    post_process::{
18        bloom::Bloom,
19        dof::{self, DepthOfField, DepthOfFieldMode},
20    },
21    prelude::*,
22};
23
24/// The increments in which the user can adjust the focal distance, in meters
25/// per frame.
26const FOCAL_DISTANCE_SPEED: f32 = 0.05;
27/// The increments in which the user can adjust the f-number, in units per frame.
28const APERTURE_F_STOP_SPEED: f32 = 0.01;
29
30/// The minimum distance that we allow the user to focus on.
31const MIN_FOCAL_DISTANCE: f32 = 0.01;
32/// The minimum f-number that we allow the user to set.
33const MIN_APERTURE_F_STOPS: f32 = 0.05;
34
35/// A resource that stores the settings that the user can change.
36#[derive(Clone, Copy, Resource)]
37struct AppSettings {
38    /// The distance from the camera to the area in the most focus.
39    focal_distance: f32,
40
41    /// The [f-number]. Lower numbers cause objects outside the focal distance
42    /// to be blurred more.
43    ///
44    /// [f-number]: https://en.wikipedia.org/wiki/F-number
45    aperture_f_stops: f32,
46
47    /// Whether depth of field is on, and, if so, whether we're in Gaussian or
48    /// bokeh mode.
49    mode: Option<DepthOfFieldMode>,
50}
51
52fn main() {
53    App::new()
54        .init_resource::<AppSettings>()
55        .add_plugins(DefaultPlugins.set(WindowPlugin {
56            primary_window: Some(Window {
57                title: "Bevy Depth of Field Example".to_string(),
58                ..default()
59            }),
60            ..default()
61        }))
62        .add_systems(Startup, setup)
63        .add_systems(Update, tweak_scene)
64        .add_systems(
65            Update,
66            (adjust_focus, change_mode, update_dof_settings, update_text).chain(),
67        )
68        .run();
69}
70
71fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
72    // Spawn the camera. Enable HDR and bloom, as that highlights the depth of
73    // field effect.
74    let mut camera = commands.spawn((
75        Camera3d::default(),
76        Transform::from_xyz(0.0, 4.5, 8.25).looking_at(Vec3::ZERO, Vec3::Y),
77        Tonemapping::TonyMcMapface,
78        Bloom::NATURAL,
79    ));
80
81    // Insert the depth of field settings.
82    if let Some(depth_of_field) = Option::<DepthOfField>::from(*app_settings) {
83        camera.insert(depth_of_field);
84    }
85
86    // Spawn the scene.
87    commands.spawn(SceneRoot(asset_server.load(
88        GltfAssetLabel::Scene(0).from_asset("models/DepthOfFieldExample/DepthOfFieldExample.glb"),
89    )));
90
91    // Spawn the help text.
92    commands.spawn((
93        create_text(&app_settings),
94        Node {
95            position_type: PositionType::Absolute,
96            bottom: px(12),
97            left: px(12),
98            ..default()
99        },
100    ));
101}
102
103/// Adjusts the focal distance and f-number per user inputs.
104fn adjust_focus(input: Res<ButtonInput<KeyCode>>, mut app_settings: ResMut<AppSettings>) {
105    // Change the focal distance if the user requested.
106    let distance_delta = if input.pressed(KeyCode::ArrowDown) {
107        -FOCAL_DISTANCE_SPEED
108    } else if input.pressed(KeyCode::ArrowUp) {
109        FOCAL_DISTANCE_SPEED
110    } else {
111        0.0
112    };
113
114    // Change the f-number if the user requested.
115    let f_stop_delta = if input.pressed(KeyCode::ArrowLeft) {
116        -APERTURE_F_STOP_SPEED
117    } else if input.pressed(KeyCode::ArrowRight) {
118        APERTURE_F_STOP_SPEED
119    } else {
120        0.0
121    };
122
123    app_settings.focal_distance =
124        (app_settings.focal_distance + distance_delta).max(MIN_FOCAL_DISTANCE);
125    app_settings.aperture_f_stops =
126        (app_settings.aperture_f_stops + f_stop_delta).max(MIN_APERTURE_F_STOPS);
127}
128
129/// Changes the depth of field mode (Gaussian, bokeh, off) per user inputs.
130fn change_mode(input: Res<ButtonInput<KeyCode>>, mut app_settings: ResMut<AppSettings>) {
131    if !input.just_pressed(KeyCode::Space) {
132        return;
133    }
134
135    app_settings.mode = match app_settings.mode {
136        Some(DepthOfFieldMode::Bokeh) => Some(DepthOfFieldMode::Gaussian),
137        Some(DepthOfFieldMode::Gaussian) => None,
138        None => Some(DepthOfFieldMode::Bokeh),
139    }
140}
141
142impl Default for AppSettings {
143    fn default() -> Self {
144        Self {
145            // Objects 7 meters away will be in full focus.
146            focal_distance: 7.0,
147
148            // Set a nice blur level.
149            //
150            // This is a really low F-number, but we want to demonstrate the
151            // effect, even if it's kind of unrealistic.
152            aperture_f_stops: 1.0 / 8.0,
153
154            // Turn on bokeh by default, as it's the nicest-looking technique.
155            mode: Some(DepthOfFieldMode::Bokeh),
156        }
157    }
158}
159
160/// Writes the depth of field settings into the camera.
161fn update_dof_settings(
162    mut commands: Commands,
163    view_targets: Query<Entity, With<Camera>>,
164    app_settings: Res<AppSettings>,
165) {
166    let depth_of_field: Option<DepthOfField> = (*app_settings).into();
167    for view in view_targets.iter() {
168        match depth_of_field {
169            None => {
170                commands.entity(view).remove::<DepthOfField>();
171            }
172            Some(depth_of_field) => {
173                commands.entity(view).insert(depth_of_field);
174            }
175        }
176    }
177}
178
179/// Makes one-time adjustments to the scene that can't be encoded in glTF.
180fn tweak_scene(
181    mut commands: Commands,
182    asset_server: Res<AssetServer>,
183    mut materials: ResMut<Assets<StandardMaterial>>,
184    mut lights: Query<&mut DirectionalLight, Changed<DirectionalLight>>,
185    mut named_entities: Query<
186        (Entity, &GltfMeshName, &MeshMaterial3d<StandardMaterial>),
187        (With<Mesh3d>, Without<Lightmap>),
188    >,
189) {
190    // Turn on shadows.
191    for mut light in lights.iter_mut() {
192        light.shadows_enabled = true;
193    }
194
195    // Add a nice lightmap to the circuit board.
196    for (entity, name, material) in named_entities.iter_mut() {
197        if &**name == "CircuitBoard" {
198            materials.get_mut(material).unwrap().lightmap_exposure = 10000.0;
199            commands.entity(entity).insert(Lightmap {
200                image: asset_server.load("models/DepthOfFieldExample/CircuitBoardLightmap.hdr"),
201                ..default()
202            });
203        }
204    }
205}
206
207/// Update the help text entity per the current app settings.
208fn update_text(mut texts: Query<&mut Text>, app_settings: Res<AppSettings>) {
209    for mut text in texts.iter_mut() {
210        *text = create_text(&app_settings);
211    }
212}
213
214/// Regenerates the app text component per the current app settings.
215fn create_text(app_settings: &AppSettings) -> Text {
216    app_settings.help_text().into()
217}
218
219impl From<AppSettings> for Option<DepthOfField> {
220    fn from(app_settings: AppSettings) -> Self {
221        app_settings.mode.map(|mode| DepthOfField {
222            mode,
223            focal_distance: app_settings.focal_distance,
224            aperture_f_stops: app_settings.aperture_f_stops,
225            max_depth: 14.0,
226            ..default()
227        })
228    }
229}
230
231impl AppSettings {
232    /// Builds the help text.
233    fn help_text(&self) -> String {
234        let Some(mode) = self.mode else {
235            return "Mode: Off (Press Space to change)".to_owned();
236        };
237
238        // We leave these as their defaults, so we don't need to store them in
239        // the app settings and can just fetch them from the default camera
240        // parameters.
241        let sensor_height = PhysicalCameraParameters::default().sensor_height;
242        let fov = PerspectiveProjection::default().fov;
243
244        format!(
245            "Focal distance: {:.2} m (Press Up/Down to change)
246Aperture F-stops: f/{:.2} (Press Left/Right to change)
247Sensor height: {:.2}mm
248Focal length: {:.2}mm
249Mode: {} (Press Space to change)",
250            self.focal_distance,
251            self.aperture_f_stops,
252            sensor_height * 1000.0,
253            dof::calculate_focal_length(sensor_height, fov) * 1000.0,
254            match mode {
255                DepthOfFieldMode::Bokeh => "Bokeh",
256                DepthOfFieldMode::Gaussian => "Gaussian",
257            }
258        )
259    }
260}