docs/bevy/examples/3d/depth_of_field.rs 8.4 K · 260 lines · raw · history

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
12 use 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.
26 const FOCAL_DISTANCE_SPEED: f32 = 0.05;
27 /// The increments in which the user can adjust the f-number, in units per frame.
28 const APERTURE_F_STOP_SPEED: f32 = 0.01;
29
30 /// The minimum distance that we allow the user to focus on.
31 const MIN_FOCAL_DISTANCE: f32 = 0.01;
32 /// The minimum f-number that we allow the user to set.
33 const 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)]
37 struct 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
52 fn 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
71 fn 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(WorldAssetRoot(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.
104 fn 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.
130 fn 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
142 impl 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.
161 fn 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.
180 fn 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.shadow_maps_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.
208 fn 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.
215 fn create_text(app_settings: &AppSettings) -> Text {
216 app_settings.help_text().into()
217 }
218
219 impl 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
231 impl 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)
246 Aperture F-stops: f/{:.3} (Press Left/Right to change)
247 Sensor height: {:.2}mm
248 Focal length: {:.2}mm
249 Mode: {} (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 }