use bevy::{
camera::PhysicalCameraParameters,
core_pipeline::tonemapping::Tonemapping,
gltf::GltfMeshName,
pbr::Lightmap,
post_process::{
bloom::Bloom,
dof::{self, DepthOfField, DepthOfFieldMode},
},
prelude::*,
};
const FOCAL_DISTANCE_SPEED: f32 = 0.05;
const APERTURE_F_STOP_SPEED: f32 = 0.01;
const MIN_FOCAL_DISTANCE: f32 = 0.01;
const MIN_APERTURE_F_STOPS: f32 = 0.05;
#[derive(Clone, Copy, Resource)]
struct AppSettings {
focal_distance: f32,
aperture_f_stops: f32,
mode: Option<DepthOfFieldMode>,
}
fn main() {
App::new()
.init_resource::<AppSettings>()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Depth of Field Example".to_string(),
..default()
}),
..default()
}))
.add_systems(Startup, setup)
.add_systems(Update, tweak_scene)
.add_systems(
Update,
(adjust_focus, change_mode, update_dof_settings, update_text).chain(),
)
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
let mut camera = commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 4.5, 8.25).looking_at(Vec3::ZERO, Vec3::Y),
Tonemapping::TonyMcMapface,
Bloom::NATURAL,
));
if let Some(depth_of_field) = Option::<DepthOfField>::from(*app_settings) {
camera.insert(depth_of_field);
}
commands.spawn(SceneRoot(asset_server.load(
GltfAssetLabel::Scene(0).from_asset("models/DepthOfFieldExample/DepthOfFieldExample.glb"),
)));
commands.spawn((
create_text(&app_settings),
Node {
position_type: PositionType::Absolute,
bottom: px(12),
left: px(12),
..default()
},
));
}
fn adjust_focus(input: Res<ButtonInput<KeyCode>>, mut app_settings: ResMut<AppSettings>) {
let distance_delta = if input.pressed(KeyCode::ArrowDown) {
-FOCAL_DISTANCE_SPEED
} else if input.pressed(KeyCode::ArrowUp) {
FOCAL_DISTANCE_SPEED
} else {
0.0
};
let f_stop_delta = if input.pressed(KeyCode::ArrowLeft) {
-APERTURE_F_STOP_SPEED
} else if input.pressed(KeyCode::ArrowRight) {
APERTURE_F_STOP_SPEED
} else {
0.0
};
app_settings.focal_distance =
(app_settings.focal_distance + distance_delta).max(MIN_FOCAL_DISTANCE);
app_settings.aperture_f_stops =
(app_settings.aperture_f_stops + f_stop_delta).max(MIN_APERTURE_F_STOPS);
}
fn change_mode(input: Res<ButtonInput<KeyCode>>, mut app_settings: ResMut<AppSettings>) {
if !input.just_pressed(KeyCode::Space) {
return;
}
app_settings.mode = match app_settings.mode {
Some(DepthOfFieldMode::Bokeh) => Some(DepthOfFieldMode::Gaussian),
Some(DepthOfFieldMode::Gaussian) => None,
None => Some(DepthOfFieldMode::Bokeh),
}
}
impl Default for AppSettings {
fn default() -> Self {
Self {
focal_distance: 7.0,
aperture_f_stops: 1.0 / 8.0,
mode: Some(DepthOfFieldMode::Bokeh),
}
}
}
fn update_dof_settings(
mut commands: Commands,
view_targets: Query<Entity, With<Camera>>,
app_settings: Res<AppSettings>,
) {
let depth_of_field: Option<DepthOfField> = (*app_settings).into();
for view in view_targets.iter() {
match depth_of_field {
None => {
commands.entity(view).remove::<DepthOfField>();
}
Some(depth_of_field) => {
commands.entity(view).insert(depth_of_field);
}
}
}
}
fn tweak_scene(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut lights: Query<&mut DirectionalLight, Changed<DirectionalLight>>,
mut named_entities: Query<
(Entity, &GltfMeshName, &MeshMaterial3d<StandardMaterial>),
(With<Mesh3d>, Without<Lightmap>),
>,
) {
for mut light in lights.iter_mut() {
light.shadows_enabled = true;
}
for (entity, name, material) in named_entities.iter_mut() {
if &**name == "CircuitBoard" {
materials.get_mut(material).unwrap().lightmap_exposure = 10000.0;
commands.entity(entity).insert(Lightmap {
image: asset_server.load("models/DepthOfFieldExample/CircuitBoardLightmap.hdr"),
..default()
});
}
}
}
fn update_text(mut texts: Query<&mut Text>, app_settings: Res<AppSettings>) {
for mut text in texts.iter_mut() {
*text = create_text(&app_settings);
}
}
fn create_text(app_settings: &AppSettings) -> Text {
app_settings.help_text().into()
}
impl From<AppSettings> for Option<DepthOfField> {
fn from(app_settings: AppSettings) -> Self {
app_settings.mode.map(|mode| DepthOfField {
mode,
focal_distance: app_settings.focal_distance,
aperture_f_stops: app_settings.aperture_f_stops,
max_depth: 14.0,
..default()
})
}
}
impl AppSettings {
fn help_text(&self) -> String {
let Some(mode) = self.mode else {
return "Mode: Off (Press Space to change)".to_owned();
};
let sensor_height = PhysicalCameraParameters::default().sensor_height;
let fov = PerspectiveProjection::default().fov;
format!(
"Focal distance: {:.2} m (Press Up/Down to change)
Aperture F-stops: f/{:.2} (Press Left/Right to change)
Sensor height: {:.2}mm
Focal length: {:.2}mm
Mode: {} (Press Space to change)",
self.focal_distance,
self.aperture_f_stops,
sensor_height * 1000.0,
dof::calculate_focal_length(sensor_height, fov) * 1000.0,
match mode {
DepthOfFieldMode::Bokeh => "Bokeh",
DepthOfFieldMode::Gaussian => "Gaussian",
}
)
}
}